Skip to content

Release

Release #9

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: Existing v* tag
required: true
type: string
notarize:
description: Submit the package to Apple notarization
required: true
default: true
type: boolean
permissions:
contents: write
concurrency:
group: release-${{ inputs.tag || github.ref_name }}
cancel-in-progress: false
jobs:
release:
runs-on: macos-15
timeout-minutes: 360
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.tag || github.ref }}
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_26.3.app/Contents/Developer
- name: Install XcodeGen
run: brew install xcodegen
- name: Check signing inputs
env:
MACOS_DEVELOPER_ID_P12_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_P12_BASE64 }}
MACOS_DEVELOPER_ID_P12_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_P12_PASSWORD }}
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY_BASE64 }}
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
for name in \
MACOS_DEVELOPER_ID_P12_BASE64 \
MACOS_DEVELOPER_ID_P12_PASSWORD \
APP_STORE_CONNECT_KEY_ID \
APP_STORE_CONNECT_ISSUER_ID \
APP_STORE_CONNECT_PRIVATE_KEY_BASE64 \
SPARKLE_PRIVATE_KEY
do
if [[ -z "${!name}" ]]; then
echo "Missing signing input: $name" >&2
exit 2
fi
done
- name: Import Developer ID certificate
env:
CERTIFICATE: ${{ secrets.MACOS_DEVELOPER_ID_P12_BASE64 }}
CERTIFICATE_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_P12_PASSWORD }}
run: |
CERTIFICATE_PATH="$RUNNER_TEMP/developer-id.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 32)"
printf '%s' "$CERTIFICATE" | base64 --decode -o "$CERTIFICATE_PATH"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERTIFICATE_PATH" \
-P "$CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 \
-k "$KEYCHAIN_PATH"
security set-key-partition-list \
-S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" \
"$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
identity="$(
security find-identity -v -p codesigning "$KEYCHAIN_PATH" \
| sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p'
)"
identity_count="$(printf '%s\n' "$identity" | awk 'NF { count++ } END { print count + 0 }')"
if [[ "$identity_count" -ne 1 ]]; then
echo "Expected exactly one Developer ID Application identity." >&2
exit 2
fi
echo "CODESIGN_IDENTITY=$identity" >> "$GITHUB_ENV"
- name: Test
run: |
xcodegen generate
xcodebuild test \
-project CodexTokenLedger.xcodeproj \
-scheme CodexTokenLedger \
-configuration Debug \
-destination 'platform=macOS'
- name: Build and sign
run: |
./scripts/build_app.sh
./scripts/package_release.sh
codesign --verify --deep --strict --verbose=2 dist/CodexTokenLedger.app
REQUIRE_DEVELOPER_ID=1 ./scripts/package_dmg.sh
- name: Notarize and staple
if: ${{ github.event_name == 'workflow_dispatch' && inputs.notarize }}
env:
KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY_BASE64 }}
run: |
APP="dist/CodexTokenLedger.app"
ZIP="dist/CodexTokenLedger-menu-bar-macOS.zip"
DMG="dist/CodexTokenLedger-menu-bar-macOS.dmg"
KEY_PATH="$RUNNER_TEMP/AuthKey_${KEY_ID}.p8"
printf '%s' "$PRIVATE_KEY" | base64 --decode -o "$KEY_PATH"
submission="$(xcrun notarytool submit "$DMG" \
--key "$KEY_PATH" \
--key-id "$KEY_ID" \
--issuer "$ISSUER_ID" \
--output-format json)"
submission_id="$(
printf '%s' "$submission" \
| /usr/bin/python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])'
)"
echo "Notary submission: $submission_id"
deadline=$((SECONDS + 18000))
while (( SECONDS < deadline )); do
if info="$(xcrun notarytool info "$submission_id" \
--key "$KEY_PATH" \
--key-id "$KEY_ID" \
--issuer "$ISSUER_ID" \
--output-format json)"
then
status="$(
printf '%s' "$info" \
| /usr/bin/python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])'
)"
echo "Notary status: $status"
case "$status" in
Accepted)
break
;;
Invalid|Rejected)
xcrun notarytool log "$submission_id" \
--key "$KEY_PATH" \
--key-id "$KEY_ID" \
--issuer "$ISSUER_ID"
exit 1
;;
esac
else
echo "Notary status request failed; retrying."
fi
sleep 30
done
if [[ "${status:-}" != "Accepted" ]]; then
echo "Notarization did not finish before the release timeout." >&2
exit 1
fi
xcrun stapler staple "$DMG"
xcrun stapler validate "$DMG"
xcrun stapler staple "$APP"
xcrun stapler validate "$APP"
spctl --assess --type execute --verbose=4 "$APP"
rm "$ZIP"
ditto -c -k --sequesterRsrc --keepParent "$APP" "$ZIP"
shasum -a 256 "$DMG" "$ZIP" | sed 's#dist/##' > dist/SHA256SUMS.txt
hdiutil verify "$DMG"
unzip -t "$ZIP" >/dev/null
- name: Validate Developer ID package
if: ${{ github.event_name != 'workflow_dispatch' || !inputs.notarize }}
run: |
APP="dist/CodexTokenLedger.app"
ZIP="dist/CodexTokenLedger-menu-bar-macOS.zip"
DMG="dist/CodexTokenLedger-menu-bar-macOS.dmg"
codesign --verify --deep --strict --verbose=2 "$APP"
codesign --verify --verbose=2 "$DMG"
hdiutil verify "$DMG"
unzip -t "$ZIP" >/dev/null
shasum -a 256 "$DMG" "$ZIP" | sed 's#dist/##' > dist/SHA256SUMS.txt
- name: Prepare release notes
env:
TAG: ${{ inputs.tag || github.ref_name }}
UNNOTARIZED: ${{ github.event_name != 'workflow_dispatch' || !inputs.notarize }}
run: |
SOURCE="release-notes/${TAG#v}.md"
OUTPUT="$RUNNER_TEMP/release-notes.md"
test -s "$SOURCE"
if [[ "$UNNOTARIZED" == "true" ]]; then
head -n 1 "$SOURCE" > "$OUTPUT"
cat >> "$OUTPUT" <<'EOF'
> 此安装包已使用 Developer ID 签名,尚未通过 Apple 公证。首次打开时 macOS 可能要求确认。
EOF
tail -n +2 "$SOURCE" >> "$OUTPUT"
else
cp "$SOURCE" "$OUTPUT"
fi
echo "RELEASE_NOTES_PATH=$OUTPUT" >> "$GITHUB_ENV"
- name: Generate update feed
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
TAG: ${{ inputs.tag || github.ref_name }}
run: |
APPCAST_DIR="$RUNNER_TEMP/appcast"
ZIP="dist/CodexTokenLedger-menu-bar-macOS.zip"
NOTES="$RELEASE_NOTES_PATH"
mkdir -p "$APPCAST_DIR"
test -s "$NOTES"
cp "$ZIP" "$APPCAST_DIR/"
cp "$NOTES" "$APPCAST_DIR/CodexTokenLedger-menu-bar-macOS.md"
GENERATE_APPCAST="$(
find build/DerivedData/SourcePackages/artifacts/sparkle \
-type f -name generate_appcast -perm -111 -print -quit
)"
if [[ -z "$GENERATE_APPCAST" ]]; then
echo "Sparkle generate_appcast was not found." >&2
exit 2
fi
printf '%s' "$SPARKLE_PRIVATE_KEY" | "$GENERATE_APPCAST" \
--ed-key-file - \
--download-url-prefix "https://github.com/Lincb522/CodexTokenLedger/releases/download/$TAG/" \
--embed-release-notes \
--maximum-versions 3 \
--maximum-deltas 0 \
-o "$GITHUB_WORKSPACE/dist/appcast.xml" \
"$APPCAST_DIR"
test -s dist/appcast.xml
- name: Publish release assets
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag || github.ref_name }}
run: |
NOTES="$RELEASE_NOTES_PATH"
test -s "$NOTES"
if gh release view "$TAG" >/dev/null 2>&1; then
gh release edit "$TAG" --notes-file "$NOTES"
gh release upload "$TAG" \
dist/CodexTokenLedger-menu-bar-macOS.dmg \
dist/CodexTokenLedger-menu-bar-macOS.zip \
dist/appcast.xml \
dist/SHA256SUMS.txt \
--clobber
else
gh release create "$TAG" \
dist/CodexTokenLedger-menu-bar-macOS.dmg \
dist/CodexTokenLedger-menu-bar-macOS.zip \
dist/appcast.xml \
dist/SHA256SUMS.txt \
--verify-tag \
--notes-file "$NOTES" \
--title "Codex Token Ledger ${TAG#v}"
fi