Skip to content

Commit 3d330cb

Browse files
authored
ci: temporary workflow to resolve Electron release versions for PRs (#586)
* ci: temporary workflow to resolve Electron release versions for PRs * chore: addressing code review comments
1 parent baf58ad commit 3d330cb

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
name: Resolve PR Release Versions
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
max_release_count:
7+
description: Max number (<= 100) of unprocessed releases to process (all if left blank)
8+
required: false
9+
type: number
10+
schedule:
11+
- cron: '0 */6 * * *'
12+
13+
permissions:
14+
actions: read
15+
16+
jobs:
17+
resolve_pr_release_versions:
18+
name: Resolve PR Release Versions
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Restore previous run data
22+
uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2
23+
with:
24+
name: resolved-pr-versions
25+
if_no_artifact_found: ignore
26+
workflow_conclusion: 'completed'
27+
search_artifacts: true
28+
- run: npm install @electron/fiddle-core
29+
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
30+
with:
31+
script: |
32+
const fs = require('node:fs/promises');
33+
const { ElectronVersions } = require('@electron/fiddle-core');
34+
const semver = require('semver');
35+
36+
// https://github.com/electron/trop/blob/a481299dfd522c7b5c5d10e2355ad9e7f0ce193e/src/utils/branch-util.ts#L90-L94
37+
const getBackportPattern = () =>
38+
/(?:^|\n)(?:manual |manually )?backport (?:of )?(?:#(\d+)|https:\/\/github.com\/.*\/pull\/(\d+))/gim;
39+
40+
const BOOTSTRAP_DATA_URL = 'https://gist.githubusercontent.com/dsanders11/eb51a04d04a6a3e0710d88db5250e698/raw/fd960b6dea1152b55427407646044f1ba187e52b/data.json';
41+
const MIN_MAJOR = 10;
42+
const RELEASE_MAX_PAGINATION_COUNT = 100;
43+
const NEW_RELEASES_QUERY = `query($endCursor: String, $count: Int!) {
44+
rateLimit {
45+
limit
46+
remaining
47+
used
48+
resetAt
49+
}
50+
repository(owner: "electron", name: "electron") {
51+
releases: refs(
52+
refPrefix: "refs/tags/",
53+
after: $endCursor,
54+
first: $count,
55+
orderBy: {field: TAG_COMMIT_DATE, direction: ASC}
56+
) {
57+
pageInfo {
58+
endCursor
59+
hasNextPage
60+
}
61+
nodes {
62+
name
63+
}
64+
}
65+
}
66+
}`;
67+
const RELEASE_PRS_QUERY = `query($releaseHeadRef: String!, $previousRelease: String!, $endCursor: String) {
68+
rateLimit {
69+
limit
70+
remaining
71+
used
72+
resetAt
73+
}
74+
repository(owner: "electron", name: "electron") {
75+
release: ref(qualifiedName: $previousRelease) {
76+
compare(headRef: $releaseHeadRef) {
77+
commits(after: $endCursor, last: 100) {
78+
pageInfo {
79+
endCursor
80+
hasNextPage
81+
}
82+
nodes {
83+
url
84+
author {
85+
user {
86+
login
87+
}
88+
}
89+
associatedPullRequests(first: 20) {
90+
pageInfo {
91+
hasNextPage
92+
}
93+
nodes {
94+
labels(first: 20) {
95+
pageInfo {
96+
hasNextPage
97+
}
98+
nodes {
99+
name
100+
}
101+
}
102+
number
103+
bodyText
104+
state
105+
}
106+
}
107+
}
108+
}
109+
}
110+
}
111+
}
112+
}`;
113+
114+
const maxReleaseCount = ${{ inputs.max_release_count || 0 }};
115+
116+
if (maxReleaseCount > 100) {
117+
core.error('max_release_count must be <= 100');
118+
return;
119+
}
120+
121+
const filename = 'data.json';
122+
let data = { endCursor: undefined, data: {} };
123+
124+
try {
125+
data = JSON.parse(await fs.readFile(filename));
126+
} catch (err) {
127+
if (err.code !== 'ENOENT') {
128+
throw err;
129+
} else {
130+
core.debug('Previous data not found, bootstrapping');
131+
const resp = await fetch(BOOTSTRAP_DATA_URL);
132+
data = await resp.json();
133+
}
134+
}
135+
136+
const { versions } = await ElectronVersions.create(undefined, { ignoreCache: true });
137+
138+
try {
139+
while (true) {
140+
const { rateLimit: rateLimitA, repository: { releases } } = await github.graphql(NEW_RELEASES_QUERY, { endCursor: data.endCursor, count: maxReleaseCount === 0 ? RELEASE_MAX_PAGINATION_COUNT : maxReleaseCount });
141+
core.debug(rateLimitA);
142+
143+
if (releases.nodes.length === 0) {
144+
core.notice('No new releases to process');
145+
break;
146+
}
147+
148+
for (const { name: tagName } of releases.nodes) {
149+
const parsedVersion = semver.parse(tagName);
150+
151+
if (parsedVersion === null) {
152+
core.error(`Could not parse version from ${tagName} - skipping`);
153+
continue;
154+
} else if (parsedVersion.major < MIN_MAJOR) {
155+
core.debug(`Skipping release ${tagName} as it's before major ${MIN_MAJOR}`);
156+
continue;
157+
}
158+
159+
let idx = versions.findIndex(({ version }) => `v${version}` === tagName);
160+
if (idx === -1) {
161+
core.warning(`Could not find release ${tagName} - skipping`);
162+
continue;
163+
} else if (idx === 0) {
164+
core.error(`No previous release for ${tagName} - skipping`);
165+
continue;
166+
}
167+
let previousRelease = versions[--idx];
168+
169+
let endCursor = undefined;
170+
while (true) {
171+
const { rateLimit: rateLimitB, repository: { release } } = await github.graphql(RELEASE_PRS_QUERY, { endCursor, releaseHeadRef: `tags/${tagName}`, previousRelease: `refs/tags/v${previousRelease}` });
172+
core.debug(rateLimitB);
173+
174+
if (release === null) {
175+
// There are occasionally missing releases which made it into index.json, so
176+
// move on to the next previous release until we're back to a valid release
177+
core.warning(`${previousRelease} is a missing release - skipping`);
178+
previousRelease = versions[--idx];
179+
continue;
180+
}
181+
182+
const { compare: { commits } } = release;
183+
184+
for (const commit of commits.nodes) {
185+
if (commit.associatedPullRequests.pageInfo.hasNextPage) {
186+
core.error(`Commit (${commit.url}) had more than expected max associated PRs - skipping`);
187+
continue;
188+
}
189+
const prs = commit.associatedPullRequests.nodes.filter(node => node.state === 'MERGED');
190+
if (prs.length !== 1) {
191+
if (!['electron-bot', 'sudowoodo-release-bot[bot]'].includes(commit.author?.user?.login)) {
192+
core.warning(`Could not determine PR associated with ${commit.url} - skipping`);
193+
} else {
194+
core.debug(`${commit.author?.user?.login} commit, ${commit.url} - skipping`);
195+
}
196+
continue;
197+
}
198+
const pr = prs[0];
199+
if (pr.labels.pageInfo.hasNextPage) {
200+
core.error(`PR #${pr.number} had more than expected max labels - skipping`);
201+
continue;
202+
}
203+
204+
//
205+
// We finally have a valid PR to process
206+
//
207+
208+
// If it's a backport, include the version number in the root PR's backport list
209+
const backportPattern = getBackportPattern();
210+
const match = backportPattern.exec(pr.bodyText);
211+
if (match) {
212+
const rootPr = match[1] ? parseInt(match[1], 10) : parseInt(match[2], 10);
213+
214+
data.data[rootPr] = data.data[rootPr] ?? { release: null, backports: [] };
215+
if (!data.data[rootPr].backports.includes(tagName)) {
216+
data.data[rootPr].backports.push(tagName);
217+
}
218+
} else {
219+
data.data[pr.number] = data.data[pr.number] ?? { release: null, backports: [] };
220+
if (data.data[pr.number].release !== null && data.data[pr.number].release !== tagName) {
221+
core.error(`PR #${pr.number} already has a different release version than expected (found ${data.data[pr.number].release} but expected ${tagName})`);
222+
continue;
223+
}
224+
data.data[pr.number].release = tagName;
225+
}
226+
}
227+
228+
if (!commits.pageInfo.hasNextPage) {
229+
break;
230+
} else {
231+
endCursor = commits.pageInfo.endCursor;
232+
}
233+
}
234+
}
235+
236+
// Only update this after all releases have been processed,
237+
// and make sure it's not null which would happen if there
238+
// were no new releases to process during the run
239+
if (releases.pageInfo.endCursor !== null) {
240+
data.endCursor = releases.pageInfo.endCursor;
241+
}
242+
243+
if (releases.pageInfo.hasNextPage && maxReleaseCount === 0) {
244+
continue;
245+
} else {
246+
break;
247+
}
248+
}
249+
} catch (error) {
250+
if (error instanceof Error && error.stack) core.debug(error.stack);
251+
core.setFailed(`Error while processing new releases: ${error}`);
252+
}
253+
254+
// Write to file to upload as artifact
255+
await fs.writeFile(filename, JSON.stringify(data));
256+
- name: Persist data
257+
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
258+
if: ${{ !cancelled() }}
259+
with:
260+
name: resolved-pr-versions
261+
path: data.json

0 commit comments

Comments
 (0)