From c36015803f337e3af37add42e62a7fdc1d732f3a Mon Sep 17 00:00:00 2001 From: tianyao Date: Wed, 4 Nov 2026 05:03:13 +0000 Subject: [PATCH] fix(cf-v2): mark a superseded Production deployment inactive instead of success with a blank URL When the stale guard supersedes a Production run, no publish step runs, but GitHub still appends `success` (no environment_url) to the Environment deployment when the Deploy job ends. The Smoke job, which runs after Deploy, now appends `inactive` to that deployment when the hand-off says stale=true. Production only; published, failed and Preview deployments and all deploy/smoke/rollback/release semantics are unchanged. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/_cf-worker-delivery.yml | 15 ++++++++++ scripts/cf-preview/test-workflow-contract.py | 30 ++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/.github/workflows/_cf-worker-delivery.yml b/.github/workflows/_cf-worker-delivery.yml index 12cc2b6..4242f81 100644 --- a/.github/workflows/_cf-worker-delivery.yml +++ b/.github/workflows/_cf-worker-delivery.yml @@ -176,6 +176,21 @@ jobs: run: | h="${HANDOFF:-}"; [ -n "$h" ] || h='{}' echo "go=$(printf '%s' "$h" | jq -r '.published == "true"')" >> "$GITHUB_OUTPUT" + # GitHub appends `success` (with no environment_url: nothing was published) to the Environment deployment when the Deploy + # job ends, even when the stale guard skipped every publish step. This job runs after Deploy ended, so it can correct that + # record: a superseded Production run is "inactive", not a successful deployment with a blank URL. Production only. + - name: Mark a superseded Production deployment inactive + if: steps.go.outputs.go != 'true' && fromJSON(inputs.descriptor).kind == 'production' + env: + ENV_NAME: ${{ fromJSON(inputs.descriptor).environment }} + run: | + h="${HANDOFF:-}"; [ -n "$h" ] || h='{}' + [ "$(printf '%s' "$h" | jq -r '.stale == "true"')" = true ] || exit 0 + id="$(gh api "repos/$GITHUB_REPOSITORY/deployments?environment=$(printf %s "$ENV_NAME" | jq -sRr @uri)&sha=$GITHUB_SHA&per_page=1" -q '.[0].id // empty')" || id="" + if [ -z "$id" ]; then echo "::warning::no $ENV_NAME deployment at $GITHUB_SHA to mark as superseded"; exit 0; fi + gh api "repos/$GITHUB_REPOSITORY/deployments/$id/statuses" -f state=inactive -f log_url="${GITHUB_SERVER_URL:-https://github.com}/$GITHUB_REPOSITORY/actions/runs/${GITHUB_RUN_ID:-0}" \ + -f description="Superseded by a newer push before publish; nothing deployed" >/dev/null \ + || echo "::warning::could not mark the superseded $ENV_NAME deployment inactive" - uses: actions/checkout@v6 if: steps.go.outputs.go == 'true' with: diff --git a/scripts/cf-preview/test-workflow-contract.py b/scripts/cf-preview/test-workflow-contract.py index 362cbd9..f69b2ff 100755 --- a/scripts/cf-preview/test-workflow-contract.py +++ b/scripts/cf-preview/test-workflow-contract.py @@ -293,6 +293,7 @@ def gate_cli(result, mj, d1="true", kind="native-branch", owner="native-branch", env = {"PATH": "/usr/bin:/bin:" + __import__("os").path.dirname(__import__("shutil").which("node") or "/usr/bin/node"), "DESCRIPTOR": d, "D1_MIGRATIONS": d1, "MIGRATION_RESULT": result, "MIGRATION_JSON": mj} r = subprocess.run(["node", "actions/cf-preview/cli.mjs", "migration-gate", "--kind", kind], env=env, capture_output=True, text=True) return r.returncode, (r.stdout + r.stderr) +import os import subprocess # noqa: E402 APPLIED = json.dumps({"migration": "applied", "kind": "native-branch", "d1_id": "DB1"}) check(gate_cli("success", APPLIED)[0] == 0, "Migration Gate (executed): a succeeded Migration job with an applied migration of the planned owner on the planned D1 authorizes the publish") @@ -722,4 +723,33 @@ def forged(group, inputs): # the REAL plan-side validation (restoreDescriptors) check(ENGINE_SH.count('"$TOOLS_DIR/build.sh"') == 1 and ENGINE_SH.count('"$TOOLS_DIR/publish.sh"') == 1 and ENGINE_SH.count("run_build ") == 2 and ENGINE_SH.count("run_publish ") == 2, "engine: ONE audited build primitive and ONE publish primitive, shared by the normal run and the recovery rebuild (recovery only supplies its state dir, rebound env/version, no host re-check)") +# ---------------------------------------------------------------- superseded Production deployments are not "success + blank URL" +# GitHub appends `success` to the Environment deployment when the Deploy job ends, even when the stale guard skipped every +# publish step (hand-off: stale=true, published unset). The Smoke job (which runs after the Deploy job ended) must then +# append `inactive` to THAT deployment - for Production only, never for a published or failed deployment. +import tempfile as _tf +SUP = next((st for st in djobs["smoke"]["steps"] if st.get("name", "").startswith("Mark a superseded")), None) +check(SUP is not None, "Smoke: a step marks a superseded Production deployment inactive") +def sup_run(handoff, kind="production", env_name="app/prod", sha="abc1234"): + if SUP is None: + return None + d = _tf.mkdtemp() + open(f"{d}/gh", "w").write('#!/usr/bin/env bash\necho "gh $*" >> "$LOG"\ncase "$*" in *"deployments?environment"*) echo 77;; esac\n') + os.chmod(f"{d}/gh", 0o755) + env = {"PATH": f"{d}:/usr/bin:/bin", "LOG": f"{d}/log", "HANDOFF": handoff, "GITHUB_REPOSITORY": "o/r", "GITHUB_SHA": sha, "KIND": kind, "ENV_NAME": env_name, + "GITHUB_RUN_ID": "1", "GITHUB_SERVER_URL": "https://github.com", "GH_TOKEN": "x"} + ran = bool(ev(SUP.get("if", "true"), Ctx({"steps": {"go": {"outputs": {"go": "false"}}}, "inputs": {"descriptor": json.dumps({"kind": kind, "environment": env_name})}}, {}))) + if not ran: + return "skipped" + r = subprocess.run(["bash", "-eo", "pipefail", "-c", SUP["run"]], env=env, capture_output=True, text=True) + return r.returncode, open(f"{d}/log").read() if os.path.exists(f"{d}/log") else "" +if SUP is not None: + rc_, log_ = sup_run('{"stale":"true","kind":"production"}') + check(rc_ == 0 and "deployments?environment=app%2Fprod&sha=abc1234" in log_ and "deployments/77/statuses" in log_ and "state=inactive" in log_ and "Superseded" in log_, "Smoke (executed): a superseded Production run appends `inactive` to its newest Environment deployment at this SHA") + check(sup_run('{"stale":"false","published":"true"}') in ("skipped",) or "statuses" not in (sup_run('{"stale":"false","published":"true"}')[1]), "Smoke (executed): a fresh run never marks its deployment inactive") + check("statuses" not in sup_run('{}')[1], "Smoke (executed): an empty hand-off marks nothing") + check(sup_run('{"stale":"true"}', kind="native-branch", env_name="app/preview") in ("skipped",) or "statuses" not in sup_run('{"stale":"true"}', kind="native-branch", env_name="app/preview")[1], "Smoke: Preview kinds keep their behaviour (only Production is touched)") + check(djobs["smoke"]["permissions"].get("deployments") == "write" and "secrets" not in yaml.dump(SUP), "Smoke: the step needs only deployments:write and uses no secrets") + + sys.exit(fail)