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
7 changes: 7 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"attribution": {
"commit": "",
"pr": "",
"sessionUrl": false
}
}
29 changes: 29 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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
# CLI exit code. Structural only (parses YAML) — no engine install needed.
on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
action-contracts:
name: action contracts (pytest)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Install test deps
run: python -m pip install --quiet --upgrade pip pytest pyyaml

- name: Run action-contract tests
run: python -m pytest tests/ -q
69 changes: 69 additions & 0 deletions .github/workflows/watchkeeper.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 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-ci/.github/workflows/watchkeeper.yml@<tag>
# 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/<PR base branch>)."
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 rac-ci release it tracks main.
- uses: itsthelore/rac-ci/watchkeeper/github@main
with:
path: ${{ inputs.path }}
base: ${{ inputs.base }}
fail-on: ${{ inputs.fail-on }}
annotate: ${{ inputs.annotate }}
rac-version: ${{ inputs.rac-version }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
*.pyc
.pytest_cache/
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# rac-ci

The CI delivery surface for [RAC](https://github.com/itsthelore/rac-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
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).

## 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>` |
| 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>`).

`bitbucket/` and `jenkins/` platform wrappers join under each capability when
demanded; the engine is already platform-neutral (SARIF/JSON), so that work is in
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.
107 changes: 107 additions & 0 deletions gatekeeper/github/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# 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 `watchkeeper/github/` and the validate
# (Registrar) action at `registrar/github/`; this gate is referenced as
# `uses: itsthelore/rac-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
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.
required: false
default: ""

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

- name: Install RAC
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

# 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."
7 changes: 7 additions & 0 deletions recordkeeper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# recordkeeper (placeholder)

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/`
(and other platforms) when the recorder lands, without re-litigating repository
structure.
90 changes: 90 additions & 0 deletions registrar/github/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 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 `watchkeeper/github/`;
# this Registrar (validate) action is referenced as
# `uses: itsthelore/rac-ci/registrar/github@<ref>`.
name: "RAC Registrar"
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.
required: false
default: ""

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

- name: Install RAC
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

# 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."
Loading