Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions docs/node-maintenance-operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,5 +354,21 @@ leaves an explicit partial result. The report is edited only after the expected
published head and unchanged remaining PR state are observed.
An uncertain push reports the attempted commit
and requires branch inspection before another attempt. Nothing merges
automatically. Normal-audit preservation and full live repair acceptance remain
required before general rollout.
automatically.

## Retain an unmerged repair during normal audits

Before checkout, staging or PR edits, the normal security publisher reads native
commit trailers in the maintenance head's history that is not reachable from the
audited default branch. An exact `Node-Repair: validated-selected-pins-v1`
trailer retains the existing PR head, report, open state and draft status. It also
protects a repair below a later maintainer commit and a published repair whose
report update failed. A marker in the editable body supplies no authority.

Missing branch history or a failed Git inspection stops publication. A failed
new audit still fails its Actions job while leaving the repaired PR intact.
The repair must be reviewed or handled by a maintainer; there is no automatic
reset or replacement. Repairs already reachable from the audited default branch
do not block later publication. Upgrade and legacy proposals keep their existing
publication behavior. Complete live repair acceptance remains required before
general rollout.
28 changes: 20 additions & 8 deletions docs/node-maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,25 @@ link. It does not show live progress. Checkbox edits alone
start no work. The Checks tab is optional for this flow.

This requires an installed repair App, a current report that supports comment
requests, and the fixed repair workflow. The current pilot only validates inputs;
it changes no dependencies. Its existing reply reports success, failure or
cancellation and links to the run. Once completion is safely recorded, a new
command can request another attempt. A failed status update requires operator
recovery before the next attempt. Production repair and report integration
remain later delivery slices. Nothing merges automatically.
requests, and the fixed repair workflow. The implemented workflow prepares exact
selected pins, validates them on a separate runner, and publishes the validated
dependency files and cumulative report to the same PR. A failed validation
publishes no repair. A push can succeed before the report update fails; inspect
the recorded commit before requesting another attempt. Once completion is safely
recorded, a new command can request another attempt. A failed status update
requires operator recovery before the next attempt. The default pilot caller
still validates inputs only; actual publication requires the reviewed workflow
reference used by the isolated acceptance test. General installation remains
unreleased. Nothing merges automatically.
See the [service reference](node-maintenance-operator.md) for current limits.

Normal security audits retain an open PR containing an unmerged native
`Node-Repair: validated-selected-pins-v1` commit. They leave its head, report,
open state and draft status unchanged, including after a later maintainer commit
or an incomplete repair report update. Editable PR text cannot establish this
protection. A failed new audit still reports failure in Actions. Once the repair
is merged into the audited default branch, normal publication can continue.

### Reading repair choices

🟡 **Needs review** means a decision or explicit pin needs attention.
Expand All @@ -317,8 +328,9 @@ not mean repair validation passed or that the dependency was upgraded.

The report component can produce the exact selection block bound to a known PR
head and base. Unbound audit reports do not offer an active repair command. The
published caller integration and actual dependency repair remain unreleased;
the harmless pilot's success does not change the package table to upgraded.
general caller integration remains unreleased. A harmless pilot's success does
not change the package table to upgraded; the actual publisher does so only
after successful separate validation and publication.

## Toolchain selection

