zc2 0.0.26

P2P compute broker with credit-based billing, WAL, and broker mesh support
name: Deploy

# Trunk-based staging deploy. Fires when the Build workflow completes
# successfully on the trunk (master/main) or a `release/**` branch, so
# every green push to the trunk ships to staging automatically — staging
# always reflects the head of master (the yoii-mcs model). release/**
# also deploys to staging as a release candidate; production ships from
# the v* tag via release.yml. Gating on Build success keeps a red build
# from deploying, without adding a test step to this fast path.
#
# What "deploy" means here:
#   1. Build this commit's broker image and push it to the in-LAN registry
#      as zakuro-broker:<sha7>.
#   2. Commit that tag into zakuro-infra's
#      k8s/zc-broker-fleet-staging/statefulset.yaml.
# ArgoCD app zc-broker-fleet-staging (automated, prune + selfHeal) syncs the
# change to the staging cluster. zakuro-infra's argocd-refresh workflow pokes
# ArgoCD on every push under k8s/**, so the rollout starts within seconds.
#
# This workflow used to POST a `deploy-staging` repository_dispatch to
# zakuro-infra and stop at the HTTP 204. Nothing there listens for that
# event (its only dispatch receiver is deploy-prod.yml). So every run went
# green while the fleet stayed on whatever tag someone last bumped by hand
# (zakuro-infra#114). Each step below now fails the job unless its effect
# is checked: the image is in the registry, and the bump commit is on
# zakuro-infra main.

on:
  workflow_run:
    workflows: ["Build"]
    types: [completed]
    branches:
      - master
      - main
      - 'release/**'
  workflow_dispatch:

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
  REGISTRY: registry.local.zakuro-ai.com
  # The image the staging statefulset runs, built from docker/Dockerfile.
  # Its runtime stage is the contract the fleet manifest relies on: zc at
  # /usr/local/bin/zc, user zc (uid 1001), and /bin/sh + sed for the
  # peer-list wrapper. Tags are the 7-char zc commit, the style the
  # hand-made bumps used (e.g. e2be662).
  IMAGE: registry.local.zakuro-ai.com/zakuroai/zakuro-broker
  FLEET_MANIFEST: k8s/zc-broker-fleet-staging/statefulset.yaml

permissions:
  contents: read

