Adds the third workstream of #90 (eliminate template repo drift): a strong contract check that exercises adapter.py the same way the runtime does at workspace boot. Without this, a template can have a syntactically-valid Dockerfile + an adapter.py that ImportErrors at runtime, build clean through Docker smoke, and crash on first user prompt — exactly the human-error class #90 is meant to eliminate. Existing checks ranked from weakest to strongest: 1. check_adapter() — text-grep for legacy `molecule_ai` imports. Catches one specific footgun. 2. Docker build smoke — `docker build` succeeds. Doesn't RUN the image, so adapter.py is never imported. Misses every adapter-load bug. 3. (NEW) check_adapter_runtime_load — imports adapter.py via the same `importlib.spec_from_file_location` path the runtime uses, and asserts at least one class inherits from molecule_runtime.adapters.base.BaseAdapter. Hard-error conditions: - adapter.py raises any exception during import (SyntaxError, ImportError, NameError, etc.). Same exception would crash the workspace at boot. - No class in the module inherits from BaseAdapter. The runtime's class-discovery silently falls through to the default langgraph executor in this case — exactly the silent-failure shape the contract is meant to catch. Skip conditions: - No adapter.py exists. Templates without one inherit the default executor by design (policy, not drift). - molecule-ai-workspace-runtime not importable in the validator env. Warns loudly so the CI-config bug surfaces, but doesn't hard-fail (we'd be reporting "your adapter is broken" when the actual cause is missing infra). Workflow update: validate-workspace-template.yml now installs the template's requirements.txt before invoking the validator (or falls back to installing molecule-ai-workspace-runtime alone if the template has no requirements.txt). This satisfies the runtime-load check's import dependencies the same way the workspace container does at boot — `pip install -r requirements.txt`. Verified locally: - 20/20 tests in test_validate_workspace_template.py pass (14 existing + 6 new). - Real langgraph template passes the full new validator including runtime-load (0 warnings, 0 errors). - Surveyed all 8 production templates' adapter.py shapes; every one already inherits from BaseAdapter, so this check turns green on first run with zero per-template fixups needed. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
4.1 KiB
YAML
93 lines
4.1 KiB
YAML
name: Validate Workspace Template
|
|
on:
|
|
workflow_call:
|
|
|
|
jobs:
|
|
validate:
|
|
name: Template validation
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
# Calling template repo (Dockerfile + config.yaml + adapter.py).
|
|
- uses: actions/checkout@v4
|
|
# Canonical validator script lives in molecule-ci, fetched fresh on
|
|
# every run. The previous setup expected `.molecule-ci/scripts/` to
|
|
# be vendored INTO each template repo, which drifted across the 8
|
|
# template repos as the validator evolved. Single source of truth
|
|
# eliminates that drift class entirely — every template runs the
|
|
# same canonical contract check on every CI run.
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
repository: Molecule-AI/molecule-ci
|
|
path: .molecule-ci-canonical
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.11"
|
|
cache: "pip"
|
|
cache-dependency-path: .molecule-ci-canonical/.molecule-ci/scripts/requirements.txt
|
|
- run: pip install pyyaml -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.
|
|
# The fallback (no requirements.txt) installs the runtime alone so
|
|
# BaseAdapter is at least importable for the class-discovery check.
|
|
- if: hashFiles('requirements.txt') != ''
|
|
run: pip install -q -r requirements.txt
|
|
- if: hashFiles('requirements.txt') == ''
|
|
run: pip install -q molecule-ai-workspace-runtime
|
|
- run: python3 .molecule-ci-canonical/scripts/validate-workspace-template.py
|
|
- name: Docker build smoke test
|
|
if: hashFiles('Dockerfile') != ''
|
|
run: docker build -t template-test . --no-cache 2>&1 | tail -5 && echo "✓ Docker build succeeded"
|
|
- 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', '.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
|