RPF-WP-0061-T03: ccr/* branch gate and CI workflow; first automated platform review on CCR-2026-0051
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 306568@bnt-lap001 Assistant-Session: 8bdf8150-ea56-41e7-9132-13f04f6b0b91
This commit is contained in:
parent
b6148da443
commit
10fddbc98f
5 changed files with 191 additions and 7 deletions
43
.forgejo/workflows/ccr-intake.yaml
Normal file
43
.forgejo/workflows/ccr-intake.yaml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# RPF-WP-0061-T03: self-service CCR intake gate.
|
||||
# ccr/* branches: exactly one new in-profile CCR (+ its new policy file) from a
|
||||
# listed submitter, nothing else changed. main: post-merge id uniqueness.
|
||||
# Source comes from the exact-revision archive, as in keycape-factor-acceptance.
|
||||
name: CCR intake gate
|
||||
on:
|
||||
push:
|
||||
branches: ["ccr/**", main]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: python:3.12-slim@sha256:d764629ce0ddd8c71fd371e9901efb324a95789d2315a47db7e4d27e78f1b0e9
|
||||
steps:
|
||||
- name: Fetch exact revision and main
|
||||
run: |
|
||||
set -eu
|
||||
python3 - <<'PYCODE'
|
||||
import os,tarfile,tempfile,urllib.request
|
||||
from pathlib import Path
|
||||
base='https://forgejo.coulomb.social/'+os.environ['GITHUB_REPOSITORY']+'/archive/'
|
||||
for name,ref in (('branch',os.environ['GITHUB_SHA']),('main','main')):
|
||||
root=Path('/tmp/ccr-intake')/name;root.mkdir(parents=True)
|
||||
with urllib.request.urlopen(base+ref+'.tar.gz',timeout=30) as response,tempfile.TemporaryFile() as archive:
|
||||
archive.write(response.read());archive.seek(0)
|
||||
with tarfile.open(fileobj=archive,mode='r:gz') as tar:tar.extractall(root,filter='data')
|
||||
PYCODE
|
||||
pip install --quiet --disable-pip-version-check pyyaml==6.0.2
|
||||
- name: Gate ccr branch
|
||||
if: startsWith(github.ref, 'refs/heads/ccr/')
|
||||
run: |
|
||||
set -eu
|
||||
cd /tmp/ccr-intake/branch/*
|
||||
main_root=$(echo /tmp/ccr-intake/main/*)
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p 'test_ccr_intake.py'
|
||||
python3 scripts/credential-change.py check-branch --branch "$GITHUB_REF" --main-root "$main_root"
|
||||
- name: Check CCR ids on main
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
set -eu
|
||||
cd /tmp/ccr-intake/branch/*
|
||||
python3 scripts/credential-change.py check-ids
|
||||
|
|
@ -17,7 +17,13 @@ review:
|
|||
- founder
|
||||
- railiance-platform-owner
|
||||
- mail-address-semantics-owner
|
||||
comments: []
|
||||
comments:
|
||||
- at: '2026-10-18T10:36:53+00:00'
|
||||
reviewer: railiance-platform-owner (policy-as-code ccr-standard-lane v1)
|
||||
decision: approved
|
||||
comment: 'Standard lane: passes ccr-standard-lane v1 sha256:5481be201456739b9e01b14b0b11211923d7d838a2e71e2e417af038a05aec95.
|
||||
Platform-owner source review only; founder approval and confirm-binding still
|
||||
required.'
|
||||
target:
|
||||
domain: infotech
|
||||
tenant: platform
|
||||
|
|
@ -71,13 +77,13 @@ risk:
|
|||
limited to namespace mas.
|
||||
- New read scope on a supplier path; founder approval required. No other lane reads
|
||||
this path (ops-mason live survey 2026-10-18T10:13:55Z).
|
||||
- Apply may be run by ops-mason under ops-mason-construct (MASON-IN-0005) once
|
||||
installed (MASON-WP-0015-T02) and this CCR is approved; that grant covers
|
||||
workload-kv-read-* policies and external-secrets-* roles only.
|
||||
- Apply may be run by ops-mason under ops-mason-construct (MASON-IN-0005) once installed
|
||||
(MASON-WP-0015-T02) and this CCR is approved; that grant covers workload-kv-read-*
|
||||
policies and external-secrets-* roles only.
|
||||
verification:
|
||||
positive:
|
||||
- Exact SA/namespace/audience login succeeds; ESO Ready/SecretSynced; metadata
|
||||
version matches admitted KV version 2 without printing values.
|
||||
- Exact SA/namespace/audience login succeeds; ESO Ready/SecretSynced; metadata version
|
||||
matches admitted KV version 2 without printing values.
|
||||
negative:
|
||||
- Wrong SA, namespace or audience login refused; sibling paths under platform/suppliers/scaleway,
|
||||
list, metadata, write and delete denied.
|
||||
|
|
|
|||
|
|
@ -220,6 +220,64 @@ def command_check_ids(args: argparse.Namespace, cc: Any) -> int:
|
|||
return 1 if duplicates else 0
|
||||
|
||||
|
||||
def tree_digests(root: Path) -> dict[str, str]:
|
||||
return {
|
||||
str(path.relative_to(root)): hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
for path in root.rglob("*")
|
||||
if path.is_file() and ".git" not in path.relative_to(root).parts
|
||||
and "__pycache__" not in path.parts
|
||||
}
|
||||
|
||||
|
||||
def branch_gate(main_root: Path, stem: str, cc: Any, profile_path: Path = DEFAULT_PROFILE) -> list[str]:
|
||||
"""CI gate for a ccr/<stem> branch checked out at REPO_DIR (RPF-WP-0061-T03).
|
||||
|
||||
The branch may add exactly one new CCR and its new policy file, nothing
|
||||
else; the CCR must be an in-profile submission from a listed submitter with
|
||||
a policy-as-code review against the current profile digest.
|
||||
"""
|
||||
errors: list[str] = []
|
||||
ccr_rel = f"credential-change-requests/{stem}.yaml"
|
||||
path = REPO_DIR / ccr_rel
|
||||
if not CCR_FILE_RE.match(path.name) or not path.exists():
|
||||
return [f"branch ccr/{stem} must add {ccr_rel}"]
|
||||
if (main_root / ccr_rel).exists():
|
||||
errors.append(f"{ccr_rel} already exists on main")
|
||||
ccr, validation_errors, _warnings = cc.validate_ccr(path)
|
||||
errors.extend(f"validate: {item}" for item in validation_errors)
|
||||
policy_rel = (ccr.get("openbao") or {}).get("policy_file")
|
||||
allowed = {ccr_rel} | ({policy_rel} if policy_rel else set())
|
||||
branch, main = tree_digests(REPO_DIR), tree_digests(main_root)
|
||||
changed = {k for k in branch.keys() | main.keys() if branch.get(k) != main.get(k)}
|
||||
for extra in sorted(changed - allowed):
|
||||
errors.append(f"ccr branch may not change {extra}")
|
||||
if policy_rel and policy_rel in main and main[policy_rel] != branch.get(policy_rel):
|
||||
errors.append(f"{policy_rel} exists on main; a submission may not modify it")
|
||||
if ccr.get("status") != "proposed":
|
||||
errors.append(f"status must be proposed, got {ccr.get('status')}")
|
||||
profile, digest = load_profile(profile_path)
|
||||
if (ccr.get("requester") or {}).get("agent") not in profile["submitters"]:
|
||||
errors.append(f"requester is not a listed submitter {profile['submitters']}")
|
||||
errors.extend(standard_violations(ccr, profile, path.parent, cc))
|
||||
comments = (ccr.get("review") or {}).get("comments") or []
|
||||
if len(comments) != 1 or comments[0].get("decision") != "approved" or digest not in str(
|
||||
comments[0].get("comment")
|
||||
):
|
||||
errors.append("review must hold exactly the policy-as-code approval for the current profile")
|
||||
errors.extend(f"duplicate id: {k} {v}" for k, v in duplicate_ids(path.parent, cc).items())
|
||||
return errors
|
||||
|
||||
|
||||
def command_check_branch(args: argparse.Namespace, cc: Any) -> int:
|
||||
stem = args.branch.removeprefix("refs/heads/").removeprefix("ccr/")
|
||||
errors = branch_gate(Path(args.main_root).resolve(), stem, cc)
|
||||
for item in errors:
|
||||
print(f"[FAIL] ccr/{stem}: {item}")
|
||||
if not errors:
|
||||
print(f"[OK] ccr/{stem}: one in-profile CCR, nothing else changed")
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
def git(*argv: str) -> None:
|
||||
subprocess.run(["git", "-C", str(REPO_DIR), *argv], check=True)
|
||||
|
||||
|
|
@ -294,6 +352,13 @@ def add_parsers(sub: Any, cc: Any, state_hub_url: str) -> None:
|
|||
check_ids = sub.add_parser("check-ids", help="Fail on duplicate CCR ids or id/file name mismatch")
|
||||
check_ids.set_defaults(func=lambda args: command_check_ids(args, cc))
|
||||
|
||||
check_branch = sub.add_parser(
|
||||
"check-branch", help="CI gate: a ccr/* branch adds exactly one in-profile CCR and its policy"
|
||||
)
|
||||
check_branch.add_argument("--branch", required=True, help="ccr/<id>-<slug> or refs/heads/ccr/...")
|
||||
check_branch.add_argument("--main-root", required=True, help="tree of main to compare against")
|
||||
check_branch.set_defaults(func=lambda args: command_check_branch(args, cc))
|
||||
|
||||
submit_p = sub.add_parser(
|
||||
"submit",
|
||||
help="File a client CCR draft under the next CCR id and run the standard review",
|
||||
|
|
|
|||
|
|
@ -194,6 +194,59 @@ class CcrIntakeTests(unittest.TestCase):
|
|||
with self.assertRaises(SystemExit):
|
||||
ccr_intake.submit(args, cc)
|
||||
|
||||
def submitted_branch(self) -> tuple[Path, str]:
|
||||
main_root = Path(tempfile.mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, main_root, True)
|
||||
shutil.copytree(self.root, main_root, dirs_exist_ok=True)
|
||||
destination, _violations = ccr_intake.submit(self.submit_args(), cc)
|
||||
return main_root, destination.stem
|
||||
|
||||
def test_branch_gate_accepts_clean_submission(self) -> None:
|
||||
main_root, stem = self.submitted_branch()
|
||||
self.assertEqual(ccr_intake.branch_gate(main_root, stem, cc), [])
|
||||
|
||||
def test_branch_gate_refuses_extra_changes(self) -> None:
|
||||
main_root, stem = self.submitted_branch()
|
||||
(self.root / "registry").mkdir()
|
||||
(self.root / "registry/ccr-standard-profile.yaml").write_text("submitters: [anyone]\n")
|
||||
errors = ccr_intake.branch_gate(main_root, stem, cc)
|
||||
self.assertIn("ccr branch may not change registry/ccr-standard-profile.yaml", errors)
|
||||
|
||||
def test_branch_gate_refuses_modified_existing_policy(self) -> None:
|
||||
main_root, stem = self.submitted_branch()
|
||||
path = self.ccrs / f"{stem}.yaml"
|
||||
data = cc.load_yaml(path)
|
||||
data["openbao"]["policy_file"] = POLICY
|
||||
cc.dump_yaml(path, data)
|
||||
(self.root / POLICY).write_text((self.root / POLICY).read_text() + "# widened\n")
|
||||
errors = ccr_intake.branch_gate(main_root, stem, cc)
|
||||
self.assertTrue(any("may not modify" in e for e in errors), errors)
|
||||
|
||||
def test_branch_gate_refuses_hand_edited_review(self) -> None:
|
||||
main_root, stem = self.submitted_branch()
|
||||
path = self.ccrs / f"{stem}.yaml"
|
||||
data = cc.load_yaml(path)
|
||||
data["review"]["comments"].append({"reviewer": "founder", "decision": "approved", "comment": "ok"})
|
||||
cc.dump_yaml(path, data)
|
||||
errors = ccr_intake.branch_gate(main_root, stem, cc)
|
||||
self.assertTrue(any("policy-as-code approval" in e for e in errors), errors)
|
||||
|
||||
def test_branch_gate_refuses_unlisted_requester(self) -> None:
|
||||
main_root, stem = self.submitted_branch()
|
||||
path = self.ccrs / f"{stem}.yaml"
|
||||
data = cc.load_yaml(path)
|
||||
data["requester"]["agent"] = "someone-else"
|
||||
cc.dump_yaml(path, data)
|
||||
errors = ccr_intake.branch_gate(main_root, stem, cc)
|
||||
self.assertTrue(any("listed submitter" in e for e in errors), errors)
|
||||
|
||||
def test_branch_gate_refuses_existing_ccr(self) -> None:
|
||||
main_root = Path(tempfile.mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, main_root, True)
|
||||
shutil.copytree(self.root, main_root, dirs_exist_ok=True)
|
||||
errors = ccr_intake.branch_gate(main_root, Path(STANDARD).stem, cc)
|
||||
self.assertTrue(any("already exists on main" in e for e in errors), errors)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ gaps, core-hub database policy assertion) predate this work.
|
|||
|
||||
```task
|
||||
id: RPF-WP-0061-T03
|
||||
status: todo
|
||||
status: progress
|
||||
priority: medium
|
||||
state_hub_task_id: "0fd97924-1bc7-5aa1-a520-c6c5737bf09b"
|
||||
```
|
||||
|
|
@ -122,6 +122,23 @@ state_hub_task_id: "0fd97924-1bc7-5aa1-a520-c6c5737bf09b"
|
|||
Forgejo Action running validate, review-standard and the secret scan; confirm
|
||||
which identities may push `ccr/*` (ops-mason first).
|
||||
|
||||
2026-10-18: `.forgejo/workflows/ccr-intake.yaml` runs on `ccr/**` and `main`
|
||||
from exact-revision archives. On `ccr/*` it runs the intake tests and
|
||||
`credential-change.py check-branch`. That command refuses the branch unless it
|
||||
adds exactly one new CCR plus its new policy file and changes nothing else
|
||||
(not the profile, not an existing policy). The CCR must validate, be
|
||||
`proposed`, come from a listed submitter, re-pass the profile in CI and carry
|
||||
exactly the policy-as-code approval for the current profile digest. On `main`
|
||||
it runs `check-ids`. 26 tests. CCR-2026-0051 received the first automated
|
||||
platform-owner review (still `proposed`).
|
||||
|
||||
Remaining, founder/Forgejo admin: a protected-branch rule on `ccr/*` that
|
||||
allows pushes only from ops-mason's Forgejo identity. Forgejo cannot require
|
||||
a check on main without blocking every direct push, so merging is a
|
||||
convention backed by detection: ops-mason fast-forwards only a green branch,
|
||||
and the main job plus `check-branch` evidence detect violations after the
|
||||
fact.
|
||||
|
||||
## T04 — Hub decision on submit and client-run `sync-decision`
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue