发布可以跟上只多了文档的测试环境头

四个生产工作流改用同一条发布修订:最近一次代码门禁成功的提交可以发布,后面只多文档不再挡住。生产环境拒绝测试验证码。63 个还没上生产的迁移写了分级、演练脚本和首发步骤。没有点任何生产按钮。

自测:发布修订 7 通过,演练安全 3 通过,tsc 0 错,lint 0 error。本机空库演练在服务角色读人物表处按设计停止(退出码 4,其他人物已确认行数为 0)。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-14 10:24:40 +08:00
co-authored by Grok (agent loop)
parent f4c53e7f9f
commit 00e6efdbfa
23 changed files with 1470 additions and 174 deletions
+10 -30
View File
@@ -40,43 +40,23 @@ jobs:
run: |
set -euo pipefail
[[ "$DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "deploy_sha must be a lowercase full commit SHA" >&2; exit 1; }
[[ "$GITEA_SHA" == "$DEPLOY_SHA" ]] || { echo "dispatch recovery from the exact main release SHA" >&2; exit 1; }
[[ "$STAGING_URL" == "https://staging.jyotisha.chat" ]] || { echo "unexpected staging acceptance URL" >&2; exit 1; }
read_ref_sha() {
local branch="$1"
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/$branch" |
jq -er --arg ref "refs/heads/$branch" '
select(type == "array" and length == 1) |
.[0] | select(.ref == $ref) | .object.sha |
select(test("^[0-9a-f]{40}$"))
'
}
staging_head="$(read_ref_sha staging)"
main_head="$(read_ref_sha main)"
[[ "$main_head" == "$DEPLOY_SHA" && "$staging_head" == "$DEPLOY_SHA" ]] || {
echo "production recovery requires main and staging to equal deploy_sha" >&2
exit 1
}
git init .
git remote remove origin 2>/dev/null || true
git remote add origin https://git.copse.top/root/Jyotisha.git
git fetch --no-tags origin "$GITEA_SHA"
git checkout --detach --force "$GITEA_SHA"
REQUESTED_SHA="$DEPLOY_SHA" CHECK_RELEASE_GATE=true bash deploy/resolve-release-revision.sh >>"$GITHUB_OUTPUT"
release_sha="$(awk -F= '$1 == "release_sha" { print $2 }' "$GITHUB_OUTPUT" | tail -n 1)"
head_sha="$(awk -F= '$1 == "head_sha" { print $2 }' "$GITHUB_OUTPUT" | tail -n 1)"
[[ "$release_sha" == "$DEPLOY_SHA" ]] || { echo "requested SHA is not the docs-only release revision" >&2; exit 1; }
[[ "$GITEA_SHA" == "$head_sha" ]] || { echo "dispatch recovery from current main" >&2; exit 1; }
observed_staging_sha="$(curl --fail --silent --show-error --connect-timeout 15 --max-time 30 --retry 3 --retry-all-errors \
"$STAGING_URL/api/health" | jq -er '.deployment.gitCommit | select(test("^[0-9a-f]{40}$"))')"
[[ "$observed_staging_sha" == "$DEPLOY_SHA" ]] || {
echo "public staging has not accepted the requested SHA" >&2
exit 1
}
release_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&event=workflow_dispatch&status=success&limit=100")"
jq -e --arg sha "$DEPLOY_SHA" '
any(.workflow_runs[]?;
(.path | split("@")[0] | endswith("release-quality-gate.yml")) and
.head_sha == $sha and .event == "workflow_dispatch" and .conclusion == "success"
)
' <<<"$release_runs" >/dev/null || {
echo "no successful exact-SHA manual release quality gate found" >&2
exit 1
}
echo "sha=$DEPLOY_SHA" >>"$GITHUB_OUTPUT"
- name: Checkout exact recovery controller
+82 -68
View File
@@ -66,79 +66,87 @@ jobs:
allow_rollback=true
fi
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=$REQUESTED_SHA&branch=staging&event=push&status=success&limit=100")"
selected_run="$(jq -cer --arg sha "$REQUESTED_SHA" '
[.workflow_runs[] | select(
(.path | split("@")[0] | endswith("backend-quality-gate.yml")) and
.head_sha == $sha and .head_branch == "staging" and
.event == "push" and .conclusion == "success"
)] | sort_by(.id) | reverse | first
' <<<"$runs")"
gate_run_id="$(jq -er '.id' <<<"$selected_run")"
[[ "$gate_run_id" =~ ^[0-9]+$ ]] || { echo "no successful exact-SHA production quality gate run found" >&2; exit 1; }
read_ref_sha() {
local branch="$1"
curl --fail --silent --show-error --connect-timeout 15 --max-time 60 --retry 3 --retry-all-errors \
git init .
git remote remove origin 2>/dev/null || true
git remote add origin https://git.copse.top/root/Jyotisha.git
git fetch --no-tags origin "$GITEA_SHA"
git checkout --detach --force "$GITEA_SHA"
if [[ "$allow_rollback" == false ]]; then
CHECK_RELEASE_GATE=true bash deploy/resolve-release-revision.sh >>"$GITHUB_OUTPUT"
release_sha="$(awk -F= '$1 == "release_sha" { print $2 }' "$GITHUB_OUTPUT" | tail -n 1)"
gate_run_id="$(awk -F= '$1 == "gate_run_id" { print $2 }' "$GITHUB_OUTPUT" | tail -n 1)"
[[ "$release_sha" == "$REQUESTED_SHA" ]] || { echo "requested SHA is not the docs-only release revision" >&2; exit 1; }
[[ "$gate_run_id" =~ ^[0-9]+$ ]] || { echo "no successful exact-SHA production quality gate run found" >&2; exit 1; }
controller_sha="$release_sha"
controller_gate_run_id="$gate_run_id"
else
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/git/refs/heads/$branch" |
jq -er --arg ref "refs/heads/$branch" '
select(type == "array" and length == 1) |
.[0] | select(.ref == $ref) | .object.sha |
select(test("^[0-9a-f]{40}$"))
'
}
staging_head="$(read_ref_sha staging)"
controller_sha="$(read_ref_sha main)"
[[ "$controller_sha" == "$staging_head" ]] || { echo "main and staging must identify the same reviewed release" >&2; exit 1; }
if [[ "$allow_rollback" == false && "$REQUESTED_SHA" != "$staging_head" ]]; then
echo "stale production revision refused; use explicit manual rollback only when intended" >&2
exit 1
fi
if [[ "$allow_rollback" == true && "$REQUESTED_SHA" != "$controller_sha" ]]; then
comparison="$(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/compare/$REQUESTED_SHA...$controller_sha")"
jq -e --arg base "$REQUESTED_SHA" --arg head "$controller_sha" '
(.commits // []) as $commits |
def parents($sha): [$commits[] | select(.sha == $sha) | (.parents // [])[] | .sha];
def reaches($sha; $seen):
if $sha == $base then true
elif ($seen | index($sha)) != null then false
else any(parents($sha)[]; . as $parent | reaches($parent; $seen + [$sha])) end;
(.total_commits | type) == "number" and
.total_commits == ($commits | length) and ($commits | length) > 0 and
([$commits[].sha] | length == (unique | length)) and reaches($head; [])
' <<<"$comparison" >/dev/null || { echo "rollback revision is not in reviewed main history" >&2; exit 1; }
fi
controller_gate_run_id="$gate_run_id"
if [[ "$controller_sha" != "$REQUESTED_SHA" ]]; then
controller_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=$controller_sha&branch=staging&event=push&status=success&limit=100")"
controller_run="$(jq -cer --arg sha "$controller_sha" '
"$GITEA_API_URL/repos/$GITEA_REPOSITORY/actions/runs?head_sha=$REQUESTED_SHA&branch=staging&event=push&status=success&limit=100")"
selected_run="$(jq -cer --arg sha "$REQUESTED_SHA" '
[.workflow_runs[] | select(
(.path | split("@")[0] | endswith("backend-quality-gate.yml")) and
.head_sha == $sha and .head_branch == "staging" and
.event == "push" and .conclusion == "success"
)] | sort_by(.id) | reverse | first
' <<<"$controller_runs")"
controller_gate_run_id="$(jq -er '.id' <<<"$controller_run")"
' <<<"$runs")"
gate_run_id="$(jq -er '.id' <<<"$selected_run")"
[[ "$gate_run_id" =~ ^[0-9]+$ ]] || { echo "no successful exact-SHA production quality gate run found" >&2; exit 1; }
read_ref_sha() {
local branch="$1"
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/$branch" |
jq -er --arg ref "refs/heads/$branch" '
select(type == "array" and length == 1) |
.[0] | select(.ref == $ref) | .object.sha |
select(test("^[0-9a-f]{40}$"))
'
}
staging_head="$(read_ref_sha staging)"
controller_sha="$(read_ref_sha main)"
[[ "$controller_sha" == "$staging_head" ]] || { echo "main and staging must identify the same reviewed release" >&2; exit 1; }
if [[ "$REQUESTED_SHA" != "$controller_sha" ]]; then
comparison="$(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/compare/$REQUESTED_SHA...$controller_sha")"
jq -e --arg base "$REQUESTED_SHA" --arg head "$controller_sha" '
(.commits // []) as $commits |
def parents($sha): [$commits[] | select(.sha == $sha) | (.parents // [])[] | .sha];
def reaches($sha; $seen):
if $sha == $base then true
elif ($seen | index($sha)) != null then false
else any(parents($sha)[]; . as $parent | reaches($parent; $seen + [$sha])) end;
(.total_commits | type) == "number" and
.total_commits == ($commits | length) and ($commits | length) > 0 and
([$commits[].sha] | length == (unique | length)) and reaches($head; [])
' <<<"$comparison" >/dev/null || { echo "rollback revision is not in reviewed main history" >&2; exit 1; }
fi
controller_gate_run_id="$gate_run_id"
if [[ "$controller_sha" != "$REQUESTED_SHA" ]]; then
controller_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=$controller_sha&branch=staging&event=push&status=success&limit=100")"
controller_run="$(jq -cer --arg sha "$controller_sha" '
[.workflow_runs[] | select(
(.path | split("@")[0] | endswith("backend-quality-gate.yml")) and
.head_sha == $sha and .head_branch == "staging" and
.event == "push" and .conclusion == "success"
)] | sort_by(.id) | reverse | first
' <<<"$controller_runs")"
controller_gate_run_id="$(jq -er '.id' <<<"$controller_run")"
fi
[[ "$controller_gate_run_id" =~ ^[0-9]+$ ]]
release_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=$REQUESTED_SHA&event=workflow_dispatch&status=success&limit=100")"
jq -e --arg sha "$REQUESTED_SHA" '
any(.workflow_runs[]?;
(.path | split("@")[0] | endswith("release-quality-gate.yml")) and
.head_sha == $sha and .event == "workflow_dispatch" and .conclusion == "success"
)
' <<<"$release_runs" >/dev/null || { echo "no successful exact-SHA manual release quality gate found" >&2; exit 1; }
fi
[[ "$controller_gate_run_id" =~ ^[0-9]+$ ]]
release_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=$REQUESTED_SHA&event=workflow_dispatch&status=success&limit=100")"
jq -e --arg sha "$REQUESTED_SHA" '
any(.workflow_runs[]?;
(.path | split("@")[0] | endswith("release-quality-gate.yml")) and
.head_sha == $sha and .event == "workflow_dispatch" and .conclusion == "success"
)
' <<<"$release_runs" >/dev/null || { echo "no successful exact-SHA manual release quality gate found" >&2; exit 1; }
if [[ "$allow_rollback" == false ]]; then
observed_staging_sha="$(curl --fail --silent --show-error --connect-timeout 15 --max-time 30 --retry 3 --retry-all-errors \
@@ -338,8 +346,14 @@ jobs:
jq -er 'select(type == "array" and length == 1) | .[0] |
select(.ref == "refs/heads/main") | .object.sha |
select(test("^[0-9a-f]{40}$"))')"
[[ "$current_main" == "$DEPLOY_SHA" && "$current_staging" == "$DEPLOY_SHA" ]] || {
echo "main or staging advanced during deployment; refusing stale mutation" >&2
[[ "$current_main" == "$current_staging" ]] || {
echo "main and staging diverged during deployment; refusing stale mutation" >&2
exit 1
}
checker=artifacts/controller/extracted/deploy/is-docs-only-range.sh
[[ -f "$checker" ]] || { echo "gate-attested controller bundle lacks deploy/is-docs-only-range.sh" >&2; exit 1; }
bash "$checker" --api "$DEPLOY_SHA" "$current_staging" || {
echo "staging advanced past the release SHA with a gated change; refusing stale mutation" >&2
exit 1
}
}
@@ -85,54 +85,18 @@ jobs:
PY
echo "Recovery attested: reference=$RECOVERY_REFERENCE created_at=$RECOVERY_CREATED_AT restore_verified=true"
echo "WARNING: migration files run sequentially and are not atomic as a whole; recovery may be required after a partial migration." >&2
read_ref_sha() {
local branch="$1"
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/$branch" |
jq -er --arg ref "refs/heads/$branch" '
select(type == "array" and length == 1) |
.[0] | select(.ref == $ref) | .object.sha |
select(test("^[0-9a-f]{40}$"))
'
}
staging_head="$(read_ref_sha staging)"
main_head="$(read_ref_sha main)"
[[ "$main_head" == "$DEPLOY_SHA" && "$staging_head" == "$DEPLOY_SHA" ]] || {
echo "production migration requires main and staging to equal deploy_sha" >&2
exit 1
}
[[ "$GITEA_SHA" == "$DEPLOY_SHA" ]] || {
echo "dispatch the production migration workflow from the exact main release SHA" >&2
exit 1
}
runs="$(curl --fail --silent --show-error \
--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" '
[.workflow_runs[] | select(
(.path | split("@")[0] | endswith("backend-quality-gate.yml")) and
.head_sha == $sha and .head_branch == "staging" and
.event == "push" and .conclusion == "success"
)] | sort_by(.id) | reverse | first
' <<<"$runs")"
gate_run_id="$(jq -er '.id' <<<"$selected_run")"
[[ "$gate_run_id" =~ ^[0-9]+$ ]] || {
echo "no successful exact-SHA staging backend quality gate found" >&2
exit 1
}
release_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&event=workflow_dispatch&status=success&limit=100")"
jq -e --arg sha "$DEPLOY_SHA" '
any(.workflow_runs[]?;
(.path | split("@")[0] | endswith("release-quality-gate.yml")) and
.head_sha == $sha and .event == "workflow_dispatch" and .conclusion == "success"
)
' <<<"$release_runs" >/dev/null || {
echo "no successful exact-SHA manual release quality gate found" >&2
exit 1
}
git init .
git remote remove origin 2>/dev/null || true
git remote add origin https://git.copse.top/root/Jyotisha.git
git fetch --no-tags origin "$GITEA_SHA"
git checkout --detach --force "$GITEA_SHA"
REQUESTED_SHA="$DEPLOY_SHA" CHECK_RELEASE_GATE=true bash deploy/resolve-release-revision.sh >>"$GITHUB_OUTPUT"
release_sha="$(awk -F= '$1 == "release_sha" { print $2 }' "$GITHUB_OUTPUT" | tail -n 1)"
gate_run_id="$(awk -F= '$1 == "gate_run_id" { print $2 }' "$GITHUB_OUTPUT" | tail -n 1)"
head_sha="$(awk -F= '$1 == "head_sha" { print $2 }' "$GITHUB_OUTPUT" | tail -n 1)"
[[ "$release_sha" == "$DEPLOY_SHA" ]] || { echo "requested SHA is not the docs-only release revision" >&2; exit 1; }
[[ "$gate_run_id" =~ ^[0-9]+$ ]] || { echo "no successful exact-SHA staging backend quality gate found" >&2; exit 1; }
[[ "$GITEA_SHA" == "$head_sha" ]] || { echo "dispatch the production migration workflow from current main" >&2; exit 1; }
observed_staging_sha="$(curl --fail --silent --show-error --connect-timeout 15 --max-time 30 --retry 3 --retry-all-errors \
"$STAGING_URL/api/health" | jq -er '.deployment.gitCommit | select(test("^[0-9a-f]{40}$"))')"
[[ "$observed_staging_sha" == "$DEPLOY_SHA" ]] || {
@@ -317,8 +281,14 @@ jobs:
jq -er 'select(type == "array" and length == 1) | .[0] |
select(.ref == "refs/heads/main") | .object.sha |
select(test("^[0-9a-f]{40}$"))')"
[[ "$current_main" == "$DEPLOY_SHA" && "$current_staging" == "$DEPLOY_SHA" ]] || {
echo "main or staging advanced during production migration; refusing stale mutation" >&2
[[ "$current_main" == "$current_staging" ]] || {
echo "main and staging diverged during production migration; refusing stale mutation" >&2
exit 1
}
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" >&2; exit 1; }
bash "$checker" --api "$DEPLOY_SHA" "$current_staging" || {
echo "staging advanced past the release SHA with a gated change; refusing stale mutation" >&2
exit 1
}
}
+18
View File
@@ -20,6 +20,24 @@ jobs:
git remote add origin https://git.copse.top/root/Jyotisha.git
git fetch --no-tags origin "$GITEA_SHA"
git checkout --detach --force "$GITEA_SHA"
- name: Record whether main is docs-only ahead of the last staging gate
env:
GITEA_API_URL: ${{ gitea.api_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
RESOLVE_MODE: discover
run: |
set -euo pipefail
bash deploy/resolve-release-revision.sh | tee "$RUNNER_TEMP/release-revision.env"
release_sha="$(awk -F= '$1 == "release_sha" { print $2 }' "$RUNNER_TEMP/release-revision.env")"
head_sha="$(awk -F= '$1 == "head_sha" { print $2 }' "$RUNNER_TEMP/release-revision.env")"
[[ "$GITEA_SHA" == "$head_sha" ]] || { echo "dispatch the release gate from current main" >&2; exit 1; }
{
echo "### Release revision"
echo "- head_sha: ${head_sha}"
echo "- release_sha: ${release_sha}"
echo "- H relative to R is docs-only (is-docs-only-range.sh exited 0)"
} >>"$GITHUB_STEP_SUMMARY"
- name: Prepare pinned Node tooling
run: |
set -euo pipefail