From 2894a7c2ba2717860f04315dc5b63a188a9c2e2e Mon Sep 17 00:00:00 2001 From: Tom Ballard Date: Mon, 29 Jun 2026 16:59:03 +0000 Subject: [PATCH 1/2] refactor(ci): carve the CI action sources out to rac-ci [roadmap:rac-ci] Implements rac/roadmaps/repo-topology/rac-ci.md (ADR-092, ADR-096). The watchkeeper/gatekeeper/validate composite actions and the reusable watchkeeper workflow now live in itsthelore/rac-ci (capability x platform). Removes the in-repo sources (action.yml, pr-gate-action/, validate-action/, the reusable workflow, and the two action-contract tests) and repoints the dogfood: pr-checks.yml now runs rac watchkeeper / rac validate --sarif / rac gate --sarif as direct source-install CLI steps, mirroring the agent-rules/eval/doctor jobs, so rac-core dogfoods the engine itself rather than its own published wrappers. Docs point consumers at the rac-ci paths. Old rac-core action tags stay resolvable (versioned cutover). --- .github/workflows/pr-checks.yml | 117 +++++++++++++++++++++------- .github/workflows/tests.yml | 2 +- .github/workflows/watchkeeper.yml | 69 ----------------- action.yml | 122 ------------------------------ docs/validation.md | 18 ++--- docs/watchkeeper.md | 5 +- pr-gate-action/action.yml | 117 ---------------------------- tests/test_pr_gate_action.py | 65 ---------------- tests/test_validate_action.py | 60 --------------- validate-action/action.yml | 100 ------------------------ 10 files changed, 102 insertions(+), 573 deletions(-) delete mode 100644 .github/workflows/watchkeeper.yml delete mode 100644 action.yml delete mode 100644 pr-gate-action/action.yml delete mode 100644 tests/test_pr_gate_action.py delete mode 100644 tests/test_validate_action.py delete mode 100644 validate-action/action.yml diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 3a93743c..ad298ba7 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -69,9 +69,10 @@ jobs: tests/test_operations.py tests/test_golden.py tests/test_dogfood.py - # Watchkeeper dogfood (v0.12.3): every pull request here gets a product - # knowledge review from the local action in source mode — which is also - # the live end-to-end test of action.yml itself. + # Watchkeeper dogfood: every pull request gets a product-knowledge review by + # running the CLI directly from a source install. The composite action wrapper + # now lives in rac-ci (itsthelore/rac-ci/watchkeeper/github); rac-core dogfoods + # the engine itself rather than its own published wrapper. watchkeeper: name: watchkeeper (dogfood, source install) runs-on: ubuntu-latest @@ -80,16 +81,28 @@ jobs: with: fetch-depth: 0 - - uses: ./ + - uses: actions/setup-python@v6 with: - path: rac - install-from: source + python-version: "3.11" - # Validate dogfood (v0.17.2): every pull request runs the local validate - # action in source mode — the live end-to-end test of validate-action/action.yml - # (install, `rac validate --sarif`, exit-code propagation). SARIF uploads to - # Code Scanning for same-repo PRs; it is skipped on forks, whose token cannot be - # granted security-events: write, so the check never fails on a missing scope. + - name: Install package + run: | + python -m pip install --upgrade pip + python -m pip install -e . + + - name: Run watchkeeper + env: + PR_BASE_REF: ${{ github.base_ref }} + run: | + git fetch --quiet --no-tags origin \ + "+refs/heads/$PR_BASE_REF:refs/remotes/origin/$PR_BASE_REF" || true + rac watchkeeper rac --base "origin/$PR_BASE_REF" --format github \ + --fail-on error > "$GITHUB_STEP_SUMMARY" + + # Validate dogfood (Registrar): run `rac validate --sarif` from a source install + # and upload to Code Scanning (skipped on forks, whose token cannot be granted + # security-events: write). The wrapper action now lives in + # itsthelore/rac-ci/registrar/github; this dogfoods the engine directly. validate: name: validate (dogfood, source install) runs-on: ubuntu-latest @@ -101,11 +114,36 @@ jobs: with: fetch-depth: 0 - - uses: ./validate-action + - uses: actions/setup-python@v6 with: - path: rac - install-from: source - upload-sarif: ${{ !github.event.pull_request.head.repo.fork }} + python-version: "3.11" + + - name: Install package + run: | + python -m pip install --upgrade pip + python -m pip install -e . + + - name: Run rac validate (SARIF) + id: validate + run: | + set +e + rac validate rac --sarif > rac.sarif + echo "exit_code=$?" >> "$GITHUB_OUTPUT" + + - name: Upload SARIF + if: ${{ always() && !github.event.pull_request.head.repo.fork }} + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: rac.sarif + + - name: Re-surface exit code + env: + EXIT_CODE: ${{ steps.validate.outputs.exit_code }} + run: | + if [ "$EXIT_CODE" != "0" ]; then + echo "::error::rac validate exited $EXIT_CODE — see Code Scanning annotations." + exit "$EXIT_CODE" + fi # Agent-rules drift gate (v0.21.16, ADR-067): every pull request re-derives the # live-decision digest from the corpus and fails if any committed agent-context @@ -184,14 +222,12 @@ jobs: - name: Run the doctor health diagnostic run: rac doctor rac/ - # PR-gate dogfood (v0.21.14): every pull request runs the local PR-gate action - # in source mode — the live end-to-end test of pr-gate-action/action.yml. It - # carries the full contract via a single `rac gate` command — validation, - # relationships, and review under the corpus enforcement policy — into one - # required check, uploading one SARIF document under the Code Scanning category - # `rac-gate` (distinct from the standalone validate job above, so the analyses - # never collide). Skipped on forks, whose token cannot be granted - # security-events: write. + # PR-gate dogfood: run the full contract as a single `rac gate --sarif` from a + # source install — validation, relationships, and review under the corpus + # enforcement policy — uploading one SARIF under the Code Scanning category + # `rac-gate` (distinct from the standalone validate job, so the analyses never + # collide). The wrapper action now lives in itsthelore/rac-ci/gatekeeper/github. + # Skipped on forks, whose token cannot be granted security-events: write. gate: name: pr-gate (dogfood, source install) runs-on: ubuntu-latest @@ -203,9 +239,36 @@ jobs: with: fetch-depth: 0 - - uses: ./pr-gate-action + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Install package + run: | + python -m pip install --upgrade pip + python -m pip install -e . + + - name: Run rac gate (SARIF) + id: gate + run: | + set +e + mkdir -p rac-sarif + rac gate rac --sarif > rac-sarif/gate.sarif + echo "exit_code=$?" >> "$GITHUB_OUTPUT" + + - name: Upload gate SARIF + if: ${{ always() && !github.event.pull_request.head.repo.fork }} + uses: github/codeql-action/upload-sarif@v3 with: - path: rac - install-from: source - upload-sarif: ${{ !github.event.pull_request.head.repo.fork }} + sarif_file: rac-sarif/gate.sarif + category: rac-gate + + - name: Re-surface exit code + env: + EXIT_CODE: ${{ steps.gate.outputs.exit_code }} + run: | + if [ "$EXIT_CODE" != "0" ]; then + echo "::error::RAC PR gate failed (exit $EXIT_CODE) — see Code Scanning annotations." + exit "$EXIT_CODE" + fi diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8fcf3aae..b395a93d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -126,7 +126,7 @@ jobs: - name: release paths: "tests/test_release_version.py" - name: watchkeeper - paths: "tests/test_watchkeeper.py tests/test_validate_action.py tests/test_pr_gate_action.py tests/test_gate.py tests/test_no_egress.py" + paths: "tests/test_watchkeeper.py tests/test_gate.py tests/test_no_egress.py" # Trust gates (v0.7.9): dogfood validates RAC's own rac/ corpus, # golden pins CLI stdout byte-for-byte against committed files. - name: dogfood diff --git a/.github/workflows/watchkeeper.yml b/.github/workflows/watchkeeper.yml deleted file mode 100644 index 6aed7016..00000000 --- a/.github/workflows/watchkeeper.yml +++ /dev/null @@ -1,69 +0,0 @@ -# Reusable Watchkeeper workflow (v0.12.3). -# -# Consumers call this with `uses:` to get product knowledge review on pull -# requests without writing checkout/base-ref wiring themselves: -# -# jobs: -# watchkeeper: -# uses: itsthelore/rac-core/.github/workflows/watchkeeper.yml@ -# with: -# path: rac -# -# Pin an exact release tag. No moving major tag is published: package -# versions derive from git tags via setuptools-scm, and a floating tag -# would corrupt version derivation (see docs/watchkeeper.md). -name: Watchkeeper - -on: - workflow_call: - inputs: - path: - description: "Corpus directory to compare." - type: string - required: false - default: "rac" - base: - description: "Base revision (empty: origin/)." - type: string - required: false - default: "" - fail-on: - description: "Failure policy: error, warning, or none." - type: string - required: false - default: "error" - annotate: - description: "Emit inline annotations." - type: boolean - required: false - default: true - rac-version: - description: "Exact rac-core version from PyPI (empty: latest)." - type: string - required: false - default: "" - -permissions: - contents: read - -jobs: - watchkeeper: - name: watchkeeper - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - # The comparison needs the base revision's history, and a - # source-mode install needs git metadata for setuptools-scm. - fetch-depth: 0 - - # Reusable workflows run in the caller's repository, so the action is - # referenced by full path. Update this ref when cutting a release tag; - # until the first v0.12.x release it tracks main. - - uses: itsthelore/rac-core@main - with: - path: ${{ inputs.path }} - base: ${{ inputs.base }} - fail-on: ${{ inputs.fail-on }} - annotate: ${{ inputs.annotate }} - rac-version: ${{ inputs.rac-version }} diff --git a/action.yml b/action.yml deleted file mode 100644 index 783623ee..00000000 --- a/action.yml +++ /dev/null @@ -1,122 +0,0 @@ -# RAC Watchkeeper composite action (v0.12.3). -# -# A thin wrapper: install RAC, resolve the base revision, run one -# `rac watchkeeper --format github` invocation, and route its outputs — -# stdout to the step summary, stderr (workflow-command annotations) to the -# step log. All analysis, rendering, and failure policy live in the Python -# package (ADR-015); the action never reinterprets exit codes. -name: "RAC Watchkeeper" -description: >- - Review product knowledge changes (requirements, decisions, roadmaps, - designs, prompts) on pull requests: changed artifacts, validation and - relationship deltas, deterministic intent findings, and a review verdict. -author: "Tom Ballard" - -branding: - icon: "eye" - color: "purple" - -inputs: - path: - description: >- - Corpus directory to compare (passed to `rac watchkeeper`). - required: false - default: "rac" - base: - description: >- - Base state: a git revision or a directory. Empty (the default) - resolves to `origin/`. - required: false - default: "" - fail-on: - description: >- - Failure policy forwarded to `rac watchkeeper --fail-on`: - `error` (fail when review is recommended), `warning` (also fail on - any warning finding), or `none` (never fail, still report). - required: false - default: "error" - annotate: - description: >- - Emit inline annotations via workflow commands (`true` or `false`). - required: false - default: "true" - rac-version: - description: >- - Exact rac-core version to install from PyPI. Empty - installs the latest release. Ignored when `install-from` is `source`. - required: false - default: "" - install-from: - description: >- - Where to install RAC from: `pypi` (the default for consumers) or - `source` (the action checkout itself — used by this repository's own - dogfood job; requires `uses: ./` on a full checkout). - required: false - default: "pypi" - -runs: - using: "composite" - steps: - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install RAC - shell: bash - env: - INSTALL_FROM: ${{ inputs.install-from }} - RAC_VERSION: ${{ inputs.rac-version }} - run: | - python -m pip install --quiet --upgrade pip - if [ "$INSTALL_FROM" = "source" ]; then - # The action checkout itself; setuptools-scm needs git metadata, - # so this path requires `uses: ./` on a real checkout. - python -m pip install --quiet "$GITHUB_ACTION_PATH" - elif [ -n "$RAC_VERSION" ]; then - python -m pip install --quiet "rac-core==$RAC_VERSION" - else - python -m pip install --quiet rac-core - fi - - - name: Resolve base revision - shell: bash - env: - INPUT_BASE: ${{ inputs.base }} - PR_BASE_REF: ${{ github.base_ref }} - run: | - BASE="$INPUT_BASE" - if [ -z "$BASE" ]; then - BASE="origin/$PR_BASE_REF" - fi - # Make sure the base ref exists locally; harmless when it already - # does, best-effort when the base is a directory or a plain SHA - # (rac fails with exit 2 and a clear message if it cannot resolve). - case "$BASE" in - origin/*) - REF="${BASE#origin/}" - git fetch --quiet --no-tags origin \ - "+refs/heads/$REF:refs/remotes/origin/$REF" || true - ;; - esac - echo "WATCHKEEPER_BASE=$BASE" >> "$GITHUB_ENV" - - - name: Run watchkeeper - shell: bash - env: - INPUT_PATH: ${{ inputs.path }} - FAIL_ON: ${{ inputs.fail-on }} - ANNOTATE: ${{ inputs.annotate }} - run: | - ANNOTATE_FLAG="" - if [ "$ANNOTATE" != "true" ]; then - ANNOTATE_FLAG="--no-annotate" - fi - # stdout becomes the job's step summary; stderr stays in the live - # step log where the runner parses the annotation commands. The - # exit code propagates unchanged — policy lives in --fail-on. - rac watchkeeper "$INPUT_PATH" \ - --base "$WATCHKEEPER_BASE" \ - --format github \ - --fail-on "$FAIL_ON" \ - $ANNOTATE_FLAG \ - > "$GITHUB_STEP_SUMMARY" diff --git a/docs/validation.md b/docs/validation.md index c0516054..4efca416 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -136,13 +136,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: itsthelore/rac-core/validate-action@v0 + - uses: itsthelore/rac-ci/registrar/github@v1 with: path: rac/ ``` Inputs: `path` (default `rac`), `upload-sarif` (default `true`), `sarif-file`, -`rac-version` (pin a release), and `install-from` (`pypi` or `source`). Errors +and `rac-version` (pin a release). Errors fail the check; warnings — including findings downgraded in `.rac/config.yaml` — annotate without failing, so a legacy repo can adopt the gate green on day one and tighten over time. @@ -152,15 +152,15 @@ tighten over time. > custom relationship edges are deferred (ADR-052, ADR-055); a repo-local schema > registry is a future, separately recorded decision. -(The Watchkeeper action at the repository root is the complementary PR-review -surface — see [Watchkeeper](watchkeeper.md).) +(The Watchkeeper action in [rac-ci](https://github.com/itsthelore/rac-ci) is the +complementary PR-review surface — see [Watchkeeper](watchkeeper.md).) ### The full PR gate (`rac gate`) To carry the whole contract into one required check, `rac gate` composes validation, relationship integrity, and review into a single enforced verdict under the corpus **enforcement policy**, and emits one combined SARIF document -(v0.21.14). The `pr-gate-action` runs it and uploads that single SARIF to Code +(v0.21.14). The Gatekeeper action runs it and uploads that single SARIF to Code Scanning under one category (`rac-gate`), failing when any finding is *blocking*. It is the same thin wrapper — the engine decides what is blocking, the action computes nothing ([ADR-063](https://github.com/itsthelore/rac-core/blob/main/rac/decisions/adr-063-non-python-clients-are-thin.md)): @@ -177,14 +177,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: itsthelore/rac-core/pr-gate-action@v0 + - uses: itsthelore/rac-ci/gatekeeper/github@v1 with: path: rac/ ``` -Inputs mirror `validate-action`: `path` (default `rac`), `upload-sarif` (default -`true`), `sarif-dir` (default `rac-sarif`, now one `gate.sarif`), `rac-version`, -and `install-from` (`pypi` or `source`). +Inputs mirror the Registrar action: `path` (default `rac`), `upload-sarif` +(default `true`), `sarif-dir` (default `rac-sarif`, now one `gate.sarif`), and +`rac-version`. `rac gate ` is also runnable locally — `--json` and `--sarif` produce the machine contracts, the exit code is `0` when nothing is blocking and `1` diff --git a/docs/watchkeeper.md b/docs/watchkeeper.md index 286065b3..0908ab5f 100644 --- a/docs/watchkeeper.md +++ b/docs/watchkeeper.md @@ -45,7 +45,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: itsthelore/rac-core@ + - uses: itsthelore/rac-ci/watchkeeper/github@v1 ``` On every pull request that touches your corpus you get: @@ -64,7 +64,6 @@ On every pull request that touches your corpus you get: | `fail-on` | `error` | `error` · `warning` · `none` (report, never fail) | | `annotate` | `true` | Emit inline annotations | | `rac-version` | `''` | Exact PyPI version to install (empty: latest) | -| `install-from` | `pypi` | `pypi`, or `source` for this repository's own dogfood | The action is a wrapper: it installs RAC, resolves the base ref, runs one `rac watchkeeper --format github` invocation, and propagates its exit code @@ -78,7 +77,7 @@ Prefer calling a workflow instead of composing steps: ```yaml jobs: watchkeeper: - uses: itsthelore/rac-core/.github/workflows/watchkeeper.yml@ + uses: itsthelore/rac-ci/.github/workflows/watchkeeper.yml@v1 with: path: rac ``` diff --git a/pr-gate-action/action.yml b/pr-gate-action/action.yml deleted file mode 100644 index 23c2c997..00000000 --- a/pr-gate-action/action.yml +++ /dev/null @@ -1,117 +0,0 @@ -# RAC PR-gate composite action (v0.21.14, ADR-049 / ADR-063). -# -# Carries the full RAC contract into a single required pull-request check: -# install RAC and run `rac gate` — one command that enforces validation, -# relationship integrity, and review under the corpus enforcement policy -# (`.rac/config.yaml`), emitting one SARIF document (ADR-054). The single SARIF -# is uploaded to GitHub Code Scanning under one category (`rac-gate`), and the -# CLI exit code is re-surfaced. The action is a thin consumer (ADR-063): all -# analysis and enforcement policy live in the package (ADR-015 / ADR-049); the -# action never reinterprets findings or decides what is blocking. -# -# The Watchkeeper action lives at the repository root and the single-command -# validate action at `validate-action/`; this gate is referenced as -# `uses: itsthelore/rac-core/pr-gate-action@`. -name: "RAC PR gate" -description: >- - Enforce a requirements-as-code (RAC) corpus on a pull request with a single - `rac gate` command — validation, relationship integrity, and review under the - corpus enforcement policy — surfaced inline via GitHub Code Scanning (SARIF) as - a required status check. A thin wrapper over the `rac` CLI. -author: "Tom Ballard" - -branding: - icon: "shield" - color: "purple" - -inputs: - path: - description: "The RAC corpus directory to enforce (passed to `rac gate`)." - required: false - default: "rac" - upload-sarif: - description: >- - Upload SARIF to GitHub Code Scanning (`true` or `false`). Requires the job - to grant `security-events: write`. - required: false - default: "true" - sarif-dir: - description: "Directory the single `gate.sarif` document is written to." - required: false - default: "rac-sarif" - rac-version: - description: >- - Exact rac-core version to install from PyPI. Empty installs the - latest release. Ignored when `install-from` is `source`. - required: false - default: "" - install-from: - description: >- - Where to install RAC from: `pypi` (the default for consumers) or `source` - (the action checkout itself — for this repository's own dogfood job; - requires `uses: ./pr-gate-action` on a full checkout). - required: false - default: "pypi" - -runs: - using: "composite" - steps: - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install RAC - shell: bash - env: - INSTALL_FROM: ${{ inputs.install-from }} - RAC_VERSION: ${{ inputs.rac-version }} - run: | - python -m pip install --quiet --upgrade pip - if [ "$INSTALL_FROM" = "source" ]; then - python -m pip install --quiet "$GITHUB_ACTION_PATH/.." - elif [ -n "$RAC_VERSION" ]; then - python -m pip install --quiet "rac-core==$RAC_VERSION" - else - python -m pip install --quiet rac-core - fi - - # The CLI is the source of truth (ADR-058). `set +e` lets a non-zero exit - # still produce SARIF for upload; the exit code is re-surfaced below. One - # command, one SARIF document — `rac gate` already composes validation, - # relationships, and review under the corpus enforcement policy. - - name: Run RAC gate (SARIF) - id: gate - shell: bash - env: - INPUT_PATH: ${{ inputs.path }} - SARIF_DIR: ${{ inputs.sarif-dir }} - run: | - set +e - mkdir -p "$SARIF_DIR" - rac gate "$INPUT_PATH" --sarif > "$SARIF_DIR/gate.sarif" - code=$? - echo "gate exited $code" - echo "exit_code=$code" >> "$GITHUB_OUTPUT" - - # A single SARIF document uploaded under one Code Scanning category. The gate - # is one tool ("rac") producing one run, so one categorised analysis suffices. - - name: Upload gate SARIF - if: ${{ always() && inputs.upload-sarif == 'true' }} - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ${{ inputs.sarif-dir }}/gate.sarif - category: rac-gate - - # Any non-zero CLI exit fails the check; the Code Scanning annotations show - # every finding, blocking and advisory. What is blocking is decided by the - # engine under the corpus enforcement policy (ADR-049), not here. - - name: Report result - shell: bash - env: - EXIT_CODE: ${{ steps.gate.outputs.exit_code }} - run: | - if [ "$EXIT_CODE" != "0" ]; then - echo "::error::RAC PR gate failed (exit $EXIT_CODE) — see the Code Scanning annotations." - exit "$EXIT_CODE" - fi - echo "RAC PR gate passed." diff --git a/tests/test_pr_gate_action.py b/tests/test_pr_gate_action.py deleted file mode 100644 index 79064fd0..00000000 --- a/tests/test_pr_gate_action.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Structural tests for the RAC PR-gate composite action (v0.21.14). - -The action is a thin wrapper that runs a single `rac gate --sarif` — one -command that composes validation, relationship integrity, and review under the -corpus enforcement policy — uploads the single SARIF document, and re-surfaces -the CLI exit code. Its analysis is owned by the (separately tested) CLI; these -tests pin the action's *contract* so the wiring cannot silently drift (ADR-063: -the action computes nothing). -""" - -from __future__ import annotations - -from pathlib import Path - -import yaml - -ACTION = Path(__file__).parent.parent / "pr-gate-action" / "action.yml" - - -def _action() -> dict: - return yaml.safe_load(ACTION.read_text(encoding="utf-8")) - - -def test_action_is_composite(): - a = _action() - assert a["runs"]["using"] == "composite" - assert a["name"] == "RAC PR gate" - - -def test_action_declares_expected_inputs(): - inputs = _action()["inputs"] - for name in ("path", "upload-sarif", "sarif-dir", "rac-version", "install-from"): - assert name in inputs, f"missing input: {name}" - assert inputs["path"]["default"] == "rac" - assert inputs["upload-sarif"]["default"] == "true" - - -def test_action_runs_single_gate_command(): - run_steps = " ".join(s.get("run", "") for s in _action()["runs"]["steps"]) - assert "rac gate" in run_steps - assert "--sarif" in run_steps - # The three separate checks are now collapsed into the one gate command. - assert "rac validate" not in run_steps - assert "rac relationships" not in run_steps - assert "rac review" not in run_steps - - -def test_action_uploads_single_sarif_once(): - steps = _action()["runs"]["steps"] - uploads = [s for s in steps if "upload-sarif" in str(s.get("uses", ""))] - assert len(uploads) == 1, "the gate uploads exactly one SARIF document" - # Upload even on failure so findings still annotate the PR. - assert "always()" in uploads[0]["if"] - assert uploads[0]["with"]["category"] == "rac-gate" - - -def test_action_resurfaces_exit_code(): - run_steps = " ".join(s.get("run", "") for s in _action()["runs"]["steps"]) - assert 'exit "$EXIT_CODE"' in run_steps - - -def test_action_install_supports_source_for_dogfood(): - # `install-from: source` lets the repo dogfood the action with uses: ./pr-gate-action. - run_steps = " ".join(s.get("run", "") for s in _action()["runs"]["steps"]) - assert "GITHUB_ACTION_PATH" in run_steps diff --git a/tests/test_validate_action.py b/tests/test_validate_action.py deleted file mode 100644 index f17193a2..00000000 --- a/tests/test_validate_action.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Structural tests for the RAC validate composite action (v0.17.2, ADR-058). - -The action is a thin wrapper over `rac validate --sarif`; its behaviour is owned -by the (separately tested) CLI. These tests pin the action's *contract* — that it -stays a composite action that runs `rac validate --sarif`, uploads SARIF, and -re-surfaces the CLI exit code — so the wiring cannot silently drift. -""" - -from __future__ import annotations - -from pathlib import Path - -import yaml - -ACTION = Path(__file__).parent.parent / "validate-action" / "action.yml" - - -def _action() -> dict: - return yaml.safe_load(ACTION.read_text(encoding="utf-8")) - - -def test_action_is_composite(): - a = _action() - assert a["runs"]["using"] == "composite" - assert a["name"] == "RAC validate" - - -def test_action_declares_expected_inputs(): - inputs = _action()["inputs"] - for name in ("path", "upload-sarif", "sarif-file", "rac-version", "install-from"): - assert name in inputs, f"missing input: {name}" - assert inputs["path"]["default"] == "rac" - assert inputs["upload-sarif"]["default"] == "true" - - -def test_action_runs_rac_validate_sarif(): - steps = _action()["runs"]["steps"] - run_steps = " ".join(s.get("run", "") for s in steps) - assert "rac validate" in run_steps - assert "--sarif" in run_steps - - -def test_action_uploads_sarif(): - steps = _action()["runs"]["steps"] - uploads = [s for s in steps if "upload-sarif" in str(s.get("uses", ""))] - assert uploads, "no SARIF upload step" - # Upload even on failure so findings still annotate the PR. - assert "always()" in uploads[0]["if"] - - -def test_action_resurfaces_exit_code(): - steps = _action()["runs"]["steps"] - run_steps = " ".join(s.get("run", "") for s in steps) - assert 'exit "$EXIT_CODE"' in run_steps - - -def test_action_install_supports_source_for_dogfood(): - # `install-from: source` lets the repo dogfood the action with uses: ./validate-action. - run_steps = " ".join(s.get("run", "") for s in _action()["runs"]["steps"]) - assert "GITHUB_ACTION_PATH" in run_steps diff --git a/validate-action/action.yml b/validate-action/action.yml deleted file mode 100644 index babfbcbc..00000000 --- a/validate-action/action.yml +++ /dev/null @@ -1,100 +0,0 @@ -# RAC validate composite action (v0.17.2, ADR-058). -# -# A thin wrapper: install RAC, run one `rac validate --sarif`, upload the SARIF -# to GitHub Code Scanning, and re-surface the CLI exit code. All analysis and -# severity policy live in the package (ADR-015 / ADR-053); the action never -# reinterprets findings. The Watchkeeper action lives at the repository root; -# this validate action is referenced as -# `uses: itsthelore/rac-core/validate-action@`. -name: "RAC validate" -description: >- - Validate a requirements-as-code (RAC) corpus and surface findings on the pull - request via GitHub Code Scanning (SARIF). A thin wrapper over the `rac` CLI. -author: "Tom Ballard" - -branding: - icon: "check-circle" - color: "purple" - -inputs: - path: - description: "The RAC corpus directory to validate (passed to `rac validate`)." - required: false - default: "rac" - upload-sarif: - description: >- - Upload SARIF to GitHub Code Scanning (`true` or `false`). Requires the job - to grant `security-events: write`. - required: false - default: "true" - sarif-file: - description: "Where the SARIF document is written." - required: false - default: "rac.sarif" - rac-version: - description: >- - Exact rac-core version to install from PyPI. Empty installs the - latest release. Ignored when `install-from` is `source`. - required: false - default: "" - install-from: - description: >- - Where to install RAC from: `pypi` (the default for consumers) or `source` - (the action checkout itself — for this repository's own dogfood job; - requires `uses: ./validate-action` on a full checkout). - required: false - default: "pypi" - -runs: - using: "composite" - steps: - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install RAC - shell: bash - env: - INSTALL_FROM: ${{ inputs.install-from }} - RAC_VERSION: ${{ inputs.rac-version }} - run: | - python -m pip install --quiet --upgrade pip - if [ "$INSTALL_FROM" = "source" ]; then - python -m pip install --quiet "$GITHUB_ACTION_PATH/.." - elif [ -n "$RAC_VERSION" ]; then - python -m pip install --quiet "rac-core==$RAC_VERSION" - else - python -m pip install --quiet rac-core - fi - - # The CLI is the source of truth (ADR-058). `set +e` lets a non-zero exit - # still produce SARIF for upload; the exit code is re-surfaced below. - - name: Run rac validate (SARIF) - id: validate - shell: bash - env: - INPUT_PATH: ${{ inputs.path }} - SARIF_FILE: ${{ inputs.sarif-file }} - run: | - set +e - rac validate "$INPUT_PATH" --sarif > "$SARIF_FILE" - echo "exit_code=$?" >> "$GITHUB_OUTPUT" - - - name: Upload SARIF to Code Scanning - if: ${{ always() && inputs.upload-sarif == 'true' }} - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: ${{ inputs.sarif-file }} - - # Errors fail the check; warnings (including findings downgraded in - # .rac/config.yaml, ADR-053) annotate without failing — warnings-first. - - name: Report result - shell: bash - env: - EXIT_CODE: ${{ steps.validate.outputs.exit_code }} - run: | - if [ "$EXIT_CODE" != "0" ]; then - echo "::error::rac validate exited $EXIT_CODE — see the Code Scanning annotations." - exit "$EXIT_CODE" - fi - echo "rac validate passed." From ac1ec9e436ecc17703bc58fc6fdd5db005b00e98 Mon Sep 17 00:00:00 2001 From: Tom Ballard Date: Mon, 29 Jun 2026 16:59:03 +0000 Subject: [PATCH 2/2] docs(decision): add ADR-096 for the rac-ci consolidation [roadmap:rac-ci] Implements rac/roadmaps/repo-topology/rac-ci.md. Records the rac-ci decisions: capability x platform layout (watchkeeper/gatekeeper/registrar + recordkeeper placeholder), wrappers install the published rac-core (source-install mode dropped), rac-core dogfoods via direct source-install CLI steps, and rac-ci is versioned with a moving major v1 tag (ADR-076 CalVer is for PyPI releases; rac-ci ships no package). Links the rac-ci roadmap to ADR-096 and regenerates the agent-rules managed blocks. --- .cursor/rules | 3 +- .github/copilot-instructions.md | 3 +- AGENTS.md | 3 +- CLAUDE.md | 3 +- rac/decisions/adr-096-rac-ci-consolidation.md | 145 ++++++++++++++++++ rac/roadmaps/repo-topology/rac-ci.md | 1 + 6 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 rac/decisions/adr-096-rac-ci-consolidation.md diff --git a/.cursor/rules b/.cursor/rules index 2bc70cff..a4373683 100644 --- a/.cursor/rules +++ b/.cursor/rules @@ -1,4 +1,4 @@ - + ## Settled decisions (RAC) @@ -68,4 +68,5 @@ These decisions are already accepted. Do not re-open or contradict them; ask the - **RAC-KW6HY8W1CBK6** — ADR-093: Roadmap Intent Lives in the Corpus; Execution Is Tracked in GitHub Issues _(Process)_ - **RAC-KW6NQ4Y814N7** — ADR-094: Roadmaps Are Identified by Stable Codename, Not a vX.Y.Z Scope-Fence _(Process)_ - **RAC-KW8ZTVEQJ366** — ADR-095: rac-connectors Repackage, PyPI Rename, and Outbound Field Rename _(Architecture)_ +- **RAC-KWA4ZD9Q6EPZ** — ADR-096: rac-ci Consolidation — Capability × Platform, Source-Install Dogfood, v1 Tagging _(Architecture)_ diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2bc70cff..a4373683 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,4 +1,4 @@ - + ## Settled decisions (RAC) @@ -68,4 +68,5 @@ These decisions are already accepted. Do not re-open or contradict them; ask the - **RAC-KW6HY8W1CBK6** — ADR-093: Roadmap Intent Lives in the Corpus; Execution Is Tracked in GitHub Issues _(Process)_ - **RAC-KW6NQ4Y814N7** — ADR-094: Roadmaps Are Identified by Stable Codename, Not a vX.Y.Z Scope-Fence _(Process)_ - **RAC-KW8ZTVEQJ366** — ADR-095: rac-connectors Repackage, PyPI Rename, and Outbound Field Rename _(Architecture)_ +- **RAC-KWA4ZD9Q6EPZ** — ADR-096: rac-ci Consolidation — Capability × Platform, Source-Install Dogfood, v1 Tagging _(Architecture)_ diff --git a/AGENTS.md b/AGENTS.md index 2bc70cff..a4373683 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ - + ## Settled decisions (RAC) @@ -68,4 +68,5 @@ These decisions are already accepted. Do not re-open or contradict them; ask the - **RAC-KW6HY8W1CBK6** — ADR-093: Roadmap Intent Lives in the Corpus; Execution Is Tracked in GitHub Issues _(Process)_ - **RAC-KW6NQ4Y814N7** — ADR-094: Roadmaps Are Identified by Stable Codename, Not a vX.Y.Z Scope-Fence _(Process)_ - **RAC-KW8ZTVEQJ366** — ADR-095: rac-connectors Repackage, PyPI Rename, and Outbound Field Rename _(Architecture)_ +- **RAC-KWA4ZD9Q6EPZ** — ADR-096: rac-ci Consolidation — Capability × Platform, Source-Install Dogfood, v1 Tagging _(Architecture)_ diff --git a/CLAUDE.md b/CLAUDE.md index d5b49644..b48f4d9c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ to the corpus artifact and they load through the imports below. - Previous series: `rac/roadmaps/v0.21.x-editor/` (complete) - Decisions (ADRs): `rac/decisions/` - + ## Settled decisions (RAC) @@ -93,4 +93,5 @@ These decisions are already accepted. Do not re-open or contradict them; ask the - **RAC-KW6HY8W1CBK6** — ADR-093: Roadmap Intent Lives in the Corpus; Execution Is Tracked in GitHub Issues _(Process)_ - **RAC-KW6NQ4Y814N7** — ADR-094: Roadmaps Are Identified by Stable Codename, Not a vX.Y.Z Scope-Fence _(Process)_ - **RAC-KW8ZTVEQJ366** — ADR-095: rac-connectors Repackage, PyPI Rename, and Outbound Field Rename _(Architecture)_ +- **RAC-KWA4ZD9Q6EPZ** — ADR-096: rac-ci Consolidation — Capability × Platform, Source-Install Dogfood, v1 Tagging _(Architecture)_ diff --git a/rac/decisions/adr-096-rac-ci-consolidation.md b/rac/decisions/adr-096-rac-ci-consolidation.md new file mode 100644 index 00000000..a885287d --- /dev/null +++ b/rac/decisions/adr-096-rac-ci-consolidation.md @@ -0,0 +1,145 @@ +--- +schema_version: 1 +id: RAC-KWA4ZD9Q6EPZ +type: decision +tags: [structure, org, ci, distribution] +--- +# ADR-096: rac-ci Consolidation — Capability × Platform, Source-Install Dogfood, v1 Tagging + +## Context + +ADR-092 places all CI delivery in a single `rac-ci` repository and the `rac-ci` +roadmap consolidates the action sources that had lived in `rac-core` +(`action.yml`, `pr-gate-action/`, `validate-action/`) plus the standalone +`rac-actions` / `lore-watchkeeper` / `lore-gatekeeper` stubs. Landing that move +surfaced three choices the roadmap left open and that this ADR settles, since they +exceed a pure relocation. + +`rac-core` **dogfooded** its own wrappers via `uses: ./` with `install-from: +source`, and the wrappers' `source` branch installed the in-repo engine from +`$GITHUB_ACTION_PATH`. That mode cannot survive the move (the rac-ci subdirs are +not the engine package), so the dogfood mechanism and the wrapper install surface +had to be decided, along with how the actions repo is versioned. + +## Decision + +### Capability × platform layout + +`rac-ci` holds one subdir per **capability**, platform nested within (`github/` +first): + +- `watchkeeper/github/` — `rac watchkeeper` (PR knowledge review). +- `gatekeeper/github/` — `rac gate --sarif` (required merge gate). +- `registrar/github/` — `rac validate --sarif` (well-formedness, ADR-058). The + validate capability is named **Registrar**; it is a distinct, separately + pinnable action, not folded into the gate. +- `recordkeeper/` — placeholder for the ADR-084 read-access recorder (no source + yet). + +`bitbucket/` and `jenkins/` join under each capability when demanded; the engine +is platform-neutral (SARIF/JSON), so that work is in the wrappers. + +### Wrappers install the published engine + +The `rac-ci` wrappers install the published `rac-core` from PyPI (default) or a +pinned `rac-version`. The previous `install-from: source` mode is removed — its +only consumer was `rac-core`'s own dogfood. + +### rac-core dogfoods via source-install, not its own wrappers + +`rac-core`'s pre-merge checks run the gates as **direct source-install CLI steps** +(`pip install -e .` then `rac watchkeeper` / `rac validate --sarif` / `rac gate +--sarif`), the same shape as its existing agent-rules / eval / doctor jobs. +`rac-core` does not consume its own published wrappers — it dogfoods the engine +itself, stays self-contained, and avoids a circular dependency on a `rac-ci` +release tag. The wrapper-contract tests move to `rac-ci`. + +### Versioning: a moving major `v1` + +`rac-ci` is consumed as `uses: itsthelore/rac-ci//github@` and +ships **no PyPI artifact**, so ADR-076's CalVer (whose rationale is PyPI release +sorting) does not apply. `rac-ci` publishes a **moving major tag `v1`** that +consumers pin and that is repointed on each release — the GitHub Actions idiom. +The `setuptools-scm` constraint that forbade a moving tag in `rac-core` does not +exist here (no package). + +### Versioned cutover + +Old `rac-core` action references stay resolvable on their existing tags +(`itsthelore/rac-core@`, `…/validate-action@v0`, `…/pr-gate-action@v0`); +only `@main` and future tags drop the actions. New consumers use the `rac-ci` +paths. The predecessor repos are archived with redirect notes. + +## Consequences + +### Positive + +- One bounded CI repo, capability-first, consistent with ADR-092; the per-action + repos retire. +- `rac-core`'s dogfood tests the real engine and carries no dependency on a + `rac-ci` tag. +- Actions versioning follows the ecosystem idiom (`@v1`) instead of a PyPI scheme + that does not fit. + +### Negative + +- The wrappers lose `install-from: source`; anyone who relied on it (only + `rac-core` did) uses a source install of the CLI directly instead. +- A moving `v1` is mutable by design; consumers wanting immutability pin a SHA. + +### Risks + +- A consumer pinned to an old in-repo action path breaks if those tags are + deleted. Mitigation: retain the old `rac-core` tags; cut over by version. + +## Status + +Accepted + +## Category + +Architecture + +## Alternatives Considered + +### rac-core consumes rac-ci's own actions for dogfood + +Rejected: it couples `rac-core`'s CI to a `rac-ci` release tag (chicken-and-egg on +the first cut) and tests the wrapper rather than the engine. Source-install +dogfood is self-contained and more honest. + +### CalVer (`YYYY.MM.N`) tags for rac-ci + +Rejected: ADR-076's CalVer exists for PyPI release sorting; `rac-ci` ships no +package and is consumed by `uses:` ref, where a moving major `v1` is the +convention. + +### Fold validate into the gate + +Rejected: `rac gate` already runs validation, but the standalone Registrar +(`rac validate --sarif`, ADR-058) is a lighter, separately pinnable check +consumers adopt first; keeping it distinct preserves that adoption path. + +## Relationship to Other Decisions + +- ADR-092: realises the `rac-ci` repo in the topology. +- ADR-064: restores the single-CI-repo consolidation intent. +- ADR-068: retires the per-action `lore-watchkeeper` / `lore-gatekeeper` repos. +- ADR-090: capability-first satellite intent, realised as capability subdirs. +- ADR-063, ADR-058, ADR-049: the wrappers stay thin CLI consumers. +- ADR-076: CalVer governs PyPI releases; `rac-ci` (no package) uses a moving `v1`. + +## Related Decisions + +- adr-092 +- adr-064 +- adr-068 +- adr-090 +- adr-063 +- adr-058 +- adr-076 + +## Related Roadmaps + +- rac-ci +- repo-topology-convergence diff --git a/rac/roadmaps/repo-topology/rac-ci.md b/rac/roadmaps/repo-topology/rac-ci.md index 3d5e72d2..0173ba3f 100644 --- a/rac/roadmaps/repo-topology/rac-ci.md +++ b/rac/roadmaps/repo-topology/rac-ci.md @@ -84,6 +84,7 @@ GitHub→other-platform work is in the thin wrappers, not the engine. - adr-049 - adr-058 - adr-090 +- adr-096 ## Related Roadmaps