Enforce private-by-default enablement templates
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa
This commit is contained in:
parent
abe1865877
commit
27ac54b32d
9 changed files with 138 additions and 28 deletions
29
INTENT.md
29
INTENT.md
|
|
@ -108,26 +108,25 @@ This layer is:
|
|||
* a **promotion handoff** surface between source repos and S5 deployments
|
||||
* a future **developer portal** and self-service entry point
|
||||
* a place to codify delivery knowledge into **repeatable automation**
|
||||
* as of 2026-08-11, the owner of the **forge layer responsibility**: runner
|
||||
placement and labels, registry retention, artifact lifecycle, and package
|
||||
credentials
|
||||
* the owner of the **consumer-facing forge contract**: which runner labels,
|
||||
registry interfaces, credentials, and artifact evidence paved paths require;
|
||||
`railiance-forge` remains the operational provider of those capabilities
|
||||
|
||||
---
|
||||
|
||||
## Inherited: the forge layer responsibility
|
||||
## Forge accountability and operating boundary
|
||||
|
||||
Decision `d151d817` (2026-08-11) placed `railiance-forge`. The Forgejo
|
||||
**workload** becomes `rapp-forgejo`; the **layer responsibility** folds into S4
|
||||
— here — because this layer already declared a handoff contract with forge for
|
||||
runner labels, package credentials, registry endpoints, and artifact evidence.
|
||||
Owning the contract's other side is the natural resolution.
|
||||
Decision `d151d817` (2026-08-11) placed the consumer-facing forge
|
||||
responsibility in S4. That means this repo owns the reusable workflow contract
|
||||
for runner labels, package credentials, registry endpoints, and artifact
|
||||
evidence. It does not make this repo the operator of those systems:
|
||||
`railiance-forge` continues to own the live forge, registries, runners,
|
||||
credentials, retention, and operating evidence until a separately approved
|
||||
migration changes that boundary. The Forgejo workload itself belongs in
|
||||
`rapp-forgejo`.
|
||||
|
||||
This is S4's first concrete owned responsibility. That is worth stating plainly:
|
||||
until now this layer has been almost entirely aspiration — a rich Direction of
|
||||
Evolution with 25 commits and no workplans behind it. The forge responsibility
|
||||
gives it something real to be accountable for, and the gap between what this
|
||||
INTENT promises and what the repo contains should be read as an open debt, not
|
||||
as a description of the present.
|
||||
The split is deliberate: S4 makes the correct delivery path reusable;
|
||||
`railiance-forge` makes the underlying capability dependable.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
8
Makefile
8
Makefile
|
|
@ -3,3 +3,11 @@ SHELL := /usr/bin/env bash
|
|||
|
||||
help: ## Show this help
|
||||
@grep -E '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) | sort | sed 's/:.*##/: /'
|
||||
|
||||
check: ## Verify workflow templates remain private and deployment-free
|
||||
python3 tools/check_private_defaults.py
|
||||
|
||||
test: ## Run enablement regression tests
|
||||
python3 -m unittest discover -s tests -v
|
||||
|
||||
.PHONY: help check test
|
||||
|
|
|
|||
14
SCOPE.md
14
SCOPE.md
|
|
@ -74,13 +74,11 @@ services. S5 applications consume S4 templates and conventions, while
|
|||
|
||||
## Current State
|
||||
|
||||
- Status: emerging — **now owns the forge layer responsibility** (decision
|
||||
`d151d817`, 2026-08-11), which is S4's first concrete owned accountability;
|
||||
still no S4-owned workplans
|
||||
- Inherited from `railiance-forge`: runner placement and labels, registry
|
||||
retention, artifact lifecycle, package credentials. The Forgejo **workload**
|
||||
becomes `rapp-forgejo`; this layer takes the **layer** responsibility, because
|
||||
it already declared the handoff contract for exactly those concerns
|
||||
- Status: emerging, with reusable Forgejo workflow templates and an active
|
||||
private-by-default template workplan.
|
||||
- S4 owns the consumer-facing contract for runner labels, registries,
|
||||
credentials, and artifact evidence. `railiance-forge` continues to operate
|
||||
those capabilities; they are not duplicated here.
|
||||
- Implementation: ArgoCD is deployed in the `argocd` namespace on CoulombCore as
|
||||
a cluster addon managed from S2 — verified still true 2026-08-12
|
||||
- **But GitOps does not reach the live cluster.** ArgoCD is not deployed on
|
||||
|
|
@ -151,5 +149,5 @@ keywords: [template, sdk, helm, deployment, developer, buildpack]
|
|||
## Getting Oriented
|
||||
|
||||
- Start with: `CLAUDE.md` (session protocol, OAS boundary rules)
|
||||
- Key files / directories: `workplans/` (empty), `Makefile`
|
||||
- Key files / directories: `workflows/`, `docs/`, `workplans/`, `Makefile`
|
||||
- Pre-conditions: railiance-platform (S3) must be operational
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@
|
|||
|
||||
| Kind | ID | Status | Lane | Source |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| workplan | RAIL-EN-WP-0001 | ready | — | workplans/RAIL-EN-WP-0001-private-by-default-templates.md |
|
||||
| task | RAIL-EN-WP-0001-T01 | todo | — | workplans/RAIL-EN-WP-0001-private-by-default-templates.md |
|
||||
| workplan | RAIL-EN-WP-0001 | finished | — | workplans/RAIL-EN-WP-0001-private-by-default-templates.md |
|
||||
| task | RAIL-EN-WP-0001-T01 | done | — | workplans/RAIL-EN-WP-0001-private-by-default-templates.md |
|
||||
|
|
|
|||
16
docs/private-by-default-template-contract.md
Normal file
16
docs/private-by-default-template-contract.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Private-by-default template contract
|
||||
|
||||
Railiance enablement templates produce build and promotion evidence; they do
|
||||
not create a public listener. ADR-0008 is enforced by the execution rail:
|
||||
|
||||
- Kubernetes workload scaffolding is owned by `rail-kubernetes`.
|
||||
- Its default Service is `ClusterIP`, its generated ingress policy is
|
||||
default-deny, and its Stage 2 values do not enable Ingress.
|
||||
- A public Ingress requires matching rapp and reef declarations at the rail's
|
||||
deploy gate. An Ingress object or successful deployment is not a grant.
|
||||
- Operator access uses the named tunnel documented by the owning rail or rapp.
|
||||
|
||||
`make check` rejects enablement workflow templates that embed Ingress,
|
||||
LoadBalancer, NodePort, or direct Kubernetes/Helm deployment commands. This
|
||||
keeps reusable build workflows from becoming an accidental application or
|
||||
cluster deployment owner.
|
||||
BIN
tests/__pycache__/test_private_defaults.cpython-312.pyc
Normal file
BIN
tests/__pycache__/test_private_defaults.cpython-312.pyc
Normal file
Binary file not shown.
35
tests/test_private_defaults.py
Normal file
35
tests/test_private_defaults.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
|
||||
from check_private_defaults import violations # noqa: E402
|
||||
|
||||
|
||||
class PrivateDefaultsTests(unittest.TestCase):
|
||||
def check(self, text: str) -> list[str]:
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
path = Path(temp) / "template.yaml"
|
||||
path.write_text(text, encoding="utf-8")
|
||||
return violations([path])
|
||||
|
||||
def test_build_workflow_is_allowed(self) -> None:
|
||||
self.assertEqual([], self.check("jobs:\n build:\n runs-on: container-build\n"))
|
||||
|
||||
def test_ingress_is_rejected(self) -> None:
|
||||
self.assertTrue(self.check("apiVersion: networking.k8s.io/v1\nkind: Ingress\n"))
|
||||
|
||||
def test_public_service_is_rejected(self) -> None:
|
||||
self.assertTrue(self.check("kind: Service\nspec:\n type: LoadBalancer\n"))
|
||||
|
||||
def test_direct_apply_is_rejected(self) -> None:
|
||||
self.assertTrue(self.check("run: kubectl apply -f deployment.yaml\n"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
44
tools/check_private_defaults.py
Executable file
44
tools/check_private_defaults.py
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Fail when an enablement template creates a public Kubernetes path."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
FORBIDDEN = {
|
||||
"Ingress resource": re.compile(r"(?m)^\s*kind:\s*(Ingress|IngressRoute)\s*$"),
|
||||
"public Service type": re.compile(r"(?m)^\s*type:\s*(LoadBalancer|NodePort)\s*$"),
|
||||
"direct kubectl apply": re.compile(r"\bkubectl\s+(?:[^\n]*\s)?apply\b"),
|
||||
"direct helm deployment": re.compile(r"\bhelm\s+(upgrade|install)\b"),
|
||||
}
|
||||
|
||||
|
||||
def violations(paths: list[Path]) -> list[str]:
|
||||
found: list[str] = []
|
||||
for path in paths:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for label, pattern in FORBIDDEN.items():
|
||||
if pattern.search(text):
|
||||
found.append(f"{path}: {label}")
|
||||
return found
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("paths", nargs="*", type=Path)
|
||||
args = parser.parse_args()
|
||||
paths = args.paths or sorted(Path("workflows").glob("*.yaml"))
|
||||
problems = violations(paths)
|
||||
if problems:
|
||||
print("\n".join(problems), file=sys.stderr)
|
||||
return 1
|
||||
print(f"private-default template check passed: {len(paths)} file(s)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -4,11 +4,11 @@ type: workplan
|
|||
title: "Templates emit private Services, never a public Ingress by default"
|
||||
domain: financials
|
||||
repo: railiance-enablement
|
||||
status: ready
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-08-15"
|
||||
updated: "2026-08-15"
|
||||
updated: "2026-08-22"
|
||||
related:
|
||||
- RMASTER-WP-0023
|
||||
- ADR-0008
|
||||
|
|
@ -29,7 +29,7 @@ They never emit a public Ingress by default. If a template mentions
|
|||
|
||||
```task
|
||||
id: RAIL-EN-WP-0001-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ceea9e5a-385e-4f13-b8ea-0b1e72cddaee"
|
||||
```
|
||||
|
|
@ -40,3 +40,13 @@ that cites ADR-0008.
|
|||
|
||||
**Done when:** a new package from the paved path is private unless the
|
||||
author adds an explicit grant.
|
||||
|
||||
**Outcome (2026-08-22):** all reusable workflow templates are checked by
|
||||
`tools/check_private_defaults.py`. The check rejects Ingress/IngressRoute,
|
||||
LoadBalancer/NodePort Services, and direct deployment commands. The documented
|
||||
template contract points public exposure through the rail-owned ADR-0008 gate.
|
||||
|
||||
## Completion evidence
|
||||
|
||||
- `make check`: 4 workflow templates passed.
|
||||
- `make test`: 4 regression tests passed.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue