Merge pull request #2561 from Molecule-AI/fix/cascade-list-drift-gate

feat(ci): structural drift gate for cascade list vs manifest (RFC #388 PR-3)
This commit is contained in:
Hongming Wang 2026-05-03 10:55:08 +00:00 committed by GitHub
commit 78721f7a42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 134 additions and 0 deletions

View File

@ -0,0 +1,39 @@
name: cascade-list-drift-gate
# Structural gate: TEMPLATES list in publish-runtime.yml must match
# manifest.json's workspace_templates exactly. Closes the recurrence
# path of PR #2556 (the data fix) and is the first concrete deliverable
# of RFC #388 PR-3.
#
# Why a gate, not just discipline: PR #2536 pruned the manifest, but the
# cascade list wasn't updated for ~weeks before someone (PR #2556)
# noticed during an unrelated audit. During that window, codex never
# rebuilt on a runtime publish. A structural gate catches the drift
# the same day either file changes.
#
# Triggers narrowly to keep CI quiet: only on PRs that actually change
# one of the two files. The path-filtered split + always-emit-result
# pattern (memory: "Required check names need a job that always runs")
# is unnecessary here because the workflow IS the check name and PR
# branch protection should require it directly. Future-proof: if this
# becomes a required check, add a no-op aggregator with always() so the
# name still emits when paths don't match.
on:
pull_request:
branches: [staging, main]
paths:
- manifest.json
- .github/workflows/publish-runtime.yml
- scripts/check-cascade-list-vs-manifest.sh
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Check cascade list matches manifest
run: bash scripts/check-cascade-list-vs-manifest.sh

View File

@ -0,0 +1,95 @@
#!/usr/bin/env bash
# check-cascade-list-vs-manifest.sh — structural drift gate for the
# publish-runtime cascade list vs manifest.json workspace_templates.
#
# WHY: PR #2536 pruned the manifest to 4 supported runtimes; PR #2556
# realigned the cascade list to match. The underlying drift hazard
# (cascade-list ≠ manifest) was unguarded — the data fix didn't prevent
# recurrence. This script is the structural gate that does.
#
# Behavior-based per project pattern: derives the expected set from
# manifest.json and the actual set from the workflow YAML, fails on
# any divergence in either direction.
#
# missing-from-cascade → templates in manifest that publish-runtime.yml
# won't auto-rebuild on a new wheel publish
# (the codex-stuck-on-stale-runtime bug class)
# extra-in-cascade → cascade dispatches to deprecated templates
# (the wasted-API-calls + dead-CI-noise class)
#
# Suffix mapping: manifest names map to GHCR repos via
# {name without -default suffix} → molecule-ai-workspace-template-<suffix>
# That's the same map publish-runtime.yml's TEMPLATES variable iterates.
#
# Exit:
# 0 cascade matches manifest exactly
# 1 drift detected (script prints the diff)
# 2 bad usage / missing inputs
set -eu
MANIFEST="${1:-manifest.json}"
WORKFLOW="${2:-.github/workflows/publish-runtime.yml}"
if [ ! -f "$MANIFEST" ]; then
echo "::error::manifest not found: $MANIFEST" >&2
exit 2
fi
if [ ! -f "$WORKFLOW" ]; then
echo "::error::workflow not found: $WORKFLOW" >&2
exit 2
fi
# Expected cascade entries: manifest workspace_templates → suffix-only
# (strip -default tail, e.g. claude-code-default → claude-code, since
# publish-runtime.yml's TEMPLATES uses suffixes that match the
# molecule-ai-workspace-template-<suffix> repo naming).
EXPECTED=$(jq -r '.workspace_templates[].name' "$MANIFEST" \
| sed 's/-default$//' \
| sort -u)
# Actual cascade entries: extract from the TEMPLATES="…" line. We look
# for the line, pull the contents between the quotes, and split into
# one-per-line. Single source of truth in the workflow itself, no
# parallel registry needed.
#
# Why not \s in the regex: BSD sed (macOS) doesn't recognize \s as
# whitespace — treats it as literal `s`. POSIX [[:space:]] works on
# both BSD and GNU sed. Same hazard nuked the original draft of this
# script: \s* matched empty-prefix-of-literal-s, then the leading
# whitespace stayed in the captured group.
ACTUAL=$(grep -E '[[:space:]]*TEMPLATES="' "$WORKFLOW" \
| head -1 \
| sed -E 's/^[[:space:]]*TEMPLATES="([^"]*)".*$/\1/' \
| tr ' ' '\n' \
| grep -v '^$' \
| sort -u)
if [ -z "$ACTUAL" ]; then
echo "::error::could not extract TEMPLATES=\"…\" from $WORKFLOW — has the variable name or quoting changed?" >&2
exit 2
fi
MISSING=$(comm -23 <(printf '%s\n' "$EXPECTED") <(printf '%s\n' "$ACTUAL"))
EXTRA=$(comm -13 <(printf '%s\n' "$EXPECTED") <(printf '%s\n' "$ACTUAL"))
if [ -z "$MISSING" ] && [ -z "$EXTRA" ]; then
echo "✓ cascade list matches manifest workspace_templates ($(echo "$EXPECTED" | wc -l | tr -d ' ') entries)"
exit 0
fi
echo "::error::cascade list drift detected between $MANIFEST and $WORKFLOW" >&2
echo "" >&2
if [ -n "$MISSING" ]; then
echo " Templates in manifest but MISSING from cascade (won't auto-rebuild on wheel publish):" >&2
echo "$MISSING" | sed 's/^/ - /' >&2
echo "" >&2
fi
if [ -n "$EXTRA" ]; then
echo " Templates in cascade but NOT in manifest (deprecated, wasting dispatch calls):" >&2
echo "$EXTRA" | sed 's/^/ - /' >&2
echo "" >&2
fi
echo " Fix: edit the TEMPLATES=\"…\" line in $WORKFLOW so the set matches" >&2
echo " manifest.json's workspace_templates (suffix-stripped). See PR #2556 for context." >&2
exit 1