Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: tests

# Pin the capability wrappers' contract: each action.yml stays a thin composite
# action that runs the right `rac` command, uploads SARIF, and re-surfaces the
# action that runs the right `decided` command, uploads SARIF, and re-surfaces the
# CLI exit code. Structural only (parses YAML) — no engine install needed.
on:
push:
Expand Down Expand Up @@ -29,7 +29,7 @@ jobs:
run: python -m pytest tests/ -q --ignore=tests/test_herald_renderer.py

# The Herald renderer is behavioral, not just structural: it shells the real
# `rac decisions-for --json` (published rac-core — the same contract the
# `decided decisions-for --json` (published asdecided-core — the same contract the
# wrapper consumes in the field, ADR-063) and must render byte-identical
# comments for the same corpus and diff. Kept out of the structural job so
# that tier stays engine-free.
Expand All @@ -43,8 +43,11 @@ jobs:
with:
python-version: "3.12"

- name: Install test deps and the published engine
run: python -m pip install --quiet --upgrade pip pytest rac-core
- name: Install test deps
run: python -m pip install --quiet --upgrade pip pytest

- name: Install the verified native engine
run: bash shared/install-native.sh 0.23.1

- name: Run the renderer battery
run: python -m pytest tests/test_herald_renderer.py -q
31 changes: 17 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
# rac-ci
# asdecided-ci

