Skip to content
48 changes: 24 additions & 24 deletions .github/workflows/advance-deploy-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
# Default mapping
case "$BRANCH" in
develop) DEPLOY_ENV="dev"; STATUS_NAME="On dev" ;;
staging) DEPLOY_ENV="staging"; STATUS_NAME="Staging (human review)" ;;
staging) DEPLOY_ENV="staging"; STATUS_NAME="FR on staging" ;;
Comment thread
cursor[bot] marked this conversation as resolved.
master|main) DEPLOY_ENV="prod"; STATUS_NAME="Prod" ;;
*) DEPLOY_ENV=""; STATUS_NAME="" ;;
esac
Expand Down Expand Up @@ -167,37 +167,37 @@ jobs:
| select(.name=="Status") | .id')
STATUS_OPT=$(echo "$PROJ" | jq -r --arg s "$STATUS_NAME" '.data.organization.projectV2.fields.nodes[]
| select(.name=="Status") | .options[] | select(.name==$s) | .id')
# Rename window (backend#1592). "FR on staging" becomes "Staging (human
# review)" via a UI rename, which is a single instant the board flips --
# there is no period where both options exist. So resolution asks for the
# NEW name and falls back to the OLD one, and this file works on either
# side of that instant. The fallback is removed in step 3 of #1592.
if [ -z "$STATUS_OPT" ] || [ "$STATUS_OPT" = "null" ]; then
case "$STATUS_NAME" in
"Staging (human review)") FALLBACK="FR on staging" ;;
*) FALLBACK="" ;;
esac
if [ -n "$FALLBACK" ]; then
STATUS_OPT=$(echo "$PROJ" | jq -r --arg s "$FALLBACK" '.data.organization.projectV2.fields.nodes[]
| select(.name=="Status") | .options[] | select(.name==$s) | .id')
[ -n "$STATUS_OPT" ] && [ "$STATUS_OPT" != "null" ] && STATUS_NAME="$FALLBACK"
fi
fi

if [ -z "$DEPLOY_OPT" ] || [ "$DEPLOY_OPT" = "null" ]; then
echo "Could not resolve Deploy environment option for '$DEPLOY_ENV' - aborting"
exit 1
fi

# Status update degrades gracefully: if the Status field or its target
# option can't be resolved, log a warning and skip just the Status step
# rather than failing the whole workflow (Deploy env update still wins).
SKIP_STATUS=0
# Status resolution does NOT degrade gracefully -- see the abort below.
# It used to, and that was the bug: a graceful degrade here means the
# board stops advancing while every run reports success.
# FAIL CLOSED. This used to warn and set SKIP_STATUS=1, so an unresolvable
# Status option meant the run stayed GREEN while no card advanced -- the
# board silently stops tracking the pipeline and the only signal is a
# warning nobody reads (Bugbot, .github#243, High).
#
# It is also the ONE asymmetry in this file and its siblings: the Deploy
# environment lookup twelve lines above aborts, and
# kanban-closure-router.yml aborts on exactly this condition. A column
# rename, a project renumbering or a `.kanban.yml` naming a column that
# does not exist are all misconfigurations, and every one of them is
# cheaper to find as a red run than as three weeks of un-advanced cards.
if [ -z "$STATUS_FIELD" ] || [ "$STATUS_FIELD" = "null" ] \
|| [ -z "$STATUS_OPT" ] || [ "$STATUS_OPT" = "null" ]; then
echo "::warning::Could not resolve Status option '$STATUS_NAME' in project #$PROJECT_NUMBER - skipping Status updates"
SKIP_STATUS=1
echo "::error::Could not resolve Status option '$STATUS_NAME' in project #$PROJECT_NUMBER." \
"NOTHING WAS WRITTEN: this check runs before any field update, so no card" \
"was touched and there is no half-applied state to repair." \
"Check the board's Status column names against this workflow's branch map." >&2
exit 1
Comment thread
cursor[bot] marked this conversation as resolved.
fi
# Now always 0 -- the branch that set it to 1 aborts instead. Kept so the
# two `[ "$SKIP_STATUS" != "1" ]` guards below stay valid without
# re-indenting their blocks; there is no live skip path.
SKIP_STATUS=0

# Pipeline order (mirrors fr-gate's rank). Advancement is monotonic:
# a push only ever moves a card FORWARD. Pushes routinely carry commits
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/bricked-prs-selftest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Bricked PRs selftest

# The watcher's product is a DISTINCTION, not a list: a context absent because
# it will never report, versus absent because the run has not started, versus a
# branch that could not be read. Get any of those wrong and the report is either
# ignorable or misleading -- both were observed while building it (backend#1721),
# so the paths are asserted rather than trusted.
#
# Same shape as blocked-gate-selftest.yml: offline, no token, path-filtered
# because it only needs to run when the thing it tests changes.

on:
pull_request:
paths:
- scripts/bricked-prs.py
- scripts/tests/bricked-prs-selftest.py
- .github/workflows/bricked-prs.yml
- .github/workflows/bricked-prs-selftest.yml
push:
branches: [main, develop, staging]
paths:
- scripts/bricked-prs.py
- scripts/tests/bricked-prs-selftest.py
- .github/workflows/bricked-prs.yml
- .github/workflows/bricked-prs-selftest.yml

permissions:
contents: read

concurrency:
group: bricked-prs-selftest-${{ github.ref }}
cancel-in-progress: true

jobs:
selftest:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.12'
# bricked-prs.py imports caller-drift.py for the protection reader, and
# that module hard-fails without PyYAML by design.
- run: pip install --quiet pyyaml
- run: python scripts/tests/bricked-prs-selftest.py
48 changes: 48 additions & 0 deletions .github/workflows/bricked-prs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Bricked PRs

# A required status check that never reports leaves a PR approved, with nothing
# red to point at, and permanently unmergeable (backend#1721). It is the one CI
# failure mode with NO red signal at all -- nobody is notified, and no reviewer
# sees a problem, because there is no failure, only an absence. So it needs a
# watcher; nothing inside a PR can detect it.
#
# Runs in tracebloc/.github only, like the other org-wide crons.

on:
schedule:
# Every four hours. The failure is not urgent -- a bricked PR stays bricked
# -- but it is invisible, so the cost is the hours a human spends not
# realising. Four hours bounds that without adding noise.
- cron: "17 */4 * * *"
workflow_dispatch: {}

permissions:
contents: read

concurrency:
group: bricked-prs
cancel-in-progress: false

jobs:
audit:
name: Required checks that never report
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
# Without this the `pip install` below hits PEP 668 on ubuntu-latest
# (24.04): the runner's Python is externally managed, pip refuses, the step
# fails and THE AUDIT NEVER RUNS. A scheduled guard that cannot start is
# worse than none — nothing reports, and silence reads as "no bricked PRs".
# bricked-prs-selftest.yml already does this before the same install; this
# is that shape, not a new idea (Bugbot, #243).
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.12'
- name: Install PyYAML
run: pip install --quiet pyyaml
Comment thread
cursor[bot] marked this conversation as resolved.
- name: Audit
env:
# Needs to read branch protection, rulesets and PRs across the org.
GH_TOKEN: ${{ secrets.PROJECTS_KANBAN_TOKEN }}
run: python3 scripts/bricked-prs.py
44 changes: 31 additions & 13 deletions .github/workflows/kanban-closure-router.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
if [ "$PR_MERGED" = "true" ]; then
case "$BASE_REF" in
main|master) STATUS="Prod" ;;
staging) STATUS="Staging (human review)" ;;
staging) STATUS="FR on staging" ;;
develop) STATUS="On dev" ;;
*)
# A PR merged into a SIBLING feature branch deploys nothing by
Expand Down Expand Up @@ -153,7 +153,7 @@ jobs:
elif [ "$CLOSER_TYPE" = "PullRequest" ]; then
case "$CLOSING_PR_BASE" in
main|master) STATUS="Prod" ;;
staging) STATUS="Staging (human review)" ;;
staging) STATUS="FR on staging" ;;
develop) STATUS="On dev" ;;
# Unrecognised base: mirror the pull_request branch's default so
# the issue and its PR agree.
Expand Down Expand Up @@ -226,16 +226,6 @@ jobs:
| select(.name=="Status") | .id')
STATUS_OPT=$(echo "$PROJ" | jq -r --arg s "$STATUS_NAME" '.data.organization.projectV2.fields.nodes[]
| select(.name=="Status") | .options[] | select(.name==$s) | .id')
# Rename window (backend#1592): prefer the new column name, fall back to
# the old. Removed in step 3.
if [ -z "$STATUS_OPT" ] || [ "$STATUS_OPT" = "null" ]; then
if [ "$STATUS_NAME" = "Staging (human review)" ]; then
STATUS_OPT=$(echo "$PROJ" | jq -r --arg s "FR on staging" '.data.organization.projectV2.fields.nodes[]
| select(.name=="Status") | .options[] | select(.name==$s) | .id')
[ -n "$STATUS_OPT" ] && [ "$STATUS_OPT" != "null" ] && STATUS_NAME="FR on staging"
fi
fi

if [ -z "$STATUS_OPT" ] || [ "$STATUS_OPT" = "null" ]; then
echo "Could not resolve Status option '$STATUS_NAME' — aborting"
exit 1
Expand Down Expand Up @@ -284,10 +274,38 @@ jobs:
#
# Only Done is guarded. A PR-derived Status is a deploy fact and may advance a
# card normally.
# THE COLUMN LIST WAS STALE, AND THAT REOPENED THE HOLE IT CLOSES
# (backend#1846). `CURRENT_COL` is read from the BOARD, whose column is
# "Staging (agent review)" -- this list carried only the pre-rename
# "Staging (human review)". So an issue hand-closed while sitting in the
# agent-review column did not match, Done overwrote its deploy state,
# and kanban-archive.yml then hid the card entirely: exactly the
# sequence this guard exists to prevent, through a string that quietly
# stopped being true.
#
# kanban-reconcile.yml's equivalent guard already lists both names. Two
# copies of one list, one updated at the rename and one not, is how this
# drifted -- so when the rename window closes, drop the legacy name in
# BOTH places.
if [ "$STATUS_NAME" = "Done" ]; then
case "${CURRENT_COL:-}" in
"On dev"|"Staging (human review)"|"FR on staging"|"Ready for prod"|"Prod")
"On dev"|"Staging (agent review)"|"Staging (human review)"|"FR on staging"|"Ready for prod"|"Prod")
echo "::notice::#$NUMBER hand-closed but sits in '$CURRENT_COL', a deploy state - NOT setting Done (D8: follow the PR's stage)"
# AND SAY SO WHERE SOMEONE WILL SEE IT. Refusing is right, but it
# parks the card in a deploy state with no way to self-heal, and a
# run-log notice is invisible by the time anyone looks. The two
# real cases needed OPPOSITE answers -- backend#1493 had shipped
# via cli#452 and belonged in Prod; data-ingestors#488 was
# reverted and belonged in Done -- so no default is correct and
# only the person closing it knows which.
CLOSE_NOTE="Closed while the board still shows \`$CURRENT_COL\`, which records a deployment."
CLOSE_NOTE="$CLOSE_NOTE The automation will not overwrite a deploy state with \`Done\` (RFC-BACKEND-1405 D8),"
CLOSE_NOTE="$CLOSE_NOTE so this card stays where it is until someone says which happened:"
CLOSE_NOTE="$CLOSE_NOTE **it shipped** - move the card to the column it reached (\`Prod\` if it is in production);"
CLOSE_NOTE="$CLOSE_NOTE **nothing was deployed** (reverted, abandoned, superseded) - clear the deploy state, then \`Done\`."
CLOSE_NOTE="$CLOSE_NOTE Both cases are real and they need opposite answers, which is why this is not decided automatically."
gh issue comment "$NUMBER" --repo "$REPO_FULL" --body "$CLOSE_NOTE" >/dev/null 2>&1 \
|| echo "::warning::could not comment on #$NUMBER - it is parked in '$CURRENT_COL' with no note on the issue"
exit 0 ;;
esac
fi
Expand Down
69 changes: 69 additions & 0 deletions .github/workflows/kanban-columns.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# The board's Status column names and the workflows that write them are two
# systems that must agree, and nothing checked that they did.
#
# The rename window for `FR on staging` was carried by hand across three PRs
# (backend#1592). In between, the writers emitted a column the board did not
# have, and only a fallback made it work — while an unresolvable Status was a
# `::warning::` on a GREEN run (fixed in #246), so a frozen board and a working
# board were indistinguishable.
#
# Runs on PRs that touch the writers OR this check, and daily — because the
# board can be renamed in the UI at any time, with no PR to hang a check on.
# That is the failure this is really for: a rename nobody pairs with a code
# change.
name: Kanban column conformance

