Files
molecule-core/.gitea/workflows/pin-provenance-guard.yml
T
devops-engineer 27123ef4a1
Check migration collisions / Migration version collision check (pull_request) Skipped
SECRET_PATTERNS drift lint / Detect SECRET_PATTERNS drift (pull_request) Skipped
Block internal-flavored paths / Block forbidden paths (push) Skipped
CI / Canvas Deploy Status (push) Skipped
CI / Python Lint & Test (push) Skipped
Concierge Creates Workspace Hermetic / Concierge Creates Workspace Hermetic (push) Skipped
E2E Chat / E2E Chat (push) Skipped
E2E Ephemeral CP Happy Path / E2E Ephemeral CP Happy Path (push) Skipped
Handlers Postgres Integration / Handlers Postgres Integration (push) Skipped
Harness Replays / detect-changes (push) Skipped
lint-continue-on-error-tracking / lint-continue-on-error-tracking (push) Skipped
lint-env-coupling-dismissal / lint-env-coupling-dismissal (push) Skipped
lint-no-coe-on-required / lint-no-coe-on-required (push) Skipped
lint-phantom-gate / lint-phantom-gate (push) Skipped
Lint publish-runner timeout-minutes / Lint publish-runner timeout-minutes (push) Skipped
Lint schedule budget / Zero-cron ratchet (push) Skipped
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (push) Skipped
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (push) Skipped
main-canary / canary (push) Skipped
merge-commit-guard / Verify merged PR commits are on main (push) Skipped
nodup-lint / no-duplication (push) Skipped
pin-provenance-guard / Pin provenance (push) Skipped
publish-canvas-image / Build & push canvas image (push) Skipped
publish-desktop-image / Build & push desktop image (push) Skipped
publish-desktop-image / Promote desktop :latest to CI-green build (push) Skipped
publish-workspace-server-image / Build & push tenant image (push) Skipped
publish-workspace-server-image / Promote tenant :latest to CI-green build (push) Skipped
runtime-default-flip-gate / flip-gate (push) Skipped
sdk-pin-drift / SDK consumer pin vs molecule-ai-sdk main (push) Skipped
sdk-route-milestone-contract-drift / SDK route + milestone derive-gates (push) Skipped
SECRET_PATTERNS drift lint / Detect SECRET_PATTERNS drift (push) Skipped
Secret scan / Scan diff for credential-shaped strings (push) Skipped
Self-Host Concierge Schedules E2E / Self-Host Concierge Schedules E2E (stub) (push) Skipped
staging-tenant-cd / await-image (staging-<sha> published) (push) Skipped
staging-tenant-cd / advance-pin (staging tenant image DB pin) (push) Skipped
staging-tenant-cd / redeploy-fleet (roll staging tenants, session-preserving) (push) Skipped
staging-tenant-cd / e2e-smoke (staging real provision + A2A HARD GATE) (push) Skipped
staging-tenant-cd / rollback-pin (revert pin if the fleet roll failed) (push) Skipped
sync-providers-yaml / Verify SDK registry dependency (push) Skipped
template-delivery-e2e / detect-changes (push) Skipped
Ops Scripts Tests / Ops scripts (unittest) (push) Skipped
verify-providers-gen / Regenerate providers artifact and fail on drift (push) Skipped
boot-to-registration-e2e / Minimal cell (SSOT runtime + molecules-server) (push) Skipped
ci-required-drift / drift (push) Skipped
CI / Platform (Go) (push) Skipped
CI / Shellcheck (E2E scripts) (push) Skipped
CI / all-required (push) Skipped
E2E API Smoke Test / E2E API Smoke Test (push) Skipped
E2E Ephemeral CP Happy Path / detect (push) Skipped
E2E Workspace Lifecycle (staginge2e) / E2E Workspace Lifecycle (compile+skip) (push) Skipped
Handlers Postgres Integration / detect-changes (push) Skipped
Harness Replays / Harness Replays (push) Skipped
lint-bp-context-emit-match / lint-bp-context-emit-match (push) Skipped
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (push) Skipped
CI / Detect changes (push) Skipped
CI / Canvas (Next.js) (push) Skipped
contract-ssot-sync / Verify vendored plugin-manifest schema matches the molecule-ai-sdk SSOT (push) Skipped
cron-fixtures-ssot-drift / Verify vendored cron fixtures are byte-identical to the molecule-ai-sdk SSOT (push) Skipped
design-token-drift / Canvas ↔ app design-token SSOT drift (push) Skipped
E2E API Smoke Test / detect-changes (push) Skipped
E2E External Connect-Snippet + Leak-Lint / E2E External Connect-Snippet + Leak-Lint (push) Skipped
Lint curl status-code capture / Scan workflows for curl status-capture pollution (push) Skipped
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (push) Skipped
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (push) Skipped
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (push) Skipped
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (push) Skipped
lint-setup-go-cache / lint-setup-go-cache (push) Skipped
Lint workflow YAML (repository compatibility policy) / Lint workflow YAML for repository compatibility policy (push) Skipped
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (push) Skipped
mcp-plugin-delivery-contract-drift / Compare MCP plugin delivery contract against template and runtime canonicals (push) Skipped
mcp-verb-published-manifest / Published mcp-server manifest ⊇ contract required verbs (push) Skipped
provision-request-ssot-sync / Verify core's provision-request mirror matches the molecule-ai-sdk SSOT (push) Skipped
staging-tenant-cd / runtime-image-readiness (exact promoted digests on provisioner daemon) (push) Skipped
staging-tenant-cd / rollback-audit (prove rollback-pin itself actually executed) (push) Skipped
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Skipped
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (push) Skipped
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (pull_request) Successful in 5s
CI / Python Lint & Test (pull_request) Successful in 5s
CI / Detect changes (pull_request) Successful in 8s
E2E API Smoke Test / detect-changes (pull_request) Successful in 8s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 4s
E2E Ephemeral CP Happy Path / detect (pull_request) Successful in 7s
boot-to-registration-e2e / Minimal cell (SSOT runtime + molecules-server) (pull_request) Skipped
contract-ssot-sync / Verify vendored plugin-manifest schema matches the molecule-ai-sdk SSOT (pull_request) Skipped
cron-fixtures-ssot-drift / Verify vendored cron fixtures are byte-identical to the molecule-ai-sdk SSOT (pull_request) Skipped
design-token-drift / Canvas ↔ app design-token SSOT drift (pull_request) Skipped
E2E External Connect-Snippet + Leak-Lint / E2E External Connect-Snippet + Leak-Lint (pull_request) Skipped
E2E Workspace Lifecycle (staginge2e) / E2E Workspace Lifecycle (compile+skip) (pull_request) Skipped
Harness Replays / detect-changes (pull_request) Skipped
Harness Replays / Harness Replays (pull_request) Skipped
manifest-entry-existence-check / Verify manifest entries exist on Gitea (pull_request) Skipped
mcp-plugin-delivery-contract-drift / Compare MCP plugin delivery contract against template and runtime canonicals (pull_request) Skipped
mcp-verb-published-manifest / Published mcp-server manifest ⊇ contract required verbs (pull_request) Skipped
nodup-lint / no-duplication (pull_request) Skipped
provision-request-ssot-sync / Verify core's provision-request mirror matches the molecule-ai-sdk SSOT (pull_request) Skipped
Self-Host Concierge Schedules E2E / Self-Host Concierge Schedules E2E (stub) (pull_request) Skipped
sync-providers-yaml / Verify SDK registry dependency (pull_request) Skipped
verify-providers-gen / Regenerate providers artifact and fail on drift (pull_request) Skipped
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 4s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 4s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (pull_request) Successful in 4s
Concierge Creates Workspace Hermetic / Concierge Creates Workspace Hermetic (pull_request) Successful in 20s
lint-env-coupling-dismissal / lint-env-coupling-dismissal (pull_request) Successful in 20s
lint-bp-context-emit-match / lint-bp-context-emit-match (pull_request) Successful in 25s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 25s
lint-mask-pr-atomicity / lint-mask-pr-atomicity (pull_request) Successful in 20s
lint-no-coe-on-required / lint-no-coe-on-required (pull_request) Successful in 18s
lint-phantom-gate / lint-phantom-gate (pull_request) Successful in 17s
Lint publish-runner timeout-minutes / Lint publish-runner timeout-minutes (pull_request) Successful in 16s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 17s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 4s
ci-required-drift / drift (pull_request) Successful in 40s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 3s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 3s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 22s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 15s
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (pull_request) Successful in 21s
Lint workflow YAML (repository compatibility policy) / Lint workflow YAML for repository compatibility policy (pull_request) Successful in 22s
lint-setup-go-cache / lint-setup-go-cache (pull_request) Successful in 25s
Lint schedule budget / Zero-cron ratchet (pull_request) Successful in 27s
Secret scan / Scan diff for credential-shaped strings (pull_request) No credential-shaped strings (deterministic re-assert)
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 8s
sdk-route-milestone-contract-drift / SDK route + milestone derive-gates (pull_request) Successful in 18s
template-delivery-e2e / detect-changes (pull_request) Successful in 9s
sdk-pin-drift / SDK consumer pin vs molecule-ai-sdk main (pull_request) Successful in 19s
pin-provenance-guard / Pin provenance (pull_request) Successful in 21s
lint-context-set-shrink / lint-context-set-shrink (pull_request) Successful in 1m33s
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (pull_request) Successful in 2s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 1m26s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Failing after 1m58s
gitea-merge-queue / queue (pull_request_review) Successful in 19s
E2E Chat / E2E Chat (pull_request) Successful in 3m46s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 1m3s
CI / Platform (Go) (pull_request) Canceled after 4m26s
CI / Canvas (Next.js) (pull_request) Canceled after 4m23s
CI / Shellcheck (E2E scripts) (pull_request) Canceled after 4m23s
CI / Canvas Deploy Status (pull_request) Canceled after 0s
CI / all-required (pull_request) Canceled after 0s
E2E Ephemeral CP Happy Path / E2E Ephemeral CP Happy Path (pull_request) Successful in 26m27s
fix(release): promote staging-qualified images independently of inventory drift
2026-10-11 18:29:14 -07:00

