diff --git a/README.md b/README.md index 5bdcc27..b91bd02 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ It works in this way: given the provided `pull/merge request` it infers the serv After that it clones the corresponding git repository, check out in the provided `target branch` and create a new branch from that (name automatically generated if not provided as option). -By default the tool will try to cherry-pick the single squashed/merged commit into the newly created branch. The `--no-squash` and `--auto-no-squash` options control this behavior according the following table. +By default the tool will try to cherry-pick the single squashed/merged commit into the newly created branch. The `--no-squash` and `--auto-no-squash` options control this behavior according the following table. | No squash | Auto no squash |Behavior| |---|---|---| @@ -135,7 +135,7 @@ This tool comes with some inputs that allow users to override the default behavi | Strategy Option | --strategy-option | N | Cherry pick merging strategy option, see [git-merge](https://git-scm.com/docs/git-merge#_merge_strategies) doc for all possible values | "theirs" | | Cherry-pick Options | --cherry-pick-options | N | Additional cherry-pick options, see [git-cherry-pick](https://git-scm.com/docs/git-cherry-pick) doc for all possible values | "theirs" | | Additional comments | --comments | N | Semicolon separated list of additional comments to be posted to the backported pull request | [] | -| Enable error notification | --enable-err-notification | N | If true, enable the error notification as comment on the original pull request | false | +| Enable error notification | --enable-err-notification | N | If true, enable the error notification as comment on the original pull request, see [Error notification](#error-notification) | false | | Dry Run | -d, --dry-run | N | If enabled the tool does not push nor create anything remotely, use this to skip PR creation | false | > **NOTE**: `pull request` and (`target branch` or `target branch pattern`) are *mandatory*, they must be provided as CLI options or as part of the configuration file (if used). @@ -179,6 +179,16 @@ $ git-backporting -tb v1 -pr https://github.com/upstream/project/pull/123 -a *** When using `--tb-repo` alone, the backport branch is pushed directly to the target repository, so your PAT needs push access there. When combining it with `--bp-repo`, the branch is pushed to the fork instead, so your PAT needs push access on the fork and only the ability to open a pull request on the target repository. +#### Error notification + +If error notifications are enabled, every failed backport is reported via comment on the original pull request. +The comment contains the error, a reconstruction of the attempted steps and a hidden marker identifying the target branch. + +This marker is used to debounce the reporting of the same backport failing multiple times in quick succession. +Before backporting to a target branch, the tool inspects the most recent comments of the original pull request. +If a failure has already been reported, the backport attempt will be skipped. +To retry the backport, delete the failure comment (or edit out the marker). + #### Configuration file example This is an example of a configuration file that can be used. @@ -211,7 +221,7 @@ This action can be used in any *GitHub* workflow, below you can find a simple ex ```yml name: Pull Request Backporting using Git Backporting -on: +on: workflow_dispatch: inputs: targetBranch: @@ -220,12 +230,12 @@ on: type: string pullRequest: description: 'Pull request' - required: true + required: true type: string dryRun: description: 'Dry run' required: false - default: "true" + default: "true" type: string jobs: diff --git a/dist/cli/index.js b/dist/cli/index.js index e458db3..8b98481 100755 --- a/dist/cli/index.js +++ b/dist/cli/index.js @@ -1002,6 +1002,48 @@ class GitHubClient { const { owner, project, id } = this.extractPullRequestData(prUrl); return this.getPullRequest(owner, project, id, squash); } + async getLatestPullRequestComments(prUrl) { + const { owner, project, id } = this.extractPullRequestData(prUrl); + this.logger.debug(`Fetching latest comments of pull request ${owner}/${project}/${id}`); + const perPage = 100; + const params = { + owner: owner, + repo: project, + issue_number: id, + per_page: perPage, + }; + const { data, headers } = await this.octokit.issues.listComments(params); + const firstPage = data.map(c => c.body ?? ""); + // Forgejo so far didn't implement pagination, and just always returns all comments. + const total = this.extractTotalCount(headers); + if (total !== undefined && data.length >= total) { + return firstPage; + } + const lastPage = this.extractLastPage(headers, perPage); + if (lastPage <= 1) { + return firstPage; + } + // last page may hold only one single comment, so always fetch last two pages + const fetchPage = async (page) => { + const { data } = await this.octokit.issues.listComments({ ...params, page }); + return data.map(c => c.body ?? ""); + }; + const secondLast = lastPage === 2 ? firstPage : await fetchPage(lastPage - 1); + const last = await fetchPage(lastPage); + return [...secondLast, ...last]; + } + extractTotalCount(headers) { + const total = parseInt(`${headers["x-total-count"]}`); + return isNaN(total) ? undefined : total; + } + extractLastPage(headers, perPage) { + const last = /[?&]page=(\d+)[^>]*>;\s*rel="last"/.exec(headers.link ?? ""); + if (last) { + return parseInt(last[1]); + } + const total = this.extractTotalCount(headers); + return total === undefined ? 1 : Math.ceil(total / perPage); + } /** * Read the message out of a git.getCommit response. GitHub returns it at the top level * (`message`), whereas Gitea/Forgejo nest it under `commit` (`commit.message`). Support @@ -1382,6 +1424,14 @@ class GitLabClient { } return commentUrl; } + // https://docs.gitlab.com/ee/api/notes.html#list-all-merge-request-notes + async getLatestPullRequestComments(mrUrl) { + const { namespace, project, id } = this.extractMergeRequestData(mrUrl); + const projectId = this.getProjectId(namespace, project); + const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${id}/notes?sort=desc&per_page=100`); + const notes = (data ?? []); + return notes.map(n => n.body ?? ""); + } // UTILS /** * Retrieve a gitlab user given its username @@ -1762,6 +1812,23 @@ class Runner { } return token; } + /** + * Check whether a previous run already reported a failed backport to the same + * target branch, by looking for a hidden marker in the latest comments of the + * original pull request. + * @returns true if the failure has already been reported, false otherwise or if + * the comments could not be fetched + */ + async failureAlreadyReported(configs, backportPR, git) { + try { + const comments = await git.gitClientApi.getLatestPullRequestComments(configs.originalPullRequest.url); + return comments.some(c => c.includes(failureMarker(backportPR.base))); + } + catch (error) { + this.logger.warn(`Unable to fetch the comments of ${configs.originalPullRequest.url}: ${error}`); + return false; + } + } async executeBackport(configs, backportPR, git) { const remote = backportPR.headRepo?.cloneUrl ?? backportPR.cloneUrl; const branchExists = await git.gitCli.remoteBranchExists(remote, backportPR.head); @@ -1769,6 +1836,11 @@ class Runner { this.logger.warn(`Backport branch ${backportPR.head} already exists on ${remote}, skipping`); return; } + const notifyError = !configs.dryRun && configs.errorNotification.enabled && configs.errorNotification.message.length > 0; + if (notifyError && await this.failureAlreadyReported(configs, backportPR, git)) { + this.logger.warn(`Backport to ${backportPR.base} already failed. Delete failure comment to retry. Skipping.`); + return; + } let i = 0; for (const step of backportSteps(this.logger, configs, backportPR, git)) { try { @@ -1776,7 +1848,7 @@ class Runner { } catch (error) { this.logger.error(`Something went wrong backporting to ${backportPR.base}: ${error}`); - if (!configs.dryRun && configs.errorNotification.enabled && configs.errorNotification.message.length > 0) { + if (notifyError) { // notify the failure as comment in the original pull request let comment = (0, runner_util_1.injectError)(configs.errorNotification.message, error); comment = (0, runner_util_1.injectTargetBranch)(comment, backportPR.base); @@ -1789,6 +1861,8 @@ class Runner { catch (scriptError) { this.logger.error(`Something went wrong reconstructing the script: ${scriptError}`); } + comment += `\n\nThe backport to \`${backportPR.base}\` will not be retried until this comment is deleted.`; + comment += `\n\n${failureMarker(backportPR.base)}`; await git.gitClientApi.createPullRequestComment(configs.originalPullRequest.url, comment); } throw error; @@ -1798,6 +1872,10 @@ class Runner { } } exports["default"] = Runner; +// Hidden marker appended to the failure notification comment. +function failureMarker(base) { + return ``; +} function* backportSteps(logger, configs, backportPR, git) { // every failible operation should be in one dedicated closure // whether the backport pr targets a different repository than the original pull request's one (--tb-repo), @@ -1919,6 +1997,9 @@ async function backportScript(configs, backportPR, git, failed) { for (const step of backportSteps(fakeLogger, configs, backportPR, { gitClientType: git.gitClientType, gitClientApi: { + async getLatestPullRequestComments(_prUrl) { + return []; + }, async createPullRequest(_backport) { s += `# ${git.gitClientType}.createPullRequest`; return ""; diff --git a/dist/gha/index.js b/dist/gha/index.js index 39d4eaf..392c75a 100755 --- a/dist/gha/index.js +++ b/dist/gha/index.js @@ -965,6 +965,48 @@ class GitHubClient { const { owner, project, id } = this.extractPullRequestData(prUrl); return this.getPullRequest(owner, project, id, squash); } + async getLatestPullRequestComments(prUrl) { + const { owner, project, id } = this.extractPullRequestData(prUrl); + this.logger.debug(`Fetching latest comments of pull request ${owner}/${project}/${id}`); + const perPage = 100; + const params = { + owner: owner, + repo: project, + issue_number: id, + per_page: perPage, + }; + const { data, headers } = await this.octokit.issues.listComments(params); + const firstPage = data.map(c => c.body ?? ""); + // Forgejo so far didn't implement pagination, and just always returns all comments. + const total = this.extractTotalCount(headers); + if (total !== undefined && data.length >= total) { + return firstPage; + } + const lastPage = this.extractLastPage(headers, perPage); + if (lastPage <= 1) { + return firstPage; + } + // last page may hold only one single comment, so always fetch last two pages + const fetchPage = async (page) => { + const { data } = await this.octokit.issues.listComments({ ...params, page }); + return data.map(c => c.body ?? ""); + }; + const secondLast = lastPage === 2 ? firstPage : await fetchPage(lastPage - 1); + const last = await fetchPage(lastPage); + return [...secondLast, ...last]; + } + extractTotalCount(headers) { + const total = parseInt(`${headers["x-total-count"]}`); + return isNaN(total) ? undefined : total; + } + extractLastPage(headers, perPage) { + const last = /[?&]page=(\d+)[^>]*>;\s*rel="last"/.exec(headers.link ?? ""); + if (last) { + return parseInt(last[1]); + } + const total = this.extractTotalCount(headers); + return total === undefined ? 1 : Math.ceil(total / perPage); + } /** * Read the message out of a git.getCommit response. GitHub returns it at the top level * (`message`), whereas Gitea/Forgejo nest it under `commit` (`commit.message`). Support @@ -1345,6 +1387,14 @@ class GitLabClient { } return commentUrl; } + // https://docs.gitlab.com/ee/api/notes.html#list-all-merge-request-notes + async getLatestPullRequestComments(mrUrl) { + const { namespace, project, id } = this.extractMergeRequestData(mrUrl); + const projectId = this.getProjectId(namespace, project); + const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${id}/notes?sort=desc&per_page=100`); + const notes = (data ?? []); + return notes.map(n => n.body ?? ""); + } // UTILS /** * Retrieve a gitlab user given its username @@ -1725,6 +1775,23 @@ class Runner { } return token; } + /** + * Check whether a previous run already reported a failed backport to the same + * target branch, by looking for a hidden marker in the latest comments of the + * original pull request. + * @returns true if the failure has already been reported, false otherwise or if + * the comments could not be fetched + */ + async failureAlreadyReported(configs, backportPR, git) { + try { + const comments = await git.gitClientApi.getLatestPullRequestComments(configs.originalPullRequest.url); + return comments.some(c => c.includes(failureMarker(backportPR.base))); + } + catch (error) { + this.logger.warn(`Unable to fetch the comments of ${configs.originalPullRequest.url}: ${error}`); + return false; + } + } async executeBackport(configs, backportPR, git) { const remote = backportPR.headRepo?.cloneUrl ?? backportPR.cloneUrl; const branchExists = await git.gitCli.remoteBranchExists(remote, backportPR.head); @@ -1732,6 +1799,11 @@ class Runner { this.logger.warn(`Backport branch ${backportPR.head} already exists on ${remote}, skipping`); return; } + const notifyError = !configs.dryRun && configs.errorNotification.enabled && configs.errorNotification.message.length > 0; + if (notifyError && await this.failureAlreadyReported(configs, backportPR, git)) { + this.logger.warn(`Backport to ${backportPR.base} already failed. Delete failure comment to retry. Skipping.`); + return; + } let i = 0; for (const step of backportSteps(this.logger, configs, backportPR, git)) { try { @@ -1739,7 +1811,7 @@ class Runner { } catch (error) { this.logger.error(`Something went wrong backporting to ${backportPR.base}: ${error}`); - if (!configs.dryRun && configs.errorNotification.enabled && configs.errorNotification.message.length > 0) { + if (notifyError) { // notify the failure as comment in the original pull request let comment = (0, runner_util_1.injectError)(configs.errorNotification.message, error); comment = (0, runner_util_1.injectTargetBranch)(comment, backportPR.base); @@ -1752,6 +1824,8 @@ class Runner { catch (scriptError) { this.logger.error(`Something went wrong reconstructing the script: ${scriptError}`); } + comment += `\n\nThe backport to \`${backportPR.base}\` will not be retried until this comment is deleted.`; + comment += `\n\n${failureMarker(backportPR.base)}`; await git.gitClientApi.createPullRequestComment(configs.originalPullRequest.url, comment); } throw error; @@ -1761,6 +1835,10 @@ class Runner { } } exports["default"] = Runner; +// Hidden marker appended to the failure notification comment. +function failureMarker(base) { + return ``; +} function* backportSteps(logger, configs, backportPR, git) { // every failible operation should be in one dedicated closure // whether the backport pr targets a different repository than the original pull request's one (--tb-repo), @@ -1882,6 +1960,9 @@ async function backportScript(configs, backportPR, git, failed) { for (const step of backportSteps(fakeLogger, configs, backportPR, { gitClientType: git.gitClientType, gitClientApi: { + async getLatestPullRequestComments(_prUrl) { + return []; + }, async createPullRequest(_backport) { s += `# ${git.gitClientType}.createPullRequest`; return ""; diff --git a/src/service/git/git-client.ts b/src/service/git/git-client.ts index 2097da5..6ee7d52 100644 --- a/src/service/git/git-client.ts +++ b/src/service/git/git-client.ts @@ -35,6 +35,13 @@ import { BackportPullRequest, GitClientType, GitPullRequest } from "@bp/service/ */ getPullRequestFromUrl(prUrl: string, squash: boolean | undefined): Promise; + /** + * Get the most recent comments of the pull request. + * @param prUrl pull request's URL + * @returns {Promise} the list of comments + */ + getLatestPullRequestComments(prUrl: string): Promise; + // WRITE /** diff --git a/src/service/git/github/github-client.ts b/src/service/git/github/github-client.ts index 1ab3658..e5f887c 100644 --- a/src/service/git/github/github-client.ts +++ b/src/service/git/github/github-client.ts @@ -55,11 +55,11 @@ export default class GitHubClient implements GitClient { if (squash === undefined) { let commit_sha: string | undefined = undefined; if (!open) { - const commit = await this.octokit.rest.git.getCommit({ + const commit = await this.octokit.rest.git.getCommit({ owner: owner, repo: repo, commit_sha: (data.merge_commit_sha as string), - }); + }); // A merge commit with a single parent is produced by BOTH a "squash and merge" // and a "rebase and merge", so the parent count alone cannot tell them apart. // A rebase replays every commit of the pull request preserving @@ -99,6 +99,58 @@ export default class GitHubClient implements GitClient { return this.getPullRequest(owner, project, id, squash); } + async getLatestPullRequestComments(prUrl: string): Promise { + const { owner, project, id } = this.extractPullRequestData(prUrl); + this.logger.debug(`Fetching latest comments of pull request ${owner}/${project}/${id}`); + + const perPage = 100; + const params = { + owner: owner, + repo: project, + issue_number: id, + per_page: perPage, + }; + + const { data, headers } = await this.octokit.issues.listComments(params); + const firstPage = data.map(c => c.body ?? ""); + + // Forgejo so far didn't implement pagination, and just always returns all comments. + const total = this.extractTotalCount(headers); + if (total !== undefined && data.length >= total) { + return firstPage; + } + + const lastPage = this.extractLastPage(headers, perPage); + if (lastPage <= 1) { + return firstPage; + } + + // last page may hold only one single comment, so always fetch last two pages + const fetchPage = async (page: number): Promise => { + const { data } = await this.octokit.issues.listComments({ ...params, page }); + return data.map(c => c.body ?? ""); + }; + const secondLast = lastPage === 2 ? firstPage : await fetchPage(lastPage - 1); + const last = await fetchPage(lastPage); + + return [...secondLast, ...last]; + } + + private extractTotalCount(headers: { [header: string]: string | number | undefined }): number | undefined { + const total = parseInt(`${headers["x-total-count"]}`); + return isNaN(total) ? undefined : total; + } + + private extractLastPage(headers: { link?: string, [header: string]: string | number | undefined }, perPage: number): number { + const last = /[?&]page=(\d+)[^>]*>;\s*rel="last"/.exec(headers.link ?? ""); + if (last) { + return parseInt(last[1]); + } + + const total = this.extractTotalCount(headers); + return total === undefined ? 1 : Math.ceil(total / perPage); + } + /** * Read the message out of a git.getCommit response. GitHub returns it at the top level * (`message`), whereas Gitea/Forgejo nest it under `commit` (`commit.message`). Support diff --git a/src/service/git/gitlab/gitlab-client.ts b/src/service/git/gitlab/gitlab-client.ts index 581c342..937224e 100644 --- a/src/service/git/gitlab/gitlab-client.ts +++ b/src/service/git/gitlab/gitlab-client.ts @@ -187,6 +187,17 @@ export default class GitLabClient implements GitClient { return commentUrl; } + // https://docs.gitlab.com/ee/api/notes.html#list-all-merge-request-notes + async getLatestPullRequestComments(mrUrl: string): Promise { + const { namespace, project, id } = this.extractMergeRequestData(mrUrl); + const projectId = this.getProjectId(namespace, project); + + const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${id}/notes?sort=desc&per_page=100`); + const notes = (data ?? []) as { body?: string }[]; + + return notes.map(n => n.body ?? ""); + } + // UTILS /** diff --git a/src/service/runner/runner.ts b/src/service/runner/runner.ts index b43cbdc..6656c5a 100644 --- a/src/service/runner/runner.ts +++ b/src/service/runner/runner.ts @@ -13,7 +13,7 @@ import { injectError, injectTargetBranch } from "./runner-util"; interface Git { gitClientType: GitClientType; - gitClientApi: Pick; + gitClientApi: Pick; gitCli: Pick; } @@ -124,6 +124,23 @@ export default class Runner { return token; } + /** + * Check whether a previous run already reported a failed backport to the same + * target branch, by looking for a hidden marker in the latest comments of the + * original pull request. + * @returns true if the failure has already been reported, false otherwise or if + * the comments could not be fetched + */ + private async failureAlreadyReported(configs: Configs, backportPR: BackportPullRequest, git: Git): Promise { + try { + const comments = await git.gitClientApi.getLatestPullRequestComments(configs.originalPullRequest.url); + return comments.some(c => c.includes(failureMarker(backportPR.base))); + } catch (error) { + this.logger.warn(`Unable to fetch the comments of ${configs.originalPullRequest.url}: ${error}`); + return false; + } + } + async executeBackport(configs: Configs, backportPR: BackportPullRequest, git: Git): Promise { const remote = backportPR.headRepo?.cloneUrl ?? backportPR.cloneUrl; const branchExists = await git.gitCli.remoteBranchExists(remote, backportPR.head); @@ -132,13 +149,20 @@ export default class Runner { return; } + const notifyError = !configs.dryRun && configs.errorNotification.enabled && configs.errorNotification.message.length > 0; + + if (notifyError && await this.failureAlreadyReported(configs, backportPR, git)) { + this.logger.warn(`Backport to ${backportPR.base} already failed. Delete failure comment to retry. Skipping.`); + return; + } + let i = 0; for (const step of backportSteps(this.logger, configs, backportPR, git)) { try { await step(); } catch (error) { this.logger.error(`Something went wrong backporting to ${backportPR.base}: ${error}`); - if (!configs.dryRun && configs.errorNotification.enabled && configs.errorNotification.message.length > 0) { + if (notifyError) { // notify the failure as comment in the original pull request let comment = injectError(configs.errorNotification.message, error as string); comment = injectTargetBranch(comment, backportPR.base); @@ -150,6 +174,8 @@ export default class Runner { } catch (scriptError) { this.logger.error(`Something went wrong reconstructing the script: ${scriptError}`); } + comment += `\n\nThe backport to \`${backportPR.base}\` will not be retried until this comment is deleted.`; + comment += `\n\n${failureMarker(backportPR.base)}`; await git.gitClientApi.createPullRequestComment(configs.originalPullRequest.url, comment); } throw error; @@ -160,6 +186,11 @@ export default class Runner { } +// Hidden marker appended to the failure notification comment. +function failureMarker(base: string): string { + return ``; +} + function* backportSteps(logger: Pick, configs: Configs, backportPR: BackportPullRequest, git: Git): Generator<() => Promise, void, unknown> { // every failible operation should be in one dedicated closure @@ -291,6 +322,9 @@ async function backportScript(configs: Configs, backportPR: BackportPullRequest, for (const step of backportSteps(fakeLogger, configs, backportPR, { gitClientType: git.gitClientType, gitClientApi: { + async getLatestPullRequestComments(_prUrl: string): Promise { + return []; + }, async createPullRequest(_backport: BackportPullRequest): Promise { s += `# ${git.gitClientType}.createPullRequest`; return ""; diff --git a/test/service/runner/cli-codeberg-runner.test.ts b/test/service/runner/cli-codeberg-runner.test.ts index fe1ebce..6355813 100644 --- a/test/service/runner/cli-codeberg-runner.test.ts +++ b/test/service/runner/cli-codeberg-runner.test.ts @@ -73,7 +73,7 @@ describe("cli runner", () => { "-pr", "https://codeberg.org/owner/reponame/pulls/2368" ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -86,7 +86,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-28f63db"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -106,7 +106,7 @@ describe("cli runner", () => { "-pr", "https://codeberg.org/owner/reponame/pulls/2368" ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -116,7 +116,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-28f63db"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -137,7 +137,7 @@ describe("cli runner", () => { "-f", "folder" ]); - + await runner.execute(); const cwd = process.cwd() + "/folder"; @@ -150,7 +150,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-28f63db"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -174,7 +174,7 @@ describe("cli runner", () => { "-f", "/tmp/folder" ]); - + await runner.execute(); const cwd = "/tmp/folder"; @@ -187,7 +187,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-28f63db"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -205,7 +205,7 @@ describe("cli runner", () => { "-pr", "https://codeberg.org/owner/reponame/pulls/2368" ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -218,7 +218,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-28f63db"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -230,12 +230,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-target-28f63db", - base: "target", - title: "[target] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-target-28f63db", + base: "target", + title: "[target] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -253,7 +253,7 @@ describe("cli runner", () => { "-pr", "https://codeberg.org/owner/reponame/pulls/8632" ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -266,7 +266,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-28f63db"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(0); expect(GitCLIService.prototype.cherryPick).toHaveBeenCalledTimes(1); @@ -277,12 +277,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-target-28f63db", - base: "target", - title: "[target] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-target-28f63db", + base: "target", + title: "[target] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/8632\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -325,7 +325,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-9174896"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/4444/head:pr/4444"); @@ -337,12 +337,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-target-9174896", - base: "target", - title: "[target] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-target-9174896", + base: "target", + title: "[target] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/4444\r\n\r\nPlease review and merge", reviewers: ["gh-user"], assignees: [], @@ -422,7 +422,7 @@ describe("cli runner", () => { "--assignees", "user3,user4", ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -435,7 +435,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp_branch_name"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -447,12 +447,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp_branch_name", - base: "target", - title: "New Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp_branch_name", + base: "target", + title: "New Title", body: "New Body Prefix\r\n\r\nNew Body", reviewers: ["user1", "user2"], assignees: ["user3", "user4"], @@ -481,7 +481,7 @@ describe("cli runner", () => { "--assignees", "user3,user4", ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -494,7 +494,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp_branch_name"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -506,12 +506,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp_branch_name", - base: "target", - title: "New Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp_branch_name", + base: "target", + title: "New Title", body: "New Body Prefix - New Body", reviewers: [], assignees: ["user3", "user4"], @@ -532,7 +532,7 @@ describe("cli runner", () => { "cherry-pick :cherries:, backport prod", "--inherit-labels", ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -545,7 +545,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-28f63db"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -557,12 +557,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-target-28f63db", - base: "target", - title: "[target] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-target-28f63db", + base: "target", + title: "[target] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -582,7 +582,7 @@ describe("cli runner", () => { "--labels", "first-label, second-label ", ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -595,7 +595,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-28f63db"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -607,12 +607,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-target-28f63db", - base: "target", - title: "[target] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-target-28f63db", + base: "target", + title: "[target] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -628,7 +628,7 @@ describe("cli runner", () => { "--config-file", GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME, ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -641,7 +641,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp_branch_name"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -653,12 +653,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp_branch_name", - base: "target", - title: "New Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp_branch_name", + base: "target", + title: "New Title", body: "New Body Prefix - New Body", reviewers: [], assignees: ["user3", "user4"], @@ -677,7 +677,7 @@ describe("cli runner", () => { "-pr", "https://codeberg.org/api/v1/repos/owner/reponame/pulls/2368" ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -690,7 +690,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-28f63db"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -702,12 +702,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-target-28f63db", - base: "target", - title: "[target] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-target-28f63db", + base: "target", + title: "[target] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -726,7 +726,7 @@ describe("cli runner", () => { "https://codeberg.org/owner/reponame/pulls/8632", "--no-squash", ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -739,7 +739,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-11da4e3-0404fb9"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(0); expect(GitCLIService.prototype.cherryPick).toHaveBeenCalledTimes(2); @@ -751,12 +751,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-target-11da4e3-0404fb9", - base: "target", - title: "[target] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-target-11da4e3-0404fb9", + base: "target", + title: "[target] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/8632\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -779,7 +779,7 @@ describe("cli runner", () => { "--bp-branch-name", tooLongBranchName, ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -794,7 +794,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, truncatedBranch); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -806,12 +806,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: truncatedBranch, - base: "target", - title: "[target] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: truncatedBranch, + base: "target", + title: "[target] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -834,7 +834,7 @@ describe("cli runner", () => { "--strategy-option", "find-renames", ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -847,7 +847,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-11da4e3-0404fb9"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(0); expect(GitCLIService.prototype.cherryPick).toHaveBeenCalledTimes(2); @@ -859,12 +859,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-target-11da4e3-0404fb9", - base: "target", - title: "[target] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-target-11da4e3-0404fb9", + base: "target", + title: "[target] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/8632\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -886,7 +886,7 @@ describe("cli runner", () => { "--body", "New body" ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -899,7 +899,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledTimes(1); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-target-28f63db"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(0); expect(GitCLIService.prototype.cherryPick).toHaveBeenCalledTimes(1); @@ -910,12 +910,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-target-28f63db", - base: "target", - title: "[target] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-target-28f63db", + base: "target", + title: "[target] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/8632\r\n\r\nNew body", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -935,7 +935,7 @@ describe("cli runner", () => { "-f", "/tmp/folder" ]); - + await runner.execute(); const cwd = "/tmp/folder"; @@ -952,7 +952,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-v1-28f63db"); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-v2-28f63db"); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "bp-v3-28f63db"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(3); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -968,12 +968,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(3); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-v1-28f63db", - base: "v1", - title: "[v1] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-v1-28f63db", + base: "v1", + title: "[v1] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -981,12 +981,12 @@ describe("cli runner", () => { comments: [], }); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-v2-28f63db", - base: "v2", - title: "[v2] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-v2-28f63db", + base: "v2", + title: "[v2] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -994,12 +994,12 @@ describe("cli runner", () => { comments: [], }); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "bp-v3-28f63db", - base: "v3", - title: "[v3] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "bp-v3-28f63db", + base: "v3", + title: "[v3] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -1020,7 +1020,7 @@ describe("cli runner", () => { "--bp-branch-name", "custom1, custom1, custom2, custom3", ]); - + await runner.execute(); const cwd = "/tmp/folder"; @@ -1037,7 +1037,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "custom1"); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "custom2"); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "custom3"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(3); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -1053,12 +1053,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(3); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "custom1", - base: "v1", - title: "[v1] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "custom1", + base: "v1", + title: "[v1] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -1066,12 +1066,12 @@ describe("cli runner", () => { comments: [], }); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "custom2", - base: "v2", - title: "[v2] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "custom2", + base: "v2", + title: "[v2] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -1079,12 +1079,12 @@ describe("cli runner", () => { comments: [], }); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "custom3", - base: "v3", - title: "[v3] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "custom3", + base: "v3", + title: "[v3] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -1109,7 +1109,7 @@ describe("cli runner", () => { "--bp-branch-name", "custom-failure-head", ]); - + await expect(() => runner.execute()).rejects.toThrow("Failure occurred during one of the backports: [Error: Mocked error ; Error: Mocked error ; Error: Mocked error]"); const cwd = "/tmp/folder"; @@ -1126,7 +1126,7 @@ describe("cli runner", () => { expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "custom-failure-head-v1"); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "custom-failure-head-v2"); expect(GitCLIService.prototype.createLocalBranch).toHaveBeenCalledWith(cwd, "custom-failure-head-v3"); - + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(3); expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368"); @@ -1142,12 +1142,12 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(3); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "custom-failure-head-v1", - base: "v1", - title: "[v1] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "custom-failure-head-v1", + base: "v1", + title: "[v1] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -1155,12 +1155,12 @@ describe("cli runner", () => { comments: [], }); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "custom-failure-head-v2", - base: "v2", - title: "[v2] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "custom-failure-head-v2", + base: "v2", + title: "[v2] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -1168,12 +1168,12 @@ describe("cli runner", () => { comments: [], }); expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ - owner: "owner", - repo: "reponame", - cloneUrl: "https://codeberg.org/owner/reponame.git", - head: "custom-failure-head-v3", - base: "v3", - title: "[v3] PR Title", + owner: "owner", + repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", + head: "custom-failure-head-v3", + base: "v3", + title: "[v3] PR Title", body: "**Backport:** https://codeberg.org/owner/reponame/pulls/2368\r\n\r\nPlease review and merge", reviewers: ["gh-user", "that-s-a-user"], assignees: [], @@ -1195,7 +1195,7 @@ describe("cli runner", () => { "-pr", "https://codeberg.org/owner/reponame/pulls/8632" ]); - + await runner.execute(); expect(GitClientFactory.getOrCreate).toHaveBeenCalledTimes(1); @@ -1214,7 +1214,7 @@ describe("cli runner", () => { "-a", "mytoken" ]); - + await runner.execute(); expect(GitClientFactory.getOrCreate).toHaveBeenCalledTimes(1); @@ -1232,7 +1232,7 @@ describe("cli runner", () => { "-pr", "https://codeberg.org/owner/reponame/pulls/8632" ]); - + await runner.execute(); expect(GitClientFactory.getOrCreate).toHaveBeenCalledTimes(1); @@ -1248,7 +1248,7 @@ describe("cli runner", () => { "-pr", "https://codeberg.org/owner/reponame/pulls/2368" ]); - + await runner.execute(); const cwd = process.cwd() + "/bp"; @@ -1366,7 +1366,11 @@ git cherry-pick -m 1 --strategy=recursive --strategy-option=theirs 28f63db774185 git push origin custom-failure-head-v1 # the step below failed # codeberg.createPullRequest -${"```"}`); +${"```"} + +The backport to ${"`v1`"} will not be retried until this comment is deleted. + +`); expect(GitHubClient.prototype.createPullRequestComment).toHaveBeenCalledWith("https://codeberg.org/api/v1/repos/owner/reponame/pulls/2368", `The backport to ${"`v2`"} failed. Check the latest run for more details. Reconstruction of the attempted steps (beware that escaping may be missing): @@ -1378,7 +1382,11 @@ git cherry-pick -m 1 --strategy=recursive --strategy-option=theirs 28f63db774185 git push origin custom-failure-head-v2 # the step below failed # codeberg.createPullRequest -${"```"}`); +${"```"} + +The backport to ${"`v2`"} will not be retried until this comment is deleted. + +`); expect(GitHubClient.prototype.createPullRequestComment).toHaveBeenCalledWith("https://codeberg.org/api/v1/repos/owner/reponame/pulls/2368", `The backport to ${"`v3`"} failed. Check the latest run for more details. Reconstruction of the attempted steps (beware that escaping may be missing): @@ -1390,7 +1398,11 @@ git fetch origin pull/2368/head:pr/2368 git cherry-pick -m 1 --strategy=recursive --strategy-option=theirs 28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc git push origin custom-failure-head-v3 # codeberg.createPullRequest -${"```"}`); +${"```"} + +The backport to ${"`v3`"} will not be retried until this comment is deleted. + +`); createPullRequestSpy.mockReset(); cherryPickSpy.mockReset(); diff --git a/test/service/runner/cli-github-runner.test.ts b/test/service/runner/cli-github-runner.test.ts index 9d1ac23..0ac96df 100644 --- a/test/service/runner/cli-github-runner.test.ts +++ b/test/service/runner/cli-github-runner.test.ts @@ -1542,7 +1542,11 @@ git cherry-pick -m 1 --strategy=recursive --strategy-option=theirs 28f63db774185 git push origin custom-failure-head-v1 # the step below failed # github.createPullRequest -${"```"}`); +${"```"} + +The backport to ${"`v1`"} will not be retried until this comment is deleted. + +`); expect(GitHubClient.prototype.createPullRequestComment).toHaveBeenCalledWith("https://api.github.com/repos/owner/reponame/pulls/2368", `The backport to ${"`v2`"} failed. Check the latest run for more details. Reconstruction of the attempted steps (beware that escaping may be missing): @@ -1554,7 +1558,11 @@ git cherry-pick -m 1 --strategy=recursive --strategy-option=theirs 28f63db774185 git push origin custom-failure-head-v2 # the step below failed # github.createPullRequest -${"```"}`); +${"```"} + +The backport to ${"`v2`"} will not be retried until this comment is deleted. + +`); expect(GitHubClient.prototype.createPullRequestComment).toHaveBeenCalledWith("https://api.github.com/repos/owner/reponame/pulls/2368", `The backport to ${"`v3`"} failed. Check the latest run for more details. Reconstruction of the attempted steps (beware that escaping may be missing): @@ -1566,9 +1574,50 @@ git cherry-pick -m 1 --strategy=recursive --strategy-option=theirs 28f63db774185 git push origin custom-failure-head-v3 # the step below failed # github.createPullRequest -${"```"}`); +${"```"} + +The backport to ${"`v3`"} will not be retried until this comment is deleted. + +`); + + createPullRequestSpy.mockReset(); + }); + + test("skip target branch whose backport already failed and was reported", async () => { + const getCommentsSpy = jest.spyOn(GitHubClient.prototype, "getLatestPullRequestComments").mockResolvedValue([ + "just an unrelated comment", + "The backport to `v2` failed. Check the latest run for more details.\n\n", + ]); + const createPullRequestSpy = jest.spyOn(GitHubClient.prototype, "createPullRequest").mockImplementation((backport: BackportPullRequest) => { + throw new Error(`Mocked error: ${backport.base}`); + }); + + addProcessArgs([ + "-tb", + "v1, v2", + "-pr", + "https://github.com/owner/reponame/pull/2368", + "-f", + "/tmp/folder", + "--bp-branch-name", + "custom-failure-head", + "--enable-err-notification", + ]); + + // only v1 is attempted, hence only its failure is reported + await expect(() => runner.execute()).rejects.toThrow("Failure occurred during one of the backports: [Error: Mocked error: v1]"); + + expect(GitCLIService.prototype.clone).toHaveBeenCalledTimes(1); + expect(GitCLIService.prototype.clone).toHaveBeenCalledWith("https://github.com/owner/reponame.git", "/tmp/folder", "v1"); + + expect(GitHubClient.prototype.createPullRequestComment).toHaveBeenCalledTimes(1); + expect(GitHubClient.prototype.createPullRequestComment).toHaveBeenCalledWith( + "https://api.github.com/repos/owner/reponame/pulls/2368", + expect.stringContaining("") + ); createPullRequestSpy.mockReset(); + getCommentsSpy.mockRestore(); }); test("with some failures and dry run enabled", async () => { diff --git a/test/support/mock/git-client-mock-support.ts b/test/support/mock/git-client-mock-support.ts index 150b066..21906fc 100644 --- a/test/support/mock/git-client-mock-support.ts +++ b/test/support/mock/git-client-mock-support.ts @@ -17,6 +17,9 @@ const logger = LoggerServiceFactory.getLogger(); const listCommitsParams = (params: { owner: string; repo: string; pull_number: number }) => params as unknown as undefined; +const listCommentsParams = (params: { owner: string; repo: string }) => + params as unknown as undefined; + // AXIOS export const getAxiosMocked = (url: string) => { @@ -264,6 +267,18 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit => data: {} }); + // no previously reported backport failure + mock.rest.issues + .listComments(listCommentsParams({ + owner: TARGET_OWNER, + repo: REPO, + })) + .reply({ + repeat: REPEAT, + status: 200, + data: [] + }); + mock.rest.git .getCommit({ owner: TARGET_OWNER, @@ -458,6 +473,18 @@ export const mockCodebergClient = (apiUrl = "https://codeberg.org/api/v1"): Moct data: {} }); + // no previously reported backport failure + mock.rest.issues + .listComments(listCommentsParams({ + owner: CB_TARGET_OWNER, + repo: CB_REPO, + })) + .reply({ + repeat: REPEAT, + status: 200, + data: [] + }); + mock.rest.git .getCommit({ owner: CB_TARGET_OWNER,