d969fe7f4f
lint-infisical-extractor / lint-infisical-extractor (pull_request) Successful in 4s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
meta-ci-advisory / meta (advisory) (pull_request) Successful in 6s
consumer-drift / runtime-ssot-consumers (pull_request) Successful in 19s
ci / obs-smoke (pull_request) Has been skipped
ci / lint (pull_request) Successful in 14s
ci / build (pull_request) Successful in 19s
ci / smoke-install (pull_request) Successful in 25s
ci / unit-tests (pull_request) Successful in 1m12s
ci / responsiveness-e2e (pull_request) Successful in 1m23s
The scheduled `merge-bumps` job has failed on EVERY run since the tokens
were provisioned, always at the same line:
::error::cannot resolve the opener identity (GET /user with
DISPATCH_TOKEN failed); refusing to guess the bump-PR author.
Root cause is a collision between two individually-correct decisions:
* #311 reissued RUNTIME_BOT_TOKEN as `write:repository`-ONLY (least
privilege, deliberately no `read:user`).
* The 2026-07-05 identity-drift RC replaced a stale HARDCODED author
with a dynamic `whoami()` — because the hardcode had gone stale and
made the sweeper no-op GREEN while consumer pins drifted 5+ releases.
`whoami()` is `GET /user`, which requires `read:user`. Verified live:
GET /user -> 403 {"required":[read:user], "token scope":
"write:repository"}
So the precondition is structurally unsatisfiable and every unblock
since (2026-07-20, -24, -28) has been manual.
Fix — keep BOTH properties instead of trading one for the other:
* `whoami()` still wins whenever it works, so restoring the scope later
silently returns to fully-dynamic resolution with no config edit.
* When it can't, fall back to an explicit `BUMP_OPENER_LOGIN` env var
(named config, not a constant buried in source).
* A stale/wrong opener can no longer no-op green: a new drift alarm
fails the run RED when zero PRs match yet bump-SHAPED PRs exist under
another author, naming both sides. That is strictly stronger than the
pre-#339 behaviour, which only caught drift when whoami worked at all.
Also fixes a live regression hazard the dry-run exposed. Template main
was at 0.4.55 (a hand-merged outage unblock) while the newest OPEN bump
was 0.4.54, so "merge the latest open bump" meant merging a REGRESSION of
the production runtime pin. It currently fails on a git conflict — luck,
not a safety property, and one rebase away from silently going backwards.
It also WEDGED the sweeper: it picked the stale PR, failed to merge, and
`continue`d, so it never closed the superseded ones and could never make
progress even with the identity fix. Bumps at or below main's pin are now
treated as superseded and closed; only strictly-newer ones can merge.
Verified live against Gitea (--dry-run, all six consumer repos): before,
hard error and zero progress; after, all four maintained templates
correctly report `no bump newer than 0.4.55 — up to date` and the 66
piled-up stale bump PRs are identified for closure. Nothing merges.
Tests: 57 pass (25 new). Every guarantee negative-controlled — removing
the fallback, the typo guard, the whoami precedence, the anonymous probe,
the fail-open, or the numeric compare each fails exactly its own test.
Two things here were WRONG in the first cut and were caught only by
hitting live Gitea, so they carry regression tests: the `/users/{login}`
validation probe must be sent ANONYMOUSLY (Gitea rejects an out-of-scope
token *before* falling back to public access, so sending it 403s every
login and would have re-bricked the sweeper), and it must fail OPEN —
only a definitive 404 refuses.
Refs #339, #311, runtime#131
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
167 lines
9.5 KiB
YAML
167 lines
9.5 KiB
YAML
name: merge-runtime-version-bumps
|
|
|
|
# runtime#131: auto-merge propagated `.runtime-version` bump PRs on consumer
|
|
# template repos. Closes the "auto-bump-default gap" from the CTO directive
|
|
# (2026-06-10): green merge → auto-release → auto-deploy to prod was the
|
|
# rule, except for runtime templates (the explicit gap). The propagate
|
|
# script (scripts/propagate_runtime_version.py) opens a bump PR per
|
|
# consumer template on every release; this sweeper is the human-approval
|
|
# automation that was missing.
|
|
#
|
|
# Tightly scoped:
|
|
# - only PRs authored by the opener identity — resolved dynamically as
|
|
# whoami(DISPATCH_TOKEN), the same Infisical /shared/runtime-bot
|
|
# credential the propagate script opens the PRs with (never a
|
|
# hardcoded username; identity-drift RC 2026-07-05)
|
|
# - only `.runtime-version` and `requirements.txt` (the dual-pin
|
|
# templates bump both atomically; nothing else is allowed)
|
|
# - only when the head's combined commit status is `success` AND the
|
|
# universal wheel-install gate ("Template validation (runtime)")
|
|
# posted success (substring-matched; the old exact names never
|
|
# matched what the template CIs actually emit)
|
|
# - per-repo opt-out via `.gitea/REPO.yaml`'s `runtime-merge-bumps: false`
|
|
#
|
|
# Replaces the manual approve+merge that had to happen every release
|
|
# (the 2026-06-13 incident: openclaw had 3 stacked unmerged bumps;
|
|
# claude-code/hermes/codex also drifted). Now: cron + post-propagate.
|
|
|
|
on:
|
|
schedule:
|
|
# Every 30 min. Cheap (one API call per consumer repo).
|
|
# 10 random minutes in [:05, :35] skews the load vs the 17:10
|
|
# consumer-drift sweep so two CI waves don't fire back-to-back.
|
|
- cron: "*/30 5-23 * * *"
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
# Serialize: never cancel an in-flight sweep (a partially-completed
|
|
# merge is worse than a delayed one — the next sweep will retry).
|
|
group: merge-runtime-version-bumps
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
# runtime#131 RC 13418: GITEA_HOST must include the URL scheme. The
|
|
# script uses this value as the API base URL (`--base-url` default
|
|
# in scripts/merge_runtime_version_bumps.py) and concatenates it
|
|
# directly with `/api/v1/...`; a bare host ("git.moleculesai.app")
|
|
# would build "git.moleculesai.app/api/v1/..." (no scheme) and
|
|
# Python urllib rejects it with `ValueError: unknown url type`.
|
|
# Convention matches the test (`GITEA_HOST="https://example.invalid"`)
|
|
# and the script's default (`https://git.moleculesai.app`).
|
|
GITEA_HOST: https://git.moleculesai.app
|
|
|
|
jobs:
|
|
merge-bumps:
|
|
timeout-minutes: 15
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: "3.11"
|
|
|
|
# CI bot-token consolidation (RFC: Gitea-Actions-secrets -> Infisical/KMS,
|
|
# mirrors #971 / #3274): source the molecule-runtime-release-bot READ token
|
|
# from the Infisical SSOT (/shared/runtime-bot, key RUNTIME_BOT_TOKEN) via
|
|
# the CI machine identity, instead of the per-repo Gitea Actions secret
|
|
# DISPATCH_TOKEN. The fetched value is exported to $GITHUB_ENV under
|
|
# DISPATCH_TOKEN — the exact env var name the consumer step + the merge
|
|
# script (--read-token-env DISPATCH_TOKEN) already read. Only DISPATCH_TOKEN
|
|
# is consolidated here; CONSUMER_BUMP_MERGE_TOKEN is a DISTINCT non-author
|
|
# approve+merge identity (runtime#131) and is intentionally left on
|
|
# secrets.* — out of scope for this bot-token consolidation.
|
|
#
|
|
# ABSENT -> SKIP DEGRADE PRESERVED (runtime#83 + runtime#131): if the
|
|
# bootstrap INFISICAL_CI_* creds are absent (a config gap), this step warns
|
|
# and exports nothing, so the downstream `[ -z "${DISPATCH_TOKEN:-}" ]` check
|
|
# skips the sweeper (exit 0) rather than painting runtime main red. When the
|
|
# creds ARE present, Infisical is AUTHORITATIVE: a broken/empty/404 fetch
|
|
# hard-fails here so a broken migration cannot masquerade as a green skip.
|
|
- name: Fetch RUNTIME_BOT_TOKEN from Infisical SSOT
|
|
id: runtime_bot_token
|
|
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
|
|
if [ -z "${INFISICAL_CI_CLIENT_ID:-}" ] || [ -z "${INFISICAL_CI_CLIENT_SECRET:-}" ] || [ -z "${INFISICAL_PROJECT_ID:-}" ]; then
|
|
echo "::warning::INFISICAL_CI_* bootstrap creds absent; cannot fetch RUNTIME_BOT_TOKEN. The sweeper will skip pre-flight (config gap, runtime#83). Provision INFISICAL_CI_CLIENT_ID/_CLIENT_SECRET/_PROJECT_ID to enable."
|
|
exit 0
|
|
fi
|
|
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;d=json.load(sys.stdin);v=d.get("accessToken");print(v if isinstance(v,str) and v else "")')
|
|
if [ -z "${TOK}" ]; then
|
|
echo "::error::Infisical universal-auth login returned no accessToken -- creds present but auth failed; this is a real infra fault, failing loudly instead of skipping." >&2
|
|
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;d=json.load(sys.stdin);v=(d.get("secret") or {}).get("secretValue");print(v if isinstance(v,str) and v else "")'
|
|
}
|
|
RUNTIME_BOT_TOKEN=$(read_secret RUNTIME_BOT_TOKEN %2Fshared%2Fruntime-bot)
|
|
if [ -z "${RUNTIME_BOT_TOKEN}" ]; then
|
|
echo "::error::Infisical returned empty for RUNTIME_BOT_TOKEN at /shared/runtime-bot (prod) -- creds present but authoritative fetch failed; failing loudly so a broken migration cannot masquerade as a green skip." >&2
|
|
exit 1
|
|
fi
|
|
echo "::add-mask::$RUNTIME_BOT_TOKEN"
|
|
# Export under DISPATCH_TOKEN — the read-token name the consumer + script expect.
|
|
echo "DISPATCH_TOKEN=$RUNTIME_BOT_TOKEN" >> "$GITHUB_ENV"
|
|
echo "RUNTIME_BOT_TOKEN loaded from Infisical /shared/runtime-bot (non-empty, len=${#RUNTIME_BOT_TOKEN})"
|
|
|
|
- name: Merge propagated .runtime-version bump PRs
|
|
env:
|
|
# runtime#131 contract: the sweeper performs approve+merge
|
|
# as a NON-AUTHOR identity distinct from the bot that opened
|
|
# the bump PR (`molecule-runtime-release-bot`). Two tokens
|
|
# are wired:
|
|
# - CONSUMER_BUMP_MERGE_TOKEN: the non-author approve+merge
|
|
# identity. DISTINCT identity — intentionally NOT consolidated
|
|
# into RUNTIME_BOT; stays on secrets.*. If absent the script
|
|
# exits 0 with a loud warning (config gap per runtime#83; not a
|
|
# runtime regression — must not paint runtime main red).
|
|
# - DISPATCH_TOKEN: the read-only token for listing PRs,
|
|
# fetching files, checking commit status, and reading
|
|
# the per-repo opt-out file. Same molecule-runtime-release-bot
|
|
# identity the propagate + auto-release workflows use — now
|
|
# sourced from the Infisical SSOT (/shared/runtime-bot,
|
|
# RUNTIME_BOT_TOKEN) by the fetch step above and exported to
|
|
# $GITHUB_ENV under DISPATCH_TOKEN. The old per-repo Gitea secret
|
|
# DISPATCH_TOKEN is kept in place until a green run validates this
|
|
# path (validate-before-delete).
|
|
CONSUMER_BUMP_MERGE_TOKEN: ${{ secrets.CONSUMER_BUMP_MERGE_TOKEN }}
|
|
DISPATCH_TOKEN: ${{ env.DISPATCH_TOKEN }}
|
|
# issue #339. DISPATCH_TOKEN is RUNTIME_BOT_TOKEN, reissued
|
|
# write:repository-ONLY by issue #311's least-privilege fix — so
|
|
# `GET /user` 401s by design and the script's dynamic whoami() can
|
|
# never resolve the opener. That killed EVERY scheduled sweep since,
|
|
# and each unblock (2026-07-20, -24, -28) has been manual.
|
|
#
|
|
# This names the opener explicitly as a FALLBACK: whoami() still wins
|
|
# whenever it works, so re-granting read:user later silently restores
|
|
# the fully-dynamic behaviour with no edit here. A stale value can no
|
|
# longer no-op green — the script fails red when bump-shaped PRs
|
|
# exist under a different author.
|
|
#
|
|
# Verified 2026-07-28: all 66 open bump PRs across the four
|
|
# maintained templates are authored by molecule-runtime-release-bot.
|
|
BUMP_OPENER_LOGIN: molecule-runtime-release-bot
|
|
run: |
|
|
if [ -z "${CONSUMER_BUMP_MERGE_TOKEN:-}" ]; then
|
|
echo "::warning::CONSUMER_BUMP_MERGE_TOKEN secret absent; skipping consumer runtime-version bump merge sweeper (runtime#131 + runtime#83). Provision a non-author identity with write on the consumer template repos to enable."
|
|
exit 0
|
|
fi
|
|
if [ -z "${DISPATCH_TOKEN:-}" ]; then
|
|
echo "::warning::DISPATCH_TOKEN unavailable (Infisical fetch skipped on absent bootstrap creds); cannot list PRs / check status. Provision INFISICAL_CI_* to enable pre-flight checks."
|
|
exit 0
|
|
fi
|
|
python scripts/merge_runtime_version_bumps.py
|