authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-30 03:21:26-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-30 03:21:32-07:00
log354f001dec3d731a4d1a6fac0a25d9e28550d781
treeaa0bdbb45425d3e8df2428d68b3b3ca67ef8b76c
parent0aad32984f17248dab49bcb4b36a5342a8fcf036
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix(release): notarize with an API key file; stop shadowing folder()

Assisted-by: claude-opus-5.5

2 files changed, 21 insertions(+), 16 deletions(-)

tools/RELEASE.md+5-4
......@@ -56,10 +56,11 @@ agent's shell; `security set-key-partition-list -S apple-tool:,apple:,codesign:
5656-s -k PASSWORD ~/Library/Keychains/login.keychain-db` lets it sign without
5757asking.
5858
59Notarization runs when the keychain profile `snowbound` exists and signs in,
60and is skipped with a note otherwise. Make the profile once, with an App Store
61Connect API key (Users and Access, Integrations, Team Keys: a key with the
62Developer role; its `.p8`, key ID and issuer ID):
59Notarization runs when `~/.config/snowbound/notary.json` names an App Store
60Connect API key that signs in (`{"key": "~/.config/snowbound/AuthKey_ID.p8",
61"key_id": "ID", "issuer": "ISSUER"}`, mode 600), and is skipped with a note
62otherwise. A notarytool keychain profile would do, but storing one fails from an
63agent's shell, where the keychain refuses new items.
6364
6465```sh
6566xcrun notarytool store-credentials snowbound --key AuthKey_KEYID.p8 --key-id KEYID --issuer ISSUER-UUID
tools/release.py+16-12
......@@ -27,8 +27,8 @@ CHECKS = [
2727# Clover's Developer ID Application certificate, by its SHA-1 hash: its name is the account
2828# holder's legal name, which nothing here prints or stores.
2929IDENTITY = 'BA308AA3591299E053E8824CEF1651F686F8908E'
30# The notarytool keychain profile that notarizes it, once `xcrun notarytool store-credentials` makes it.
31NOTARY = 'snowbound'
30# The App Store Connect API key that notarizes it: {"key": P8 PATH, "key_id": ID, "issuer": ID}.
31NOTARY = Path('~/.config/snowbound/notary.json').expanduser()
3232# What hardened runtime needs for Record Audio and Record Video.
3333ENTITLEMENTS = {
3434 'com.apple.security.device.audio-input': True,
......@@ -98,10 +98,14 @@ def zip_bundle(bundle, archive):
9898 run(['ditto', '-c', '-k', '--norsrc', '--noextattr', '--noqtn', '--noacl', '--keepParent', bundle, archive])
9999
100100
101def notarizes():
102 """Whether the NOTARY profile exists and signs in."""
103 return subprocess.run(['xcrun', 'notarytool', 'history', '--keychain-profile', NOTARY],
104 capture_output=True).returncode == 0
101def notary():
102 """notarytool's credential arguments from NOTARY, if they sign in."""
103 if not NOTARY.exists():
104 return None
105 key = json.loads(NOTARY.read_text())
106 arguments = ['--key', str(Path(key['key']).expanduser()), '--key-id', key['key_id'], '--issuer', key['issuer']]
107 signs_in = subprocess.run(['xcrun', 'notarytool', 'history', *arguments], capture_output=True).returncode == 0
108 return arguments if signs_in else None
105109
106110
107111def build_mac(platform, folder, developer_id, notarize):
......@@ -117,7 +121,7 @@ def build_mac(platform, folder, developer_id, notarize):
117121 '--sign', IDENTITY, bundle])
118122 if notarize:
119123 zip_bundle(bundle, archive)
120 run(['xcrun', 'notarytool', 'submit', archive, '--keychain-profile', NOTARY, '--wait'])
124 run(['xcrun', 'notarytool', 'submit', archive, *notarize, '--wait'])
121125 run(['xcrun', 'stapler', 'staple', bundle])
122126 archive.unlink()
123127 zip_bundle(bundle, archive)
......@@ -143,9 +147,9 @@ def main():
143147 capture_output=True, text=True).stdout
144148 if developer_id and IDENTITY not in identities:
145149 sys.exit(f'The keychain has no signing identity {IDENTITY}; release with --ad-hoc, or add it.')
146 notarize = developer_id and notarizes()
150 notarize = notary() if developer_id else None
147151 if developer_id and not notarize:
148 print(f'Not notarizing: no notarytool profile "{NOTARY}" that signs in (see tools/RELEASE.md).',
152 print(f'Not notarizing: no API key in {NOTARY} that signs in (see tools/RELEASE.md).',
149153 file=sys.stderr)
150154
151155 commit = jj('log', '--no-graph', '-r', 'main', '-T', 'commit_id').strip()
......@@ -174,9 +178,9 @@ def main():
174178 built = {}
175179 for platform in args.platforms:
176180 if platform.startswith('macos'):
177 folder = stage / platform
178 folder.mkdir()
179 built[platform] = build_mac(platform, folder, developer_id, notarize)
181 work = stage / platform
182 work.mkdir()
183 built[platform] = build_mac(platform, work, developer_id, notarize)
180184 linux = [platform.removeprefix('linux-') for platform in args.platforms if platform.startswith('linux')]
181185 if linux:
182186 built |= build_linux(linux)