Enforce private-by-default enablement templates
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa
This commit is contained in:
tegwick 2026-08-22 12:34:25 +02:00
parent abe1865877
commit 27ac54b32d
9 changed files with 138 additions and 28 deletions

View file

@ -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.
---

View file

@ -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

View file

@ -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

View file

@ -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 |

View 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.

View 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
View 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())

View file

@ -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.