Files
molecule-core/.gitea/workflows/lint-phantom-gate.yml
T
gitea-agent-botandClaude Opus 5 6563d5a661
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
fix(ci): Guard S — an ENFORCED required context must not be able to post skipped
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>
2026-08-03 00:48:57 -07:00

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