The CI delivery surface for [RAC](https://github.com/itsthelore/rac-core)
The CI delivery surface for [RAC](https://github.com/itsthelore/asdecided-core)
(requirements-as-code) — one subdir per **capability**, with delivery platforms
nested inside (`github/` first). Per ADR-092 (one repo per concern, subdir per
member) this consolidates the CI wrappers that previously lived in `rac-core` and
member) this consolidates the CI wrappers that previously lived in `asdecided-core` and
in the standalone `rac-actions` / `lore-watchkeeper` / `lore-gatekeeper` repos.

Every capability is a **thin wrapper over the public `rac` CLI** (ADR-063); all
analysis and policy live in the engine package. The wrappers install the
published `rac-core` from PyPI (pin with the `rac-version` input).
Every capability is a **thin wrapper over the public `decided` CLI** (ADR-063);
all analysis and policy live in the native engine. The wrappers download a
checksum-verified `asdecided-core` release (pin with the
`asdecided-version` input). Python is used only by Herald's comment renderer,
not as the engine.

## Capabilities

| Capability | Subdir | Wraps | Consumed as |
| --- | --- | --- | --- |
| Watchkeeper | [`watchkeeper/github/`](watchkeeper/github/) | `rac watchkeeper` (PR knowledge review) | `uses: itsthelore/rac-ci/watchkeeper/github@<ref>` |
| Gatekeeper | [`gatekeeper/github/`](gatekeeper/github/) | `rac gate --sarif` (required merge gate) | `uses: itsthelore/rac-ci/gatekeeper/github@<ref>` |
| Registrar | [`registrar/github/`](registrar/github/) | `rac validate --sarif` (well-formedness, ADR-058) | `uses: itsthelore/rac-ci/registrar/github@<ref>` |
| Herald | [`herald/github/`](herald/github/) | `rac decisions-for --json` (advisory governing-decisions comment on PRs) | `uses: itsthelore/rac-ci/herald/github@<ref>` |
| Watchkeeper | [`watchkeeper/github/`](watchkeeper/github/) | `decided watchkeeper` (PR knowledge review) | `uses: itsthelore/asdecided-ci/watchkeeper/github@<ref>` |
| Gatekeeper | [`gatekeeper/github/`](gatekeeper/github/) | `decided gate --sarif` (required merge gate) | `uses: itsthelore/asdecided-ci/gatekeeper/github@<ref>` |
| Registrar | [`registrar/github/`](registrar/github/) | `decided validate --sarif` (well-formedness, ADR-058) | `uses: itsthelore/asdecided-ci/registrar/github@<ref>` |
| Herald | [`herald/github/`](herald/github/) | `decided decisions-for --json` (advisory governing-decisions comment on PRs) | `uses: itsthelore/asdecided-ci/herald/github@<ref>` |
| Recordkeeper | [`recordkeeper/`](recordkeeper/) | read-access audit recorder (ADR-084) | *placeholder — not yet shipped* |

A reusable Watchkeeper workflow is also published at
[`.github/workflows/watchkeeper.yml`](.github/workflows/watchkeeper.yml)
(`uses: itsthelore/rac-ci/.github/workflows/watchkeeper.yml@<ref>`).
(`uses: itsthelore/asdecided-ci/.github/workflows/watchkeeper.yml@<ref>`).

`bitbucket/` and `jenkins/` platform wrappers join under each capability when
demanded; the engine is already platform-neutral (SARIF/JSON), so that work is in
Expand All @@ -31,6 +33,7 @@ the wrappers, not the engine.
## History

The `watchkeeper/`, `gatekeeper/`, and `registrar/` wrappers moved here from
`rac-core` with history preserved (ADR-092 convergence). Consumers pinned to the
old `itsthelore/rac-core@<tag>`, `…/pr-gate-action@v0`, or `…/validate-action@v0`
paths keep resolving on those tags; new consumers use the `rac-ci` paths above.
`asdecided-core` with history preserved (ADR-092 convergence). Consumers pinned to the
old `itsthelore/asdecided-core@<tag>`, `…/pr-gate-action@v0`, or `…/validate-action@v0`
paths keep resolving on those tags; new consumers use the `asdecided-ci` paths
above.
35 changes: 12 additions & 23 deletions gatekeeper/github/action.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 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,
# install RAC and run `decided 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
Expand All @@ -11,11 +11,11 @@
#
# The Watchkeeper action lives at `watchkeeper/github/` and the validate
# (Registrar) action at `registrar/github/`; this gate is referenced as
# `uses: itsthelore/rac-ci/gatekeeper/github@<ref>`.
# `uses: itsthelore/asdecided-ci/gatekeeper/github@<ref>`.
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
`decided 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"
Expand All @@ -26,7 +26,7 @@ branding:

inputs:
path:
description: "The RAC corpus directory to enforce (passed to `rac gate`)."
description: "The RAC corpus directory to enforce (passed to `decided gate`)."
required: false
default: "rac"
upload-sarif:
Expand All @@ -39,35 +39,24 @@ inputs:
description: "Directory the single `gate.sarif` document is written to."
required: false
default: "rac-sarif"
rac-version:
asdecided-version:
description: >-
Exact rac-core version to install from PyPI. Empty installs the
latest release.
Verified native asdecided-core release to install.
required: false
default: ""
default: "0.23.1"

runs:
using: "composite"
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install RAC
- name: Install AsDecided
shell: bash
env:
RAC_VERSION: ${{ inputs.rac-version }}
run: |
python -m pip install --quiet --upgrade pip
if [ -n "$RAC_VERSION" ]; then
python -m pip install --quiet "rac-core==$RAC_VERSION"
else
python -m pip install --quiet rac-core
fi
bash "$GITHUB_ACTION_PATH/../../shared/install-native.sh" \
"${{ inputs.asdecided-version }}"

# 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,
# command, one SARIF document — `decided gate` already composes validation,
# relationships, and review under the corpus enforcement policy.
- name: Run RAC gate (SARIF)
id: gate
Expand All @@ -78,7 +67,7 @@ runs:
run: |
set +e
mkdir -p "$SARIF_DIR"
rac gate "$INPUT_PATH" --sarif > "$SARIF_DIR/gate.sarif"
decided gate "$INPUT_PATH" --sarif > "$SARIF_DIR/gate.sarif"
code=$?
echo "gate exited $code"
echo "exit_code=$code" >> "$GITHUB_OUTPUT"
Expand Down
33 changes: 13 additions & 20 deletions herald/github/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# Proclaims the recorded decisions that govern a pull request's changed paths
# as ONE advisory comment: compute the merge-base diff, run
# `rac decisions-for --json` per changed path (the live-decisions lookup the
# `decided decisions-for --json` per changed path (the live-decisions lookup the
# CLI and MCP serve, `decision-to-code-proximity`), and post a terse,
# deduplicated comment that re-runs update in place. Facts, never verdicts
# (ADR-034): the comment names what governs and recommends review; it never
Expand All @@ -15,26 +15,26 @@
#
# Siblings: Watchkeeper at `watchkeeper/github/`, the gate at
# `gatekeeper/github/`, validate at `registrar/github/`. This surface is
# referenced as `uses: itsthelore/rac-ci/herald/github@<ref>`.
# referenced as `uses: itsthelore/asdecided-ci/herald/github@<ref>`.
#
# Requirements: a pull_request-triggered workflow, `fetch-depth: 0` on the
# checkout (the diff needs the merge base, like Watchkeeper), and
# `pull-requests: write` on the job for the comment. On forks, where the
# token is read-only, the comment falls back to the step summary instead of
# failing the check.
name: "Lore decisions on PR"
name: "AsDecided decisions on PR"
description: >-
Comment the recorded decisions that govern a pull request's changed paths —
id, title, and the declared scope that matched — as one advisory,
update-in-place comment. A thin wrapper over `rac decisions-for`; facts,
update-in-place comment. A thin wrapper over `decided decisions-for`; facts,
never a merge gate.
author: "Tom Ballard"
branding:
icon: "book-open"
color: "purple"
inputs:
path:
description: "The RAC corpus directory (passed to `rac decisions-for`)."
description: "The RAC corpus directory (passed to `decided decisions-for`)."
required: false
default: "rac"
max-inline:
Expand All @@ -43,29 +43,22 @@ inputs:
details expander.
required: false
default: "5"
rac-version:
asdecided-version:
description: >-
Exact rac-core version to install from PyPI. Empty installs the
latest release.
Verified native asdecided-core release to install.
required: false
default: ""
default: "0.23.1"
runs:
using: "composite"
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install RAC
- name: Install AsDecided
shell: bash
env:
RAC_VERSION: ${{ inputs.rac-version }}
run: |
python -m pip install --quiet --upgrade pip
if [ -n "$RAC_VERSION" ]; then
python -m pip install --quiet "rac-core==$RAC_VERSION"
else
python -m pip install --quiet rac-core
fi
bash "$GITHUB_ACTION_PATH/../../shared/install-native.sh" \
"${{ inputs.asdecided-version }}"
# The PR's changed paths, from the merge base (triple-dot): what the PR
# itself changes, correct even when the base branch has moved ahead.
# Requires fetch-depth: 0 on the checkout.
Expand All @@ -76,12 +69,12 @@ runs:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ -z "$BASE_SHA" ] || [ -z "$HEAD_SHA" ]; then
echo "::error::Lore decisions on PR runs on pull_request events only (no base/head sha in this event)."
echo "::error::AsDecided decisions on PR runs on pull_request events only (no base/head sha in this event)."
exit 1
fi
git diff --name-only "$BASE_SHA...$HEAD_SHA" > lore-changed-paths.txt
echo "$(wc -l < lore-changed-paths.txt) changed path(s)"
# One deterministic comment body: `rac decisions-for --json` per changed
# One deterministic comment body: `decided decisions-for --json` per changed
# path, merged by decision id, sorted, no wall-clock input — the same
# bytes for the same corpus and diff.
- name: Render governing decisions
Expand Down
18 changes: 9 additions & 9 deletions herald/github/render.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Render the decisions-on-PR comment from `rac decisions-for --json`.
"""Render the decisions-on-PR comment from `decided decisions-for --json`.

The thin-client half of the `pr-decision-surfacing` design (ADR-063): run the
engine's live-decisions lookup for each changed path, merge by decision id,
Expand Down Expand Up @@ -34,17 +34,17 @@
_PATHS_SHOWN = 3


def _lookup(rac_bin: str, path: str, corpus: str) -> dict | None:
"""One `rac decisions-for <path> <corpus> --json` call, parsed, or None."""
def _lookup(decided_bin: str, path: str, corpus: str) -> dict | None:
"""One `decided decisions-for <path> <corpus> --json` call, parsed, or None."""
try:
proc = subprocess.run(
[rac_bin, "decisions-for", path, corpus, "--json"],
[decided_bin, "decisions-for", path, corpus, "--json"],
capture_output=True,
text=True,
check=False,
)
except FileNotFoundError:
print(f"error: rac binary not found: {rac_bin!r}", file=sys.stderr)
print(f"error: decided binary not found: {decided_bin!r}", file=sys.stderr)
raise SystemExit(1) from None
if proc.returncode != 0:
print(
Expand All @@ -59,7 +59,7 @@ def _lookup(rac_bin: str, path: str, corpus: str) -> dict | None:
return None


def collect(rac_bin: str, corpus: str, changed_paths: list[str]) -> list[dict]:
def collect(decided_bin: str, corpus: str, changed_paths: list[str]) -> list[dict]:
"""Governing decisions merged across paths, sorted by decision id.

Each entry: id, title, status, path (the artifact file), matching entries
Expand All @@ -68,7 +68,7 @@ def collect(rac_bin: str, corpus: str, changed_paths: list[str]) -> list[dict]:
"""
merged: dict[str, dict] = {}
for changed in changed_paths:
result = _lookup(rac_bin, changed, corpus)
result = _lookup(decided_bin, changed, corpus)
if not result:
continue
for decision in result.get("decisions", []):
Expand Down Expand Up @@ -143,11 +143,11 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--link-base", default="")
parser.add_argument("--max-inline", type=int, default=5)
parser.add_argument("--out", required=True)
parser.add_argument("--rac-bin", default="rac")
parser.add_argument("--decided-bin", default="decided")
args = parser.parse_args(argv)
with open(args.paths_file, encoding="utf-8") as fh:
changed_paths = sorted({line.strip() for line in fh if line.strip()})
decisions = collect(args.rac_bin, args.corpus, changed_paths)
decisions = collect(args.decided_bin, args.corpus, changed_paths)
body = render(decisions, args.link_base.rstrip("/"), args.max_inline)
with open(args.out, "w", encoding="utf-8") as fh:
fh.write(body)
Expand Down
3 changes: 2 additions & 1 deletion recordkeeper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Reserved for the **read-access audit recorder** (ADR-084) — a CI capability that
records who read which artifacts. No wrapper ships yet; this directory holds the
capability's place in the `rac-ci` layout so it joins as `recordkeeper/github/`
capability's place in the `asdecided-ci` layout so it joins as
`recordkeeper/github/`
(and other platforms) when the recorder lands, without re-litigating repository
structure.
Loading