2d087fbd92
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 6s
meta-ci-advisory / meta (advisory) (pull_request) Successful in 11s
verify-providers-projection / Regenerate projection, fail on drift, assert registry ⊆ template (pull_request) Successful in 1m25s
CI / Template validation (static) (pull_request) Successful in 8s
CI / Adapter unit tests (pull_request) Successful in 9s
CI / Adapter conformance (ADR-004 socket) (pull_request) Successful in 32s
CI / Template validation (runtime) (pull_request) Successful in 1m42s
CI / T4 tier-4 conformance (live) (pull_request) Successful in 1m49s
CI / validate (pull_request) Successful in 5s
Class fix, ported from template-hermes. Both jobs that build this template's
image ran
docker build ... --no-cache 2>&1 | tail -5
A failing buildkit run ends with the offending Dockerfile fragment and a
"failed to solve ... exit code: 1" summary, so those five lines are the same
every time and never contain the cause. On the hermes sibling that turned a
transient into a manual sweep against the live forge just to distinguish a bad
pin from a network blip; the rerun was green, so the only lasting artifact of
the incident was that CI could not explain its own failure. All four workspace
templates carried the identical pipe.
Nothing here changes what CI accepts -- only what it can tell you when it
refuses.
The build is now redirected to a file instead of piped, and the whole log is
printed inside a collapsed ::group:: when it fails. The FULL log, not a bigger
tail: buildkit interleaves parallel stages, so the failing layer's output can
sit arbitrarily far from the end, and a larger constant is the same defect with
a different number. Redirect rather than pipe because `tail` should not be in a
position to own the build's exit status.
Kept INLINE rather than extracted to a helper: tests/test_ci_runtime_image_pin.py
already pins "exactly one `docker build` step per job" as part of the
runtime-image contract, and a first attempt to source a shell helper broke that
guard in all four templates. The guard was right; the refactor was wrong. The
failure-path echoes say "image build" for the same reason -- "docker build" in
an echo would inflate that count.
test_failing_image_build_surfaces_its_own_output covers both jobs and kills
three mutants: restoring the pipe, printing a tail instead of the full log on
failure, and swallowing the build's exit status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
805 lines
43 KiB
YAML
805 lines
43 KiB
YAML
name: CI
|
|
|
|
# Repository-local validation pipeline. It fetches the canonical validator
|
|
# from molecule-ci at a reviewed immutable commit on each run, while keeping
|
|
# this repository's protected context names and runner requirements explicit.
|
|
#
|
|
# Four-surface migration audit (feedback_gitea_actions_migration_audit_pattern):
|
|
# 1. YAML — no `workflow_dispatch.inputs`; no `merge_group`; preserved
|
|
# `on: [push, pull_request]` from the original. Added workflow-level
|
|
# env.GITHUB_SERVER_URL (feedback_act_runner_github_server_url).
|
|
# 2. Package source — pip resolves through the Gitea PyPI registry;
|
|
# no runtime dependency is fetched from PyPI/GitHub in CI.
|
|
# 3. Token — uses auto-injected GITHUB_TOKEN (Gitea-aliased). Validator
|
|
# job needs only `contents: read` (no write to issues/PRs).
|
|
# 4. Docs — anonymous exact-SHA fetch of public molecule-ci with credential
|
|
# helpers disabled.
|
|
#
|
|
# Avoid job-level `if:` guards on protected contexts: skipped required jobs
|
|
# can remain pending in the branch-protection view. Runtime validation runs
|
|
# after static validation via `needs:` so protected contexts complete.
|
|
#
|
|
# Cross-links:
|
|
# - internal#326 — parent tracking issue
|
|
# - molecule-ai/molecule-ci/.gitea/workflows/validate-workspace-template.yml — pattern source
|
|
# - molecule-ai/molecule-core/.gitea/workflows/ci.yml — Gitea port style reference
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
types: [opened, synchronize, reopened]
|
|
|
|
# Defense-in-depth de-dup ONLY (the t4-conformance unique-name fix is the
|
|
# actual fail-closed primitive against the shared-host-daemon race; see
|
|
# that job). Scope per workflow + ref + EVENT so the push run and the
|
|
# pull_request run of the same internal-PR commit get DISTINCT groups —
|
|
# they must both complete (each emits its own required-status context;
|
|
# feedback_gitea_gate_check_required_list_not_combined_status). Never
|
|
# per-SHA-global: that silently cross-cancels legit required checks
|
|
# (feedback_concurrency_group_per_sha). cancel-in-progress:false so an
|
|
# in-flight live T4 probe is never aborted mid-assertion (a cancelled
|
|
# privileged probe would look like a gate failure / flake); a newer push
|
|
# to the same ref+event simply queues behind it.
|
|
concurrency:
|
|
group: ci-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
# Belt-and-suspenders against the runner-default trap
|
|
# (feedback_act_runner_github_server_url). Runners are configured
|
|
# with this env via /opt/molecule/runners/config.yaml runner.envs,
|
|
# but pinning at the workflow level protects against a runner
|
|
# regenerated without the config file.
|
|
GITHUB_SERVER_URL: https://git.moleculesai.app
|
|
# Canonical validation tooling is reviewed and content-addressed.
|
|
MOLECULE_CI_REF: 11b8598e5c0b3f0b1031733a8d5f6bc238f146a4
|
|
# Public test tooling resolves normally. The molecule-ci installer acquires
|
|
# the runtime wheel from the private Gitea index only, then supplies that
|
|
# local wheel while resolving public dependencies. The SDK conformance job
|
|
# installs its SDK source from an immutable Gitea commit.
|
|
|
|
# Defense-in-depth on the GITHUB_TOKEN scope. Dynamic validation is disabled
|
|
# for fork PRs; internal PRs still install requirements, import adapter.py, and
|
|
# docker-build the Dockerfile. Keep the token read-only for those steps.
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
validate-static:
|
|
name: Template validation (static)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
# Canonical validator script lives in molecule-ci. Fetch the reviewed
|
|
# commit anonymously without consulting runner credential helpers.
|
|
- name: Fetch immutable molecule-ci canonical scripts
|
|
run: |
|
|
set -euo pipefail
|
|
CI_ROOT=".molecule-ci-canonical"
|
|
mkdir "$CI_ROOT"
|
|
git -C "$CI_ROOT" init -q
|
|
git -C "$CI_ROOT" remote add origin https://git.moleculesai.app/molecule-ai/molecule-ci.git
|
|
fetched=false
|
|
for attempt in 1 2 3; do
|
|
if GIT_ASKPASS=/bin/false GIT_TERMINAL_PROMPT=0 git -c credential.helper= -c http.userAgent=curl/8.4.0 -C "$CI_ROOT" fetch --no-tags --depth 1 origin "$MOLECULE_CI_REF"; then
|
|
fetched=true
|
|
break
|
|
fi
|
|
sleep "$attempt"
|
|
done
|
|
if [ "$fetched" != true ]; then
|
|
echo "::error::immutable molecule-ci validator fetch failed"
|
|
exit 1
|
|
fi
|
|
git -C "$CI_ROOT" checkout -q --detach FETCH_HEAD
|
|
ACTUAL_CI_REF="$(git -C "$CI_ROOT" rev-parse HEAD)"
|
|
if [ "$ACTUAL_CI_REF" != "$MOLECULE_CI_REF" ]; then
|
|
echo "::error::immutable molecule-ci validator ref mismatch"
|
|
exit 1
|
|
fi
|
|
# Secret scan — the most important check. Always runs, including
|
|
# on fork PRs (no third-party code executes here).
|
|
- name: Check for secrets
|
|
run: |
|
|
python3 - << 'PYEOF'
|
|
import os, re, sys
|
|
from pathlib import Path
|
|
|
|
PATTERNS = [
|
|
re.compile(r'''["']sk-ant-[a-zA-Z0-9]{50,}["']'''),
|
|
re.compile(r'''["']ghp_[a-zA-Z0-9]{36,}["']'''),
|
|
re.compile(r'''["']AKIA[A-Z0-9]{16}["']'''),
|
|
re.compile(r'''["'][a-zA-Z0-9/+=]{40}["']'''),
|
|
re.compile(r'''["']sk_test_[a-zA-Z0-9]{24,}["']'''),
|
|
re.compile(r'''["']Bearer\s+[a-zA-Z0-9_.-]{20,}["']'''),
|
|
re.compile(r'''ghp_[a-zA-Z0-9]{36,}'''),
|
|
re.compile(r'''sk-ant-[a-zA-Z0-9]{50,}'''),
|
|
]
|
|
SKIP_DIRS = {'.molecule-ci-canonical', '.git', 'node_modules', '__pycache__'}
|
|
EXTENSIONS = {'.yaml', '.yml', '.md', '.py', '.sh'}
|
|
|
|
def is_false_positive(line):
|
|
ctx = line.lower()
|
|
return '...' in ctx or '<example' in ctx or '</example' in ctx
|
|
|
|
root = Path(os.environ.get('GITHUB_WORKSPACE', '.'))
|
|
warnings = []
|
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
for filename in filenames:
|
|
if Path(filename).suffix not in EXTENSIONS:
|
|
continue
|
|
filepath = Path(dirpath) / filename
|
|
try:
|
|
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
|
for lineno, line in enumerate(f.readlines(), 1):
|
|
for pattern in PATTERNS:
|
|
for match in pattern.finditer(line):
|
|
if not is_false_positive(line):
|
|
warnings.append(f" {filepath}:{lineno}: {match.group(0)[:40]}...")
|
|
except Exception:
|
|
pass
|
|
|
|
if warnings:
|
|
print("::error::Potential secret found in committed files:")
|
|
for w in warnings:
|
|
print(w)
|
|
sys.exit(1)
|
|
else:
|
|
print("::notice::No secrets detected")
|
|
PYEOF
|
|
# Static-only validator — file existence checks, YAML parse,
|
|
# AST inspection of adapter.py (no import). Doesn't execute any
|
|
# third-party code; safe on fork PRs.
|
|
- run: pip install pyyaml jsonschema packaging -q
|
|
- name: Reject legacy declared-plugin installer
|
|
run: |
|
|
python3 - << 'PYEOF'
|
|
from pathlib import Path
|
|
from packaging.requirements import Requirement
|
|
from packaging.version import Version
|
|
|
|
entrypoint = Path("entrypoint.sh").read_text(encoding="utf-8")
|
|
executable_lines = "\n".join(
|
|
line
|
|
for line in entrypoint.splitlines()
|
|
if not line.lstrip().startswith("#")
|
|
)
|
|
forbidden = (
|
|
"MOLECULE_DECLARED_PLUGINS",
|
|
"molecule_runtime.plugin_sources",
|
|
"install_declared_plugins",
|
|
"for _plg_src in $MOLECULE_DECLARED_PLUGINS",
|
|
"/archive/${_plg_ref}.tar.gz",
|
|
"Authorization: token ${MOLECULE_TEMPLATE_REPO_TOKEN}",
|
|
'mkdir -p "/configs/plugins/$_plg_name"',
|
|
'cp -a "$_plg_dir/." "/configs/plugins/$_plg_name/"',
|
|
)
|
|
found = [fragment for fragment in forbidden if fragment in executable_lines]
|
|
if found:
|
|
raise SystemExit(
|
|
"legacy privileged declared-plugin installer detected: "
|
|
+ ", ".join(found)
|
|
)
|
|
|
|
runtime_version = Version(Path(".runtime-version").read_text().strip())
|
|
hardened_version = Version("0.4.0")
|
|
runtime_requirement = next(
|
|
Requirement(line)
|
|
for line in Path("requirements.txt").read_text().splitlines()
|
|
if line.strip().startswith("molecules-workspace-runtime")
|
|
)
|
|
if runtime_version < hardened_version:
|
|
raise SystemExit("runtime pin predates hardened plugin installer")
|
|
if runtime_version not in runtime_requirement.specifier:
|
|
raise SystemExit("runtime pin is outside the template requirement")
|
|
hardened_floor = any(
|
|
specifier.operator in {">=", ">", "==", "===", "~="}
|
|
and Version(specifier.version) >= hardened_version
|
|
for specifier in runtime_requirement.specifier
|
|
)
|
|
if not hardened_floor:
|
|
raise SystemExit("template requirement lacks a hardened runtime floor")
|
|
if Version("0.3.125") in runtime_requirement.specifier:
|
|
raise SystemExit("template requirement permits vulnerable runtime")
|
|
PYEOF
|
|
- run: python3 .molecule-ci-canonical/scripts/validate-workspace-template.py --static-only
|
|
# Audit E1: scripts/molecule-askpass is byte-copied across every workspace
|
|
# template (no shared base image yet). Pin its sha256 so an unintentional
|
|
# edit — or a hand-copy that drifts from its peers — reds this PR. All
|
|
# templates pin the SAME value; a legitimate change must update the file
|
|
# AND this pin in every template together (which a reviewer sees). Always
|
|
# fires (validate-static has no paths filter / fork guard) and feeds the
|
|
# required `validate` aggregate, so it is a hard gate with no secrets.
|
|
- name: askpass helper drift guard (audit E1)
|
|
run: |
|
|
expected=7def3e86a3edf8ff28018081f14a6e390790cb102fd06c27c3f7040b07464303
|
|
test -f scripts/molecule-askpass || { echo "::error::scripts/molecule-askpass is missing"; exit 1; }
|
|
actual=$(sha256sum scripts/molecule-askpass | cut -d' ' -f1)
|
|
if [ "$actual" != "$expected" ]; then
|
|
echo "::error::scripts/molecule-askpass drifted from the pinned cross-template sha256."
|
|
echo " expected $expected"
|
|
echo " actual $actual"
|
|
echo " If this change is intentional, apply it identically to EVERY workspace template and bump this pin in each template's ci.yml."
|
|
exit 1
|
|
fi
|
|
echo "askpass pin OK ($actual)"
|
|
|
|
# pc2-safe: false — needs docker for Docker build smoke; routes to docker-host only
|
|
# (task #390 B-lite — PC2 native runners advertise `ubuntu-latest` but cannot
|
|
# docker due to npipe→Linux-container bridging gap; AND-match on both labels
|
|
# excludes them while keeping the Linux Docker-host runners eligible.)
|
|
validate-runtime:
|
|
name: Template validation (runtime)
|
|
runs-on: [ubuntu-latest, docker-host]
|
|
timeout-minutes: 15
|
|
needs: validate-static
|
|
# Runs after static validation. Avoid job-level if: guards here so the
|
|
# protected context always reaches a terminal state.
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
- name: Fetch immutable molecule-ci canonical scripts
|
|
run: |
|
|
set -euo pipefail
|
|
CI_ROOT=".molecule-ci-canonical"
|
|
mkdir "$CI_ROOT"
|
|
git -C "$CI_ROOT" init -q
|
|
git -C "$CI_ROOT" remote add origin https://git.moleculesai.app/molecule-ai/molecule-ci.git
|
|
fetched=false
|
|
for attempt in 1 2 3; do
|
|
if GIT_ASKPASS=/bin/false GIT_TERMINAL_PROMPT=0 git -c credential.helper= -c http.userAgent=curl/8.4.0 -C "$CI_ROOT" fetch --no-tags --depth 1 origin "$MOLECULE_CI_REF"; then
|
|
fetched=true
|
|
break
|
|
fi
|
|
sleep "$attempt"
|
|
done
|
|
if [ "$fetched" != true ]; then
|
|
echo "::error::immutable molecule-ci validator fetch failed"
|
|
exit 1
|
|
fi
|
|
git -C "$CI_ROOT" checkout -q --detach FETCH_HEAD
|
|
ACTUAL_CI_REF="$(git -C "$CI_ROOT" rev-parse HEAD)"
|
|
if [ "$ACTUAL_CI_REF" != "$MOLECULE_CI_REF" ]; then
|
|
echo "::error::immutable molecule-ci validator ref mismatch"
|
|
exit 1
|
|
fi
|
|
- run: pip install pyyaml jsonschema packaging -q
|
|
# Install the template's runtime dependencies so the validator's
|
|
# check_adapter_runtime_load() can import adapter.py the same way
|
|
# the workspace container does at boot. Without this, a
|
|
# syntactically-valid adapter that ImportErrors on a missing
|
|
# transitive dep would build clean and crash on first user prompt.
|
|
- name: Fork PR dynamic-validation notice
|
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
|
run: echo "::notice::fork PR — static validation ran; dependency install, adapter import, and Docker execution are disabled"
|
|
- name: Install workspace dependencies through molecule-ci
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
|
run: python3 .molecule-ci-canonical/scripts/install_workspace_dependencies.py --allow-missing --break-system-packages
|
|
- name: Run canonical dynamic validator
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
|
run: python3 .molecule-ci-canonical/scripts/validate-workspace-template.py
|
|
- name: entrypoint restore-from-secondary unit test (cp#326)
|
|
# Container-runnable bash unit test for entrypoint.sh's
|
|
# restore_from_secondary_volume(). Mocks blkid/mount/mountpoint/
|
|
# rsync so it runs without a real blockdev or docker daemon —
|
|
# pure logic coverage (idempotency, device-absent no-op, happy-
|
|
# path data landing, rsync-failure exit-code handling). The
|
|
# wire-level contract against real provisioned infrastructure lives in
|
|
# the CP repo's
|
|
# Stage C smoke (stage-c-workspace-backup-smoke.sh step 6b).
|
|
if: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && hashFiles('tests/test_entrypoint_restore.sh') != '' }}
|
|
run: bash tests/test_entrypoint_restore.sh
|
|
- name: Entrypoint plugin-skills link unit test
|
|
# Container-runnable bash unit test for entrypoint.sh's
|
|
# link_plugin_skills_into_claude_home() — the ~/.claude/skills ->
|
|
# /configs/skills bridge without which plugin-contributed skills
|
|
# install fine but never appear in Claude Code's skill listing
|
|
# (live-diagnosed 2026-07-05: lark-connect invisible on the
|
|
# agents-team platform agent until the symlink existed). Pure
|
|
# filesystem logic in a mktemp sandbox; chown is mocked.
|
|
if: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && hashFiles('tests/test_entrypoint_skills_link.sh') != '' }}
|
|
run: bash tests/test_entrypoint_skills_link.sh
|
|
- name: Docker build smoke test
|
|
if: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && hashFiles('Dockerfile') != '' }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Graceful skip when the runner's job-container can't reach the
|
|
# Docker daemon (e.g. /var/run/docker.sock not mounted into the
|
|
# act job container, or the in-container uid not in the docker
|
|
# group). Without this guard, CI stays red even when the
|
|
# template's Dockerfile is fine — see internal#222 for the
|
|
# proper runner-config fix.
|
|
if ! docker info >/dev/null 2>&1; then
|
|
echo "::warning::docker daemon unreachable from runner job container — skipping Docker build smoke (runner-config gap, not a template issue)."
|
|
exit 0
|
|
fi
|
|
if [ ! -s .runtime-version ]; then
|
|
echo "::error::.runtime-version is missing or empty"
|
|
exit 1
|
|
fi
|
|
if [ -z "${GITHUB_RUN_ID:-}" ] || [ -z "${GITHUB_RUN_ATTEMPT:-}" ]; then
|
|
echo "::error::GITHUB_RUN_ID and GITHUB_RUN_ATTEMPT are required for an isolated Docker tag"
|
|
exit 1
|
|
fi
|
|
EXPECTED_RUNTIME_VERSION="$(tr -d '[:space:]' < .runtime-version)"
|
|
SMOKE_TAG="molecule-ai-workspace-claude-code-smoke-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
|
trap 'docker image rm -f "$SMOKE_TAG" >/dev/null 2>&1 || true' EXIT
|
|
# Redirect, do not pipe: `tail` must not be able to truncate the cause
|
|
# or own the exit status. On failure the WHOLE log is printed -- a fixed
|
|
# window is what made template-hermes#342 undiagnosable, and buildkit
|
|
# interleaves parallel stages so the failing layer can sit far from the end.
|
|
BUILD_LOG="$(mktemp)"
|
|
rc=0
|
|
docker build --build-arg RUNTIME_VERSION="$EXPECTED_RUNTIME_VERSION" -t "$SMOKE_TAG" . --no-cache >"$BUILD_LOG" 2>&1 || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "::group::image build output ($(wc -l <"$BUILD_LOG") lines, build failed)"
|
|
cat "$BUILD_LOG"
|
|
echo "::endgroup::"
|
|
echo "::error::image build failed (exit $rc)"
|
|
exit "$rc"
|
|
fi
|
|
tail -5 "$BUILD_LOG"
|
|
ACTUAL_RUNTIME_VERSION="$(
|
|
docker run --rm --entrypoint python3 "$SMOKE_TAG" \
|
|
-c 'from importlib.metadata import version; print(version("molecules-workspace-runtime"))'
|
|
)"
|
|
if [ "$ACTUAL_RUNTIME_VERSION" != "$EXPECTED_RUNTIME_VERSION" ]; then
|
|
echo "::error::image runtime $ACTUAL_RUNTIME_VERSION does not match .runtime-version $EXPECTED_RUNTIME_VERSION"
|
|
exit 1
|
|
fi
|
|
echo "Docker build succeeded with exact runtime $ACTUAL_RUNTIME_VERSION"
|
|
|
|
# --- Layer-3: real T4 tier-4 conformance gate (RFC internal#456 §11) ---
|
|
# NOT a string-match. Builds the actual image, runs it under the EXACT
|
|
# flags the controlplane provisioner emits for tier-4
|
|
# (userdata_containerized.go @ec2384c: --privileged --pid=host --network host
|
|
# -v /:/host -v /var/run/docker.sock:/var/run/docker.sock), then drives
|
|
# the *uniform T4 privilege contract* defined in
|
|
# molecule-ai/molecule-core's workspace-server/internal/provisioner/
|
|
# t4_privilege_contract.go and rendered via
|
|
# `go run ./workspace-server/cmd/t4-contract-dump`. Each capability
|
|
# in the YAML has a stable name, a shell probe that exits 0 on pass,
|
|
# and a severity (hard|advisory). Hard misses fail the gate; new
|
|
# capabilities propagate WITHOUT a per-template PR (just bump the
|
|
# MOLECULE_CORE_REF env, or let it float to main).
|
|
#
|
|
# PILOT (internal #174): this is the first template to consume the
|
|
# uniform contract. template-hermes / template-codex follow on
|
|
# sequenced PRs after this lands green.
|
|
#
|
|
# Anti-tautology (per memory feedback_hermes_listpeers_401_token_…):
|
|
# all probes run against a RUNNING container started via the real
|
|
# `docker run` flags the provisioner emits — no `chown` + immediate
|
|
# `stat` self-fulfilling pairs. The contract's
|
|
# `host_root_reach_via_nsenter` probe fails closed if `exec gosu agent`
|
|
# ever regresses, exactly as the Hermes equivalent does.
|
|
#
|
|
# The `list_peers_http_200` probe is OPT-IN (advisory by default in
|
|
# this template) because the platform a2a_mcp_server is only spun up
|
|
# by the real start.sh boot path with credentials we don't want in
|
|
# CI. The probe iterates capabilities; for `list_peers_http_200` we
|
|
# skip-with-warning if `/configs/.auth_token` is absent (smoke-mode).
|
|
# On a fresh prod provision the probe is exercised end-to-end by the
|
|
# post-pin live-verify (task #195).
|
|
#
|
|
# Concurrency-flake: per-run-unique `--name` + per-run-unique probe
|
|
# file paths under /host/tmp/. Push and pull_request runs of the
|
|
# same commit share a host Docker daemon (--network host); a static
|
|
# name would collide and false-negative. See sibling template-hermes
|
|
# ci.yml + task #207 for the canonical rationale.
|
|
# pc2-safe: false — hard-gate docker (builds + runs real container under
|
|
# tier-4 provisioner flags); routes to docker-host only (task #390 B-lite).
|
|
t4-conformance:
|
|
name: T4 tier-4 conformance (live)
|
|
runs-on: [ubuntu-latest, docker-host]
|
|
timeout-minutes: 20
|
|
needs: validate-static
|
|
# Never execute fork-controlled Dockerfiles with host PID, /:/host, and
|
|
# docker.sock. The aggregate job explicitly accepts this fork-only skip.
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
|
env:
|
|
# The molecule-core ref the contract YAML is generated from.
|
|
# Default `main` floats with the latest contract; pin to a SHA
|
|
# for deterministic gate behavior across template branches.
|
|
# Adopters MAY override per-PR to test an unmerged contract change.
|
|
MOLECULE_CORE_REF: main
|
|
# Immutable reviewed source for the management-MCP attestation and
|
|
# final-image verifier. Keep this at job scope so every consuming step
|
|
# is bound to the same source commit.
|
|
MOLECULE_CI_REF: 11b8598e5c0b3f0b1031733a8d5f6bc238f146a4
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
- run: pip install -q pyyaml
|
|
- name: Fetch generated t4_capabilities.yaml from the uniform contract
|
|
run: |
|
|
set -euo pipefail
|
|
curl -fsSL \
|
|
"https://git.moleculesai.app/molecule-ai/molecule-core/raw/branch/${MOLECULE_CORE_REF}/workspace-server/internal/provisioner/t4_capabilities.yaml" \
|
|
-o t4_capabilities.yaml
|
|
# Defense-in-depth: schema-version assertion so a contract
|
|
# bump that breaks the parser shape is caught here, not at
|
|
# runtime where it would look like a phantom capability miss.
|
|
grep -q '^version: 1$' t4_capabilities.yaml || { echo "::error::t4_capabilities.yaml schema version unrecognized"; exit 1; }
|
|
echo "=== contract preview ==="
|
|
head -40 t4_capabilities.yaml
|
|
echo "=== capability names ==="
|
|
grep '^ - name:' t4_capabilities.yaml
|
|
- name: Prepare immutable management-MCP attestation
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
|
run: |
|
|
set -euo pipefail
|
|
: "${RUNNER_TEMP:?RUNNER_TEMP is required}"
|
|
RUN_SUFFIX="${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
|
|
MOLECULE_CI_ROOT="$RUNNER_TEMP/molecule-ci-mcp-e2e-$RUN_SUFFIX"
|
|
MCP_ATTESTATION="$RUNNER_TEMP/mcp-lockstep-$RUN_SUFFIX.json"
|
|
MCP_ATTESTATION_TMP="${MCP_ATTESTATION}.tmp"
|
|
MCP_ATTESTATION_SHA256="${MCP_ATTESTATION}.sha256"
|
|
RUNTIME_VERSION_FILE="$RUNNER_TEMP/mcp-runtime-version-$RUN_SUFFIX"
|
|
mkdir "$MOLECULE_CI_ROOT"
|
|
git -C "$MOLECULE_CI_ROOT" init -q
|
|
git -C "$MOLECULE_CI_ROOT" remote add origin \
|
|
https://git.moleculesai.app/molecule-ai/molecule-ci.git
|
|
fetched=false
|
|
for attempt in 1 2 3; do
|
|
if GIT_ASKPASS=/bin/false GIT_TERMINAL_PROMPT=0 \
|
|
git -c credential.helper= -c http.userAgent=curl/8.4.0 \
|
|
-C "$MOLECULE_CI_ROOT" \
|
|
fetch --no-tags --depth 1 origin "$MOLECULE_CI_REF"; then
|
|
fetched=true
|
|
break
|
|
fi
|
|
sleep "$attempt"
|
|
done
|
|
if [ "$fetched" != true ]; then
|
|
echo "::error::immutable molecule-ci verifier fetch failed"
|
|
exit 1
|
|
fi
|
|
git -C "$MOLECULE_CI_ROOT" checkout -q --detach FETCH_HEAD
|
|
ACTUAL_CI_REF="$(git -C "$MOLECULE_CI_ROOT" rev-parse HEAD)"
|
|
if [ "$ACTUAL_CI_REF" != "$MOLECULE_CI_REF" ]; then
|
|
echo "::error::immutable molecule-ci verifier ref mismatch"
|
|
exit 1
|
|
fi
|
|
git -C "$MOLECULE_CI_ROOT" diff --quiet --no-ext-diff --no-textconv "$MOLECULE_CI_REF" -- scripts/mcp_pin_lockstep.py scripts/mcp_built_image_e2e.py
|
|
python3 "$MOLECULE_CI_ROOT/scripts/mcp_pin_lockstep.py" \
|
|
--repo-root . --json > "$MCP_ATTESTATION_TMP"
|
|
mv "$MCP_ATTESTATION_TMP" "$MCP_ATTESTATION"
|
|
sha256sum "$MCP_ATTESTATION" > "$MCP_ATTESTATION_SHA256"
|
|
git -C "$MOLECULE_CI_ROOT" diff --quiet --no-ext-diff --no-textconv "$MOLECULE_CI_REF" -- scripts/mcp_pin_lockstep.py scripts/mcp_built_image_e2e.py
|
|
sha256sum --check "$MCP_ATTESTATION_SHA256"
|
|
EXPECTED_RUNTIME_VERSION="$(python3 - "$MOLECULE_CI_ROOT" "$MCP_ATTESTATION" <<'PY'
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(sys.argv[1]) / "scripts"))
|
|
from mcp_built_image_e2e import load_attestation
|
|
|
|
with Path(sys.argv[2]).open("rb") as stream:
|
|
sys.stdout.write(load_attestation(stream).runtime_version)
|
|
PY
|
|
)"
|
|
printf '%s\n' "$EXPECTED_RUNTIME_VERSION" > "$RUNTIME_VERSION_FILE"
|
|
- name: Build the runtime image
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
|
run: |
|
|
set -euo pipefail
|
|
: "${RUNNER_TEMP:?RUNNER_TEMP is required}"
|
|
RUN_SUFFIX="${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
|
|
MOLECULE_CI_ROOT="$RUNNER_TEMP/molecule-ci-mcp-e2e-$RUN_SUFFIX"
|
|
MCP_ATTESTATION="$RUNNER_TEMP/mcp-lockstep-$RUN_SUFFIX.json"
|
|
MCP_ATTESTATION_SHA256="${MCP_ATTESTATION}.sha256"
|
|
RUNTIME_VERSION_FILE="$RUNNER_TEMP/mcp-runtime-version-$RUN_SUFFIX"
|
|
T4_TAG="t4-conformance-test:${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
|
|
MCP_VERIFY_LOG="$RUNNER_TEMP/mcp-built-image-e2e-$RUN_SUFFIX.log"
|
|
MCP_VERIFY_CONTAINER="mcp-built-image-e2e-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
|
|
KEEP_T4_IMAGE=0
|
|
cleanup_t4_build() {
|
|
docker rm -f "$MCP_VERIFY_CONTAINER" >/dev/null 2>&1 || true
|
|
rm -rf -- "$MOLECULE_CI_ROOT"
|
|
rm -f -- "$MCP_ATTESTATION" "$MCP_ATTESTATION_SHA256" "$RUNTIME_VERSION_FILE" "$MCP_VERIFY_LOG"
|
|
if [ "$KEEP_T4_IMAGE" -ne 1 ]; then
|
|
docker image rm -f "$T4_TAG" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cleanup_t4_build EXIT
|
|
if ! docker info >/dev/null 2>&1; then
|
|
echo "::error::docker daemon unreachable — T4 conformance gate CANNOT verify host-root reach. This is a hard gate; failing closed (do NOT treat as skip). Fix runner-config (internal#222) to unblock."
|
|
exit 1
|
|
fi
|
|
if [ ! -s "$MCP_ATTESTATION" ] || [ ! -s "$RUNTIME_VERSION_FILE" ]; then
|
|
echo "::error::immutable management-MCP attestation is missing"
|
|
exit 1
|
|
fi
|
|
if [ ! -f "$MOLECULE_CI_ROOT/scripts/mcp_built_image_e2e.py" ]; then
|
|
echo "::error::immutable final-image verifier is missing"
|
|
exit 1
|
|
fi
|
|
EXPECTED_RUNTIME_VERSION="$(<"$RUNTIME_VERSION_FILE")"
|
|
DECLARED_RUNTIME_VERSION="$(tr -d '[:space:]' < .runtime-version)"
|
|
if [ "$EXPECTED_RUNTIME_VERSION" != "$DECLARED_RUNTIME_VERSION" ]; then
|
|
echo "::error::attested runtime version disagrees with .runtime-version"
|
|
exit 1
|
|
fi
|
|
# Redirect, do not pipe: `tail` must not be able to truncate the cause
|
|
# or own the exit status. On failure the WHOLE log is printed -- a fixed
|
|
# window is what made template-hermes#342 undiagnosable, and buildkit
|
|
# interleaves parallel stages so the failing layer can sit far from the end.
|
|
BUILD_LOG="$(mktemp)"
|
|
rc=0
|
|
docker build --build-arg RUNTIME_VERSION="$EXPECTED_RUNTIME_VERSION" -t "$T4_TAG" . --no-cache >"$BUILD_LOG" 2>&1 || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "::group::image build output ($(wc -l <"$BUILD_LOG") lines, build failed)"
|
|
cat "$BUILD_LOG"
|
|
echo "::endgroup::"
|
|
echo "::error::image build failed (exit $rc)"
|
|
exit "$rc"
|
|
fi
|
|
tail -5 "$BUILD_LOG"
|
|
ACTUAL_RUNTIME_VERSION="$(
|
|
docker run --rm --entrypoint python3 "$T4_TAG" \
|
|
-c 'from importlib.metadata import version; print(version("molecules-workspace-runtime"))'
|
|
)"
|
|
if [ "$ACTUAL_RUNTIME_VERSION" != "$EXPECTED_RUNTIME_VERSION" ]; then
|
|
echo "::error::T4 image runtime $ACTUAL_RUNTIME_VERSION does not match .runtime-version $EXPECTED_RUNTIME_VERSION"
|
|
exit 1
|
|
fi
|
|
rm -f "$MCP_VERIFY_LOG"
|
|
docker rm -f "$MCP_VERIFY_CONTAINER" >/dev/null 2>&1 || true
|
|
docker create --interactive --name "$MCP_VERIFY_CONTAINER" \
|
|
--network none \
|
|
--user 1000:1000 --workdir /tmp \
|
|
--cap-drop ALL --security-opt no-new-privileges \
|
|
--pids-limit 128 --memory 768m --cpus 1 \
|
|
--tmpfs /tmp:size=64m \
|
|
--entrypoint python3 "$T4_TAG" \
|
|
/mcp_built_image_e2e.py >/dev/null
|
|
git -C "$MOLECULE_CI_ROOT" diff --quiet --no-ext-diff --no-textconv "$MOLECULE_CI_REF" -- scripts/mcp_pin_lockstep.py scripts/mcp_built_image_e2e.py
|
|
docker cp "$MOLECULE_CI_ROOT/scripts/mcp_built_image_e2e.py" \
|
|
"$MCP_VERIFY_CONTAINER:/mcp_built_image_e2e.py"
|
|
sha256sum --check "$MCP_ATTESTATION_SHA256"
|
|
docker start --attach --interactive "$MCP_VERIFY_CONTAINER" \
|
|
< "$MCP_ATTESTATION" | tee "$MCP_VERIFY_LOG"
|
|
docker rm "$MCP_VERIFY_CONTAINER" >/dev/null
|
|
if ! grep -qxF 'mcp-built-image-e2e:sentinel:executed' "$MCP_VERIFY_LOG"; then
|
|
echo "::error::final-image management-MCP verifier sentinel is missing"
|
|
exit 1
|
|
fi
|
|
KEEP_T4_IMAGE=1
|
|
- name: Run under EXACT tier-4 provisioner flags + iterate contract capabilities
|
|
env:
|
|
# Per-run-unique probe-id. Used by individual capability
|
|
# probes (agent_home_writable, host_fs_write_readback) to
|
|
# scope their on-disk markers; without this, concurrent
|
|
# same-commit push+pull_request runs would collide on the
|
|
# /host/tmp/* path (see template-hermes ci.yml + task #207).
|
|
MOLECULE_T4_PROBE_ID: "${{ github.run_id }}-${{ github.run_attempt }}"
|
|
# Container name is computed in the script body and exported
|
|
# so the inline Python iterator can `docker exec` into it.
|
|
T4_PROBE_NAME: "t4probe-${{ github.run_id }}-${{ github.run_attempt }}"
|
|
run: |
|
|
set -euo pipefail
|
|
T4_TAG="t4-conformance-test:${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}"
|
|
T4_PROBE="$T4_PROBE_NAME"
|
|
trap 'docker rm -f "$T4_PROBE" >/dev/null 2>&1 || true; docker rmi -f "$T4_TAG" >/dev/null 2>&1 || true' EXIT
|
|
docker rm -f "$T4_PROBE" >/dev/null 2>&1 || true
|
|
docker run -d \
|
|
--name "$T4_PROBE" \
|
|
--network host \
|
|
--privileged \
|
|
--pid=host \
|
|
-v /:/host \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-e MOLECULE_T4_PROBE_ID="$MOLECULE_T4_PROBE_ID" \
|
|
-e MOLECULE_T4_EGRESS_TARGETS="https://git.moleculesai.app/api/v1/version" \
|
|
--entrypoint /bin/sh \
|
|
"$T4_TAG" -c 'sleep 600' >/dev/null
|
|
|
|
# ----- Reproduce SaaS-mode token agent-ownership pre-state -----
|
|
# The real entrypoint chowns /configs:agent before gosu; in this
|
|
# smoke probe /configs is unmounted, so reproduce the contract
|
|
# step. The `auth_token_agent_owned` probe THEN asserts the
|
|
# post-condition. This is NOT a tautology: the probe asserts
|
|
# `stat -c %u` returns 1000, which would fail if the entrypoint
|
|
# ever wrote the token as root in the live boot path
|
|
# (`host_root_reach_via_nsenter` + the gosu chain is the
|
|
# anti-regression guard for that — both probes must pass).
|
|
docker exec "$T4_PROBE" sh -c 'mkdir -p /configs && touch /configs/.auth_token && chown -R agent:agent /configs'
|
|
|
|
# ----- Iterate the contract YAML -----
|
|
# Pure-python YAML walker (PyYAML installed earlier). We
|
|
# don't exec the probe via shell-only because shell-parsing
|
|
# YAML is fragile; we do execute each probe IN the running
|
|
# container via `docker exec -u agent` so uid-1000 context is
|
|
# enforced.
|
|
python3 - <<'PYEOF'
|
|
import os, subprocess, sys, yaml
|
|
with open("t4_capabilities.yaml") as f:
|
|
doc = yaml.safe_load(f)
|
|
probe = os.environ["T4_PROBE_NAME"]
|
|
fails_hard = []
|
|
fails_soft = []
|
|
for cap in doc.get("capabilities", []):
|
|
name = cap["name"]
|
|
sev = cap.get("severity", "advisory")
|
|
probe_sh = cap["probe"]
|
|
# OPT-OUT semantics for capabilities that need a live
|
|
# platform/runtime not stood up in this probe. They are
|
|
# exercised end-to-end by the post-pin live-verify burst
|
|
# (task #195) instead.
|
|
if name == "list_peers_http_200":
|
|
# Only run if the in-container runtime has spun up;
|
|
# smoke-mode does not. Skip-with-notice keeps the
|
|
# gate honest without false negatives.
|
|
port = subprocess.run(
|
|
["docker","exec","-u","agent",probe,"sh","-c","[ -f /configs/.platform_port ]"],
|
|
capture_output=True,
|
|
).returncode
|
|
if port != 0:
|
|
print(f"::notice::skipping {name} — runtime not booted in CI smoke probe; covered by live post-pin verify")
|
|
continue
|
|
r = subprocess.run(
|
|
["docker","exec","-u","agent",probe,"sh","-c",probe_sh],
|
|
capture_output=True, text=True,
|
|
)
|
|
if r.returncode == 0:
|
|
print(f" PASS {name} ({sev})")
|
|
else:
|
|
msg = f"FAIL {name} ({sev}): rc={r.returncode} source={cap.get('source','?')}"
|
|
print(f"::error::{msg}")
|
|
if r.stderr.strip():
|
|
print(f" stderr: {r.stderr.strip()}")
|
|
if sev == "hard":
|
|
fails_hard.append(name)
|
|
else:
|
|
fails_soft.append(name)
|
|
if fails_hard:
|
|
print(f"::error::T4 conformance FAILED — hard capabilities not satisfied: {fails_hard} (RFC internal#456 §11; the gate is fail-closed)")
|
|
sys.exit(1)
|
|
if fails_soft:
|
|
print(f"::warning::T4 conformance: advisory capabilities failed: {fails_soft} (non-blocking, but inspect)")
|
|
print(f"::notice::T4 tier-4 conformance PASS — uniform contract satisfied ({len(doc.get('capabilities',[]))} capabilities checked)")
|
|
PYEOF
|
|
|
|
# Aggregator that emits a single `validate` check name — matches the
|
|
# historical required-check name on this repo's branch protection.
|
|
validate:
|
|
name: validate
|
|
runs-on: ubuntu-latest
|
|
needs: [validate-static, validate-runtime, t4-conformance, tests]
|
|
timeout-minutes: 1
|
|
# A fork PR intentionally skips the privileged T4 job. Always aggregate so
|
|
# that fork-only skip is evaluated below instead of propagating as a skip.
|
|
if: ${{ always() }}
|
|
steps:
|
|
- name: Aggregate
|
|
run: |
|
|
static="${{ needs.validate-static.result }}"
|
|
runtime="${{ needs.validate-runtime.result }}"
|
|
t4="${{ needs.t4-conformance.result }}"
|
|
tests="${{ needs.tests.result }}"
|
|
echo "validate-static: $static"
|
|
echo "validate-runtime: $runtime"
|
|
echo "t4-conformance: $t4"
|
|
echo "tests: $tests"
|
|
if [ "$static" != "success" ]; then
|
|
echo "::error::validate-static did not succeed: $static"
|
|
exit 1
|
|
fi
|
|
# Preserve compatibility with older runs that skipped runtime validation.
|
|
if [ "$runtime" != "success" ] && [ "$runtime" != "skipped" ]; then
|
|
echo "::error::validate-runtime did not succeed: $runtime"
|
|
exit 1
|
|
fi
|
|
# T4 conformance is a HARD gate on internal (non-fork) PRs
|
|
# and main pushes. `skipped` is only acceptable on fork PRs.
|
|
# Any other non-success fails the build: "verified" T4 requires
|
|
# this live gate green, never inference.
|
|
is_fork_pr="${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}"
|
|
if [ "$t4" != "success" ]; then
|
|
if [ "$t4" = "skipped" ] && [ "$is_fork_pr" = "true" ]; then
|
|
echo "::notice::t4-conformance skipped on fork PR — allowing aggregate to pass."
|
|
else
|
|
echo "::error::t4-conformance did not succeed: $t4 — T4 host-root reach / token-ownership not verified on a live container. Failing closed (RFC internal#456 §11)."
|
|
exit 1
|
|
fi
|
|
fi
|
|
if [ "$tests" != "success" ]; then
|
|
echo "::error::tests did not succeed: $tests"
|
|
exit 1
|
|
fi
|
|
echo "::notice::Template validation aggregate passed (static=$static, runtime=$runtime, t4=$t4, tests=$tests)"
|
|
|
|
tests:
|
|
name: Adapter unit tests
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
- name: Skip executable tests for external forks
|
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
|
run: echo "::notice::dependency installation and repository tests are disabled for external forks"
|
|
# pyyaml is the runtime dep that adapter.py's _load_providers reads
|
|
# /configs/config.yaml through. In production it arrives transitively
|
|
# via molecules-workspace-runtime; in this minimal test env we
|
|
# install it explicitly so the YAML-loading code path is actually
|
|
# exercised (without it, _load_providers' broad except-Exception
|
|
# swallows the ImportError and silently falls back to _BUILTIN_PROVIDERS,
|
|
# which is exactly the behavior that bit us 2026-04-30 when CI
|
|
# claimed green on a build that couldn't route any third-party model).
|
|
- run: pip install -q pytest pytest-asyncio pyyaml packaging
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
|
# Tests live under tests/ with their own pytest.ini that anchors
|
|
# rootdir there — keeps pytest from importing the package
|
|
# __init__.py (which does `from .adapter import ...` for runtime
|
|
# discovery and can't be satisfied without molecule_runtime
|
|
# installed). See tests/pytest.ini for the full rationale.
|
|
- run: python3 -m pytest tests/ -v
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
|
|
|
conformance:
|
|
name: Adapter conformance (ADR-004 socket)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
- name: Fetch immutable molecule-ci canonical scripts
|
|
run: |
|
|
set -euo pipefail
|
|
CI_ROOT=".molecule-ci-canonical"
|
|
mkdir "$CI_ROOT"
|
|
git -C "$CI_ROOT" init -q
|
|
git -C "$CI_ROOT" remote add origin https://git.moleculesai.app/molecule-ai/molecule-ci.git
|
|
fetched=false
|
|
for attempt in 1 2 3; do
|
|
if GIT_ASKPASS=/bin/false GIT_TERMINAL_PROMPT=0 git -c credential.helper= -c http.userAgent=curl/8.4.0 -C "$CI_ROOT" fetch --no-tags --depth 1 origin "$MOLECULE_CI_REF"; then
|
|
fetched=true
|
|
break
|
|
fi
|
|
sleep "$attempt"
|
|
done
|
|
if [ "$fetched" != true ]; then
|
|
echo "::error::immutable molecule-ci validator fetch failed"
|
|
exit 1
|
|
fi
|
|
git -C "$CI_ROOT" checkout -q --detach FETCH_HEAD
|
|
ACTUAL_CI_REF="$(git -C "$CI_ROOT" rev-parse HEAD)"
|
|
if [ "$ACTUAL_CI_REF" != "$MOLECULE_CI_REF" ]; then
|
|
echo "::error::immutable molecule-ci validator ref mismatch"
|
|
exit 1
|
|
fi
|
|
# The ADR-004 conformance suite (tests_conformance/) is the OPPOSITE of the
|
|
# unit `tests/` job: it drives the adapter through the REAL
|
|
# molecule_runtime.adapter_base.BaseAdapter + the real boot-safe MCP probe
|
|
# and inherits the SDK-owned battery
|
|
# molecule_plugin.adapter_conformance.AdapterConformance. So it needs the
|
|
# real runtime wheel AND molecule-ai-sdk (which ships molecule_plugin)
|
|
# installed — NOT the stubbed molecule_runtime the tests/conftest.py fakes.
|
|
# pytest-asyncio drives the async socket tests. packaging is required by
|
|
# molecule-ci's canonical dependency installer.
|
|
- run: pip install -q pytest pytest-asyncio pyyaml packaging
|
|
- name: Fork PR conformance notice
|
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
|
run: echo "::notice::fork PR — dependency installation and executable conformance tests are disabled"
|
|
- name: Install workspace dependencies through molecule-ci
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
|
run: python3 .molecule-ci-canonical/scripts/install_workspace_dependencies.py --allow-missing --break-system-packages
|
|
- name: Install SDK from reviewed immutable source
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
|
run: python3 -m pip install --isolated --disable-pip-version-check --quiet --break-system-packages "molecule-ai-sdk @ git+https://git.moleculesai.app/molecule-ai/molecule-ai-sdk.git@da42c7f2dae122aaa6f34a74c13e598a87870586"
|
|
# tests_conformance/ has its OWN conftest (no stubbing) + pytest.ini
|
|
# (importlib import-mode, auto asyncio) that anchors rootdir there.
|
|
- if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
|
run: python3 -m pytest tests_conformance/ -v
|