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