fix(ci/release): archive unsigned + codesign Developer ID directly
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 29s
apple / swift (push) Successful in 1m18s
ci / rust (push) Successful in 1m24s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 3m2s
docker / deploy-docs (push) Successful in 17s
rpm / build-publish (push) Successful in 4m19s

xcodebuild's archive gate demands a provisioning profile for the app's
keychain-access-groups entitlement (the 'Keychain Sharing' capability) under
both automatic AND manual signing — even though a Developer ID app honours that
team-prefixed entitlement at runtime with no profile. So manual signing just
traded the -61 keychain error for 'requires a provisioning profile'.

Sidestep the gate: archive with CODE_SIGNING_ALLOWED=NO, then codesign the app
bundle directly with the Developer ID identity, hardened runtime and a secure
timestamp, applying the entitlements via --entitlements (with $(AppIdentifierPrefix)
resolved to the team prefix, which codesign won't expand). Safe because the
bundle is a single statically-linked binary — static PunktfunkCore.xcframework,
SPM static products, macOS 14 target, no Embed-Frameworks phase — so there is no
nested code to sign inside-out. No Apple Developer portal profile or new secret
needed. iOS App Store path unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 14:35:16 +00:00
parent 3c617f655e
commit 4d93eb24ff
+37 -36
View File
@@ -120,50 +120,51 @@ jobs:
printf '%s' "$ASC_P8" > "$RUNNER_TEMP/asc.p8" printf '%s' "$ASC_P8" > "$RUNNER_TEMP/asc.p8"
chmod 600 "$RUNNER_TEMP/asc.p8" chmod 600 "$RUNNER_TEMP/asc.p8"
- name: Archive macOS - name: Archive macOS (unsigned — signed by codesign below)
run: | run: |
# Manual Developer ID signing — same reasoning as the export step below. With # Archive WITHOUT signing, then codesign with Developer ID in the next step. We do
# -allowProvisioningUpdates the archive runs AUTOMATIC signing, which tries to mint # NOT let xcodebuild sign during archive because the app's keychain-access-groups
# an Apple Development cert + a "Mac App Development" profile for io.unom.punktfunk: # entitlement is the "Keychain Sharing" capability, and Xcode's archive gate demands
# installing that cert into the CI keychain fails (DVTSecErrorDomain -61 "Write # a provisioning profile for it under BOTH automatic and manual signing — even
# permissions error") and no such profile exists — a Developer ID DMG needs # though a Developer ID app honours that team-prefixed entitlement at RUNTIME with
# neither. Pin the Developer ID identity and no profile: the app is non-sandboxed # no profile (the gate is an Xcode build-phase check, not a real requirement). Raw
# and its lone entitlement (keychain-access-groups, team-prefixed) is authorized by # codesign has no such gate. Safe because the bundle is a single statically-linked
# the Developer ID team itself, so no provisioning profile is required. The ASC key # binary: static PunktfunkCore.xcframework, SPM static products, macOS 14 target (no
# is still staged above for notarytool + the iOS App Store archive. # embedded Swift dylibs), and no Embed-Frameworks phase — so nothing nested to sign.
DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \ DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \
-project "$PROJECT" -scheme Punktfunk \ -project "$PROJECT" -scheme Punktfunk \
-destination 'generic/platform=macOS' \ -destination 'generic/platform=macOS' \
-archivePath "$RUNNER_TEMP/Punktfunk-macos.xcarchive" \ -archivePath "$RUNNER_TEMP/Punktfunk-macos.xcarchive" \
MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \ MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \
CODE_SIGN_STYLE=Manual \ CODE_SIGNING_ALLOWED=NO
CODE_SIGN_IDENTITY="Developer ID Application" \
DEVELOPMENT_TEAM="$TEAM_ID" \
PROVISIONING_PROFILE_SPECIFIER=""
- name: Export macOS (Developer ID) - name: Sign macOS app (Developer ID, hardened runtime)
run: | run: |
cat > "$RUNNER_TEMP/export-devid.plist" <<EOF APP="$RUNNER_TEMP/Punktfunk-macos.xcarchive/Products/Applications/Punktfunk.app"
<?xml version="1.0" encoding="UTF-8"?> # codesign does NOT expand $(AppIdentifierPrefix) (an Xcode build-setting var), so
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> # resolve it to the real team prefix — otherwise keychain-access-groups would be the
<plist version="1.0"> # literal string instead of the team-scoped group.
<dict> RESOLVED="$RUNNER_TEMP/Punktfunk.entitlements"
<key>method</key><string>developer-id</string> sed "s/\$(AppIdentifierPrefix)/${TEAM_ID}./g" \
<key>teamID</key><string>$TEAM_ID</string> clients/apple/Config/Punktfunk.entitlements > "$RESOLVED"
<key>destination</key><string>export</string> # Inside-out: sign any nested Mach-O first (defensive — the static build normally
<!-- Manual + explicit cert: with -allowProvisioningUpdates Xcode prefers # has none), then the app bundle with the resolved entitlements + hardened runtime +
CLOUD-managed Developer ID signing, which the App-Manager-role API key # secure timestamp, which is what notarization requires.
can't do ("Cloud signing permission error") and it never falls back to if [ -d "$APP/Contents/Frameworks" ]; then
the perfectly valid local identity. --> find "$APP/Contents/Frameworks" -depth \( -name '*.framework' -o -name '*.dylib' \) \
<key>signingStyle</key><string>manual</string> -print0 | while IFS= read -r -d '' f; do
<key>signingCertificate</key><string>Developer ID Application</string> codesign --force --options runtime --timestamp \
</dict> --sign "Developer ID Application" "$f"
</plist> done
EOF fi
DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild -exportArchive \ codesign --force --options runtime --timestamp \
-archivePath "$RUNNER_TEMP/Punktfunk-macos.xcarchive" \ --entitlements "$RESOLVED" \
-exportOptionsPlist "$RUNNER_TEMP/export-devid.plist" \ --sign "Developer ID Application" "$APP"
-exportPath "$RUNNER_TEMP/export-devid" codesign --verify --strict --verbose=2 "$APP"
# Stage where the DMG step expects it ($RUNNER_TEMP/export-devid/Punktfunk.app).
mkdir -p "$RUNNER_TEMP/export-devid"
rm -rf "$RUNNER_TEMP/export-devid/Punktfunk.app"
cp -R "$APP" "$RUNNER_TEMP/export-devid/Punktfunk.app"
- name: Notarized DMG - name: Notarized DMG
run: | run: |