Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (pull_request) Successful in 41s
CI / Python Lint & Test (pull_request) Successful in 39s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 39s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 46s
ci-required-drift / drift (pull_request) Successful in 52s
Concierge Creates Workspace Hermetic / Concierge Creates Workspace Hermetic (pull_request) Successful in 53s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 18s
lint-bp-context-emit-match / lint-bp-context-emit-match (pull_request) Successful in 56s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 56s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 22s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 29s
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (pull_request) Successful in 31s
lint-no-coe-on-required / lint-no-coe-on-required (pull_request) Successful in 38s
Lint schedule budget / Zero-cron ratchet (pull_request) Successful in 34s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 45s
lint-phantom-gate / lint-phantom-gate (pull_request) Successful in 54s
Lint publish-runner timeout-minutes / Lint publish-runner timeout-minutes (pull_request) Successful in 56s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 53s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 5s
lint-setup-go-cache / lint-setup-go-cache (pull_request) Successful in 48s
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (pull_request) Successful in 42s
Lint workflow YAML (repository compatibility policy) / Lint workflow YAML for repository compatibility policy (pull_request) Successful in 36s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 25s
E2E Chat / E2E Chat (pull_request) Successful in 2m19s
sdk-route-milestone-contract-drift / SDK route + milestone derive-gates (pull_request) Successful in 36s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 53s
E2E API Smoke Test / detect-changes (pull_request) Successful in 2m51s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 1m10s
lint-env-coupling-dismissal / lint-env-coupling-dismissal (pull_request) Successful in 2m59s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 2m55s
CI / Detect changes (pull_request) Successful in 3m50s
E2E Ephemeral CP Happy Path / detect (pull_request) Successful in 3m49s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 2m55s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 4s
template-delivery-e2e / detect-changes (pull_request) Successful in 3m34s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 1m2s
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (pull_request) Successful in 4s
CI / Platform (Go) (pull_request) Successful in 5m14s
CI / Canvas (Next.js) (pull_request) Successful in 5m28s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6m11s
CI / Canvas Deploy Status (pull_request) Successful in 3s
CI / all-required (pull_request) Successful in 10s
E2E Ephemeral CP Happy Path / E2E Ephemeral CP Happy Path (pull_request) Successful in 27m37s
gitea-merge-queue / queue (pull_request_review) Successful in 21s
Branch protection on main is `status_check_contexts: ["*"]`. Verified against
Gitea 1.26 source (`modules/commitstatus.Combine`): a `skipped` status is
counted toward the success tally, so a required check that STOPS RUNNING
merges GREEN. And the wildcard glob can only evaluate contexts that are
actually POSTED — it has no way to demand that a specific context exist, so a
context that vanishes is simply invisible to it.
`gitea-merge-queue.py` is the only thing that fail-closes on `skipped`
(`enforced_file_contexts_green`). It is not the only way a PR reaches main:
of the 101 PRs merged since 2026-07-14, 3 were merged by the queue actor and
98 were merged through the UI/API, gated by `["*"]` alone.
The existing guards each cover a different way a gate goes quiet, and none
covers this one:
* lint-required-no-paths — no `on: paths:` filter on a required workflow.
* Guard F (phantom-gate) — the producer WORKFLOW is Actions-`active`.
* Guard G — a disabled workflow must not still fire on PRs.
* lint_no_coe_on_required — no `continue-on-error` masking a red.
* ci-required-drift F1/F1b— sentinel `needs:` <-> ci.yml job set lockstep.
Guard S adds the missing property: every ENFORCED entry in
`.gitea/required-contexts.txt` must resolve to a real JOB whose transitive
`needs:` closure carries NO job-level `if:`.
S1 — the context names a job that does not exist (rename/delete). Guard F
resolves only the producer WORKFLOW, so a job rename leaves it green.
S2 — the producer job, or anything in its `needs:` closure, has a job-level
`if:`. A false `if:` skips that job, and Gitea does not start a
dependent whose `needs:` was skipped — the skip propagates to the
enforced context.
S2 generalises a live observation. On PRs #4911 and #5016, `CI / Platform
(Go)` did not succeed, so Gitea never started `CI / all-required` and posted
`skipped` for it (run 593358, job 879118 failure -> job 879124 skipped). Those
two were held only because the FAILURE was also posted and `["*"]` does block
on a posted failure. Replace the failure with a skip — put a job-level `if:`
on any of the sentinel's six `needs:` — and the board is all-green-or-skipped
with the CI aggregator having enforced nothing.
Note the deliberate asymmetry this closes: ci_job_names() (F1) EXCLUDES
`if:`-gated jobs from the set that must appear under the sentinel's `needs:`,
because event-scoped jobs legitimately do not run on every trigger. That
exclusion says nothing about whether such a job may BE a need — and nothing
forbade it. Guard S forbids it.
Fail-before proof (both mutations run against the REAL tree, not a fixture):
* job-level `if: github.event_name == 'push'` on `canvas-deploy-status`
(a real `all-required` need) -> Guard S exit 1, naming the job.
F1/F1b both still PASS on that same tree: the `if:` removes the job from
ci_job_names() so F1 does not demand it, while it remains in `needs:` and
F1b only checks existence.
* renaming the `Secret scan` job -> Guard S exit 1 (S1). Restored -> exit 0.
Baseline on unmodified main: exit 0, all 8 ENFORCED entries clean, so this is
green on main today and wedges nothing.
Guard S needs NO credentials (it reads only checked-in files), so it runs as
the FIRST step of the existing meta-guard job, before the Infisical fetch — a
KMS outage must not be able to stop it from running.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
215 lines
12 KiB
YAML
215 lines
12 KiB
YAML
# lint-phantom-gate — Guard F (regression-prevention-guards.md §Guard F),
|
|
# plus Guard G (disabled PR emitters) and Guard S (skippable enforced gates).
|
|
#
|
|
# The three answer three different ways an ENFORCED required context stops
|
|
# being a gate while the board still reads green:
|
|
# Guard F — its producer WORKFLOW is toggled off, so it never posts.
|
|
# Guard G — a DISABLED workflow still fires on PR events, posting statuses
|
|
# that cannot be rerun (wedge).
|
|
# Guard S — its producer JOB is gone, or the job can be `skipped` because
|
|
# something in its `needs:` closure carries a job-level `if:`.
|
|
# `["*"]` branch protection treats `skipped` as PASSING.
|
|
#
|
|
# The META-guard that guards the gates. A `.gitea/required-contexts.txt` entry
|
|
# declared ENFORCED (merge-blocking) whose PRODUCER workflow is toggled off
|
|
# (`disabled_manually` / `disabled`) is a PHANTOM GATE: under the `[*]` all-green
|
|
# branch protection the disabled workflow never posts its status, so the gate is
|
|
# required-on-paper but silently enforces NOTHING. This is the blind spot that
|
|
# let the mgmt-MCP / provision regressions reach prod uncaught (the enforced
|
|
# `E2E Staging SaaS ... Concierge Creates Workspace` gate's producer had been
|
|
# disabled since the 2026-06-27 AWS toggle-off).
|
|
#
|
|
# This lint reads the SAME enforced set the merge queue enforces
|
|
# (`load_enforced_file_contexts`, everything above the first `# pending-#NNNN`
|
|
# marker) and, for each entry, asserts its producer workflow's Gitea Actions
|
|
# state is `active`. It FAILS RED on a phantom.
|
|
#
|
|
# TRIGGERS — PROMOTED to a PR-time gate (regression-prevention-guards.md + #3467):
|
|
# - `pull_request` — the promotion. Under the `[*]` all-green branch protection
|
|
# a red status on the PR head blocks the merge, so running Guard F on PRs
|
|
# makes a phantom (an ENFORCED required-context whose producer is disabled /
|
|
# missing) RED-BLOCK the PR BEFORE it lands, instead of only being detected
|
|
# post-merge on push:[main]. This was gated behind "phantom must be resolved
|
|
# first" (a live phantom would RED-block every open PR = repo-wide wedge);
|
|
# that precondition now HOLDS — e2e-staging-saas was re-enabled (Guard B /
|
|
# #3464 landed, task #231/#232) so all ENFORCED producers are `active` and
|
|
# Guard F is GREEN on main. It is safe to run on every relevant PR because it
|
|
# PASSES on a clean main and only goes red on a REAL phantom — which SHOULD
|
|
# block merges until the producer is re-enabled or the context is parked
|
|
# below a `# pending-#NNNN` marker. Scoped with `paths:` to the files that
|
|
# can INTRODUCE a phantom in a PR diff (required-contexts.txt SSOT, any
|
|
# workflow file, the detector script, this test) so it does not run on every
|
|
# unrelated PR; the out-of-band "producer toggled off via the Actions API"
|
|
# case (not a file change) stays covered by the push:[main] detector below.
|
|
# - `push: [main]` — regression detector on the branch where a workflow
|
|
# toggle-off actually lands. A red on main is TRUE signal surfaced to
|
|
# main-red-watchdog.
|
|
# - `workflow_dispatch` — on-demand audit.
|
|
#
|
|
# NOT-A-WEDGE contract: Guard F is NOT listed in `.gitea/required-contexts.txt`
|
|
# and NOT an individual BP context, so the merge queue never WAITS for it; under
|
|
# `[*]` only POSTED statuses are required, so a `paths`-skipped PR (no status
|
|
# posted) is not blocked. A green Guard F is the common case; a red one is a real
|
|
# phantom that SHOULD hold the queue. (The `bp-required: pending #3467` directive
|
|
# stays `pending` — the literal context can never be individually listed while BP
|
|
# is the `[*]` wildcard; the wildcard itself provides the enforcement.)
|
|
#
|
|
# FAIL-BEFORE proof (verified live, HTTPS = CI path): flags
|
|
# `E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Creates Workspace`
|
|
# (producer disabled_manually) RED while the re-enabled hermetic gate PASSES.
|
|
# TRIGGER regression is covered by test_phantom_gate_lint.py
|
|
# (`test_guard_f_runs_on_pull_request` / `test_guard_f_keeps_push_main_and_dispatch`).
|
|
#
|
|
# Behaviour-based gate: the decision logic lives in ci-required-drift.py
|
|
# (`--phantom-gate-lint`) with fail-before/pass-after unit tests in
|
|
# .gitea/scripts/tests/test_phantom_gate_lint.py.
|
|
#
|
|
# IMPORTANT — repository flat-dispatch policy: do NOT add an `inputs:` block
|
|
# to `workflow_dispatch:` without a coordinated policy/caller migration.
|
|
|
|
name: lint-phantom-gate
|
|
|
|
on:
|
|
# PR-time gate (#3467 promotion). Scoped to the files that can INTRODUCE a
|
|
# phantom in a PR diff: the enforced-contexts SSOT, any workflow file (a
|
|
# rename/delete of a producer is a P2 phantom), the detector itself, and this
|
|
# regression test. Under the `[*]` all-green BP a red here blocks the PR.
|
|
# PATHS FILTER REMOVED 2026-07-14.
|
|
#
|
|
# Guards F and G do not read the PR's diff — they read repo-GLOBAL state: the
|
|
# enforced-contexts SSOT and, critically, the Gitea Actions **workflow states**
|
|
# (active / disabled_manually), which are changed through the API or the web UI
|
|
# and therefore change WITHOUT ANY COMMIT AT ALL.
|
|
#
|
|
# A paths filter on a guard whose input is out-of-band state is a guard that
|
|
# cannot fire when it matters. Someone disables a workflow in the UI; every open
|
|
# PR is now wedgeable by an unrerunnable status; and this lint stays silent until
|
|
# somebody happens to touch a file under .gitea/. The condition it detects is
|
|
# global, so its trigger must be global.
|
|
#
|
|
# Cost is one fast Actions-API read per PR. That is the correct price for a gate
|
|
# that tells a PR it is about to be wedged BEFORE it is wedged.
|
|
pull_request:
|
|
types: [opened, synchronize, reopened]
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
# Read the Actions API (workflow states) + local SSOT file. No writes.
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: lint-phantom-gate-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
# REAL gate (not bp-exempt). It now emits on pull_request, so under the `[*]`
|
|
# wildcard BP its status is merge-blocking. The directive stays `pending` — not
|
|
# `yes` — because Tier 2g (lint-required-context-exists-in-bp) validates
|
|
# `bp-required: yes` by requiring the LITERAL context in
|
|
# branch_protections.status_check_contexts, which is `['*']` here; the wildcard
|
|
# already enforces it, so the literal listing is intentionally never added.
|
|
# Directive must stay within 3 lines of the job key:
|
|
# bp-required: pending #3467
|
|
lint-phantom-gate:
|
|
name: lint-phantom-gate
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Check out repo (we read required-contexts.txt locally)
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- name: Set up Python
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: '3.12'
|
|
# PyYAML is imported at module top of ci-required-drift.py; install it even
|
|
# though the phantom-lint path itself does not parse YAML, so the import
|
|
# succeeds.
|
|
- name: Install PyYAML
|
|
run: python -m pip install --quiet 'PyYAML==6.0.2'
|
|
|
|
# Guard S — runs FIRST and needs NO credentials.
|
|
#
|
|
# Guard F proves an ENFORCED context's producer WORKFLOW is active.
|
|
# Guard S proves the context can actually REACH A TERMINAL, NON-SKIPPED
|
|
# status: its producer JOB still exists, and no job in that job's
|
|
# transitive `needs:` closure carries a job-level `if:` (a false `if:`
|
|
# makes the job `skipped`, and Gitea does not start a dependent whose
|
|
# `needs:` was skipped — the skip propagates to the enforced context).
|
|
#
|
|
# Why that matters: branch protection here is
|
|
# `status_check_contexts: ["*"]`, which treats `skipped` as PASSING and
|
|
# cannot require a context that never posts. gitea-merge-queue.py is the
|
|
# only thing that fail-closes on `skipped` — and it performs a small
|
|
# minority of merges; everything merged through the UI/API is gated by
|
|
# `["*"]` alone. So a skippable required gate is a gate that enforces
|
|
# nothing on most merges.
|
|
#
|
|
# Placed BEFORE the Infisical fetch on purpose: Guard S reads only
|
|
# checked-in files, so an Infisical/KMS outage must not be able to stop
|
|
# it from running.
|
|
- name: Run Guard S — enforced gates must not be able to post `skipped`
|
|
env:
|
|
ENFORCED_CONTEXTS_FILE: .gitea/required-contexts.txt
|
|
run: python3 .gitea/scripts/ci-required-drift.py --skippable-gate-lint
|
|
# KMS consolidation (mirrors ci-required-drift.yml / lint-required-no-paths.yml):
|
|
# fetch the consolidated DRIFT_BOT_TOKEN from the Infisical SSOT (prod
|
|
# /shared/drift-bot) via the CI machine identity. Reading the Actions API
|
|
# workflow-state list requires an authenticated token; the auto GITHUB_TOKEN
|
|
# is 401 on this endpoint. molecule-core PRs are same-repo (not forks), so
|
|
# the INFISICAL_CI_* machine-identity secrets are present on pull_request.
|
|
- name: Fetch DRIFT_BOT_TOKEN from Infisical SSOT
|
|
env:
|
|
INFISICAL_CI_CLIENT_ID: ${{ secrets.INFISICAL_CI_CLIENT_ID }}
|
|
INFISICAL_CI_CLIENT_SECRET: ${{ secrets.INFISICAL_CI_CLIENT_SECRET }}
|
|
INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_CI_PROJECT_ID }}
|
|
run: |
|
|
set -uo pipefail
|
|
BASE="https://key.moleculesai.app"
|
|
TOK=$(curl -fsS -X POST "$BASE/api/v1/auth/universal-auth/login" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "{\"clientId\":\"$INFISICAL_CI_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CI_CLIENT_SECRET\"}" \
|
|
| python3 -c 'import sys,json; v=json.load(sys.stdin).get("accessToken"); sys.stdout.write(v if isinstance(v,str) else "")')
|
|
if [ -z "$TOK" ]; then echo "::error::Infisical accessToken empty - failing closed"; exit 1; fi
|
|
read_secret() {
|
|
curl -fsS "$BASE/api/v3/secrets/raw/$1?workspaceId=$INFISICAL_PROJECT_ID&environment=prod&secretPath=$2" \
|
|
-H "Authorization: Bearer $TOK" \
|
|
| python3 -c 'import sys,json; v=json.load(sys.stdin).get("secret",{}).get("secretValue"); sys.stdout.write(v if isinstance(v,str) else "")'
|
|
}
|
|
DRIFT_BOT_TOKEN=$(read_secret DRIFT_BOT_TOKEN %2Fshared%2Fdrift-bot)
|
|
echo "::add-mask::$DRIFT_BOT_TOKEN"
|
|
if [ -z "$DRIFT_BOT_TOKEN" ]; then
|
|
echo "::error::Infisical returned empty DRIFT_BOT_TOKEN at prod /shared/drift-bot — failing closed."
|
|
exit 1
|
|
fi
|
|
echo "DRIFT_BOT_TOKEN=$DRIFT_BOT_TOKEN" >> "$GITHUB_ENV"
|
|
echo "DRIFT_BOT_TOKEN loaded from Infisical (len=${#DRIFT_BOT_TOKEN})"
|
|
- name: Run Guard F phantom-gate lint
|
|
env:
|
|
# DRIFT_BOT_TOKEN reads the Actions API (workflow states). Same
|
|
# least-privilege persona as ci-required-drift.yml.
|
|
GITEA_TOKEN: ${{ env.DRIFT_BOT_TOKEN }}
|
|
GITEA_HOST: git.moleculesai.app
|
|
REPO: ${{ github.repository }}
|
|
ENFORCED_CONTEXTS_FILE: .gitea/required-contexts.txt
|
|
run: python3 .gitea/scripts/ci-required-drift.py --phantom-gate-lint
|
|
|
|
# Guard G — the INVERSE of Guard F, and the one that actually wedges.
|
|
#
|
|
# Guard F: an ENFORCED context whose producer is disabled → required on
|
|
# paper, silently unenforced. (Nothing blocks; protection is a lie.)
|
|
# Guard G: a DISABLED workflow that still fires on PR events → it keeps
|
|
# posting statuses Gitea will not let you rerun
|
|
# (400 "workflow is disabled"), so under the `[*]` branch
|
|
# protection the PR is stuck with no API path out.
|
|
#
|
|
# Runs in the same job to reuse the DRIFT_BOT_TOKEN fetched above — the
|
|
# Actions API workflow-state read is the same call both guards need.
|
|
- name: Run Guard G — disabled workflows must not still fire on PR events
|
|
env:
|
|
GITEA_TOKEN: ${{ env.DRIFT_BOT_TOKEN }}
|
|
GITEA_HOST: git.moleculesai.app
|
|
REPO: ${{ github.repository }}
|
|
run: python3 .gitea/scripts/ci-required-drift.py --disabled-pr-emitter-lint
|