on:
pull_request:
# MUST LIST EVERY FILE IN `WRITERS`. It listed only the original two, so a PR
# touching just set-pr-status.yml or fr-pass-comment.yml never ran this check
# and a bad column write could merge until the next cron (Bugbot, #248). The
# selftest asserts this list against WRITERS, so they cannot drift apart.
paths:
- ".github/workflows/advance-deploy-env.yml"
- ".github/workflows/kanban-closure-router.yml"
- ".github/workflows/set-pr-status.yml"
- ".github/workflows/fr-pass-comment.yml"
- ".github/workflows/kanban-columns.yml"
- "scripts/kanban-columns-check.py"
- "scripts/tests/kanban-columns-selftest.py"
schedule:
# 06:15 UTC, before caller-drift at 06:30 — a board rename breaks promotions,
# so it should be the first thing the morning tells you.
- cron: "15 6 * * *"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: kanban-columns
cancel-in-progress: false

jobs:
selftest:
name: Selftest the checker
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
# Guard the guard: a checker that cannot fail is worse than no checker,
# because its green is read as conformance.
- run: python3 scripts/tests/kanban-columns-selftest.py

check:
name: Written Status names exist on the board
# Gate on selftest: a conformance PASS emitted by a checker whose own
# failure paths have regressed is a false green (the checker can't fail, so
# its success means nothing). Sibling guards caller-drift + standards-sync
# gate their audit on selftest for exactly this reason. (Bugbot, #247.)
needs: selftest
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Check
env:
# Reads the org project's Status field.
GH_TOKEN: ${{ secrets.PROJECTS_KANBAN_TOKEN }}
run: python3 scripts/kanban-columns-check.py
Loading
Loading