From bbc2335ce4f2cb41f97bc03bedd2db9294b47637 Mon Sep 17 00:00:00 2001 From: aarroyo Date: Sun, 2 Aug 2026 16:59:27 -0500 Subject: [PATCH] =?UTF-8?q?feat(ci):=20guarded=20Coolify=20deploy=20for=20?= =?UTF-8?q?the=20Tracker=20=E2=80=94=20GT-435?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The publish half landed yesterday; this is the other half. Gated on vars.TRACKER_DEPLOY_ENABLED and running only from main, so it stays silent until somebody turns it on. DELIBERATE DIFFERENCE from the Core's equivalent job, which this otherwise mirrors. There, an unset hook warns and exits 0 per service. That is right for "not configured yet" and wrong for "half configured": set the token, typo one hook, and CD reports success while that service never deploys. Here nothing configured is a skip and PARTIALLY configured is a failure. A deploy everybody believes is wired and silently is not is the exact shape of the defects found all week. All three hooks are attempted before failing, so one bad hook does not hide the state of the other two - the same reason the build matrix does not fail fast. curl --fail is explicit, because without it curl exits 0 on a 4xx and a rejected hook would read as a successful deploy. And triggering is not deploying: Coolify works asynchronously, so a green job means the request was accepted, never that the new image is serving. The job writes that into its own step summary instead of letting the tick imply it, because GT-448 asks for a deploy evidenced by a recorded run against the live service and this job cannot provide that. Classification logic exercised in all four states: nothing set (skip), all set (proceed), and two shapes of partial (fail). --- .github/workflows/images.yml | 85 ++++++++++++++++++++++++++++++++++++ product/infra/helm/README.md | 24 ++++++++++ 2 files changed, 109 insertions(+) diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index ada7813..bda39f4 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -88,3 +88,88 @@ jobs: image="ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}:${{ github.sha }}" docker buildx imagetools inspect "$image" > /dev/null echo "verified pullable: $image" + + # ── Deploy ────────────────────────────────────────────────────────────────── + # + # Triggers the Coolify deployment of the freshly published images. Gated on + # `vars.TRACKER_DEPLOY_ENABLED` so it stays silent until somebody decides to turn it on, and it + # runs only from `main`. + # + # DELIBERATE DIFFERENCE FROM THE CORE'S EQUIVALENT JOB, which this otherwise mirrors: there, an + # unset hook warns and exits 0 for every service independently. That is right for "not configured + # yet" and wrong for "half configured" — set the token, typo one hook, and CD reports success + # while that service never deploys. Here the two states are separated: nothing configured is a + # skip, PARTIALLY configured is a FAILURE. A deploy everybody believes is wired and silently is + # not is the exact shape of the defects this repository has spent the week finding. + deploy: + name: Deploy to Coolify + needs: build-push + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' && vars.TRACKER_DEPLOY_ENABLED == 'true' + steps: + - name: Classify the deploy configuration + id: cfg + env: + TOKEN: ${{ secrets.COOLIFY_API_TOKEN }} + HOOK_API: ${{ secrets.COOLIFY_TRACKER_API_DEPLOY_HOOK }} + HOOK_GATEWAY: ${{ secrets.COOLIFY_TRACKER_GATEWAY_DEPLOY_HOOK }} + HOOK_WEB: ${{ secrets.COOLIFY_TRACKER_WEB_DEPLOY_HOOK }} + run: | + set=0; unset=0 + for v in "$TOKEN" "$HOOK_API" "$HOOK_GATEWAY" "$HOOK_WEB"; do + if [ -n "$v" ]; then set=$((set+1)); else unset=$((unset+1)); fi + done + + if [ "$set" -eq 0 ]; then + echo "::warning::Coolify is not configured (no token, no hooks). Skipping the deploy. Set COOLIFY_API_TOKEN and the three hooks to enable CD." + echo "state=absent" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "$unset" -gt 0 ]; then + echo "::error::Coolify is PARTIALLY configured: $set of 4 values present. A half-wired deploy reports success while some services never ship, so this fails instead of warning. Set all of COOLIFY_API_TOKEN, COOLIFY_TRACKER_API_DEPLOY_HOOK, COOLIFY_TRACKER_GATEWAY_DEPLOY_HOOK and COOLIFY_TRACKER_WEB_DEPLOY_HOOK, or unset them all." + exit 1 + fi + echo "state=complete" >> "$GITHUB_OUTPUT" + + - name: Trigger the deploys + if: steps.cfg.outputs.state == 'complete' + env: + TOKEN: ${{ secrets.COOLIFY_API_TOKEN }} + HOOK_API: ${{ secrets.COOLIFY_TRACKER_API_DEPLOY_HOOK }} + HOOK_GATEWAY: ${{ secrets.COOLIFY_TRACKER_GATEWAY_DEPLOY_HOOK }} + HOOK_WEB: ${{ secrets.COOLIFY_TRACKER_WEB_DEPLOY_HOOK }} + run: | + failed=0 + trigger() { + # `--fail` matters: without it curl exits 0 on a 4xx and the deploy would report + # success for a hook that was rejected. + if curl --fail --silent --show-error -X GET "$2" -H "Authorization: Bearer $TOKEN" -o /dev/null; then + echo "$1 deploy triggered" + else + echo "::error::$1 deploy hook did not accept the request" + failed=1 + fi + } + # All three are attempted before failing, so one bad hook does not hide the state of the + # other two — the same reason the build matrix does not fail fast. + trigger tracker-api "$HOOK_API" + trigger tracker-gateway "$HOOK_GATEWAY" + trigger tracker-web "$HOOK_WEB" + exit $failed + + # Triggering is not deploying. Coolify accepts the webhook and works asynchronously, so a + # green step here means "the request was accepted", never "the new image is serving". Said + # out loud because GT-448 requires a deploy evidenced by a recorded run rather than by a job + # exiting zero, and this job cannot provide that evidence. + - name: What this job did and did not prove + if: steps.cfg.outputs.state == 'complete' + run: | + { + echo '### Deploy triggered' + echo '' + echo 'Coolify accepted the webhook for all three services at `${{ github.sha }}`.' + echo '' + echo '> This proves the REQUEST was accepted. It does not prove the new image is' + echo '> serving traffic: Coolify deploys asynchronously. GT-448 asks for a recorded' + echo '> run against the live service, which has to be taken there, not here.' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/product/infra/helm/README.md b/product/infra/helm/README.md index e4b6485..6193e83 100644 --- a/product/infra/helm/README.md +++ b/product/infra/helm/README.md @@ -122,6 +122,30 @@ actually been exercised. `check-deployable-images.mjs` runs in CI and fails when a chart names an image no workflow publishes, a local-only tag, or a semver with no git tag behind it. +### Turning the deploy on + +`images.yml` also carries a `deploy` job. It is gated on the repository variable +`TRACKER_DEPLOY_ENABLED` and runs only from `main`, so it stays silent until somebody decides to +turn it on. + +| what | where | +|---|---| +| `TRACKER_DEPLOY_ENABLED=true` | repository **variable** | +| `COOLIFY_API_TOKEN` | repository **secret** | +| `COOLIFY_TRACKER_API_DEPLOY_HOOK` | repository secret | +| `COOLIFY_TRACKER_GATEWAY_DEPLOY_HOOK` | repository secret | +| `COOLIFY_TRACKER_WEB_DEPLOY_HOOK` | repository secret | + +**Nothing configured is a skip; PARTIALLY configured is a failure.** That is a deliberate +difference from the Core's equivalent job, where an unset hook warns and exits zero per service: +set the token, typo one hook, and CD reports success while that service never deploys. Half-wired +is not a state worth being quiet about. + +**And triggering is not deploying.** Coolify accepts the webhook and works asynchronously, so a +green job means the *request* was accepted — never that the new image is serving. `GT-448` asks for +a deploy evidenced by a recorded run against the live service, and that evidence has to be taken +there, not here. The job says so in its own step summary rather than letting the tick imply it. + ## Notes - **Connection string.** The .NET config system cannot concatenate a password