mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
* ci: auto-submit iOS and macOS App Store builds for review The iOS and Mac App Store workflows previously stopped after uploading the build to App Store Connect via altool, leaving version creation, "What's New" and submission as manual steps. Add fastlane lanes (ios/mac release) that upload the prebuilt .ipa / MAS .pkg using App Store Connect API key auth (reusing the existing notarization key secrets), push release notes derived from build/release-notes.md, wait for processing, submit for review and flag automatic release on approval. Final version tags submit for review; pre-release tags (RC/beta/alpha) and manual runs only upload the build. Listing metadata and screenshots remain curated by hand in App Store Connect. https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ * ci: wire iOS and Mac Store workflows to fastlane submit lane The previous commit added the fastlane lanes but the workflow edits were not applied. Replace the altool validate/upload steps in the iOS and Mac App Store workflows with the fastlane submit lane: install fastlane, generate the App Store "What's New" notes and run `fastlane <platform> release` with App Store Connect API key auth. Also extend the Mac workflow's harden-runner egress allowlist with the rubygems and App Store Connect endpoints used by fastlane. https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ * fix(ci): harden Apple App Store auto-submission after review Address review findings on the iOS/macOS App Store automation: - Tag gating: submit only when the tag has no "-" (final semver), instead of denylisting RC/beta/alpha. GitHub Actions contains() is case-sensitive and the repo's RC tags are mostly lowercase (-rc.N), so the old guard would have auto-submitted release candidates to production review. - Fastfile: set skip_metadata so deliver no longer reads back and re-uploads curated listing fields; push only "What's New" via an inline release_notes hash. Warn against verbose mode (can dump the API key). - Gemfile.lock: add arm64-darwin/x86_64-darwin platforms so bundle install works on the macOS runners. - Workflows: install deps via pinned ruby/setup-ruby (bundler cache), and resolve the artifact path with a strict nullglob check (exactly one match) instead of ls | head. - release-notes script: tighten emphasis regexes so stray * / _ (globs, snake_case) survive, anchor footer patterns so legitimate "download" lines are not dropped, and drop a duplicate mkdir. - Docs: document the hyphen-based gate, single-use build numbers, automatic release behavior and inline validation. https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ * fix(ci): correct deliver metadata + setup-ruby version (second review pass) Two bugs introduced by the previous review-fix commit, both confirmed against upstream source: - Fastfile: skip_metadata: true makes deliver's upload_metadata return early (verified in fastlane 2.225.0 deliver/lib/deliver/upload_metadata.rb), so the "What's New" notes were never uploaded. Revert to metadata_path pointing at a dir that contains only <locale>/release_notes.txt; load_from_filesystem reads only that file (next unless File.exist?) with no remote read-back, so other listing fields stay untouched. Removed the now-unused inline release_notes helper. - Workflows: ruby/setup-ruby throws when ruby-version is unset and no .ruby-version file exists (it does not fall back to system Ruby). Pin ruby-version: '3.3' in both workflows. Docs updated to match the corrected metadata approach. https://claude.ai/code/session_014c1W1mX7tfvFzpZ6wyWzsJ * fix(ci): remove invalid wait_for_uploaded_build from deliver lanes wait_for_uploaded_build is a pilot/upload_to_testflight option, not a deliver one. Passing it to upload_to_app_store makes fastlane raise on the unknown key and fail both iOS and macOS release lanes on every run. deliver already waits for the build to finish processing during submit (select_build -> wait_for_build_processing_to_be_complete), so no replacement is needed. Also pin the ruby/setup-ruby comment to its resolved version (v1.310.0), and slice the App Store release notes by code point so a multi-byte character is never split at the 4000-char cap. --------- Co-authored-by: Claude <noreply@anthropic.com>
245 lines
10 KiB
YAML
245 lines
10 KiB
YAML
name: iOS App Store Release on Release
|
|
on:
|
|
push:
|
|
tags:
|
|
- v*
|
|
workflow_dispatch:
|
|
inputs: {}
|
|
|
|
jobs:
|
|
ios-app-store-release:
|
|
runs-on: macos-26
|
|
env:
|
|
UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY }}
|
|
UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }}
|
|
|
|
steps:
|
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
with:
|
|
node-version: 22
|
|
- name: Check out Git repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Select Xcode 26.2
|
|
run: sudo xcode-select -s /Applications/Xcode_26.2.app
|
|
|
|
- name: Verify Xcode and SDK version
|
|
run: |
|
|
echo "=== Xcode Version ==="
|
|
xcodebuild -version
|
|
echo ""
|
|
echo "=== SDK Information ==="
|
|
xcrun --show-sdk-version
|
|
xcodebuild -showsdks | grep -E "(iOS|iphoneos)"
|
|
|
|
# work around for npm installs from git+https://github.com/johannesjo/J2M.git
|
|
- name: Reconfigure git to use HTTP authentication
|
|
run: >
|
|
git config --global url."https://github.com/".insteadOf
|
|
ssh://git@github.com/
|
|
|
|
- name: Install Apple WWDR intermediate certificate
|
|
run: |
|
|
curl -fsSL https://www.apple.com/certificateauthority/AppleWWDRCAG4.cer -o /tmp/AppleWWDRCAG4.cer
|
|
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/AppleWWDRCAG4.cer
|
|
|
|
- name: Configure signing keychain
|
|
env:
|
|
# Reuse mac_certs - "Apple Distribution" certificate works for both macOS and iOS
|
|
MAC_CERTS: ${{ secrets.mac_certs }}
|
|
MAC_CERTS_PASSWORD: ${{ secrets.mac_certs_password }}
|
|
run: |
|
|
CERT_PATH="$RUNNER_TEMP/dist-cert.p12"
|
|
KEYCHAIN_NAME="build.keychain"
|
|
KEYCHAIN_PATH="$HOME/Library/Keychains/${KEYCHAIN_NAME}-db"
|
|
echo "$MAC_CERTS" | base64 --decode > "$CERT_PATH"
|
|
echo "=== DIAGNOSTIC: Decoded .p12 SHA256 ==="
|
|
shasum -a 256 "$CERT_PATH"
|
|
echo "========================================"
|
|
security create-keychain -p "" "$KEYCHAIN_NAME"
|
|
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
|
security unlock-keychain -p "" "$KEYCHAIN_PATH"
|
|
curl -fsSL https://www.apple.com/certificateauthority/AppleWWDRCAG4.cer -o "$RUNNER_TEMP/AppleWWDRCAG4.cer"
|
|
security import "$RUNNER_TEMP/AppleWWDRCAG4.cer" -k "$KEYCHAIN_PATH" -T /usr/bin/codesign
|
|
security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$MAC_CERTS_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
|
|
security list-keychains -s "$KEYCHAIN_PATH" "$HOME/Library/Keychains/login.keychain-db"
|
|
security default-keychain -s "$KEYCHAIN_PATH"
|
|
security set-key-partition-list -S apple-tool:,apple: -k "" "$KEYCHAIN_PATH"
|
|
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
|
|
|
- name: List available signing identities
|
|
run: security find-identity -v -p codesigning
|
|
|
|
- name: Install provisioning profile
|
|
env:
|
|
IOS_PROVISION_PROFILE: ${{ secrets.IOS_PROVISION_PROFILE }}
|
|
run: |
|
|
PROFILE_PATH="$RUNNER_TEMP/ios-provisioning.mobileprovision"
|
|
echo "$IOS_PROVISION_PROFILE" | base64 --decode > "$PROFILE_PATH"
|
|
# Get the UUID from the provisioning profile
|
|
PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print UUID" /dev/stdin <<< $(security cms -D -i "$PROFILE_PATH"))
|
|
echo "Profile UUID: $PROFILE_UUID"
|
|
# Create the provisioning profiles directory if it doesn't exist
|
|
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
|
|
# Copy the profile with its UUID as the filename
|
|
cp "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/${PROFILE_UUID}.mobileprovision"
|
|
echo "PROVISIONING_PROFILE_UUID=$PROFILE_UUID" >> "$GITHUB_ENV"
|
|
|
|
- name: Verify provisioning profile
|
|
run: |
|
|
security cms -D -i "$HOME/Library/MobileDevice/Provisioning Profiles/${PROVISIONING_PROFILE_UUID}.mobileprovision" > /tmp/profile.plist
|
|
echo "=== PROVISIONING PROFILE INFO ==="
|
|
echo "Name: $(/usr/libexec/PlistBuddy -c "Print Name" /tmp/profile.plist)"
|
|
echo "Team: $(/usr/libexec/PlistBuddy -c "Print TeamIdentifier:0" /tmp/profile.plist)"
|
|
PROFILE_APP_ID=$(/usr/libexec/PlistBuddy -c "Print Entitlements:application-identifier" /tmp/profile.plist | sed 's/^[A-Z0-9]*\.//')
|
|
echo "App ID: $PROFILE_APP_ID"
|
|
echo "=================================="
|
|
# Verify bundle ID matches
|
|
EXPECTED_BUNDLE_ID="com.super-productivity.app"
|
|
if [ "$PROFILE_APP_ID" != "$EXPECTED_BUNDLE_ID" ]; then
|
|
echo "ERROR: Provisioning profile app ID ($PROFILE_APP_ID) does not match expected bundle ID ($EXPECTED_BUNDLE_ID)"
|
|
echo "Please create a new provisioning profile with the correct bundle ID"
|
|
exit 1
|
|
fi
|
|
echo "Bundle ID verification passed!"
|
|
|
|
- name: Get npm cache directory
|
|
id: npm-cache-dir
|
|
run: |
|
|
echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
|
|
|
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
|
id: npm-cache
|
|
with:
|
|
path: ${{ steps.npm-cache-dir.outputs.dir }}
|
|
key: ${{ runner.os }}-node22-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-node22-
|
|
|
|
- name: Install npm Packages
|
|
run: npm i
|
|
|
|
- name: Generate environment
|
|
run: npm run env
|
|
|
|
- name: Set iOS version from package.json
|
|
run: |
|
|
FULL_VERSION=$(node -p "require('./package.json').version")
|
|
# Strip pre-release suffix (e.g., "17.0.0-RC.3" -> "17.0.0")
|
|
# App Store requires X.Y.Z format; build number differentiates uploads
|
|
VERSION=$(echo "$FULL_VERSION" | sed 's/-.*//')
|
|
BUILD_NUMBER=$(date +%Y%m%d%H%M)
|
|
echo "Setting iOS version to $VERSION (build $BUILD_NUMBER) [from $FULL_VERSION]"
|
|
|
|
cd ios/App
|
|
if ! xcrun agvtool new-marketing-version "$VERSION"; then
|
|
echo "Error: Failed to set marketing version to $VERSION"
|
|
exit 1
|
|
fi
|
|
if ! xcrun agvtool new-version -all "$BUILD_NUMBER"; then
|
|
echo "Error: Failed to set build number to $BUILD_NUMBER"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Build Angular frontend
|
|
run: npm run buildFrontend:prodWeb
|
|
|
|
- name: Sync Capacitor iOS
|
|
run: npm run sync:ios
|
|
|
|
- name: Install CocoaPods
|
|
run: |
|
|
cd ios/App
|
|
pod install
|
|
|
|
- name: Archive iOS app
|
|
run: |
|
|
cd ios/App
|
|
# Archive without code signing - signing is handled during export
|
|
# This avoids conflicts with CocoaPods frameworks
|
|
xcodebuild archive \
|
|
-workspace App.xcworkspace \
|
|
-scheme App \
|
|
-configuration Release \
|
|
-archivePath "$RUNNER_TEMP/App.xcarchive" \
|
|
-destination 'generic/platform=iOS' \
|
|
CODE_SIGNING_REQUIRED=NO \
|
|
CODE_SIGNING_ALLOWED=NO
|
|
|
|
- name: Create ExportOptions.plist
|
|
run: |
|
|
cat > "$RUNNER_TEMP/ExportOptions.plist" << EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>method</key>
|
|
<string>app-store-connect</string>
|
|
<key>teamID</key>
|
|
<string>${{ secrets.APPLE_TEAM_ID }}</string>
|
|
<key>uploadSymbols</key>
|
|
<true/>
|
|
<key>signingStyle</key>
|
|
<string>manual</string>
|
|
<key>provisioningProfiles</key>
|
|
<dict>
|
|
<key>com.super-productivity.app</key>
|
|
<string>${{ env.PROVISIONING_PROFILE_UUID }}</string>
|
|
</dict>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
|
|
- name: Export IPA
|
|
run: |
|
|
xcodebuild -exportArchive \
|
|
-archivePath "$RUNNER_TEMP/App.xcarchive" \
|
|
-exportPath "$RUNNER_TEMP/ipa-output" \
|
|
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist"
|
|
|
|
- name: List exported files
|
|
run: ls -la "$RUNNER_TEMP/ipa-output"
|
|
|
|
- name: Upload IPA artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: sup-ios-release
|
|
path: ${{ runner.temp }}/ipa-output/*.ipa
|
|
|
|
- name: Set up Ruby and fastlane
|
|
# Pinned by SHA per repo policy. ruby-version is required: with no
|
|
# .ruby-version file in the repo the action errors out rather than
|
|
# using system Ruby. bundler-cache runs `bundle install` and caches the
|
|
# ~210-gem fastlane tree so it isn't reinstalled each run.
|
|
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
|
|
with:
|
|
ruby-version: '3.3'
|
|
bundler-cache: true
|
|
|
|
- name: Prepare App Store release notes
|
|
run: node tools/prepare-appstore-release-notes.js
|
|
|
|
# Upload the IPA to App Store Connect and (for final release tags) submit
|
|
# it for review with automatic release on approval. Pre-release tags (any
|
|
# tag containing "-", e.g. -rc.0/-beta.1) and manual runs only upload the
|
|
# build (TestFlight). The "-" check is case-insensitive by construction,
|
|
# unlike contains(...,'RC'); the repo's RC tags are mostly lowercase.
|
|
- name: Upload to App Store Connect and submit for review
|
|
env:
|
|
ASC_KEY_ID: ${{ secrets.mac_api_key_id }}
|
|
ASC_ISSUER_ID: ${{ secrets.mac_api_key_issuer_id }}
|
|
ASC_KEY_CONTENT: ${{ secrets.mac_api_key }}
|
|
SUBMIT_FOR_REVIEW: ${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }}
|
|
run: |
|
|
set -euo pipefail
|
|
shopt -s nullglob
|
|
ipas=("$RUNNER_TEMP"/ipa-output/*.ipa)
|
|
if [ ${#ipas[@]} -ne 1 ]; then
|
|
echo "Expected exactly 1 .ipa, found ${#ipas[@]}: ${ipas[*]:-none}"
|
|
exit 1
|
|
fi
|
|
export IPA_PATH="${ipas[0]}"
|
|
echo "Submitting $IPA_PATH (submit_for_review=$SUBMIT_FOR_REVIEW)"
|
|
bundle exec fastlane ios release
|