Skip to content

Commit a523ad9

Browse files
authored
Merge pull request #440 from AppsFlyerSDK/releases/6.x.x/6.17.x/6.17.9-rc1
Release 6.17.9
2 parents 02bfa04 + ad2c18f commit a523ad9

16 files changed

Lines changed: 365 additions & 178 deletions

File tree

.github/workflows/ci.yml

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,26 @@ jobs:
164164
working-directory: example
165165
run: flutter pub get
166166

167-
# Step 7: Build Android APK (debug mode)
167+
# Step 7: Create dummy .env file for CI (not committed to repo)
168+
- name: 🔑 Create dummy .env for CI build
169+
working-directory: example
170+
run: |
171+
echo "DEV_KEY=dummy_dev_key" > .env
172+
echo "APP_ID=dummy_app_id" >> .env
173+
174+
# Step 8: Build Android APK (debug mode)
168175
# This validates that the plugin integrates correctly with Android
169176
- name: 🔨 Build Android APK (debug)
170177
working-directory: example
171178
run: flutter build apk --debug
172179

173-
# Step 8: Build Android App Bundle (release mode, no signing)
180+
# Step 9: Build Android App Bundle (release mode, no signing)
174181
# App Bundle is the preferred format for Play Store
175182
- name: 🔨 Build Android App Bundle (release)
176183
working-directory: example
177184
run: flutter build appbundle --release
178-
179-
# Step 9: Upload build artifacts (optional)
185+
186+
# Step 10: Upload build artifacts (optional)
180187
# Useful for manual testing or archiving
181188
- name: 📤 Upload APK artifact
182189
if: success()
@@ -239,19 +246,26 @@ jobs:
239246
working-directory: example/ios
240247
run: pod install
241248

242-
# Step 8: Build for iOS Simulator (fastest iOS build)
249+
# Step 8: Create dummy .env file for CI (not committed to repo)
250+
- name: 🔑 Create dummy .env for CI build
251+
working-directory: example
252+
run: |
253+
echo "DEV_KEY=dummy_dev_key" > .env
254+
echo "APP_ID=dummy_app_id" >> .env
255+
256+
# Step 9: Build for iOS Simulator (fastest iOS build)
243257
# Validates that the plugin compiles for iOS
244258
- name: 🔨 Build iOS for Simulator
245259
working-directory: example
246260
run: flutter build ios --simulator --debug
247261

248-
# Step 9: Build iOS IPA without code signing (release mode)
262+
# Step 10: Build iOS IPA without code signing (release mode)
249263
# This validates a full release build without requiring certificates
250264
- name: 🔨 Build iOS IPA (no codesign)
251265
working-directory: example
252266
run: flutter build ipa --release --no-codesign
253-
254-
# Step 10: Upload build artifacts (optional)
267+
268+
# Step 11: Upload build artifacts (optional)
255269
- name: 📤 Upload iOS build artifact
256270
if: success()
257271
uses: actions/upload-artifact@v4

.github/workflows/production-release.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,24 @@ jobs:
220220
- name: 🔍 Validate package
221221
run: |
222222
echo "Running pub publish dry-run to validate package..."
223+
# Run dry-run and capture exit code
224+
# Exit code 65 = warnings only (acceptable, e.g., gitignored files)
225+
# Exit code 0 = success
226+
# Other exit codes = real errors
227+
set +e
223228
flutter pub publish --dry-run
229+
EXIT_CODE=$?
230+
set -e
231+
232+
if [ $EXIT_CODE -eq 0 ]; then
233+
echo "✅ Package validation passed with no warnings"
234+
elif [ $EXIT_CODE -eq 65 ]; then
235+
echo "⚠️ Package validation passed with warnings (acceptable)"
236+
echo "Warnings don't prevent publishing"
237+
else
238+
echo "❌ Package validation failed with exit code $EXIT_CODE"
239+
exit $EXIT_CODE
240+
fi
224241
225242
- name: 📝 Check pub.dev credentials
226243
run: |
Lines changed: 194 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,214 @@
1-
name: Promote Release - Merge on QA Pass and Publish
1+
# =============================================================================
2+
# Promote Release - Prepare Release Branch for Production
3+
# =============================================================================
4+
#
5+
# Purpose: When QA approves an RC, this workflow prepares the release branch
6+
# for production by removing the -rc suffix from version numbers.
7+
#
8+
# IMPORTANT: This workflow does NOT merge the PR (org rules prevent bot merges).
9+
# Instead, it updates the release branch so when a human merges, the version
10+
# is clean (e.g., 6.17.8 instead of 6.17.8-rc1).
11+
#
12+
# Flow:
13+
# 1. QA tests the RC version
14+
# 2. QA adds label "pass QA ready for deploy" to the PR
15+
# 3. This workflow triggers and:
16+
# - Updates the release branch to remove -rcN suffix
17+
# - Updates all version files
18+
# - Commits changes to the release branch
19+
# 4. Human reviews and manually merges the PR
20+
# 5. production-release.yml triggers on merge
21+
#
22+
# =============================================================================
23+
24+
name: Promote Release - Prepare for Production
225

