From 5fc72e127bedb3177f4e17ff1182827c78154ef1 Mon Sep 17 00:00:00 2001 From: Andrea Lamparelli Date: Thu, 3 Aug 2023 21:57:11 +0200 Subject: [PATCH] feat(issue-77): handle multiple target branches (#78) fix: https://github.com/kiegroup/git-backporting/issues/77 This enhancement allow users to backport the same change to multiple branches with one single tool invocation --- README.md | 4 +- action.yml | 4 +- dist/cli/index.js | 141 ++++-- dist/gha/index.js | 137 ++++-- src/service/args/args-parser.ts | 4 +- src/service/args/args.types.ts | 8 +- src/service/args/cli/cli-args-parser.ts | 4 +- src/service/configs/configs.types.ts | 3 +- .../configs/pullrequest/pr-configs-parser.ts | 73 +-- src/service/git/git-cli.ts | 8 +- src/service/logger/console-logger-service.ts | 22 +- src/service/logger/logger-service.ts | 4 + src/service/runner/runner.ts | 52 ++- test/service/args/cli/cli-args-parser.test.ts | 59 +++ test/service/args/gha/gha-args-parser.test.ts | 52 +++ .../github-pr-configs-parser-multiple.test.ts | 422 ++++++++++++++++++ .../github-pr-configs-parser.test.ts | 94 ++-- .../gitlab-pr-configs-parser-multiple.test.ts | 349 +++++++++++++++ .../gitlab-pr-configs-parser.test.ts | 38 +- test/service/git/git-cli.test.ts | 9 + test/service/git/github/github-client.test.ts | 4 +- test/service/runner/cli-github-runner.test.ts | 264 ++++++++++- test/service/runner/gha-github-runner.test.ts | 166 +++++++ test/support/mock/git-client-mock-support.ts | 67 +-- test/support/mock/github-data.ts | 20 +- 25 files changed, 1774 insertions(+), 234 deletions(-) create mode 100644 test/service/configs/pullrequest/github-pr-configs-parser-multiple.test.ts create mode 100644 test/service/configs/pullrequest/gitlab-pr-configs-parser-multiple.test.ts diff --git a/README.md b/README.md index 1988232..9ee9fb9 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ This tool comes with some inputs that allow users to override the default behavi |---------------|----------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------| | Version | -V, --version | - | Current version of the tool | | | Help | -h, --help | - | Display the help message | | -| Target Branch | -tb, --target-branch | N | Branch where the changes must be backported to | | +| Target Branches | -tb, --target-branch | N | Comma separated list of branches where the changes must be backported to | | | Pull Request | -pr, --pull-request | N | Original pull request url, the one that must be backported, e.g., https://github.com/kiegroup/git-backporting/pull/1 | | | Configuration File | -cf, --config-file | N | Configuration file, in JSON format, containing all options to be overridded, note that if provided all other CLI options will be ignored | | | Auth | -a, --auth | N | `GITHUB_TOKEN`, `GITLAB_TOKEN` or a `repo` scoped [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) | "" | @@ -107,7 +107,7 @@ This tool comes with some inputs that allow users to override the default behavi | Reviewers | --reviewers | N | Backporting pull request comma-separated reviewers list | [] | | Assignees | --assignes | N | Backporting pull request comma-separated assignees list | [] | | 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 Name | --bp-branch-name | N | Name of the backporting pull request branch, if it exceeds 250 chars it will be truncated | bp-{target-branch}-{sha1}...{shaN} | +| 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} | | 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 | If provided the backporting will try to backport all pull request commits without squashing | false | diff --git a/action.yml b/action.yml index cb4fd89..6f69528 100644 --- a/action.yml +++ b/action.yml @@ -5,7 +5,7 @@ inputs: description: "URL of the pull request to backport, e.g., https://github.com/kiegroup/git-backporting/pull/1" required: false target-branch: - description: "Branch where the pull request must be backported to" + description: "Comma separated list of branches where the pull request must be backported to" required: false config-file: description: "Path to a file containing the json configuration for this tool, the object must match the Args interface" @@ -36,7 +36,7 @@ inputs: description: "Backporting PR body. Default is the original PR body" required: false bp-branch-name: - description: "Backporting PR branch name. Default is auto-generated from commit" + description: "Comma separated list of backporting PR branch names. Default is auto-generated from commit and target branches" 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 e436752..aab00ba 100755 --- a/dist/cli/index.js +++ b/dist/cli/index.js @@ -40,8 +40,8 @@ class ArgsParser { parse() { const args = this.readArgs(); // validate and fill with defaults - if (!args.pullRequest || !args.targetBranch) { - throw new Error("Missing option: pull request and target branch must be provided"); + if (!args.pullRequest || !args.targetBranch || args.targetBranch.trim().length == 0) { + throw new Error("Missing option: pull request and target branches must be provided"); } return { pullRequest: args.pullRequest, @@ -179,7 +179,7 @@ class CLIArgsParser extends args_parser_1.default { return new commander_1.Command(package_json_1.name) .version(package_json_1.version) .description(package_json_1.description) - .option("-tb, --target-branch ", "branch where changes must be backported to") + .option("-tb, --target-branch ", "comma separated list of branches where changes must be backported to") .option("-pr, --pull-request ", "pull request url, e.g., https://github.com/kiegroup/git-backporting/pull/1") .option("-d, --dry-run", "if enabled the tool does not create any pull request nor push anything remotely") .option("-a, --auth ", "git service authentication string, e.g., github token") @@ -189,7 +189,7 @@ class CLIArgsParser extends args_parser_1.default { .option("--title ", "backport pr title, default original pr title prefixed by target branch") .option("--body ", "backport pr title, default original pr body prefixed by bodyPrefix") .option("--body-prefix ", "backport pr body prefix, default `backport `") - .option("--bp-branch-name ", "backport pr branch name, default auto-generated by the commit") + .option("--bp-branch-name ", "comma separated list of backport pr branch names, default auto-generated by the commit and target branch") .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") @@ -288,6 +288,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); +const args_utils_1 = __nccwpck_require__(8048); const configs_parser_1 = __importDefault(__nccwpck_require__(5799)); const git_client_factory_1 = __importDefault(__nccwpck_require__(8550)); class PullRequestConfigsParser extends configs_parser_1.default { @@ -305,15 +306,19 @@ class PullRequestConfigsParser extends configs_parser_1.default { throw error; } const folder = args.folder ?? this.getDefaultFolder(); + const targetBranches = [...new Set((0, args_utils_1.getAsCommaSeparatedList)(args.targetBranch))]; + const bpBranchNames = [...new Set(args.bpBranchName ? ((0, args_utils_1.getAsCleanedCommaSeparatedList)(args.bpBranchName) ?? []) : [])]; + if (bpBranchNames.length > 1 && bpBranchNames.length != targetBranches.length) { + throw new Error(`The number of backport branch names, if provided, must match the number of target branches or just one, provided ${bpBranchNames.length} branch names instead`); + } return { dryRun: args.dryRun, auth: args.auth, folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`, - targetBranch: args.targetBranch, mergeStrategy: args.strategy, mergeStrategyOption: args.strategyOption, originalPullRequest: pr, - backportPullRequest: this.getDefaultBackportPullRequest(pr, args), + backportPullRequests: this.generateBackportPullRequestsData(pr, args, targetBranches, bpBranchNames), git: { user: args.gitUser ?? this.gitClient.getDefaultGitUser(), email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(), @@ -330,7 +335,7 @@ class PullRequestConfigsParser extends configs_parser_1.default { * @param targetBranch target branch where the backport should be applied * @returns {GitPullRequest} */ - getDefaultBackportPullRequest(originalPullRequest, args) { + generateBackportPullRequestsData(originalPullRequest, args, targetBranches, bpBranchNames) { const reviewers = args.reviewers ?? []; if (reviewers.length == 0 && args.inheritReviewers) { // inherit only if args.reviewers is empty and args.inheritReviewers set to true @@ -345,29 +350,37 @@ class PullRequestConfigsParser extends configs_parser_1.default { if (args.inheritLabels) { labels.push(...originalPullRequest.labels); } - let backportBranch = args.bpBranchName; - if (backportBranch === undefined || backportBranch.trim() === "") { - // for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects - const concatenatedCommits = originalPullRequest.commits.map(c => c.slice(0, 7)).join("-"); - backportBranch = `bp-${args.targetBranch}-${concatenatedCommits}`; - } - if (backportBranch.length > 250) { - this.logger.warn(`Backport branch (length=${backportBranch.length}) exceeded the max length of 250 chars, branch name truncated!`); - backportBranch = backportBranch.slice(0, 250); - } - return { - owner: originalPullRequest.targetRepo.owner, - repo: originalPullRequest.targetRepo.project, - head: backportBranch, - base: args.targetBranch, - title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, - // preserve new line chars - body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"), - reviewers: [...new Set(reviewers)], - assignees: [...new Set(args.assignees)], - labels: [...new Set(labels)], - comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [], - }; + return targetBranches.map((tb, idx) => { + // if there multiple branch names take the corresponding one, otherwise get the the first one if it exists + let backportBranch = bpBranchNames.length > 1 ? bpBranchNames[idx] : bpBranchNames[0]; + if (backportBranch === undefined || backportBranch.trim() === "") { + // for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects + const concatenatedCommits = originalPullRequest.commits.map(c => c.slice(0, 7)).join("-"); + backportBranch = `bp-${tb}-${concatenatedCommits}`; + } + else if (bpBranchNames.length == 1 && targetBranches.length > 1) { + // multiple targets and single custom backport branch name we need to differentiate branch names + // so append "-${tb}" to the provided name + backportBranch = backportBranch + `-${tb}`; + } + if (backportBranch.length > 250) { + this.logger.warn(`Backport branch (length=${backportBranch.length}) exceeded the max length of 250 chars, branch name truncated!`); + backportBranch = backportBranch.slice(0, 250); + } + return { + owner: originalPullRequest.targetRepo.owner, + repo: originalPullRequest.targetRepo.project, + head: backportBranch, + base: tb, + title: args.title ?? `[${tb}] ${originalPullRequest.title}`, + // preserve new line chars + body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"), + reviewers: [...new Set(reviewers)], + assignees: [...new Set(args.assignees)], + labels: [...new Set(labels)], + comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [], + }; + }); } } exports["default"] = PullRequestConfigsParser; @@ -436,10 +449,12 @@ class GitCLIService { this.logger.info(`Cloning repository ${from} to ${to}`); if (!fs_1.default.existsSync(to)) { await (0, simple_git_1.default)().clone(this.remoteWithAuth(from), to, ["--quiet", "--shallow-submodules", "--no-tags", "--branch", branch]); + return; } - else { - this.logger.warn(`Folder ${to} already exist. Won't clone`); - } + this.logger.info(`Folder ${to} already exist. Won't clone`); + // checkout to the proper branch + this.logger.info(`Checking out branch ${branch}`); + await this.git(to).checkout(branch); } /** * Create a new branch starting from the current one and checkout in it @@ -1115,22 +1130,31 @@ class ConsoleLoggerService { this.logger = new logger_1.default(); this.verbose = verbose; } + setContext(newContext) { + this.context = newContext; + } + clearContext() { + this.context = undefined; + } trace(message) { - this.logger.log("TRACE", message); + this.logger.log("TRACE", this.fromContext(message)); } debug(message) { if (this.verbose) { - this.logger.log("DEBUG", message); + this.logger.log("DEBUG", this.fromContext(message)); } } info(message) { - this.logger.log("INFO", message); + this.logger.log("INFO", this.fromContext(message)); } warn(message) { - this.logger.log("WARN", message); + this.logger.log("WARN", this.fromContext(message)); } error(message) { - this.logger.log("ERROR", message); + this.logger.log("ERROR", this.fromContext(message)); + } + fromContext(msg) { + return this.context ? `[${this.context}] ${msg}` : msg; } } exports["default"] = ConsoleLoggerService; @@ -1242,39 +1266,62 @@ class Runner { // 3. parse configs this.logger.debug("Parsing configs.."); const configs = await new pr_configs_parser_1.default().parseAndValidate(args); - const originalPR = configs.originalPullRequest; - const backportPR = configs.backportPullRequest; + const backportPRs = configs.backportPullRequests; // start local git operations const git = new git_cli_1.default(configs.auth, configs.git); + const failures = []; + // we need sequential backporting as they will operate on the same folder + // avoid cloning the same repo multiple times + for (const pr of backportPRs) { + try { + await this.executeBackport(configs, pr, { + gitClientType: gitClientType, + gitClientApi: gitApi, + gitCli: git, + }); + } + catch (error) { + this.logger.error(`Something went wrong backporting to ${pr.base}: ${error}`); + failures.push(error); + } + } + if (failures.length > 0) { + throw new Error(`Failure occurred during one of the backports: [${failures.join(" ; ")}]`); + } + } + async executeBackport(configs, backportPR, git) { + this.logger.setContext(backportPR.base); + const originalPR = configs.originalPullRequest; // 4. clone the repository this.logger.debug("Cloning repo.."); - await git.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, configs.targetBranch); + await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base); // 5. create new branch from target one and checkout this.logger.debug("Creating local branch.."); - await git.createLocalBranch(configs.folder, backportPR.head); + 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 || configs.originalPullRequest.state === "open") { this.logger.debug("Fetching pull request remote.."); - const prefix = gitClientType === git_types_1.GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab - await git.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); + const prefix = git.gitClientType === git_types_1.GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab + await git.gitCli.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); } // 7. apply all changes to the new branch this.logger.debug("Cherry picking commits.."); for (const sha of originalPR.commits) { - await git.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption); + await git.gitCli.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption); } if (!configs.dryRun) { // 8. push the new branch to origin - await git.push(configs.folder, backportPR.head); + await git.gitCli.push(configs.folder, backportPR.head); // 9. create pull request new branch -> target branch (using octokit) - const prUrl = await gitApi.createPullRequest(backportPR); + const prUrl = await git.gitClientApi.createPullRequest(backportPR); this.logger.info(`Pull request created: ${prUrl}`); } else { this.logger.warn("Pull request creation and remote push skipped"); this.logger.info(`${JSON.stringify(backportPR, null, 2)}`); } + this.logger.clearContext(); } } exports["default"] = Runner; diff --git a/dist/gha/index.js b/dist/gha/index.js index 0261515..f8f564c 100755 --- a/dist/gha/index.js +++ b/dist/gha/index.js @@ -40,8 +40,8 @@ class ArgsParser { parse() { const args = this.readArgs(); // validate and fill with defaults - if (!args.pullRequest || !args.targetBranch) { - throw new Error("Missing option: pull request and target branch must be provided"); + if (!args.pullRequest || !args.targetBranch || args.targetBranch.trim().length == 0) { + throw new Error("Missing option: pull request and target branches must be provided"); } return { pullRequest: args.pullRequest, @@ -258,6 +258,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); +const args_utils_1 = __nccwpck_require__(8048); const configs_parser_1 = __importDefault(__nccwpck_require__(5799)); const git_client_factory_1 = __importDefault(__nccwpck_require__(8550)); class PullRequestConfigsParser extends configs_parser_1.default { @@ -275,15 +276,19 @@ class PullRequestConfigsParser extends configs_parser_1.default { throw error; } const folder = args.folder ?? this.getDefaultFolder(); + const targetBranches = [...new Set((0, args_utils_1.getAsCommaSeparatedList)(args.targetBranch))]; + const bpBranchNames = [...new Set(args.bpBranchName ? ((0, args_utils_1.getAsCleanedCommaSeparatedList)(args.bpBranchName) ?? []) : [])]; + if (bpBranchNames.length > 1 && bpBranchNames.length != targetBranches.length) { + throw new Error(`The number of backport branch names, if provided, must match the number of target branches or just one, provided ${bpBranchNames.length} branch names instead`); + } return { dryRun: args.dryRun, auth: args.auth, folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`, - targetBranch: args.targetBranch, mergeStrategy: args.strategy, mergeStrategyOption: args.strategyOption, originalPullRequest: pr, - backportPullRequest: this.getDefaultBackportPullRequest(pr, args), + backportPullRequests: this.generateBackportPullRequestsData(pr, args, targetBranches, bpBranchNames), git: { user: args.gitUser ?? this.gitClient.getDefaultGitUser(), email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(), @@ -300,7 +305,7 @@ class PullRequestConfigsParser extends configs_parser_1.default { * @param targetBranch target branch where the backport should be applied * @returns {GitPullRequest} */ - getDefaultBackportPullRequest(originalPullRequest, args) { + generateBackportPullRequestsData(originalPullRequest, args, targetBranches, bpBranchNames) { const reviewers = args.reviewers ?? []; if (reviewers.length == 0 && args.inheritReviewers) { // inherit only if args.reviewers is empty and args.inheritReviewers set to true @@ -315,29 +320,37 @@ class PullRequestConfigsParser extends configs_parser_1.default { if (args.inheritLabels) { labels.push(...originalPullRequest.labels); } - let backportBranch = args.bpBranchName; - if (backportBranch === undefined || backportBranch.trim() === "") { - // for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects - const concatenatedCommits = originalPullRequest.commits.map(c => c.slice(0, 7)).join("-"); - backportBranch = `bp-${args.targetBranch}-${concatenatedCommits}`; - } - if (backportBranch.length > 250) { - this.logger.warn(`Backport branch (length=${backportBranch.length}) exceeded the max length of 250 chars, branch name truncated!`); - backportBranch = backportBranch.slice(0, 250); - } - return { - owner: originalPullRequest.targetRepo.owner, - repo: originalPullRequest.targetRepo.project, - head: backportBranch, - base: args.targetBranch, - title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, - // preserve new line chars - body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"), - reviewers: [...new Set(reviewers)], - assignees: [...new Set(args.assignees)], - labels: [...new Set(labels)], - comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [], - }; + return targetBranches.map((tb, idx) => { + // if there multiple branch names take the corresponding one, otherwise get the the first one if it exists + let backportBranch = bpBranchNames.length > 1 ? bpBranchNames[idx] : bpBranchNames[0]; + if (backportBranch === undefined || backportBranch.trim() === "") { + // for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects + const concatenatedCommits = originalPullRequest.commits.map(c => c.slice(0, 7)).join("-"); + backportBranch = `bp-${tb}-${concatenatedCommits}`; + } + else if (bpBranchNames.length == 1 && targetBranches.length > 1) { + // multiple targets and single custom backport branch name we need to differentiate branch names + // so append "-${tb}" to the provided name + backportBranch = backportBranch + `-${tb}`; + } + if (backportBranch.length > 250) { + this.logger.warn(`Backport branch (length=${backportBranch.length}) exceeded the max length of 250 chars, branch name truncated!`); + backportBranch = backportBranch.slice(0, 250); + } + return { + owner: originalPullRequest.targetRepo.owner, + repo: originalPullRequest.targetRepo.project, + head: backportBranch, + base: tb, + title: args.title ?? `[${tb}] ${originalPullRequest.title}`, + // preserve new line chars + body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"), + reviewers: [...new Set(reviewers)], + assignees: [...new Set(args.assignees)], + labels: [...new Set(labels)], + comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [], + }; + }); } } exports["default"] = PullRequestConfigsParser; @@ -406,10 +419,12 @@ class GitCLIService { this.logger.info(`Cloning repository ${from} to ${to}`); if (!fs_1.default.existsSync(to)) { await (0, simple_git_1.default)().clone(this.remoteWithAuth(from), to, ["--quiet", "--shallow-submodules", "--no-tags", "--branch", branch]); + return; } - else { - this.logger.warn(`Folder ${to} already exist. Won't clone`); - } + this.logger.info(`Folder ${to} already exist. Won't clone`); + // checkout to the proper branch + this.logger.info(`Checking out branch ${branch}`); + await this.git(to).checkout(branch); } /** * Create a new branch starting from the current one and checkout in it @@ -1085,22 +1100,31 @@ class ConsoleLoggerService { this.logger = new logger_1.default(); this.verbose = verbose; } + setContext(newContext) { + this.context = newContext; + } + clearContext() { + this.context = undefined; + } trace(message) { - this.logger.log("TRACE", message); + this.logger.log("TRACE", this.fromContext(message)); } debug(message) { if (this.verbose) { - this.logger.log("DEBUG", message); + this.logger.log("DEBUG", this.fromContext(message)); } } info(message) { - this.logger.log("INFO", message); + this.logger.log("INFO", this.fromContext(message)); } warn(message) { - this.logger.log("WARN", message); + this.logger.log("WARN", this.fromContext(message)); } error(message) { - this.logger.log("ERROR", message); + this.logger.log("ERROR", this.fromContext(message)); + } + fromContext(msg) { + return this.context ? `[${this.context}] ${msg}` : msg; } } exports["default"] = ConsoleLoggerService; @@ -1212,39 +1236,62 @@ class Runner { // 3. parse configs this.logger.debug("Parsing configs.."); const configs = await new pr_configs_parser_1.default().parseAndValidate(args); - const originalPR = configs.originalPullRequest; - const backportPR = configs.backportPullRequest; + const backportPRs = configs.backportPullRequests; // start local git operations const git = new git_cli_1.default(configs.auth, configs.git); + const failures = []; + // we need sequential backporting as they will operate on the same folder + // avoid cloning the same repo multiple times + for (const pr of backportPRs) { + try { + await this.executeBackport(configs, pr, { + gitClientType: gitClientType, + gitClientApi: gitApi, + gitCli: git, + }); + } + catch (error) { + this.logger.error(`Something went wrong backporting to ${pr.base}: ${error}`); + failures.push(error); + } + } + if (failures.length > 0) { + throw new Error(`Failure occurred during one of the backports: [${failures.join(" ; ")}]`); + } + } + async executeBackport(configs, backportPR, git) { + this.logger.setContext(backportPR.base); + const originalPR = configs.originalPullRequest; // 4. clone the repository this.logger.debug("Cloning repo.."); - await git.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, configs.targetBranch); + await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base); // 5. create new branch from target one and checkout this.logger.debug("Creating local branch.."); - await git.createLocalBranch(configs.folder, backportPR.head); + 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 || configs.originalPullRequest.state === "open") { this.logger.debug("Fetching pull request remote.."); - const prefix = gitClientType === git_types_1.GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab - await git.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); + const prefix = git.gitClientType === git_types_1.GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab + await git.gitCli.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); } // 7. apply all changes to the new branch this.logger.debug("Cherry picking commits.."); for (const sha of originalPR.commits) { - await git.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption); + await git.gitCli.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption); } if (!configs.dryRun) { // 8. push the new branch to origin - await git.push(configs.folder, backportPR.head); + await git.gitCli.push(configs.folder, backportPR.head); // 9. create pull request new branch -> target branch (using octokit) - const prUrl = await gitApi.createPullRequest(backportPR); + const prUrl = await git.gitClientApi.createPullRequest(backportPR); this.logger.info(`Pull request created: ${prUrl}`); } else { this.logger.warn("Pull request creation and remote push skipped"); this.logger.info(`${JSON.stringify(backportPR, null, 2)}`); } + this.logger.clearContext(); } } exports["default"] = Runner; diff --git a/src/service/args/args-parser.ts b/src/service/args/args-parser.ts index 5aee4a6..1c8cb51 100644 --- a/src/service/args/args-parser.ts +++ b/src/service/args/args-parser.ts @@ -17,8 +17,8 @@ export default abstract class ArgsParser { const args = this.readArgs(); // validate and fill with defaults - if (!args.pullRequest || !args.targetBranch) { - throw new Error("Missing option: pull request and target branch must be provided"); + if (!args.pullRequest || !args.targetBranch || args.targetBranch.trim().length == 0) { + throw new Error("Missing option: pull request and target branches must be provided"); } return { diff --git a/src/service/args/args.types.ts b/src/service/args/args.types.ts index 880257c..1a38562 100644 --- a/src/service/args/args.types.ts +++ b/src/service/args/args.types.ts @@ -1,8 +1,9 @@ /** - * Input arguments + * Tool's input arguments interface */ export interface Args { - targetBranch: string, // branch on the target repo where the change should be backported to + // NOTE: keep targetBranch as singular and of type string for backward compatibilities + targetBranch: string, // comma separated list of branches on the target repo where the change should be backported to pullRequest: string, // url of the pull request to backport dryRun?: boolean, // if enabled do not push anything remotely auth?: string, // git service auth, like github token @@ -12,7 +13,8 @@ export interface Args { title?: string, // backport pr title, default original pr title prefixed by target branch body?: string, // backport pr title, default original pr body prefixed by bodyPrefix bodyPrefix?: string, // backport pr body prefix, default `backport ` - bpBranchName?: string, // backport pr branch name, default computed from commit + // 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 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 0e8b4e3..9a9b348 100644 --- a/src/service/args/cli/cli-args-parser.ts +++ b/src/service/args/cli/cli-args-parser.ts @@ -10,7 +10,7 @@ export default class CLIArgsParser extends ArgsParser { return new Command(name) .version(version) .description(description) - .option("-tb, --target-branch ", "branch where changes must be backported to") + .option("-tb, --target-branch ", "comma separated list of branches where changes must be backported to") .option("-pr, --pull-request ", "pull request url, e.g., https://github.com/kiegroup/git-backporting/pull/1") .option("-d, --dry-run", "if enabled the tool does not create any pull request nor push anything remotely") .option("-a, --auth ", "git service authentication string, e.g., github token") @@ -20,7 +20,7 @@ export default class CLIArgsParser extends ArgsParser { .option("--title ", "backport pr title, default original pr title prefixed by target branch") .option("--body ", "backport pr title, default original pr body prefixed by bodyPrefix") .option("--body-prefix ", "backport pr body prefix, default `backport `") - .option("--bp-branch-name ", "backport pr branch name, default auto-generated by the commit") + .option("--bp-branch-name ", "comma separated list of backport pr branch names, default auto-generated by the commit and target branch") .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") diff --git a/src/service/configs/configs.types.ts b/src/service/configs/configs.types.ts index be46157..fa44e51 100644 --- a/src/service/configs/configs.types.ts +++ b/src/service/configs/configs.types.ts @@ -15,10 +15,9 @@ export interface Configs { auth?: string, git: LocalGit, folder: string, - targetBranch: string, mergeStrategy?: string, // cherry-pick merge strategy mergeStrategyOption?: string, // cherry-pick merge strategy option originalPullRequest: GitPullRequest, - backportPullRequest: BackportPullRequest, + backportPullRequests: BackportPullRequest[], } diff --git a/src/service/configs/pullrequest/pr-configs-parser.ts b/src/service/configs/pullrequest/pr-configs-parser.ts index 39d39e7..00e220e 100644 --- a/src/service/configs/pullrequest/pr-configs-parser.ts +++ b/src/service/configs/pullrequest/pr-configs-parser.ts @@ -1,3 +1,4 @@ +import { getAsCleanedCommaSeparatedList, getAsCommaSeparatedList } from "@bp/service/args/args-utils"; import { Args } from "@bp/service/args/args.types"; import ConfigsParser from "@bp/service/configs/configs-parser"; import { Configs } from "@bp/service/configs/configs.types"; @@ -25,15 +26,21 @@ export default class PullRequestConfigsParser extends ConfigsParser { const folder: string = args.folder ?? this.getDefaultFolder(); + const targetBranches: string[] = [...new Set(getAsCommaSeparatedList(args.targetBranch)!)]; + const bpBranchNames: string[] = [...new Set(args.bpBranchName ? (getAsCleanedCommaSeparatedList(args.bpBranchName) ?? []) : [])]; + + if (bpBranchNames.length > 1 && bpBranchNames.length != targetBranches.length) { + throw new Error(`The number of backport branch names, if provided, must match the number of target branches or just one, provided ${bpBranchNames.length} branch names instead`); + } + return { dryRun: args.dryRun!, auth: args.auth, folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`, - targetBranch: args.targetBranch, mergeStrategy: args.strategy, mergeStrategyOption: args.strategyOption, originalPullRequest: pr, - backportPullRequest: this.getDefaultBackportPullRequest(pr, args), + backportPullRequests: this.generateBackportPullRequestsData(pr, args, targetBranches, bpBranchNames), git: { user: args.gitUser ?? this.gitClient.getDefaultGitUser(), email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(), @@ -52,7 +59,13 @@ export default class PullRequestConfigsParser extends ConfigsParser { * @param targetBranch target branch where the backport should be applied * @returns {GitPullRequest} */ - private getDefaultBackportPullRequest(originalPullRequest: GitPullRequest, args: Args): BackportPullRequest { + private generateBackportPullRequestsData( + originalPullRequest: GitPullRequest, + args: Args, + targetBranches: string[], + bpBranchNames: string[] + ): BackportPullRequest[] { + const reviewers = args.reviewers ?? []; if (reviewers.length == 0 && args.inheritReviewers) { // inherit only if args.reviewers is empty and args.inheritReviewers set to true @@ -70,30 +83,38 @@ export default class PullRequestConfigsParser extends ConfigsParser { labels.push(...originalPullRequest.labels); } - let backportBranch = args.bpBranchName; - if (backportBranch === undefined || backportBranch.trim() === "") { - // for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects - const concatenatedCommits: string = originalPullRequest.commits!.map(c => c.slice(0, 7)).join("-"); - backportBranch = `bp-${args.targetBranch}-${concatenatedCommits}`; - } + return targetBranches.map((tb, idx) => { - if (backportBranch.length > 250) { - this.logger.warn(`Backport branch (length=${backportBranch.length}) exceeded the max length of 250 chars, branch name truncated!`); - backportBranch = backportBranch.slice(0, 250); - } + // if there multiple branch names take the corresponding one, otherwise get the the first one if it exists + let backportBranch = bpBranchNames.length > 1 ? bpBranchNames[idx] : bpBranchNames[0]; + if (backportBranch === undefined || backportBranch.trim() === "") { + // for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects + const concatenatedCommits: string = originalPullRequest.commits!.map(c => c.slice(0, 7)).join("-"); + backportBranch = `bp-${tb}-${concatenatedCommits}`; + } else if (bpBranchNames.length == 1 && targetBranches.length > 1) { + // multiple targets and single custom backport branch name we need to differentiate branch names + // so append "-${tb}" to the provided name + backportBranch = backportBranch + `-${tb}`; + } + + if (backportBranch.length > 250) { + this.logger.warn(`Backport branch (length=${backportBranch.length}) exceeded the max length of 250 chars, branch name truncated!`); + backportBranch = backportBranch.slice(0, 250); + } - return { - owner: originalPullRequest.targetRepo.owner, - repo: originalPullRequest.targetRepo.project, - head: backportBranch, - base: args.targetBranch, - title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, - // preserve new line chars - body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"), - reviewers: [...new Set(reviewers)], - assignees: [...new Set(args.assignees)], - labels: [...new Set(labels)], - comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [], - }; + return { + owner: originalPullRequest.targetRepo.owner, + repo: originalPullRequest.targetRepo.project, + head: backportBranch, + base: tb, + title: args.title ?? `[${tb}] ${originalPullRequest.title}`, + // preserve new line chars + body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"), + reviewers: [...new Set(reviewers)], + assignees: [...new Set(args.assignees)], + labels: [...new Set(labels)], + comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [], + }; + }) as BackportPullRequest[]; } } \ No newline at end of file diff --git a/src/service/git/git-cli.ts b/src/service/git/git-cli.ts index 26239a7..76f6fc3 100644 --- a/src/service/git/git-cli.ts +++ b/src/service/git/git-cli.ts @@ -63,9 +63,13 @@ export default class GitCLIService { this.logger.info(`Cloning repository ${from} to ${to}`); if (!fs.existsSync(to)) { await simpleGit().clone(this.remoteWithAuth(from), to, ["--quiet", "--shallow-submodules", "--no-tags", "--branch", branch]); - } else { - this.logger.warn(`Folder ${to} already exist. Won't clone`); + return; } + + this.logger.info(`Folder ${to} already exist. Won't clone`); + // checkout to the proper branch + this.logger.info(`Checking out branch ${branch}`); + await this.git(to).checkout(branch); } /** diff --git a/src/service/logger/console-logger-service.ts b/src/service/logger/console-logger-service.ts index ee9339b..4a3088e 100644 --- a/src/service/logger/console-logger-service.ts +++ b/src/service/logger/console-logger-service.ts @@ -5,32 +5,44 @@ export default class ConsoleLoggerService implements LoggerService { private readonly logger: Logger; private readonly verbose: boolean; + private context?: string; constructor(verbose = true) { this.logger = new Logger(); this.verbose = verbose; } + + setContext(newContext: string) { + this.context = newContext; + } + + clearContext() { + this.context = undefined; + } trace(message: string): void { - this.logger.log("TRACE", message); + this.logger.log("TRACE", this.fromContext(message)); } debug(message: string): void { if (this.verbose) { - this.logger.log("DEBUG", message); + this.logger.log("DEBUG", this.fromContext(message)); } } info(message: string): void { - this.logger.log("INFO", message); + this.logger.log("INFO", this.fromContext(message)); } warn(message: string): void { - this.logger.log("WARN", message); + this.logger.log("WARN", this.fromContext(message)); } error(message: string): void { - this.logger.log("ERROR", message); + this.logger.log("ERROR", this.fromContext(message)); } + private fromContext(msg: string): string { + return this.context ? `[${this.context}] ${msg}` : msg; + } } \ No newline at end of file diff --git a/src/service/logger/logger-service.ts b/src/service/logger/logger-service.ts index 34b1895..0488ac6 100644 --- a/src/service/logger/logger-service.ts +++ b/src/service/logger/logger-service.ts @@ -3,6 +3,10 @@ */ export default interface LoggerService { + setContext(newContext: string): void; + + clearContext(): void; + trace(message: string): void; debug(message: string): void; diff --git a/src/service/runner/runner.ts b/src/service/runner/runner.ts index e5d0db0..6f256b4 100644 --- a/src/service/runner/runner.ts +++ b/src/service/runner/runner.ts @@ -10,6 +10,12 @@ import LoggerService from "@bp/service/logger/logger-service"; import LoggerServiceFactory from "@bp/service/logger/logger-service-factory"; import { inferGitClient, inferGitApiUrl } from "@bp/service/git/git-util"; +interface Git { + gitClientType: GitClientType; + gitClientApi: GitClient; + gitCli: GitCLIService; +} + /** * Main runner implementation, it implements the core logic flow */ @@ -62,47 +68,73 @@ export default class Runner { // 3. parse configs this.logger.debug("Parsing configs.."); const configs: Configs = await new PullRequestConfigsParser().parseAndValidate(args); - const originalPR: GitPullRequest = configs.originalPullRequest; - const backportPR: BackportPullRequest = configs.backportPullRequest; + const backportPRs: BackportPullRequest[] = configs.backportPullRequests; // start local git operations const git: GitCLIService = new GitCLIService(configs.auth, configs.git); + const failures: string[] = []; + // we need sequential backporting as they will operate on the same folder + // avoid cloning the same repo multiple times + for(const pr of backportPRs) { + try { + await this.executeBackport(configs, pr, { + gitClientType: gitClientType, + gitClientApi: gitApi, + gitCli: git, + }); + } catch(error) { + this.logger.error(`Something went wrong backporting to ${pr.base}: ${error}`); + failures.push(error as string); + } + } + + if (failures.length > 0) { + throw new Error(`Failure occurred during one of the backports: [${failures.join(" ; ")}]`); + } + } + + async executeBackport(configs: Configs, backportPR: BackportPullRequest, git: Git): Promise { + this.logger.setContext(backportPR.base); + + const originalPR: GitPullRequest = configs.originalPullRequest; + // 4. clone the repository this.logger.debug("Cloning repo.."); - await git.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, configs.targetBranch); + await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base); // 5. create new branch from target one and checkout this.logger.debug("Creating local branch.."); - await git.createLocalBranch(configs.folder, backportPR.head); + 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 || configs.originalPullRequest.state === "open") { this.logger.debug("Fetching pull request remote.."); - const prefix = gitClientType === GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab - await git.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); + const prefix = git.gitClientType === GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab + await git.gitCli.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`); } // 7. apply all changes to the new branch this.logger.debug("Cherry picking commits.."); for (const sha of originalPR.commits!) { - await git.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption); + await git.gitCli.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption); } if (!configs.dryRun) { // 8. push the new branch to origin - await git.push(configs.folder, backportPR.head); + await git.gitCli.push(configs.folder, backportPR.head); // 9. create pull request new branch -> target branch (using octokit) - const prUrl = await gitApi.createPullRequest(backportPR); + const prUrl = await git.gitClientApi.createPullRequest(backportPR); this.logger.info(`Pull request created: ${prUrl}`); } else { this.logger.warn("Pull request creation and remote push skipped"); this.logger.info(`${JSON.stringify(backportPR, null, 2)}`); } - } + this.logger.clearContext(); + } } \ No newline at end of file diff --git a/test/service/args/cli/cli-args-parser.test.ts b/test/service/args/cli/cli-args-parser.test.ts index 2951bf3..bf82586 100644 --- a/test/service/args/cli/cli-args-parser.test.ts +++ b/test/service/args/cli/cli-args-parser.test.ts @@ -433,4 +433,63 @@ describe("cli args parser", () => { expect(args.squash).toEqual(true); expectArrayEqual(args.comments!,["first comment", "second comment"]); }); + + test("valid execution with multiple branches", () => { + addProcessArgs([ + "-tb", + "target, old", + "-pr", + "https://localhost/whatever/pulls/1" + ]); + + const args: Args = parser.parse(); + expect(args.dryRun).toEqual(false); + expect(args.auth).toEqual(undefined); + expect(args.gitUser).toEqual(undefined); + expect(args.gitEmail).toEqual(undefined); + expect(args.folder).toEqual(undefined); + expect(args.targetBranch).toEqual("target, old"); + expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1"); + expect(args.title).toEqual(undefined); + expect(args.body).toEqual(undefined); + expect(args.bodyPrefix).toEqual(undefined); + expect(args.bpBranchName).toEqual(undefined); + expect(args.reviewers).toEqual([]); + expect(args.assignees).toEqual([]); + expect(args.inheritReviewers).toEqual(true); + expect(args.labels).toEqual([]); + expect(args.inheritLabels).toEqual(false); + expect(args.squash).toEqual(true); + expect(args.strategy).toEqual(undefined); + expect(args.strategyOption).toEqual(undefined); + }); + + test("invalid execution with empty target branch", () => { + addProcessArgs([ + "-tb", + " ", + "-pr", + "https://localhost/whatever/pulls/1" + ]); + + expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + }); + + test("invalid execution with missing mandatory target branch", () => { + addProcessArgs([ + "-pr", + "https://localhost/whatever/pulls/1" + ]); + + expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + }); + + test("invalid execution with missin mandatory pull request", () => { + addProcessArgs([ + "-tb", + "target", + ]); + + expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + }); }); \ No newline at end of file diff --git a/test/service/args/gha/gha-args-parser.test.ts b/test/service/args/gha/gha-args-parser.test.ts index 76efa13..20642d9 100644 --- a/test/service/args/gha/gha-args-parser.test.ts +++ b/test/service/args/gha/gha-args-parser.test.ts @@ -262,4 +262,56 @@ describe("gha args parser", () => { expect(args.squash).toEqual(true); expectArrayEqual(args.comments!,["first comment", "second comment"]); }); + + test("valid execution with multiple branches", () => { + spyGetInput({ + "target-branch": "target,old", + "pull-request": "https://localhost/whatever/pulls/1" + }); + + const args: Args = parser.parse(); + expect(args.dryRun).toEqual(false); + expect(args.auth).toEqual(undefined); + expect(args.gitUser).toEqual(undefined); + expect(args.gitEmail).toEqual(undefined); + expect(args.folder).toEqual(undefined); + expect(args.targetBranch).toEqual("target,old"); + expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1"); + expect(args.title).toEqual(undefined); + expect(args.body).toEqual(undefined); + expect(args.reviewers).toEqual([]); + expect(args.assignees).toEqual([]); + expect(args.inheritReviewers).toEqual(true); + expect(args.labels).toEqual([]); + expect(args.inheritLabels).toEqual(false); + expect(args.squash).toEqual(true); + expect(args.strategy).toEqual(undefined); + expect(args.strategyOption).toEqual(undefined); + }); + + + test("invalid execution with empty target branch", () => { + spyGetInput({ + "target-branch": " ", + "pull-request": "https://localhost/whatever/pulls/1" + }); + + expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + }); + + test("invalid execution with missing mandatory target branch", () => { + spyGetInput({ + "pull-request": "https://localhost/whatever/pulls/1" + }); + + expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + }); + + test("invalid execution with missin mandatory pull request", () => { + spyGetInput({ + "target-branch": "target,old", + }); + + expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + }); }); \ No newline at end of file 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 new file mode 100644 index 0000000..e1f7289 --- /dev/null +++ b/test/service/configs/pullrequest/github-pr-configs-parser-multiple.test.ts @@ -0,0 +1,422 @@ +import { Args } from "@bp/service/args/args.types"; +import { Configs } from "@bp/service/configs/configs.types"; +import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser"; +import GitClientFactory from "@bp/service/git/git-client-factory"; +import { GitClientType } from "@bp/service/git/git.types"; +import { mockGitHubClient } from "../../../support/mock/git-client-mock-support"; +import { resetProcessArgs } from "../../../support/utils"; +import { MERGED_PR_FIXTURE, REPO, TARGET_OWNER, MULT_COMMITS_PR_FIXTURE } from "../../../support/mock/github-data"; +import GitHubMapper from "@bp/service/git/github/github-mapper"; +import GitHubClient from "@bp/service/git/github/github-client"; + +jest.spyOn(GitHubMapper.prototype, "mapPullRequest"); +jest.spyOn(GitHubClient.prototype, "getPullRequest"); + +describe("github pull request config parser", () => { + + const mergedPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`; + const multipleCommitsPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MULT_COMMITS_PR_FIXTURE.number}`; + + let configParser: PullRequestConfigsParser; + + beforeAll(() => { + GitClientFactory.reset(); + GitClientFactory.getOrCreate(GitClientType.GITHUB, "whatever", "http://localhost/api/v3"); + }); + + beforeEach(() => { + // reset process.env variables + resetProcessArgs(); + + // mock octokit + mockGitHubClient("http://localhost/api/v3"); + + // create a fresh new instance every time + configParser = new PullRequestConfigsParser(); + }); + + test("multiple backports", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "v1, v2, v3", + gitUser: "Me", + gitEmail: "me@email.com", + title: "New Title", + body: "New Body", + bodyPrefix: "New Body Prefix -", + reviewers: [], + assignees: ["user3", "user4"], + inheritReviewers: false, + labels: [], + inheritLabels: false, + comments: [], + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []); + + expect(configs.dryRun).toEqual(false); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.backportPullRequests.length).toEqual(3); + expect(configs.backportPullRequests).toEqual( + expect.arrayContaining([ + { + owner: "owner", + repo: "reponame", + head: "bp-v1-28f63db", + base: "v1", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "owner", + repo: "reponame", + head: "bp-v2-28f63db", + base: "v2", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "owner", + repo: "reponame", + head: "bp-v3-28f63db", + base: "v3", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + ]) + ); + }); + + test("multiple backports ignore duplicates", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "v1, v2, v2, v3", + gitUser: "Me", + gitEmail: "me@email.com", + title: "New Title", + body: "New Body", + bodyPrefix: "New Body Prefix -", + reviewers: [], + assignees: ["user3", "user4"], + inheritReviewers: false, + labels: [], + inheritLabels: false, + comments: [], + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []); + + expect(configs.dryRun).toEqual(false); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.backportPullRequests.length).toEqual(3); + expect(configs.backportPullRequests).toEqual( + expect.arrayContaining([ + { + owner: "owner", + repo: "reponame", + head: "bp-v1-28f63db", + base: "v1", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "owner", + repo: "reponame", + head: "bp-v2-28f63db", + base: "v2", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "owner", + repo: "reponame", + head: "bp-v3-28f63db", + base: "v3", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + ]) + ); + }); + + test("multiple backports with custom branch name", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "v1, v2, v3", + gitUser: "Me", + gitEmail: "me@email.com", + title: "New Title", + body: "New Body", + bodyPrefix: "New Body Prefix -", + reviewers: [], + assignees: ["user3", "user4"], + inheritReviewers: false, + labels: [], + inheritLabels: false, + comments: [], + bpBranchName: "custom-branch", + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []); + + expect(configs.dryRun).toEqual(false); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.backportPullRequests.length).toEqual(3); + expect(configs.backportPullRequests).toEqual( + expect.arrayContaining([ + { + owner: "owner", + repo: "reponame", + head: "custom-branch-v1", + base: "v1", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "owner", + repo: "reponame", + head: "custom-branch-v2", + base: "v2", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "owner", + repo: "reponame", + head: "custom-branch-v3", + base: "v3", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + ]) + ); + }); + + test("multiple backports with multiple custom branch names", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "v1, v2, v3", + gitUser: "Me", + gitEmail: "me@email.com", + title: "New Title", + body: "New Body", + bodyPrefix: "New Body Prefix -", + reviewers: [], + assignees: ["user3", "user4"], + inheritReviewers: false, + labels: [], + inheritLabels: false, + comments: [], + bpBranchName: "custom-branch1, custom-branch2, custom-branch3", + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []); + + expect(configs.dryRun).toEqual(false); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.backportPullRequests.length).toEqual(3); + expect(configs.backportPullRequests).toEqual( + expect.arrayContaining([ + { + owner: "owner", + repo: "reponame", + head: "custom-branch1", + base: "v1", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "owner", + repo: "reponame", + head: "custom-branch2", + base: "v2", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "owner", + repo: "reponame", + head: "custom-branch3", + base: "v3", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + ]) + ); + }); + + test("multiple backports with incorrect number of bp branch names", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "v1, v2, v3", + gitUser: "Me", + gitEmail: "me@email.com", + title: "New Title", + body: "New Body", + bodyPrefix: "New Body Prefix -", + reviewers: [], + assignees: ["user3", "user4"], + inheritReviewers: false, + labels: [], + inheritLabels: false, + comments: [], + bpBranchName: "custom-branch1, custom-branch2", + }; + + await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("The number of backport branch names, if provided, must match the number of target branches or just one, provided 2 branch names instead"); + }); + + test("multiple backports and multiple commits", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: multipleCommitsPRUrl, + targetBranch: "v1, v2, v3", + gitUser: "GitHub", + gitEmail: "noreply@github.com", + reviewers: [], + assignees: [], + inheritReviewers: true, + squash: false, + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 8632, false); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"]); + + expect(configs.dryRun).toEqual(false); + expect(configs.git).toEqual({ + user: "GitHub", + email: "noreply@github.com" + }); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.backportPullRequests.length).toEqual(3); + expect(configs.backportPullRequests).toEqual( + expect.arrayContaining([ + { + owner: "owner", + repo: "reponame", + head: "bp-v1-0404fb9-11da4e3", + base: "v1", + title: "[v1] PR Title", + body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge", + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + labels: [], + comments: [], + }, + { + owner: "owner", + repo: "reponame", + head: "bp-v2-0404fb9-11da4e3", + base: "v2", + title: "[v2] PR Title", + body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge", + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + labels: [], + comments: [], + }, + { + owner: "owner", + repo: "reponame", + head: "bp-v3-0404fb9-11da4e3", + base: "v3", + title: "[v3] PR Title", + body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge", + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + labels: [], + comments: [], + }, + ]) + ); + }); + +}); \ No newline at end of file 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 bca5d75..974a02b 100644 --- a/test/service/configs/pullrequest/github-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/github-pr-configs-parser.test.ts @@ -5,7 +5,7 @@ import GitClientFactory from "@bp/service/git/git-client-factory"; import { GitClientType } from "@bp/service/git/git.types"; import { mockGitHubClient } from "../../../support/mock/git-client-mock-support"; import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../../support/utils"; -import { mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, repo, targetOwner, multipleCommitsPullRequestFixture } from "../../../support/mock/github-data"; +import { MERGED_PR_FIXTURE, OPEN_PR_FIXTURE, NOT_MERGED_PR_FIXTURE, REPO, TARGET_OWNER, MULT_COMMITS_PR_FIXTURE } from "../../../support/mock/github-data"; import CLIArgsParser from "@bp/service/args/cli/cli-args-parser"; import GitHubMapper from "@bp/service/git/github/github-mapper"; import GitHubClient from "@bp/service/git/github/github-client"; @@ -13,14 +13,14 @@ import GitHubClient from "@bp/service/git/github/github-client"; const GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME = "./github-pr-configs-parser-simple-pr-merged.json"; const GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT = { "targetBranch": "prod", - "pullRequest": `https://github.com/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`, + "pullRequest": `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`, }; const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME = "./github-pr-configs-parser-complex-pr-merged.json"; const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { "dryRun": false, "auth": "my-auth-token", - "pullRequest": `https://github.com/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`, + "pullRequest": `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`, "targetBranch": "prod", "gitUser": "Me", "gitEmail": "me@email.com", @@ -39,10 +39,10 @@ jest.spyOn(GitHubClient.prototype, "getPullRequest"); describe("github pull request config parser", () => { - const mergedPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`; - const openPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${openPullRequestFixture.number}`; - const notMergedPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${notMergedPullRequestFixture.number}`; - const multipleCommitsPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${multipleCommitsPullRequestFixture.number}`; + const mergedPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`; + const openPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${OPEN_PR_FIXTURE.number}`; + const notMergedPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${NOT_MERGED_PR_FIXTURE.number}`; + const multipleCommitsPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MULT_COMMITS_PR_FIXTURE.number}`; let argsParser: CLIArgsParser; let configParser: PullRequestConfigsParser; @@ -100,7 +100,6 @@ describe("github pull request config parser", () => { email: "noreply@github.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 2368, @@ -128,7 +127,8 @@ describe("github pull request config parser", () => { nCommits: 2, commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", head: "bp-prod-28f63db", @@ -160,7 +160,6 @@ describe("github pull request config parser", () => { expect(configs.dryRun).toEqual(true); expect(configs.auth).toEqual("whatever"); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual("/tmp/test"); expect(configs.git).toEqual({ user: "GitHub", @@ -190,7 +189,6 @@ describe("github pull request config parser", () => { expect(configs.dryRun).toEqual(true); expect(configs.auth).toEqual("whatever"); - expect(configs.targetBranch).toEqual("prod"); expect(configs.git).toEqual({ user: "GitHub", email: "noreply@github.com" @@ -271,7 +269,6 @@ describe("github pull request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 2368, @@ -300,7 +297,8 @@ describe("github pull request config parser", () => { nCommits: 2, commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", head: "custom-branch", @@ -314,6 +312,48 @@ describe("github pull request config parser", () => { }); }); + test("override backport with empty bp branch name", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "prod", + gitUser: "Me", + gitEmail: "me@email.com", + title: "New Title", + body: "New Body", + bodyPrefix: "New Body Prefix -", + reviewers: [], + assignees: [], + inheritReviewers: true, + bpBranchName: " " + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []); + + expect(configs.dryRun).toEqual(false); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ + owner: "owner", + repo: "reponame", + head: "bp-prod-28f63db", + base: "prod", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + labels: [], + comments: [], + }); + }); + test("override backport pr reviewers and assignees", async () => { const args: Args = { dryRun: false, @@ -343,7 +383,6 @@ describe("github pull request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 2368, @@ -372,7 +411,8 @@ describe("github pull request config parser", () => { nCommits: 2, commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", head: "bp-prod-28f63db", @@ -415,7 +455,6 @@ describe("github pull request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 2368, @@ -444,7 +483,8 @@ describe("github pull request config parser", () => { nCommits: 2, commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", head: "bp-prod-28f63db", @@ -489,7 +529,6 @@ describe("github pull request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 2368, @@ -518,7 +557,8 @@ describe("github pull request config parser", () => { nCommits: 2, commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", head: "bp-prod-28f63db", @@ -552,7 +592,6 @@ describe("github pull request config parser", () => { email: "noreply@github.com" }); expect(configs.auth).toEqual(undefined); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 2368, @@ -580,7 +619,8 @@ describe("github pull request config parser", () => { nCommits: 2, commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", head: "bp-prod-28f63db", @@ -614,7 +654,6 @@ describe("github pull request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual("my-auth-token"); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 2368, @@ -643,7 +682,8 @@ describe("github pull request config parser", () => { nCommits: 2, commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", head: "bp-prod-28f63db", @@ -684,7 +724,6 @@ describe("github pull request config parser", () => { email: "noreply@github.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 8632, @@ -712,7 +751,8 @@ describe("github pull request config parser", () => { nCommits: 2, commits: ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", head: "bp-prod-0404fb9-11da4e3", @@ -758,7 +798,6 @@ describe("github pull request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 2368, @@ -787,7 +826,8 @@ describe("github pull request config parser", () => { nCommits: 2, commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "owner", repo: "reponame", head: "bp-prod-28f63db", 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 new file mode 100644 index 0000000..deff8de --- /dev/null +++ b/test/service/configs/pullrequest/gitlab-pr-configs-parser-multiple.test.ts @@ -0,0 +1,349 @@ +import { Args } from "@bp/service/args/args.types"; +import { Configs } from "@bp/service/configs/configs.types"; +import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser"; +import GitClientFactory from "@bp/service/git/git-client-factory"; +import { GitClientType } from "@bp/service/git/git.types"; +import { getAxiosMocked } from "../../../support/mock/git-client-mock-support"; +import { MERGED_SQUASHED_MR } from "../../../support/mock/gitlab-data"; +import GitLabClient from "@bp/service/git/gitlab/gitlab-client"; +import GitLabMapper from "@bp/service/git/gitlab/gitlab-mapper"; + +jest.spyOn(GitLabMapper.prototype, "mapPullRequest"); +jest.spyOn(GitLabClient.prototype, "getPullRequest"); + +jest.mock("axios", () => { + return { + create: jest.fn(() => ({ + get: getAxiosMocked, + })), + }; +}); + +describe("gitlab merge request config parser", () => { + + const mergedPRUrl = `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${MERGED_SQUASHED_MR.iid}`; + + let configParser: PullRequestConfigsParser; + + beforeAll(() => { + GitClientFactory.reset(); + GitClientFactory.getOrCreate(GitClientType.GITLAB, "whatever", "my.gitlab.host.com"); + }); + + beforeEach(() => { + configParser = new PullRequestConfigsParser(); + }); + + test("multiple backports", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "v1, v2, v3", + gitUser: "Me", + gitEmail: "me@email.com", + title: "New Title", + body: "New Body", + bodyPrefix: "New Body Prefix -", + reviewers: [], + assignees: ["user3", "user4"], + inheritReviewers: false, + labels: [], + inheritLabels: false, + comments: [], + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []); + + expect(configs.dryRun).toEqual(false); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.backportPullRequests.length).toEqual(3); + expect(configs.backportPullRequests).toEqual( + expect.arrayContaining([ + { + owner: "superuser", + repo: "backporting-example", + head: "bp-v1-ebb1eca", + base: "v1", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "superuser", + repo: "backporting-example", + head: "bp-v2-ebb1eca", + base: "v2", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "superuser", + repo: "backporting-example", + head: "bp-v3-ebb1eca", + base: "v3", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + } + ]) + ); + }); + + test("multiple backports ignore duplicates", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "v1, v2, v3, v1", + gitUser: "Me", + gitEmail: "me@email.com", + title: "New Title", + body: "New Body", + bodyPrefix: "New Body Prefix -", + reviewers: [], + assignees: ["user3", "user4"], + inheritReviewers: false, + labels: [], + inheritLabels: false, + comments: [], + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []); + + expect(configs.dryRun).toEqual(false); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.backportPullRequests.length).toEqual(3); + expect(configs.backportPullRequests).toEqual( + expect.arrayContaining([ + { + owner: "superuser", + repo: "backporting-example", + head: "bp-v1-ebb1eca", + base: "v1", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "superuser", + repo: "backporting-example", + head: "bp-v2-ebb1eca", + base: "v2", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "superuser", + repo: "backporting-example", + head: "bp-v3-ebb1eca", + base: "v3", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + } + ]) + ); + }); + + test("multiple backports with custom branch name", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "v1, v2, v3", + gitUser: "Me", + gitEmail: "me@email.com", + title: "New Title", + body: "New Body", + bodyPrefix: "New Body Prefix -", + reviewers: [], + assignees: ["user3", "user4"], + inheritReviewers: false, + labels: [], + inheritLabels: false, + comments: [], + bpBranchName: "custom-branch" + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []); + + expect(configs.dryRun).toEqual(false); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.backportPullRequests.length).toEqual(3); + expect(configs.backportPullRequests).toEqual( + expect.arrayContaining([ + { + owner: "superuser", + repo: "backporting-example", + head: "custom-branch-v1", + base: "v1", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "superuser", + repo: "backporting-example", + head: "custom-branch-v2", + base: "v2", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "superuser", + repo: "backporting-example", + head: "custom-branch-v3", + base: "v3", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + } + ]) + ); + }); + + test("multiple backports with multiple custom branch names", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "v1, v2, v3", + gitUser: "Me", + gitEmail: "me@email.com", + title: "New Title", + body: "New Body", + bodyPrefix: "New Body Prefix -", + reviewers: [], + assignees: ["user3", "user4"], + inheritReviewers: false, + labels: [], + inheritLabels: false, + comments: [], + bpBranchName: "custom1, custom2, custom3" + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1); + expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []); + + expect(configs.dryRun).toEqual(false); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.backportPullRequests.length).toEqual(3); + expect(configs.backportPullRequests).toEqual( + expect.arrayContaining([ + { + owner: "superuser", + repo: "backporting-example", + head: "custom1", + base: "v1", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "superuser", + repo: "backporting-example", + head: "custom2", + base: "v2", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + }, + { + owner: "superuser", + repo: "backporting-example", + head: "custom3", + base: "v3", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: [], + } + ]) + ); + }); + + test("multiple backports with incorrect number of bp branch names", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranch: "v1, v2, v3", + gitUser: "Me", + gitEmail: "me@email.com", + title: "New Title", + body: "New Body", + bodyPrefix: "New Body Prefix -", + reviewers: [], + assignees: ["user3", "user4"], + inheritReviewers: false, + labels: [], + inheritLabels: false, + comments: [], + bpBranchName: "custom-branch1, custom-branch2, custom-branch2, custom-branch3, custom-branch4", + }; + + await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("The number of backport branch names, if provided, must match the number of target branches or just one, provided 4 branch names instead"); + }); +}); \ No newline at end of file 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 691f061..75057ca 100644 --- a/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts @@ -100,7 +100,6 @@ describe("gitlab merge request config parser", () => { email: "noreply@gitlab.com" }); expect(configs.auth).toEqual(undefined); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 1, @@ -128,7 +127,8 @@ describe("gitlab merge request config parser", () => { nCommits: 1, commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", head: "bp-prod-ebb1eca", @@ -166,7 +166,6 @@ describe("gitlab merge request config parser", () => { expect(configs.dryRun).toEqual(true); expect(configs.auth).toEqual("whatever"); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual("/tmp/test"); expect(configs.git).toEqual({ user: "Gitlab", @@ -196,7 +195,6 @@ describe("gitlab merge request config parser", () => { expect(configs.dryRun).toEqual(true); expect(configs.auth).toEqual("whatever"); - expect(configs.targetBranch).toEqual("prod"); expect(configs.git).toEqual({ user: "Gitlab", email: "noreply@gitlab.com" @@ -276,7 +274,6 @@ describe("gitlab merge request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 1, @@ -304,7 +301,8 @@ describe("gitlab merge request config parser", () => { nCommits: 1, commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", head: "bp-prod-ebb1eca", @@ -347,7 +345,6 @@ describe("gitlab merge request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 1, @@ -375,7 +372,8 @@ describe("gitlab merge request config parser", () => { nCommits: 1, commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", head: "bp-prod-ebb1eca", @@ -418,7 +416,6 @@ describe("gitlab merge request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 1, @@ -446,7 +443,8 @@ describe("gitlab merge request config parser", () => { nCommits: 1, commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", head: "bp-prod-ebb1eca", @@ -491,7 +489,6 @@ describe("gitlab merge request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 1, @@ -519,7 +516,8 @@ describe("gitlab merge request config parser", () => { nCommits: 1, commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", head: "bp-prod-ebb1eca", @@ -552,7 +550,6 @@ describe("gitlab merge request config parser", () => { email: "noreply@gitlab.com" }); expect(configs.auth).toEqual(undefined); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 1, @@ -580,7 +577,8 @@ describe("gitlab merge request config parser", () => { nCommits: 1, commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", head: "bp-prod-ebb1eca", @@ -613,7 +611,6 @@ describe("gitlab merge request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual("my-token"); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 1, @@ -641,7 +638,8 @@ describe("gitlab merge request config parser", () => { nCommits: 1, commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", head: "bp-prod-ebb1eca", @@ -678,7 +676,6 @@ describe("gitlab merge request config parser", () => { expect(configs.dryRun).toEqual(true); expect(configs.auth).toEqual("whatever"); - expect(configs.targetBranch).toEqual("prod"); expect(configs.git).toEqual({ user: "Gitlab", email: "noreply@gitlab.com" @@ -710,7 +707,8 @@ describe("gitlab merge request config parser", () => { nCommits: 2, commits: ["e4dd336a4a20f394df6665994df382fb1d193a11", "974519f65c9e0ed65277cd71026657a09fca05e7"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", head: "bp-prod-e4dd336-974519f", @@ -756,7 +754,6 @@ describe("gitlab merge request config parser", () => { email: "me@email.com" }); expect(configs.auth).toEqual(""); - expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ number: 1, @@ -784,7 +781,8 @@ describe("gitlab merge request config parser", () => { nCommits: 1, commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] }); - expect(configs.backportPullRequest).toEqual({ + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ owner: "superuser", repo: "backporting-example", head: "bp-prod-ebb1eca", diff --git a/test/service/git/git-cli.test.ts b/test/service/git/git-cli.test.ts index 04c069f..2b9c364 100644 --- a/test/service/git/git-cli.test.ts +++ b/test/service/git/git-cli.test.ts @@ -114,4 +114,13 @@ describe("git cli service", () => { const output = spawnSync("git", ["cherry", "-v"], { cwd }).stdout.toString(); expect(output.includes(expressionToTest)).toBe(false); }); + + + test("git clone on already created repo", async () => { + await git.clone("remote", cwd, "tbranch"); + + // use rev-parse to double check the current branch is the expected one + const post = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd }).stdout.toString().trim(); + expect(post).toEqual("tbranch"); + }); }); \ No newline at end of file diff --git a/test/service/git/github/github-client.test.ts b/test/service/git/github/github-client.test.ts index 093ce07..97fc996 100644 --- a/test/service/git/github/github-client.test.ts +++ b/test/service/git/github/github-client.test.ts @@ -1,7 +1,7 @@ import GitClientFactory from "@bp/service/git/git-client-factory"; import { GitPullRequest, GitClientType } from "@bp/service/git/git.types"; import GitHubClient from "@bp/service/git/github/github-client"; -import { mergedPullRequestFixture, repo, targetOwner } from "../../../support/mock/github-data"; +import { MERGED_PR_FIXTURE, REPO, TARGET_OWNER } from "../../../support/mock/github-data"; import { mockGitHubClient } from "../../../support/mock/git-client-mock-support"; describe("github service", () => { @@ -22,7 +22,7 @@ describe("github service", () => { }); test("get pull request: success", async () => { - const res: GitPullRequest = await gitClient.getPullRequest(targetOwner, repo, mergedPullRequestFixture.number); + const res: GitPullRequest = await gitClient.getPullRequest(TARGET_OWNER, REPO, MERGED_PR_FIXTURE.number); expect(res.sourceRepo).toEqual({ owner: "fork", project: "reponame", diff --git a/test/service/runner/cli-github-runner.test.ts b/test/service/runner/cli-github-runner.test.ts index f5ebe36..bbd31da 100644 --- a/test/service/runner/cli-github-runner.test.ts +++ b/test/service/runner/cli-github-runner.test.ts @@ -6,7 +6,7 @@ import CLIArgsParser from "@bp/service/args/cli/cli-args-parser"; import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../support/utils"; import { mockGitHubClient } from "../../support/mock/git-client-mock-support"; import GitClientFactory from "@bp/service/git/git-client-factory"; -import { GitClientType } from "@bp/service/git/git.types"; +import { BackportPullRequest, GitClientType } from "@bp/service/git/git.types"; const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME = "./cli-github-runner-pr-merged-with-overrides.json"; const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT = { @@ -236,6 +236,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("same owner", async () => { @@ -281,6 +282,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("closed and not merged pull request", async () => { @@ -338,6 +340,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("override backporting pr data", async () => { @@ -396,6 +399,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("set empty reviewers", async () => { @@ -453,6 +457,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("set custom labels with inheritance", async () => { @@ -502,6 +507,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("set custom labels without inheritance", async () => { @@ -550,6 +556,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("using config file with overrides", async () => { @@ -594,6 +601,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); // to check: https://github.com/kiegroup/git-backporting/issues/52 @@ -641,6 +649,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("multiple commits pr", async () => { @@ -688,6 +697,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("too long bp branch name", async () => { @@ -741,6 +751,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("multiple commits pr with different strategy", async () => { @@ -792,6 +803,7 @@ describe("cli runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("additional pr comments", async () => { @@ -841,5 +853,255 @@ describe("cli runner", () => { comments: ["first comment", "second comment"], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); + }); + + test("with multiple target branches", async () => { + addProcessArgs([ + "-tb", + "v1, v2, v3", + "-pr", + "https://github.com/owner/reponame/pull/2368", + "-f", + "/tmp/folder" + ]); + + await runner.execute(); + + const cwd = "/tmp/folder"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(3); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1"); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2"); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v1-28f63db"); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v2-28f63db"); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v3-28f63db"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(3); + expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + + expect(GitCLIService.prototype.push).toBeCalledTimes(3); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v1-28f63db"); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v2-28f63db"); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v3-28f63db"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-v1-28f63db", + base: "v1", + title: "[v1] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-v2-28f63db", + base: "v2", + title: "[v2] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-v3-28f63db", + base: "v3", + title: "[v3] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(3); + }); + + test("with multiple target branches and multiple bp names", async () => { + addProcessArgs([ + "-tb", + "v1, v2, v3", + "-pr", + "https://github.com/owner/reponame/pull/2368", + "-f", + "/tmp/folder", + "--bp-branch-name", + "custom1, custom1, custom2, custom3", + ]); + + await runner.execute(); + + const cwd = "/tmp/folder"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(3); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1"); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2"); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom1"); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom2"); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom3"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(3); + expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + + expect(GitCLIService.prototype.push).toBeCalledTimes(3); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom1"); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom2"); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom3"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "custom1", + base: "v1", + title: "[v1] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "custom2", + base: "v2", + title: "[v2] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "custom3", + base: "v3", + title: "[v3] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(3); + }); + + test("with multiple target branches and one failure", async () => { + jest.spyOn(GitHubClient.prototype, "createPullRequest").mockImplementation((_backport: BackportPullRequest) => { + + throw new Error("Mocked error"); + }); + + addProcessArgs([ + "-tb", + "v1, v2, v3", + "-pr", + "https://github.com/owner/reponame/pull/2368", + "-f", + "/tmp/folder", + "--bp-branch-name", + "custom-failure-head", + ]); + + await expect(() => runner.execute()).rejects.toThrowError("Failure occurred during one of the backports: [Error: Mocked error ; Error: Mocked error ; Error: Mocked error]"); + + const cwd = "/tmp/folder"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(3); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1"); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2"); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v1"); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v2"); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v3"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(3); + expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + + expect(GitCLIService.prototype.push).toBeCalledTimes(3); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v1"); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v2"); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v3"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "custom-failure-head-v1", + base: "v1", + title: "[v1] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "custom-failure-head-v2", + base: "v2", + title: "[v2] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "custom-failure-head-v3", + base: "v3", + title: "[v3] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toThrowError(); }); }); \ No newline at end of file diff --git a/test/service/runner/gha-github-runner.test.ts b/test/service/runner/gha-github-runner.test.ts index 44f098b..3e9fd73 100644 --- a/test/service/runner/gha-github-runner.test.ts +++ b/test/service/runner/gha-github-runner.test.ts @@ -128,6 +128,7 @@ describe("gha runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("closed and not merged pull request", async () => { @@ -181,6 +182,7 @@ describe("gha runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("override backporting pr data", async () => { @@ -231,6 +233,7 @@ describe("gha runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("set empty reviewers", async () => { @@ -282,6 +285,7 @@ describe("gha runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("set custom labels with inheritance", async () => { @@ -328,6 +332,7 @@ describe("gha runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("set custom labels without inheritance", async () => { @@ -374,6 +379,7 @@ describe("gha runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("using config file with overrides", async () => { @@ -417,6 +423,7 @@ describe("gha runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); // to check: https://github.com/kiegroup/git-backporting/issues/52 @@ -462,6 +469,7 @@ describe("gha runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("multiple commits pr", async () => { @@ -507,6 +515,7 @@ describe("gha runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("using github api url and different strategy", async () => { @@ -553,6 +562,7 @@ describe("gha runner", () => { comments: [], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); }); test("additional pr comments", async () => { @@ -598,5 +608,161 @@ describe("gha runner", () => { comments: ["first comment", "second comment"], } ); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1); + }); + + test("with multiple target branches", async () => { + spyGetInput({ + "target-branch": "v1, v2, v3", + "pull-request": "https://github.com/owner/reponame/pull/2368", + "folder": "/tmp/folder", + }); + + await runner.execute(); + + const cwd = "/tmp/folder"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(3); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1"); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2"); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v1-28f63db"); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v2-28f63db"); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v3-28f63db"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(3); + expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + + expect(GitCLIService.prototype.push).toBeCalledTimes(3); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v1-28f63db"); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v2-28f63db"); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v3-28f63db"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-v1-28f63db", + base: "v1", + title: "[v1] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-v2-28f63db", + base: "v2", + title: "[v2] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-v3-28f63db", + base: "v3", + title: "[v3] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(3); + }); + + test("with multiple target branches and single custom bp branch", async () => { + spyGetInput({ + "target-branch": "v1, v2, v3", + "pull-request": "https://github.com/owner/reponame/pull/2368", + "folder": "/tmp/folder", + "bp-branch-name": "custom" + }); + + await runner.execute(); + + const cwd = "/tmp/folder"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(3); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1"); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2"); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-v1"); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-v2"); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-v3"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(3); + expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined); + + expect(GitCLIService.prototype.push).toBeCalledTimes(3); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-v1"); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-v2"); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-v3"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "custom-v1", + base: "v1", + title: "[v1] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "custom-v2", + base: "v2", + title: "[v2] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "custom-v3", + base: "v3", + title: "[v3] 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: [], + }); + expect(GitHubClient.prototype.createPullRequest).toReturnTimes(3); }); }); \ No newline at end of file diff --git a/test/support/mock/git-client-mock-support.ts b/test/support/mock/git-client-mock-support.ts index e36db5e..985f799 100644 --- a/test/support/mock/git-client-mock-support.ts +++ b/test/support/mock/git-client-mock-support.ts @@ -1,8 +1,12 @@ import LoggerServiceFactory from "@bp/service/logger/logger-service-factory"; import { Moctokit } from "@kie/mock-github"; -import { targetOwner, repo, mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, notFoundPullRequestNumber, multipleCommitsPullRequestFixture, multipleCommitsPullRequestCommits } from "./github-data"; +import { TARGET_OWNER, REPO, MERGED_PR_FIXTURE, OPEN_PR_FIXTURE, NOT_MERGED_PR_FIXTURE, NOT_FOUND_PR_NUMBER, MULT_COMMITS_PR_FIXTURE, MULT_COMMITS_PR_COMMITS, NEW_PR_URL, NEW_PR_NUMBER } from "./github-data"; import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, OPEN_MR, OPEN_PR_COMMITS, PROJECT_EXAMPLE, SUPERUSER} from "./gitlab-data"; +// high number, for each test we are not expecting +// to send more than 3 reqs per api endpoint +const REPEAT = 20; + const logger = LoggerServiceFactory.getLogger(); // AXIOS @@ -94,76 +98,82 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit => // valid requests mock.rest.pulls .get({ - owner: targetOwner, - repo: repo, - pull_number: mergedPullRequestFixture.number + owner: TARGET_OWNER, + repo: REPO, + pull_number: MERGED_PR_FIXTURE.number }) .reply({ status: 200, - data: mergedPullRequestFixture + data: MERGED_PR_FIXTURE }); mock.rest.pulls .get({ - owner: targetOwner, - repo: repo, - pull_number: multipleCommitsPullRequestFixture.number + owner: TARGET_OWNER, + repo: REPO, + pull_number: MULT_COMMITS_PR_FIXTURE.number }) .reply({ status: 200, - data: multipleCommitsPullRequestFixture + data: MULT_COMMITS_PR_FIXTURE }); mock.rest.pulls .get({ - owner: targetOwner, - repo: repo, - pull_number: openPullRequestFixture.number + owner: TARGET_OWNER, + repo: REPO, + pull_number: OPEN_PR_FIXTURE.number }) .reply({ status: 200, - data: openPullRequestFixture + data: OPEN_PR_FIXTURE }); mock.rest.pulls .get({ - owner: targetOwner, - repo: repo, - pull_number: notMergedPullRequestFixture.number + owner: TARGET_OWNER, + repo: REPO, + pull_number: NOT_MERGED_PR_FIXTURE.number }) .reply({ status: 200, - data: notMergedPullRequestFixture + data: NOT_MERGED_PR_FIXTURE }); mock.rest.pulls .listCommits({ - owner: targetOwner, - repo: repo, - pull_number: multipleCommitsPullRequestFixture.number + owner: TARGET_OWNER, + repo: REPO, + pull_number: MULT_COMMITS_PR_FIXTURE.number }) .reply({ status: 200, - data: multipleCommitsPullRequestCommits + data: MULT_COMMITS_PR_COMMITS }); mock.rest.pulls .create() .reply({ + repeat: REPEAT, status: 201, - data: mergedPullRequestFixture + data: { + number: NEW_PR_NUMBER, + html_url: NEW_PR_URL, + } }); mock.rest.pulls .requestReviewers() .reply({ + repeat: REPEAT, status: 201, - data: mergedPullRequestFixture + data: MERGED_PR_FIXTURE }); mock.rest.issues .addAssignees() .reply({ + repeat: REPEAT, status: 201, data: {} }); @@ -171,6 +181,7 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit => mock.rest.issues .addLabels() .reply({ + repeat: REPEAT, status: 200, data: {} }); @@ -178,6 +189,7 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit => mock.rest.issues .createComment() .reply({ + repeat: REPEAT, status: 201, data: {} }); @@ -185,16 +197,17 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit => // invalid requests mock.rest.pulls .get({ - owner: targetOwner, - repo: repo, - pull_number: notFoundPullRequestNumber + owner: TARGET_OWNER, + repo: REPO, + pull_number: NOT_FOUND_PR_NUMBER }) .reply({ + repeat: REPEAT, status: 404, data: { message: "Not found" } }); - + return mock; }; diff --git a/test/support/mock/github-data.ts b/test/support/mock/github-data.ts index e91b723..8407834 100644 --- a/test/support/mock/github-data.ts +++ b/test/support/mock/github-data.ts @@ -1,9 +1,11 @@ -export const targetOwner = "owner"; -export const sourceOwner = "fork"; -export const repo = "reponame"; -export const notFoundPullRequestNumber = 1; +export const TARGET_OWNER = "owner"; +export const SOURCE_OWNER = "fork"; +export const REPO = "reponame"; +export const NOT_FOUND_PR_NUMBER = 1; +export const NEW_PR_URL = "new_pr_url"; +export const NEW_PR_NUMBER = 9999; -export const mergedPullRequestFixture = { +export const MERGED_PR_FIXTURE = { "url": "https://api.github.com/repos/owner/reponame/pulls/2368", "id": 1137188271, "node_id": "PR_kwDOABTq6s5DyB2v", @@ -474,7 +476,7 @@ export const mergedPullRequestFixture = { "changed_files": 2 }; -export const openPullRequestFixture = { +export const OPEN_PR_FIXTURE = { "url": "https://api.github.com/repos/owner/reponame/pulls/4444", "id": 1137188271, "node_id": "PR_kwDOABTq6s5DyB2v", @@ -898,7 +900,7 @@ export const openPullRequestFixture = { "changed_files": 2 }; -export const notMergedPullRequestFixture = { +export const NOT_MERGED_PR_FIXTURE = { "url": "https://api.github.com/repos/owner/reponame/pulls/6666", "id": 1137188271, "node_id": "PR_kwDOABTq6s5DyB2v", @@ -1341,7 +1343,7 @@ export const notMergedPullRequestFixture = { "changed_files": 2 }; -export const multipleCommitsPullRequestFixture = { +export const MULT_COMMITS_PR_FIXTURE = { "url": "https://api.github.com/repos/owner/reponame/pulls/8632", "id": 1137188271, "node_id": "PR_kwDOABTq6s5DyB2v", @@ -1804,7 +1806,7 @@ export const multipleCommitsPullRequestFixture = { "changed_files": 2 }; -export const multipleCommitsPullRequestCommits = [ +export const MULT_COMMITS_PR_COMMITS = [ { "sha": "0404fb922ab75c3a8aecad5c97d9af388df04695", "node_id": "C_kwDOImgs99oAKDA0MDRmYjkyMmFiNzVjM2E4YWVjYWQ1Yzk3ZDlhZjM4OGRmMDQ2OTU",