Expand Down
11 changes: 11 additions & 0 deletions node/publish/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ if [ "$REFRESH_ONLY" = true ] && { [ -z "${EXPECTED_PR:-}" ] || [ "$pr_number" !
echo 'The original proposal is no longer open; refresh will not create or replace a PR.'
exit 0
fi
if [ "$PROPOSAL_KIND" = security ] && [ -n "$pr_number" ]; then
if [ -z "$expected_head" ] || ! repair_trailers=$(git log --format='%(trailers:key=Node-Repair,valueonly)' "$AUDIT_SHA..$expected_head"); then
echo '::error::Unable to inspect unmerged repair history; refusing publication.' >&2
exit 1
fi
if grep -Fx 'validated-selected-pins-v1' <<< "$repair_trailers" >/dev/null; then
echo 'The open security PR contains an unmerged validated repair; its branch, report and review state are retained. See the Actions summary for this audit.'
validation_status
exit 0
fi
fi
git checkout -B "$PR_BRANCH"
git add -- "$NPM_PROJECT/package.json"
for file in package-lock.json pnpm-lock.yaml pnpm-workspace.yaml; do
Expand Down
181 changes: 181 additions & 0 deletions node/tests/test_selected_repairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,187 @@
REVISION = {"head": "a" * 40, "base": "b" * 40}


@pytest.fixture
def normal_publication(tmp_path):
"""Run the actual normal publisher against a native remote and PR state."""
import shutil

remote, work, binary = (
tmp_path / "remote.git",
tmp_path / "work",
tmp_path / "bin",
)

def git(*args):
return subprocess.check_output(
["git", *args], cwd=work if work.exists() else tmp_path, text=True
).strip()

git("init", "--bare", "-q", str(remote))
git("clone", "-q", str(remote), str(work))
git("config", "user.name", "Fixture")
git("config", "user.email", "fixture@example.com")
git("checkout", "-qb", "main")
project = work / "app"
project.mkdir()
(project / "package.json").write_text('{"name":"fixture"}\n')
(project / "pnpm-lock.yaml").write_text("baseline\n")
git("add", ".")
git("commit", "-qm", "baseline")
git("push", "-q", "origin", "main")
base = git("rev-parse", "HEAD")
binary.mkdir()
commands = tmp_path / "commands.jsonl"
state = tmp_path / "pr.json"
state.write_text(json.dumps({"body": "Original report", "draft": False}))
(binary / "gh").write_text(
f"#!{sys.executable}\n"
"import json, os, sys\nfrom pathlib import Path\n"
"args=sys.argv[1:]\n"
"with open(os.environ['COMMAND_LOG'],'a') as f: f.write(json.dumps(args)+'\\n')\n"
"path=Path(os.environ['PR_STATE']); state=json.loads(path.read_text())\n"
"if args[:2]==['pr','list']: print('1')\n"
"elif args[:2]==['pr','view']:\n"
" field=args[args.index('--json')+1]\n"
" print({'state':'OPEN','isDraft':str(state['draft']).lower(),'body':state['body'],'baseRefName':'main','title':'Original title'}[field])\n"
"else:\n"
" state['mutated']=True\n"
" if '--body-file' in args: state['body']=Path(args[args.index('--body-file')+1]).read_text()\n"
" path.write_text(json.dumps(state))\n"
)
(binary / "gh").chmod(0o755)
real_git = shutil.which("git")
(binary / "git").write_text(
f"#!{sys.executable}\nimport os,sys\n"
"if sys.argv[1]=='log' and os.environ.get('FAIL_HISTORY')=='true': raise SystemExit(73)\n"
f"os.execv({real_git!r},[{real_git!r},*sys.argv[1:]])\n"
)
(binary / "git").chmod(0o755)

def publish(history, candidate="changed"):
nonlocal base
git("checkout", "-qb", "maintenance/node-security")
(project / "pnpm-lock.yaml").write_text("selected repair\n")
git("add", ".")
trailer = (
"Node-Repair: validated-selected-pins-v1"
if history not in {"ordinary", "forged-body", "lookalike"}
else "Node-Repair: validated-selected-pins-v10"
if history == "lookalike"
else ""
)
git("commit", "-qm", "Selected repair\n\n" + trailer)
if history == "maintainer":
(work / "maintainer.md").write_text("Keep this maintainer change\n")
git("add", ".")
git("commit", "-qm", "Later maintainer change")
head = git("rev-parse", "HEAD")
git("push", "-q", "origin", "maintenance/node-security")
if history == "merged":
git("checkout", "main")
git("merge", "--ff-only", head)
git("push", "-q", "origin", "main")
base = head
git("checkout", "--detach", base)
if history == "missing-head":
git("update-ref", "-d", "refs/remotes/origin/maintenance/node-security")
if candidate != "unchanged":
(project / "pnpm-lock.yaml").write_text("new normal candidate\n")
if history == "stale-body":
state.write_text(
json.dumps({"body": "Stale pre-repair report", "draft": True})
)
elif history == "forged-body":
state.write_text(
json.dumps(
{"body": "Node-Repair: validated-selected-pins-v1", "draft": False}
)
)
before = state.read_bytes()
(tmp_path / "dependency-audit-pr.md").write_text("New normal report\n")
result = subprocess.run(
["bash", str(ROOT / "node/publish/publish.sh")],
cwd=work,
env={
**os.environ,
"PATH": str(binary) + os.pathsep + os.environ["PATH"],
"COMMAND_LOG": str(commands),
"PR_STATE": str(state),
"NPM_PROJECT": "app",
"BASE_BRANCH": "main",
"PROPOSAL_KIND": "security",
"AUDIT_SHA": base,
"VALIDATION_PASSED": str(candidate != "invalid").lower(),
"PROPOSAL_COMPLETE": "true",
"RUNNER_TEMP": str(tmp_path),
"PR_FOOTER": "Normal audit run",
"FAIL_HISTORY": str(history == "log-error").lower(),
},
capture_output=True,
text=True,
)
calls = [json.loads(line) for line in commands.read_text().splitlines()]
actual = git(
"ls-remote", "origin", "refs/heads/maintenance/node-security"
).split()[0]
return (
result,
head,
actual,
before,
state.read_bytes(),
calls,
git("rev-parse", "HEAD"),
base,
)

return publish


@pytest.mark.parametrize("history", ["repair", "maintainer", "stale-body"])
@pytest.mark.parametrize("candidate", ["changed", "unchanged", "invalid"])
def test_normal_audit_retains_unmerged_native_repair(
normal_publication, history, candidate
):
result, head, actual, before, after, calls, checkout, base = normal_publication(
history, candidate
)
assert result.returncode == int(candidate == "invalid"), (
result.stdout + result.stderr
)
assert actual == head
assert before == after
assert checkout == base
assert all(call[:2] == ["pr", "list"] for call in calls)
assert "unmerged validated repair" in result.stdout


@pytest.mark.parametrize("history", ["ordinary", "forged-body", "lookalike", "merged"])
def test_normal_audit_requires_unmerged_exact_native_repair(
normal_publication, history
):
result, head, actual, _, _, calls, _, _ = normal_publication(history)
assert result.returncode == 0, result.stdout + result.stderr
assert actual != head
assert "unmerged validated repair" not in result.stdout
assert any(call[:2] == ["pr", "edit"] for call in calls)


@pytest.mark.parametrize("history", ["missing-head", "log-error"])
def test_normal_audit_fails_closed_when_repair_history_unavailable(
normal_publication, history
):
result, head, actual, before, after, calls, checkout, base = normal_publication(
history
)
assert result.returncode != 0
assert actual == head
assert before == after
assert checkout == base
assert all(call[:2] == ["pr", "list"] for call in calls)
assert "Unable to inspect unmerged repair history" in result.stderr


@pytest.mark.parametrize("override", ["2.14.1", "2.14.2", "^2.14.1", None])
def test_security_candidate_cannot_bypass_retained_exact_direct_pin(tmp_path, override):
manifest = {
Expand Down
18 changes: 18 additions & 0 deletions repair-app/pilot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,21 @@ expected-head guards after the bounded test. No source merge, stable-tag update,
Hydra change or App/Worker/D1 deployment is included. The deployed App's historical
harmless-pilot wording must be corrected before general actual-repair use; this
is an isolated publisher test, not general consumer rollout.

## Isolated normal-audit preservation acceptance

Use this slice's reviewed reusable workflow reference and keep the repair
caller's five inputs unchanged. After a genuine validated repair is published,
run normal security publication against that open PR and verify that its head,
body, open state and draft status remain unchanged. Repeat with a later
maintainer commit and with the preserved partial-publication fixture whose body
is stale. Retain the original native repair receipts and actual run URLs.

The normal publisher reads unmerged native commit trailers, not the PR body.
Real Git controls must reject a forged body marker and a lookalike trailer,
permit later publication after the repair is merged, and stop on unavailable
history. A failed new audit must remain visible in Actions without drafting or
rewriting the existing repaired PR. Restore the exact isolated fixture after
verification. The complete G-REPAIR gate also requires a clean review of
preparation, separate validation, publication and preservation together. No merge
or consumer rollout follows from that gate alone.
Loading