fix(ai-image): forward the client's idempotency_key to the upstream #409
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI/CD | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| # Serialize runs on a ref and cancel superseded ones, so an older run can't roll prod | |
| # back to its (older) image after a newer run has already deployed. | |
| concurrency: | |
| group: vapi-cicd-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Least privilege: no job writes to the repo (images push with Docker Hub | |
| # credentials, deploys use SSH secrets). | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Gate every PR and every deploy on the .NET test suite (crypto golden vectors, | |
| # RPC failover, JS-semantics parity helpers). | |
| test: | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # PR-triggered job; don't leave the token in .git/config | |
| persist-credentials: false | |
| - uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Build | |
| run: dotnet build dotnet/EcencyApi/EcencyApi.csproj -c Release | |
| - name: Test | |
| run: dotnet test dotnet/EcencyApi.Tests/EcencyApi.Tests.csproj -c Release | |
| build: | |
| # Build/push only on merges to main; PRs stop at the test gate. | |
| if: github.event_name == 'push' | |
| needs: test | |
| runs-on: ubuntu-24.04 | |
| outputs: | |
| digest: ${{ steps.docker_build.outputs.digest }} | |
| env: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| steps: | |
| - | |
| name: Check Out Repo | |
| uses: actions/checkout@v4 | |
| - | |
| name: Login to Docker Hub | |
| uses: docker/login-action@v3 | |
| if: ${{ env.DOCKERHUB_USERNAME != 0 }} | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - | |
| # One-time, idempotent: before the first C# image overwrites :latest, | |
| # preserve the last Node build under a durable tag. Rollback to the | |
| # pre-rewrite service is then always `ecency/api:node-legacy`. | |
| name: Preserve last Node image as rollback tag | |
| if: ${{ env.DOCKERHUB_USERNAME != 0 }} | |
| run: | | |
| if docker manifest inspect ecency/api:node-legacy >/dev/null 2>&1; then | |
| echo "node-legacy tag already exists; skipping" | |
| else | |
| docker buildx imagetools create --tag ecency/api:node-legacy ecency/api:latest | |
| echo "tagged current :latest as ecency/api:node-legacy" | |
| fi | |
| - | |
| # The deployed implementation is the C# service under dotnet/. Every build | |
| # is also tagged with the commit SHA, so any previous version can be | |
| # redeployed by tag (the deploy boxes prune local images, so rollback | |
| # comes from the registry): | |
| # docker service update --image ecency/api:sha-<commit> vision_vapi | |
| name: Build and push | |
| id: docker_build | |
| uses: docker/build-push-action@v5 | |
| if: ${{ env.DOCKERHUB_USERNAME != 0 }} | |
| with: | |
| context: ./dotnet | |
| file: ./dotnet/Dockerfile | |
| push: true | |
| tags: | | |
| ecency/api:latest | |
| ecency/api:sha-${{ github.sha }} | |
| - | |
| name: Image digest | |
| run: echo ${{ steps.docker_build.outputs.digest }} | |
| # Roll the freshly-pushed image out to the running `vision_vapi` swarm service in each | |
| # region, so a vision-api change (e.g. rotating Feature Spotlight content) ships on its | |
| # own instead of waiting for the next vision-web deploy. Mirrors the converge-wait that | |
| # vision-web uses for `vision_web`. Assumes the swarm service is named `vision_vapi` | |
| # (stack `vision`, service `vapi`) — verify once with `docker service ls`. | |
| deploy: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # ALPHA is the staging origin; it runs the same vision_vapi service and | |
| # tracks main so staging cannot drift behind production again. | |
| region: [EU, US, ALPHA] | |
| steps: | |
| - name: Deploy vision_vapi (${{ matrix.region }}) | |
| uses: appleboy/ssh-action@v1.0.3 | |
| env: | |
| IMAGE_DIGEST: ${{ needs.build.outputs.digest }} | |
| with: | |
| host: ${{ secrets[format('SSH_HOST_{0}', matrix.region)] }} | |
| username: ${{ secrets.SSH_USERNAME }} | |
| key: ${{ secrets.SSH_KEY }} | |
| port: ${{ secrets.SSH_PORT }} | |
| envs: IMAGE_DIGEST | |
| script: | | |
| # Deploy the exact image digest THIS run built — never the mutable :latest tag — | |
| # so out-of-order :latest writes from overlapping runs cannot move prod. | |
| if [ -z "$IMAGE_DIGEST" ]; then | |
| echo "::error::missing build digest; refusing to deploy a mutable tag" | |
| exit 1 | |
| fi | |
| IMAGE="ecency/api@${IMAGE_DIGEST}" | |
| if ! docker pull "$IMAGE"; then | |
| echo "::error::failed to pull $IMAGE" | |
| exit 1 | |
| fi | |
| # --detach=false blocks until the rollout reaches a terminal state, so the | |
| # UpdateStatus read afterwards is THIS rollout's, not a stale prior 'completed'. | |
| docker service update --image "$IMAGE" --with-registry-auth --detach=false vision_vapi | |
| upd_rc=$? | |
| state="$(docker service inspect vision_vapi --format '{{if .UpdateStatus}}{{.UpdateStatus.State}}{{end}}' 2>/dev/null || echo "")" | |
| case "$state" in | |
| rollback_*|paused) | |
| echo "::error::vision_vapi rolled back (UpdateStatus=$state); prod still on the previous image" | |
| exit 1 ;; | |
| esac | |
| if [ "$upd_rc" -ne 0 ] && [ "$state" != "completed" ]; then | |
| echo "::error::vision_vapi update failed (rc=$upd_rc, UpdateStatus=${state:-none})" | |
| exit 1 | |
| fi | |
| echo "vision_vapi on $IMAGE (UpdateStatus=${state:-none})" | |
| docker system prune -f |