1- name : Release to App Store
1+ name : iOS Release Pipeline
22
33on :
4+ push :
5+ branches :
6+ - main
7+ paths :
8+ - ' V2er/Info.plist'
9+ - ' V2er.xcodeproj/project.pbxproj'
410 workflow_dispatch :
511 inputs :
6- release_type :
7- description : ' Release type'
8- required : true
9- default : ' patch'
10- type : choice
11- options :
12- - patch
13- - minor
14- - major
15- testflight_only :
16- description : ' TestFlight only (no App Store release)'
12+ force_release :
13+ description : ' Force release even if version unchanged'
1714 required : false
1815 default : false
1916 type : boolean
@@ -22,197 +19,158 @@ env:
2219 DEVELOPER_DIR : /Applications/Xcode.app/Contents/Developer
2320
2421jobs :
25- release :
26- name : Build and Release
27- runs-on : macos-latest
28-
22+ version-check :
23+ name : Check Version and Create Tag
24+ runs-on : ubuntu-latest
25+ outputs :
26+ should_release : ${{ steps.check.outputs.should_release }}
27+ new_tag : ${{ steps.check.outputs.new_tag }}
28+ version : ${{ steps.check.outputs.version }}
29+ build : ${{ steps.check.outputs.build }}
30+
2931 steps :
3032 - name : Checkout repository
33+ uses : actions/checkout@v4
34+ with :
35+ fetch-depth : 0
36+ token : ${{ secrets.GITHUB_TOKEN }}
37+
38+ - name : Check version and create tag if needed
39+ id : check
40+ run : |
41+ # Get current version from Info.plist using plutil for robust XML parsing
42+ CURRENT_VERSION=$(/usr/bin/plutil -extract CFBundleShortVersionString xml1 -o - V2er/Info.plist | grep '<string>' | sed 's/.*<string>\(.*\)<\/string>.*/\1/' | xargs)
43+ CURRENT_BUILD=$(/usr/bin/plutil -extract CFBundleVersion xml1 -o - V2er/Info.plist | grep '<string>' | sed 's/.*<string>\(.*\)<\/string>.*/\1/' | xargs)
44+
45+ echo "Current version: $CURRENT_VERSION (build $CURRENT_BUILD)"
46+
47+ # Check if tag already exists
48+ TAG_NAME="v$CURRENT_VERSION"
49+
50+ if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
51+ if [[ "${{ github.event.inputs.force_release }}" == "true" ]]; then
52+ echo "Tag $TAG_NAME exists but force_release is true"
53+ # Delete existing tag for force release
54+ git push origin --delete "$TAG_NAME" 2>/dev/null || true
55+ echo "should_release=true" >> $GITHUB_OUTPUT
56+ else
57+ echo "Tag $TAG_NAME already exists, skipping release"
58+ echo "should_release=false" >> $GITHUB_OUTPUT
59+ fi
60+ else
61+ echo "Tag $TAG_NAME does not exist, will create it"
62+ echo "should_release=true" >> $GITHUB_OUTPUT
63+ fi
64+
65+ echo "new_tag=$TAG_NAME" >> $GITHUB_OUTPUT
66+ echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
67+ echo "build=$CURRENT_BUILD" >> $GITHUB_OUTPUT
68+
69+ - name : Create and push tag
70+ if : steps.check.outputs.should_release == 'true'
71+ run : |
72+ git config --local user.email "action@github.com"
73+ git config --local user.name "GitHub Action"
74+
75+ TAG_NAME="${{ steps.check.outputs.new_tag }}"
76+ VERSION="${{ steps.check.outputs.version }}"
77+ BUILD="${{ steps.check.outputs.build }}"
78+
79+ # Create annotated tag
80+ git tag -a "$TAG_NAME" -m "Release version $VERSION (build $BUILD)"
81+
82+ # Push tag
83+ git push origin "$TAG_NAME"
84+
85+ echo "✅ Successfully created tag: $TAG_NAME"
86+
87+ build-and-release :
88+ name : Build and Release to TestFlight
89+ needs : version-check
90+ if : needs.version-check.outputs.should_release == 'true'
91+ runs-on : macos-latest
92+
93+ steps :
94+ - name : Checkout repository at tag
3195 uses : actions/checkout@v4
3296 with :
3397 submodules : recursive
3498 fetch-depth : 0
35-
99+ ref : ${{ needs.version-check.outputs.new_tag }}
100+
36101 - name : Select Xcode version
37102 run : sudo xcode-select -s /Applications/Xcode_16.0.app/Contents/Developer
38-
103+
39104 - name : Setup Ruby
40105 uses : ruby/setup-ruby@v1
41106 with :
42107 ruby-version : ' 3.0'
43- bundler-cache : true
44-
108+ bundler-cache : false
109+
45110 - name : Install Fastlane
46111 run : |
47112 gem install fastlane
48113 gem install xcpretty
49-
50- - name : Configure Git
51- run : |
52- git config --local user.email "action@github.com"
53- git config --local user.name "GitHub Action"
54-
55- - name : Import certificates
56- env :
57- CERTIFICATES_P12 : ${{ secrets.CERTIFICATES_P12 }}
58- CERTIFICATES_PASSWORD : ${{ secrets.CERTIFICATES_PASSWORD }}
59- KEYCHAIN_PASSWORD : ${{ secrets.KEYCHAIN_PASSWORD }}
60- run : |
61- # Create variables
62- CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
63- KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
64-
65- # Import certificate from secrets
66- echo -n "$CERTIFICATES_P12" | base64 --decode -o $CERTIFICATE_PATH
67-
68- # Create temporary keychain
69- security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
70- security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
71- security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
72-
73- # Import certificate to keychain
74- security import $CERTIFICATE_PATH -P "$CERTIFICATES_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
75- security list-keychain -d user -s $KEYCHAIN_PATH
76-
77- - name : Download provisioning profiles
114+
115+ - name : Setup SSH for Match repository
116+ uses : webfactory/ssh-agent@v0.8.0
117+ with :
118+ ssh-private-key : ${{ secrets.DEPLOY_KEY }}
119+
120+ - name : Create App Store Connect API Key
78121 env :
79- PROVISIONING_PROFILE_BASE64 : ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
80- run : |
81- # Create the provisioning profiles directory
82- mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
83-
84- # Decode and save provisioning profile
85- echo -n "$PROVISIONING_PROFILE_BASE64" | base64 --decode -o ~/Library/MobileDevice/Provisioning\ Profiles/V2er.mobileprovision
86-
87- - name : Bump version
88- id : version
122+ APP_STORE_CONNECT_KEY_ID : ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
123+ APP_STORE_CONNECT_ISSUER_ID : ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
124+ APP_STORE_CONNECT_API_KEY_BASE64 : ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
89125 run : |
90- # Get current version
91- CURRENT_VERSION=$(xcodebuild -project V2er.xcodeproj -showBuildSettings | grep MARKETING_VERSION | tr -d 'MARKETING_VERSION = ')
92- echo "Current version: $CURRENT_VERSION"
93-
94- # Calculate new version based on input
95- IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION"
96- MAJOR=${VERSION_PARTS[0]}
97- MINOR=${VERSION_PARTS[1]}
98- PATCH=${VERSION_PARTS[2]}
99-
100- case "${{ github.event.inputs.release_type }}" in
101- major)
102- NEW_VERSION="$((MAJOR + 1)).0.0"
103- ;;
104- minor)
105- NEW_VERSION="$MAJOR.$((MINOR + 1)).0"
106- ;;
107- patch)
108- NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
109- ;;
110- esac
111-
112- echo "New version: $NEW_VERSION"
113- echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
114-
115- # Update version in project
116- xcrun agvtool new-marketing-version $NEW_VERSION
117-
118- # Get and increment build number
119- BUILD_NUMBER=$(xcodebuild -project V2er.xcodeproj -showBuildSettings | grep CURRENT_PROJECT_VERSION | tr -d 'CURRENT_PROJECT_VERSION = ')
120- NEW_BUILD_NUMBER=$((BUILD_NUMBER + 1))
121- xcrun agvtool new-version -all $NEW_BUILD_NUMBER
122-
123- - name : Archive app
126+ mkdir -p ~/.appstoreconnect/private_keys
127+ echo "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8
128+ chmod 600 ~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8
129+
130+ # Set environment variables for Fastlane
131+ echo "APP_STORE_CONNECT_API_KEY_KEY_ID=$APP_STORE_CONNECT_KEY_ID" >> $GITHUB_ENV
132+ echo "APP_STORE_CONNECT_API_KEY_ISSUER_ID=$APP_STORE_CONNECT_ISSUER_ID" >> $GITHUB_ENV
133+ echo "APP_STORE_CONNECT_API_KEY_KEY=~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8" >> $GITHUB_ENV
134+
135+ - name : Run Fastlane Match
124136 env :
137+ MATCH_PASSWORD : ${{ secrets.MATCH_PASSWORD }}
138+ MATCH_GIT_URL : ${{ secrets.MATCH_GIT_URL }}
125139 TEAM_ID : ${{ secrets.TEAM_ID }}
126140 run : |
127- xcodebuild archive \
128- -project V2er.xcodeproj \
129- -scheme V2er \
130- -sdk iphoneos \
131- -configuration Release \
132- -archivePath $PWD/build/V2er.xcarchive \
133- DEVELOPMENT_TEAM=$TEAM_ID \
134- CODE_SIGN_STYLE=Manual \
135- CODE_SIGN_IDENTITY="iPhone Distribution" \
136- PROVISIONING_PROFILE_SPECIFIER="V2er AppStore" | xcpretty
137-
138- - name : Export IPA
141+ fastlane match appstore --readonly
142+
143+ - name : Build and Upload to TestFlight
139144 env :
145+ MATCH_PASSWORD : ${{ secrets.MATCH_PASSWORD }}
146+ MATCH_GIT_URL : ${{ secrets.MATCH_GIT_URL }}
140147 TEAM_ID : ${{ secrets.TEAM_ID }}
141148 run : |
142- # Create export options plist
143- cat > ExportOptions.plist <<EOF
144- <?xml version="1.0" encoding="UTF-8"?>
145- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
146- <plist version="1.0">
147- <dict>
148- <key>method</key>
149- <string>app-store</string>
150- <key>teamID</key>
151- <string>$TEAM_ID</string>
152- <key>uploadSymbols</key>
153- <true/>
154- <key>compileBitcode</key>
155- <false/>
156- <key>provisioningProfiles</key>
157- <dict>
158- <key>com.v2er.app</key>
159- <string>V2er AppStore</string>
160- </dict>
161- </dict>
162- </plist>
163- EOF
164-
165- xcodebuild -exportArchive \
166- -archivePath $PWD/build/V2er.xcarchive \
167- -exportOptionsPlist ExportOptions.plist \
168- -exportPath $PWD/build \
169- -allowProvisioningUpdates | xcpretty
170-
171- - name : Upload to TestFlight
172- env :
173- APP_STORE_CONNECT_API_KEY_ID : ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
174- APP_STORE_CONNECT_API_KEY_ISSUER_ID : ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
175- APP_STORE_CONNECT_API_KEY : ${{ secrets.APP_STORE_CONNECT_API_KEY }}
176- run : |
177- # Create API key file
178- mkdir -p ~/.appstoreconnect/private_keys
179- echo -n "$APP_STORE_CONNECT_API_KEY" > ~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8
180-
181- xcrun altool --upload-app \
182- --type ios \
183- --file build/V2er.ipa \
184- --apiKey $APP_STORE_CONNECT_API_KEY_ID \
185- --apiIssuer $APP_STORE_CONNECT_API_KEY_ISSUER_ID
186-
187- - name : Create release tag
188- run : |
189- git add -A
190- git commit -m "Release version ${{ steps.version.outputs.version }}"
191- git tag -a "v${{ steps.version.outputs.version }}" -m "Release version ${{ steps.version.outputs.version }}"
192- git push origin main --tags
193-
149+ fastlane beta
150+
194151 - name : Create GitHub Release
195152 uses : softprops/action-gh-release@v1
196153 with :
197- tag_name : v ${{ steps .version.outputs.version }}
198- name : Release ${{ steps .version.outputs.version }}
154+ tag_name : ${{ needs .version-check .outputs.new_tag }}
155+ name : Release ${{ needs .version-check .outputs.version }}
199156 body : |
200- ## What's New
201-
202- This release includes bug fixes and performance improvements.
203-
204- ### Changes
205- - Version bump to ${{ steps.version.outputs.version }}
206-
157+ ## 🚀 Version ${{ needs.version-check.outputs.version }}
158+ Build: ${{ needs.version-check.outputs.build }}
159+
207160 ### TestFlight
208- This version has been submitted to TestFlight for testing.
209-
210- ${{ github.event.inputs.testflight_only == 'true' && '### Note\nThis is a TestFlight-only release.' || '### App Store\nThis version will be submitted to the App Store after TestFlight testing.' }}
161+ This version has been automatically submitted to TestFlight for beta testing.
162+
163+ ### What's New
164+ - See [commit history](https://github.com/${{ github.repository }}/commits/${{ needs.version-check.outputs.new_tag }}) for changes
165+
166+ ---
167+ *This release was automatically created by GitHub Actions*
211168 draft : false
212- prerelease : ${{ github.event.inputs.testflight_only == 'true' }}
213-
214- - name : Clean up
215- if : always ()
169+ prerelease : false
170+
171+ - name : Post release notification
172+ if : success ()
216173 run : |
217- security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
218- rm -f ~/Library/MobileDevice/Provisioning\ Profiles/V2er.mobileprovision || true
174+ echo "✅ Successfully released version ${{ needs.version-check.outputs.version }} to TestFlight!"
175+ echo "🏷️ Tag: ${{ needs.version-check.outputs.new_tag }}"
176+ echo "🔢 Build: ${{ needs.version-check.outputs.build }}"
0 commit comments