dynoxide-rs 0.11.1

A lightweight, embeddable DynamoDB emulator backed by SQLite
Documentation
name: Publish Docker image (recovery)

# Manual recovery workflow. The primary publish path is release.yml's
# publish-docker job. Use this only to re-run the Docker publish for a
# specific tag, e.g. when GHCR auth was momentarily wrong or when a
# Sigstore outage skipped the provenance attestation.
#
# Diverges from release.yml's inline job in two ways:
#   - tags are computed from the workflow_dispatch input rather than
#     github.ref, so metadata-action receives an explicit value=
#   - ECR Public is intentionally not mirrored from this workflow. The
#     OIDC trust policy is scoped to tag refs, not branch refs, so a
#     dispatch from main cannot assume the ECR Public role. Republish
#     ECR Public via release.yml on a follow-up patch tag instead.

on:
  workflow_dispatch:
    inputs:
      tag:
        description: "Release tag (e.g. v0.9.10)"
        required: true
        type: string

jobs:
  publish-docker:
    name: Publish Docker image
    if: github.repository == 'nubo-db/dynoxide'
    runs-on: ubuntu-latest
    timeout-minutes: 20

    permissions:
      contents: read
      packages: write
      id-token: write
      attestations: write

    steps:
      - uses: actions/checkout@v4

      - name: Validate and resolve tag
        id: tag
        env:
          INPUT_TAG: ${{ inputs.tag }}
        run: |
          set -euo pipefail
          if ! [[ "$INPUT_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
            echo "::error::Invalid tag format: $INPUT_TAG (expected vX.Y.Z or vX.Y.Z-pre.N)"
            exit 1
          fi
          echo "tag=$INPUT_TAG" >> "$GITHUB_OUTPUT"
          echo "version=${INPUT_TAG#v}" >> "$GITHUB_OUTPUT"

      - name: Stage prebuilt linux-musl binaries
        env:
          GH_TOKEN: ${{ github.token }}
          TAG: ${{ steps.tag.outputs.tag }}
        run: |
          set -euo pipefail
          rm -rf dist staging
          mkdir -p dist/amd64 dist/arm64 staging
          gh release download "$TAG" \
            --repo nubo-db/dynoxide \
            --pattern '*linux-musl.tar.gz' \
            --dir staging/
          tar -xzf staging/dynoxide-x86_64-unknown-linux-musl.tar.gz -C dist/amd64/
          tar -xzf staging/dynoxide-aarch64-unknown-linux-musl.tar.gz -C dist/arm64/
          chmod +x dist/amd64/dynoxide dist/arm64/dynoxide
          file dist/amd64/dynoxide
          file dist/arm64/dynoxide
          file dist/amd64/dynoxide | grep -q 'x86-64'
          file dist/arm64/dynoxide | grep -q 'aarch64'

      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        id: ghcr-login
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Login to Docker Hub
        id: dockerhub-login
        continue-on-error: true
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Compute image tags and labels
        id: meta
        uses: docker/metadata-action@v5
        env:
          INPUT_TAG: ${{ inputs.tag }}
        with:
          images: ghcr.io/${{ github.repository }}
          flavor: |
            latest=auto
          tags: |
            type=semver,pattern={{version}},value=${{ inputs.tag }}
            type=semver,pattern={{major}}.{{minor}},value=${{ inputs.tag }}
            type=semver,pattern={{major}},value=${{ inputs.tag }},enable=${{ !startsWith(inputs.tag, 'v0.') }}

      - name: Build and push to GHCR
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          provenance: mode=max
          sbom: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

      - name: Verify published image runs on both architectures
        env:
          IMAGE: ghcr.io/${{ github.repository }}:${{ steps.tag.outputs.version }}
          EXPECTED: ${{ steps.tag.outputs.version }}
        run: |
          set -euo pipefail
          for arch in linux/amd64 linux/arm64; do
            actual=$(docker run --rm --pull=always --platform "$arch" "$IMAGE" --version | awk '{print $NF}')
            if [ "$actual" != "$EXPECTED" ]; then
              echo "::error::$arch reported version $actual, expected $EXPECTED"
              exit 1
            fi
          done

      - name: Attest build provenance
        id: attest
        continue-on-error: true
        uses: actions/attest-build-provenance@v2
        with:
          subject-name: ghcr.io/${{ github.repository }}
          subject-digest: ${{ steps.build.outputs.digest }}
          push-to-registry: true

      - name: Mirror to Docker Hub
        id: mirror-dockerhub
        if: ${{ !cancelled() && steps.dockerhub-login.outcome == 'success' }}
        continue-on-error: true
        env:
          META_JSON: ${{ steps.meta.outputs.json }}
        run: |
          set -euo pipefail
          echo "$META_JSON" | jq -r '.tags[]' | while read -r src_tag; do
            tag="${src_tag##*:}"
            docker buildx imagetools create \
              -t "docker.io/nubodb/dynoxide:${tag}" \
              "$src_tag"
          done

      - name: Publish summary
        if: ${{ !cancelled() }}
        env:
          META_JSON: ${{ steps.meta.outputs.json }}
          GHCR_OUTCOME: ${{ steps.build.outcome }}
          ATTEST_OUTCOME: ${{ steps.attest.outcome }}
          DOCKERHUB_OUTCOME: ${{ steps.mirror-dockerhub.outcome }}
        run: |
          {
            echo "## Docker recovery publish summary"
            echo
            echo "| Surface | Outcome |"
            echo "| --- | --- |"
            echo "| GHCR push | ${GHCR_OUTCOME} |"
            echo "| Provenance attestation | ${ATTEST_OUTCOME} |"
            echo "| Docker Hub mirror | ${DOCKERHUB_OUTCOME:-skipped} |"
            echo "| ECR Public mirror | not run from docker.yml |"
            echo
            echo "### Pushed tags"
            echo
            echo "$META_JSON" | jq -r '.tags[] | "- `\(.)`"'
          } >> "$GITHUB_STEP_SUMMARY"