Skip to content

Commit 664e67e

Browse files
authored
Merge pull request #33 from v2er-app/feature/ios-release-pipeline
feat: iOS Release Pipeline with Fastlane Match
2 parents 0190998 + 9efc626 commit 664e67e

File tree

5 files changed

+332
-175
lines changed

5 files changed

+332
-175
lines changed

.github/workflows/release.yml

Lines changed: 133 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
name: Release to App Store
1+
name: iOS Release Pipeline
22

33
on:
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

2421
jobs:
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 }}"

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,15 @@ xcuserdata/
55
*.ipa
66
*.dSYM.zip
77
*.dSYM
8+
9+
## Fastlane
10+
fastlane/report.xml
11+
fastlane/Preview.html
12+
fastlane/screenshots/**/*.png
13+
fastlane/test_output
14+
fastlane/.env*
15+
!fastlane/.env.example
16+
17+
## Match
18+
fastlane/certificates/
19+
fastlane/profiles/

fastlane/Appfile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Appfile - App specific configuration
2+
3+
# App Bundle ID
4+
app_identifier("v2er.app")
5+
6+
# Apple ID (optional if using API key)
7+
# apple_id("your@email.com")
8+
9+
# Team ID from Apple Developer Portal
10+
team_id(ENV["TEAM_ID"])
11+
12+
# iTunes Connect Team ID (if different from Developer Portal team)
13+
itc_team_id(ENV["ITC_TEAM_ID"] || ENV["TEAM_ID"])
14+
15+
# You can set different app identifiers per lane
16+
for_lane :beta do
17+
app_identifier("v2er.app")
18+
end
19+
20+
for_lane :release do
21+
app_identifier("v2er.app")
22+
end

0 commit comments

Comments
 (0)