release: the macOS signing script stops changing directory - #18
Merged
donislawdev merged 4 commits intoAug 28, 2026
Merged
Conversation
Found by the first real release, which it stopped twice in two consecutive steps. Phase B refused on tfg-gui.app with "was signed by a DIFFERENT certificate, expected C656..., got none". The signature was correct: codesign -dv on that same bundle shows Developer ID Application, a chain to the Apple Root CA and a timestamp. What failed was reading it back. certificate_of cd'd into a temporary directory and then asked codesign about the bundle, because --extract-certificates looked like a file name rather than a path. The bundle path is relative - sign_release.py hands this script a directory under the home directory - so after the cd it named nothing, codesign answered "No such file or directory", and no certificate came out. A refusal that is right about there being a problem and wrong about what it is costs more than a silent one: it sends somebody to look at the card. Measured on the Mac, all three ways: relative after a cd gives nothing, absolute gives exactly the pinned digest, and relative with the prefix written as a path and no cd gives the same. So the cd goes. The same trap sat one step further on and was measured before anybody reached it: the zip for notarisation cd'd into the unpacked directory and wrote to a path relative to where the script started, so the file was never created. Bare ditto with both paths as they arrive works, and --keepParent still puts the .app at the top of the archive - read back out of the zip rather than assumed. Why the rehearsal did not show this: it ran the script by hand with an absolute directory, and the real caller passes a relative one. The command was proven, the call was not. The guard asks whether there is any cd at all rather than whether the paths are absolute, because that is the property that can be read off the file. If a directory change is ever genuinely needed here, the guard is the conversation about it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The second thing the first real release found, and this one landed after the signing was already done - which is the expensive moment for a release step to stop. attest-release.yml fetched the draft with its own list of patterns - archives, the SBOM, the checksums - and then ran sha256sum -c over the checksums. The checksums describe ten files, including the provenance bundle, and no pattern matched that one. sha256sum said "No such file or directory" about a file that was on the release page the whole time, and the run failed. The failure mode is not a missing file. It is a check reporting a problem with the release when the problem is in the checker, at the one moment when somebody is holding a card and half a release. So the second list goes rather than growing a pattern: the names come out of verify-SHA256SUMS.txt, which is the same document that is then verified. An asset added later cannot make the two disagree, because there is one list. The names are collected before anything is fetched, because gh reads stdin and a download inside the read loop eats the rest of the list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ly carries The third thing the first real release found, and this refusal was correct. Phase C stopped with "the release notes tell people to pass --predicate-type https://spdx.dev/Document and the statement that was just made is https://spdx.dev/Document/v2.3". That check exists precisely because the provenance plan wrote the URI down as a promise taken from the action's documentation rather than from an attestation, and said out loud it was unmeasured. It has now been measured: the statement carries the versioned one. The value was written in five places across three files and only one of them was being checked. Three of the five are not checks at all - they are the commands a person copies out of the release notes, and with the wrong URI gh answers "no attestation found", which reads exactly like a release nobody attested. It cannot be written once: three workflows are three files and a workflow cannot read a constant out of another one. So the guard asks for agreement rather than for a single home - it collects every spdx.dev URI in .github/workflows and refuses when there are two different ones. The lesson worth keeping: read a predicate type out of a bundle, not out of documentation. base64 -d on dsseEnvelope.payload and predicateType is inside. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rule 13 again, and by me again - the same mistake this file already records twice. The markers belong in the project's notes, which are not in the repository. The workflows and the shell script keep theirs: the punctuation guard reads .go files, and those files have carried that style since they were written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
donislawdev
deleted the
fix/the-mac-signing-script-does-not-change-directory
branch
August 28, 2026 22:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found by the first real release of 0.2.0, which it stopped twice in two consecutive steps. Nothing was published and nothing was handed back - the release is still an empty draft with the build sitting as an artefact.
What phase B said, and why it was wrong about itself
The signature was correct. On that same bundle, still sitting on the Mac:
What failed was reading it back.
certificate_ofcd'd into a temporary directory and only then handed codesign the bundle path - because--extract-certificatesreads like a file name. That path is relative:sign_release.pyhands this script a directory under the home directory. After the cd it named nothing, codesign answeredNo such file or directory, nocert0appeared, and the function returnednone.A refusal that is right that there is a problem and wrong about what it is costs more than a silent one - it sends somebody to look at the card and the certificate.
Measured on the Mac, all three ways:
No such file or directory, nocert0, returnsnonecert0appears, hashes to exactly the pinned digest"$tmp/cert"The same trap one step further on
The zip for notarisation cd'd into the unpacked directory and then wrote to a path relative to where the script started, so the file was never created and the next line would have stopped the release with "could not zip". Measured before anybody reached it: as written, the zip is not created; without the cd it is, and
--keepParentstill putstfg-gui.appat the top of the archive - read back withunzip -lrather than assumed.Why the rehearsal missed it
The rehearsal on 2026-08-28 ran
sign_macos.shby hand with an absolute directory. The real caller passes a relative one. The command was proven, the call was not.The guard
TestTheMacSigningScriptDoesNotDependOnWhereItIsRunFrom, proven by a mutation that puts the cd back. It asks whether there is any cd rather than whether the paths are absolute, because that is the property that can be read off the file - and if a directory change is ever genuinely needed here, the guard is the conversation about it.Not in the tag, on purpose
v0.2.0points at the commit that built the archives, and this fixes tooling that runs on the signing machine -sign_release.pycopiessign_macos.shout of the working tree, so phase B picks the fix up without rebuilding or re-tagging. The published bytes are the same either way, and re-tagging would mean throwing away a build that is already attested.