From 53cc505f17630fb30daa70f75895323325cc0c7d Mon Sep 17 00:00:00 2001 From: Andrea Lamparelli Date: Sat, 30 Mar 2024 19:19:17 +0100 Subject: [PATCH] feat(gh75): extract target branched from pr labels (#112) --- README.md | 3 +- action.yml | 72 +++++++--- dist/cli/index.js | 48 ++++++- dist/gha/index.js | 49 ++++++- src/service/args/args-parser.ts | 8 +- src/service/args/args.types.ts | 3 +- src/service/args/cli/cli-args-parser.ts | 2 + src/service/args/gha/gha-args-parser.ts | 3 +- .../configs/pullrequest/pr-configs-parser.ts | 39 +++++- test/service/args/cli/cli-args-parser.test.ts | 8 +- test/service/args/gha/gha-args-parser.test.ts | 6 +- .../github-pr-configs-parser-multiple.test.ts | 73 +++++++++- .../github-pr-configs-parser.test.ts | 125 ++++++++++++++++-- .../gitlab-pr-configs-parser.test.ts | 96 ++++++++++++-- test/service/runner/cli-github-runner.test.ts | 25 +++- test/service/runner/cli-gitlab-runner.test.ts | 4 +- test/service/runner/gha-github-runner.test.ts | 4 +- test/service/runner/gha-gitlab-runner.test.ts | 4 +- test/support/mock/github-data.ts | 32 ++++- test/support/mock/gitlab-data.ts | 2 +- 20 files changed, 523 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index e999d8a..6bf646e 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,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 Branches | -tb, --target-branch | N | Comma separated list of branches where the changes must be backported to | | +| Target Branches Pattern | -tbp, --target-branch-pattern | N | Regular expression pattern to extract target branch(es) from pr labels. The branches will be extracted from the pattern's required `target` named capturing group, e.g., `^backport (?([^ ]+))$` | | | 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 | Git access/authorization token, if provided all token env variables will be ignored. See [auth token](#authorization-token) section for more details | "" | @@ -126,7 +127,7 @@ This tool comes with some inputs that allow users to override the default behavi | Additional comments | --comments | N | Semicolon separated list of additional comments to be posted to the backported pull request | [] | | Dry Run | -d, --dry-run | N | If enabled the tool does not push nor create anything remotely, use this to skip PR creation | false | -> **NOTE**: `pull request` and `target branch` are *mandatory*, they must be provided as CLI options or as part of the configuration file (if used). +> **NOTE**: `pull request` and (`target branch` or `target branch pattern`) are *mandatory*, they must be provided as CLI options or as part of the configuration file (if used). #### Authorization token diff --git a/action.yml b/action.yml index d7b5281..33e04ee 100644 --- a/action.yml +++ b/action.yml @@ -1,77 +1,105 @@ name: "Backporting GitHub Action" -description: "GitHub action providing an automated way to backport pull requests from one branch to another" +description: GitHub action providing an automated way to backport pull requests from one branch to another inputs: pull-request: - description: "URL of the pull request to backport, e.g., https://github.com/kiegroup/git-backporting/pull/1" + description: > + URL of the pull request to backport, e.g., "https://github.com/kiegroup/git-backporting/pull/1" required: false target-branch: - description: "Comma separated list of branches where the pull request must be backported to" + description: > + Comma separated list of branches where the pull request must be backported to + required: false + target-branch-pattern: + description: > + Regular expression pattern to extract target branch(es) from pr labels. + The branches will be extracted from the pattern's required `target` named capturing group, + for instance "^backport (?([^ ]+))$" required: false config-file: - description: "Path to a file containing the json configuration for this tool, the object must match the Args interface" + description: > + Path to a file containing the json configuration for this tool, + the object must match the Args interface required: false dry-run: - description: "If enabled the tool does not create any pull request nor push anything remotely" + description: > + If enabled the tool does not create any pull request nor push anything remotely required: false default: "false" auth: - description: "GITHUB_TOKEN or a `repo` scoped Personal Access Token (PAT), if not provided will look for existing env variables like GITHUB_TOKEN" + description: > + GITHUB_TOKEN or a `repo` scoped Personal Access Token (PAT), + if not provided will look for existing env variables like GITHUB_TOKEN default: ${{ github.token }} required: false git-client: - description: "Git client type , if not set it is infered from pull-request" + description: > + Git client type , if not set it is infered from pull-request required: false git-user: - description: "Local git user name" + description: Local git user name default: "GitHub" required: false git-email: - description: "Local git user email" + description: Local git user email default: "noreply@github.com" required: false title: - description: "Backporting PR title. Default is the original PR title prefixed by the target branch" + description: > + Backporting PR title. Default is the original PR title prefixed by the target branch required: false body-prefix: - description: "Backporting PR body prefix. Default is `Backport: `" + description: > + Backporting PR body prefix. Default is `Backport: ` required: false body: - description: "Backporting PR body. Default is the original PR body" + description: > + Backporting PR body. Default is the original PR body required: false bp-branch-name: - description: "Comma separated list of backporting PR branch names. Default is auto-generated from commit and target branches" + 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" + description: > + Comma separated list of reviewers for the backporting pull request required: false assignees: - description: "Comma separated list of reviewers for the backporting pull request" + description: > + Comma separated list of reviewers for the backporting pull request required: false no-inherit-reviewers: - description: "Considered only if reviewers is empty, if true keep reviewers as empty list, otherwise inherit from original pull request" + description: > + Considered only if reviewers is empty, if true keep reviewers as empty list, + otherwise inherit from original pull request required: false default: "false" labels: - description: "Comma separated list of labels to be assigned to the backported pull request" + description: > + Comma separated list of labels to be assigned to the backported pull request required: false inherit-labels: - description: "If true the backported pull request will inherit labels from the original one" + description: > + If true the backported pull request will inherit labels from the original one required: false default: "false" no-squash: - description: "If set to true the tool will backport all commits as part of the pull request instead of the suqashed one" + description: > + If set to true the tool will backport all commits as part of the pull request + instead of the suqashed one required: false default: "false" strategy: - description: "Cherry-pick merge strategy" + description: Cherry-pick merge strategy required: false default: "recursive" strategy-option: - description: "Cherry-pick merge strategy option" + description: Cherry-pick merge strategy option required: false default: "theirs" comments: - description: "Semicolon separated list of additional comments to be posted to the backported pull request" + description: > + Semicolon separated list of additional comments to be posted to the backported pull request required: false runs: diff --git a/dist/cli/index.js b/dist/cli/index.js index bd91c1f..447d098 100755 --- a/dist/cli/index.js +++ b/dist/cli/index.js @@ -39,13 +39,17 @@ class ArgsParser { } parse() { const args = this.readArgs(); + if (!args.pullRequest) { + throw new Error("Missing option: pull request must be provided"); + } // validate and fill with defaults - if (!args.pullRequest || !args.targetBranch || args.targetBranch.trim().length == 0) { - throw new Error("Missing option: pull request and target branches must be provided"); + if ((!args.targetBranch || args.targetBranch.trim().length == 0) && !args.targetBranchPattern) { + throw new Error("Missing option: target branch(es) or target regular expression must be provided"); } return { pullRequest: args.pullRequest, targetBranch: args.targetBranch, + targetBranchPattern: args.targetBranchPattern, dryRun: this.getOrDefault(args.dryRun, false), auth: this.getOrDefault(args.auth), folder: this.getOrDefault(args.folder), @@ -181,6 +185,7 @@ class CLIArgsParser extends args_parser_1.default { .version(package_json_1.version) .description(package_json_1.description) .option("-tb, --target-branch ", "comma separated list of branches where changes must be backported to") + .option("-tbp, --target-branch-pattern ", "regular expression pattern to extract target branch(es) from pr labels, the branches will be extracted from the pattern's required `target` named capturing group") .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 authentication string, if not provided fallback by looking for existing env variables like GITHUB_TOKEN") @@ -218,6 +223,7 @@ class CLIArgsParser extends args_parser_1.default { auth: opts.auth, pullRequest: opts.pullRequest, targetBranch: opts.targetBranch, + targetBranchPattern: opts.targetBranchPattern, folder: opts.folder, gitClient: opts.gitClient, gitUser: opts.gitUser, @@ -331,7 +337,18 @@ 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))]; + let targetBranches = []; + if (args.targetBranchPattern) { + // parse labels to extract target branch(es) + targetBranches = this.getTargetBranchesFromLabels(args.targetBranchPattern, pr.labels); + if (targetBranches.length === 0) { + throw new Error(`Unable to extract target branches with regular expression "${args.targetBranchPattern}"`); + } + } + else { + // target branch must be provided if targetRegExp is missing + 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`); @@ -353,6 +370,28 @@ class PullRequestConfigsParser extends configs_parser_1.default { getDefaultFolder() { return "bp"; } + /** + * Parse the provided labels and return a list of target branches + * obtained by applying the provided pattern as regular expression extractor + * @param pattern reg exp pattern to extract target branch from label name + * @param labels list of labels to check + * @returns list of target branches + */ + getTargetBranchesFromLabels(pattern, labels) { + this.logger.debug(`Extracting branches from [${labels}] using ${pattern}`); + const regExp = new RegExp(pattern); + const branches = []; + for (const l of labels) { + const result = regExp.exec(l); + if (result?.groups) { + const { target } = result.groups; + if (target) { + branches.push(target); + } + } + } + return [...new Set(branches)]; + } /** * Create a backport pull request starting from the target branch and * the original pr to be backported @@ -5891,7 +5930,6 @@ var preservedUrlFields = [ "protocol", "query", "search", - "hash", ]; // Create handlers that pass events from native requests @@ -6325,7 +6363,7 @@ RedirectableRequest.prototype._processResponse = function (response) { redirectUrl.protocol !== "https:" || redirectUrl.host !== currentHost && !isSubdomain(redirectUrl.host, currentHost)) { - removeMatchingHeaders(/^(?:(?:proxy-)?authorization|cookie)$/i, this._options.headers); + removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers); } // Evaluate the beforeRedirect callback diff --git a/dist/gha/index.js b/dist/gha/index.js index b50c45c..11e617b 100755 --- a/dist/gha/index.js +++ b/dist/gha/index.js @@ -39,13 +39,17 @@ class ArgsParser { } parse() { const args = this.readArgs(); + if (!args.pullRequest) { + throw new Error("Missing option: pull request must be provided"); + } // validate and fill with defaults - if (!args.pullRequest || !args.targetBranch || args.targetBranch.trim().length == 0) { - throw new Error("Missing option: pull request and target branches must be provided"); + if ((!args.targetBranch || args.targetBranch.trim().length == 0) && !args.targetBranchPattern) { + throw new Error("Missing option: target branch(es) or target regular expression must be provided"); } return { pullRequest: args.pullRequest, targetBranch: args.targetBranch, + targetBranchPattern: args.targetBranchPattern, dryRun: this.getOrDefault(args.dryRun, false), auth: this.getOrDefault(args.auth), folder: this.getOrDefault(args.folder), @@ -186,7 +190,8 @@ class GHAArgsParser extends args_parser_1.default { dryRun: (0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("dry-run")), auth: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("auth")), pullRequest: (0, core_1.getInput)("pull-request"), - targetBranch: (0, core_1.getInput)("target-branch"), + targetBranch: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("target-branch")), + targetBranchPattern: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("target-reg-exp")), folder: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("folder")), gitClient: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("git-client")), gitUser: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("git-user")), @@ -300,7 +305,18 @@ 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))]; + let targetBranches = []; + if (args.targetBranchPattern) { + // parse labels to extract target branch(es) + targetBranches = this.getTargetBranchesFromLabels(args.targetBranchPattern, pr.labels); + if (targetBranches.length === 0) { + throw new Error(`Unable to extract target branches with regular expression "${args.targetBranchPattern}"`); + } + } + else { + // target branch must be provided if targetRegExp is missing + 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`); @@ -322,6 +338,28 @@ class PullRequestConfigsParser extends configs_parser_1.default { getDefaultFolder() { return "bp"; } + /** + * Parse the provided labels and return a list of target branches + * obtained by applying the provided pattern as regular expression extractor + * @param pattern reg exp pattern to extract target branch from label name + * @param labels list of labels to check + * @returns list of target branches + */ + getTargetBranchesFromLabels(pattern, labels) { + this.logger.debug(`Extracting branches from [${labels}] using ${pattern}`); + const regExp = new RegExp(pattern); + const branches = []; + for (const l of labels) { + const result = regExp.exec(l); + if (result?.groups) { + const { target } = result.groups; + if (target) { + branches.push(target); + } + } + } + return [...new Set(branches)]; + } /** * Create a backport pull request starting from the target branch and * the original pr to be backported @@ -7621,7 +7659,6 @@ var preservedUrlFields = [ "protocol", "query", "search", - "hash", ]; // Create handlers that pass events from native requests @@ -8055,7 +8092,7 @@ RedirectableRequest.prototype._processResponse = function (response) { redirectUrl.protocol !== "https:" || redirectUrl.host !== currentHost && !isSubdomain(redirectUrl.host, currentHost)) { - removeMatchingHeaders(/^(?:(?:proxy-)?authorization|cookie)$/i, this._options.headers); + removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers); } // Evaluate the beforeRedirect callback diff --git a/src/service/args/args-parser.ts b/src/service/args/args-parser.ts index bc71810..2c83a72 100644 --- a/src/service/args/args-parser.ts +++ b/src/service/args/args-parser.ts @@ -16,14 +16,18 @@ export default abstract class ArgsParser { public parse(): Args { const args = this.readArgs(); + if (!args.pullRequest) { + throw new Error("Missing option: pull request must be provided"); + } // validate and fill with defaults - if (!args.pullRequest || !args.targetBranch || args.targetBranch.trim().length == 0) { - throw new Error("Missing option: pull request and target branches must be provided"); + if ((!args.targetBranch || args.targetBranch.trim().length == 0) && !args.targetBranchPattern) { + throw new Error("Missing option: target branch(es) or target regular expression must be provided"); } return { pullRequest: args.pullRequest, targetBranch: args.targetBranch, + targetBranchPattern: args.targetBranchPattern, dryRun: this.getOrDefault(args.dryRun, false), auth: this.getOrDefault(args.auth), folder: this.getOrDefault(args.folder), diff --git a/src/service/args/args.types.ts b/src/service/args/args.types.ts index 2e34064..95ad4ea 100644 --- a/src/service/args/args.types.ts +++ b/src/service/args/args.types.ts @@ -3,7 +3,8 @@ */ export interface Args { // 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 + targetBranch?: string, // comma separated list of branches on the target repo where the change should be backported to + targetBranchPattern?: string, // regular expression to extract target branch(es) from pull request labels 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 diff --git a/src/service/args/cli/cli-args-parser.ts b/src/service/args/cli/cli-args-parser.ts index f96cb55..568ee68 100644 --- a/src/service/args/cli/cli-args-parser.ts +++ b/src/service/args/cli/cli-args-parser.ts @@ -11,6 +11,7 @@ export default class CLIArgsParser extends ArgsParser { .version(version) .description(description) .option("-tb, --target-branch ", "comma separated list of branches where changes must be backported to") + .option("-tbp, --target-branch-pattern ", "regular expression pattern to extract target branch(es) from pr labels, the branches will be extracted from the pattern's required `target` named capturing group") .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 authentication string, if not provided fallback by looking for existing env variables like GITHUB_TOKEN") @@ -49,6 +50,7 @@ export default class CLIArgsParser extends ArgsParser { auth: opts.auth, pullRequest: opts.pullRequest, targetBranch: opts.targetBranch, + targetBranchPattern: opts.targetBranchPattern, folder: opts.folder, gitClient: opts.gitClient, gitUser: opts.gitUser, diff --git a/src/service/args/gha/gha-args-parser.ts b/src/service/args/gha/gha-args-parser.ts index 9d0b04e..b650eae 100644 --- a/src/service/args/gha/gha-args-parser.ts +++ b/src/service/args/gha/gha-args-parser.ts @@ -16,7 +16,8 @@ export default class GHAArgsParser extends ArgsParser { dryRun: getAsBooleanOrDefault(getInput("dry-run")), auth: getOrUndefined(getInput("auth")), pullRequest: getInput("pull-request"), - targetBranch: getInput("target-branch"), + targetBranch: getOrUndefined(getInput("target-branch")), + targetBranchPattern: getOrUndefined(getInput("target-reg-exp")), folder: getOrUndefined(getInput("folder")), gitClient: getOrUndefined(getInput("git-client")), gitUser: getOrUndefined(getInput("git-user")), diff --git a/src/service/configs/pullrequest/pr-configs-parser.ts b/src/service/configs/pullrequest/pr-configs-parser.ts index cca2c8d..d212f98 100644 --- a/src/service/configs/pullrequest/pr-configs-parser.ts +++ b/src/service/configs/pullrequest/pr-configs-parser.ts @@ -26,7 +26,17 @@ export default class PullRequestConfigsParser extends ConfigsParser { const folder: string = args.folder ?? this.getDefaultFolder(); - const targetBranches: string[] = [...new Set(getAsCommaSeparatedList(args.targetBranch)!)]; + let targetBranches: string[] = []; + if (args.targetBranchPattern) { + // parse labels to extract target branch(es) + targetBranches = this.getTargetBranchesFromLabels(args.targetBranchPattern, pr.labels); + if (targetBranches.length === 0) { + throw new Error(`Unable to extract target branches with regular expression "${args.targetBranchPattern}"`); + } + } else { + // target branch must be provided if targetRegExp is missing + targetBranches = [...new Set(getAsCommaSeparatedList(args.targetBranch!)!)]; + } const bpBranchNames: string[] = [...new Set(args.bpBranchName ? (getAsCleanedCommaSeparatedList(args.bpBranchName) ?? []) : [])]; if (bpBranchNames.length > 1 && bpBranchNames.length != targetBranches.length) { @@ -52,6 +62,33 @@ export default class PullRequestConfigsParser extends ConfigsParser { return "bp"; } + /** + * Parse the provided labels and return a list of target branches + * obtained by applying the provided pattern as regular expression extractor + * @param pattern reg exp pattern to extract target branch from label name + * @param labels list of labels to check + * @returns list of target branches + */ + private getTargetBranchesFromLabels(pattern: string, labels: string[]): string[] { + this.logger.debug(`Extracting branches from [${labels}] using ${pattern}`); + const regExp = new RegExp(pattern); + + const branches: string[] = []; + for (const l of labels) { + const result = regExp.exec(l); + + if (result?.groups) { + const { target } = result.groups; + if (target){ + branches.push(target); + } + } + } + + + return [...new Set(branches)]; + } + /** * Create a backport pull request starting from the target branch and * the original pr to be backported diff --git a/test/service/args/cli/cli-args-parser.test.ts b/test/service/args/cli/cli-args-parser.test.ts index c8b462d..3da4db5 100644 --- a/test/service/args/cli/cli-args-parser.test.ts +++ b/test/service/args/cli/cli-args-parser.test.ts @@ -489,7 +489,7 @@ describe("cli args parser", () => { "https://localhost/whatever/pulls/1" ]); - expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + expect(() => parser.parse()).toThrowError("Missing option: target branch(es) or target regular expression must be provided"); }); test("invalid execution with missing mandatory target branch", () => { @@ -498,15 +498,15 @@ describe("cli args parser", () => { "https://localhost/whatever/pulls/1" ]); - expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + expect(() => parser.parse()).toThrowError("Missing option: target branch(es) or target regular expression must be provided"); }); - test("invalid execution with missin mandatory pull request", () => { + test("invalid execution with missing mandatory pull request", () => { addProcessArgs([ "-tb", "target", ]); - expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + expect(() => parser.parse()).toThrowError("Missing option: pull request 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 20642d9..f8d9003 100644 --- a/test/service/args/gha/gha-args-parser.test.ts +++ b/test/service/args/gha/gha-args-parser.test.ts @@ -296,7 +296,7 @@ describe("gha args parser", () => { "pull-request": "https://localhost/whatever/pulls/1" }); - expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + expect(() => parser.parse()).toThrowError("Missing option: target branch(es) or target regular expression must be provided"); }); test("invalid execution with missing mandatory target branch", () => { @@ -304,7 +304,7 @@ describe("gha args parser", () => { "pull-request": "https://localhost/whatever/pulls/1" }); - expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + expect(() => parser.parse()).toThrowError("Missing option: target branch(es) or target regular expression must be provided"); }); test("invalid execution with missin mandatory pull request", () => { @@ -312,6 +312,6 @@ describe("gha args parser", () => { "target-branch": "target,old", }); - expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided"); + expect(() => parser.parse()).toThrowError("Missing option: pull request 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 index eab1bfa..96e0ce0 100644 --- a/test/service/configs/pullrequest/github-pr-configs-parser-multiple.test.ts +++ b/test/service/configs/pullrequest/github-pr-configs-parser-multiple.test.ts @@ -353,7 +353,78 @@ describe("github pull request config parser", () => { dryRun: false, auth: "", pullRequest: multipleCommitsPRUrl, - targetBranch: "v1, v2, v3", + targetBranch: "v4, v5, v6", + 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-v4-0404fb9-11da4e3", + base: "v4", + title: "[v4] 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-v5-0404fb9-11da4e3", + base: "v5", + title: "[v5] 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-v6-0404fb9-11da4e3", + base: "v6", + title: "[v6] 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: [], + }, + ]) + ); + }); + + test("multiple extracted branches and multiple commits", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: multipleCommitsPRUrl, + targetBranchPattern: "^backport (?([^ ]+))$", gitUser: "GitHub", gitEmail: "noreply@github.com", reviewers: [], 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 f392cc7..758b0b4 100644 --- a/test/service/configs/pullrequest/github-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/github-pr-configs-parser.test.ts @@ -112,7 +112,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], - labels: ["original-label"], + labels: ["backport prod"], targetRepo: { owner: "owner", project: "reponame", @@ -281,7 +281,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], - labels: ["original-label"], + labels: ["backport prod"], targetRepo: { owner: "owner", project: "reponame", @@ -395,7 +395,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], - labels: ["original-label"], + labels: ["backport prod"], targetRepo: { owner: "owner", project: "reponame", @@ -467,7 +467,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], - labels: ["original-label"], + labels: ["backport prod"], targetRepo: { owner: "owner", project: "reponame", @@ -511,7 +511,7 @@ describe("github pull request config parser", () => { reviewers: [], assignees: ["user3", "user4"], inheritReviewers: false, - labels: ["custom-label", "original-label"], // also include the one inherited + labels: ["custom-label", "backport prod"], // also include the one inherited inheritLabels: true, }; @@ -541,7 +541,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], - labels: ["original-label"], + labels: ["backport prod"], targetRepo: { owner: "owner", project: "reponame", @@ -566,7 +566,7 @@ describe("github pull request config parser", () => { body: "New Body Prefix -New Body", reviewers: [], assignees: ["user3", "user4"], - labels: ["custom-label", "original-label"], + labels: ["custom-label", "backport prod"], comments: [], }); }); @@ -604,7 +604,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], - labels: ["original-label"], + labels: ["backport prod"], targetRepo: { owner: "owner", project: "reponame", @@ -666,7 +666,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], - labels: ["original-label"], + labels: ["backport prod"], targetRepo: { owner: "owner", project: "reponame", @@ -691,7 +691,7 @@ describe("github pull request config parser", () => { body: "New Body Prefix -New Body", reviewers: ["user1", "user2"], assignees: ["user3", "user4"], - labels: ["cherry-pick :cherries:", "original-label"], + labels: ["cherry-pick :cherries:", "backport prod"], comments: [], }); }); @@ -736,7 +736,11 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], - labels: [], + labels: [ + "backport v1", + "backport v2", + "backport v3", + ], targetRepo: { owner: "owner", project: "reponame", @@ -810,7 +814,104 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], - labels: ["original-label"], + labels: ["backport prod"], + targetRepo: { + owner: "owner", + project: "reponame", + cloneUrl: "https://github.com/owner/reponame.git" + }, + sourceRepo: { + owner: "fork", + project: "reponame", + cloneUrl: "https://github.com/fork/reponame.git" + }, + bpBranchName: undefined, + nCommits: 2, + commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"], + }); + 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: [], + assignees: ["user3", "user4"], + labels: [], + comments: ["First comment", "Second comment"], + }); + }); + + test("no extracted target branches from pr labels due to wrong group name", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranchPattern: "^backport (?([^ ]+))$", + 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: ["First comment", "Second comment"], + }; + + await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("Unable to extract target branches with regular expression"); + }); + + test("extract target branches from pr labels", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranchPattern: "^backport (?([^ ]+))$", + 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: ["First comment", "Second comment"], + }; + + 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.git).toEqual({ + user: "Me", + email: "me@email.com" + }); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.originalPullRequest).toEqual({ + number: 2368, + author: "gh-user", + url: "https://api.github.com/repos/owner/reponame/pulls/2368", + htmlUrl: "https://github.com/owner/reponame/pull/2368", + state: "closed", + merged: true, + mergedBy: "that-s-a-user", + title: "PR Title", + body: "Please review and merge", + reviewers: ["requested-gh-user", "gh-user"], + assignees: [], + labels: ["backport prod"], targetRepo: { owner: "owner", project: "reponame", 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 981cb25..b10ae17 100644 --- a/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts @@ -116,7 +116,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], - labels: ["gitlab-original-label"], + labels: ["backport-prod"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -290,7 +290,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], - labels: ["gitlab-original-label"], + labels: ["backport-prod"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -361,7 +361,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], - labels: ["gitlab-original-label"], + labels: ["backport-prod"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -432,7 +432,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], - labels: ["gitlab-original-label"], + labels: ["backport-prod"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -475,7 +475,7 @@ describe("gitlab merge request config parser", () => { reviewers: [], assignees: ["user3", "user4"], inheritReviewers: false, - labels: ["custom-label", "gitlab-original-label"], // also include the one inherited + labels: ["custom-label", "backport-prod"], // also include the one inherited inheritLabels: true, }; @@ -505,7 +505,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], - labels: ["gitlab-original-label"], + labels: ["backport-prod"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -529,7 +529,7 @@ describe("gitlab merge request config parser", () => { body: "New Body Prefix -New Body", reviewers: [], assignees: ["user3", "user4"], - labels: ["custom-label", "gitlab-original-label"], + labels: ["custom-label", "backport-prod"], comments: [], }); }); @@ -566,7 +566,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], - labels: ["gitlab-original-label"], + labels: ["backport-prod"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -627,7 +627,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], - labels: ["gitlab-original-label"], + labels: ["backport-prod"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -651,7 +651,7 @@ describe("gitlab merge request config parser", () => { body: "New Body Prefix -New Body", reviewers: [], assignees: ["user3", "user4"], - labels: ["cherry-pick :cherries:", "gitlab-original-label"], + labels: ["cherry-pick :cherries:", "backport-prod"], comments: [], }); }); @@ -770,7 +770,81 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], - labels: ["gitlab-original-label"], + labels: ["backport-prod"], + targetRepo: { + owner: "superuser", + project: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git" + }, + sourceRepo: { + owner: "superuser", + project: "backporting-example", + cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git" + }, + nCommits: 1, + commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] + }); + expect(configs.backportPullRequests.length).toEqual(1); + expect(configs.backportPullRequests[0]).toEqual({ + owner: "superuser", + repo: "backporting-example", + head: "bp-prod-ebb1eca", + base: "prod", + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: [], + comments: ["First comment", "Second comment"], + }); + }); + + test("extract target branches from pr labels", async () => { + const args: Args = { + dryRun: false, + auth: "", + pullRequest: mergedPRUrl, + targetBranchPattern: "^backport-(?([^ ]+))$", + 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: ["First comment", "Second comment"], + }; + + 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.git).toEqual({ + user: "Me", + email: "me@email.com" + }); + expect(configs.auth).toEqual(""); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.originalPullRequest).toEqual({ + number: 1, + author: "superuser", + url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1", + htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1", + state: "merged", + merged: true, + mergedBy: "superuser", + title: "Update test.txt", + body: "This is the body", + reviewers: ["superuser1", "superuser2"], + assignees: ["superuser"], + labels: ["backport-prod"], targetRepo: { owner: "superuser", project: "backporting-example", diff --git a/test/service/runner/cli-github-runner.test.ts b/test/service/runner/cli-github-runner.test.ts index 7d747b4..66aa218 100644 --- a/test/service/runner/cli-github-runner.test.ts +++ b/test/service/runner/cli-github-runner.test.ts @@ -471,7 +471,7 @@ describe("cli runner", () => { "-pr", "https://github.com/owner/reponame/pull/2368", "--labels", - "cherry-pick :cherries:, original-label", + "cherry-pick :cherries:, backport prod", "--inherit-labels", ]); @@ -507,7 +507,7 @@ describe("cli runner", () => { 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: ["cherry-pick :cherries:", "original-label"], + labels: ["cherry-pick :cherries:", "backport prod"], comments: [], } ); @@ -601,7 +601,7 @@ describe("cli runner", () => { body: "New Body Prefix - New Body", reviewers: [], assignees: ["user3", "user4"], - labels: ["cli github cherry pick :cherries:", "original-label"], + labels: ["cli github cherry pick :cherries:", "backport prod"], comments: [], } ); @@ -1163,4 +1163,23 @@ describe("cli runner", () => { // Not interested in all subsequent calls, already tested in other test cases }); + + test("extract target branch from label", async () => { + addProcessArgs([ + "--target-branch-pattern", + "^backport (?([^ ]+))$", + "-pr", + "https://github.com/owner/reponame/pull/2368" + ]); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitClientFactory.getOrCreate).toBeCalledTimes(1); + expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com"); + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "prod"); + }); }); \ No newline at end of file diff --git a/test/service/runner/cli-gitlab-runner.test.ts b/test/service/runner/cli-gitlab-runner.test.ts index b5e985a..376f444 100644 --- a/test/service/runner/cli-gitlab-runner.test.ts +++ b/test/service/runner/cli-gitlab-runner.test.ts @@ -408,7 +408,7 @@ describe("cli runner", () => { body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), reviewers: ["superuser"], assignees: [], - labels: ["cherry-pick :cherries:", "another-label", "gitlab-original-label"], + labels: ["cherry-pick :cherries:", "another-label", "backport-prod"], comments: [], } ); @@ -502,7 +502,7 @@ describe("cli runner", () => { body: expect.stringContaining("**This is a backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), reviewers: [], assignees: ["user3", "user4"], - labels: ["cli gitlab cherry pick :cherries:", "gitlab-original-label"], + labels: ["cli gitlab cherry pick :cherries:", "backport-prod"], comments: [], } ); diff --git a/test/service/runner/gha-github-runner.test.ts b/test/service/runner/gha-github-runner.test.ts index dfdf902..14190ba 100644 --- a/test/service/runner/gha-github-runner.test.ts +++ b/test/service/runner/gha-github-runner.test.ts @@ -331,7 +331,7 @@ describe("gha runner", () => { 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: ["cherry-pick :cherries:", "another-label", "original-label"], + labels: ["cherry-pick :cherries:", "another-label", "backport prod"], comments: [], } ); @@ -422,7 +422,7 @@ describe("gha runner", () => { body: "New Body Prefix - New Body", reviewers: [], assignees: ["user3", "user4"], - labels: ["gha github cherry pick :cherries:", "original-label"], + labels: ["gha github cherry pick :cherries:", "backport prod"], comments: [], } ); diff --git a/test/service/runner/gha-gitlab-runner.test.ts b/test/service/runner/gha-gitlab-runner.test.ts index 5212da8..9c691cc 100644 --- a/test/service/runner/gha-gitlab-runner.test.ts +++ b/test/service/runner/gha-gitlab-runner.test.ts @@ -336,7 +336,7 @@ describe("gha runner", () => { body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), reviewers: ["superuser"], assignees: [], - labels: ["cherry-pick :cherries:", "another-label", "gitlab-original-label"], + labels: ["cherry-pick :cherries:", "another-label", "backport-prod"], comments: [], } ); @@ -424,7 +424,7 @@ describe("gha runner", () => { body: expect.stringContaining("**This is a backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), reviewers: [], assignees: ["user3", "user4"], - labels: ["gha gitlab cherry pick :cherries:", "gitlab-original-label"], + labels: ["gha gitlab cherry pick :cherries:", "backport-prod"], comments: [], } ); diff --git a/test/support/mock/github-data.ts b/test/support/mock/github-data.ts index 8407834..5673fd7 100644 --- a/test/support/mock/github-data.ts +++ b/test/support/mock/github-data.ts @@ -96,8 +96,8 @@ export const MERGED_PR_FIXTURE = { { "id": 4901021057, "node_id": "LA_kwDOImgs2354988AAAABJB-lgQ", - "url": "https://api.github.com/repos/owner/reponame/labels/original-label", - "name": "original-label", + "url": "https://api.github.com/repos/owner/reponame/labels/backport-prod", + "name": "backport prod", "color": "AB975B", "default": false, "description": "" @@ -1431,7 +1431,33 @@ export const MULT_COMMITS_PR_FIXTURE = { ], "labels": [ - + { + "id": 4901021057, + "node_id": "LA_kwDOImgs2354988AAAABJB-lgQ", + "url": "https://api.github.com/repos/owner/reponame/labels/backport-v1", + "name": "backport v1", + "color": "AB975B", + "default": false, + "description": "" + }, + { + "id": 4901021057, + "node_id": "LA_kwDOImgs2354988AAAABJB-lgQ", + "url": "https://api.github.com/repos/owner/reponame/labels/backport-v2", + "name": "backport v2", + "color": "AB975B", + "default": false, + "description": "" + }, + { + "id": 4901021057, + "node_id": "LA_kwDOImgs2354988AAAABJB-lgQ", + "url": "https://api.github.com/repos/owner/reponame/labels/backport-v3", + "name": "backport v3", + "color": "AB975B", + "default": false, + "description": "" + } ], "milestone": null, "draft": false, diff --git a/test/support/mock/gitlab-data.ts b/test/support/mock/gitlab-data.ts index 3f32b81..5202925 100644 --- a/test/support/mock/gitlab-data.ts +++ b/test/support/mock/gitlab-data.ts @@ -401,7 +401,7 @@ export const MERGED_SQUASHED_MR = { "source_project_id":76316, "target_project_id":76316, "labels":[ - "gitlab-original-label" + "backport-prod" ], "draft":false, "work_in_progress":false,