NK-WP-0048: {param:env} format spec; check follow-through/rollback templates
Answers ops-warden's observation on the literal KEYCAPE_<CLIENT_ID>_CLIENT_SECRET. The type adopts the spec once ops-warden's planner renders it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 469893@bnt-lap001 Assistant-Session: 49296f15-2171-4137-b972-2443bab95edb
This commit is contained in:
parent
502907a2d3
commit
850907a987
3 changed files with 61 additions and 1 deletions
|
|
@ -171,3 +171,28 @@ def test_forgejo_reference_case_is_accepted_and_matches_the_example():
|
|||
assert {b["role"] for b in case["spec"]["acceptance"]["by"]} == {"owner", "founder"}
|
||||
assert msgs(v.validate_case(case, TYPE, require_implemented=False)) == []
|
||||
assert msgs(v.validate_case(case, as_implemented())) == []
|
||||
|
||||
|
||||
def test_env_format_spec_renders_the_keycape_env_name():
|
||||
# {client_id:env} gives the name keycape-client.py derives (env_name): upper
|
||||
# case, '-' as '_'. The type adopts it once ops-warden's planner renders it
|
||||
# too (ops-warden message 06303d1d); until then it keeps the literal form.
|
||||
t = as_implemented()
|
||||
for item in t["spec"]["follow_through"] + t["spec"]["rollback"]:
|
||||
for key in ("entry_point", "action"):
|
||||
if key in item:
|
||||
item[key] = item[key].replace("KEYCAPE_<CLIENT_ID>_CLIENT_SECRET", "KEYCAPE_{client_id:env}_CLIENT_SECRET")
|
||||
assert msgs(v.validate_type(t)) == []
|
||||
issues, r = v.render_case(with_params(FORGEJO, client_id="my-forge"), t, DECLS)
|
||||
assert msgs(issues) == []
|
||||
steps = {s["id"]: s for s in r["follow_through"]}
|
||||
assert "env KEYCAPE_MY_FORGE_CLIENT_SECRET from sso/keycape-my-forge-client" in steps["bind-env"]["entry_point"]
|
||||
assert "<" not in steps["bind-env"]["entry_point"]
|
||||
undo = [s for s in r["rollback"] if s["reverses"] == "bind-env"]
|
||||
assert "KEYCAPE_MY_FORGE_CLIENT_SECRET" in undo[0]["action"]
|
||||
|
||||
|
||||
def test_unknown_format_spec_is_rejected():
|
||||
t = copy.deepcopy(TYPE)
|
||||
t["spec"]["rollback"][0]["action"] += " {client_id:shout}"
|
||||
assert any("unknown format spec" in m for m in msgs(v.validate_type(t)))
|
||||
|
|
|
|||
|
|
@ -79,6 +79,15 @@ def placeholders(text: str) -> set[str]:
|
|||
return {f[1] for f in string.Formatter().parse(text) if f[1]}
|
||||
|
||||
|
||||
#: Format specs a template may apply to a parameter. ``env`` renders an
|
||||
#: environment-variable fragment: upper case, ``-`` as ``_`` ({client_id:env}).
|
||||
FORMAT_SPECS = {"", "env"}
|
||||
|
||||
|
||||
def format_specs(text: str) -> set[str]:
|
||||
return {f[2] or "" for f in string.Formatter().parse(text) if f[1]}
|
||||
|
||||
|
||||
def _walk_strings(value: Any, path: str):
|
||||
if isinstance(value, str):
|
||||
yield path, value
|
||||
|
|
@ -146,6 +155,9 @@ def validate_type(data: dict[str, Any], type_dir: Path | None = None) -> list[Is
|
|||
_validate_materialization(spec["materialization"], params, issues)
|
||||
_validate_verification(spec["verification"], params, issues)
|
||||
_validate_rollback(spec["rollback"], step_ids, spec["materialization"], issues)
|
||||
for section in ("follow_through", "rollback"):
|
||||
if isinstance(spec[section], list):
|
||||
_validate_templates(spec[section], f"type.spec.{section}", params, issues)
|
||||
_validate_acceptance(spec["acceptance"], issues)
|
||||
for section in ("materialization", "follow_through", "verification", "rollback"):
|
||||
for i, item in enumerate(spec[section] if isinstance(spec[section], list) else []):
|
||||
|
|
@ -241,6 +253,8 @@ def _validate_templates(items: Any, path: str, params: dict[str, dict], issues:
|
|||
for name in placeholders(text):
|
||||
if name not in params:
|
||||
issues.append(err(spath, f"template uses unknown parameter {{{name}}}"))
|
||||
for spec in sorted(format_specs(text) - FORMAT_SPECS):
|
||||
issues.append(err(spath, f"template uses unknown format spec {spec!r}"))
|
||||
|
||||
|
||||
def _validate_materialization(items: Any, params: dict[str, dict], issues: list[Issue]) -> None:
|
||||
|
|
@ -446,6 +460,15 @@ def _applicable(items: list[dict], values: dict[str, Any]) -> list[dict]:
|
|||
return [{k: v for k, v in i.items() if k != "when"} for i in items if holds(i.get("when"), values)]
|
||||
|
||||
|
||||
class _Param(str):
|
||||
"""A string parameter value that also renders the ``env`` format spec."""
|
||||
|
||||
def __format__(self, spec: str) -> str:
|
||||
if spec == "env":
|
||||
return self.upper().replace("-", "_")
|
||||
return str.__format__(self, spec)
|
||||
|
||||
|
||||
class _Blank(dict):
|
||||
"""Template values where an optional parameter a case leaves out renders empty."""
|
||||
|
||||
|
|
@ -455,7 +478,8 @@ class _Blank(dict):
|
|||
|
||||
def _fill(value: Any, values: dict[str, Any]) -> Any:
|
||||
if isinstance(value, str):
|
||||
return value.format_map(_Blank({k: (",".join(map(str, v)) if isinstance(v, list) else v)
|
||||
return value.format_map(_Blank({k: _Param(",".join(map(str, v)) if isinstance(v, list) else v)
|
||||
if isinstance(v, (str, list)) else v
|
||||
for k, v in values.items()}))
|
||||
if isinstance(value, dict):
|
||||
return {k: _fill(v, values) for k, v in value.items()}
|
||||
|
|
|
|||
|
|
@ -362,6 +362,17 @@ the pod: `ensure_service_groups` and `revoke_service_groups` present, `/readyz`
|
|||
deploy/identity-provisioner provisioner=forgejo.coulomb.social/coulomb/identity-provisioner@sha256:ffacd5d790a5b7800e0535f3aa95b7de52c7a68e4f575b9192c3e1622fa672c2`.
|
||||
The pin in `sso-mfa/k8s/identity-provisioner/deployment.yaml` follows.
|
||||
|
||||
2026-10-09: ops-warden closed WARDEN-WP-0044-T01 and shipped `warden case plan`
|
||||
(T02, message `06303d1d`); the accepted Forgejo case is refused until ops-mason
|
||||
materializes it, and a materialized copy plans cleanly once
|
||||
`keycape-client-secret-generate` exists. Its observation that `bind-env` carries
|
||||
a literal `KEYCAPE_<CLIENT_ID>_CLIENT_SECRET`: the validator now renders a
|
||||
`{param:env}` format spec (upper case, `-` as `_`, as `keycape-client.py`
|
||||
derives the name) and rejects unknown specs, and follow-through and rollback
|
||||
text is now checked for unknown placeholders too (patch-level, canon section 6).
|
||||
The type keeps the literal until ops-warden's planner (`text.format`) renders the
|
||||
spec as well; asked of ops-warden. 53 tests pass.
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T07
|
||||
status: wait
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue