ferrokinesis 0.7.0

A local AWS Kinesis mock server for testing, written in Rust
Documentation
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 }}