Skip to content

refactor(notarization): move stage record rules into Go - #65

Merged
Dunky13 merged 4 commits into
mainfrom
arch/notarization-record
Nov 3, 2026
Merged

Dunky13 merged 4 commits into
mainfrom
arch/notarization-record

Conversation

@Dunky13

@Dunky13 Dunky13 commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

Go now owns the notarization stage record. internal/notaryapi/record.go parses and validates it and owns the phase transitions, and both the REST submit path and the pipeline scripts use it through queue-tool notarize record {get,validate,create,advance}. The duplicated stage-record jq validators and writers are gone from macos-resume.sh, macos-receipts.sh and distribution-publish.sh.

This is intended to preserve behaviour. Records already written still validate and resume, including those without prepared_run_id, and a failed preparation checkpoint still resumes. New output matches the last jq writers byte for byte, though a rewrite of a real production record reverses prepared_attempt and prepared_run_id, which is harmless because consumers ignore key order. Exit codes keep set -e, pipefail and the existing || fail messages working. The differences that do exist are listed in the handoff, most notably that macos-resume.sh now requires QUEUE_NOTARY_TOOL for every operation on existing state, including local Keychain-profile use.

Tool provisioning. Each job builds queue-tool from its own checkout, and it never crosses jobs or architectures as an artifact.

Job Runner Tool Workflow change
notarize macos-15 Existing build step before any record use None
publish ubuntu-24.04 New one-step build; setup-go was already there One step
recover ubuntu-24.04 Not available None, see below

Reported rather than worked around. distribution-recovery.yml has no Go toolchain. It runs every 15 minutes with actions: write and a 10 minute budget, so adding a toolchain there is a meaningful workflow change. Its jq therefore still restates the phase list, the empty-ID rule, the SHA-256 format and the UUID format for recovery.json, which is a separate 20-key document. TestRecoveryProjectionMatchesRecordRules runs those rules through real jq and the module's valid() over the same corpus and fails on any disagreement it exposes, from either side. It only sees differences that corpus exposes, and it leaves out trailing-newline values, where the two differ on purpose.

Wiring guard limits. TestStageRecordToolIsBuiltBeforeEveryConsumer reads workflow text and does not interpret it. It catches a missing, commented-out, conditional or cross-compiled build, a build allowed to fail, a missing $GITHUB_ENV export, a consumer given a different path, a later $GITHUB_ENV rewrite, and a later cp, mv, redirect or download-artifact onto the path. It misses, confirmed by trying them, a write through another variable, rm of the tool, and a write inside a called script. A missing tool fails closed at the first record read. A replaced binary that still behaves would go unnoticed. Whether to add Go there is a separate decision.

Out of scope and unchanged: GitHub inventory pagination duplicated between scripts/macos-checkpoint.sh and scripts/distribution-common.sh.

Evidence

Local results are from a host under sustained load with swap nearly exhausted. "Latest repair" is the commit after afd42e1.

Check Candidate Result
go test ./internal/notaryapi (non-race) Latest repair PASS
Narrow non-race internal/tooling runs: the new and changed tests Latest repair PASS
scripts/lint.sh Latest repair PASS
26 mutations tried on the real files, each checked to compile Latest repair 23 caught by their assertion, 3 survived (the wiring guard limits above)
Narrow non-race runs of TestDistributionPublish*, TestWorkflow* and the main resume, upload and origin tests afd42e1 PASS
Seven earlier mutations on the real files afd42e1 All caught by their assertion
internal/notaryapi under -race, inside validate job 2777 c38735c PASS
Focused non-race runs of internal/tooling: 139 of 176 top-level tests, covering resume, receipts, publication, checkpoint, signing and workflow c38735c 137 PASS, 2 skipped (TestNotarizedHomebrew*, environment-gated)
Linux amd64 cross-build of queue-tool c38735c PASS
scripts/validate.sh, job 2777 c38735c FAIL

Corrections. At afd42e1 the recovery drift test missed four format widenings, and the failure message echoed non-UUID values such as deadbeef. Both are fixed here.

Correction to the earlier claim on jq and Go. I said every disagreement was a trailing newline. That was wrong, because my 835-record corpus lacked the other shapes. Independent review compared them over 1,477 unique records: jq accepted 108 that Go rejected (trailing newline 38, duplicate keys 20, over 16 KiB 20, multiple documents 20, BOM 10), and Go accepted nothing jq rejected. No writer produces any of these.

scripts/validate.sh did not pass, and neither tooling shard completed locally. No commit after c38735c reran them locally, by instruction, because of host memory pressure.

  • Job 2777 stopped at go test, so web, browser and native tests, the app build, and the installer and DMG checks did not run.
  • internal/tooling reached the 12 minute limit with no test failure reported, so the package did not complete.
  • internal/backend reached the same limit. This branch does not touch it.
  • internal/remote failed three Caddy tests on wall-clock limits. It does not import notaryapi.
  • Shard 1 (job 2758) hit the 12 minute limit before reads were batched. Shard 2 (job 2759) was interrupted by my own cancel. Jobs 2769 and 2770 were cancelled on the coordinator's request because the host ran out of swap.