326
on:
427
pull_request:
5-
types: [labeled, synchronize, reopened, ready_for_review]
6-
branches:
7-
- master
8-
pull_request_review:
9-
types: [submitted]
28+
types: [labeled]
1029
branches:
1130
- master
1231

1332
concurrency:
14-
group: promote-release-${{ github.event.pull_request.number || github.run_id }}
33+
group: promote-release-${{ github.event.pull_request.number }}
1534
cancel-in-progress: true
1635

1736
jobs:
18-
gate-and-merge:
19-
name: 🔐 Gate, Verify Checks, and Merge
20-
if: >-
21-
${ { github.event.pull_request.head.ref } } == '' || startsWith(github.event.pull_request.head.ref, 'releases/')
37+
# ===========================================================================
38+
# Job 1: Prepare Release Branch for Production
39+
# ===========================================================================
40+
prepare-for-production:
41+
name: 🚀 Prepare Release for Production
42+
# Only run when the specific label is added AND it's from a releases/ branch
43+
if: |
44+
github.event.label.name == 'pass QA ready for deploy' &&
45+
startsWith(github.event.pull_request.head.ref, 'releases/')
2246
runs-on: ubuntu-latest
23-
permissions:
24-
contents: write
25-
pull-requests: write
26-
checks: read
27-
statuses: read
47+
2848
outputs:
29-
merged: ${{ steps.merge.outputs.merged }}
30-
version: ${{ steps.version.outputs.version }}
49+
version: ${{ steps.compute-version.outputs.version }}
50+
release_branch: ${{ steps.compute-version.outputs.release_branch }}
51+
3152
steps:
32-
- name: 🧠 Evaluate conditions
33-
id: eval
34-
uses: actions/github-script@v7
53+
- name: 📥 Checkout release branch
54+
uses: actions/checkout@v4
3555
with:
36-
script: |
37-
const core = require('@actions/core');
38-
const pr = context.payload.pull_request || (await github.rest.pulls.get({owner: context.repo.owner, repo: context.repo.repo, pull_number: context.payload.pull_request?.number || context.issue.number})).data;
39-
if (!pr) core.setFailed('No PR context');
40-
const hasLabel = pr.labels.some(l => l.name === 'pass QA ready for deploy');
41-
if (!hasLabel) core.setFailed('Required label not present: pass QA ready for deploy');
42-
// Check approvals
43-
const reviews = await github.rest.pulls.listReviews({owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number});
44-
const approved = reviews.data.some(r => r.state === 'APPROVED');
45-
if (!approved) core.setFailed('No approval found on the PR');
46-
core.setOutput('pr_number', pr.number.toString());
47-
- name: ⏳ Wait for required status checks to pass
56+
ref: ${{ github.event.pull_request.head.ref }}
57+
fetch-depth: 0
58+
token: ${{ secrets.GITHUB_TOKEN }}
59+
60+
- name: 🔍 Compute production version
61+
id: compute-version
62+
run: |
63+
RELEASE_BRANCH="${{ github.event.pull_request.head.ref }}"
64+
echo "Release branch: $RELEASE_BRANCH"
65+
66+
# Get current version from pubspec.yaml
67+
CURRENT_VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //' | tr -d ' ')
68+
echo "Current version: $CURRENT_VERSION"
69+
70+
# Remove -rcN suffix to get production version
71+
PROD_VERSION=$(echo "$CURRENT_VERSION" | sed 's/-rc[0-9]*$//')
72+
echo "Production version: $PROD_VERSION"
73+
74+
# Validate it's different (was an RC version)
75+
if [[ "$CURRENT_VERSION" == "$PROD_VERSION" ]]; then
76+
echo "⚠️ Version doesn't have -rc suffix. Already production ready?"
77+
echo "Current: $CURRENT_VERSION"
78+
fi
79+
80+
echo "version=$PROD_VERSION" >> $GITHUB_OUTPUT
81+
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
82+
echo "release_branch=$RELEASE_BRANCH" >> $GITHUB_OUTPUT
83+
84+
- name: 📝 Update pubspec.yaml to production version
85+
run: |
86+
VERSION='${{ steps.compute-version.outputs.version }}'
87+
echo "Updating pubspec.yaml to production version: $VERSION"
88+
sed -i "s/^version: .*/version: $VERSION/" pubspec.yaml
89+
grep "^version:" pubspec.yaml
90+
91+
- name: 📝 Update plugin version constants (Android/iOS/Dart)
92+
run: |
93+
VERSION='${{ steps.compute-version.outputs.version }}'
94+
echo "Updating PLUGIN_VERSION constants to: $VERSION"
95+
96+
# Android - AppsFlyerConstants.java
97+
ANDROID_FILE="android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java"
98+
if [ -f "$ANDROID_FILE" ]; then
99+
sed -i "s/PLUGIN_VERSION = \".*\"/PLUGIN_VERSION = \"$VERSION\"/" "$ANDROID_FILE"
100+
echo "✅ Android:" && grep "PLUGIN_VERSION" "$ANDROID_FILE"
101+
fi
102+
103+
# Dart - appsflyer_constants.dart
104+
DART_FILE="lib/src/appsflyer_constants.dart"
105+
if [ -f "$DART_FILE" ]; then
106+
sed -i "s/PLUGIN_VERSION = \".*\"/PLUGIN_VERSION = \"$VERSION\"/" "$DART_FILE"
107+
echo "✅ Dart:" && grep "PLUGIN_VERSION" "$DART_FILE"
108+
fi
109+
110+
# iOS - AppsflyerSdkPlugin.h (#define)
111+
IOS_FILE="ios/Classes/AppsflyerSdkPlugin.h"
112+
if [ -f "$IOS_FILE" ]; then
113+
sed -i 's/kAppsFlyerPluginVersion[[:space:]]*@"[^"]*"/kAppsFlyerPluginVersion @"'"$VERSION"'"/' "$IOS_FILE"
114+
echo "✅ iOS:" && grep "kAppsFlyerPluginVersion" "$IOS_FILE"
115+
fi
116+
117+
- name: 💾 Commit and push version changes
118+
run: |
119+
VERSION='${{ steps.compute-version.outputs.version }}'
120+
CURRENT='${{ steps.compute-version.outputs.current_version }}'
121+
122+
git config user.email "github-actions[bot]@users.noreply.github.com"
123+
git config user.name "github-actions[bot]"
124+
125+
if [[ -n $(git status -s) ]]; then
126+
git add pubspec.yaml android/ ios/
127+
git commit -m "chore: prepare production release $VERSION (from $CURRENT)"
128+
git push
129+
echo "✅ Pushed version update to release branch"
130+
else
131+
echo "ℹ️ No version changes needed"
132+
fi
133+
134+
- name: 📝 Update PR description
48135
uses: actions/github-script@v7
49136
with:
50137
script: |
51-
const prNumber = Number(core.getInput('pr_number', { required: false })) || ${{ steps.eval.outputs.pr_number || '0' }};
52-
const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber });
53-
const ref = pr.head.sha;
54-
const start = Date.now();
55-
const timeoutMs = 60*60*1000; // 60 minutes
56-
const sleep = ms => new Promise(r => setTimeout(r, ms));
57-
while (true) {
58-
const { data: combined } = await github.rest.repos.getCombinedStatusForRef({ owner: context.repo.owner, repo: context.repo.repo, ref });
59-
const checksOk = combined.state === 'success';
60-
if (checksOk) break;
61-
if (Date.now() - start > timeoutMs) throw new Error('Timeout waiting for status checks to pass');
62-
core.info(`Waiting for checks. Current state: ${combined.state}`);
63-
await sleep(15000);
64-
}
65-
- name: 📥 Checkout
66-
uses: actions/checkout@v4
67-
with:
68-
fetch-depth: 0
69-
- name: 🔀 Merge PR immediately
70-
id: merge
138+
const version = '${{ steps.compute-version.outputs.version }}';
139+
const currentVersion = '${{ steps.compute-version.outputs.current_version }}';
140+
const pr = context.payload.pull_request;
141+
142+
const newBody = `### Production Release ${version}
143+
144+
**Status:** ✅ Ready for manual merge
145+
146+
**Version updated:** ${currentVersion} → ${version}
147+
148+
---
149+
150+
${pr.body || ''}
151+
152+
---
153+
154+
**Next steps:**
155+
1. ✅ QA approved (label added)
156+
2. ✅ Version updated to production (${version})
157+
3. ⏳ **Awaiting manual merge** by a maintainer
158+
4. ⏳ Production release will trigger automatically after merge
159+
`;
160+
161+
await github.rest.pulls.update({
162+
owner: context.repo.owner,
163+
repo: context.repo.repo,
164+
pull_number: pr.number,
165+
body: newBody
166+
});
167+
168+
- name: 📢 Add comment to PR
71169
uses: actions/github-script@v7
72170
with:
73171
script: |
74-
const prNumber = Number(${ { steps.eval.outputs.pr_number } });
75-
const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber });
76-
if (pr.merged) { core.setOutput('merged', 'true'); return; }
77-
const method = 'merge'; // use repo default merge method
78-
await github.rest.pulls.merge({ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, merge_method: method });
79-
core.setOutput('merged', 'true');
80-
- name: 📝 Read version from pubspec on master
81-
id: version
82-
run: |
83-
git fetch origin master:master
84-
git checkout master
85-
VER=$(grep '^version:' pubspec.yaml | sed 's/version: //' | tr -d ' ')
86-
echo "version=$VER" >> $GITHUB_OUTPUT
87-
88-
call-production:
89-
name: 🚀 Production Release
90-
needs: gate-and-merge
91-
if: needs.gate-and-merge.outputs.merged == 'true'
92-
uses: ./.github/workflows/production-release.yml
93-
with:
94-
version: ${{ needs.gate-and-merge.outputs.version }}
95-
skip_tests: false
96-
dry_run: false
97-
secrets: inherit
172+
const version = '${{ steps.compute-version.outputs.version }}';
173+
174+
await github.rest.issues.createComment({
175+
owner: context.repo.owner,
176+
repo: context.repo.repo,
177+
issue_number: context.payload.pull_request.number,
178+
body: `## 🚀 Ready for Production Release
179+
180+
The release branch has been updated:
181+
- **Version:** \`${version}\` (removed -rc suffix)
182+
- **All version files updated**
183+
184+
### Next Steps
185+
1. **Review the changes** in this PR
186+
2. **Merge this PR** when ready
187+
3. The production release workflow will automatically:
188+
- Publish \`${version}\` to pub.dev
189+
- Create GitHub release
190+
- Send notifications
191+
192+
> ⚠️ **Note:** This PR requires manual merge due to branch protection rules.`
193+
});
194+
195+
# ===========================================================================
196+
# Job 2: Notify Team
197+
# ===========================================================================
198+
notify-ready:
199+
name: 📢 Notify Ready for Merge
200+
needs: prepare-for-production
201+
runs-on: ubuntu-latest
202+
if: always() && needs.prepare-for-production.result == 'success'
203+
204+
steps:
205+
- name: 📨 Send Slack notification
206+
uses: slackapi/slack-github-action@v1
207+
with:
208+
payload: |
209+
{
210+
"text": "<!here>\n:white_check_mark: *Flutter Plugin Ready for Production*\n\nVersion: ${{ needs.prepare-for-production.outputs.version }}\nPR: ${{ github.event.pull_request.html_url }}\n\n*Action Required:* A maintainer needs to manually merge the PR to trigger the production release."
211+
}
212+
env:
213+
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
214+
continue-on-error: true # Will gracefully fail if webhook not configured

0 commit comments

Comments
 (0)