From 9762a69224e0225f1f5c8e8e37737a68caaac4ff Mon Sep 17 00:00:00 2001 From: john spurling Date: Fri, 31 Jul 2026 09:02:37 +0000 Subject: [PATCH 1/2] feat: add --tb-repo to backport into a different target repository (#223) Adds a --tb-repo/tb-repo flag, mirroring the existing --bp-repo, so the backport pull request can be opened against a repository other than the one targeted by the original PR. This unblocks workflows like GraalVM's community LTS repos, where backports of oracle/graal PRs need to land in a separate downstream repo (e.g. graalvm/graalvm-community-jdk25u) rather than in a fork of the same repository. Unlike --bp-repo, which only changes where the branch is pushed, --tb-repo changes where the repo is cloned from and where the PR is ultimately opened. Because the original PR's commits generally don't exist in the history of an unrelated target repository, the runner now adds an "upstream" remote back to the original PR's repo and fetches the PR ref from there before cherry-picking, rather than assuming origin. Also made addRemote idempotent (update the URL if the remote already exists) since the working folder is reused across multiple target branches, which would otherwise make --tb-repo (and pre-existing --bp-repo) usage with multiple target branches fail on the second iteration. --- README.md | 18 +++ action.yml | 4 + dist/cli/index.js | 76 ++++++++--- dist/gha/index.js | 73 +++++++++-- src/service/args/args-parser.ts | 1 + src/service/args/args.types.ts | 1 + src/service/args/cli/cli-args-parser.ts | 2 + src/service/args/gha/gha-args-parser.ts | 1 + .../configs/pullrequest/pr-configs-parser.ts | 29 ++++- src/service/git/git-cli.ts | 10 +- src/service/git/git.types.ts | 1 + src/service/runner/runner.ts | 37 ++++-- test/service/args/cli/cli-args-parser.test.ts | 3 + test/service/args/gha/gha-args-parser.test.ts | 2 + .../github-pr-configs-parser-multiple.test.ts | 18 +++ .../github-pr-configs-parser.test.ts | 97 ++++++++++++++ .../gitlab-pr-configs-parser-multiple.test.ts | 12 ++ .../gitlab-pr-configs-parser.test.ts | 10 ++ test/service/git/git-cli.test.ts | 8 ++ test/service/git/gitlab/gitlab-client.test.ts | 7 ++ .../runner/cli-codeberg-runner.test.ts | 25 ++++ test/service/runner/cli-github-runner.test.ts | 119 ++++++++++++++++++ test/service/runner/cli-gitlab-runner.test.ts | 10 ++ test/service/runner/gha-github-runner.test.ts | 18 +++ test/service/runner/gha-gitlab-runner.test.ts | 10 ++ 25 files changed, 552 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index eb2c5de..fdddff9 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ This tool comes with some inputs that allow users to override the default behavi | No Reviewers Inheritance | --no-inherit-reviewers | N | Considered only if reviewers is empty, if true keep reviewers as empty list, otherwise inherit from original pull request | false | | Backport Branch Names | --bp-branch-name | N | Comma separated lists of the backporting pull request branch names, if they exceeds 250 chars they will be truncated | bp-{target-branch}-{sha1}...{shaN} | | Backport Repository | --bp-repo | N | Optional source repository (format owner/repo) where the backport branch is pushed, useful to open the PR from a fork | {target-owner}/{target-repo} | +| Target Repository | --tb-repo | N | Optional target repository (format owner/repo) where the backport pull request should be opened against, useful to backport to a different repository than the original one | {original-pr-target-owner}/{original-pr-target-repo} | | Labels | --labels | N | Provide custom labels to be added to the backporting pull request | [] | | Inherit labels | --inherit-labels | N | If enabled inherit lables from the original pull request | false | | No squash | --no-squash | N | Backport all commits found in the pull request. The default behavior is to only backport the first commit that was merged in the base branch. | | @@ -161,6 +162,23 @@ $ git-backporting -tb v1 -pr https://github.com/upstream/project/pull/123 -a *** In this mode you should provide a PAT with enough permissions on the fork repository. +#### Backport to a different target repository + +By default, the backport pull request is opened against the same repository targeted by the original pull request. +If you want to open the backport PR against a different repository altogether, set `--tb-repo` (or action input `tb-repo`) to `owner/repo`. The repository is cloned from and the backport branch is pushed to `--tb-repo` instead of the original pull request's repository. + +```bash +$ git-backporting -tb v1 -pr https://github.com/upstream/project/pull/123 -a ***** --tb-repo my-org/downstream-project +``` + +`--tb-repo` and `--bp-repo` can be combined: `--tb-repo` selects where the backport PR is opened, while `--bp-repo` selects the fork the backport branch is pushed from. + +```bash +$ git-backporting -tb v1 -pr https://github.com/upstream/project/pull/123 -a ***** --tb-repo my-org/downstream-project --bp-repo my-user/downstream-project +``` + +In this mode you should provide a PAT with enough permissions on the target repository (and on the fork, if `--bp-repo` is also used). + #### Configuration file example This is an example of a configuration file that can be used. diff --git a/action.yml b/action.yml index 60f3ba7..628f371 100644 --- a/action.yml +++ b/action.yml @@ -64,6 +64,10 @@ inputs: description: > Optional backport repository as owner/repo where the backport branch is pushed, useful to create PRs from a fork required: false + tb-repo: + description: > + Optional target repository as owner/repo where the backport pull request should be opened against, useful to backport to a different repository than the original one + required: false reviewers: description: > Comma separated list of reviewers for the backporting pull request diff --git a/dist/cli/index.js b/dist/cli/index.js index cf88b0b..35db393 100755 --- a/dist/cli/index.js +++ b/dist/cli/index.js @@ -61,6 +61,7 @@ class ArgsParser { bodyPrefix: this.getOrDefault(args.bodyPrefix), bpBranchName: this.getOrDefault(args.bpBranchName), bpRepo: this.getOrDefault(args.bpRepo), + tbRepo: this.getOrDefault(args.tbRepo), reviewers: this.getOrDefault(args.reviewers, []), assignees: this.getOrDefault(args.assignees, []), inheritReviewers: this.getOrDefault(args.inheritReviewers, true), @@ -202,6 +203,7 @@ class CLIArgsParser extends args_parser_1.default { .option("--body-prefix ", "backport pr body prefix, default `backport `") .option("--bp-branch-name ", "comma separated list of backport pr branch names, default auto-generated by the commit and target branch") .option("--bp-repo ", "optional backport repository where the branch should be pushed, e.g. my-fork/my-repo") + .option("--tb-repo ", "optional target repository where the backport pull request should be opened against, e.g. my-org/my-repo") .option("--reviewers ", "comma separated list of reviewers for the backporting pull request", args_utils_1.getAsCleanedCommaSeparatedList) .option("--assignees ", "comma separated list of assignees for the backporting pull request", args_utils_1.getAsCleanedCommaSeparatedList) .option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request") @@ -241,6 +243,7 @@ class CLIArgsParser extends args_parser_1.default { bodyPrefix: opts.bodyPrefix, bpBranchName: opts.bpBranchName, bpRepo: opts.bpRepo, + tbRepo: opts.tbRepo, reviewers: opts.reviewers, assignees: opts.assignees, inheritReviewers: opts.inheritReviewers, @@ -427,7 +430,7 @@ class PullRequestConfigsParser extends configs_parser_1.default { * @returns {GitPullRequest} */ generateBackportPullRequestsData(originalPullRequest, args, targetBranches, bpBranchNames) { - const targetRepo = originalPullRequest.targetRepo; + const targetRepo = this.getBackportTargetRepo(args.tbRepo, originalPullRequest.targetRepo); const sourceRepo = this.getBackportSourceRepo(args.bpRepo, targetRepo); const reviewers = args.reviewers ?? []; if (reviewers.length == 0 && args.inheritReviewers) { @@ -466,6 +469,7 @@ class PullRequestConfigsParser extends configs_parser_1.default { return { owner: targetRepo.owner, repo: targetRepo.project, + cloneUrl: targetRepo.cloneUrl, head: backportBranch, headRepo: sourceRepo, base: tb, @@ -483,12 +487,29 @@ class PullRequestConfigsParser extends configs_parser_1.default { if (!bpRepo || bpRepo.trim() === "") { return undefined; } - const sanitized = bpRepo.trim(); + return this.parseRepo(bpRepo, "bp", targetRepo); + } + getBackportTargetRepo(tbRepo, targetRepo) { + if (!tbRepo || tbRepo.trim() === "") { + return targetRepo; + } + return this.parseRepo(tbRepo, "tb", targetRepo); + } + /** + * Parse a "owner/repo" formatted repository override and derive its clone url + * by reusing the scheme/host of the provided reference repository + * @param repo owner/repo formatted repository override + * @param optionName name of the option the override came from, used in the error message + * @param referenceRepo repository whose clone url is used to derive the scheme/host + * @returns {GitRepository} + */ + parseRepo(repo, optionName, referenceRepo) { + const sanitized = repo.trim(); const parts = sanitized.split("/").map(p => p.trim()).filter(p => p.length > 0); if (parts.length < 2) { - throw new Error(`Invalid bp repo format "${bpRepo}", expected "owner/repo"`); + throw new Error(`Invalid ${optionName} repo format "${repo}", expected "owner/repo"`); } - const cloneUrl = new URL(targetRepo.cloneUrl); + const cloneUrl = new URL(referenceRepo.cloneUrl); cloneUrl.pathname = `/${parts.join("/")}.git`; return { owner: parts[0], @@ -588,13 +609,20 @@ class GitCLIService { await this.git(cwd).checkoutLocalBranch(newBranch); } /** - * Add a new remote to the current repository + * Add a new remote to the current repository, or update its url if a remote + * with the same name already exists, e.g., because the working folder is + * reused across multiple backports * @param cwd repository in which addRemote should be performed * @param remote remote git link * @param remoteName [optional] name of the remote, by default 'fork' is used */ async addRemote(cwd, remote, remoteName = "fork") { this.logger.info(`Adding new remote ${remote}`); + const existingRemotes = await this.git(cwd).getRemotes(); + if (existingRemotes.some(r => r.name === remoteName)) { + await this.git(cwd).remote(["set-url", remoteName, this.remoteWithAuth(remote)]); + return; + } await this.git(cwd).addRemote(remoteName, this.remoteWithAuth(remote)); } /** @@ -1732,26 +1760,46 @@ class Runner { exports["default"] = Runner; 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), + // in which case the original pr's commits are not reachable from a clone of the backport target repo alone + const usingDifferentTargetRepo = backportPR.cloneUrl !== configs.originalPullRequest.targetRepo.cloneUrl; // 4. clone the repository yield async () => { logger.debug("Cloning repo.."); - await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base); + await git.gitCli.clone(backportPR.cloneUrl, configs.folder, backportPR.base); }; // 5. create new branch from target one and checkout yield async () => { logger.debug("Creating local branch.."); await git.gitCli.createLocalBranch(configs.folder, backportPR.head); }; - // 6. fetch pull request remote if source owner != target owner or pull request still open - if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner || + let commitsRemote = undefined; + if (usingDifferentTargetRepo) { + // 6. add a remote pointing to the original pull request's repository, needed to fetch + // commits that only exist there, since the backport target repo won't have them + commitsRemote = "upstream"; + yield async () => { + await git.gitCli.addRemote(configs.folder, configs.originalPullRequest.targetRepo.cloneUrl, commitsRemote); + }; + } + // 7. fetch pull request remote if source owner != target owner, pull request still open, + // or backporting to a different repository than the original pull request's one + if (usingDifferentTargetRepo || + configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner || configs.originalPullRequest.state === "open") { yield async () => { logger.debug("Fetching pull request remote.."); const prefix = git.gitClientType === git_types_1.GitClientType.GITLAB ? "merge-requests" : "pull"; // default is for gitlab - await git.gitCli.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); + const ref = `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`; + if (commitsRemote) { + await git.gitCli.fetch(configs.folder, ref, commitsRemote); + } + else { + await git.gitCli.fetch(configs.folder, ref); + } }; } - // 7. apply all changes to the new branch + // 8. apply all changes to the new branch yield async () => { logger.debug("Cherry picking commits.."); }; @@ -1762,18 +1810,18 @@ function* backportSteps(logger, configs, backportPR, git) { } let target_remote = undefined; if (backportPR.headRepo) { - // 8. add fork-remote to push backport branch to + // 9. add fork-remote to push backport branch to target_remote = "fork"; yield async () => { await git.gitCli.addRemote(configs.folder, backportPR.headRepo.cloneUrl, target_remote); }; } if (!configs.dryRun) { - // 9. push the new branch to origin + // 10. push the new branch to origin yield async () => { await git.gitCli.push(configs.folder, backportPR.head, target_remote); }; - // 10. create pull request new branch -> target branch (using octokit) + // 11. create pull request new branch -> target branch (using octokit) yield async () => { const prUrl = await git.gitClientApi.createPullRequest(backportPR); logger.info(`Pull request created: ${prUrl}`); @@ -25517,7 +25565,7 @@ const dist_src_Octokit = Octokit.plugin(requestLog, legacyRestEndpointMethods, p /***/ ((module) => { "use strict"; -module.exports = JSON.parse('{"name":"@kie/git-backporting","version":"4.10.0","description":"Git backporting is a tool to execute automatic pull request git backporting.","author":"","license":"MIT","private":false,"main":"./dist/gha/index.js","bin":{"git-backporting":"./dist/cli/index.js"},"files":["dist/cli/index.js"],"publishConfig":{"access":"public"},"scripts":{"prepare":"husky install","clean":"rm -rf ./build ./dist","compile":"tsc -p tsconfig.json && tsc-alias -p tsconfig.json","package":"npm run package:cli && npm run package:gha","package:cli":"ncc build ./build/src/bin/cli.js -o dist/cli","package:gha":"ncc build ./build/src/bin/gha.js -o dist/gha","build":"npm run clean && npm run compile && npm run package","test":"jest --silent","test:report":"npm test -- --coverage --testResultsProcessor=jest-sonar-reporter","lint":"eslint . --ext .ts","lint:fix":"npm run lint -- --fix","ts-node":"ts-node","postversion":"npm run build && git add dist && rm -rf build","release":"release-it","release:branch":"git checkout -b release/$(release-it --release-version) main","release:prepare":"release-it --no-npm.publish --no-github.release --no-git.push --no-git.tag --no-git.requireUpstream","release:prepare:all":"npm run release:branch && npm run release:prepare"},"repository":{"type":"git","url":"git+https://github.com/kiegroup/git-backporting.git"},"keywords":["backporting","pull-requests","merge-requests","github-action","cherry-pick"],"bugs":{"url":"https://github.com/kiegroup/git-backporting/issues"},"homepage":"https://github.com/kiegroup/git-backporting#readme","devDependencies":{"@commitlint/cli":"^20.1.0","@commitlint/config-conventional":"^20.0.0","@gitbeaker/rest":"^39.1.0","@kie/mock-github":"^2.0.2","@octokit/webhooks-types":"^6.8.0","@release-it/conventional-changelog":"^10.0.0","@types/fs-extra":"^9.0.13","@types/jest":"^29.2.4","@types/node":"^18.11.17","@typescript-eslint/eslint-plugin":"^5.47.0","@typescript-eslint/parser":"^5.47.0","@vercel/ncc":"^0.36.0","conventional-commits-parser":"^6.2.0","eslint":"^8.30.0","husky":"^8.0.2","jest":"^30.0.0","jest-sonar-reporter":"^2.0.0","release-it":"^19.0.6","semver":"^7.3.8","ts-jest":"^29.4.5","ts-node":"^10.8.1","tsc-alias":"^1.8.2","tsconfig-paths":"^4.1.0","typescript":"^4.9.3"},"dependencies":{"@actions/core":"^1.10.0","@octokit/rest":"22.0.1","axios":"^1.4.0","commander":"^9.3.0","fs-extra":"^11.1.0","https":"^1.0.0","simple-git":"^3.15.1"},"overrides":{"conventional-changelog-conventionalcommits":"^8.0.0","undici":"6.28.0","brace-expansion":"5.0.8"}}'); +module.exports = JSON.parse('{"name":"@kie/git-backporting","version":"4.10.0","description":"Git backporting is a tool to execute automatic pull request git backporting.","author":"","license":"MIT","private":false,"main":"./dist/gha/index.js","bin":{"git-backporting":"./dist/cli/index.js"},"files":["dist/cli/index.js"],"publishConfig":{"access":"public"},"scripts":{"prepare":"husky install","clean":"rm -rf ./build ./dist","compile":"tsc -p tsconfig.json && tsc-alias -p tsconfig.json","package":"npm run package:cli && npm run package:gha","package:cli":"ncc build ./build/src/bin/cli.js -o dist/cli","package:gha":"ncc build ./build/src/bin/gha.js -o dist/gha","build":"npm run clean && npm run compile && npm run package","test":"jest --silent","test:report":"npm test -- --coverage --testResultsProcessor=jest-sonar-reporter","lint":"eslint . --ext .ts","lint:fix":"npm run lint -- --fix","ts-node":"ts-node","postversion":"npm run build && git add dist && rm -rf build","release":"release-it","release:branch":"git checkout -b release/$(release-it --release-version) main","release:prepare":"release-it --no-npm.publish --no-github.release --no-git.push --no-git.tag --no-git.requireUpstream","release:prepare:all":"npm run release:branch && npm run release:prepare"},"repository":{"type":"git","url":"git+https://github.com/kiegroup/git-backporting.git"},"keywords":["backporting","pull-requests","merge-requests","github-action","cherry-pick"],"bugs":{"url":"https://github.com/kiegroup/git-backporting/issues"},"homepage":"https://github.com/kiegroup/git-backporting#readme","devDependencies":{"@commitlint/cli":"^20.1.0","@commitlint/config-conventional":"^20.0.0","@gitbeaker/rest":"^39.1.0","@kie/mock-github":"^2.0.2","@octokit/webhooks-types":"^6.8.0","@release-it/conventional-changelog":"^10.0.0","@types/fs-extra":"^9.0.13","@types/jest":"^29.2.4","@types/node":"^18.11.17","@typescript-eslint/eslint-plugin":"^5.47.0","@typescript-eslint/parser":"^5.47.0","@vercel/ncc":"^0.36.0","conventional-commits-parser":"^6.2.0","eslint":"^8.30.0","husky":"^8.0.2","jest":"^30.0.0","jest-sonar-reporter":"^2.0.0","release-it":"^19.0.6","semver":"^7.3.8","ts-jest":"^29.4.5","ts-node":"^10.8.1","tsc-alias":"^1.8.2","tsconfig-paths":"^4.1.0","typescript":"^4.9.3"},"dependencies":{"@actions/core":"^1.10.0","@octokit/rest":"22.0.1","axios":"^1.4.0","commander":"^9.3.0","fs-extra":"^11.1.0","https":"^1.0.0","simple-git":"^3.15.1"},"overrides":{"conventional-changelog-conventionalcommits":"^8.0.0","undici":"6.28.0","brace-expansion":"5.0.8"},"allowScripts":{"unrs-resolver@1.11.1":true,"husky@8.0.2":true}}'); /***/ }), diff --git a/dist/gha/index.js b/dist/gha/index.js index 1632e33..db46c4e 100755 --- a/dist/gha/index.js +++ b/dist/gha/index.js @@ -61,6 +61,7 @@ class ArgsParser { bodyPrefix: this.getOrDefault(args.bodyPrefix), bpBranchName: this.getOrDefault(args.bpBranchName), bpRepo: this.getOrDefault(args.bpRepo), + tbRepo: this.getOrDefault(args.tbRepo), reviewers: this.getOrDefault(args.reviewers, []), assignees: this.getOrDefault(args.assignees, []), inheritReviewers: this.getOrDefault(args.inheritReviewers, true), @@ -205,6 +206,7 @@ class GHAArgsParser extends args_parser_1.default { bodyPrefix: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("body-prefix", { trimWhitespace: false })), bpBranchName: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("bp-branch-name")), bpRepo: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("bp-repo")), + tbRepo: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("tb-repo")), reviewers: (0, args_utils_1.getAsCleanedCommaSeparatedList)((0, core_1.getInput)("reviewers")), assignees: (0, args_utils_1.getAsCleanedCommaSeparatedList)((0, core_1.getInput)("assignees")), inheritReviewers: !(0, args_utils_1.getAsBooleanOrUndefined)((0, core_1.getInput)("no-inherit-reviewers")), @@ -391,7 +393,7 @@ class PullRequestConfigsParser extends configs_parser_1.default { * @returns {GitPullRequest} */ generateBackportPullRequestsData(originalPullRequest, args, targetBranches, bpBranchNames) { - const targetRepo = originalPullRequest.targetRepo; + const targetRepo = this.getBackportTargetRepo(args.tbRepo, originalPullRequest.targetRepo); const sourceRepo = this.getBackportSourceRepo(args.bpRepo, targetRepo); const reviewers = args.reviewers ?? []; if (reviewers.length == 0 && args.inheritReviewers) { @@ -430,6 +432,7 @@ class PullRequestConfigsParser extends configs_parser_1.default { return { owner: targetRepo.owner, repo: targetRepo.project, + cloneUrl: targetRepo.cloneUrl, head: backportBranch, headRepo: sourceRepo, base: tb, @@ -447,12 +450,29 @@ class PullRequestConfigsParser extends configs_parser_1.default { if (!bpRepo || bpRepo.trim() === "") { return undefined; } - const sanitized = bpRepo.trim(); + return this.parseRepo(bpRepo, "bp", targetRepo); + } + getBackportTargetRepo(tbRepo, targetRepo) { + if (!tbRepo || tbRepo.trim() === "") { + return targetRepo; + } + return this.parseRepo(tbRepo, "tb", targetRepo); + } + /** + * Parse a "owner/repo" formatted repository override and derive its clone url + * by reusing the scheme/host of the provided reference repository + * @param repo owner/repo formatted repository override + * @param optionName name of the option the override came from, used in the error message + * @param referenceRepo repository whose clone url is used to derive the scheme/host + * @returns {GitRepository} + */ + parseRepo(repo, optionName, referenceRepo) { + const sanitized = repo.trim(); const parts = sanitized.split("/").map(p => p.trim()).filter(p => p.length > 0); if (parts.length < 2) { - throw new Error(`Invalid bp repo format "${bpRepo}", expected "owner/repo"`); + throw new Error(`Invalid ${optionName} repo format "${repo}", expected "owner/repo"`); } - const cloneUrl = new URL(targetRepo.cloneUrl); + const cloneUrl = new URL(referenceRepo.cloneUrl); cloneUrl.pathname = `/${parts.join("/")}.git`; return { owner: parts[0], @@ -552,13 +572,20 @@ class GitCLIService { await this.git(cwd).checkoutLocalBranch(newBranch); } /** - * Add a new remote to the current repository + * Add a new remote to the current repository, or update its url if a remote + * with the same name already exists, e.g., because the working folder is + * reused across multiple backports * @param cwd repository in which addRemote should be performed * @param remote remote git link * @param remoteName [optional] name of the remote, by default 'fork' is used */ async addRemote(cwd, remote, remoteName = "fork") { this.logger.info(`Adding new remote ${remote}`); + const existingRemotes = await this.git(cwd).getRemotes(); + if (existingRemotes.some(r => r.name === remoteName)) { + await this.git(cwd).remote(["set-url", remoteName, this.remoteWithAuth(remote)]); + return; + } await this.git(cwd).addRemote(remoteName, this.remoteWithAuth(remote)); } /** @@ -1696,26 +1723,46 @@ class Runner { exports["default"] = Runner; 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), + // in which case the original pr's commits are not reachable from a clone of the backport target repo alone + const usingDifferentTargetRepo = backportPR.cloneUrl !== configs.originalPullRequest.targetRepo.cloneUrl; // 4. clone the repository yield async () => { logger.debug("Cloning repo.."); - await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base); + await git.gitCli.clone(backportPR.cloneUrl, configs.folder, backportPR.base); }; // 5. create new branch from target one and checkout yield async () => { logger.debug("Creating local branch.."); await git.gitCli.createLocalBranch(configs.folder, backportPR.head); }; - // 6. fetch pull request remote if source owner != target owner or pull request still open - if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner || + let commitsRemote = undefined; + if (usingDifferentTargetRepo) { + // 6. add a remote pointing to the original pull request's repository, needed to fetch + // commits that only exist there, since the backport target repo won't have them + commitsRemote = "upstream"; + yield async () => { + await git.gitCli.addRemote(configs.folder, configs.originalPullRequest.targetRepo.cloneUrl, commitsRemote); + }; + } + // 7. fetch pull request remote if source owner != target owner, pull request still open, + // or backporting to a different repository than the original pull request's one + if (usingDifferentTargetRepo || + configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner || configs.originalPullRequest.state === "open") { yield async () => { logger.debug("Fetching pull request remote.."); const prefix = git.gitClientType === git_types_1.GitClientType.GITLAB ? "merge-requests" : "pull"; // default is for gitlab - await git.gitCli.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); + const ref = `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`; + if (commitsRemote) { + await git.gitCli.fetch(configs.folder, ref, commitsRemote); + } + else { + await git.gitCli.fetch(configs.folder, ref); + } }; } - // 7. apply all changes to the new branch + // 8. apply all changes to the new branch yield async () => { logger.debug("Cherry picking commits.."); }; @@ -1726,18 +1773,18 @@ function* backportSteps(logger, configs, backportPR, git) { } let target_remote = undefined; if (backportPR.headRepo) { - // 8. add fork-remote to push backport branch to + // 9. add fork-remote to push backport branch to target_remote = "fork"; yield async () => { await git.gitCli.addRemote(configs.folder, backportPR.headRepo.cloneUrl, target_remote); }; } if (!configs.dryRun) { - // 9. push the new branch to origin + // 10. push the new branch to origin yield async () => { await git.gitCli.push(configs.folder, backportPR.head, target_remote); }; - // 10. create pull request new branch -> target branch (using octokit) + // 11. create pull request new branch -> target branch (using octokit) yield async () => { const prUrl = await git.gitClientApi.createPullRequest(backportPR); logger.info(`Pull request created: ${prUrl}`); diff --git a/src/service/args/args-parser.ts b/src/service/args/args-parser.ts index d9c4d28..cf30377 100644 --- a/src/service/args/args-parser.ts +++ b/src/service/args/args-parser.ts @@ -39,6 +39,7 @@ export default abstract class ArgsParser { bodyPrefix: this.getOrDefault(args.bodyPrefix), bpBranchName: this.getOrDefault(args.bpBranchName), bpRepo: this.getOrDefault(args.bpRepo), + tbRepo: this.getOrDefault(args.tbRepo), reviewers: this.getOrDefault(args.reviewers, []), assignees: this.getOrDefault(args.assignees, []), inheritReviewers: this.getOrDefault(args.inheritReviewers, true), diff --git a/src/service/args/args.types.ts b/src/service/args/args.types.ts index 82e7ae8..68145b6 100644 --- a/src/service/args/args.types.ts +++ b/src/service/args/args.types.ts @@ -18,6 +18,7 @@ export interface Args { // NOTE: keep bpBranchName as singular and of type string for backward compatibilities bpBranchName?: string, // comma separated list of backport pr branch names, default computed from commit and target branches bpRepo?: string, // optional backport repository as owner/repo where the branch will be pushed + tbRepo?: string, // optional target repository as owner/repo where the backport pr should be opened against reviewers?: string[], // backport pr reviewers assignees?: string[], // backport pr assignees inheritReviewers?: boolean, // if true and reviewers == [] then inherit reviewers from original pr diff --git a/src/service/args/cli/cli-args-parser.ts b/src/service/args/cli/cli-args-parser.ts index fbaaceb..2374981 100644 --- a/src/service/args/cli/cli-args-parser.ts +++ b/src/service/args/cli/cli-args-parser.ts @@ -24,6 +24,7 @@ export default class CLIArgsParser extends ArgsParser { .option("--body-prefix ", "backport pr body prefix, default `backport `") .option("--bp-branch-name ", "comma separated list of backport pr branch names, default auto-generated by the commit and target branch") .option("--bp-repo ", "optional backport repository where the branch should be pushed, e.g. my-fork/my-repo") + .option("--tb-repo ", "optional target repository where the backport pull request should be opened against, e.g. my-org/my-repo") .option("--reviewers ", "comma separated list of reviewers for the backporting pull request", getAsCleanedCommaSeparatedList) .option("--assignees ", "comma separated list of assignees for the backporting pull request", getAsCleanedCommaSeparatedList) .option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request") @@ -64,6 +65,7 @@ export default class CLIArgsParser extends ArgsParser { bodyPrefix: opts.bodyPrefix, bpBranchName: opts.bpBranchName, bpRepo: opts.bpRepo, + tbRepo: opts.tbRepo, reviewers: opts.reviewers, assignees: opts.assignees, inheritReviewers: opts.inheritReviewers, diff --git a/src/service/args/gha/gha-args-parser.ts b/src/service/args/gha/gha-args-parser.ts index 79053b2..f3892bd 100644 --- a/src/service/args/gha/gha-args-parser.ts +++ b/src/service/args/gha/gha-args-parser.ts @@ -27,6 +27,7 @@ export default class GHAArgsParser extends ArgsParser { bodyPrefix: getOrUndefined(getInput("body-prefix", { trimWhitespace: false })), bpBranchName: getOrUndefined(getInput("bp-branch-name")), bpRepo: getOrUndefined(getInput("bp-repo")), + tbRepo: getOrUndefined(getInput("tb-repo")), reviewers: getAsCleanedCommaSeparatedList(getInput("reviewers")), assignees: getAsCleanedCommaSeparatedList(getInput("assignees")), inheritReviewers: !getAsBooleanOrUndefined(getInput("no-inherit-reviewers")), diff --git a/src/service/configs/pullrequest/pr-configs-parser.ts b/src/service/configs/pullrequest/pr-configs-parser.ts index 72da3cc..4a8f2f1 100644 --- a/src/service/configs/pullrequest/pr-configs-parser.ts +++ b/src/service/configs/pullrequest/pr-configs-parser.ts @@ -116,7 +116,7 @@ export default class PullRequestConfigsParser extends ConfigsParser { bpBranchNames: string[] ): BackportPullRequest[] { - const targetRepo = originalPullRequest.targetRepo; + const targetRepo = this.getBackportTargetRepo(args.tbRepo, originalPullRequest.targetRepo); const sourceRepo = this.getBackportSourceRepo(args.bpRepo, targetRepo); const reviewers = args.reviewers ?? []; @@ -161,6 +161,7 @@ export default class PullRequestConfigsParser extends ConfigsParser { return { owner: targetRepo.owner, repo: targetRepo.project, + cloneUrl: targetRepo.cloneUrl, head: backportBranch, headRepo: sourceRepo, base: tb, @@ -180,13 +181,33 @@ export default class PullRequestConfigsParser extends ConfigsParser { return undefined; } - const sanitized = bpRepo.trim(); + return this.parseRepo(bpRepo, "bp", targetRepo); + } + + private getBackportTargetRepo(tbRepo: string | undefined, targetRepo: GitRepository): GitRepository { + if (!tbRepo || tbRepo.trim() === "") { + return targetRepo; + } + + return this.parseRepo(tbRepo, "tb", targetRepo); + } + + /** + * Parse a "owner/repo" formatted repository override and derive its clone url + * by reusing the scheme/host of the provided reference repository + * @param repo owner/repo formatted repository override + * @param optionName name of the option the override came from, used in the error message + * @param referenceRepo repository whose clone url is used to derive the scheme/host + * @returns {GitRepository} + */ + private parseRepo(repo: string, optionName: string, referenceRepo: GitRepository): GitRepository { + const sanitized = repo.trim(); const parts = sanitized.split("/").map(p => p.trim()).filter(p => p.length > 0); if (parts.length < 2) { - throw new Error(`Invalid bp repo format "${bpRepo}", expected "owner/repo"`); + throw new Error(`Invalid ${optionName} repo format "${repo}", expected "owner/repo"`); } - const cloneUrl = new URL(targetRepo.cloneUrl); + const cloneUrl = new URL(referenceRepo.cloneUrl); cloneUrl.pathname = `/${parts.join("/")}.git`; return { diff --git a/src/service/git/git-cli.ts b/src/service/git/git-cli.ts index 02d51e3..dc82ef5 100644 --- a/src/service/git/git-cli.ts +++ b/src/service/git/git-cli.ts @@ -93,13 +93,21 @@ export default class GitCLIService { } /** - * Add a new remote to the current repository + * Add a new remote to the current repository, or update its url if a remote + * with the same name already exists, e.g., because the working folder is + * reused across multiple backports * @param cwd repository in which addRemote should be performed * @param remote remote git link * @param remoteName [optional] name of the remote, by default 'fork' is used */ async addRemote(cwd: string, remote: string, remoteName = "fork"): Promise { this.logger.info(`Adding new remote ${remote}`); + const existingRemotes = await this.git(cwd).getRemotes(); + if (existingRemotes.some(r => r.name === remoteName)) { + await this.git(cwd).remote(["set-url", remoteName, this.remoteWithAuth(remote)]); + return; + } + await this.git(cwd).addRemote(remoteName, this.remoteWithAuth(remote)); } diff --git a/src/service/git/git.types.ts b/src/service/git/git.types.ts index 965e097..2a0bfdc 100644 --- a/src/service/git/git.types.ts +++ b/src/service/git/git.types.ts @@ -27,6 +27,7 @@ export interface GitRepository { export interface BackportPullRequest { owner: string, // repository's owner repo: string, // repository's name + cloneUrl: string, // clone url of the target repository (owner/repo above) head: string, // name of the source branch headRepo?: GitRepository, // optional: source repository, for cross-repository pull requests base: string, // name of the target branch diff --git a/src/service/runner/runner.ts b/src/service/runner/runner.ts index 0ab367d..df917d7 100644 --- a/src/service/runner/runner.ts +++ b/src/service/runner/runner.ts @@ -156,10 +156,14 @@ export default class Runner { function* backportSteps(logger: Pick, configs: Configs, backportPR: BackportPullRequest, git: Git): Generator<() => Promise, void, unknown> { // 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), + // in which case the original pr's commits are not reachable from a clone of the backport target repo alone + const usingDifferentTargetRepo = backportPR.cloneUrl !== configs.originalPullRequest.targetRepo.cloneUrl; + // 4. clone the repository yield async () => { logger.debug("Cloning repo.."); - await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base); + await git.gitCli.clone(backportPR.cloneUrl, configs.folder, backportPR.base); }; // 5. create new branch from target one and checkout @@ -168,17 +172,34 @@ function* backportSteps(logger: Pick, await git.gitCli.createLocalBranch(configs.folder, backportPR.head); }; - // 6. fetch pull request remote if source owner != target owner or pull request still open - if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner || + let commitsRemote: string | undefined = undefined; + if (usingDifferentTargetRepo) { + // 6. add a remote pointing to the original pull request's repository, needed to fetch + // commits that only exist there, since the backport target repo won't have them + commitsRemote = "upstream"; + yield async () => { + await git.gitCli.addRemote(configs.folder, configs.originalPullRequest.targetRepo.cloneUrl, commitsRemote); + }; + } + + // 7. fetch pull request remote if source owner != target owner, pull request still open, + // or backporting to a different repository than the original pull request's one + if (usingDifferentTargetRepo || + configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner || configs.originalPullRequest.state === "open") { yield async () => { logger.debug("Fetching pull request remote.."); const prefix = git.gitClientType === GitClientType.GITLAB ? "merge-requests" : "pull"; // default is for gitlab - await git.gitCli.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); + const ref = `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`; + if (commitsRemote) { + await git.gitCli.fetch(configs.folder, ref, commitsRemote); + } else { + await git.gitCli.fetch(configs.folder, ref); + } }; } - // 7. apply all changes to the new branch + // 8. apply all changes to the new branch yield async () => { logger.debug("Cherry picking commits.."); }; @@ -191,7 +212,7 @@ function* backportSteps(logger: Pick, let target_remote: string | undefined = undefined; if (backportPR.headRepo) { - // 8. add fork-remote to push backport branch to + // 9. add fork-remote to push backport branch to target_remote = "fork"; yield async () => { await git.gitCli.addRemote(configs.folder, backportPR.headRepo!.cloneUrl, target_remote); @@ -199,12 +220,12 @@ function* backportSteps(logger: Pick, } if (!configs.dryRun) { - // 9. push the new branch to origin + // 10. push the new branch to origin yield async () => { await git.gitCli.push(configs.folder, backportPR.head, target_remote); }; - // 10. create pull request new branch -> target branch (using octokit) + // 11. create pull request new branch -> target branch (using octokit) yield async () => { const prUrl = await git.gitClientApi.createPullRequest(backportPR); logger.info(`Pull request created: ${prUrl}`); diff --git a/test/service/args/cli/cli-args-parser.test.ts b/test/service/args/cli/cli-args-parser.test.ts index ff35305..a964648 100644 --- a/test/service/args/cli/cli-args-parser.test.ts +++ b/test/service/args/cli/cli-args-parser.test.ts @@ -244,6 +244,8 @@ describe("cli args parser", () => { "bp_branch_name", "--bp-repo", "fork-user/reponame", + "--tb-repo", + "target-org/reponame", "--reviewers", "al , john, jack", "--assignees", @@ -268,6 +270,7 @@ describe("cli args parser", () => { expect(args.bodyPrefix).toEqual("New Body Prefix"); expect(args.bpBranchName).toEqual("bp_branch_name"); expect(args.bpRepo).toEqual("fork-user/reponame"); + expect(args.tbRepo).toEqual("target-org/reponame"); expectArrayEqual(args.reviewers!, ["al", "john", "jack"]); expectArrayEqual(args.assignees!, ["pippo", "pluto", "paperino"]); expect(args.inheritReviewers).toEqual(false); diff --git a/test/service/args/gha/gha-args-parser.test.ts b/test/service/args/gha/gha-args-parser.test.ts index 0469aa0..0744e67 100644 --- a/test/service/args/gha/gha-args-parser.test.ts +++ b/test/service/args/gha/gha-args-parser.test.ts @@ -88,6 +88,7 @@ describe("gha args parser", () => { "body-prefix": "New Body Prefix", "bp-branch-name": "bp_branch_name", "bp-repo": "fork-user/reponame", + "tb-repo": "target-org/reponame", "reviewers": "al , john, jack", "assignees": " pippo,pluto, paperino", "no-inherit-reviewers": "true", @@ -108,6 +109,7 @@ describe("gha args parser", () => { expect(args.bodyPrefix).toEqual("New Body Prefix"); expect(args.bpBranchName).toEqual("bp_branch_name"); expect(args.bpRepo).toEqual("fork-user/reponame"); + expect(args.tbRepo).toEqual("target-org/reponame"); expectArrayEqual(args.reviewers!, ["al", "john", "jack"]); expectArrayEqual(args.assignees!, ["pippo", "pluto", "paperino"]); expect(args.inheritReviewers).toEqual(false); diff --git a/test/service/configs/pullrequest/github-pr-configs-parser-multiple.test.ts b/test/service/configs/pullrequest/github-pr-configs-parser-multiple.test.ts index 2141633..f4dbe41 100644 --- a/test/service/configs/pullrequest/github-pr-configs-parser-multiple.test.ts +++ b/test/service/configs/pullrequest/github-pr-configs-parser-multiple.test.ts @@ -70,6 +70,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v1-28f63db", base: "v1", title: "New Title", @@ -82,6 +83,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v2-28f63db", base: "v2", title: "New Title", @@ -94,6 +96,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v3-28f63db", base: "v3", title: "New Title", @@ -142,6 +145,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v1-28f63db", base: "v1", title: "New Title", @@ -154,6 +158,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v2-28f63db", base: "v2", title: "New Title", @@ -166,6 +171,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v3-28f63db", base: "v3", title: "New Title", @@ -215,6 +221,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-branch-v1", base: "v1", title: "New Title", @@ -227,6 +234,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-branch-v2", base: "v2", title: "New Title", @@ -239,6 +247,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-branch-v3", base: "v3", title: "New Title", @@ -288,6 +297,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-branch1", base: "v1", title: "New Title", @@ -300,6 +310,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-branch2", base: "v2", title: "New Title", @@ -312,6 +323,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-branch3", base: "v3", title: "New Title", @@ -382,6 +394,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v4-0404fb9-11da4e3", base: "v4", title: "[v4] PR Title", @@ -394,6 +407,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v5-0404fb9-11da4e3", base: "v5", title: "[v5] PR Title", @@ -406,6 +420,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v6-0404fb9-11da4e3", base: "v6", title: "[v6] PR Title", @@ -453,6 +468,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v1-0404fb9-11da4e3", base: "v1", title: "[v1] PR Title", @@ -465,6 +481,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v2-0404fb9-11da4e3", base: "v2", title: "[v2] PR Title", @@ -477,6 +494,7 @@ describe("github pull request config parser", () => { { owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v3-0404fb9-11da4e3", base: "v3", title: "[v3] PR Title", diff --git a/test/service/configs/pullrequest/github-pr-configs-parser.test.ts b/test/service/configs/pullrequest/github-pr-configs-parser.test.ts index ba83b50..207272e 100644 --- a/test/service/configs/pullrequest/github-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/github-pr-configs-parser.test.ts @@ -130,6 +130,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-prod-28f63db", base: "prod", title: "[prod] PR Title", @@ -303,6 +304,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-branch", base: "prod", title: "New Title", @@ -345,6 +347,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-prod-28f63db", base: "prod", title: "New Title", @@ -417,6 +420,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-prod-28f63db", base: "prod", title: "New Title", @@ -489,6 +493,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-prod-28f63db", base: "prod", title: "New Title", @@ -563,6 +568,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-prod-28f63db", base: "prod", title: "New Title", @@ -593,6 +599,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-prod-28f63db", headRepo: { cloneUrl: "https://github.com/my-fork/reponame.git", @@ -609,6 +616,91 @@ describe("github pull request config parser", () => { }); }); + test("override backport target repository", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "prod", + tbRepo: "target-org/reponame", + gitUser: "Me", + gitEmail: "me@email.com", + reviewers: [], + assignees: [], + inheritReviewers: false, + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(configs.backportPullRequests[0]).toEqual({ + owner: "target-org", + repo: "reponame", + cloneUrl: "https://github.com/target-org/reponame.git", + head: "bp-prod-28f63db", + base: "prod", + title: "[prod] PR Title", + body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge", + reviewers: [], + assignees: [], + labels: [], + comments: [], + }); + }); + + test("override backport target and source repository", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "prod", + tbRepo: "target-org/reponame", + bpRepo: "my-fork/reponame", + gitUser: "Me", + gitEmail: "me@email.com", + reviewers: [], + assignees: [], + inheritReviewers: false, + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(configs.backportPullRequests[0]).toEqual({ + owner: "target-org", + repo: "reponame", + cloneUrl: "https://github.com/target-org/reponame.git", + head: "bp-prod-28f63db", + headRepo: { + cloneUrl: "https://github.com/my-fork/reponame.git", + owner: "my-fork", + project: "reponame", + }, + base: "prod", + title: "[prod] PR Title", + body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge", + reviewers: [], + assignees: [], + labels: [], + comments: [], + }); + }); + + test("invalid tb repo format", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "prod", + tbRepo: "invalid-format", + gitUser: "Me", + gitEmail: "me@email.com", + reviewers: [], + assignees: [], + inheritReviewers: false, + }; + + await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("Invalid tb repo format \"invalid-format\", expected \"owner/repo\""); + }); + test("using simple config file", async () => { addProcessArgs([ "-cf", @@ -660,6 +752,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-prod-28f63db", base: "prod", title: "[prod] PR Title", @@ -723,6 +816,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-prod-28f63db", base: "prod", title: "New Title", @@ -796,6 +890,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-prod-0404fb9-11da4e3", base: "prod", title: "[prod] PR Title", @@ -871,6 +966,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-prod-28f63db", base: "prod", title: "New Title", @@ -968,6 +1064,7 @@ describe("github pull request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-prod-28f63db", base: "prod", title: "New Title", diff --git a/test/service/configs/pullrequest/gitlab-pr-configs-parser-multiple.test.ts b/test/service/configs/pullrequest/gitlab-pr-configs-parser-multiple.test.ts index 343f87d..fb015a7 100644 --- a/test/service/configs/pullrequest/gitlab-pr-configs-parser-multiple.test.ts +++ b/test/service/configs/pullrequest/gitlab-pr-configs-parser-multiple.test.ts @@ -70,6 +70,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-v1-ebb1eca", base: "v1", title: "New Title", @@ -82,6 +83,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-v2-ebb1eca", base: "v2", title: "New Title", @@ -94,6 +96,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-v3-ebb1eca", base: "v3", title: "New Title", @@ -143,6 +146,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-v1-ebb1eca", base: "v1", title: "New Title", @@ -155,6 +159,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-v2-ebb1eca", base: "v2", title: "New Title", @@ -167,6 +172,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-v3-ebb1eca", base: "v3", title: "New Title", @@ -217,6 +223,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "custom-branch-v1", base: "v1", title: "New Title", @@ -229,6 +236,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "custom-branch-v2", base: "v2", title: "New Title", @@ -241,6 +249,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "custom-branch-v3", base: "v3", title: "New Title", @@ -291,6 +300,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "custom1", base: "v1", title: "New Title", @@ -303,6 +313,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "custom2", base: "v2", title: "New Title", @@ -315,6 +326,7 @@ describe("gitlab merge request config parser", () => { { owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "custom3", base: "v3", title: "New Title", diff --git a/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts b/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts index d2ffa14..04246a7 100644 --- a/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts @@ -135,6 +135,7 @@ describe("gitlab merge request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-ebb1eca", base: "prod", title: "[prod] Update test.txt", @@ -317,6 +318,7 @@ describe("gitlab merge request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-ebb1eca", base: "prod", title: "New Title", @@ -389,6 +391,7 @@ describe("gitlab merge request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-ebb1eca", base: "prod", title: "New Title", @@ -461,6 +464,7 @@ describe("gitlab merge request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-ebb1eca", base: "prod", title: "New Title", @@ -535,6 +539,7 @@ describe("gitlab merge request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-ebb1eca", base: "prod", title: "New Title", @@ -596,6 +601,7 @@ describe("gitlab merge request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-ebb1eca", base: "prod", title: "[prod] Update test.txt", @@ -657,6 +663,7 @@ describe("gitlab merge request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-ebb1eca", base: "prod", title: "New Title", @@ -726,6 +733,7 @@ describe("gitlab merge request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-e4dd336-974519f", base: "prod", title: "[prod] Update test.txt opened", @@ -801,6 +809,7 @@ describe("gitlab merge request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-ebb1eca", base: "prod", title: "New Title", @@ -876,6 +885,7 @@ describe("gitlab merge request config parser", () => { expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-ebb1eca", base: "prod", title: "New Title", diff --git a/test/service/git/git-cli.test.ts b/test/service/git/git-cli.test.ts index 9497b32..65410b3 100644 --- a/test/service/git/git-cli.test.ts +++ b/test/service/git/git-cli.test.ts @@ -124,6 +124,14 @@ describe("git cli service", () => { expect(post).toEqual("tbranch"); }); + test("add remote twice updates the url instead of throwing", async () => { + await git.addRemote(cwd, "https://example.com/first/repo.git", "upstream"); + await expect(git.addRemote(cwd, "https://example.com/second/repo.git", "upstream")).resolves.not.toThrow(); + + const remoteURL = spawnSync("git", ["remote", "get-url", "upstream"], { cwd }).stdout.toString().trim(); + expect(remoteURL).toEqual("https://example.com/second/repo.git"); + }); + test("git clone set url with auth correctly for API token", async () => { const git2 = new GitCLIService("api-token", { user: "Backporting bot", diff --git a/test/service/git/gitlab/gitlab-client.test.ts b/test/service/git/gitlab/gitlab-client.test.ts index 2799ab1..22f0a34 100644 --- a/test/service/git/gitlab/gitlab-client.test.ts +++ b/test/service/git/gitlab/gitlab-client.test.ts @@ -84,6 +84,7 @@ describe("github service", () => { body: "Backport Body", owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", base: "old/branch", head: "bp-branch", reviewers: [], @@ -115,6 +116,7 @@ describe("github service", () => { body: "Backport Body", owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", base: "old/branch", head: "bp-branch", reviewers: ["superuser", "invalid"], @@ -151,6 +153,7 @@ describe("github service", () => { body: "Backport Body", owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", base: "old/branch", head: "bp-branch", reviewers: [], @@ -187,6 +190,7 @@ describe("github service", () => { body: "Backport Body", owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", base: "old/branch", head: "bp-branch-2", reviewers: ["superuser", "invalid"], @@ -223,6 +227,7 @@ describe("github service", () => { body: "Backport Body", owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", base: "old/branch", head: "bp-branch-2", reviewers: [], @@ -259,6 +264,7 @@ describe("github service", () => { body: "Backport Body", owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", base: "old/branch", head: "bp-branch-2", reviewers: [], @@ -293,6 +299,7 @@ describe("github service", () => { body: "Backport Body", owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", base: "old/branch", head: "bp-branch-2", reviewers: [], diff --git a/test/service/runner/cli-codeberg-runner.test.ts b/test/service/runner/cli-codeberg-runner.test.ts index 3115de8..fe1ebce 100644 --- a/test/service/runner/cli-codeberg-runner.test.ts +++ b/test/service/runner/cli-codeberg-runner.test.ts @@ -232,6 +232,7 @@ describe("cli runner", () => { 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", @@ -278,6 +279,7 @@ describe("cli runner", () => { 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", @@ -337,6 +339,7 @@ describe("cli runner", () => { 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", @@ -386,6 +389,7 @@ describe("cli runner", () => { 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", @@ -445,6 +449,7 @@ describe("cli runner", () => { 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", @@ -503,6 +508,7 @@ describe("cli runner", () => { 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", @@ -553,6 +559,7 @@ describe("cli runner", () => { 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", @@ -602,6 +609,7 @@ describe("cli runner", () => { 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", @@ -647,6 +655,7 @@ describe("cli runner", () => { 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", @@ -695,6 +704,7 @@ describe("cli runner", () => { 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", @@ -743,6 +753,7 @@ describe("cli runner", () => { 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", @@ -797,6 +808,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", head: truncatedBranch, base: "target", title: "[target] PR Title", @@ -849,6 +861,7 @@ describe("cli runner", () => { 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", @@ -899,6 +912,7 @@ describe("cli runner", () => { 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", @@ -956,6 +970,7 @@ describe("cli runner", () => { 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", @@ -968,6 +983,7 @@ describe("cli runner", () => { 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", @@ -980,6 +996,7 @@ describe("cli runner", () => { 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", @@ -1038,6 +1055,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", head: "custom1", base: "v1", title: "[v1] PR Title", @@ -1050,6 +1068,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", head: "custom2", base: "v2", title: "[v2] PR Title", @@ -1062,6 +1081,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://codeberg.org/owner/reponame.git", head: "custom3", base: "v3", title: "[v3] PR Title", @@ -1124,6 +1144,7 @@ describe("cli runner", () => { 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", @@ -1136,6 +1157,7 @@ describe("cli runner", () => { 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", @@ -1148,6 +1170,7 @@ describe("cli runner", () => { 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", @@ -1295,6 +1318,7 @@ describe("cli runner", () => { 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", @@ -1307,6 +1331,7 @@ describe("cli runner", () => { 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", diff --git a/test/service/runner/cli-github-runner.test.ts b/test/service/runner/cli-github-runner.test.ts index e55974b..87339af 100644 --- a/test/service/runner/cli-github-runner.test.ts +++ b/test/service/runner/cli-github-runner.test.ts @@ -232,6 +232,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -269,6 +270,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", headRepo: { cloneUrl: "https://github.com/my-fork/reponame.git", @@ -286,6 +288,43 @@ describe("cli runner", () => { ); }); + test("without dry run using target backport repo", async () => { + addProcessArgs([ + "-tb", + "target", + "-pr", + "https://github.com/owner/reponame/pull/2368", + "--tb-repo", + "target-org/reponame", + ]); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitCLIService.prototype.clone).toHaveBeenCalledTimes(1); + expect(GitCLIService.prototype.clone).toHaveBeenCalledWith("https://github.com/target-org/reponame.git", cwd, "target"); + + expect(GitCLIService.prototype.push).toHaveBeenCalledTimes(1); + expect(GitCLIService.prototype.push).toHaveBeenCalledWith(cwd, "bp-target-28f63db", undefined); + + expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(1); + expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ + owner: "target-org", + repo: "reponame", + cloneUrl: "https://github.com/target-org/reponame.git", + head: "bp-target-28f63db", + base: "target", + title: "[target] PR Title", + body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge", + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + labels: [], + comments: [], + } + ); + }); + test("same owner", async () => { addProcessArgs([ "-tb", @@ -319,6 +358,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -378,6 +418,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-9174896", base: "target", title: "[target] PR Title", @@ -427,6 +468,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-0404fb9-11da4e3", base: "target", title: "[target] PR Title", @@ -486,6 +528,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp_branch_name", base: "target", title: "New Title", @@ -544,6 +587,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp_branch_name", base: "target", title: "New Title", @@ -594,6 +638,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -643,6 +688,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -688,6 +734,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp_branch_name", base: "target", title: "New Title", @@ -736,6 +783,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -784,6 +832,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-0404fb9-11da4e3", base: "target", title: "[target] PR Title", @@ -838,6 +887,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: truncatedBranch, base: "target", title: "[target] PR Title", @@ -890,6 +940,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-0404fb9-11da4e3", base: "target", title: "[target] PR Title", @@ -940,6 +991,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -997,6 +1049,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v1-28f63db", base: "v1", title: "[v1] PR Title", @@ -1009,6 +1062,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v2-28f63db", base: "v2", title: "[v2] PR Title", @@ -1021,6 +1075,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v3-28f63db", base: "v3", title: "[v3] PR Title", @@ -1033,6 +1088,61 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveReturnedTimes(3); }); + test("with multiple target branches using target backport repo", async () => { + addProcessArgs([ + "-tb", + "v1, v2, v3", + "-pr", + "https://github.com/owner/reponame/pull/2368", + "--tb-repo", + "target-org/reponame", + "-f", + "/tmp/folder" + ]); + + await runner.execute(); + + const cwd = "/tmp/folder"; + + expect(GitCLIService.prototype.clone).toHaveBeenCalledTimes(3); + expect(GitCLIService.prototype.clone).toHaveBeenCalledWith("https://github.com/target-org/reponame.git", cwd, "v1"); + expect(GitCLIService.prototype.clone).toHaveBeenCalledWith("https://github.com/target-org/reponame.git", cwd, "v2"); + expect(GitCLIService.prototype.clone).toHaveBeenCalledWith("https://github.com/target-org/reponame.git", cwd, "v3"); + + // the "upstream" remote (pointing back at the original pr's repo) must be (re)added on every + // iteration since the working folder is reused across target branches + expect(GitCLIService.prototype.addRemote).toHaveBeenCalledTimes(3); + expect(GitCLIService.prototype.addRemote).toHaveBeenCalledWith(cwd, "https://github.com/owner/reponame.git", "upstream"); + + expect(GitCLIService.prototype.fetch).toHaveBeenCalledTimes(3); + expect(GitCLIService.prototype.fetch).toHaveBeenCalledWith(cwd, "pull/2368/head:pr/2368", "upstream"); + + expect(GitCLIService.prototype.push).toHaveBeenCalledTimes(3); + expect(GitCLIService.prototype.push).toHaveBeenCalledWith(cwd, "bp-v1-28f63db", undefined); + expect(GitCLIService.prototype.push).toHaveBeenCalledWith(cwd, "bp-v2-28f63db", undefined); + expect(GitCLIService.prototype.push).toHaveBeenCalledWith(cwd, "bp-v3-28f63db", undefined); + + expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledTimes(3); + expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith(expect.objectContaining({ + owner: "target-org", + repo: "reponame", + cloneUrl: "https://github.com/target-org/reponame.git", + base: "v1", + })); + expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith(expect.objectContaining({ + owner: "target-org", + repo: "reponame", + cloneUrl: "https://github.com/target-org/reponame.git", + base: "v2", + })); + expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith(expect.objectContaining({ + owner: "target-org", + repo: "reponame", + cloneUrl: "https://github.com/target-org/reponame.git", + base: "v3", + })); + }); + test("with multiple target branches and multiple bp names", async () => { addProcessArgs([ "-tb", @@ -1079,6 +1189,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom1", base: "v1", title: "[v1] PR Title", @@ -1091,6 +1202,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom2", base: "v2", title: "[v2] PR Title", @@ -1103,6 +1215,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom3", base: "v3", title: "[v3] PR Title", @@ -1165,6 +1278,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-failure-head-v1", base: "v1", title: "[v1] PR Title", @@ -1177,6 +1291,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-failure-head-v2", base: "v2", title: "[v2] PR Title", @@ -1189,6 +1304,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-failure-head-v3", base: "v3", title: "[v3] PR Title", @@ -1329,6 +1445,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-failure-head-v1", base: "v1", title: "[v1] PR Title", @@ -1341,6 +1458,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-failure-head-v2", base: "v2", title: "[v2] PR Title", @@ -1353,6 +1471,7 @@ describe("cli runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-failure-head-v3", base: "v3", title: "[v3] PR Title", diff --git a/test/service/runner/cli-gitlab-runner.test.ts b/test/service/runner/cli-gitlab-runner.test.ts index 0345910..d75f534 100644 --- a/test/service/runner/cli-gitlab-runner.test.ts +++ b/test/service/runner/cli-gitlab-runner.test.ts @@ -180,6 +180,7 @@ describe("cli runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-9e15674", base: "target", title: "[target] Update test.txt opened", @@ -239,6 +240,7 @@ describe("cli runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-ebb1eca", base: "target", title: "[target] Update test.txt", @@ -299,6 +301,7 @@ describe("cli runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp_branch_name", base: "target", title: "New Title", @@ -356,6 +359,7 @@ describe("cli runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp_branch_name", base: "target", title: "New Title", @@ -406,6 +410,7 @@ describe("cli runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-ebb1eca", base: "target", title: "[target] Update test.txt", @@ -455,6 +460,7 @@ describe("cli runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-ebb1eca", base: "target", title: "[target] Update test.txt", @@ -500,6 +506,7 @@ describe("cli runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-ebb1eca", base: "prod", title: "New Title", @@ -544,6 +551,7 @@ describe("cli runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-e4dd336", base: "target", title: "[target] Update test.txt", @@ -592,6 +600,7 @@ describe("cli runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-e4dd336-974519f", base: "target", title: "[target] Update test.txt opened", @@ -636,6 +645,7 @@ describe("cli runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-e4dd336", base: "target", title: "[target] Update test.txt", diff --git a/test/service/runner/gha-github-runner.test.ts b/test/service/runner/gha-github-runner.test.ts index 37c5220..ac72388 100644 --- a/test/service/runner/gha-github-runner.test.ts +++ b/test/service/runner/gha-github-runner.test.ts @@ -123,6 +123,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -177,6 +178,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-9174896", base: "target", title: "[target] PR Title", @@ -228,6 +230,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp_branch_name", base: "target", title: "New Title", @@ -280,6 +283,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp_branch_name", base: "target", title: "New Title", @@ -327,6 +331,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -374,6 +379,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -418,6 +424,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp_branch_name", base: "target", title: "New Title", @@ -464,6 +471,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -510,6 +518,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-0404fb9-11da4e3", base: "target", title: "[target] PR Title", @@ -557,6 +566,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -603,6 +613,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -649,6 +660,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-target-28f63db", base: "target", title: "[target] PR Title", @@ -703,6 +715,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v1-28f63db", base: "v1", title: "[v1] PR Title", @@ -715,6 +728,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v2-28f63db", base: "v2", title: "[v2] PR Title", @@ -727,6 +741,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "bp-v3-28f63db", base: "v3", title: "[v3] PR Title", @@ -781,6 +796,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-v1", base: "v1", title: "[v1] PR Title", @@ -793,6 +809,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-v2", base: "v2", title: "[v2] PR Title", @@ -805,6 +822,7 @@ describe("gha runner", () => { expect(GitHubClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "owner", repo: "reponame", + cloneUrl: "https://github.com/owner/reponame.git", head: "custom-v3", base: "v3", title: "[v3] PR Title", diff --git a/test/service/runner/gha-gitlab-runner.test.ts b/test/service/runner/gha-gitlab-runner.test.ts index d2d7823..f940ffa 100644 --- a/test/service/runner/gha-gitlab-runner.test.ts +++ b/test/service/runner/gha-gitlab-runner.test.ts @@ -134,6 +134,7 @@ describe("gha runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-9e15674", base: "target", title: "[target] Update test.txt opened", @@ -186,6 +187,7 @@ describe("gha runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-ebb1eca", base: "target", title: "[target] Update test.txt", @@ -236,6 +238,7 @@ describe("gha runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp_branch_name", base: "target", title: "New Title", @@ -287,6 +290,7 @@ describe("gha runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp_branch_name", base: "target", title: "New Title", @@ -332,6 +336,7 @@ describe("gha runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-ebb1eca", base: "target", title: "[target] Update test.txt", @@ -376,6 +381,7 @@ describe("gha runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-ebb1eca", base: "target", title: "[target] Update test.txt", @@ -420,6 +426,7 @@ describe("gha runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-prod-ebb1eca", base: "prod", title: "New Title", @@ -462,6 +469,7 @@ describe("gha runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-e4dd336", base: "target", title: "[target] Update test.txt", @@ -508,6 +516,7 @@ describe("gha runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-e4dd336-974519f", base: "target", title: "[target] Update test.txt opened", @@ -550,6 +559,7 @@ describe("gha runner", () => { expect(GitLabClient.prototype.createPullRequest).toHaveBeenCalledWith({ owner: "superuser", repo: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git", head: "bp-target-e4dd336", base: "target", title: "[target] Update test.txt", From 8bf1c82de08ee86301e4f80b90dfa20e2f3712ff Mon Sep 17 00:00:00 2001 From: john spurling Date: Fri, 31 Jul 2026 13:57:08 +0000 Subject: [PATCH 2/2] docs: clarify --tb-repo permission requirements and drop stray build artifact Correct the README note for the 'backport to a different target repository' mode (#223): the required permissions differ depending on whether --bp-repo is also used. With --tb-repo alone the branch is pushed straight to the target repo (needs push access there); combined with --bp-repo the branch goes to the fork and only PR-open ability is needed on the target repo. Also drop the unrelated allowScripts entry that had leaked into the bundled package.json inside dist/cli/index.js during an earlier rebuild. --- README.md | 2 +- dist/cli/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fdddff9..5bdcc27 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ $ git-backporting -tb v1 -pr https://github.com/upstream/project/pull/123 -a *** $ git-backporting -tb v1 -pr https://github.com/upstream/project/pull/123 -a ***** --tb-repo my-org/downstream-project --bp-repo my-user/downstream-project ``` -In this mode you should provide a PAT with enough permissions on the target repository (and on the fork, if `--bp-repo` is also used). +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. #### Configuration file example diff --git a/dist/cli/index.js b/dist/cli/index.js index 35db393..8f6d2d1 100755 --- a/dist/cli/index.js +++ b/dist/cli/index.js @@ -25565,7 +25565,7 @@ const dist_src_Octokit = Octokit.plugin(requestLog, legacyRestEndpointMethods, p /***/ ((module) => { "use strict"; -module.exports = JSON.parse('{"name":"@kie/git-backporting","version":"4.10.0","description":"Git backporting is a tool to execute automatic pull request git backporting.","author":"","license":"MIT","private":false,"main":"./dist/gha/index.js","bin":{"git-backporting":"./dist/cli/index.js"},"files":["dist/cli/index.js"],"publishConfig":{"access":"public"},"scripts":{"prepare":"husky install","clean":"rm -rf ./build ./dist","compile":"tsc -p tsconfig.json && tsc-alias -p tsconfig.json","package":"npm run package:cli && npm run package:gha","package:cli":"ncc build ./build/src/bin/cli.js -o dist/cli","package:gha":"ncc build ./build/src/bin/gha.js -o dist/gha","build":"npm run clean && npm run compile && npm run package","test":"jest --silent","test:report":"npm test -- --coverage --testResultsProcessor=jest-sonar-reporter","lint":"eslint . --ext .ts","lint:fix":"npm run lint -- --fix","ts-node":"ts-node","postversion":"npm run build && git add dist && rm -rf build","release":"release-it","release:branch":"git checkout -b release/$(release-it --release-version) main","release:prepare":"release-it --no-npm.publish --no-github.release --no-git.push --no-git.tag --no-git.requireUpstream","release:prepare:all":"npm run release:branch && npm run release:prepare"},"repository":{"type":"git","url":"git+https://github.com/kiegroup/git-backporting.git"},"keywords":["backporting","pull-requests","merge-requests","github-action","cherry-pick"],"bugs":{"url":"https://github.com/kiegroup/git-backporting/issues"},"homepage":"https://github.com/kiegroup/git-backporting#readme","devDependencies":{"@commitlint/cli":"^20.1.0","@commitlint/config-conventional":"^20.0.0","@gitbeaker/rest":"^39.1.0","@kie/mock-github":"^2.0.2","@octokit/webhooks-types":"^6.8.0","@release-it/conventional-changelog":"^10.0.0","@types/fs-extra":"^9.0.13","@types/jest":"^29.2.4","@types/node":"^18.11.17","@typescript-eslint/eslint-plugin":"^5.47.0","@typescript-eslint/parser":"^5.47.0","@vercel/ncc":"^0.36.0","conventional-commits-parser":"^6.2.0","eslint":"^8.30.0","husky":"^8.0.2","jest":"^30.0.0","jest-sonar-reporter":"^2.0.0","release-it":"^19.0.6","semver":"^7.3.8","ts-jest":"^29.4.5","ts-node":"^10.8.1","tsc-alias":"^1.8.2","tsconfig-paths":"^4.1.0","typescript":"^4.9.3"},"dependencies":{"@actions/core":"^1.10.0","@octokit/rest":"22.0.1","axios":"^1.4.0","commander":"^9.3.0","fs-extra":"^11.1.0","https":"^1.0.0","simple-git":"^3.15.1"},"overrides":{"conventional-changelog-conventionalcommits":"^8.0.0","undici":"6.28.0","brace-expansion":"5.0.8"},"allowScripts":{"unrs-resolver@1.11.1":true,"husky@8.0.2":true}}'); +module.exports = JSON.parse('{"name":"@kie/git-backporting","version":"4.10.0","description":"Git backporting is a tool to execute automatic pull request git backporting.","author":"","license":"MIT","private":false,"main":"./dist/gha/index.js","bin":{"git-backporting":"./dist/cli/index.js"},"files":["dist/cli/index.js"],"publishConfig":{"access":"public"},"scripts":{"prepare":"husky install","clean":"rm -rf ./build ./dist","compile":"tsc -p tsconfig.json && tsc-alias -p tsconfig.json","package":"npm run package:cli && npm run package:gha","package:cli":"ncc build ./build/src/bin/cli.js -o dist/cli","package:gha":"ncc build ./build/src/bin/gha.js -o dist/gha","build":"npm run clean && npm run compile && npm run package","test":"jest --silent","test:report":"npm test -- --coverage --testResultsProcessor=jest-sonar-reporter","lint":"eslint . --ext .ts","lint:fix":"npm run lint -- --fix","ts-node":"ts-node","postversion":"npm run build && git add dist && rm -rf build","release":"release-it","release:branch":"git checkout -b release/$(release-it --release-version) main","release:prepare":"release-it --no-npm.publish --no-github.release --no-git.push --no-git.tag --no-git.requireUpstream","release:prepare:all":"npm run release:branch && npm run release:prepare"},"repository":{"type":"git","url":"git+https://github.com/kiegroup/git-backporting.git"},"keywords":["backporting","pull-requests","merge-requests","github-action","cherry-pick"],"bugs":{"url":"https://github.com/kiegroup/git-backporting/issues"},"homepage":"https://github.com/kiegroup/git-backporting#readme","devDependencies":{"@commitlint/cli":"^20.1.0","@commitlint/config-conventional":"^20.0.0","@gitbeaker/rest":"^39.1.0","@kie/mock-github":"^2.0.2","@octokit/webhooks-types":"^6.8.0","@release-it/conventional-changelog":"^10.0.0","@types/fs-extra":"^9.0.13","@types/jest":"^29.2.4","@types/node":"^18.11.17","@typescript-eslint/eslint-plugin":"^5.47.0","@typescript-eslint/parser":"^5.47.0","@vercel/ncc":"^0.36.0","conventional-commits-parser":"^6.2.0","eslint":"^8.30.0","husky":"^8.0.2","jest":"^30.0.0","jest-sonar-reporter":"^2.0.0","release-it":"^19.0.6","semver":"^7.3.8","ts-jest":"^29.4.5","ts-node":"^10.8.1","tsc-alias":"^1.8.2","tsconfig-paths":"^4.1.0","typescript":"^4.9.3"},"dependencies":{"@actions/core":"^1.10.0","@octokit/rest":"22.0.1","axios":"^1.4.0","commander":"^9.3.0","fs-extra":"^11.1.0","https":"^1.0.0","simple-git":"^3.15.1"},"overrides":{"conventional-changelog-conventionalcommits":"^8.0.0","undici":"6.28.0","brace-expansion":"5.0.8"}}'); /***/ }),