CI workflow (Cranot/roam-code)
The CI workflow from Cranot/roam-code, explained and optimized by Latchkey.
CI health: A - excellent
Point runs-on at Latchkey and get caching, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the CI workflow from the Cranot/roam-code repository, a real project running GitHub Actions. It is shown here with attribution under its Apache-2.0 license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
# CI pipeline for roam-code itself.
#
# Jobs:
# test - pytest across all supported Python versions (3.10-3.13;
# 3.9 dropped 2026-05-16 per pyproject requires-python).
# lint - ruff format/lint check (Python 3.12 only, fast gate)
# self-analysis - roam analyses its own codebase on PRs using the local
# composite action (uses: ./), posting a sticky comment
# and uploading SARIF to GitHub Code Scanning.
#
# For the example template that downstream users copy into their repos,
# see .github/workflows/roam.yml (dormant, workflow_dispatch only).
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write # needed for sticky PR comments in self-analysis
security-events: write # needed for SARIF upload in self-analysis
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# -- Test suite across all supported Python versions -----------------------
# Ubuntu-only: roam-code is pure Python with no compiled extensions; a
# single-OS matrix covers correctness without the 3x cost of a full matrix.
# Use -m "not slow" to skip timing-sensitive performance tests that are
# prone to flaking under CI resource constraints.
#
# 3.9 dropped 2026-05-16: pyproject.toml requires-python = ">=3.10". The
# 3.9 matrix entry was failing every PR with "Package 'roam-code' requires
# a different Python: 3.9.25 not in '>=3.10'".
test:
runs-on: ubuntu-latest
# 3.10 is consistently slower than 3.11/3.12/3.13 (no stdlib tomllib,
# legacy pathlib, slower asyncio). History: 20 min was tight (killed at
# ~95% on 84343dc4) -> 30 min. The suite kept growing; on the v13.3
# storm-reset run (fdd2d3be) test (3.10) hit the 30-min cap and was
# cancelled while 3.11/3.12/3.13 passed just under it. 45 min restores
# real headroom for the slowest lane without masking regressions
# (a genuine hang would trip -x long before 45 min).
timeout-minutes: 45
strategy:
fail-fast: false # let all Python versions report; don't abort early
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history needed for git-stats and pr-risk tests
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Run tests
run: pytest tests/ -x -q -m "not slow"
# -- No-optional-deps lane: actually exercise fallback paths --------------
# The 2026-05-01 CI session caught a class of latent bugs where ImportError
# fallbacks (no scipy → degree-based PageRank, no leiden → Louvain, no
# onnxruntime → empty semantic scores) returned shapes that violated their
# documented contract. Tests passed because they happened to use the real
# library. This lane installs *only* the base dependencies + pytest, so
# ``tests/test_fallback_contracts.py`` actually runs the fallback path that
# production users without optional extras will hit. One Python version is
# enough; the matrix above covers grammar/version drift.
test-no-optional-deps:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install only the base dependencies (no [dev], no extras)
run: |
pip install -e .
pip install pytest pytest-xdist pytest-asyncio
- name: Run only the fallback-contract suite
run: pytest tests/test_fallback_contracts.py -v
# -- Lint (ruff) -----------------------------------------------------------
# Fast code style / lint gate. Only needs to pass on one Python version.
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Ruff format check
run: ruff format --check src/ tests/
- name: Ruff lint
run: ruff check src/ tests/
# -- Doc hygiene: anti-drift gates ---------------------------------------
# Gates that catch the most common doc-rot patterns:
# - test_no_internal_language.py - fail if any tracked file matches a
# forbidden internal-session pattern (Pass NN, Phase X of v2, dogfood
# notes YYYY-MM-DD, personal Windows paths, day-job customer names).
# - sync_surface_counts.py - fail if README/llms-install/server.json
# quote stale command/MCP-tool/language counts.
# - linkcheck.py - fail if any internal landing-page link 404s.
# - strip_metadata.py - fail if any tracked PDF/PNG/SVG has identifying
# metadata (author, creator, EXIF, embedded paths).
doc-hygiene:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # for the test that uses git ls-files
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install
run: |
pip install -e ".[dev]"
pip install pypdf
- name: Anti-leak language gate
run: pytest tests/test_no_internal_language.py -v
- name: Surface-count drift gate
run: python scripts/sync_surface_counts.py
- name: README/CLAUDE/llms-install count drift gate
run: python dev/build_readme_counts.py --check
- name: Changelog-render drift gate
run: python scripts/build_changelog_html.py
- name: Internal-link integrity
run: python scripts/linkcheck.py --strict
- name: Binary metadata gate
run: python scripts/strip_metadata.py
# -- Wheel-built smoke: drift-guards under a real pip install -------------
# W577: the wheel-drift suite (tests/test_package_data_wheel_drift.py) is a
# NO-OP under editable installs (`pip install -e .`) because
# ``importlib.resources.files(...)`` falls back to a filesystem walk that
# resolves anything physically on disk in src/, whether or not
# ``[tool.setuptools.package-data]`` in pyproject.toml ships it. W554 was
# exactly that bug - templates/audit-report/control-mapping.yaml resolved
# in dev but was missing from every pip-install user's wheel.
#
# This lane builds the wheel with ``python -m build``, installs it into a
# FRESH venv (no editable resolution), and runs the drift tests from a
# working directory OUTSIDE the source checkout so the wheel install is
# the only resolution path. If any package-data glob regresses, this lane
# fails before merge - not after the bad wheel ships to PyPI.
wheel-smoke:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build wheel
run: |
python -m pip install --upgrade pip build
python -m build --wheel
- name: Install wheel into a fresh venv (no source checkout)
run: |
python -m venv /tmp/wheel-test
/tmp/wheel-test/bin/pip install --upgrade pip
/tmp/wheel-test/bin/pip install dist/roam_code-*.whl
/tmp/wheel-test/bin/pip install pytest
# PyYAML is a documented optional runtime dep
# (pyproject.toml note: "In production it stays optional - the
# in-tree _parse_simple_yaml fallback covers the documented shapes").
# The smoke step below invokes `roam evidence-oscal --kind
# control-mapping`, which goes through src/roam/evidence/oscal.py
# - that path raises RuntimeError when PyYAML is absent. The
# smoke job needs it to verify the full evidence-OSCAL path.
/tmp/wheel-test/bin/pip install pyyaml
- name: Run wheel-drift tests against the installed wheel
# cwd OUTSIDE the source tree so importlib.resources resolves
# ONLY through the installed wheel - never through the src/
# filesystem fallback that editable installs use.
run: |
cp tests/test_package_data_wheel_drift.py /tmp/test_wheel_drift.py
cd /tmp
/tmp/wheel-test/bin/pytest /tmp/test_wheel_drift.py -x -v
- name: Smoke-run roam evidence-oscal from the wheel
# Sanity: the wheel-bundled control-mapping.yaml resolves AND
# produces non-empty OSCAL JSON with control entries. Belt-and-
# suspenders for the drift-guard above: even if the test file
# were ever skipped, an actual end-user invocation must work.
run: |
cd /tmp
/tmp/wheel-test/bin/roam evidence-oscal --kind control-mapping > /tmp/oscal.json
python -c "import json,sys; d=json.load(open('/tmp/oscal.json')); assert 'control-mapping' in d or 'controls' in d or 'components' in d or len(d)>0, 'OSCAL JSON empty'; print(f'OSCAL JSON OK: {len(d)} top-level keys')"
# -- Self-analysis: roam eats its own dog food on PRs ----------------------
# Runs the local composite action (uses: ./) to analyse this repo on every
# pull request. Posts a sticky PR comment with health score and pr-risk,
# uploads SARIF findings to GitHub Code Scanning, and enforces a soft
# health gate (>=50) so the job fails loudly if quality degrades.
self-analysis:
runs-on: ubuntu-latest
timeout-minutes: 20
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history required for pr-risk and git analysis
- uses: ./ # local composite action - tests action.yml itself
with:
commands: 'health pr-risk'
changed-only: 'true'
sarif: 'true'
sarif-commands: 'health'
sarif-category: 'roam-code-self-analysis'
comment: 'true'
gate: 'health_score>=50'
python-version: '3.12'
The same workflow, on Latchkey
Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.
# CI pipeline for roam-code itself. # # Jobs: # test - pytest across all supported Python versions (3.10-3.13; # 3.9 dropped 2026-05-16 per pyproject requires-python). # lint - ruff format/lint check (Python 3.12 only, fast gate) # self-analysis - roam analyses its own codebase on PRs using the local # composite action (uses: ./), posting a sticky comment # and uploading SARIF to GitHub Code Scanning. # # For the example template that downstream users copy into their repos, # see .github/workflows/roam.yml (dormant, workflow_dispatch only). name: CI on: push: branches: [main] pull_request: branches: [main] permissions: contents: read pull-requests: write # needed for sticky PR comments in self-analysis security-events: write # needed for SARIF upload in self-analysis concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # -- Test suite across all supported Python versions ----------------------- # Ubuntu-only: roam-code is pure Python with no compiled extensions; a # single-OS matrix covers correctness without the 3x cost of a full matrix. # Use -m "not slow" to skip timing-sensitive performance tests that are # prone to flaking under CI resource constraints. # # 3.9 dropped 2026-05-16: pyproject.toml requires-python = ">=3.10". The # 3.9 matrix entry was failing every PR with "Package 'roam-code' requires # a different Python: 3.9.25 not in '>=3.10'". test: runs-on: latchkey-small # 3.10 is consistently slower than 3.11/3.12/3.13 (no stdlib tomllib, # legacy pathlib, slower asyncio). History: 20 min was tight (killed at # ~95% on 84343dc4) -> 30 min. The suite kept growing; on the v13.3 # storm-reset run (fdd2d3be) test (3.10) hit the 30-min cap and was # cancelled while 3.11/3.12/3.13 passed just under it. 45 min restores # real headroom for the slowest lane without masking regressions # (a genuine hang would trip -x long before 45 min). timeout-minutes: 45 strategy: fail-fast: false # let all Python versions report; don't abort early matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # full history needed for git-stats and pr-risk tests - uses: actions/setup-python@v5 with: cache: 'pip' python-version: ${{ matrix.python-version }} - name: Install dependencies run: pip install -e ".[dev]" - name: Run tests run: pytest tests/ -x -q -m "not slow" # -- No-optional-deps lane: actually exercise fallback paths -------------- # The 2026-05-01 CI session caught a class of latent bugs where ImportError # fallbacks (no scipy → degree-based PageRank, no leiden → Louvain, no # onnxruntime → empty semantic scores) returned shapes that violated their # documented contract. Tests passed because they happened to use the real # library. This lane installs *only* the base dependencies + pytest, so # ``tests/test_fallback_contracts.py`` actually runs the fallback path that # production users without optional extras will hit. One Python version is # enough; the matrix above covers grammar/version drift. test-no-optional-deps: runs-on: latchkey-small timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: cache: 'pip' python-version: "3.12" - name: Install only the base dependencies (no [dev], no extras) run: | pip install -e . pip install pytest pytest-xdist pytest-asyncio - name: Run only the fallback-contract suite run: pytest tests/test_fallback_contracts.py -v # -- Lint (ruff) ----------------------------------------------------------- # Fast code style / lint gate. Only needs to pass on one Python version. lint: runs-on: latchkey-small timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: cache: 'pip' python-version: "3.12" - name: Install dependencies run: pip install -e ".[dev]" - name: Ruff format check run: ruff format --check src/ tests/ - name: Ruff lint run: ruff check src/ tests/ # -- Doc hygiene: anti-drift gates --------------------------------------- # Gates that catch the most common doc-rot patterns: # - test_no_internal_language.py - fail if any tracked file matches a # forbidden internal-session pattern (Pass NN, Phase X of v2, dogfood # notes YYYY-MM-DD, personal Windows paths, day-job customer names). # - sync_surface_counts.py - fail if README/llms-install/server.json # quote stale command/MCP-tool/language counts. # - linkcheck.py - fail if any internal landing-page link 404s. # - strip_metadata.py - fail if any tracked PDF/PNG/SVG has identifying # metadata (author, creator, EXIF, embedded paths). doc-hygiene: runs-on: latchkey-small timeout-minutes: 15 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # for the test that uses git ls-files - uses: actions/setup-python@v5 with: cache: 'pip' python-version: "3.12" - name: Install run: | pip install -e ".[dev]" pip install pypdf - name: Anti-leak language gate run: pytest tests/test_no_internal_language.py -v - name: Surface-count drift gate run: python scripts/sync_surface_counts.py - name: README/CLAUDE/llms-install count drift gate run: python dev/build_readme_counts.py --check - name: Changelog-render drift gate run: python scripts/build_changelog_html.py - name: Internal-link integrity run: python scripts/linkcheck.py --strict - name: Binary metadata gate run: python scripts/strip_metadata.py # -- Wheel-built smoke: drift-guards under a real pip install ------------- # W577: the wheel-drift suite (tests/test_package_data_wheel_drift.py) is a # NO-OP under editable installs (`pip install -e .`) because # ``importlib.resources.files(...)`` falls back to a filesystem walk that # resolves anything physically on disk in src/, whether or not # ``[tool.setuptools.package-data]`` in pyproject.toml ships it. W554 was # exactly that bug - templates/audit-report/control-mapping.yaml resolved # in dev but was missing from every pip-install user's wheel. # # This lane builds the wheel with ``python -m build``, installs it into a # FRESH venv (no editable resolution), and runs the drift tests from a # working directory OUTSIDE the source checkout so the wheel install is # the only resolution path. If any package-data glob regresses, this lane # fails before merge - not after the bad wheel ships to PyPI. wheel-smoke: runs-on: latchkey-small timeout-minutes: 20 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: cache: 'pip' python-version: "3.12" - name: Build wheel run: | python -m pip install --upgrade pip build python -m build --wheel - name: Install wheel into a fresh venv (no source checkout) run: | python -m venv /tmp/wheel-test /tmp/wheel-test/bin/pip install --upgrade pip /tmp/wheel-test/bin/pip install dist/roam_code-*.whl /tmp/wheel-test/bin/pip install pytest # PyYAML is a documented optional runtime dep # (pyproject.toml note: "In production it stays optional - the # in-tree _parse_simple_yaml fallback covers the documented shapes"). # The smoke step below invokes `roam evidence-oscal --kind # control-mapping`, which goes through src/roam/evidence/oscal.py # - that path raises RuntimeError when PyYAML is absent. The # smoke job needs it to verify the full evidence-OSCAL path. /tmp/wheel-test/bin/pip install pyyaml - name: Run wheel-drift tests against the installed wheel # cwd OUTSIDE the source tree so importlib.resources resolves # ONLY through the installed wheel - never through the src/ # filesystem fallback that editable installs use. run: | cp tests/test_package_data_wheel_drift.py /tmp/test_wheel_drift.py cd /tmp /tmp/wheel-test/bin/pytest /tmp/test_wheel_drift.py -x -v - name: Smoke-run roam evidence-oscal from the wheel # Sanity: the wheel-bundled control-mapping.yaml resolves AND # produces non-empty OSCAL JSON with control entries. Belt-and- # suspenders for the drift-guard above: even if the test file # were ever skipped, an actual end-user invocation must work. run: | cd /tmp /tmp/wheel-test/bin/roam evidence-oscal --kind control-mapping > /tmp/oscal.json python -c "import json,sys; d=json.load(open('/tmp/oscal.json')); assert 'control-mapping' in d or 'controls' in d or 'components' in d or len(d)>0, 'OSCAL JSON empty'; print(f'OSCAL JSON OK: {len(d)} top-level keys')" # -- Self-analysis: roam eats its own dog food on PRs ---------------------- # Runs the local composite action (uses: ./) to analyse this repo on every # pull request. Posts a sticky PR comment with health score and pr-risk, # uploads SARIF findings to GitHub Code Scanning, and enforces a soft # health gate (>=50) so the job fails loudly if quality degrades. self-analysis: runs-on: latchkey-small timeout-minutes: 20 if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # full history required for pr-risk and git analysis - uses: ./ # local composite action - tests action.yml itself with: commands: 'health pr-risk' changed-only: 'true' sarif: 'true' sarif-commands: 'health' sarif-category: 'roam-code-self-analysis' comment: 'true' gate: 'health_score>=50' python-version: '3.12'
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cache dependency installs on the setup step so they are served from cache.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Dependency installs
This workflow runs 6 jobs (9 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.