Skip to content
Latchkey

CI workflow (Cranot/roam-code)

The CI workflow from Cranot/roam-code, explained and optimized by Latchkey.

A

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.

Grade your own workflow free or run it on Latchkey →
Source: Cranot/roam-code.github/workflows/roam-ci.ymlLicense Apache-2.0View source

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

workflow (.yml)
# 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

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:

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.

Actions used in this workflow