jobs:
  deploy-staging:
    if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
    runs-on: cpu
    # An uncached image build recompiles the whole dependency graph
    # (~15-20 min, see docker/Dockerfile). Reading docker.yml's GHA cache
    # usually cuts it to a few minutes.
    timeout-minutes: 45
    # The staging fleet is one shared environment behind one manifest line.
    # Deploy one commit at a time, in order, and never cancel a deploy that
    # has started.
    concurrency:
      group: zc-deploy-staging
      cancel-in-progress: false
    env:
      # In the workflow_run context, github.sha resolves to the default
      # branch. Pin to the SHA that triggered Build, so that the image and
      # the manifest bump point at the right commit.
      DEPLOY_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
      DEPLOY_REF: ${{ github.event.workflow_run.head_branch || github.ref_name }}
    steps:
      - name: Check secrets
        env:
          DEPLOY_PAT: ${{ secrets.DEPLOY_PAT }}
          DRIVE_DEPLOY_KEY: ${{ secrets.DRIVE_DEPLOY_KEY }}
        run: |
          set -euo pipefail
          # Fail loudly on a missing secret rather than deploying nothing. An
          # unset DEPLOY_PAT once made this job report successful deploys
          # that never happened.
          missing=0
          for name in DEPLOY_PAT DRIVE_DEPLOY_KEY; do
            if [ -z "${!name:-}" ]; then
              echo "::error::${name} is not set on zakuro-ai/zc — cannot deploy staging."
              missing=1
            fi
          done
          [ "$missing" = 0 ]
          echo "TAG=${DEPLOY_SHA::7}" >> "$GITHUB_ENV"

      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # v7.0.0
        with:
          ref: ${{ env.DEPLOY_SHA }}
          persist-credentials: false

      # zakuro-client is a git dep on the private zakuro-ai/zakuro-drive
      # repo. This uses the same read-only deploy key and ssh-agent
      # forwarding as docker.yml, so the key never lands in an image layer.
      - name: Start ssh-agent with the drive deploy key
        env:
          DRIVE_DEPLOY_KEY: ${{ secrets.DRIVE_DEPLOY_KEY }}
        run: |
          eval "$(ssh-agent -s)"
          echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK" >> "$GITHUB_ENV"
          echo "SSH_AGENT_PID=$SSH_AGENT_PID" >> "$GITHUB_ENV"
          printf '%s\n' "$DRIVE_DEPLOY_KEY" | ssh-add -

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5  # v4

      - name: Build and push the broker image
        uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf  # v7
        with:
          context: .
          file: docker/Dockerfile
          # The staging node is amd64.
          platforms: linux/amd64
          ssh: default=${{ env.SSH_AUTH_SOCK }}
          tags: ${{ env.IMAGE }}:${{ env.TAG }}
          push: true
          # Read docker.yml's amd64 layer cache, which the image build writes
          # on every trunk push, so unchanged dependencies are not recompiled.
          cache-from: type=gha,scope=linux/amd64

      - name: Verify the image is in the registry
        run: |
          set -euo pipefail
          code=$(curl -sS -o /dev/null -w '%{http_code}' \
            -H 'Accept: application/vnd.oci.image.index.v1+json' \
            -H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
            -H 'Accept: application/vnd.oci.image.manifest.v1+json' \
            -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
            "https://${REGISTRY}/v2/zakuroai/zakuro-broker/manifests/${TAG}")
          if [ "$code" != "200" ]; then
            echo "::error::${IMAGE}:${TAG} is not in the registry (HTTP $code) — staging was NOT deployed."
            exit 1
          fi
          echo "${IMAGE}:${TAG} is in the registry."

      - name: Promote the image in zakuro-infra
        env:
          DEPLOY_PAT: ${{ secrets.DEPLOY_PAT }}
        run: |
          set -euo pipefail
          INFRA_DIR="$(mktemp -d)/zakuro-infra"
          git clone --depth=1 \
            "https://x-access-token:${DEPLOY_PAT}@github.com/zakuro-ai/zakuro-infra.git" \
            "$INFRA_DIR"
          cd "$INFRA_DIR"

          # Require exactly one broker image line. Anything else means the
          # manifest changed shape, and a blind sed would either do nothing
          # or rewrite the wrong container.
          OLD=$(sed -n -E "s|^[[:space:]]*image: ${IMAGE}:([^[:space:]]+).*|\1|p" "$FLEET_MANIFEST")
          if [ "$(printf '%s\n' "$OLD" | grep -c .)" != "1" ]; then
            echo "::error::expected exactly one '${IMAGE}:<tag>' line in ${FLEET_MANIFEST}, found: '${OLD}'"
            exit 1
          fi
          if [ "$OLD" = "$TAG" ]; then
            echo "The fleet is already on ${TAG}; nothing to promote."
            exit 0
          fi
          sed -i -E "s|^([[:space:]]*image: ${IMAGE}:)[^[:space:]]+|\1${TAG}|" "$FLEET_MANIFEST"
          grep -n "image: ${IMAGE}:" "$FLEET_MANIFEST"

          git config user.name  "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add "$FLEET_MANIFEST"
          git commit \
            -m "deploy(staging): broker image ${OLD} -> ${TAG}" \
            -m "Source: zakuro-ai/zc@${TAG} on ${DEPLOY_REF}. ArgoCD app zc-broker-fleet-staging auto-syncs."

          pushed=false
          for attempt in 1 2 3 4 5; do
            if git push origin HEAD:main; then pushed=true; break; fi
            echo "push rejected, rebasing (attempt $attempt)"
            git pull --rebase origin main
            sleep 3
          done
          if [ "$pushed" != true ]; then
            echo "::error::could not push the tag bump to zakuro-infra — staging was NOT deployed."
            exit 1
          fi

          # Check the effect, not the exit code: the bump must be on main.
          git fetch -q origin main
          if ! git merge-base --is-ancestor HEAD origin/main; then
            echo "::error::the bump commit $(git rev-parse --short HEAD) is not on zakuro-infra main — staging was NOT deployed."
            exit 1
          fi
          echo "Promoted ${IMAGE}:${TAG} in zakuro-infra ($(git rev-parse --short HEAD)); ArgoCD app zc-broker-fleet-staging syncs it."