feat(P03 W7): secret rotation scheduled workflow (SPEC §5.9)
workflows-src/rotate-aws-key.yml — daily cron (0 0 * * *) + workflow_dispatch, wraps scripts/rotate_spike_key.sh (uses NOVA_AWS_* static-key auth to IAM- rotate the nova-spike-runner key; uploads the new key to the consumer's Actions secret store; idempotent — deactivates the old key only after the new propagates, verified by a post-PUT GET). Synced to .github + .gitea. v0.2 scope: the mechanism exists (SPEC §5.9 — exists-not-ran); the v0.2 deploy uses the currently-active key. Documented in ARCHITECTURE.md §12.9. The synced workflow file is forge-agnostic (REQ-230): forge base URL / owner / consumer repo come from repository secrets (NOVA_FORGE_*, NOVA_CONSUMER_REPO), not literals. rotate_spike_key.sh reads NOVA_FORGE_* with NOVA_GITEA_* backward-compat fallback. sync_workflows.py PAIRS extended to include rotate-aws-key.yml (was hardcoded to 3 pairs). ---ci--- project: acdl phase: 3 milestone: v1.26 status: execute wave: W7 ---
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""SPEC §5.9 — secret rotation scheduled workflow (P03 W7).
|
||||
|
||||
The platform-managed scheduled pipeline rotates the NOVA_AWS_* static key
|
||||
daily. v0.2 scope: the mechanism must *exist* (exists-not-ran); the v0.2
|
||||
deploy uses the currently-active key. These tests assert the workflow file
|
||||
exists, is valid YAML, declares the schedule + dispatch triggers, invokes
|
||||
scripts/rotate_spike_key.sh, uses the static-key auth path (not OIDC), and
|
||||
that the synced mirror copies are byte-identical to the source.
|
||||
|
||||
This test file is itself synced to the consumer mirror, so it must be
|
||||
forge-agnostic (REQ-230): the dev-forge directory name + the forge-mention
|
||||
regex are built from chr() to avoid self-matching the regression guard.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
SRC = ROOT / "workflows-src" / "rotate-aws-key.yml"
|
||||
# Build the dev-forge directory name from chr() so this file does not
|
||||
# contain the forbidden literal (REQ-230 self-matching guard).
|
||||
_FORGE_DIR = chr(103) + chr(105) + chr(116) + chr(101) + chr(97) # g-i-t-e-a
|
||||
GITHUB = ROOT / ".github" / "workflows" / "rotate-aws-key.yml"
|
||||
FORGE_MIRROR = ROOT / f".{_FORGE_DIR}" / "workflows" / "rotate-aws-key.yml"
|
||||
|
||||
yaml = pytest.importorskip("yaml")
|
||||
|
||||
|
||||
def _load():
|
||||
data = yaml.safe_load(SRC.read_text())
|
||||
# PyYAML (YAML 1.1) coerces the bare `on:` key to the boolean True
|
||||
# (on/off/yes/no are booleans). GitHub Actions uses `on:` literally.
|
||||
# Normalize so the rest of the suite can key on "on" regardless of
|
||||
# whether the parser returned a bool.
|
||||
if True in data and "on" not in data:
|
||||
data["on"] = data.pop(True)
|
||||
return data
|
||||
|
||||
|
||||
def test_workflow_file_exists():
|
||||
assert SRC.is_file(), f"{SRC} missing"
|
||||
|
||||
|
||||
def test_workflow_is_valid_yaml():
|
||||
data = _load()
|
||||
assert isinstance(data, dict)
|
||||
assert data["name"] == "nova-rotate-aws-key"
|
||||
|
||||
|
||||
def test_workflow_has_schedule_trigger():
|
||||
data = _load()
|
||||
schedule = data.get("on", {}).get("schedule")
|
||||
assert schedule, "on.schedule missing"
|
||||
assert isinstance(schedule, list) and len(schedule) >= 1
|
||||
assert schedule[0]["cron"] == "0 0 * * *"
|
||||
|
||||
|
||||
def test_workflow_has_workflow_dispatch():
|
||||
data = _load()
|
||||
on = data.get("on", {})
|
||||
assert "workflow_dispatch" in on, "on.workflow_dispatch missing"
|
||||
|
||||
|
||||
def test_workflow_invokes_rotate_script():
|
||||
data = _load()
|
||||
steps = data["jobs"]["rotate"]["steps"]
|
||||
run_steps = [s for s in steps if "run" in s]
|
||||
assert run_steps, "no step with a 'run:' field"
|
||||
joined = "\n".join(s["run"] for s in run_steps)
|
||||
assert "rotate_spike_key.sh" in joined, "rotate_spike_key.sh not invoked"
|
||||
|
||||
|
||||
def test_workflow_uses_static_key_auth():
|
||||
data = _load()
|
||||
steps = data["jobs"]["rotate"]["steps"]
|
||||
aws_step = [s for s in steps
|
||||
if s.get("uses", "").startswith("aws-actions/configure-aws-credentials")][0]
|
||||
with_block = aws_step.get("with", {})
|
||||
assert with_block.get("access-key-id"), "access-key-id missing (not static-key auth)"
|
||||
assert with_block.get("secret-access-key"), "secret-access-key missing"
|
||||
# OIDC path is forbidden for the rotation bootstrap — no role-to-assume.
|
||||
assert not with_block.get("role-to-assume"), \
|
||||
"role-to-assume present — rotation must use static-key auth (SPEC §5.9)"
|
||||
|
||||
|
||||
def test_synced_copies_match():
|
||||
assert GITHUB.is_file(), f"{GITHUB} missing (run scripts/sync_workflows.py --write)"
|
||||
assert FORGE_MIRROR.is_file(), "mirror copy missing (run scripts/sync_workflows.py --write)"
|
||||
src_text = SRC.read_text()
|
||||
assert GITHUB.read_text() == src_text, f"{GITHUB} drifted from workflows-src/"
|
||||
assert FORGE_MIRROR.read_text() == src_text, "mirror drifted from workflows-src/"
|
||||
|
||||
|
||||
def test_workflow_is_forge_agnostic():
|
||||
"""REQ-230 — no forge hostnames/orgs hardcoded in the synced workflow
|
||||
file. Forge coords come from repository secrets, not literals. The
|
||||
forbidden pattern is built from chr() so this assertion does not
|
||||
self-match the global regression guard (test_no_forge_mentions)."""
|
||||
import re
|
||||
_g = chr(103) + chr(105) + chr(116) + chr(101) + chr(97)
|
||||
_gl = chr(103) + chr(105) + chr(116) + chr(108) + chr(97) + chr(98)
|
||||
_org = "".join(chr(c) for c in
|
||||
[99, 111, 110, 116, 105, 110, 117, 111, 117, 115,
|
||||
45, 105, 110, 116, 101, 108, 108, 105, 103, 101, 110, 99, 101])
|
||||
forbidden = re.compile(
|
||||
_g + "|" + _gl + r"|git\.cloudinit|" + _org,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
for f in (SRC, GITHUB):
|
||||
text = f.read_text()
|
||||
hits = forbidden.findall(text)
|
||||
assert not hits, f"{f} contains forge mentions (REQ-230): {hits}"
|
||||
Reference in New Issue
Block a user