name: Release
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: write
packages: write
jobs:
check:
name: Check
if: startsWith(github.event.release.tag_name, 'v') || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust-toolchain
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Format
run: cargo fmt --all -- --check
- name: Lint
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: Test
run: cargo test --workspace
build:
name: Build (${{ matrix.name }})
needs: check
if: startsWith(github.event.release.tag_name, 'v') || github.event_name == 'workflow_dispatch'
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
name: linux-amd64
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
name: linux-arm64
- target: x86_64-apple-darwin
os: macos-latest
name: macos-amd64
- target: aarch64-apple-darwin
os: macos-latest
name: macos-arm64
- target: x86_64-pc-windows-gnu
os: ubuntu-latest
name: windows-amd64
ext: .exe
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust-toolchain
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Cache pip packages
if: runner.os == 'Linux'
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-zigbuild-${{ runner.os }}
- name: Install cargo-zigbuild and zig
if: runner.os == 'Linux'
run: pip3 install cargo-zigbuild ziglang
- name: Build (zigbuild)
if: runner.os == 'Linux'
run: cargo zigbuild --release --target ${{ matrix.target }} -p ferrokinesis --bin ferrokinesis -p ferrokinesis-cli --bin ferro
- name: Build (native)
if: runner.os != 'Linux'
run: cargo build --release --target ${{ matrix.target }} -p ferrokinesis --bin ferrokinesis -p ferrokinesis-cli --bin ferro
- name: Rename binaries
run: |
cp target/${{ matrix.target }}/release/ferrokinesis${{ matrix.ext }} ferrokinesis-${{ matrix.name }}${{ matrix.ext }}
cp target/${{ matrix.target }}/release/ferro${{ matrix.ext }} ferro-${{ matrix.name }}${{ matrix.ext }}
- name: Upload ferrokinesis binary
uses: actions/upload-artifact@v4
with:
name: ferrokinesis-${{ matrix.name }}
path: ferrokinesis-${{ matrix.name }}${{ matrix.ext }}
- name: Upload ferro binary
uses: actions/upload-artifact@v4
with:
name: ferro-${{ matrix.name }}
path: ferro-${{ matrix.name }}${{ matrix.ext }}
upload-assets:
name: Upload Assets
needs: build
if: startsWith(github.event.release.tag_name, 'v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Generate checksums
run: |
cd artifacts
find . -type f -not -name SHA256SUMS | sort | xargs sha256sum | sed "s| .*/| |" > SHA256SUMS
- name: Upload release assets
run: |
TAG="${{ github.event.release.tag_name }}"
find artifacts -type f | sort | xargs \
gh release upload "$TAG" --clobber --repo "${{ github.repository }}"
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
docker:
name: Docker
needs: [build, upload-assets]
if: ${{ always() && (startsWith(github.event.release.tag_name, 'v') || github.event_name == 'workflow_dispatch') && needs.build.result == 'success' && (needs.upload-assets.result == 'success' || needs.upload-assets.result == 'skipped') }}
runs-on: ubuntu-latest
permissions:
packages: write
services:
registry:
image: registry:2
ports:
- 5000:5000
steps:
- uses: actions/checkout@v4
- name: Get version
id: version
run: |
if [[ "${{ github.event_name }}" == "release" ]]; then
VERSION="${{ github.event.release.tag_name }}"
echo "version=${VERSION#v}" >> $GITHUB_OUTPUT
else
echo "version=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')" >> $GITHUB_OUTPUT
fi
- name: Download linux-amd64 binary
uses: actions/download-artifact@v4
with:
name: ferrokinesis-linux-amd64
path: docker-context/amd64
- name: Download ferro linux-amd64 binary
uses: actions/download-artifact@v4
with:
name: ferro-linux-amd64
path: docker-context/amd64
- name: Download linux-arm64 binary
uses: actions/download-artifact@v4
with:
name: ferrokinesis-linux-arm64
path: docker-context/arm64
- name: Download ferro linux-arm64 binary
uses: actions/download-artifact@v4
with:
name: ferro-linux-arm64
path: docker-context/arm64
- name: Prepare binaries
run: |
mv docker-context/amd64/ferrokinesis-linux-amd64 docker-context/amd64/ferrokinesis
mv docker-context/amd64/ferro-linux-amd64 docker-context/amd64/ferro
mv docker-context/arm64/ferrokinesis-linux-arm64 docker-context/arm64/ferrokinesis
mv docker-context/arm64/ferro-linux-arm64 docker-context/arm64/ferro
chmod +x docker-context/*/ferrokinesis docker-context/*/ferro
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
buildkitd-config-inline: |
[registry."localhost:5000"]
http = true
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ steps.version.outputs.version }}
type=raw,value=v${{ steps.version.outputs.version }}
type=raw,value=latest
type=sha,prefix=sha-
- name: Build multi-arch image
uses: docker/build-push-action@v6
with:
context: docker-context
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: localhost:5000/ferrokinesis:${{ github.sha }}
labels: ${{ steps.meta.outputs.labels }}
- name: Smoke test
run: |
# Pull from local registry (gets native amd64 on the GHA runner)
docker pull localhost:5000/ferrokinesis:${{ github.sha }}
# Start the container
docker run -d --name ferrokinesis-smoke -p 4567:4567 localhost:5000/ferrokinesis:${{ github.sha }}
# Wait for the service to be ready
curl --retry 10 --retry-delay 1 --retry-connrefused --silent --fail \
http://localhost:4567/_health || true
# Fake AWS SigV4 auth headers (signature is not validated by the mock)
AUTH='AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20260101/us-east-1/kinesis/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-target, Signature=fakesignature'
# CreateStream
curl --silent --fail --show-error \
-X POST http://localhost:4567/ \
-H 'Content-Type: application/x-amz-json-1.1' \
-H 'X-Amz-Target: Kinesis_20131202.CreateStream' \
-H "Authorization: $AUTH" \
-H 'X-Amz-Date: 20260101T000000Z' \
-d '{"StreamName":"smoke-test","ShardCount":1}'
# Wait briefly for the stream to become available
sleep 1
# ListStreams – assert the response contains "smoke-test"
RESPONSE=$(curl --silent --fail --show-error \
-X POST http://localhost:4567/ \
-H 'Content-Type: application/x-amz-json-1.1' \
-H 'X-Amz-Target: Kinesis_20131202.ListStreams' \
-H "Authorization: $AUTH" \
-H 'X-Amz-Date: 20260101T000000Z' \
-d '{}')
echo "ListStreams response: $RESPONSE"
echo "$RESPONSE" | grep -q "smoke-test"
# Companion CLI is present in the image and can talk to the server
FERRO_OUTPUT=$(docker exec ferrokinesis-smoke /ferro --endpoint http://127.0.0.1:4567 streams list)
echo "ferro streams list output:"
echo "$FERRO_OUTPUT"
echo "$FERRO_OUTPUT" | grep -q "smoke-test"
# Cleanup
docker stop ferrokinesis-smoke && docker rm ferrokinesis-smoke
- name: Scan image for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: localhost:5000/ferrokinesis:${{ github.sha }}
format: table
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL,HIGH'
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push to GHCR
run: |
TAG_ARGS=""
while IFS= read -r tag; do
[ -n "$tag" ] && TAG_ARGS="$TAG_ARGS --tag $tag"
done <<< "$TAGS"
docker buildx imagetools create $TAG_ARGS localhost:5000/ferrokinesis:${{ github.sha }}
env:
TAGS: ${{ steps.meta.outputs.tags }}