mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix(release): notarize macOS apps before packaging
Replace electron-builder's internal macOS notarization wait with an explicit notarytool flow: build a signed app with update config, submit it with a bounded notarytool timeout, staple and validate it, then rebuild dmg/zip from the notarized app via --prepackaged. Keep draft-only signed/no-notary builds available for fast artifact checks.\n\nTested: bun test scripts/pr/release-workflow.test.ts\nTested: git diff --check\nTested: bun run scripts/release.ts 0.4.3 --dry\nTested: local arm64 signed/no-notary build followed by --prepackaged dmg/zip rebuild\nTested: bun run test:package-smoke --platform macos --package-kind release --artifacts-dir desktop/build-artifacts/electron\nTested: codesign --verify --deep --strict --verbose=2 desktop/build-artifacts/electron/mac-arm64/Claude\ Code\ Haha.app\nConfidence: medium\nScope-risk: moderate
This commit is contained in:
parent
f9b48a3031
commit
6715e75161
135
.github/workflows/release-desktop.yml
vendored
135
.github/workflows/release-desktop.yml
vendored
@ -76,11 +76,13 @@ jobs:
|
||||
- platform: macos-latest
|
||||
target_triple: aarch64-apple-darwin
|
||||
builder_args: --mac dmg zip --arm64
|
||||
app_bundle_dir: mac-arm64
|
||||
label: macOS-ARM64
|
||||
smoke_platform: macos
|
||||
- platform: macos-latest
|
||||
target_triple: x86_64-apple-darwin
|
||||
builder_args: --mac dmg zip --x64
|
||||
app_bundle_dir: mac
|
||||
label: macOS-x64
|
||||
smoke_platform: macos
|
||||
- platform: ubuntu-22.04
|
||||
@ -191,9 +193,9 @@ jobs:
|
||||
echo "macOS notarization requested: ${MACOS_NOTARIZE}"
|
||||
echo "::endgroup::"
|
||||
if [ "$MACOS_NOTARIZE" = "true" ]; then
|
||||
max_attempts=2
|
||||
build_timeout_seconds=2100
|
||||
builder_args=( ${{ matrix.builder_args }} --publish never )
|
||||
max_attempts=1
|
||||
build_timeout_seconds=900
|
||||
builder_args=( ${{ matrix.builder_args }} --publish never -c.mac.notarize=false )
|
||||
else
|
||||
max_attempts=1
|
||||
build_timeout_seconds=900
|
||||
@ -234,25 +236,118 @@ jobs:
|
||||
wait "$build_pid"
|
||||
}
|
||||
|
||||
for attempt in $(seq 1 "$max_attempts"); do
|
||||
echo "Starting signed electron-builder attempt ${attempt}/${max_attempts} at $(date -u '+%Y-%m-%dT%H:%M:%SZ') with ${build_timeout_seconds}s watchdog"
|
||||
rm -rf build-artifacts/electron
|
||||
set +e
|
||||
run_signed_electron_builder "$build_timeout_seconds" node ./node_modules/electron-builder/out/cli/cli.js "${builder_args[@]}"
|
||||
status=$?
|
||||
set -e
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo "Finished signed electron-builder attempt ${attempt}/${max_attempts} at $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
exit 0
|
||||
run_electron_builder_with_retries() {
|
||||
local attempts="$1"
|
||||
local timeout_seconds="$2"
|
||||
local clean_before_attempt="$3"
|
||||
shift 3
|
||||
local status=0
|
||||
|
||||
for attempt in $(seq 1 "$attempts"); do
|
||||
echo "Starting signed electron-builder attempt ${attempt}/${attempts} at $(date -u '+%Y-%m-%dT%H:%M:%SZ') with ${timeout_seconds}s watchdog"
|
||||
if [ "$clean_before_attempt" = "true" ]; then
|
||||
rm -rf build-artifacts/electron
|
||||
fi
|
||||
set +e
|
||||
run_signed_electron_builder "$timeout_seconds" "$@"
|
||||
status=$?
|
||||
set -e
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo "Finished signed electron-builder attempt ${attempt}/${attempts} at $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$attempt" -eq "$attempts" ]; then
|
||||
echo "::error::Signed electron-builder failed after ${attempts} attempts"
|
||||
return "$status"
|
||||
fi
|
||||
echo "::warning::Signed electron-builder attempt ${attempt}/${attempts} failed with exit code ${status}; retrying after 120 seconds"
|
||||
sleep 120
|
||||
done
|
||||
}
|
||||
|
||||
notarize_app_bundle() {
|
||||
local app_path="$1"
|
||||
local notary_zip="${RUNNER_TEMP}/${{ matrix.label }}-notary.zip"
|
||||
local notary_attempts=3
|
||||
local notary_timeout=20m
|
||||
local status=0
|
||||
|
||||
if [ ! -d "$app_path" ]; then
|
||||
echo "::error::Expected signed macOS app bundle does not exist: ${app_path}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$attempt" -eq "$max_attempts" ]; then
|
||||
echo "::error::Signed electron-builder failed after ${max_attempts} attempts"
|
||||
exit "$status"
|
||||
fi
|
||||
echo "::warning::Signed electron-builder attempt ${attempt}/${max_attempts} failed with exit code ${status}; retrying after 120 seconds"
|
||||
sleep 120
|
||||
done
|
||||
echo "::group::macOS notarization"
|
||||
echo "Verifying signed app before notarization: ${app_path}"
|
||||
codesign --verify --deep --strict --verbose=2 "$app_path"
|
||||
rm -f "$notary_zip"
|
||||
(
|
||||
cd "$(dirname "$app_path")"
|
||||
ditto -c -k --sequesterRsrc --keepParent "$(basename "$app_path")" "$notary_zip"
|
||||
)
|
||||
|
||||
for attempt in $(seq 1 "$notary_attempts"); do
|
||||
echo "Starting notarytool attempt ${attempt}/${notary_attempts} at $(date -u '+%Y-%m-%dT%H:%M:%SZ') with ${notary_timeout} timeout"
|
||||
set +e
|
||||
xcrun notarytool submit "$notary_zip" \
|
||||
--apple-id "$APPLE_ID" \
|
||||
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
|
||||
--team-id "$APPLE_TEAM_ID" \
|
||||
--wait \
|
||||
--timeout "$notary_timeout" \
|
||||
--output-format json
|
||||
status=$?
|
||||
set -e
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo "Notarytool attempt ${attempt}/${notary_attempts} succeeded at $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
xcrun stapler staple "$app_path"
|
||||
xcrun stapler validate "$app_path"
|
||||
spctl -a -vv -t execute "$app_path"
|
||||
echo "::endgroup::"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$attempt" -eq "$notary_attempts" ]; then
|
||||
echo "::endgroup::"
|
||||
echo "::error::notarytool failed after ${notary_attempts} attempts"
|
||||
return "$status"
|
||||
fi
|
||||
echo "::warning::notarytool attempt ${attempt}/${notary_attempts} failed with exit code ${status}; retrying after 120 seconds"
|
||||
sleep 120
|
||||
done
|
||||
}
|
||||
|
||||
set +e
|
||||
run_electron_builder_with_retries "$max_attempts" "$build_timeout_seconds" true node ./node_modules/electron-builder/out/cli/cli.js "${builder_args[@]}"
|
||||
status=$?
|
||||
set -e
|
||||
if [ "$status" -ne 0 ]; then
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
if [ "$MACOS_NOTARIZE" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
app_path="build-artifacts/electron/${{ matrix.app_bundle_dir }}/Claude Code Haha.app"
|
||||
set +e
|
||||
notarize_app_bundle "$app_path"
|
||||
status=$?
|
||||
set -e
|
||||
if [ "$status" -ne 0 ]; then
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
package_args=( ${{ matrix.builder_args }} --prepackaged "$app_path" --publish never -c.mac.notarize=false )
|
||||
find build-artifacts/electron -maxdepth 1 -type f -delete
|
||||
set +e
|
||||
run_electron_builder_with_retries 1 900 false node ./node_modules/electron-builder/out/cli/cli.js "${package_args[@]}"
|
||||
status=$?
|
||||
set -e
|
||||
if [ "$status" -ne 0 ]; then
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
- name: Build unsigned Electron release artifacts
|
||||
if: matrix.smoke_platform != 'macos' || needs.signing-preflight.outputs.macos_signed != 'true'
|
||||
|
||||
@ -129,6 +129,8 @@ describe('release desktop workflow', () => {
|
||||
const signedBuildStep = extractStep(workflow, 'Build signed macOS Electron release artifacts')
|
||||
const unsignedBuildStep = extractStep(workflow, 'Build unsigned Electron release artifacts')
|
||||
|
||||
expect(workflow).toContain('app_bundle_dir: mac-arm64')
|
||||
expect(workflow).toContain('app_bundle_dir: mac')
|
||||
expect(signedBuildStep).toContain("if: matrix.smoke_platform == 'macos' && needs.signing-preflight.outputs.macos_signed == 'true'")
|
||||
expect(signedBuildStep).toContain('CSC_LINK: ${{ secrets.MACOS_CERTIFICATE }}')
|
||||
expect(signedBuildStep).toContain('CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}')
|
||||
@ -143,16 +145,26 @@ describe('release desktop workflow', () => {
|
||||
expect(signedBuildStep).toContain('xcrun --find notarytool')
|
||||
expect(signedBuildStep).toContain('security find-identity -v -p codesigning')
|
||||
expect(signedBuildStep).toContain('macOS notarization requested: ${MACOS_NOTARIZE}')
|
||||
expect(signedBuildStep).toContain('max_attempts=2')
|
||||
expect(signedBuildStep).toContain('build_timeout_seconds=2100')
|
||||
expect(signedBuildStep).toContain('builder_args=( ${{ matrix.builder_args }} --publish never -c.mac.notarize=false )')
|
||||
expect(signedBuildStep).toContain('max_attempts=1')
|
||||
expect(signedBuildStep).toContain('build_timeout_seconds=900')
|
||||
expect(signedBuildStep).toContain('-c.mac.notarize=false')
|
||||
expect(signedBuildStep).toContain('macOS notarization is disabled for this draft run')
|
||||
expect(signedBuildStep).toContain('run_signed_electron_builder')
|
||||
expect(signedBuildStep).toContain('run_electron_builder_with_retries')
|
||||
expect(signedBuildStep).toContain('notarize_app_bundle')
|
||||
expect(signedBuildStep).toContain('xcrun notarytool submit "$notary_zip"')
|
||||
expect(signedBuildStep).toContain('--timeout "$notary_timeout"')
|
||||
expect(signedBuildStep).toContain('notary_attempts=3')
|
||||
expect(signedBuildStep).toContain('xcrun stapler staple "$app_path"')
|
||||
expect(signedBuildStep).toContain('xcrun stapler validate "$app_path"')
|
||||
expect(signedBuildStep).toContain('spctl -a -vv -t execute "$app_path"')
|
||||
expect(signedBuildStep).toContain('app_path="build-artifacts/electron/${{ matrix.app_bundle_dir }}/Claude Code Haha.app"')
|
||||
expect(signedBuildStep).toContain('package_args=( ${{ matrix.builder_args }} --prepackaged "$app_path" --publish never -c.mac.notarize=false )')
|
||||
expect(signedBuildStep).toContain('find build-artifacts/electron -maxdepth 1 -type f -delete')
|
||||
expect(signedBuildStep).toContain('Signed electron-builder timed out')
|
||||
expect(signedBuildStep).toContain('pkill -TERM -P "$build_pid"')
|
||||
expect(signedBuildStep).toContain('with ${build_timeout_seconds}s watchdog')
|
||||
expect(signedBuildStep).toContain('with ${timeout_seconds}s watchdog')
|
||||
expect(signedBuildStep).toContain('set +e')
|
||||
expect(signedBuildStep).toContain('status=$?')
|
||||
expect(signedBuildStep).toContain('if [ "$status" -eq 0 ]; then')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user