Skip to content

release: the macOS signing script stops changing directory - #18

Merged
donislawdev merged 4 commits into
mainfrom
fix/the-mac-signing-script-does-not-change-directory
Aug 28, 2026
Merged

release: the macOS signing script stops changing directory#18
donislawdev merged 4 commits into
mainfrom
fix/the-mac-signing-script-does-not-change-directory

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

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

sign_macos: tfg-gui.app was signed by a DIFFERENT certificate
expected C656A279...
got none

The signature was correct. On that same bundle, still sitting on the Mac:

Authority=Developer ID Application: Dominik Babiarz (...)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=28 Aug 2026 at 22:50:55

What failed was reading it back. certificate_of cd'd into a temporary directory and only then handed codesign the bundle path - because --extract-certificates reads like a file name. That path is relative: sign_release.py hands this script a directory under the home directory. After the cd it named nothing, codesign answered No such file or directory, no cert0 appeared, and the function returned none.

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:

bundle path result
relative, after a cd into the temp dir No such file or directory, no cert0, returns none
absolute cert0 appears, hashes to exactly the pinned digest
relative, no cd, prefix written as "$tmp/cert" the same, correctly

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 --keepParent still puts tfg-gui.app at the top of the archive - read back with unzip -l rather than assumed.

Why the rehearsal missed it

The rehearsal on 2026-08-28 ran sign_macos.sh by 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.0 points at the commit that built the archives, and this fixes tooling that runs on the signing machine - sign_release.py copies sign_macos.sh out 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.

donislawdev and others added 4 commits August 28, 2026 23:58
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
donislawdev merged commit a670276 into main Aug 28, 2026
18 checks passed
@donislawdev
donislawdev deleted the fix/the-mac-signing-script-does-not-change-directory branch August 28, 2026 22:32
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