fix(staging): let migrate accept docs-only-ahead like deploy
Independent Staging Quality Gate / validate (push) Successful in 8m57s
Independent Staging Quality Gate / publish (push) Successful in 1m49s

Docs-only staging HEAD blocked Migrate Staging Database even when an ancestor already had a successful exact-SHA gate, so pending SQL could not be applied.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-03 10:29:19 +08:00
co-authored by Cursor
parent 1d77b5b129
commit 13bde020fb
3 changed files with 73 additions and 10 deletions
+49 -7
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
deploy_sha:
description: Full current staging SHA to migrate
description: Exact tested 40-character staging commit SHA
required: true
type: string
@@ -52,9 +52,7 @@ jobs:
select(test("^[0-9a-f]{40}$"))
'
}
staging_head="$(read_ref_sha staging)"
[[ "$staging_head" == "$DEPLOY_SHA" ]] || { echo "migration requires current staging head" >&2; exit 1; }
runs="$(curl --fail --silent --show-error \
runs="$(curl --fail --silent --show-error --connect-timeout 15 --max-time 60 --retry 3 --retry-all-errors \
--header "Authorization: token $GITEA_TOKEN" \
"$GITEA_API_URL/repos/$GITEA_REPOSITORY/actions/runs?head_sha=$DEPLOY_SHA&branch=staging&event=push&status=success&limit=100")"
selected_run="$(jq -cer --arg sha "$DEPLOY_SHA" '
@@ -65,10 +63,23 @@ jobs:
)] | sort_by(.id) | reverse | first
' <<<"$runs")"
gate_run_id="$(jq -er '.id' <<<"$selected_run")"
[[ "$gate_run_id" =~ ^[0-9]+$ ]]
[[ "$gate_run_id" =~ ^[0-9]+$ ]] || { echo "no successful exact-SHA staging quality gate run found" >&2; exit 1; }
staging_head="$(read_ref_sha staging)"
head_check=current
if [[ "$DEPLOY_SHA" != "$staging_head" ]]; then
# Docs-only pushes (every change outside deploy/gated-paths.txt) no
# longer run the gate, so staging may legitimately be ahead of the
# tested SHA. That is decided only after the gate-attested controller
# bundle is downloaded, by its own deploy/is-docs-only-range.sh, so
# this job never executes an untested checker; anything that is not
# a pure docs-only advance is still refused there before mutation.
echo "staging head $staging_head differs from requested $DEPLOY_SHA; deferring the docs-only range check to the attested controller"
head_check=deferred
fi
{
echo "sha=$DEPLOY_SHA"
echo "gate_run_id=$gate_run_id"
echo "head_check=$head_check"
} >>"$GITHUB_OUTPUT"
- name: Prepare pinned Node tooling
@@ -181,7 +192,7 @@ jobs:
python3 - "$controller_tar" <<'PY'
import pathlib, sys, tarfile
archive = pathlib.Path(sys.argv[1])
required = {"deploy/run-staging-migration.sh", "frontend/scripts/staging-image-manifest.mjs"}
required = {"deploy/run-staging-migration.sh", "deploy/is-docs-only-range.sh", "frontend/scripts/staging-image-manifest.mjs"}
with tarfile.open(archive, "r:") as bundle:
members = bundle.getmembers()
names = [member.name for member in members]
@@ -199,6 +210,34 @@ jobs:
node artifacts/staging-image/extracted/frontend/scripts/staging-image-manifest.mjs \
"$manifest" "$DEPLOY_SHA" "$IMAGE_REPOSITORY" >>"$GITHUB_OUTPUT"
- name: Refuse stale staging revision unless only docs advanced
env:
DEPLOY_SHA: ${{ steps.revision.outputs.sha }}
HEAD_CHECK: ${{ steps.revision.outputs.head_check }}
run: |
set -euo pipefail
staging_head="$(curl --fail --silent --show-error --connect-timeout 15 --max-time 60 --retry 3 --retry-all-errors \
--header "Authorization: token $GITEA_TOKEN" \
"$GITEA_API_URL/repos/$GITEA_REPOSITORY/git/refs/heads/staging" |
jq -er 'select(type == "array" and length == 1) | .[0] |
select(.ref == "refs/heads/staging") | .object.sha |
select(test("^[0-9a-f]{40}$"))')"
if [[ "$staging_head" == "$DEPLOY_SHA" ]]; then
echo "staging head is the tested revision $DEPLOY_SHA (initial check: $HEAD_CHECK)"
exit 0
fi
# Only the gate-attested controller's checker and path list are trusted;
# it proves DEPLOY_SHA is an ancestor of the head and that every path in
# between is outside deploy/gated-paths.txt via the Gitea compare API.
checker=artifacts/staging-image/extracted/deploy/is-docs-only-range.sh
[[ -f "$checker" ]] || { echo "gate-attested controller bundle lacks deploy/is-docs-only-range.sh; cannot accept an advanced staging head" >&2; exit 1; }
if bash "$checker" --api "$DEPLOY_SHA" "$staging_head"; then
echo "staging advanced to $staging_head by docs-only commits; releasing tested $DEPLOY_SHA"
else
echo "stale staging revision refused; migrate the current gated SHA" >&2
exit 1
fi
- name: Apply digest-pinned migration under host lock
env:
SSH_PRIVATE_KEY_BASE64: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
@@ -227,7 +266,10 @@ jobs:
jq -er 'select(type == "array" and length == 1) | .[0] |
select(.ref == "refs/heads/staging") | .object.sha |
select(test("^[0-9a-f]{40}$"))')"
[[ "$current_head" == "$DEPLOY_SHA" ]] || { echo "staging advanced during migration; refusing stale mutation" >&2; exit 1; }
[[ "$current_head" == "$DEPLOY_SHA" ]] && return
# Docs-only pushes may land while a migration is in flight; the attested checker decides.
bash artifacts/staging-image/extracted/deploy/is-docs-only-range.sh --api "$DEPLOY_SHA" "$current_head" ||
{ echo "staging advanced during migration; refusing stale mutation" >&2; exit 1; }
}
cleanup() {
if [[ -n "$incoming" ]]; then