Splits auto-promote-staging.yml into:
- auto-promote-branch.yml — new reusable workflow with
`on: workflow_call`. Inputs `from-branch` (default 'staging') and
`to-branch` (default 'main'). Repo-agnostic: gates are read from
the consuming repo's branch protection at run time, not hardcoded.
- auto-promote-staging.yml — molecule-ci's own self-running flow,
now a ~25-line wrapper that calls the reusable workflow with
staging→main hardcoded. Trigger and behavior unchanged for
molecule-ci itself.
Adoption pattern in any consumer repo:
# .github/workflows/auto-promote.yml
name: Auto-promote staging → main
on:
push:
branches: [staging]
workflow_dispatch:
permissions:
contents: write
statuses: read
jobs:
promote:
uses: Molecule-AI/molecule-ci/.github/workflows/auto-promote-branch.yml@main
with:
from-branch: staging
to-branch: main
Excluded by policy: molecule-core + molecule-controlplane stay
manual per CEO directive 2026-04-24. Those repos do NOT adopt the
reusable workflow; the extraction adds no surface to repos that
don't call it.
Closes monorepo task #93.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the cascade cache trap that bit us 5x today. Each cascade
rebuild ran against the same Dockerfile + requirements.txt content,
producing the same docker layer cache key — so even though
publish-runtime had just shipped a new version, pip install hit the
cached layer with the OLD version.
Mechanism:
- Reusable workflow now accepts optional `runtime_version` input
- Forwarded as `--build-arg RUNTIME_VERSION=$VERSION` to docker build
- Templates that declare `ARG RUNTIME_VERSION` get cache-key
invalidation per-version (different ARG value → different cache
key → fresh pip install layer)
- Templates that don't declare the ARG silently ignore it (no
breakage; phased rollout)
Pairs with molecule-core PR #2181 (PyPI propagation wait + path
filter expansion). Together: cascade waits until PyPI serves the
new version, then fires with the version, templates rebuild against
that exact version with cache invalidation. No more "I shipped
0.1.X but image installs 0.1.X-1."
Phase 2 (separate PRs in template repos): each template's caller
forwards `${{ github.event.client_payload.runtime_version }}` and
each Dockerfile declares `ARG RUNTIME_VERSION` near pip install.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the new reusable workflow shipped in PR #10:
- Caller pattern (~10 lines per consuming repo) under Usage
- Full description in "What each workflow validates" — explains the
2026-04-27 motivation, the org-wide repo setting it pairs with,
and the false-positive note for CI bot pushes
Companion to molecule-core CONTRIBUTING.md update (PR #2177) which
documents the contract from the developer's perspective. Both must
land for the safety guards to be discoverable from where teams read.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reusable workflow that consumers call from their pr-guards.yml on
pull_request:synchronize. When a new commit is pushed to an open PR
that has auto-merge enabled, this disables auto-merge and posts a
comment so the operator must explicitly re-engage after verifying.
Background: on 2026-04-27, PR #2174 in molecule-core auto-merged
with only the first commit because the second commit was pushed
AFTER the merge queue had locked the PR's SHA. The second commit
ended up orphaned on a merged-and-deleted branch (the wider
"automatically delete head branches" repo setting now blocks the
push entirely; this workflow catches the race window where the PR
is queued but not yet merged).
Defense in depth — if both fixes are active:
1. Repo setting "delete branch on merge" prevents pushes to a
merged branch (post-merge orphan case).
2. This workflow catches in-queue races (push lands while the
queue is processing) by force-disabling auto-merge so the
operator must re-engage explicitly.
Together they cover the full lifecycle of "auto-merge enabled →
new commits arrive" without relying on operator discipline.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches the bare-imports lint from an inline RUNTIME_MODULES list
to the _runtime_modules.json manifest emitted by molecule-core's
build_runtime_package.py. Eliminates the third place the runtime
module list lived — now the build script is the single source of
truth.
Tonight surfaced that the same closed list lived in three places
that drifted independently. The build script's TOP_LEVEL_MODULES
went stale on transcript_auth, the smoke-test step here had a
hardcoded mirror that would have drifted next time a top-level
module was added, and runtime-pin-compat tested transitively via
import molecule_runtime.main (which only catches breakage, not
drift). One source of truth fixes all three at once.
Implementation:
- pip download molecule-ai-workspace-runtime --no-deps to /tmp
- unzip _runtime_modules.json from the wheel
- merge top_level_modules + subpackages into the regex alternation
(subpackages can be bare-imported too — `from lib.pre_stop`)
- on any fetch failure (network, missing manifest in older wheel),
fall back to the inline list with a workflow warning so the lint
still runs but the operator knows to investigate
Two consequences:
- Templates rebuilt against runtime ≥ the version that ships the
manifest get the always-fresh list automatically.
- Templates rebuilt against the old wheel (pre-manifest) still get
the working inline list — no regression.
Future cleanup (separate PR after a few release cycles): once all
template repos have rebuilt at least once with the manifest path,
the inline fallback can shrink to a panic message.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new gates that would have prevented today's
post-#87 template-extraction bug parade:
1. **Bare-import lint** — fail-fast pre-build check that grep's
template *.py files for `from <runtime_module> import` (where
<runtime_module> is in the closed list mirroring workspace/*.py
basenames). When the runtime was bundled into workspace/, bare
imports resolved against sibling files; in standalone template
repos they explode at startup. Five separate templates shipped
broken on 2026-04-27 because of this exact pattern (claude-code:
plugins, executor_helpers, heartbeat, a2a_client, platform_auth;
langgraph: agent, a2a_executor; deepagents: a2a_executor;
gemini-cli: config, executor_helpers x2). The lint runs before
docker login + buildx setup so a bad PR returns red in seconds.
2. **Import every /app/*.py at boot** (deeper smoke) — replaces
`python -c "import adapter"` with a loop importing every Python
module at /app/. The old single-import didn't traverse to
sibling modules adapter.py imports lazily inside
`create_executor()` (the executor.py family). That's why the
hermes a2a-sdk migration bug and langgraph's bare a2a_executor
import slipped through every prior gate even though the boot
smoke "passed." Importing every module module-level forces all
imports to resolve, including those in executor.py.
Both gates use the closed-list pattern (deliberate, easy to update,
no false-positives on legit third-party imports). The runtime module
list mirrors the equivalent in scripts/build_runtime_package.py;
both should be updated together when a new top-level workspace
module ships.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Today's incident: a template's adapter.py imported a symbol
(RuntimeCapabilities) from molecule_runtime that the published runtime
didn't yet export. The image built fine, the existing "smoke test"
inspected the entrypoint string and passed, and a broken :latest
shipped to GHCR. Every claude-code + hermes provision then hung in
"provisioning" status until the 10-min sweep marked them failed.
The old smoke test was named correctly but didn't actually exercise
anything — `docker inspect` doesn't catch ImportError. This change
splits the build/push step into three:
1. Build with `load: true, push: false` so the image lands on the
runner's local docker.
2. Smoke test runs `docker run ... python -c "import adapter"` against
the loaded image. This catches the version-skew class of bug
(adapter.py imports a symbol the installed runtime doesn't export),
plus syntax errors, missing files, and anything else that breaks
import-time.
3. Push :latest + :sha-* only if the smoke test passes. The push step
reuses the cached build, so it's fast.
Net cost: ~5s per publish (the docker run). Net benefit: broken images
can no longer poison :latest.
All 8 caller templates (claude-code, gemini-cli, hermes, langgraph,
crewai, autogen, deepagents, openclaw) inherit the gate automatically
since this is the reusable workflow they all call.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All 8 template repos are public → GHA-hosted minutes are free, so
there's no cost incentive to stay on the self-hosted Mac mini. The
only reason we started there was to avoid GHA rate limits (memory
feedback_selfhosted_runner); that concern doesn't apply here because:
- Linux/amd64 builds go native on ubuntu-latest (no QEMU emulation
from arm64 → amd64), so builds run ~2-3x faster.
- docker/login-action@v3 + GITHUB_TOKEN handles GHCR auth cleanly,
no Keychain gymnastics needed.
- No queue wait when the Mac mini is busy publishing canvas/platform
or running e2e.
Concretely this change:
- runs-on: [self-hosted, macos, arm64] → ubuntu-latest
- Drops the hand-rolled `auths` config step (macOS Keychain
workaround) in favour of `docker/login-action@v3`.
- Drops `docker/setup-qemu-action` (unnecessary for a linux/amd64
target on an amd64 runner).
- Uses setup-buildx@v3 to match the login-action major version.
Self-hosted Mac mini remains the runner for private-repo workflows
(follow-up PRs will migrate other public-repo workflows in
molecule-core).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each Molecule-AI/molecule-ai-workspace-template-* repo currently has
no way to publish its Docker image. Tenants build locally via
workspace/rebuild-runtime-images.sh after a manual clone — which
means "merge template PR" and "template live on tenants" are two
separate manual steps per tenant.
This workflow is the `publish` half of that pipeline. Called from
each template repo via `uses: Molecule-AI/molecule-ci/.github/
workflows/publish-template-image.yml@main`, it:
- Derives runtime name from the caller repo (strip
`molecule-ai-workspace-template-` prefix) so per-repo wrappers
stay one-line.
- Builds linux/amd64 (self-hosted macOS arm64 runner + QEMU) and
pushes to `ghcr.io/molecule-ai/workspace-template-<runtime>:latest`
plus `:sha-<7>` for per-commit pinning.
- Uses the Keychain-avoiding GHCR auth pattern from canvas' publish
workflow — osxkeychain write fails under the locked launchd keychain
on the Mac mini runner; writing auths map directly works.
- Smoke-tests the pushed image by pulling and inspecting entrypoint.
Follow-up (not in this PR):
- Each template repo gets a ~10-line caller workflow.
- Monorepo provisioner.RuntimeImages map switches from bare
`workspace-template:<runtime>` (local-only) to
`ghcr.io/molecule-ai/workspace-template-<runtime>:latest`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The grep-based secrets check matched literal credential patterns in
documentation (e.g., "sk-ant-..." in CLAUDE.md examples), causing
false-positive CI failures.
Replace with a Python script that:
- Skips .molecule-ci/ directory entirely
- Uses context-aware matching (requires quotes or assignment context)
- Filters out documentation examples with "..." or <example> markers
- Handles all three reusable workflows (plugin, workspace-template, org-template)
- Remove redundant nested checkout of molecule-ci in workflow_call jobs
- Add timeout-minutes to prevent hung jobs (plugin: 10m, workspace: 15m)
- Add pip cache using requirements.txt
- Add missing SKILL.md heading check in validate-plugin
- Add legacy import and runtime dependency warnings in workspace validation
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds standard credential gitignore (.env / *.pem / .secrets/ / .auth_token).
Per-CEO directive 2026-04-16: every plugin and template repo should
gitignore credentials so self-hosters can't accidentally commit real
tokens to public repos.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Heredocs in GitHub Actions YAML were being echoed as script text
instead of executed. Moving validation logic to scripts/ and running
via 'python3 .molecule-ci/scripts/validate-*.py' after checking out
the molecule-ci repo at .molecule-ci/ path.