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:...@@ -56,10 +56,11 @@ agent's shell; `security set-key-partition-list -S apple-tool:,apple:,codesign:
56-s -k PASSWORD ~/Library/Keychains/login.keychain-db` lets it sign without56-s -k PASSWORD ~/Library/Keychains/login.keychain-db` lets it sign without
57asking.57asking.
5858
59Notarization runs when the keychain profile `snowbound` exists and signs in,59Notarization runs when `~/.config/snowbound/notary.json` names an App Store
60and is skipped with a note otherwise. Make the profile once, with an App Store60Connect API key that signs in (`{"key": "~/.config/snowbound/AuthKey_ID.p8",
61Connect API key (Users and Access, Integrations, Team Keys: a key with the61"key_id": "ID", "issuer": "ISSUER"}`, mode 600), and is skipped with a note
62Developer role; its `.p8`, key ID and issuer ID):62otherwise. A notarytool keychain profile would do, but storing one fails from an
63agent's shell, where the keychain refuses new items.
6364
64```sh65```sh
65xcrun notarytool store-credentials snowbound --key AuthKey_KEYID.p8 --key-id KEYID --issuer ISSUER-UUID66xcrun notarytool store-credentials snowbound --key AuthKey_KEYID.p8 --key-id KEYID --issuer ISSUER-UUID
tools/release.py+16-12
...@@ -27,8 +27,8 @@ CHECKS = [...@@ -27,8 +27,8 @@ CHECKS = [
27# Clover's Developer ID Application certificate, by its SHA-1 hash: its name is the account27# Clover's Developer ID Application certificate, by its SHA-1 hash: its name is the account
28# holder's legal name, which nothing here prints or stores.28# holder's legal name, which nothing here prints or stores.
29IDENTITY = 'BA308AA3591299E053E8824CEF1651F686F8908E'29IDENTITY = 'BA308AA3591299E053E8824CEF1651F686F8908E'
30# The notarytool keychain profile that notarizes it, once `xcrun notarytool store-credentials` makes it.30# The App Store Connect API key that notarizes it: {"key": P8 PATH, "key_id": ID, "issuer": ID}.
31NOTARY = 'snowbound'31NOTARY = Path('~/.config/snowbound/notary.json').expanduser()
32# What hardened runtime needs for Record Audio and Record Video.32# What hardened runtime needs for Record Audio and Record Video.
33ENTITLEMENTS = {33ENTITLEMENTS = {
34 'com.apple.security.device.audio-input': True,34 'com.apple.security.device.audio-input': True,
...@@ -98,10 +98,14 @@ def zip_bundle(bundle, archive):...@@ -98,10 +98,14 @@ def zip_bundle(bundle, archive):
98 run(['ditto', '-c', '-k', '--norsrc', '--noextattr', '--noqtn', '--noacl', '--keepParent', bundle, archive])98 run(['ditto', '-c', '-k', '--norsrc', '--noextattr', '--noqtn', '--noacl', '--keepParent', bundle, archive])
9999
100100
101def notarizes():101def notary():
102 """Whether the NOTARY profile exists and signs in."""102 """notarytool's credential arguments from NOTARY, if they sign in."""
103 return subprocess.run(['xcrun', 'notarytool', 'history', '--keychain-profile', NOTARY],103 if not NOTARY.exists():
104 capture_output=True).returncode == 0104 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
107def build_mac(platform, folder, developer_id, notarize):111def build_mac(platform, folder, developer_id, notarize):
...@@ -117,7 +121,7 @@ def build_mac(platform, folder, developer_id, notarize):...@@ -117,7 +121,7 @@ def build_mac(platform, folder, developer_id, notarize):
117 '--sign', IDENTITY, bundle])121 '--sign', IDENTITY, bundle])
118 if notarize:122 if notarize:
119 zip_bundle(bundle, archive)123 zip_bundle(bundle, archive)
120 run(['xcrun', 'notarytool', 'submit', archive, '--keychain-profile', NOTARY, '--wait'])124 run(['xcrun', 'notarytool', 'submit', archive, *notarize, '--wait'])
121 run(['xcrun', 'stapler', 'staple', bundle])125 run(['xcrun', 'stapler', 'staple', bundle])
122 archive.unlink()126 archive.unlink()
123 zip_bundle(bundle, archive)127 zip_bundle(bundle, archive)
...@@ -143,9 +147,9 @@ def main():...@@ -143,9 +147,9 @@ def main():
143 capture_output=True, text=True).stdout147 capture_output=True, text=True).stdout
144 if developer_id and IDENTITY not in identities:148 if developer_id and IDENTITY not in identities:
145 sys.exit(f'The keychain has no signing identity {IDENTITY}; release with --ad-hoc, or add it.')149 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
147 if developer_id and not notarize:151 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).',
149 file=sys.stderr)153 file=sys.stderr)
150154
151 commit = jj('log', '--no-graph', '-r', 'main', '-T', 'commit_id').strip()155 commit = jj('log', '--no-graph', '-r', 'main', '-T', 'commit_id').strip()
...@@ -174,9 +178,9 @@ def main():...@@ -174,9 +178,9 @@ def main():
174 built = {}178 built = {}
175 for platform in args.platforms:179 for platform in args.platforms:
176 if platform.startswith('macos'):180 if platform.startswith('macos'):
177 folder = stage / platform181 work = stage / platform
178 folder.mkdir()182 work.mkdir()
179 built[platform] = build_mac(platform, folder, developer_id, notarize)183 built[platform] = build_mac(platform, work, developer_id, notarize)
180 linux = [platform.removeprefix('linux-') for platform in args.platforms if platform.startswith('linux')]184 linux = [platform.removeprefix('linux-') for platform in args.platforms if platform.startswith('linux')]
181 if linux:185 if linux:
182 built |= build_linux(linux)186 built |= build_linux(linux)