315 lines
16 KiB
YAML

name: pin-provenance-guard
# THE MECHANISM THAT REPLACES "we always bump pins through CI".
#
# Two pins decide what runs in production and both could be moved by hand:
#
# SDK pin workspace-server/go.mod + the paired canonicalRegistrySHA256
# checkpoint. Moving it by hand is four commands, and the repo
# PRINTS those four commands at you — check_sdk_pin_drift.py's
# STUCK remediation and sync_canonical_test.go's failure message
# both spell out the manual equivalent. It has been done for
# real: 45a61bb on 2026-08-05, four minutes after sdk-pin-bump
# run 620174 went green having done nothing.
#
# Tenant pin the runtime_image_pins row the control plane serves. Moving it
# by hand is one curl, documented in
# molecule-controlplane/docs/operations/provider-aware-tenant-image-rollout.md
# and endorsed in this repo's own promote lane ("Reconcile that
# case by running scripts/deploy/advance-staging-tenant-pin.sh
# directly"). The live prod row read on 2026-08-09 was a hand
# write: notes were a sentence a person typed.
#
# WHY A GUARD AND NOT A CONVENTION. `promoted_by` is not evidence — CI and an
# operator's laptop present the SAME CP admin token, so both render as
# `api-token-<8 hex>`. Nothing in either repo inspected a pin's provenance
# before this file existed. A convention with no mechanism is prose.
#
# ---------------------------------------------------------------------------
# THE TWO HALVES ARE NOT EQUALLY STRONG. READ THIS BEFORE TRUSTING THE GATE.
#
# SDK pin PREVENTED. The edit is a commit, the guard runs on the diff,
# and a PR carrying an unattributed pin edit cannot go green. The
# bad state never lands.
#
# TENANT pin DETECTED, not prevented. The blessed writer
# (scripts/deploy/advance-staging-tenant-pin.sh) refuses without a
# CI-minted stamp, so the documented "just run the script
# directly" path is closed. But a raw
# curl -X POST …/cp/admin/runtime-image/promote
# STILL SUCCEEDS. The control plane accepts it: the promote
# dispatcher has a `GuardPromote` hook designed for exactly this
# (molecule-controlplane internal/handlers/admin_pin_handler.go),
# and `runtimeImagePinResource` deliberately leaves it unset
# ("No GuardPromote: the molecule-tenant pin promotes through the
# SAME ungated authenticated-admin path…", pin_runtime_image.go).
# Until that hook rejects an unstamped molecule-tenant promote,
# this lane catches a hand write AFTER it has already changed what
# fresh provisions get — on the next operational audit dispatch,
# not at the moment of the write.
#
# Do not read the word "blocking" below as "prevented" for the tenant half. It
# means the audit's verdict blocks; it does not mean the write was stopped.
# ---------------------------------------------------------------------------
#
# WHAT EACH ARM DOES
#
# sdk-pin (every event, BLOCKING) lint_sdk_pin_provenance.py over
# base..head. Fires only when the diff moves an SDK pin LINE, so it cannot
# wedge unrelated PRs, and when it fires it demands a Pin-Provenance
# trailer citing a real sdk-pin-bump.yml run — resolved through the
# Actions API, not believed from the note.
#
# self-test (every event, BLOCKING) pytest over both guards' disposition
# matrices, in the SAME job. Copied deliberately from sdk-pin-drift.yml:
# if the classifier regresses into "never blocks", THIS step goes red even
# though the arm above is green. Without it a broken guard and a healthy
# repo look identical.
#
# tenant-pin audit (manual dispatch, operational) reads the live
# (molecule-tenant, global) row from EVERY production control plane and
# requires a provenance stamp on it.
#
# WHY THE LIVE AUDIT DOES NOT RUN ON pull_request. It needs the prod CP admin
# token. Putting a cross-tenant admin credential in a job whose next step runs
# code from the PR's own tree is the exact trade this repository already
# rejected once, in writing, in required-contexts.txt ("a branch push is not a
# review"). The PR arm is therefore the two offline arms — which are real,
# deterministic and blocking, not a vacuous self-check. The audit runs where the
# credential is safe: a manual dispatch on main. Push and PR runs validate
# code only; existing production data cannot invalidate code CI.
#
# NO paths: filter. A pin's provenance is a property of the DIFF and of ANOTHER
# system's state; a paths filter would let a pin edit dodge the guard by
# arriving alongside a file the filter did not list, and lint-required-no-paths
# forbids paths on a context that is to become required.
#
# NO schedule/cron: molecule-core enforces a zero-cron ratchet
# (lint_schedule_budget.py).
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main]
workflow_dispatch:
env:
GITHUB_SERVER_URL: https://git.moleculesai.app
permissions:
contents: read
concurrency:
group: pin-provenance-guard-${{ github.ref }}
cancel-in-progress: true
jobs:
# main's branch protection is status_check_contexts=['*'], so every context
# this job posts is required. That is intended: a red here means a pin moved
# with no pipeline behind it, which is the whole subject of the gate. It is
# declared rather than left implicit so lint_required_context_exists_in_bp
# records the intent at the emitter, where a scheduled audit can read it.
# bp-required: yes
guard:
name: Pin provenance
runs-on: ubuntu-latest
timeout-minutes: 12
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# The SDK arm diffs base..head and reads commit metadata, so it needs
# real history, not a shallow tip.
fetch-depth: 0
# BLOBLESS. `fetch-depth: 0` fetches every ref in a repository with
# hundreds of branches, and on the tailnet-remote runners (~190ms) that
# is not merely slow: run 637991 sat inside
# [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune …
# on molecule-runner-robot-2-10 for over twenty minutes while every
# later step stayed `queued`. A gate that posts on every PR must not
# be the slowest thing in CI, and a gate that can hang is a gate that
# can be cancelled — which under BP ['*'] is a context that never
# posts. Blobs are fetched on demand; this guard reads a handful of
# small text files.
#
# SHALLOWING IS THE NEXT THING SOMEONE WILL REACH FOR HERE, and it is
# the one that used to break the audit silently: on a truncated
# history `git log --diff-filter=A` names the shallow boundary as the
# commit that added pin_provenance.py, sliding the grandfathering
# window forward to the checkout's tip date and excusing every hand
# write in between — with `checked:` still printing a healthy count.
# That is now impossible rather than merely discouraged:
# mechanism_landed_at() checks `git rev-parse
# --is-shallow-repository` FIRST and refuses to derive a boundary at
# all, which makes the audit grandfather NOTHING and say so loudly.
# So `fetch-depth: 0` is still what you want here, but the audit's
# correctness no longer depends on your remembering that.
filter: blob:none
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.11'
- name: Prove the stamp can be minted on this runner
id: mintcheck
run: |
set -euo pipefail
# The stamp is minted from the ambient Actions environment by
# staging-tenant-cd and by the prod promote lane. If GITHUB_RUN_ID
# were absent on these runners, EVERY pin write would refuse and the
# deploy path would be down — so prove minting works here, in a real
# run, rather than assuming the variable exists.
#
# This also makes the guard's own first run non-vacuous: it executes
# the minting path under the same runner the writers use.
stamp="$(python3 .gitea/scripts/pin_provenance.py mint)"
echo "minted: ${stamp}"
python3 .gitea/scripts/pin_provenance.py parse "${stamp}" >/dev/null
echo "round-trip: OK"
# And prove the refusal is real, here, not just in a unit test.
set +e
env -u GITHUB_RUN_ID python3 .gitea/scripts/pin_provenance.py mint >/dev/null 2>&1
rc=$?
set -e
if [ "$rc" != "3" ]; then
echo "::error::minting WITHOUT GITHUB_RUN_ID exited ${rc}, expected 3. The fail-closed half of the mechanism is not working, which would let a hand promote mint a stamp."
exit 1
fi
echo "fail-closed without GITHUB_RUN_ID: OK (exit 3)"
- name: Resolve the diff range
id: range
env:
EVENT: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
BEFORE: ${{ github.event.before }}
HEAD_BRANCH: ${{ github.head_ref }}
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
head="$(git rev-parse HEAD)"
base=""
if [ "$EVENT" = "pull_request" ] && [ -n "${BASE_SHA:-}" ]; then
base="$BASE_SHA"
elif [ -n "${BEFORE:-}" ] && [ "${BEFORE}" != "0000000000000000000000000000000000000000" ]; then
base="$BEFORE"
fi
if [ -z "$base" ] || ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
# Fall back to the previous first-parent commit. Never fall back to
# "compare HEAD with itself": an empty range makes the SDK arm find
# nothing and report success, which is the vacuous-pass shape this
# repository keeps paying for.
base="$(git rev-parse "${head}~1" 2>/dev/null || true)"
fi
if [ -z "$base" ]; then
echo "::error::could not resolve a base commit for the diff — refusing to run the SDK arm over an empty range."
exit 1
fi
branch="${HEAD_BRANCH:-$REF_NAME}"
echo "base=${base}" >> "$GITHUB_OUTPUT"
echo "head=${head}" >> "$GITHUB_OUTPUT"
echo "branch=${branch}" >> "$GITHUB_OUTPUT"
echo "::notice::diff range ${base}..${head} on branch ${branch}"
- name: Guard — the SDK pin may only move through the pipeline
env:
PIN_PROVENANCE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITEA_HOST: https://git.moleculesai.app
BASE: ${{ steps.range.outputs.base }}
HEAD: ${{ steps.range.outputs.head }}
BRANCH: ${{ steps.range.outputs.branch }}
run: |
set -euo pipefail
python3 .gitea/scripts/lint_sdk_pin_provenance.py \
--base "$BASE" --head "$HEAD" --branch "$BRANCH" \
| tee -a "$GITHUB_STEP_SUMMARY"
- name: Self-test both guards' disposition matrices
run: |
set -euo pipefail
# Same-job on purpose (see sdk-pin-drift.yml for the precedent). These
# suites assert BOTH directions: a hand-edited pin goes red AND a
# pipeline-produced one goes green. A one-directional suite has
# already shipped a gate that failed the commit fixing the thing it
# guarded.
python3 -m pip install --quiet pytest
python3 -m pytest .gitea/scripts/tests/test_pin_provenance.py \
.gitea/scripts/tests/test_lint_sdk_pin_provenance.py -q
- name: Read the production CP admin token from the Infisical SSOT
id: cptoken
if: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' }}
env:
INFISICAL_CLIENT_ID: ${{ secrets.INFISICAL_CI_CLIENT_ID }}
INFISICAL_CLIENT_SECRET: ${{ secrets.INFISICAL_CI_CLIENT_SECRET }}
INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_CI_PROJECT_ID }}
INFISICAL_BASE: https://key.moleculesai.app
run: |
set -euo pipefail
for v in INFISICAL_CLIENT_ID INFISICAL_CLIENT_SECRET INFISICAL_PROJECT_ID; do
eval "val=\${$v:-}"
if [ -z "$val" ]; then
echo "::error::$v is not provisioned — the live pin audit reads the CP admin token from the Infisical SSOT rather than holding a second long-lived copy. Failing closed rather than reporting an audit that read nothing."
exit 1
fi
done
tok="$(curl -fsS -A curl/8.4.0 -X POST "$INFISICAL_BASE/api/v1/auth/universal-auth/login" \
-H 'Content-Type: application/json' \
-d "{\"clientId\":\"$INFISICAL_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CLIENT_SECRET\"}" \
| python3 -c 'import sys,json; sys.stdout.write(json.load(sys.stdin).get("accessToken") or "")')"
if [ -z "$tok" ]; then
echo "::error::Infisical auth returned an empty accessToken — failing closed."
exit 1
fi
echo "::add-mask::$tok"
# env slug is `prod`, NOT `production`.
cp_token="$(curl -fsS -A curl/8.4.0 \
"$INFISICAL_BASE/api/v3/secrets/raw/CP_ADMIN_API_TOKEN?workspaceId=$INFISICAL_PROJECT_ID&environment=prod&secretPath=%2Fshared%2Fcontrolplane-admin" \
-H "Authorization: Bearer $tok" \
| python3 -c 'import sys,json; d=json.load(sys.stdin); sys.stdout.write(((d.get("secret") or {}).get("secretValue")) or "")')"
if [ -z "$cp_token" ]; then
echo "::error::Infisical returned an EMPTY CP_ADMIN_API_TOKEN at prod /shared/controlplane-admin — failing closed."
exit 1
fi
echo "::add-mask::$cp_token"
echo "CP_ADMIN_API_TOKEN=$cp_token" >> "$GITHUB_ENV"
echo "prod CP admin token loaded from the Infisical SSOT (len=${#cp_token})"
- name: Audit — the live tenant pin on EVERY control plane
if: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' }}
env:
PIN_PROVENANCE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITEA_HOST: https://git.moleculesai.app
PROD_CP_BASE_URLS: ${{ vars.PROD_CP_BASE_URLS || 'cp_prod=https://api.moleculesai.app=prod,cp_staging=https://staging-api.moleculesai.app=staging' }}
run: |
set -euo pipefail
if [ -z "${CP_ADMIN_API_TOKEN:-}" ]; then
echo "::error::CP_ADMIN_API_TOKEN is empty after the Infisical read — refusing to audit."
exit 1
fi
args=()
# Same endpoint grammar as the promote lane, from the same parser, so
# the audit can never inspect a different set of control planes than
# the promote writes to.
endpoints="$(python3 .gitea/scripts/prod_cp_endpoints.py "$PROD_CP_BASE_URLS")"
while IFS=$'\t' read -r label base inf_env; do
[ -n "$label" ] || continue
resp="${RUNNER_TEMP:-/tmp}/audit-${label}.json"
# lint-curl-status-capture shape: body to a file, status to a var,
# no or-else fallback inside the subshell.
code=$(curl -sS -A curl/8.4.0 -o "$resp" -w '%{http_code}' \
-H "Authorization: Bearer ${CP_ADMIN_API_TOKEN}" \
"${base%/}/cp/admin/runtime-image")
if [ "$code" != "200" ]; then
echo "::error::[${label}] could not read the live pin at ${base} (HTTP $code) — an unread control plane is not an audited one."
exit 1
fi
if [ ! -s "$resp" ]; then
echo "::error::[${label}] CP returned HTTP 200 with an EMPTY body — the known silent-failure shape on this path. Refusing to audit against it."
exit 1
fi
args+=("${label}=${resp}")
done <<< "$endpoints"
python3 .gitea/scripts/pin_provenance.py audit "${args[@]}" \
| tee -a "$GITHUB_STEP_SUMMARY"