-
Notifications
You must be signed in to change notification settings - Fork 0
363 lines (322 loc) · 14.9 KB
/
create-release.yml
File metadata and controls
363 lines (322 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# Generates release notes and creates release PR
# - Takes inputs for the release branch, previous and new version, and Android and iOS SDK versions
# - Generates release notes based on the merged PRs since the last release
# - Creates release PR w/ expected release notes
# NOTE: For calls from Native SDKS, do not pass `android_from`, `android_to`, `ios_from`, or `ios_to`
# Use `target_branch` and `release_branch` instead.
name: Create SDK Release
on:
workflow_call:
inputs:
release_branch:
required: true
type: string
description: Name of the release branch
target_branch:
required: false
type: string
description: The branch to target for the release PR (e.g. 5.4.0-main)
default: main
target_repo:
required: false
type: string
description: FOR WRAPPERS ONLY. The repository to target for the release PR
android_from:
required: false
type: string
description: |
DON'T USE FOR CALLS FROM NATIVE SDKS.
Android SDK version being updated from.
android_to:
required: false
type: string
description: |
DON'T USE FOR CALLS FROM NATIVE SDKS.
Android SDK version being updated to
ios_from:
required: false
type: string
description: |
DON'T USE FOR CALLS FROM NATIVE SDKS.
iOS SDK version being updated from.
ios_to:
required: false
type: string
description: |
DON'T USE FOR CALLS FROM NATIVE SDKS.
iOS SDK version being updated to.
secrets:
GH_PUSH_TOKEN:
required: false
description: GitHub token with push permissions
jobs:
create_release:
runs-on: ubuntu-latest
env:
TARGET_REPO: ${{ inputs.target_repo || github.repository }}
GH_TOKEN: ${{ secrets.GH_PUSH_TOKEN || github.token }}
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.target_branch }}
fetch-depth: 0
repository: ${{ env.TARGET_REPO }}
token: ${{ env.GH_TOKEN }}
- name: Extract version_to from release_branch
id: release_version
run: |
# Extract version from release_branch format: rel/version_to
VERSION_TO="${{ inputs.release_branch }}"
VERSION_TO="${VERSION_TO#rel/}"
echo "version_to=$VERSION_TO" >> $GITHUB_OUTPUT
- name: Get last release commit
id: last_commit
run: |
# Get the last tag from the target branch (already checked out)
CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null) || {
# Fallback to first commit if no tags
git rev-list --max-parents=0 HEAD
}
LAST_RELEASE_DATE=$(git show -s --format=%cI "$CURRENT_VERSION")
echo "date=$LAST_RELEASE_DATE" >> $GITHUB_OUTPUT
- name: Get merged PRs
id: get_prs
uses: actions/github-script@v8
with:
script: |
const lastReleaseDate = '${{ steps.last_commit.outputs.date }}';
const releaseBranch = '${{ inputs.release_branch }}';
const targetBranch = '${{ inputs.target_branch }}';
const version = '${{ steps.release_version.outputs.version_to }}';
const targetRepo = process.env.TARGET_REPO;
// Parse target repo to get owner and repo name
const [owner, repo] = targetRepo.split('/');
// Get the creation date of the release branch (when it diverged from main)
const { data: branchInfo } = await github.rest.repos.getBranch({
owner,
repo,
branch: releaseBranch
});
const branchCreatedAt = branchInfo.commit.commit.committer.date;
// Get PRs merged to target branch since last release
const { data: prs } = await github.rest.pulls.list({
owner,
repo,
state: 'closed',
base: targetBranch,
per_page: 100
});
const mergedPrs = prs
.filter(pr =>
pr.merged_at &&
new Date(pr.merged_at) > new Date(lastReleaseDate)
)
.map(pr => ({
number: pr.number,
title: pr.title,
}));
core.setOutput('prs', JSON.stringify(mergedPrs));
- name: Generate release notes
id: release_notes
uses: actions/github-script@v8
env:
PR_DATA: ${{ steps.get_prs.outputs.prs }}
with:
script: |
const prs = JSON.parse(process.env.PR_DATA).map(pr => ({
...pr,
title: pr.title.trim()
}));
// Categorize PRs (exclude internal changes like ci/chore)
const features = prs.filter(pr => /^feat/i.test(pr.title));
const fixes = prs.filter(pr => /^fix/i.test(pr.title));
const improvements = prs.filter(pr => /^(perf|refactor)/i.test(pr.title));
const breaking = prs.filter(pr => /^breaking/i.test(pr.title) || /!\s*:/i.test(pr.title));
// Helper function to build section
const buildSection = (title, prs) => {
if (prs.length === 0) return '';
let section = `### ${title}\n\n`;
prs.forEach(pr => {
section += `- ${pr.title} (#${pr.number})\n`;
});
return section + '\n';
};
// Parse version strings handling semantic versioning with prerelease tags
const parseVersion = (v) => {
// Handle versions like "5.1.36", "5.1.36-alpha", "5.1.36-beta", "5.1.36-alpha.1", etc.
const parts = v.split('-');
const baseVersion = parts[0].split('.').map(Number);
const prerelease = parts[1] || '';
return { baseVersion, prerelease };
};
// Compare two versions considering base version and prerelease tags
const compareVersions = (v1, v2) => {
const ver1 = parseVersion(v1);
const ver2 = parseVersion(v2);
// Compare base versions first
const base1 = ver1.baseVersion;
const base2 = ver2.baseVersion;
for (let i = 0; i < Math.max(base1.length, base2.length); i++) {
const a = base1[i] || 0;
const b = base2[i] || 0;
if (a !== b) return a - b;
}
// Base versions are equal, compare prerelease
// No prerelease (stable) > any prerelease
if (!ver1.prerelease && !ver2.prerelease) return 0;
if (!ver1.prerelease) return 1; // v1 is stable, v2 is prerelease
if (!ver2.prerelease) return -1; // v2 is stable, v1 is prerelease
// Both have prerelease, compare them lexicographically
return ver1.prerelease.localeCompare(ver2.prerelease);
};
// Helper function to fetch and format native SDK release notes e.g.
// Update Android SDK from 5.1.33 to 5.1.34
// fix: ... ([OneSignal-Android-SDK#some-number](https://github.com/OneSignal/OneSignal-Android-SDK/pull/some-number))
const fetchNativeSDKReleaseNotes = async (fromVersion, toVersion, owner, repo) => {
try {
// Fetch all releases
const { data: allReleases } = await github.rest.repos.listReleases({
owner,
repo,
per_page: 100
});
// Filter releases between fromVersion and toVersion
const relevantReleases = allReleases
.filter(release => {
const tagVersion = release.tag_name;
const cmp1 = compareVersions(tagVersion, fromVersion);
const cmp2 = compareVersions(tagVersion, toVersion);
// Include releases > from and <= to
return cmp1 > 0 && cmp2 <= 0;
})
.sort((a, b) => compareVersions(b.tag_name, a.tag_name)); // Sort descending by version
// Extract bullet points from each release
let notes = '';
relevantReleases.forEach(release => {
const body = release.body || '';
// Extract bullet points (lines starting with - or *)
const bullets = body
.split('\n')
.filter(line => /^\s*[-*]\s/.test(line))
.map(line => line.replace(/^\s*[-*]\s/, '').trim())
.map(line => {
// Convert PR references (#123) to full GitHub links
return line.replace(/\(#(\d+)\)/g, `([${repo}#$1](https://github.com/${owner}/${repo}/pull/$1))`);
})
.filter(line => line.length > 0);
if (bullets.length > 0) {
bullets.forEach(bullet => {
notes += ` - ${bullet}\n`;
});
}
});
return notes;
} catch (error) {
core.warning(`Failed to fetch ${repo} release notes: ${error.message}`);
return '';
}
};
// Determine release type from version
const version = '${{ steps.release_version.outputs.version_to }}';
const releaseType = version.includes('-alpha') ? 'Alpha' :
version.includes('-beta') ? 'Beta' : 'Current';
let releaseNotes = `Channels: ${releaseType}\n\n`;
releaseNotes += buildSection('⚠️ Breaking Changes', breaking);
releaseNotes += buildSection('🚀 New Features', features);
releaseNotes += buildSection('🐛 Bug Fixes', fixes);
releaseNotes += buildSection('✨ Improvements', improvements);
// Check for native dependency changes
const androidFrom = '${{ inputs.android_from }}';
const androidTo = '${{ inputs.android_to }}';
const iosFrom = '${{ inputs.ios_from }}';
const iosTo = '${{ inputs.ios_to }}';
const hasAndroidUpdate = androidFrom && androidTo;
const hasIosUpdate = iosFrom && iosTo;
if (hasAndroidUpdate || hasIosUpdate) {
releaseNotes += '\n### 🛠️ Native Dependency Updates\n\n';
if (hasAndroidUpdate) {
releaseNotes += `- Update Android SDK from ${androidFrom} to ${androidTo}\n`;
releaseNotes += await fetchNativeSDKReleaseNotes(androidFrom, androidTo, 'OneSignal', 'OneSignal-Android-SDK');
}
if (hasIosUpdate) {
releaseNotes += `- Update iOS SDK from ${iosFrom} to ${iosTo}\n`;
releaseNotes += await fetchNativeSDKReleaseNotes(iosFrom, iosTo, 'OneSignal', 'OneSignal-iOS-SDK');
}
releaseNotes += '\n';
}
core.setOutput('notes', releaseNotes);
- name: Check for existing PR
id: check_pr
run: |
git checkout ${{ inputs.release_branch }}
EXISTING_PR=$(gh pr list --head "${{ inputs.release_branch }}" --base "${{ inputs.target_branch }}" --json number,body --jq '.[0]')
if [ -n "$EXISTING_PR" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "number=$(echo "$EXISTING_PR" | jq -r '.number')" >> $GITHUB_OUTPUT
# Write existing body to file to preserve it
echo "$EXISTING_PR" | jq -r '.body' > existing_body.md
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Merge release notes with existing PR
id: merged_notes
if: steps.check_pr.outputs.exists == 'true'
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const existingBody = fs.readFileSync('existing_body.md', 'utf8');
const newNotes = `${{ steps.release_notes.outputs.notes }}`;
// Regex to extract a single SDK update entry (main bullet + sub-bullets)
// Matches: "- Update {SDK} from X to Y\n" followed by any " - " sub-bullets
const extractSDKSection = (text, sdk) => {
const regex = new RegExp(`- Update ${sdk} SDK from [^\\n]+\\n(?: - [^\\n]+\\n)*`, 'g');
const match = text.match(regex);
return match ? match[0] : null;
};
// Extract Android and iOS sections from both existing and new
const existingAndroid = extractSDKSection(existingBody, 'Android');
const existingIos = extractSDKSection(existingBody, 'iOS');
const newAndroid = extractSDKSection(newNotes, 'Android');
const newIos = extractSDKSection(newNotes, 'iOS');
// Determine what to include (prefer new over existing for same platform)
const finalAndroid = newAndroid || existingAndroid;
const finalIos = newIos || existingIos;
// Build combined native deps section
let combinedDeps = '';
if (finalAndroid) combinedDeps += finalAndroid;
if (finalIos) combinedDeps += finalIos;
let mergedNotes = newNotes;
if (combinedDeps) {
// Check if new notes already have native deps section
const nativeDepHeaderRegex = /\n*### 🛠️ Native Dependency Updates\n\n[\s\S]*$/;
if (nativeDepHeaderRegex.test(newNotes)) {
// Replace existing section in new notes
mergedNotes = newNotes.replace(nativeDepHeaderRegex, `\n\n### 🛠️ Native Dependency Updates\n\n${combinedDeps}`);
} else {
// Append section to new notes
mergedNotes = newNotes.trimEnd() + `\n\n### 🛠️ Native Dependency Updates\n\n${combinedDeps}`;
}
}
fs.writeFileSync('release_notes.md', mergedNotes);
core.setOutput('notes', mergedNotes);
- name: Create or update release PR
run: |
NEW_VERSION="${{ steps.release_version.outputs.version_to }}"
TARGET_BRANCH="${{ inputs.target_branch }}"
if [ "${{ steps.check_pr.outputs.exists }}" == "true" ]; then
# Update existing PR body using API to avoid permission issues with gh pr edit
gh api \
--method PATCH \
"/repos/$TARGET_REPO/pulls/${{ steps.check_pr.outputs.number }}" \
-F body=@release_notes.md
else
# Write release notes to file to avoid shell interpretation
cat > release_notes.md << 'EOF'
${{ steps.release_notes.outputs.notes }}
EOF
gh pr create \
--title "chore: Release $NEW_VERSION" \
--body-file release_notes.md \
--base "$TARGET_BRANCH"
fi