From 5a60880685d27a2f08e4a5917adb6a81d741f818 Mon Sep 17 00:00:00 2001 From: Jakub Mucha Date: Tue, 10 Nov 2026 12:33:58 +0000 Subject: [PATCH] fix: open the PR for a bump branch that has none bump-upstream assumed an existing bump branch already had a PR, so a branch whose PR creation was refused (e.g. GitHub Actions not allowed to create pull requests) made every later run skip that release. --- .../bumpUpstream/github/branchNeedsPr.ts | 23 +++++++++++ .../bumpUpstream/github/index.ts | 1 + .../githubActions/bumpUpstream/index.ts | 13 ++++++- src/providers/github/Github.ts | 18 +++++++++ .../gaBumpUpstream/branchNeedsPr.test.ts | 39 +++++++++++++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/commands/githubActions/bumpUpstream/github/branchNeedsPr.ts create mode 100644 test/commands/gaBumpUpstream/branchNeedsPr.test.ts diff --git a/src/commands/githubActions/bumpUpstream/github/branchNeedsPr.ts b/src/commands/githubActions/bumpUpstream/github/branchNeedsPr.ts new file mode 100644 index 00000000..3e0e2509 --- /dev/null +++ b/src/commands/githubActions/bumpUpstream/github/branchNeedsPr.ts @@ -0,0 +1,23 @@ +/** The two Github methods this needs. */ +interface BranchPrSource { + branchExists(branch: string): Promise; + getPrsFromBranch(branch: string): Promise; +} + +/** + * True when a bump branch was pushed but never got a PR, open or closed, for + * example because the token was not allowed to create pull requests. Such a + * branch would otherwise make every later run skip that release. A branch whose + * PR was closed stays skipped: closing it was a decision. + */ +export async function branchNeedsPr({ + repo, + branchName +}: { + repo: BranchPrSource; + branchName: string; +}): Promise { + if (!(await repo.branchExists(branchName))) return false; + const prs = await repo.getPrsFromBranch(branchName); + return prs.length === 0; +} diff --git a/src/commands/githubActions/bumpUpstream/github/index.ts b/src/commands/githubActions/bumpUpstream/github/index.ts index 58c03364..8674fcfb 100644 --- a/src/commands/githubActions/bumpUpstream/github/index.ts +++ b/src/commands/githubActions/bumpUpstream/github/index.ts @@ -1,3 +1,4 @@ +export * from "./branchNeedsPr.js"; export * from "./closeOldPrs.js"; export * from "./getBumpPrBody.js"; export * from "./getGithubSettings.js"; diff --git a/src/commands/githubActions/bumpUpstream/index.ts b/src/commands/githubActions/bumpUpstream/index.ts index 34d2d160..841852e2 100644 --- a/src/commands/githubActions/bumpUpstream/index.ts +++ b/src/commands/githubActions/bumpUpstream/index.ts @@ -15,6 +15,7 @@ import { GitSettings, GithubSettings, UpstreamSettings } from "./types.js"; import { printSettings, getInitialSettings } from "./settings/index.js"; import { ManifestFormat } from "../../../files/manifest/types.js"; import { + branchNeedsPr, closeOldPrs, getBumpPrBody, getGithubSettings, @@ -107,7 +108,17 @@ async function gaBumpUpstreamHandler({ const githubSettings = await getGithubSettings(dir, upstreamSettings); const { branchName, repo } = githubSettings; if (!(await isBranchNew({ branchName, repo, dir }))) { - // We assume the PR was already opened + if (await branchNeedsPr({ repo, branchName })) { + // An earlier run pushed the branch but could not open its PR + console.log(`Branch ${branchName} exists without a PR, opening it`); + await attemptToOpenPR({ + commitMsg: createCommitMessage(upstreamSettings), + upstreamSettings, + githubSettings + }); + return; + } + // The PR was already opened console.log(`Branch ${branchName} already exists`); return; } diff --git a/src/providers/github/Github.ts b/src/providers/github/Github.ts index 12a125e1..3325c597 100644 --- a/src/providers/github/Github.ts +++ b/src/providers/github/Github.ts @@ -443,6 +443,24 @@ export class Github { return openPrsResponse.data; } + /** + * Returns PRs in any state (open or closed) where head branch equals `branch` + * Only branches and PRs originating from the same repo + */ + async getPrsFromBranch(branch: string) { + const prsResponse = await this.octokit.rest.pulls.list({ + owner: this.owner, + repo: this.repo, + state: "all", + headers: { + "X-GitHub-Api-Version": "2022-11-28" + }, + head: `${this.owner}:${branch}` + }); + + return prsResponse.data; + } + /** * Returns all branches; cleanup must never infer deletion from a partial page. * @returns branch = { diff --git a/test/commands/gaBumpUpstream/branchNeedsPr.test.ts b/test/commands/gaBumpUpstream/branchNeedsPr.test.ts new file mode 100644 index 00000000..af6c7581 --- /dev/null +++ b/test/commands/gaBumpUpstream/branchNeedsPr.test.ts @@ -0,0 +1,39 @@ +import { expect } from "chai"; +import { branchNeedsPr } from "../../../src/commands/githubActions/bumpUpstream/github/index.js"; + +const branchName = "dappnodebot/bump-upstream/org/repo@1.2.0"; + +function fakeRepo({ + remoteBranch, + prStates +}: { + remoteBranch: boolean; + prStates: string[]; +}) { + return { + branchExists: async () => remoteBranch, + getPrsFromBranch: async () => prStates.map(state => ({ state })) + }; +} + +describe("command / gaBumpUpstream / branchNeedsPr", () => { + it("is true for a pushed bump branch that never got a PR", async () => { + const repo = fakeRepo({ remoteBranch: true, prStates: [] }); + expect(await branchNeedsPr({ repo, branchName })).to.equal(true); + }); + + it("is false when the branch has an open PR", async () => { + const repo = fakeRepo({ remoteBranch: true, prStates: ["open"] }); + expect(await branchNeedsPr({ repo, branchName })).to.equal(false); + }); + + it("is false when the branch's PR was closed", async () => { + const repo = fakeRepo({ remoteBranch: true, prStates: ["closed"] }); + expect(await branchNeedsPr({ repo, branchName })).to.equal(false); + }); + + it("is false when the branch exists only locally", async () => { + const repo = fakeRepo({ remoteBranch: false, prStates: [] }); + expect(await branchNeedsPr({ repo, branchName })).to.equal(false); + }); +});