-
Notifications
You must be signed in to change notification settings - Fork 9
feat: add target/N-x-y labels to main branch Chromium roll PRs #207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c7607a2
4cc3cc0
5545283
5700933
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { Octokit } from '@octokit/rest'; | ||
|
|
||
| import { REPOS } from '../constants.js'; | ||
| import { ReposListBranchesResponseItem } from '../types.js'; | ||
| import { getSupportedBranches } from './get-supported-branches.js'; | ||
|
|
||
| export const ELECTRON_RELEASE_SCHEDULE_URL = 'https://releases.electronjs.org/schedule.json'; | ||
|
|
||
| interface ReleaseScheduleEntry { | ||
| version: string; | ||
| branch: string; | ||
| chromiumVersion: number; | ||
| } | ||
|
|
||
| // Returns the names of the supported release branches whose scheduled Chromium | ||
| // version is greater than or equal to the given Chromium major, per | ||
| // https://releases.electronjs.org/schedule - i.e. the branches whose Chromium | ||
| // upgrade is tracked by rolls to the main branch and should receive them as | ||
| // backports (via `target/N-x-y` labels) rather than independent rolls. | ||
| export async function getBranchesTrackedByMain( | ||
| octokit: Octokit, | ||
| chromiumMajorVersion: number, | ||
| ): Promise<string[]> { | ||
| const response = await fetch(ELECTRON_RELEASE_SCHEDULE_URL, { | ||
| headers: { accept: 'application/json' }, | ||
| }); | ||
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch Electron release schedule: ${response.status}`); | ||
| } | ||
| const schedule = (await response.json()) as ReleaseScheduleEntry[]; | ||
|
|
||
| const branches: ReposListBranchesResponseItem[] = await octokit.paginate( | ||
| octokit.repos.listBranches.endpoint.merge({ | ||
| ...REPOS.electron, | ||
| protected: true, | ||
| }), | ||
| ); | ||
|
Comment on lines
+32
to
+37
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should move away from this pattern , as it is quite slow (as in 20+ seconds, see electron/sudowoodo#419). There was a time historically when I'll open a follow up PR for speeding this up, as the changes in this PR add another pagination to the main code paths so we're going to be losing nearly a minute of wall time just paginating branches from |
||
| const supported = getSupportedBranches(branches); | ||
|
|
||
| return schedule | ||
| .filter( | ||
| (entry) => | ||
| supported.includes(entry.branch) && Number(entry.chromiumVersion) >= chromiumMajorVersion, | ||
| ) | ||
| .map((entry) => entry.branch) | ||
| .sort(); | ||
| } | ||
|
claude[bot] marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -17,6 +17,7 @@ import { getPRText } from './pr-text.js'; | |||||
| import { updateDepsFile } from './update-deps.js'; | ||||||
| import { Octokit } from '@octokit/rest'; | ||||||
| import { addLabels, removeLabel } from './label-utils.js'; | ||||||
| import { getBranchesTrackedByMain } from './get-target-branch-labels.js'; | ||||||
|
|
||||||
| interface RollParams { | ||||||
| rollTarget: RollTarget; | ||||||
|
|
@@ -26,20 +27,113 @@ interface RollParams { | |||||
| previousVersion?: string; | ||||||
| } | ||||||
|
|
||||||
| const TARGET_BRANCH_LABEL_PATTERN = /^target\/\d+-(?:\d+-x|x-y)$/; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This was supporting a very old branch naming pattern ( |
||||||
|
|
||||||
| // The main branch roll PR is long-lived and relabeled on every update, and | ||||||
| // labels are otherwise only ever added - so a `target/N-x-y` label that no | ||||||
| // longer qualifies (main rolled past the branch's scheduled Chromium version) | ||||||
| // or a `no-backport` added while the schedule was unavailable would stick | ||||||
| // around forever. Remove only those roller-managed labels; never touch | ||||||
| // anything else (merged/*, trop, semver, ...) and never replace the label set. | ||||||
| // Throws if any stale label could not be confirmed removed. | ||||||
| async function removeStaleBackportLabels( | ||||||
| octokit: Octokit, | ||||||
| prNumber: number, | ||||||
| targetBranchLabels: string[], | ||||||
| ) { | ||||||
| const d = debug('roller/chromium:removeStaleBackportLabels()'); | ||||||
|
|
||||||
| const { data: labelData } = await octokit.issues.listLabelsOnIssue({ | ||||||
| ...REPOS.electron, | ||||||
| issue_number: prNumber, | ||||||
| per_page: 100, | ||||||
| page: 1, | ||||||
| }); | ||||||
|
|
||||||
| const staleLabels = labelData | ||||||
| .map((label) => label.name) | ||||||
| .filter( | ||||||
| (name) => | ||||||
| (TARGET_BRANCH_LABEL_PATTERN.test(name) && !targetBranchLabels.includes(name)) || | ||||||
| (name === NO_BACKPORT && targetBranchLabels.length > 0), | ||||||
| ); | ||||||
|
|
||||||
| for (const name of staleLabels) { | ||||||
| d(`Removing stale backport label ${name} from #${prNumber}`); | ||||||
| try { | ||||||
| await octokit.issues.removeLabel({ | ||||||
| ...REPOS.electron, | ||||||
| issue_number: prNumber, | ||||||
| name, | ||||||
| }); | ||||||
| } catch (e) { | ||||||
| // Already removed out from under us - that still counts as removed. | ||||||
| if (e.status === 404) continue; | ||||||
| throw e; | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Updates the labels on a roll PR. Returns the names of the release branches | ||||||
| // the PR's `target/N-x-y` labels cover - non-empty only for Chromium rolls to | ||||||
| // the main branch whose backport labels were successfully reconciled. | ||||||
| async function updateLabels( | ||||||
| octokit: Octokit, | ||||||
| { rollTarget, electronBranch, targetVersion, previousVersion, prNumber }: RollParams, | ||||||
| ) { | ||||||
| isNewPr = false, | ||||||
| ): Promise<string[]> { | ||||||
| const d = debug(`roller/${rollTarget.name}:updateLabels()`); | ||||||
| let labels: string[] = []; | ||||||
| let labelToRemove: string; | ||||||
| let coveredBranches: string[] = []; | ||||||
|
|
||||||
| if (electronBranch.name === MAIN_BRANCH) { | ||||||
| let targetBranchLabels: string[] = []; | ||||||
| let reconciled = false; | ||||||
|
|
||||||
| // Chromium rolls to main should be labeled for backport to every supported | ||||||
| // release branch whose scheduled Chromium version is >= the rolled version. | ||||||
| if (rollTarget === ROLL_TARGETS.chromium) { | ||||||
| try { | ||||||
| const chromiumMajorVersion = Number(targetVersion.split('.')[0]); | ||||||
| if (Number.isNaN(chromiumMajorVersion)) { | ||||||
| throw new Error(`${targetVersion} is not a valid version number`); | ||||||
| } | ||||||
| const trackedBranches = await getBranchesTrackedByMain(octokit, chromiumMajorVersion); | ||||||
| targetBranchLabels = trackedBranches.map((branch) => `target/${branch}`); | ||||||
| // The PR must never carry both no-backport and target/ labels - trop | ||||||
| // rejects that as ambiguous. Only transition to the new backport label | ||||||
| // set once every label conflicting with it is confirmed removed. | ||||||
| await removeStaleBackportLabels(octokit, prNumber, targetBranchLabels); | ||||||
| coveredBranches = trackedBranches; | ||||||
| reconciled = true; | ||||||
| } catch (e) { | ||||||
| // Leave the PR's existing backport labels exactly as they were - a | ||||||
| // half-applied transition could strand conflicting labels on the PR. | ||||||
| targetBranchLabels = []; | ||||||
| coveredBranches = []; | ||||||
| d(`Failed to reconcile backport labels: ${e.message} - leaving existing labels unchanged`); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| labels.push(electronBranch.name === MAIN_BRANCH ? NO_BACKPORT : BACKPORT_CHECK_SKIP); | ||||||
| if (targetBranchLabels.length > 0) { | ||||||
| d(`Adding target branch labels: ${targetBranchLabels.join(', ')}`); | ||||||
| labels.push(...targetBranchLabels); | ||||||
| } else if (rollTarget !== ROLL_TARGETS.chromium || reconciled || isNewPr) { | ||||||
| // A reconciled empty set means no release branch qualifies; a brand-new | ||||||
| // PR has no existing backport labels to preserve, so it can take the | ||||||
| // fallback even when reconciliation failed. | ||||||
| labels.push(NO_BACKPORT); | ||||||
| } | ||||||
|
claude[bot] marked this conversation as resolved.
|
||||||
| } else { | ||||||
| labels.push(BACKPORT_CHECK_SKIP); | ||||||
| } | ||||||
|
|
||||||
| // Chromium bumps & roll bumps to the main branch are always patch bumps. | ||||||
| if (electronBranch.name === MAIN_BRANCH || rollTarget === ROLL_TARGETS.chromium) { | ||||||
| labels.push('semver/patch'); | ||||||
| await addLabels(octokit, { prNumber, labels }); | ||||||
| return; | ||||||
| return coveredBranches; | ||||||
| } | ||||||
|
|
||||||
| // Check Node.js rolls against previous version and determine the semver label to add. | ||||||
|
|
@@ -54,6 +148,8 @@ async function updateLabels( | |||||
|
|
||||||
| await removeLabel(octokit, { prNumber, name: labelToRemove }); | ||||||
| await addLabels(octokit, { prNumber, labels }); | ||||||
|
|
||||||
| return coveredBranches; | ||||||
| } | ||||||
|
|
||||||
| async function triggerChromiumUpgradeWorkflow(octokit: Octokit) { | ||||||
|
|
@@ -66,11 +162,15 @@ async function triggerChromiumUpgradeWorkflow(octokit: Octokit) { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Rolls `rollTarget` on `electronBranch` to `targetVersion`. Returns the names | ||||||
| // of the release branches covered by `target/N-x-y` labels on the roll PR - | ||||||
| // non-empty only for a Chromium roll to the main branch whose PR was | ||||||
| // successfully created or updated and correctly labeled. | ||||||
| export async function roll({ | ||||||
| rollTarget, | ||||||
| electronBranch, | ||||||
| targetVersion, | ||||||
| }: RollParams): Promise<void> { | ||||||
| }: RollParams): Promise<string[]> { | ||||||
| const d = debug(`roller/${rollTarget.name}:roll()`); | ||||||
| const github = await getOctokit(); | ||||||
|
|
||||||
|
|
@@ -79,6 +179,7 @@ export async function roll({ | |||||
| ); | ||||||
|
|
||||||
| let didRoll = false; | ||||||
| let coveredBranches: string[] = []; | ||||||
|
|
||||||
| // Look for a pre-existing PR that targets this branch to see if we can update that. | ||||||
| const existingPrsForBranch = (await github.paginate('GET /repos/:owner/:repo/pulls', { | ||||||
|
|
@@ -140,6 +241,19 @@ export async function roll({ | |||||
|
|
||||||
| if (previousDEPSVersion === newDEPSVersion) { | ||||||
| d(`DEPS version unchanged - skipping PR body update`); | ||||||
| // The release schedule moves independently of Chromium - a newly cut | ||||||
| // release branch, a schedule edit, or a label call that failed on a | ||||||
| // previous run must still reconcile the labels on the open roll PR | ||||||
| // even on a day with no DEPS change. | ||||||
| if (rollTarget === ROLL_TARGETS.chromium) { | ||||||
| coveredBranches = await updateLabels(github, { | ||||||
| rollTarget, | ||||||
| electronBranch, | ||||||
| targetVersion, | ||||||
| previousVersion: previousDEPSVersion, | ||||||
| prNumber: pr.number, | ||||||
| }); | ||||||
| } | ||||||
| continue; | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -150,7 +264,7 @@ export async function roll({ | |||||
|
|
||||||
| if (!prVersionText || prVersionText.length === 0) { | ||||||
| d('Could not find PR version text in existing PR - exiting'); | ||||||
| return; | ||||||
| return coveredBranches; | ||||||
| } | ||||||
|
|
||||||
| await github.pulls.update({ | ||||||
|
|
@@ -164,7 +278,7 @@ export async function roll({ | |||||
| }), | ||||||
| }); | ||||||
|
|
||||||
| await updateLabels(github, { | ||||||
| coveredBranches = await updateLabels(github, { | ||||||
| rollTarget, | ||||||
| electronBranch, | ||||||
| targetVersion, | ||||||
|
|
@@ -218,13 +332,17 @@ export async function roll({ | |||||
| }), | ||||||
| }); | ||||||
|
|
||||||
| await updateLabels(github, { | ||||||
| rollTarget, | ||||||
| electronBranch, | ||||||
| targetVersion, | ||||||
| previousVersion: previousDEPSVersion, | ||||||
| prNumber: newPr.data.number, | ||||||
| }); | ||||||
| coveredBranches = await updateLabels( | ||||||
| github, | ||||||
| { | ||||||
| rollTarget, | ||||||
| electronBranch, | ||||||
| targetVersion, | ||||||
| previousVersion: previousDEPSVersion, | ||||||
| prNumber: newPr.data.number, | ||||||
| }, | ||||||
| true, | ||||||
| ); | ||||||
|
|
||||||
| d(`New PR: ${newPr.data.html_url}`); | ||||||
|
|
||||||
|
|
@@ -234,4 +352,6 @@ export async function roll({ | |||||
| if (didRoll && rollTarget === ROLL_TARGETS.chromium && electronBranch.name === MAIN_BRANCH) { | ||||||
| await triggerChromiumUpgradeWorkflow(github); | ||||||
| } | ||||||
|
|
||||||
| return coveredBranches; | ||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.