Attributing those limits to host load is an inference: no baseline validate.sh ran under the same conditions.

Hosted CI runs both tooling shards on this PR, on both architectures, and is the first complete internal/tooling run on this code. Watch Intel timing: each queue-tool exec costs about 19 ms against about 3 ms for jq, and the two slowest resume tests measured about 15% slower. The 12 minute limit applies to the test binary, recent main runs took 441 s and 451 s, and the worst test step in 12 recent runs was 9:51.

Remaining verification. Real notarization cannot run here. The Distribution workflow (.github/workflows/distribution.yml, job notarize, then publish) first exercises this against Apple, only from main. No release or distribution run was triggered.

Merge Danger

Door: Two-way. Records stay readable in both directions, and reverting restores the jq validators with no migration. Key order can differ after a rewrite, and consumers ignore it.

Blast Radius: The local notarytool path now prints Apple's ID, only when it is a whole UUID, if its submitted write fails, which fixes a defect that also existed at 7b4b27c. queue-tool now runs without GH_TOKEN in distribution-publish.sh and distribution-source.sh. The main-only Distribution workflow, jobs notarize and publish, plus any in-flight checkpoint resuming under new automation. A defect here would stop or fail a notarization run closed rather than publish. An upload already in the uploading phase still requires manual reconciliation, as it does today. submit now returns invalid_record where it returned submission_refused for malformed but parseable records, still before any provider call. Local macos-resume.sh use without QUEUE_NOTARY_TOOL now stops.

Before confirming a squash merge:

  • Required checks pass on the reviewed head.
  • The final squash author name and email have an exactly matching Signed-off-by trailer. Check the selected email, including any GitHub noreply default.
  • For a maintainer-owned PR, select squash author Marc Went <marc@went.io> and retain exactly one maintainer trailer: Signed-off-by: Marc Went <marc@went.io>. Consolidate duplicate maintainer trailers and the maintainer's noreply alias; preserve other contributors' certifications. Follow the commit policy below for GitHub CLI author selection and post-merge verification.
  • After owner approval, use sh scripts/merge-pr.sh PR_NUMBER REVIEWED_HEAD SQUASH_BODY_FILE to bind the reviewed head, primary author email, and single maintainer certification, then verify the actual merged commit. Green PR checks do not validate a future web squash commit's author identity.
  • A maintainer takeover of a bot update uses a maintainer-owned PR so the final author and certification remain aligned.
  • Keep Verified signatures and branch protection enabled.

See the commit policy and squash author instructions.

🤖 Generated with Claude Code

Dunky13 and others added 4 commits November 3, 2026 23:22
Parse, validate and advance the notarization stage record in one Go module
behind `queue-tool notarize record`. The pipeline scripts and the REST submit
path now use it instead of restating the key set, phases and ID formats in jq.

Records written by earlier runs still validate and resume, including those
without prepared_run_id, and writers keep the same serialization. Writes stay
atomic, bounded and fail closed. Only prepared, uploading, submitted, accepted,
finished is a legal sequence.

The publish job gains one step that builds queue-tool from its own checkout.
The recovery observer has no Go toolchain, so its recovery.json checks stay in
jq and a test keeps them aligned with the module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Marc Went <marc@went.io>
Make the tool wiring guard follow the built path through the GITHUB_ENV export
and into each consumer, and prove it against recorded mutations. Run the rules
distribution-recover.sh restates through real jq and the record module over the
same records, so drift from either side fails.

Validate publication stages without GH_TOKEN. On the local notarytool path, keep
Apple's ID in the failure message when the submitted write fails, as the REST path
does. Cover failed writes around the provider call, a leftover temporary file and
the real attempt-first record shape.

Report a failed existence check truthfully, drop a guard that can no longer be
false, and correct the handoff claims on jq differences and byte compatibility.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Marc Went <marc@went.io>
Add non-hex and prefixed values to the recovery drift corpus, so format widening
fails from either side, and pin those formats in the module's own table.

Extend the wiring guard to a conditional, ignorable, commented-out or cross-compiled
build, a later GITHUB_ENV rewrite and a later write onto the built path. Its comment
now says what it guarantees and what it does not.

Echo a provider value in the failure message only when it is a whole UUID, as the
REST path does. Run the source artifact verifier without GH_TOKEN, which it does not
use. Test the origin binding guard, and correct the handoff claims.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Marc Went <marc@went.io>
The echo gate test never used a whole UUID, so dropping the anchors from
id_pattern still passed and text around a valid ID could reach the log. Add
a UUID with a suffix and one with a prefix. List what the wiring guard
cannot see, including dd, rsync, a continued cp and an earlier GOOS export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Marc Went <marc@went.io>
@Dunky13
Dunky13 force-pushed the arch/notarization-record branch from a20c9e3 to 3550daf Compare November 3, 2026 22:23
@Dunky13
Dunky13 marked this pull request as ready for review November 3, 2026 22:23
@Dunky13
Dunky13 merged commit b34ee05 into main Nov 3, 2026
38 of 40 checks passed
@Dunky13
Dunky13 deleted the arch/notarization-record branch November 3, 2026 23:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant