166 lines
7.0 KiB
YAML
166 lines
7.0 KiB
YAML
name: lint-bp-context-emit-match
|
|
|
|
# Tier 2f scheduled lint (per mc#1982) — detects drift between
|
|
# `branch_protections/<branch>.status_check_contexts` and the set of
|
|
# contexts emitted by `.gitea/workflows/*.yml`.
|
|
#
|
|
# Rule
|
|
# ----
|
|
# For each protected branch context (Source A — BP), there must exist
|
|
# at least one emitting workflow + job pair (Source B — workflow YAML
|
|
# + on:-event mapping) whose runtime status-name maps to it. The
|
|
# inverse direction (emitter without BP context) is informational
|
|
# only — Tier 2g handles that at PR-time.
|
|
#
|
|
# Why this exists
|
|
# ---------------
|
|
# A BP-required context with no emitter blocks merges forever: this repository's
|
|
# protection gate treats an absent context as `pending`, not `skipped`. The
|
|
# phantom-required-check class previously surfaced as
|
|
# `feedback_phantom_required_check_after_gitea_migration` (a port
|
|
# kept the GitHub context name after rename to Gitea, but no
|
|
# workflow emitted under the new name).
|
|
#
|
|
# This lint catches the same class structurally + a forward case:
|
|
# workflow renamed/deleted while still in BP.
|
|
#
|
|
# Scope
|
|
# -----
|
|
# Scheduled daily. We DON'T run on `pull_request` because (a) the
|
|
# emitter side moves with PR diffs (transitional state false-flags)
|
|
# and (b) Tier 2g handles emitter-side drift at PR-time.
|
|
#
|
|
# Cross-repo
|
|
# ----------
|
|
# Today this runs only on molecule-core/main. Per internal#349
|
|
# (cross-repo BP sweep) Class-D repos will get the same lint after
|
|
# their BP rollouts.
|
|
#
|
|
# Auth
|
|
# ----
|
|
# The token used for `GET /repos/.../branch_protections/{branch}` needs
|
|
# repo-admin access. We use DRIFT_BOT_TOKEN (same persona as
|
|
# ci-required-drift.yml — `internal#329` provisioning trail).
|
|
# Graceful-degrade per Tier 2a contract: 403/404 → exit 0 with
|
|
# ::error::.
|
|
#
|
|
# Idempotency
|
|
# -----------
|
|
# The drift issue is filed with title prefix
|
|
# `[ci-bp-drift] {repo}/{branch}: BP→emitter mismatch`. The script
|
|
# searches OPEN issues for an exact title-prefix match and PATCHes
|
|
# the existing issue (if any) instead of POSTing a duplicate.
|
|
# Mirrors `ci-required-drift.py`'s contract.
|
|
#
|
|
# Phase contract (RFC internal#219 §1 ladder)
|
|
# -------------------------------------------
|
|
# Lands at `continue-on-error: true` (Phase 3). After 7 days of clean
|
|
# scheduled runs on `main`, flip to `false` so a scheduled failure
|
|
# becomes a hard CI signal.
|
|
#
|
|
# Cross-links
|
|
# -----------
|
|
# - mc#1982 (the RFC that specs this lint)
|
|
# - internal#349 (cross-repo BP sweep)
|
|
# - feedback_phantom_required_check_after_gitea_migration
|
|
# - feedback_tier_label_ids_are_per_repo
|
|
# - ci-required-drift.yml (F2 detector, narrower-scope sibling)
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
# No `push` / `pull_request` here — Tier 2g owns PR-time drift.
|
|
pull_request:
|
|
types: [opened, synchronize, reopened]
|
|
paths:
|
|
- '.gitea/workflows/**'
|
|
- '.gitea/scripts/**'
|
|
push:
|
|
branches: [main, staging]
|
|
paths:
|
|
- '.gitea/workflows/**'
|
|
- '.gitea/scripts/**'
|
|
|
|
env:
|
|
GITHUB_SERVER_URL: https://git.moleculesai.app
|
|
|
|
permissions:
|
|
contents: read
|
|
issues: write # needed to file/edit the drift issue
|
|
|
|
concurrency:
|
|
group: lint-bp-context-emit-match-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
lint:
|
|
name: lint-bp-context-emit-match
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
# Phase 4 (RFC #219 §1): 22 days green since 2026-05-11 port,
|
|
# well past the 7-clean-run threshold. Scheduled failure is now
|
|
# a hard CI signal.
|
|
continue-on-error: false
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: '3.12'
|
|
- name: Install PyYAML
|
|
run: python -m pip install --quiet 'PyYAML==6.0.2'
|
|
# KMS consolidation (#971/#3274 pattern): the lint below previously read
|
|
# the per-bot Gitea Actions secret `DRIFT_BOT_TOKEN`. We now fetch the
|
|
# consolidated DRIFT_BOT_TOKEN from the Infisical SSOT (prod
|
|
# /shared/drift-bot) via the CI machine identity and export it to
|
|
# $GITHUB_ENV. The lint keeps reading it under the GITEA_TOKEN env var
|
|
# name it already expects, so the script is unchanged. The old
|
|
# DRIFT_BOT_TOKEN Gitea secret is left in place until a green run
|
|
# validates this path (validate-before-delete).
|
|
#
|
|
# Trust boundary: this workflow runs only on schedule / workflow_dispatch
|
|
# (trusted events), so the INFISICAL_CI_* machine-identity secrets are
|
|
# available — an untrusted fork PR cannot reach this step.
|
|
- 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 lint-bp-context-emit-match
|
|
env:
|
|
# DRIFT_BOT_TOKEN — repo-admin on this repo (internal#329
|
|
# provisioning trail). Required for branch_protections read.
|
|
# KMS consolidation: now arrives via $GITHUB_ENV from the fetch step
|
|
# above (was secrets.DRIFT_BOT_TOKEN).
|
|
GITEA_TOKEN: ${{ env.DRIFT_BOT_TOKEN }}
|
|
GITEA_HOST: git.moleculesai.app
|
|
REPO: ${{ github.repository }}
|
|
BRANCH: main
|
|
WORKFLOWS_DIR: .gitea/workflows
|
|
DRIFT_LABEL: ci-bp-drift
|
|
GITHUB_RUN_URL: https://git.moleculesai.app/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
run: python3 .gitea/scripts/lint_bp_context_emit_match.py
|
|
- name: Run lint-bp-context-emit-match unit tests
|
|
run: |
|
|
python -m pip install --quiet pytest
|
|
python3 -m pytest tests/test_lint_bp_context_emit_match.py -v
|