Skip to content

Commit ed44a6a

Browse files
committed
fix: redesign promote-release workflow for org constraints
- Remove auto-merge (not allowed by org rules) - Instead, update release branch to remove -rc suffix BEFORE merge - Human manually merges after version is cleaned up - Production release triggers on PR merge as before
1 parent c42df62 commit ed44a6a

File tree

1 file changed

+188
-76
lines changed

1 file changed

+188
-76
lines changed
Lines changed: 188 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,209 @@
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)
92+
run: |
93+
VERSION='${{ steps.compute-version.outputs.version }}'
94+
FILE="android/src/main/java/com/appsflyer/appsflyersdk/AppsFlyerConstants.java"
95+
if [ -f "$FILE" ]; then
96+
sed -i "s/kPluginVersion = \".*\"/kPluginVersion = \"$VERSION\"/" "$FILE"
97+
echo "Updated Android constants:"
98+
grep "kPluginVersion" "$FILE"
99+
fi
100+
101+
- name: 📝 Update plugin version constants (iOS)
102+
run: |
103+
VERSION='${{ steps.compute-version.outputs.version }}'
104+
FILE="ios/Classes/AppsflyerSdkPlugin.m"
105+
if [ -f "$FILE" ]; then
106+
sed -i "s/kPluginVersion = @\".*\"/kPluginVersion = @\"$VERSION\"/" "$FILE"
107+
echo "Updated iOS constants:"
108+
grep "kPluginVersion" "$FILE"
109+
fi
110+
111+
- name: 💾 Commit and push version changes
112+
run: |
113+
VERSION='${{ steps.compute-version.outputs.version }}'
114+
CURRENT='${{ steps.compute-version.outputs.current_version }}'
115+
116+
git config user.email "github-actions[bot]@users.noreply.github.com"
117+
git config user.name "github-actions[bot]"
118+
119+
if [[ -n $(git status -s) ]]; then
120+
git add pubspec.yaml android/ ios/
121+
git commit -m "chore: prepare production release $VERSION (from $CURRENT)"
122+
git push
123+
echo "✅ Pushed version update to release branch"
124+
else
125+
echo "ℹ️ No version changes needed"
126+
fi
127+
128+
- name: 📝 Update PR description
48129
uses: actions/github-script@v7
49130
with:
50131
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
132+
const version = '${{ steps.compute-version.outputs.version }}';
133+
const currentVersion = '${{ steps.compute-version.outputs.current_version }}';
134+
const pr = context.payload.pull_request;
135+
136+
const newBody = `### Production Release ${version}
137+
138+
**Status:** ✅ Ready for manual merge
139+
140+
**Version updated:** ${currentVersion} → ${version}
141+
142+
---
143+
144+
${pr.body || ''}
145+
146+
---
147+
148+
**Next steps:**
149+
1. ✅ QA approved (label added)
150+
2. ✅ Version updated to production (${version})
151+
3. ⏳ **Awaiting manual merge** by a maintainer
152+
4. ⏳ Production release will trigger automatically after merge
153+
`;
154+
155+
await github.rest.pulls.update({
156+
owner: context.repo.owner,
157+
repo: context.repo.repo,
158+
pull_number: pr.number,
159+
body: newBody
160+
});
161+
162+
- name: 📢 Add comment to PR
71163
uses: actions/github-script@v7
72164
with:
73165
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
166+
const version = '${{ steps.compute-version.outputs.version }}';
167+
168+
await github.rest.issues.createComment({
169+
owner: context.repo.owner,
170+
repo: context.repo.repo,
171+
issue_number: context.payload.pull_request.number,
172+
body: `## 🚀 Ready for Production Release
87173
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
174+
The release branch has been updated:
175+
- **Version:** \`${version}\` (removed -rc suffix)
176+
- **All version files updated**
177+
178+
### Next Steps
179+
1. **Review the changes** in this PR
180+
2. **Merge this PR** when ready
181+
3. The production release workflow will automatically:
182+
- Publish \`${version}\` to pub.dev
183+
- Create GitHub release
184+
- Send notifications
185+
186+
> ⚠️ **Note:** This PR requires manual merge due to branch protection rules.`
187+
});
188+
189+
# ===========================================================================
190+
# Job 2: Notify Team
191+
# ===========================================================================
192+
notify-ready:
193+
name: 📢 Notify Ready for Merge
194+
needs: prepare-for-production
195+
runs-on: ubuntu-latest
196+
if: always() && needs.prepare-for-production.result == 'success'
197+
198+
steps:
199+
- name: 📨 Send Slack notification
200+
if: ${{ secrets.CI_SLACK_WEBHOOK_URL != '' }}
201+
uses: slackapi/slack-github-action@v1
202+
with:
203+
payload: |
204+
{
205+
"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."
206+
}
207+
env:
208+
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
209+
continue-on-error: true

0 commit comments

Comments
 (0)