Skip to content

Commit a759478

Browse files
authored
feat(ci): seed rac-ci with the watchkeeper/gatekeeper/registrar wrappers [roadmap:rac-ci]
2 parents d27c5fb + c701ded commit a759478

11 files changed

Lines changed: 586 additions & 0 deletions

File tree

.claude/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"attribution": {
3+
"commit": "",
4+
"pr": "",
5+
"sessionUrl": false
6+
}
7+
}

.github/workflows/tests.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: tests
2+
3+
# Pin the capability wrappers' contract: each action.yml stays a thin composite
4+
# action that runs the right `rac` command, uploads SARIF, and re-surfaces the
5+
# CLI exit code. Structural only (parses YAML) — no engine install needed.
6+
on:
7+
push:
8+
branches: [main]
9+
pull_request:
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
action-contracts:
16+
name: action contracts (pytest)
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v5
20+
21+
- uses: actions/setup-python@v6
22+
with:
23+
python-version: "3.12"
24+
25+
- name: Install test deps
26+
run: python -m pip install --quiet --upgrade pip pytest pyyaml
27+
28+
- name: Run action-contract tests
29+
run: python -m pytest tests/ -q

.github/workflows/watchkeeper.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Reusable Watchkeeper workflow (v0.12.3).
2+
#
3+
# Consumers call this with `uses:` to get product knowledge review on pull
4+
# requests without writing checkout/base-ref wiring themselves:
5+
#
6+
# jobs:
7+
# watchkeeper:
8+
# uses: itsthelore/rac-ci/.github/workflows/watchkeeper.yml@<tag>
9+
# with:
10+
# path: rac
11+
#
12+
# Pin an exact release tag. No moving major tag is published: package
13+
# versions derive from git tags via setuptools-scm, and a floating tag
14+
# would corrupt version derivation (see docs/watchkeeper.md).
15+
name: Watchkeeper
16+
17+
on:
18+
workflow_call:
19+
inputs:
20+
path:
21+
description: "Corpus directory to compare."
22+
type: string
23+
required: false
24+
default: "rac"
25+
base:
26+
description: "Base revision (empty: origin/<PR base branch>)."
27+
type: string
28+
required: false
29+
default: ""
30+
fail-on:
31+
description: "Failure policy: error, warning, or none."
32+
type: string
33+
required: false
34+
default: "error"
35+
annotate:
36+
description: "Emit inline annotations."
37+
type: boolean
38+
required: false
39+
default: true
40+
rac-version:
41+
description: "Exact rac-core version from PyPI (empty: latest)."
42+
type: string
43+
required: false
44+
default: ""
45+
46+
permissions:
47+
contents: read
48+
49+
jobs:
50+
watchkeeper:
51+
name: watchkeeper
52+
runs-on: ubuntu-latest
53+
steps:
54+
- uses: actions/checkout@v5
55+
with:
56+
# The comparison needs the base revision's history, and a
57+
# source-mode install needs git metadata for setuptools-scm.
58+
fetch-depth: 0
59+
60+
# Reusable workflows run in the caller's repository, so the action is
61+
# referenced by full path. Update this ref when cutting a release tag;
62+
# until the first rac-ci release it tracks main.
63+
- uses: itsthelore/rac-ci/watchkeeper/github@main
64+
with:
65+
path: ${{ inputs.path }}
66+
base: ${{ inputs.base }}
67+
fail-on: ${{ inputs.fail-on }}
68+
annotate: ${{ inputs.annotate }}
69+
rac-version: ${{ inputs.rac-version }}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__/
2+
*.pyc
3+
.pytest_cache/

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# rac-ci
2+
3+
The CI delivery surface for [RAC](https://github.com/itsthelore/rac-core)
4+
(requirements-as-code) — one subdir per **capability**, with delivery platforms
5+
nested inside (`github/` first). Per ADR-092 (one repo per concern, subdir per
6+
member) this consolidates the CI wrappers that previously lived in `rac-core` and
7+
in the standalone `rac-actions` / `lore-watchkeeper` / `lore-gatekeeper` repos.
8+
9+
Every capability is a **thin wrapper over the public `rac` CLI** (ADR-063); all
10+
analysis and policy live in the engine package. The wrappers install the
11+
published `rac-core` from PyPI (pin with the `rac-version` input).
12+
13+
## Capabilities
14+
15+
| Capability | Subdir | Wraps | Consumed as |
16+
| --- | --- | --- | --- |
17+
| Watchkeeper | [`watchkeeper/github/`](watchkeeper/github/) | `rac watchkeeper` (PR knowledge review) | `uses: itsthelore/rac-ci/watchkeeper/github@<ref>` |
18+
| Gatekeeper | [`gatekeeper/github/`](gatekeeper/github/) | `rac gate --sarif` (required merge gate) | `uses: itsthelore/rac-ci/gatekeeper/github@<ref>` |
19+
| Registrar | [`registrar/github/`](registrar/github/) | `rac validate --sarif` (well-formedness, ADR-058) | `uses: itsthelore/rac-ci/registrar/github@<ref>` |
20+
| Recordkeeper | [`recordkeeper/`](recordkeeper/) | read-access audit recorder (ADR-084) | *placeholder — not yet shipped* |
21+
22+
A reusable Watchkeeper workflow is also published at
23+
[`.github/workflows/watchkeeper.yml`](.github/workflows/watchkeeper.yml)
24+
(`uses: itsthelore/rac-ci/.github/workflows/watchkeeper.yml@<ref>`).
25+
26+
`bitbucket/` and `jenkins/` platform wrappers join under each capability when
27+
demanded; the engine is already platform-neutral (SARIF/JSON), so that work is in
28+
the wrappers, not the engine.
29+
30+
## History
31+
32+
The `watchkeeper/`, `gatekeeper/`, and `registrar/` wrappers moved here from
33+
`rac-core` with history preserved (ADR-092 convergence). Consumers pinned to the
34+
old `itsthelore/rac-core@<tag>`, `…/pr-gate-action@v0`, or `…/validate-action@v0`
35+
paths keep resolving on those tags; new consumers use the `rac-ci` paths above.

gatekeeper/github/action.yml

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# RAC PR-gate composite action (v0.21.14, ADR-049 / ADR-063).
2+
#
3+
# Carries the full RAC contract into a single required pull-request check:
4+
# install RAC and run `rac gate` — one command that enforces validation,
5+
# relationship integrity, and review under the corpus enforcement policy
6+
# (`.rac/config.yaml`), emitting one SARIF document (ADR-054). The single SARIF
7+
# is uploaded to GitHub Code Scanning under one category (`rac-gate`), and the
8+
# CLI exit code is re-surfaced. The action is a thin consumer (ADR-063): all
9+
# analysis and enforcement policy live in the package (ADR-015 / ADR-049); the
10+
# action never reinterprets findings or decides what is blocking.
11+
#
12+
# The Watchkeeper action lives at `watchkeeper/github/` and the validate
13+
# (Registrar) action at `registrar/github/`; this gate is referenced as
14+
# `uses: itsthelore/rac-ci/gatekeeper/github@<ref>`.
15+
name: "RAC PR gate"
16+
description: >-
17+
Enforce a requirements-as-code (RAC) corpus on a pull request with a single
18+
`rac gate` command — validation, relationship integrity, and review under the
19+
corpus enforcement policy — surfaced inline via GitHub Code Scanning (SARIF) as
20+
a required status check. A thin wrapper over the `rac` CLI.
21+
author: "Tom Ballard"
22+
23+
branding:
24+
icon: "shield"
25+
color: "purple"
26+
27+
inputs:
28+
path:
29+
description: "The RAC corpus directory to enforce (passed to `rac gate`)."
30+
required: false
31+
default: "rac"
32+
upload-sarif:
33+
description: >-
34+
Upload SARIF to GitHub Code Scanning (`true` or `false`). Requires the job
35+
to grant `security-events: write`.
36+
required: false
37+
default: "true"
38+
sarif-dir:
39+
description: "Directory the single `gate.sarif` document is written to."
40+
required: false
41+
default: "rac-sarif"
42+
rac-version:
43+
description: >-
44+
Exact rac-core version to install from PyPI. Empty installs the
45+
latest release.
46+
required: false
47+
default: ""
48+
49+
runs:
50+
using: "composite"
51+
steps:
52+
- uses: actions/setup-python@v5
53+
with:
54+
python-version: "3.12"
55+
56+
- name: Install RAC
57+
shell: bash
58+
env:
59+
RAC_VERSION: ${{ inputs.rac-version }}
60+
run: |
61+
python -m pip install --quiet --upgrade pip
62+
if [ -n "$RAC_VERSION" ]; then
63+
python -m pip install --quiet "rac-core==$RAC_VERSION"
64+
else
65+
python -m pip install --quiet rac-core
66+
fi
67+
68+
# The CLI is the source of truth (ADR-058). `set +e` lets a non-zero exit
69+
# still produce SARIF for upload; the exit code is re-surfaced below. One
70+
# command, one SARIF document — `rac gate` already composes validation,
71+
# relationships, and review under the corpus enforcement policy.
72+
- name: Run RAC gate (SARIF)
73+
id: gate
74+
shell: bash
75+
env:
76+
INPUT_PATH: ${{ inputs.path }}
77+
SARIF_DIR: ${{ inputs.sarif-dir }}
78+
run: |
79+
set +e
80+
mkdir -p "$SARIF_DIR"
81+
rac gate "$INPUT_PATH" --sarif > "$SARIF_DIR/gate.sarif"
82+
code=$?
83+
echo "gate exited $code"
84+
echo "exit_code=$code" >> "$GITHUB_OUTPUT"
85+
86+
# A single SARIF document uploaded under one Code Scanning category. The gate
87+
# is one tool ("rac") producing one run, so one categorised analysis suffices.
88+
- name: Upload gate SARIF
89+
if: ${{ always() && inputs.upload-sarif == 'true' }}
90+
uses: github/codeql-action/upload-sarif@v3
91+
with:
92+
sarif_file: ${{ inputs.sarif-dir }}/gate.sarif
93+
category: rac-gate
94+
95+
# Any non-zero CLI exit fails the check; the Code Scanning annotations show
96+
# every finding, blocking and advisory. What is blocking is decided by the
97+
# engine under the corpus enforcement policy (ADR-049), not here.
98+
- name: Report result
99+
shell: bash
100+
env:
101+
EXIT_CODE: ${{ steps.gate.outputs.exit_code }}
102+
run: |
103+
if [ "$EXIT_CODE" != "0" ]; then
104+
echo "::error::RAC PR gate failed (exit $EXIT_CODE) — see the Code Scanning annotations."
105+
exit "$EXIT_CODE"
106+
fi
107+
echo "RAC PR gate passed."

recordkeeper/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# recordkeeper (placeholder)
2+
3+
Reserved for the **read-access audit recorder** (ADR-084) — a CI capability that
4+
records who read which artifacts. No wrapper ships yet; this directory holds the
5+
capability's place in the `rac-ci` layout so it joins as `recordkeeper/github/`
6+
(and other platforms) when the recorder lands, without re-litigating repository
7+
structure.

registrar/github/action.yml

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# RAC validate composite action (v0.17.2, ADR-058).
2+
#
3+
# A thin wrapper: install RAC, run one `rac validate --sarif`, upload the SARIF
4+
# to GitHub Code Scanning, and re-surface the CLI exit code. All analysis and
5+
# severity policy live in the package (ADR-015 / ADR-053); the action never
6+
# reinterprets findings. The Watchkeeper action lives at `watchkeeper/github/`;
7+
# this Registrar (validate) action is referenced as
8+
# `uses: itsthelore/rac-ci/registrar/github@<ref>`.
9+
name: "RAC Registrar"
10+
description: >-
11+
Validate a requirements-as-code (RAC) corpus and surface findings on the pull
12+
request via GitHub Code Scanning (SARIF). A thin wrapper over the `rac` CLI.
13+
author: "Tom Ballard"
14+
15+
branding:
16+
icon: "check-circle"
17+
color: "purple"
18+
19+
inputs:
20+
path:
21+
description: "The RAC corpus directory to validate (passed to `rac validate`)."
22+
required: false
23+
default: "rac"
24+
upload-sarif:
25+
description: >-
26+
Upload SARIF to GitHub Code Scanning (`true` or `false`). Requires the job
27+
to grant `security-events: write`.
28+
required: false
29+
default: "true"
30+
sarif-file:
31+
description: "Where the SARIF document is written."
32+
required: false
33+
default: "rac.sarif"
34+
rac-version:
35+
description: >-
36+
Exact rac-core version to install from PyPI. Empty installs the
37+
latest release.
38+
required: false
39+
default: ""
40+
41+
runs:
42+
using: "composite"
43+
steps:
44+
- uses: actions/setup-python@v5
45+
with:
46+
python-version: "3.12"
47+
48+
- name: Install RAC
49+
shell: bash
50+
env:
51+
RAC_VERSION: ${{ inputs.rac-version }}
52+
run: |
53+
python -m pip install --quiet --upgrade pip
54+
if [ -n "$RAC_VERSION" ]; then
55+
python -m pip install --quiet "rac-core==$RAC_VERSION"
56+
else
57+
python -m pip install --quiet rac-core
58+
fi
59+
60+
# The CLI is the source of truth (ADR-058). `set +e` lets a non-zero exit
61+
# still produce SARIF for upload; the exit code is re-surfaced below.
62+
- name: Run rac validate (SARIF)
63+
id: validate
64+
shell: bash
65+
env:
66+
INPUT_PATH: ${{ inputs.path }}
67+
SARIF_FILE: ${{ inputs.sarif-file }}
68+
run: |
69+
set +e
70+
rac validate "$INPUT_PATH" --sarif > "$SARIF_FILE"
71+
echo "exit_code=$?" >> "$GITHUB_OUTPUT"
72+
73+
- name: Upload SARIF to Code Scanning
74+
if: ${{ always() && inputs.upload-sarif == 'true' }}
75+
uses: github/codeql-action/upload-sarif@v3
76+
with:
77+
sarif_file: ${{ inputs.sarif-file }}
78+
79+
# Errors fail the check; warnings (including findings downgraded in
80+
# .rac/config.yaml, ADR-053) annotate without failing — warnings-first.
81+
- name: Report result
82+
shell: bash
83+
env:
84+
EXIT_CODE: ${{ steps.validate.outputs.exit_code }}
85+
run: |
86+
if [ "$EXIT_CODE" != "0" ]; then
87+
echo "::error::rac validate exited $EXIT_CODE — see the Code Scanning annotations."
88+
exit "$EXIT_CODE"
89+
fi
90+
echo "rac validate passed."

0 commit comments

Comments
 (0)