diff --git a/.github/workflows/generate-changelog.yaml b/.github/workflows/generate-changelog.yaml index 75caf29..e2bb4f1 100644 --- a/.github/workflows/generate-changelog.yaml +++ b/.github/workflows/generate-changelog.yaml @@ -19,6 +19,11 @@ on: required: false default: 'main' type: string + reuse_branch: + description: 'Keep a single self-updating changelog pull request instead of opening a new one on every run.' + required: false + default: true + type: boolean pull_request_title: description: 'The title of the pull request.' @@ -110,6 +115,7 @@ jobs: with: branch_name: ${{ inputs.branch_name }} base_branch_name: ${{ inputs.base_branch_name }} + reuse_branch: ${{ inputs.reuse_branch }} pull_request_title: ${{ inputs.pull_request_title }} pull_request_body: ${{ inputs.pull_request_body }} author_name: ${{ inputs.author_name }} diff --git a/actions/assemble-docs/dist/index.js b/actions/assemble-docs/dist/index.js index 7deb253..ad8744a 100644 --- a/actions/assemble-docs/dist/index.js +++ b/actions/assemble-docs/dist/index.js @@ -21269,11 +21269,11 @@ var require_dist = __commonJS({ exec2(`git config user.email "${authorEmail}"`); exec2(`git commit -m "${message}"`); } - function gitCheckoutBranch(branch) { - exec2(`git checkout -b ${branch}`); + function gitCheckoutBranch(branch, { reset = false } = {}) { + exec2(`git checkout ${reset ? "-B" : "-b"} ${branch}`); } - function gitPushBranch(branch) { - exec2(`git push -u origin ${branch}`); + function gitPushBranch(branch, { force = false } = {}) { + exec2(`git push ${force ? "--force " : ""}-u origin ${branch}`); } function gitHasChanges() { const output = exec2("git status --porcelain"); diff --git a/actions/create-pr/action.yaml b/actions/create-pr/action.yaml index 113f198..bf99d78 100644 --- a/actions/create-pr/action.yaml +++ b/actions/create-pr/action.yaml @@ -11,6 +11,10 @@ inputs: description: 'The name of the base branch to create a pull request against.' required: false default: 'main' + reuse_branch: + description: 'Reuse branch_name as-is and update the pull request already open for it, instead of creating a new randomly suffixed branch on every run. When there is nothing left to propose, the open pull request is closed.' + required: false + default: 'false' pull_request_title: description: 'The title of the pull request.' @@ -45,6 +49,8 @@ outputs: description: 'The number of the created pull request.' pull_request_created: description: 'Whether the pull request was created.' + pull_request_updated: + description: 'Whether an already open pull request was updated instead of a new one being created. Only ever true when reuse_branch is enabled.' runs: using: node24 diff --git a/actions/create-pr/dist/index.js b/actions/create-pr/dist/index.js index d846f0c..a07c74b 100644 --- a/actions/create-pr/dist/index.js +++ b/actions/create-pr/dist/index.js @@ -22084,7 +22084,7 @@ var require_dist = __commonJS({ generateRandomSuffix: () => generateRandomSuffix2, getInput: () => getInput22, getNumberInput: () => getNumberInput, - getOptInput: () => getOptInput, + getOptInput: () => getOptInput2, gitAdd: () => gitAdd2, gitCheckoutBranch: () => gitCheckoutBranch2, gitCommit: () => gitCommit2, @@ -22137,11 +22137,11 @@ var require_dist = __commonJS({ exec2(`git config user.email "${authorEmail}"`); exec2(`git commit -m "${message}"`); } - function gitCheckoutBranch2(branch) { - exec2(`git checkout -b ${branch}`); + function gitCheckoutBranch2(branch, { reset = false } = {}) { + exec2(`git checkout ${reset ? "-B" : "-b"} ${branch}`); } - function gitPushBranch2(branch) { - exec2(`git push -u origin ${branch}`); + function gitPushBranch2(branch, { force = false } = {}) { + exec2(`git push ${force ? "--force " : ""}-u origin ${branch}`); } function gitHasChanges2() { const output = exec2("git status --porcelain"); @@ -22151,7 +22151,7 @@ var require_dist = __commonJS({ function getInput22(name) { return core.getInput(name, { required: true, trimWhitespace: true }); } - function getOptInput(name, defaultValue) { + function getOptInput2(name, defaultValue) { return core.getInput(name, { required: false, trimWhitespace: true }) || defaultValue; } function getNumberInput(name) { @@ -25933,6 +25933,37 @@ async function createPullRequest({ number: res.data.number }; } +async function findOpenPullRequest({ + octokit, + owner, + repo, + head, + base +}) { + const res = await octokit.rest.pulls.list({ + owner, + repo, + base, + head: `${owner}:${head}`, + state: "open" + }); + const [pullRequest] = res.data; + return pullRequest ? { number: pullRequest.number } : void 0; +} +async function closePullRequest({ + octokit, + owner, + repo, + number +}) { + await octokit.rest.pulls.update({ + owner, + repo, + pull_number: number, + state: "closed" + }); + info(`Closed pull request #${number}`); +} // src/create-commit.ts init_core(); @@ -25941,12 +25972,13 @@ function createCommit({ message, head, authorName, - authorEmail + authorEmail, + reuseBranch = false }) { - (0, import_action_utils.gitCheckoutBranch)(head); + (0, import_action_utils.gitCheckoutBranch)(head, { reset: reuseBranch }); (0, import_action_utils.gitAdd)(); (0, import_action_utils.gitCommit)(message, authorName, authorEmail); - (0, import_action_utils.gitPushBranch)(head); + (0, import_action_utils.gitPushBranch)(head, { force: reuseBranch }); info(`Created git commit on branch ${head}`); } @@ -25955,7 +25987,9 @@ async function run() { try { const authorName = (0, import_action_utils2.getInput)("author_name"); const authorEmail = (0, import_action_utils2.getInput)("author_email"); - const head = `${(0, import_action_utils2.getInput)("branch_name")}-${(0, import_action_utils2.generateRandomSuffix)(6)}`; + const reuseBranch = (0, import_action_utils2.getOptInput)("reuse_branch", "false") === "true"; + const branchName = (0, import_action_utils2.getInput)("branch_name"); + const head = reuseBranch ? branchName : `${branchName}-${(0, import_action_utils2.generateRandomSuffix)(6)}`; const base = (0, import_action_utils2.getInput)("base_branch_name"); const message = (0, import_action_utils2.getInput)("commit_message"); const title = (0, import_action_utils2.getInput)("pull_request_title"); @@ -25968,14 +26002,42 @@ async function run() { "No changes detected, skipping commit and pull request creation" ); setOutput("pull_request_created", false); + setOutput("pull_request_updated", false); + if (reuseBranch) { + const obsolete = await findOpenPullRequest({ + octokit, + owner, + repo, + head, + base + }); + if (obsolete) { + await closePullRequest({ + octokit, + owner, + repo, + number: obsolete.number + }); + setOutput("pull_request_number", obsolete.number); + } + } return; } createCommit({ authorEmail, authorName, head, - message + message, + reuseBranch }); + const existing = reuseBranch ? await findOpenPullRequest({ octokit, owner, repo, head, base }) : void 0; + if (existing) { + info(`Updated pull request #${existing.number}`); + setOutput("pull_request_number", existing.number); + setOutput("pull_request_created", false); + setOutput("pull_request_updated", true); + return; + } const res = await createPullRequest({ octokit, owner, @@ -25987,6 +26049,7 @@ async function run() { }); setOutput("pull_request_number", res.number); setOutput("pull_request_created", true); + setOutput("pull_request_updated", false); } catch (error2) { if (error2 instanceof Error) { setFailed(error2.message); diff --git a/actions/create-pr/src/create-commit.ts b/actions/create-pr/src/create-commit.ts index daebd2b..6afa44c 100644 --- a/actions/create-pr/src/create-commit.ts +++ b/actions/create-pr/src/create-commit.ts @@ -11,6 +11,11 @@ export interface CreateCommitOptions { head: string; authorName: string; authorEmail: string; + /** + * Reuse a long-lived branch rather than a fresh one, resetting it to the + * current commit and force pushing it. + */ + reuseBranch?: boolean; } export function createCommit({ @@ -18,11 +23,12 @@ export function createCommit({ head, authorName, authorEmail, + reuseBranch = false, }: CreateCommitOptions): void { - gitCheckoutBranch(head); + gitCheckoutBranch(head, { reset: reuseBranch }); gitAdd(); gitCommit(message, authorName, authorEmail); - gitPushBranch(head); + gitPushBranch(head, { force: reuseBranch }); core.info(`Created git commit on branch ${head}`); } diff --git a/actions/create-pr/src/create-pull-request.ts b/actions/create-pr/src/create-pull-request.ts index 7ebd1aa..3e0eeef 100644 --- a/actions/create-pr/src/create-pull-request.ts +++ b/actions/create-pr/src/create-pull-request.ts @@ -39,3 +39,57 @@ export async function createPullRequest({ number: res.data.number, }; } + +export interface FindOpenPullRequestOptions { + octokit: Octokit; + owner: string; + repo: string; + head: string; + base: string; +} + +/** + * Finds the open pull request for the given head and base branches, if any. + */ +export async function findOpenPullRequest({ + octokit, + owner, + repo, + head, + base, +}: FindOpenPullRequestOptions): Promise { + const res = await octokit.rest.pulls.list({ + owner, + repo, + base, + head: `${owner}:${head}`, + state: 'open', + }); + + const [pullRequest] = res.data; + + return pullRequest ? { number: pullRequest.number } : undefined; +} + +export interface ClosePullRequestOptions { + octokit: Octokit; + owner: string; + repo: string; + number: number; +} + +export async function closePullRequest({ + octokit, + owner, + repo, + number, +}: ClosePullRequestOptions): Promise { + await octokit.rest.pulls.update({ + owner, + repo, + pull_number: number, + state: 'closed', + }); + + core.info(`Closed pull request #${number}`); +} diff --git a/actions/create-pr/src/main.ts b/actions/create-pr/src/main.ts index b90b311..f461307 100644 --- a/actions/create-pr/src/main.ts +++ b/actions/create-pr/src/main.ts @@ -3,16 +3,28 @@ import * as github from '@actions/github'; import { generateRandomSuffix, getInput, + getOptInput, gitHasChanges, } from '@dfinity/action-utils'; -import { createPullRequest } from './create-pull-request'; +import { + closePullRequest, + createPullRequest, + findOpenPullRequest, +} from './create-pull-request'; import { createCommit } from './create-commit'; export async function run(): Promise { try { const authorName = getInput('author_name'); const authorEmail = getInput('author_email'); - const head = `${getInput('branch_name')}-${generateRandomSuffix(6)}`; + const reuseBranch = getOptInput('reuse_branch', 'false') === 'true'; + const branchName = getInput('branch_name'); + // A fresh branch per run leaves a pull request behind on every run where an + // earlier one has not been merged yet. Reusing a single branch keeps one + // self-updating pull request instead. + const head = reuseBranch + ? branchName + : `${branchName}-${generateRandomSuffix(6)}`; const base = getInput('base_branch_name'); const message = getInput('commit_message'); const title = getInput('pull_request_title'); @@ -27,6 +39,30 @@ export async function run(): Promise { 'No changes detected, skipping commit and pull request creation', ); core.setOutput('pull_request_created', false); + core.setOutput('pull_request_updated', false); + + // There is nothing left to propose, so a pull request opened by an earlier + // run is obsolete. Left open it goes stale and eventually conflicts. + if (reuseBranch) { + const obsolete = await findOpenPullRequest({ + octokit, + owner, + repo, + head, + base, + }); + + if (obsolete) { + await closePullRequest({ + octokit, + owner, + repo, + number: obsolete.number, + }); + core.setOutput('pull_request_number', obsolete.number); + } + } + return; } @@ -35,8 +71,24 @@ export async function run(): Promise { authorName, head, message, + reuseBranch, }); + // The force push above has already updated any open pull request for this + // branch, so creating another one would fail. + const existing = reuseBranch + ? await findOpenPullRequest({ octokit, owner, repo, head, base }) + : undefined; + + if (existing) { + core.info(`Updated pull request #${existing.number}`); + core.setOutput('pull_request_number', existing.number); + core.setOutput('pull_request_created', false); + core.setOutput('pull_request_updated', true); + + return; + } + const res = await createPullRequest({ octokit, owner, @@ -49,6 +101,7 @@ export async function run(): Promise { core.setOutput('pull_request_number', res.number); core.setOutput('pull_request_created', true); + core.setOutput('pull_request_updated', false); } catch (error) { if (error instanceof Error) { core.setFailed(error.message); diff --git a/actions/extract-version/dist/index.js b/actions/extract-version/dist/index.js index bd0f3fc..78ffe2d 100644 --- a/actions/extract-version/dist/index.js +++ b/actions/extract-version/dist/index.js @@ -21269,11 +21269,11 @@ var require_dist = __commonJS({ exec3(`git config user.email "${authorEmail}"`); exec3(`git commit -m "${message}"`); } - function gitCheckoutBranch(branch) { - exec3(`git checkout -b ${branch}`); + function gitCheckoutBranch(branch, { reset = false } = {}) { + exec3(`git checkout ${reset ? "-B" : "-b"} ${branch}`); } - function gitPushBranch(branch) { - exec3(`git push -u origin ${branch}`); + function gitPushBranch(branch, { force = false } = {}) { + exec3(`git push ${force ? "--force " : ""}-u origin ${branch}`); } function gitHasChanges() { const output = exec3("git status --porcelain"); diff --git a/actions/submit-docs/dist/index.js b/actions/submit-docs/dist/index.js index 81938e3..70e2750 100644 --- a/actions/submit-docs/dist/index.js +++ b/actions/submit-docs/dist/index.js @@ -22137,11 +22137,11 @@ var require_dist = __commonJS({ exec2(`git config user.email "${authorEmail}"`); exec2(`git commit -m "${message}"`); } - function gitCheckoutBranch(branch) { - exec2(`git checkout -b ${branch}`); + function gitCheckoutBranch(branch, { reset = false } = {}) { + exec2(`git checkout ${reset ? "-B" : "-b"} ${branch}`); } - function gitPushBranch2(branch) { - exec2(`git push -u origin ${branch}`); + function gitPushBranch2(branch, { force = false } = {}) { + exec2(`git push ${force ? "--force " : ""}-u origin ${branch}`); } function gitHasChanges2() { const output = exec2("git status --porcelain"); diff --git a/lib/action-utils/src/git.ts b/lib/action-utils/src/git.ts index 2cc52d6..6740ed9 100644 --- a/lib/action-utils/src/git.ts +++ b/lib/action-utils/src/git.ts @@ -14,12 +14,30 @@ export function gitCommit( exec(`git commit -m "${message}"`); } -export function gitCheckoutBranch(branch: string): void { - exec(`git checkout -b ${branch}`); +export interface GitCheckoutBranchOptions { + /** + * Reset the branch to the current commit if it already exists, instead of + * failing. Required when reusing a long-lived branch. + */ + reset?: boolean; } -export function gitPushBranch(branch: string): void { - exec(`git push -u origin ${branch}`); +export function gitCheckoutBranch( + branch: string, + { reset = false }: GitCheckoutBranchOptions = {}, +): void { + exec(`git checkout ${reset ? '-B' : '-b'} ${branch}`); +} + +export interface GitPushBranchOptions { + force?: boolean; +} + +export function gitPushBranch( + branch: string, + { force = false }: GitPushBranchOptions = {}, +): void { + exec(`git push ${force ? '--force ' : ''}-u origin ${branch}`); } export function gitHasChanges(): boolean {