Skip to content

RPC pool: park only a node that is not answering #394

RPC pool: park only a node that is not answering

RPC pool: park only a node that is not answering #394

Workflow file for this run

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