diff --git a/README.md b/README.md index 2393f6f..b3480bd 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ci checks status - + npm version

@@ -94,6 +94,8 @@ This tool comes with some inputs that allow users to override the default behavi | 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 | bp-{target-branch}-{sha} | +| 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 | | 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). @@ -280,6 +282,8 @@ Every change must be submitted through a *GitHub* pull request (PR). Backporting > **Note**: you don't need to take care about typescript compilation and minifycation, there are automated [git hooks](./.husky) taking care of that! +**Hint**: if you are still in a `work in progress` branch and you want to push your changes remotely, consider adding `--no-verify` for both `commit` and `push`, e.g., `git push origin --no-verify` + ## License Backporting (BPer) open source project is licensed under the [MIT](./LICENSE) license. \ No newline at end of file diff --git a/action.yml b/action.yml index dcfe863..43c2fcc 100644 --- a/action.yml +++ b/action.yml @@ -48,6 +48,13 @@ inputs: 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" + required: false + inherit-labels: + description: "If true the backported pull request will inherit labels from the original one" + required: false + default: "false" runs: using: node16 diff --git a/dist/cli/index.js b/dist/cli/index.js index aec50b6..866fdad 100755 --- a/dist/cli/index.js +++ b/dist/cli/index.js @@ -58,6 +58,8 @@ class ArgsParser { reviewers: this.getOrDefault(args.reviewers, []), assignees: this.getOrDefault(args.assignees, []), inheritReviewers: this.getOrDefault(args.inheritReviewers, true), + labels: this.getOrDefault(args.labels, []), + inheritLabels: this.getOrDefault(args.inheritLabels, false), }; } } @@ -95,7 +97,7 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.readConfigFile = exports.parseArgs = void 0; +exports.getAsBooleanOrDefault = exports.getAsCommaSeparatedList = exports.getAsCleanedCommaSeparatedList = exports.getOrUndefined = exports.readConfigFile = exports.parseArgs = void 0; const fs = __importStar(__nccwpck_require__(7147)); /** * Parse the input configuation string as json object and @@ -117,6 +119,34 @@ function readConfigFile(pathToFile) { return parseArgs(asString); } exports.readConfigFile = readConfigFile; +/** + * Return the input only if it is not a blank or null string, otherwise returns undefined + * @param key input key + * @returns the value or undefined + */ +function getOrUndefined(value) { + return value !== "" ? value : undefined; +} +exports.getOrUndefined = getOrUndefined; +// get rid of inner spaces too +function getAsCleanedCommaSeparatedList(value) { + // trim the value + const trimmed = value.trim(); + return trimmed !== "" ? trimmed.replace(/\s/g, "").split(",") : undefined; +} +exports.getAsCleanedCommaSeparatedList = getAsCleanedCommaSeparatedList; +// preserve inner spaces +function getAsCommaSeparatedList(value) { + // trim the value + const trimmed = value.trim(); + return trimmed !== "" ? trimmed.split(",").map(v => v.trim()) : undefined; +} +exports.getAsCommaSeparatedList = getAsCommaSeparatedList; +function getAsBooleanOrDefault(value) { + const trimmed = value.trim(); + return trimmed !== "" ? trimmed.toLowerCase() === "true" : undefined; +} +exports.getAsBooleanOrDefault = getAsBooleanOrDefault; /***/ }), @@ -134,31 +164,28 @@ const args_parser_1 = __importDefault(__nccwpck_require__(3025)); const commander_1 = __nccwpck_require__(4379); const package_json_1 = __nccwpck_require__(6625); const args_utils_1 = __nccwpck_require__(8048); -function commaSeparatedList(value, _prev) { - // remove all whitespaces - const cleanedValue = value.trim(); - return cleanedValue !== "" ? cleanedValue.replace(/\s/g, "").split(",") : []; -} class CLIArgsParser extends args_parser_1.default { getCommand() { 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("-pr, --pull-request ", "pull request url, e.g., https://github.com/kiegroup/git-backporting/pull/1.") + .option("-tb, --target-branch ", "branch 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.") - .option("-gu, --git-user ", "local git user name, default is 'GitHub'.") - .option("-ge, --git-email ", "local git user email, default is 'noreply@github.com'.") - .option("-f, --folder ", "local folder where the repo will be checked out, e.g., /tmp/folder.") - .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("--reviewers ", "comma separated list of reviewers for the backporting pull request.", commaSeparatedList) - .option("--assignees ", "comma separated list of assignees for the backporting pull request.", commaSeparatedList) + .option("-a, --auth ", "git service authentication string, e.g., github token") + .option("-gu, --git-user ", "local git user name, default is 'GitHub'") + .option("-ge, --git-email ", "local git user email, default is 'noreply@github.com'") + .option("-f, --folder ", "local folder where the repo will be checked out, e.g., /tmp/folder") + .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("--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") - .option("-cf, --config-file ", "configuration file containing all valid options, the json must match Args interface."); + .option("--labels ", "comma separated list of labels to be assigned to the backported pull request", args_utils_1.getAsCommaSeparatedList) + .option("--inherit-labels", "if true the backported pull request will inherit labels from the original one") + .option("-cf, --config-file ", "configuration file containing all valid options, the json must match Args interface"); } readArgs() { const opts = this.getCommand() @@ -185,6 +212,8 @@ class CLIArgsParser extends args_parser_1.default { reviewers: opts.reviewers, assignees: opts.assignees, inheritReviewers: opts.inheritReviewers, + labels: opts.labels, + inheritLabels: opts.inheritLabels, }; } return args; @@ -292,12 +321,17 @@ class PullRequestConfigsParser extends configs_parser_1.default { } const bodyPrefix = args.bodyPrefix ?? `**Backport:** ${originalPullRequest.htmlUrl}\r\n\r\n`; const body = args.body ?? `${originalPullRequest.body}`; + const labels = args.labels ?? []; + if (args.inheritLabels) { + labels.push(...originalPullRequest.labels); + } return { author: args.gitUser ?? this.gitClient.getDefaultGitUser(), title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, body: `${bodyPrefix}${body}`, reviewers: [...new Set(reviewers)], assignees: [...new Set(args.assignees)], + labels: [...new Set(labels)], targetRepo: originalPullRequest.targetRepo, sourceRepo: originalPullRequest.targetRepo, branchName: args.bpBranchName, @@ -608,11 +642,24 @@ class GitHubClient { head: backport.head, base: backport.base, title: backport.title, - body: backport.body + body: backport.body, }); if (!data) { throw new Error("Pull request creation failed"); } + if (backport.labels.length > 0) { + try { + await this.octokit.issues.addLabels({ + owner: backport.owner, + repo: backport.repo, + issue_number: data.number, + labels: backport.labels, + }); + } + catch (error) { + this.logger.error(`Error setting labels: ${error}`); + } + } if (backport.reviewers.length > 0) { try { await this.octokit.pulls.requestReviewers({ @@ -690,6 +737,7 @@ class GitHubMapper { mergedBy: pr.merged_by?.login, reviewers: pr.requested_reviewers.filter(r => "login" in r).map((r => r?.login)), assignees: pr.assignees.filter(r => "login" in r).map(r => r.login), + labels: pr.labels.map(l => l.name), sourceRepo: await this.mapSourceRepo(pr), targetRepo: await this.mapTargetRepo(pr), nCommits: pr.commits, @@ -810,6 +858,18 @@ class GitLabClient { assignee_ids: [], }); const mr = data; + // labels + if (backport.labels.length > 0) { + try { + this.logger.info("Setting labels: " + backport.labels); + await this.client.put(`/projects/${projectId}/merge_requests/${mr.iid}`, { + labels: backport.labels.join(","), + }); + } + catch (error) { + this.logger.warn("Failure trying to update labels. " + error); + } + } // reviewers const reviewerIds = []; for (const r of backport.reviewers) { @@ -924,7 +984,6 @@ class GitLabMapper { } } async mapPullRequest(mr) { - // throw new Error("Method not implemented."); return { number: mr.iid, author: mr.author.username, @@ -937,6 +996,7 @@ class GitLabMapper { mergedBy: mr.merged_by?.username, reviewers: mr.reviewers?.map((r => r.username)) ?? [], assignees: mr.assignees?.map((r => r.username)) ?? [], + labels: mr.labels ?? [], sourceRepo: await this.mapSourceRepo(mr), targetRepo: await this.mapTargetRepo(mr), nCommits: 1, @@ -1151,6 +1211,7 @@ class Runner { body: backportPR.body, reviewers: backportPR.reviewers, assignees: backportPR.assignees, + labels: backportPR.labels, }; if (!configs.dryRun) { // 8. push the new branch to origin diff --git a/dist/gha/index.js b/dist/gha/index.js index dd6ce0d..497b6f4 100755 --- a/dist/gha/index.js +++ b/dist/gha/index.js @@ -58,6 +58,8 @@ class ArgsParser { reviewers: this.getOrDefault(args.reviewers, []), assignees: this.getOrDefault(args.assignees, []), inheritReviewers: this.getOrDefault(args.inheritReviewers, true), + labels: this.getOrDefault(args.labels, []), + inheritLabels: this.getOrDefault(args.inheritLabels, false), }; } } @@ -95,7 +97,7 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.readConfigFile = exports.parseArgs = void 0; +exports.getAsBooleanOrDefault = exports.getAsCommaSeparatedList = exports.getAsCleanedCommaSeparatedList = exports.getOrUndefined = exports.readConfigFile = exports.parseArgs = void 0; const fs = __importStar(__nccwpck_require__(7147)); /** * Parse the input configuation string as json object and @@ -117,6 +119,34 @@ function readConfigFile(pathToFile) { return parseArgs(asString); } exports.readConfigFile = readConfigFile; +/** + * Return the input only if it is not a blank or null string, otherwise returns undefined + * @param key input key + * @returns the value or undefined + */ +function getOrUndefined(value) { + return value !== "" ? value : undefined; +} +exports.getOrUndefined = getOrUndefined; +// get rid of inner spaces too +function getAsCleanedCommaSeparatedList(value) { + // trim the value + const trimmed = value.trim(); + return trimmed !== "" ? trimmed.replace(/\s/g, "").split(",") : undefined; +} +exports.getAsCleanedCommaSeparatedList = getAsCleanedCommaSeparatedList; +// preserve inner spaces +function getAsCommaSeparatedList(value) { + // trim the value + const trimmed = value.trim(); + return trimmed !== "" ? trimmed.split(",").map(v => v.trim()) : undefined; +} +exports.getAsCommaSeparatedList = getAsCommaSeparatedList; +function getAsBooleanOrDefault(value) { + const trimmed = value.trim(); + return trimmed !== "" ? trimmed.toLowerCase() === "true" : undefined; +} +exports.getAsBooleanOrDefault = getAsBooleanOrDefault; /***/ }), @@ -134,46 +164,30 @@ const args_parser_1 = __importDefault(__nccwpck_require__(3025)); const core_1 = __nccwpck_require__(2186); const args_utils_1 = __nccwpck_require__(8048); class GHAArgsParser extends args_parser_1.default { - /** - * Return the input only if it is not a blank or null string, otherwise returns undefined - * @param key input key - * @returns the value or undefined - */ - getOrUndefined(key) { - const value = (0, core_1.getInput)(key); - return value !== "" ? value : undefined; - } - getAsCommaSeparatedList(key) { - // trim the value - const value = ((0, core_1.getInput)(key) ?? "").trim(); - return value !== "" ? value.replace(/\s/g, "").split(",") : undefined; - } - getAsBooleanOrDefault(key) { - const value = (0, core_1.getInput)(key).trim(); - return value !== "" ? value.toLowerCase() === "true" : undefined; - } readArgs() { - const configFile = this.getOrUndefined("config-file"); + const configFile = (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("config-file")); let args; if (configFile) { args = (0, args_utils_1.readConfigFile)(configFile); } else { args = { - dryRun: this.getAsBooleanOrDefault("dry-run"), - auth: this.getOrUndefined("auth"), + 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"), - folder: this.getOrUndefined("folder"), - gitUser: this.getOrUndefined("git-user"), - gitEmail: this.getOrUndefined("git-email"), - title: this.getOrUndefined("title"), - body: this.getOrUndefined("body"), - bodyPrefix: this.getOrUndefined("body-prefix"), - bpBranchName: this.getOrUndefined("bp-branch-name"), - reviewers: this.getAsCommaSeparatedList("reviewers"), - assignees: this.getAsCommaSeparatedList("assignees"), - inheritReviewers: !this.getAsBooleanOrDefault("no-inherit-reviewers"), + folder: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("folder")), + gitUser: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("git-user")), + gitEmail: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("git-email")), + title: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("title")), + body: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("body")), + bodyPrefix: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("body-prefix")), + bpBranchName: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("bp-branch-name")), + reviewers: (0, args_utils_1.getAsCleanedCommaSeparatedList)((0, core_1.getInput)("reviewers")), + assignees: (0, args_utils_1.getAsCleanedCommaSeparatedList)((0, core_1.getInput)("assignees")), + inheritReviewers: !(0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("no-inherit-reviewers")), + labels: (0, args_utils_1.getAsCommaSeparatedList)((0, core_1.getInput)("labels")), + inheritLabels: (0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("inherit-labels")), }; } return args; @@ -281,12 +295,17 @@ class PullRequestConfigsParser extends configs_parser_1.default { } const bodyPrefix = args.bodyPrefix ?? `**Backport:** ${originalPullRequest.htmlUrl}\r\n\r\n`; const body = args.body ?? `${originalPullRequest.body}`; + const labels = args.labels ?? []; + if (args.inheritLabels) { + labels.push(...originalPullRequest.labels); + } return { author: args.gitUser ?? this.gitClient.getDefaultGitUser(), title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, body: `${bodyPrefix}${body}`, reviewers: [...new Set(reviewers)], assignees: [...new Set(args.assignees)], + labels: [...new Set(labels)], targetRepo: originalPullRequest.targetRepo, sourceRepo: originalPullRequest.targetRepo, branchName: args.bpBranchName, @@ -597,11 +616,24 @@ class GitHubClient { head: backport.head, base: backport.base, title: backport.title, - body: backport.body + body: backport.body, }); if (!data) { throw new Error("Pull request creation failed"); } + if (backport.labels.length > 0) { + try { + await this.octokit.issues.addLabels({ + owner: backport.owner, + repo: backport.repo, + issue_number: data.number, + labels: backport.labels, + }); + } + catch (error) { + this.logger.error(`Error setting labels: ${error}`); + } + } if (backport.reviewers.length > 0) { try { await this.octokit.pulls.requestReviewers({ @@ -679,6 +711,7 @@ class GitHubMapper { mergedBy: pr.merged_by?.login, reviewers: pr.requested_reviewers.filter(r => "login" in r).map((r => r?.login)), assignees: pr.assignees.filter(r => "login" in r).map(r => r.login), + labels: pr.labels.map(l => l.name), sourceRepo: await this.mapSourceRepo(pr), targetRepo: await this.mapTargetRepo(pr), nCommits: pr.commits, @@ -799,6 +832,18 @@ class GitLabClient { assignee_ids: [], }); const mr = data; + // labels + if (backport.labels.length > 0) { + try { + this.logger.info("Setting labels: " + backport.labels); + await this.client.put(`/projects/${projectId}/merge_requests/${mr.iid}`, { + labels: backport.labels.join(","), + }); + } + catch (error) { + this.logger.warn("Failure trying to update labels. " + error); + } + } // reviewers const reviewerIds = []; for (const r of backport.reviewers) { @@ -913,7 +958,6 @@ class GitLabMapper { } } async mapPullRequest(mr) { - // throw new Error("Method not implemented."); return { number: mr.iid, author: mr.author.username, @@ -926,6 +970,7 @@ class GitLabMapper { mergedBy: mr.merged_by?.username, reviewers: mr.reviewers?.map((r => r.username)) ?? [], assignees: mr.assignees?.map((r => r.username)) ?? [], + labels: mr.labels ?? [], sourceRepo: await this.mapSourceRepo(mr), targetRepo: await this.mapTargetRepo(mr), nCommits: 1, @@ -1140,6 +1185,7 @@ class Runner { body: backportPR.body, reviewers: backportPR.reviewers, assignees: backportPR.assignees, + labels: backportPR.labels, }; if (!configs.dryRun) { // 8. push the new branch to origin diff --git a/src/service/args/args-parser.ts b/src/service/args/args-parser.ts index e2bddb9..c36c35c 100644 --- a/src/service/args/args-parser.ts +++ b/src/service/args/args-parser.ts @@ -36,6 +36,8 @@ export default abstract class ArgsParser { reviewers: this.getOrDefault(args.reviewers, []), assignees: this.getOrDefault(args.assignees, []), inheritReviewers: this.getOrDefault(args.inheritReviewers, true), + labels: this.getOrDefault(args.labels, []), + inheritLabels: this.getOrDefault(args.inheritLabels, false), }; } } \ No newline at end of file diff --git a/src/service/args/args-utils.ts b/src/service/args/args-utils.ts index b449fd4..13a3606 100644 --- a/src/service/args/args-utils.ts +++ b/src/service/args/args-utils.ts @@ -19,4 +19,32 @@ export function parseArgs(configFileContent: string): Args { export function readConfigFile(pathToFile: string): Args { const asString: string = fs.readFileSync(pathToFile, "utf-8"); return parseArgs(asString); +} + +/** + * Return the input only if it is not a blank or null string, otherwise returns undefined + * @param key input key + * @returns the value or undefined + */ +export function getOrUndefined(value: string): string | undefined { + return value !== "" ? value : undefined; +} + +// get rid of inner spaces too +export function getAsCleanedCommaSeparatedList(value: string): string[] | undefined { + // trim the value + const trimmed: string = value.trim(); + return trimmed !== "" ? trimmed.replace(/\s/g, "").split(",") : undefined; +} + +// preserve inner spaces +export function getAsCommaSeparatedList(value: string): string[] | undefined { + // trim the value + const trimmed: string = value.trim(); + return trimmed !== "" ? trimmed.split(",").map(v => v.trim()) : undefined; +} + +export function getAsBooleanOrDefault(value: string): boolean | undefined { + const trimmed = value.trim(); + return trimmed !== "" ? trimmed.toLowerCase() === "true" : undefined; } \ No newline at end of file diff --git a/src/service/args/args.types.ts b/src/service/args/args.types.ts index 6279136..e1ada50 100644 --- a/src/service/args/args.types.ts +++ b/src/service/args/args.types.ts @@ -16,4 +16,6 @@ export interface Args { reviewers?: string[], // backport pr reviewers assignees?: string[], // backport pr assignees inheritReviewers?: boolean, // if true and reviewers == [] then inherit reviewers from original pr + labels?: string[], // backport pr labels + inheritLabels?: boolean, // if true inherit labels from original pr } \ No newline at end of file diff --git a/src/service/args/cli/cli-args-parser.ts b/src/service/args/cli/cli-args-parser.ts index fe42b9b..e3297d1 100644 --- a/src/service/args/cli/cli-args-parser.ts +++ b/src/service/args/cli/cli-args-parser.ts @@ -2,13 +2,7 @@ import ArgsParser from "@bp/service/args/args-parser"; import { Args } from "@bp/service/args/args.types"; import { Command } from "commander"; import { name, version, description } from "@bp/../package.json"; -import { readConfigFile } from "@bp/service/args/args-utils"; - -function commaSeparatedList(value: string, _prev: unknown): string[] { - // remove all whitespaces - const cleanedValue: string = value.trim(); - return cleanedValue !== "" ? cleanedValue.replace(/\s/g, "").split(",") : []; -} +import { getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, readConfigFile } from "@bp/service/args/args-utils"; export default class CLIArgsParser extends ArgsParser { @@ -16,21 +10,23 @@ 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("-pr, --pull-request ", "pull request url, e.g., https://github.com/kiegroup/git-backporting/pull/1.") + .option("-tb, --target-branch ", "branch 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.") - .option("-gu, --git-user ", "local git user name, default is 'GitHub'.") - .option("-ge, --git-email ", "local git user email, default is 'noreply@github.com'.") - .option("-f, --folder ", "local folder where the repo will be checked out, e.g., /tmp/folder.") - .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("--reviewers ", "comma separated list of reviewers for the backporting pull request.", commaSeparatedList) - .option("--assignees ", "comma separated list of assignees for the backporting pull request.", commaSeparatedList) + .option("-a, --auth ", "git service authentication string, e.g., github token") + .option("-gu, --git-user ", "local git user name, default is 'GitHub'") + .option("-ge, --git-email ", "local git user email, default is 'noreply@github.com'") + .option("-f, --folder ", "local folder where the repo will be checked out, e.g., /tmp/folder") + .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("--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") - .option("-cf, --config-file ", "configuration file containing all valid options, the json must match Args interface."); + .option("--labels ", "comma separated list of labels to be assigned to the backported pull request", getAsCommaSeparatedList) + .option("--inherit-labels", "if true the backported pull request will inherit labels from the original one") + .option("-cf, --config-file ", "configuration file containing all valid options, the json must match Args interface"); } readArgs(): Args { @@ -58,6 +54,8 @@ export default class CLIArgsParser extends ArgsParser { reviewers: opts.reviewers, assignees: opts.assignees, inheritReviewers: opts.inheritReviewers, + labels: opts.labels, + inheritLabels: opts.inheritLabels, }; } diff --git a/src/service/args/gha/gha-args-parser.ts b/src/service/args/gha/gha-args-parser.ts index 74debbd..d06e40e 100644 --- a/src/service/args/gha/gha-args-parser.ts +++ b/src/service/args/gha/gha-args-parser.ts @@ -1,53 +1,34 @@ import ArgsParser from "@bp/service/args/args-parser"; import { Args } from "@bp/service/args/args.types"; import { getInput } from "@actions/core"; -import { readConfigFile } from "@bp/service/args/args-utils"; +import { getAsBooleanOrDefault, getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, getOrUndefined, readConfigFile } from "@bp/service/args/args-utils"; export default class GHAArgsParser extends ArgsParser { - /** - * Return the input only if it is not a blank or null string, otherwise returns undefined - * @param key input key - * @returns the value or undefined - */ - public getOrUndefined(key: string): string | undefined { - const value = getInput(key); - return value !== "" ? value : undefined; - } - - public getAsCommaSeparatedList(key: string): string[] | undefined { - // trim the value - const value: string = (getInput(key) ?? "").trim(); - return value !== "" ? value.replace(/\s/g, "").split(",") : undefined; - } - - private getAsBooleanOrDefault(key: string): boolean | undefined { - const value = getInput(key).trim(); - return value !== "" ? value.toLowerCase() === "true" : undefined; - } - readArgs(): Args { - const configFile = this.getOrUndefined("config-file"); + const configFile = getOrUndefined(getInput("config-file")); let args: Args; if (configFile) { args = readConfigFile(configFile); } else { args = { - dryRun: this.getAsBooleanOrDefault("dry-run"), - auth: this.getOrUndefined("auth"), + dryRun: getAsBooleanOrDefault(getInput("dry-run")), + auth: getOrUndefined(getInput("auth")), pullRequest: getInput("pull-request"), targetBranch: getInput("target-branch"), - folder: this.getOrUndefined("folder"), - gitUser: this.getOrUndefined("git-user"), - gitEmail: this.getOrUndefined("git-email"), - title: this.getOrUndefined("title"), - body: this.getOrUndefined("body"), - bodyPrefix: this.getOrUndefined("body-prefix"), - bpBranchName: this.getOrUndefined("bp-branch-name"), - reviewers: this.getAsCommaSeparatedList("reviewers"), - assignees: this.getAsCommaSeparatedList("assignees"), - inheritReviewers: !this.getAsBooleanOrDefault("no-inherit-reviewers"), + folder: getOrUndefined(getInput("folder")), + gitUser: getOrUndefined(getInput("git-user")), + gitEmail: getOrUndefined(getInput("git-email")), + title: getOrUndefined(getInput("title")), + body: getOrUndefined(getInput("body")), + bodyPrefix: getOrUndefined(getInput("body-prefix")), + bpBranchName: getOrUndefined(getInput("bp-branch-name")), + reviewers: getAsCleanedCommaSeparatedList(getInput("reviewers")), + assignees: getAsCleanedCommaSeparatedList(getInput("assignees")), + inheritReviewers: !getAsBooleanOrDefault(getInput("no-inherit-reviewers")), + labels: getAsCommaSeparatedList(getInput("labels")), + inheritLabels: getAsBooleanOrDefault(getInput("inherit-labels")), }; } diff --git a/src/service/configs/pullrequest/pr-configs-parser.ts b/src/service/configs/pullrequest/pr-configs-parser.ts index f54ba5e..6ca50da 100644 --- a/src/service/configs/pullrequest/pr-configs-parser.ts +++ b/src/service/configs/pullrequest/pr-configs-parser.ts @@ -63,12 +63,18 @@ export default class PullRequestConfigsParser extends ConfigsParser { const bodyPrefix = args.bodyPrefix ?? `**Backport:** ${originalPullRequest.htmlUrl}\r\n\r\n`; const body = args.body ?? `${originalPullRequest.body}`; + const labels = args.labels ?? []; + if (args.inheritLabels) { + labels.push(...originalPullRequest.labels); + } + return { author: args.gitUser ?? this.gitClient.getDefaultGitUser(), title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, body: `${bodyPrefix}${body}`, reviewers: [...new Set(reviewers)], assignees: [...new Set(args.assignees)], + labels: [...new Set(labels)], targetRepo: originalPullRequest.targetRepo, sourceRepo: originalPullRequest.targetRepo, branchName: args.bpBranchName, diff --git a/src/service/git/git.types.ts b/src/service/git/git.types.ts index 9a4da4d..4ca5405 100644 --- a/src/service/git/git.types.ts +++ b/src/service/git/git.types.ts @@ -10,6 +10,7 @@ export interface GitPullRequest { body: string, reviewers: string[], assignees: string[], + labels: string[], targetRepo: GitRepository, sourceRepo: GitRepository, nCommits?: number, // number of commits in the pr @@ -32,6 +33,7 @@ export interface BackportPullRequest { body: string, // pr body reviewers: string[], // pr list of reviewers assignees: string[], // pr list of assignees + labels: string[], // pr list of assigned labels branchName?: string, } diff --git a/src/service/git/github/github-client.ts b/src/service/git/github/github-client.ts index 6f79c0c..c7d8f81 100644 --- a/src/service/git/github/github-client.ts +++ b/src/service/git/github/github-client.ts @@ -59,13 +59,26 @@ export default class GitHubClient implements GitClient { head: backport.head, base: backport.base, title: backport.title, - body: backport.body + body: backport.body, }); if (!data) { throw new Error("Pull request creation failed"); } + if (backport.labels.length > 0) { + try { + await this.octokit.issues.addLabels({ + owner: backport.owner, + repo: backport.repo, + issue_number: (data as PullRequest).number, + labels: backport.labels, + }); + } catch (error) { + this.logger.error(`Error setting labels: ${error}`); + } + } + if (backport.reviewers.length > 0) { try { await this.octokit.pulls.requestReviewers({ diff --git a/src/service/git/github/github-mapper.ts b/src/service/git/github/github-mapper.ts index 6e33b9f..caa1bb1 100644 --- a/src/service/git/github/github-mapper.ts +++ b/src/service/git/github/github-mapper.ts @@ -26,6 +26,7 @@ export default class GitHubMapper implements GitResponseMapper "login" in r).map((r => (r as User)?.login)), assignees: pr.assignees.filter(r => "login" in r).map(r => r.login), + labels: pr.labels.map(l => l.name), sourceRepo: await this.mapSourceRepo(pr), targetRepo: await this.mapTargetRepo(pr), nCommits: pr.commits, diff --git a/src/service/git/gitlab/gitlab-client.ts b/src/service/git/gitlab/gitlab-client.ts index 8b7069d..427a0ac 100644 --- a/src/service/git/gitlab/gitlab-client.ts +++ b/src/service/git/gitlab/gitlab-client.ts @@ -72,6 +72,18 @@ export default class GitLabClient implements GitClient { const mr = data as MergeRequestSchema; + // labels + if (backport.labels.length > 0) { + try { + this.logger.info("Setting labels: " + backport.labels); + await this.client.put(`/projects/${projectId}/merge_requests/${mr.iid}`, { + labels: backport.labels.join(","), + }); + } catch(error) { + this.logger.warn("Failure trying to update labels. " + error); + } + } + // reviewers const reviewerIds: number[] = []; for(const r of backport.reviewers) { diff --git a/src/service/git/gitlab/gitlab-mapper.ts b/src/service/git/gitlab/gitlab-mapper.ts index 4aa4291..cc917a2 100644 --- a/src/service/git/gitlab/gitlab-mapper.ts +++ b/src/service/git/gitlab/gitlab-mapper.ts @@ -25,7 +25,6 @@ export default class GitLabMapper implements GitResponseMapper { - // throw new Error("Method not implemented."); return { number: mr.iid, author: mr.author.username, @@ -38,6 +37,7 @@ export default class GitLabMapper implements GitResponseMapper r.username)) ?? [], assignees: mr.assignees?.map((r => r.username)) ?? [], + labels: mr.labels ?? [], sourceRepo: await this.mapSourceRepo(mr), targetRepo: await this.mapTargetRepo(mr), nCommits: 1, // info not present on mr diff --git a/src/service/runner/runner.ts b/src/service/runner/runner.ts index 2bb4e1f..642cc78 100644 --- a/src/service/runner/runner.ts +++ b/src/service/runner/runner.ts @@ -100,6 +100,7 @@ export default class Runner { body: backportPR.body, reviewers: backportPR.reviewers, assignees: backportPR.assignees, + labels: backportPR.labels, }; if (!configs.dryRun) { diff --git a/test/service/args/args-utils.test.ts b/test/service/args/args-utils.test.ts index 57f5f30..508e5d7 100644 --- a/test/service/args/args-utils.test.ts +++ b/test/service/args/args-utils.test.ts @@ -1,5 +1,6 @@ -import { parseArgs, readConfigFile } from "@bp/service/args/args-utils"; -import { createTestFile, removeTestFile } from "../../support/utils"; +import { getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, getOrUndefined, parseArgs, readConfigFile } from "@bp/service/args/args-utils"; +import { createTestFile, expectArrayEqual, removeTestFile, spyGetInput } from "../../support/utils"; +import { getInput } from "@actions/core"; const RANDOM_CONFIG_FILE_CONTENT_PATHNAME = "./args-utils-test-random-config-file.json"; const RANDOM_CONFIG_FILE_CONTENT = { @@ -39,4 +40,39 @@ describe("args utils test suite", () => { test("check readConfigFile function", () => { expect(readConfigFile(RANDOM_CONFIG_FILE_CONTENT_PATHNAME)).toStrictEqual(RANDOM_CONFIG_FILE_CONTENT); }); + + test("gha getOrUndefined", () => { + spyGetInput({ + "present": "value", + "empty": "", + }); + expect(getOrUndefined(getInput("empty"))).toStrictEqual(undefined); + expect(getOrUndefined(getInput("present"))).toStrictEqual("value"); + }); + + test("gha getAsCleanedCommaSeparatedList", () => { + spyGetInput({ + "present": "value1, value2 , value3", + "empty": "", + "blank": " ", + "inner": " inner spaces ", + }); + expectArrayEqual(getAsCleanedCommaSeparatedList(getInput("present"))!, ["value1", "value2", "value3"]); + expect(getAsCleanedCommaSeparatedList(getInput("empty"))).toStrictEqual(undefined); + expect(getAsCleanedCommaSeparatedList(getInput("blank"))).toStrictEqual(undefined); + expect(getAsCleanedCommaSeparatedList(getInput("inner"))).toStrictEqual(["innerspaces"]); + }); + + test("gha getAsCommaSeparatedList", () => { + spyGetInput({ + "present": "value1, value2 , value3", + "empty": "", + "blank": " ", + "inner": " inner spaces ", + }); + expectArrayEqual(getAsCommaSeparatedList(getInput("present"))!, ["value1", "value2", "value3"]); + expect(getAsCommaSeparatedList(getInput("empty"))).toStrictEqual(undefined); + expect(getAsCommaSeparatedList(getInput("blank"))).toStrictEqual(undefined); + expectArrayEqual(getAsCommaSeparatedList(getInput("inner"))!, ["inner spaces"]); + }); }); \ 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 ff09dd0..d7b6338 100644 --- a/test/service/args/cli/cli-args-parser.test.ts +++ b/test/service/args/cli/cli-args-parser.test.ts @@ -24,6 +24,8 @@ const RANDOM_CONFIG_FILE_CONTENT = { "reviewers": ["reviewer1", "reviewer2"], "assignees": ["assignee1", "assignee2"], "inheritReviewers": true, + "labels": ["cherry-pick :cherries:"], + "inheritLabels": true, }; describe("cli args parser", () => { @@ -72,6 +74,8 @@ describe("cli args parser", () => { expect(args.reviewers).toEqual([]); expect(args.assignees).toEqual([]); expect(args.inheritReviewers).toEqual(true); + expect(args.labels).toEqual([]); + expect(args.inheritLabels).toEqual(false); }); test("with config file [default, short]", () => { @@ -95,6 +99,8 @@ describe("cli args parser", () => { expect(args.reviewers).toEqual([]); expect(args.assignees).toEqual([]); expect(args.inheritReviewers).toEqual(true); + expect(args.labels).toEqual([]); + expect(args.inheritLabels).toEqual(false); }); test("valid execution [default, long]", () => { @@ -120,6 +126,8 @@ describe("cli args parser", () => { expect(args.reviewers).toEqual([]); expect(args.assignees).toEqual([]); expect(args.inheritReviewers).toEqual(true); + expect(args.labels).toEqual([]); + expect(args.inheritLabels).toEqual(false); }); test("with config file [default, long]", () => { @@ -143,6 +151,8 @@ describe("cli args parser", () => { expect(args.reviewers).toEqual([]); expect(args.assignees).toEqual([]); expect(args.inheritReviewers).toEqual(true); + expect(args.labels).toEqual([]); + expect(args.inheritLabels).toEqual(false); }); test("valid execution [override, short]", () => { @@ -175,6 +185,8 @@ describe("cli args parser", () => { expect(args.reviewers).toEqual([]); expect(args.assignees).toEqual([]); expect(args.inheritReviewers).toEqual(true); + expect(args.labels).toEqual([]); + expect(args.inheritLabels).toEqual(false); }); test("valid execution [override, long]", () => { @@ -203,6 +215,9 @@ describe("cli args parser", () => { "--assignees", " pippo,pluto, paperino", "--no-inherit-reviewers", + "--labels", + "cherry-pick :cherries:, another spaced label", + "--inherit-labels", ]); const args: Args = parser.parse(); @@ -220,6 +235,8 @@ describe("cli args parser", () => { expectArrayEqual(args.reviewers!, ["al", "john", "jack"]); expectArrayEqual(args.assignees!, ["pippo", "pluto", "paperino"]); expect(args.inheritReviewers).toEqual(false); + expectArrayEqual(args.labels!, ["cherry-pick :cherries:", "another spaced label"]); + expect(args.inheritLabels).toEqual(true); }); test("override using config file", () => { @@ -243,6 +260,8 @@ describe("cli args parser", () => { expectArrayEqual(args.reviewers!, ["reviewer1", "reviewer2"]); expectArrayEqual(args.assignees!,["assignee1", "assignee2"]); expect(args.inheritReviewers).toEqual(true); + expectArrayEqual(args.labels!, ["cherry-pick :cherries:"]); + expect(args.inheritLabels).toEqual(true); }); test("ignore custom option when config file is set", () => { @@ -273,6 +292,9 @@ describe("cli args parser", () => { "--assignees", " pippo,pluto, paperino", "--no-inherit-reviewers", + "--labels", + "cherry-pick :cherries:, another spaced label", + "--inherit-labels", ]); const args: Args = parser.parse(); @@ -290,5 +312,7 @@ describe("cli args parser", () => { expectArrayEqual(args.reviewers!, ["reviewer1", "reviewer2"]); expectArrayEqual(args.assignees!,["assignee1", "assignee2"]); expect(args.inheritReviewers).toEqual(true); + expectArrayEqual(args.labels!, ["cherry-pick :cherries:"]); + expect(args.inheritLabels).toEqual(true); }); }); \ 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 130f7d5..0d42806 100644 --- a/test/service/args/gha/gha-args-parser.test.ts +++ b/test/service/args/gha/gha-args-parser.test.ts @@ -24,6 +24,8 @@ const RANDOM_CONFIG_FILE_CONTENT = { "reviewers": ["reviewer1", "reviewer2"], "assignees": ["assignee1", "assignee2"], "inheritReviewers": true, + "labels": ["cherry-pick :cherries:"], + "inheritLabels": true, }; describe("gha args parser", () => { @@ -50,26 +52,6 @@ describe("gha args parser", () => { jest.clearAllMocks(); }); - test("getOrUndefined", () => { - spyGetInput({ - "present": "value", - "empty": "", - }); - expect(parser.getOrUndefined("empty")).toStrictEqual(undefined); - expect(parser.getOrUndefined("present")).toStrictEqual("value"); - }); - - test("getAsCommaSeparatedList", () => { - spyGetInput({ - "present": "value1, value2 , value3", - "empty": "", - "blank": " ", - }); - expectArrayEqual(parser.getAsCommaSeparatedList("present")!, ["value1", "value2", "value3"]); - expect(parser.getAsCommaSeparatedList("empty")).toStrictEqual(undefined); - expect(parser.getAsCommaSeparatedList("blank")).toStrictEqual(undefined); - }); - test("valid execution [default]", () => { spyGetInput({ "target-branch": "target", @@ -89,6 +71,8 @@ describe("gha args parser", () => { expect(args.reviewers).toEqual([]); expect(args.assignees).toEqual([]); expect(args.inheritReviewers).toEqual(true); + expect(args.labels).toEqual([]); + expect(args.inheritLabels).toEqual(false); }); test("valid execution [override]", () => { @@ -106,6 +90,8 @@ describe("gha args parser", () => { "reviewers": "al , john, jack", "assignees": " pippo,pluto, paperino", "no-inherit-reviewers": "true", + "labels": "cherry-pick :cherries:, another spaced label", + "inherit-labels": "true" }); const args: Args = parser.parse(); @@ -123,6 +109,8 @@ describe("gha args parser", () => { expectArrayEqual(args.reviewers!, ["al", "john", "jack"]); expectArrayEqual(args.assignees!, ["pippo", "pluto", "paperino"]); expect(args.inheritReviewers).toEqual(false); + expectArrayEqual(args.labels!, ["cherry-pick :cherries:", "another spaced label"]); + expect(args.inheritLabels).toEqual(true); }); test("using config file", () => { @@ -145,6 +133,8 @@ describe("gha args parser", () => { expect(args.reviewers).toEqual([]); expect(args.assignees).toEqual([]); expect(args.inheritReviewers).toEqual(true); + expectArrayEqual(args.labels!, []); + expect(args.inheritLabels).toEqual(false); }); test("ignore custom options when using config file", () => { @@ -163,6 +153,8 @@ describe("gha args parser", () => { "reviewers": "al , john, jack", "assignees": " pippo,pluto, paperino", "no-inherit-reviewers": "true", + "labels": "cherry-pick :cherries:, another spaced label", + "inherit-labels": "false" }); const args: Args = parser.parse(); @@ -180,5 +172,7 @@ describe("gha args parser", () => { expectArrayEqual(args.reviewers!, ["reviewer1", "reviewer2"]); expectArrayEqual(args.assignees!,["assignee1", "assignee2"]); expect(args.inheritReviewers).toEqual(true); + expectArrayEqual(args.labels!, ["cherry-pick :cherries:"]); + expect(args.inheritLabels).toEqual(true); }); }); \ 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 5f5d8ae..4d5b1ba 100644 --- a/test/service/configs/pullrequest/github-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/github-pr-configs-parser.test.ts @@ -28,6 +28,8 @@ const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { "reviewers": ["user1", "user2"], "assignees": ["user3", "user4"], "inheritReviewers": true, // not taken into account + "labels": ["cherry-pick :cherries:"], + "inheritLabels": true, }; describe("github pull request config parser", () => { @@ -104,6 +106,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], + labels: ["original-label"], targetRepo: { owner: "owner", project: "reponame", @@ -125,6 +128,7 @@ describe("github pull request config parser", () => { 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: [], targetRepo: { owner: "owner", project: "reponame", @@ -199,6 +203,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["gh-user"], assignees: [], + labels: [], targetRepo: { owner: "owner", project: "reponame", @@ -233,7 +238,7 @@ describe("github pull request config parser", () => { }); - test("override backport pr data inherting reviewers", async () => { + test("override backport pr data inheriting reviewers", async () => { const args: Args = { dryRun: false, auth: "", @@ -271,6 +276,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], + labels: ["original-label"], targetRepo: { owner: "owner", project: "reponame", @@ -293,6 +299,7 @@ describe("github pull request config parser", () => { body: "New Body Prefix -New Body", reviewers: ["gh-user", "that-s-a-user"], assignees: [], + labels: [], targetRepo: { owner: "owner", project: "reponame", @@ -345,6 +352,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], + labels: ["original-label"], targetRepo: { owner: "owner", project: "reponame", @@ -367,6 +375,7 @@ describe("github pull request config parser", () => { body: "New Body Prefix -New Body", reviewers: ["user1", "user2"], assignees: ["user3", "user4"], + labels: [], targetRepo: { owner: "owner", project: "reponame", @@ -419,6 +428,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], + labels: ["original-label"], targetRepo: { owner: "owner", project: "reponame", @@ -441,6 +451,85 @@ describe("github pull request config parser", () => { body: "New Body Prefix -New Body", reviewers: [], assignees: ["user3", "user4"], + labels: [], + targetRepo: { + owner: "owner", + project: "reponame", + cloneUrl: "https://github.com/owner/reponame.git" + }, + sourceRepo: { + owner: "owner", + project: "reponame", + cloneUrl: "https://github.com/owner/reponame.git" + }, + bpBranchName: undefined, + }); + }); + + test("override backport pr custom labels with duplicates", 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: ["user3", "user4"], + inheritReviewers: false, + labels: ["custom-label", "original-label"], // also include the one inherited + inheritLabels: true, + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(configs.dryRun).toEqual(false); + expect(configs.git).toEqual({ + user: "Me", + 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, + 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: ["original-label"], + 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.backportPullRequest).toEqual({ + author: "Me", + url: undefined, + htmlUrl: undefined, + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: ["custom-label", "original-label"], targetRepo: { owner: "owner", project: "reponame", @@ -484,6 +573,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], + labels: ["original-label"], targetRepo: { owner: "owner", project: "reponame", @@ -505,6 +595,7 @@ describe("github pull request config parser", () => { 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: [], targetRepo: { owner: "owner", project: "reponame", @@ -548,6 +639,7 @@ describe("github pull request config parser", () => { body: "Please review and merge", reviewers: ["requested-gh-user", "gh-user"], assignees: [], + labels: ["original-label"], targetRepo: { owner: "owner", project: "reponame", @@ -570,6 +662,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"], 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 6049da5..9e47f1b 100644 --- a/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts @@ -28,6 +28,8 @@ const GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { "reviewers": [], "assignees": ["user3", "user4"], "inheritReviewers": false, + "labels": ["cherry-pick :cherries:"], + "inheritLabels": true, }; @@ -107,6 +109,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], + labels: ["gitlab-original-label"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -128,6 +131,7 @@ describe("gitlab merge request config parser", () => { body: "**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1\r\n\r\nThis is the body", reviewers: ["superuser"], assignees: [], + labels: [], targetRepo: { owner: "superuser", project: "backporting-example", @@ -203,6 +207,7 @@ describe("gitlab merge request config parser", () => { body: "Still opened mr body", reviewers: ["superuser"], assignees: ["superuser"], + labels: [], targetRepo: { owner: "superuser", project: "backporting-example", @@ -236,7 +241,7 @@ describe("gitlab merge request config parser", () => { expect(async () => await configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!"); }); - test("override backport pr data inherting reviewers", async () => { + test("override backport pr data inheriting reviewers", async () => { const args: Args = { dryRun: false, auth: "", @@ -274,6 +279,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], + labels: ["gitlab-original-label"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -295,6 +301,7 @@ describe("gitlab merge request config parser", () => { body: "New Body Prefix -New Body", reviewers: ["superuser"], assignees: [], + labels: [], targetRepo: { owner: "superuser", project: "backporting-example", @@ -347,6 +354,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], + labels: ["gitlab-original-label"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -368,6 +376,7 @@ describe("gitlab merge request config parser", () => { body: "New Body Prefix -New Body", reviewers: ["user1", "user2"], assignees: ["user3", "user4"], + labels: [], targetRepo: { owner: "superuser", project: "backporting-example", @@ -420,6 +429,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], + labels: ["gitlab-original-label"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -441,6 +451,7 @@ describe("gitlab merge request config parser", () => { body: "New Body Prefix -New Body", reviewers: [], assignees: ["user3", "user4"], + labels: [], targetRepo: { owner: "superuser", project: "backporting-example", @@ -455,6 +466,82 @@ describe("gitlab merge request config parser", () => { }); }); + test("override backport pr custom labels with duplicates", 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: ["user3", "user4"], + inheritReviewers: false, + labels: ["custom-label", "gitlab-original-label"], // also include the one inherited + inheritLabels: true, + }; + + const configs: Configs = await configParser.parseAndValidate(args); + + expect(configs.dryRun).toEqual(false); + expect(configs.git).toEqual({ + user: "Me", + 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, + 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: ["gitlab-original-label"], + 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.backportPullRequest).toEqual({ + author: "Me", + url: undefined, + htmlUrl: undefined, + title: "New Title", + body: "New Body Prefix -New Body", + reviewers: [], + assignees: ["user3", "user4"], + labels: ["custom-label", "gitlab-original-label"], + 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" + }, + bpBranchName: undefined, + }); + }); test("using simple config file", async () => { spyGetInput({ @@ -484,6 +571,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], + labels: ["gitlab-original-label"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -505,6 +593,7 @@ describe("gitlab merge request config parser", () => { body: "**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1\r\n\r\nThis is the body", reviewers: ["superuser"], assignees: [], + labels: [], targetRepo: { owner: "superuser", project: "backporting-example", @@ -547,6 +636,7 @@ describe("gitlab merge request config parser", () => { body: "This is the body", reviewers: ["superuser1", "superuser2"], assignees: ["superuser"], + labels: ["gitlab-original-label"], targetRepo: { owner: "superuser", project: "backporting-example", @@ -568,6 +658,7 @@ describe("gitlab merge request config parser", () => { body: "New Body Prefix -New Body", reviewers: [], assignees: ["user3", "user4"], + labels: ["cherry-pick :cherries:", "gitlab-original-label"], targetRepo: { owner: "superuser", project: "backporting-example", diff --git a/test/service/git/gitlab/gitlab-client.test.ts b/test/service/git/gitlab/gitlab-client.test.ts index 686ba2e..2e3e58d 100644 --- a/test/service/git/gitlab/gitlab-client.test.ts +++ b/test/service/git/gitlab/gitlab-client.test.ts @@ -92,6 +92,7 @@ describe("github service", () => { head: "bp-branch", reviewers: [], assignees: [], + labels: [], }; const url: string = await gitClient.createPullRequest(backport); @@ -121,6 +122,7 @@ describe("github service", () => { head: "bp-branch", reviewers: ["superuser", "invalid"], assignees: [], + labels: [], }; const url: string = await gitClient.createPullRequest(backport); @@ -155,6 +157,7 @@ describe("github service", () => { head: "bp-branch", reviewers: [], assignees: ["superuser", "invalid"], + labels: [], }; const url: string = await gitClient.createPullRequest(backport); @@ -189,6 +192,7 @@ describe("github service", () => { head: "bp-branch-2", reviewers: ["superuser", "invalid"], assignees: [], + labels: [], }; const url: string = await gitClient.createPullRequest(backport); @@ -223,6 +227,7 @@ describe("github service", () => { head: "bp-branch-2", reviewers: [], assignees: ["superuser", "invalid"], + labels: [], }; const url: string = await gitClient.createPullRequest(backport); @@ -246,4 +251,37 @@ describe("github service", () => { assignee_ids: [14041], }); }); + + test("create backport pull request with custom labels", async () => { + const backport: BackportPullRequest = { + title: "Backport Title", + body: "Backport Body", + owner: "superuser", + repo: "backporting-example", + base: "old/branch", + head: "bp-branch-2", + reviewers: [], + assignees: [], + labels: ["label1", "label2"], + }; + + const url: string = await gitClient.createPullRequest(backport); + expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + SECOND_NEW_GITLAB_MR_ID); + + // check axios invocation + expect(axiosInstanceSpy.post).toBeCalledTimes(1); + expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({ + source_branch: "bp-branch-2", + target_branch: "old/branch", + title: "Backport Title", + description: "Backport Body", + reviewer_ids: [], + assignee_ids: [], + })); + expect(axiosInstanceSpy.get).toBeCalledTimes(0); + expect(axiosInstanceSpy.put).toBeCalledTimes(1); // just labels + expect(axiosInstanceSpy.put).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + SECOND_NEW_GITLAB_MR_ID, { + labels: "label1,label2", + }); + }); }); \ No newline at end of file diff --git a/test/service/runner/cli-github-runner.test.ts b/test/service/runner/cli-github-runner.test.ts index 0b7a752..12a648b 100644 --- a/test/service/runner/cli-github-runner.test.ts +++ b/test/service/runner/cli-github-runner.test.ts @@ -21,6 +21,8 @@ const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT = { "reviewers": [], "assignees": ["user3", "user4"], "inheritReviewers": false, + "labels": ["cli github cherry pick :cherries:"], + "inheritLabels": true, }; jest.mock("@bp/service/git/git-cli"); @@ -218,6 +220,7 @@ describe("cli runner", () => { body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"), reviewers: ["gh-user", "that-s-a-user"], assignees: [], + labels: [], } ); }); @@ -258,6 +261,7 @@ describe("cli runner", () => { body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/8632"), reviewers: ["gh-user", "that-s-a-user"], assignees: [], + labels: [], } ); }); @@ -310,6 +314,7 @@ describe("cli runner", () => { body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/4444"), reviewers: ["gh-user"], assignees: [], + labels: [], } ); }); @@ -331,7 +336,7 @@ describe("cli runner", () => { "--reviewers", "user1,user2", "--assignees", - "user3,user4" + "user3,user4", ]); await runner.execute(); @@ -363,6 +368,7 @@ describe("cli runner", () => { body: "New Body Prefix - New Body", reviewers: ["user1", "user2"], assignees: ["user3", "user4"], + labels: [], } ); }); @@ -415,6 +421,96 @@ describe("cli runner", () => { body: "New Body Prefix - New Body", reviewers: [], assignees: ["user3", "user4"], + labels: [], + } + ); + }); + + test("set custom labels with inheritance", async () => { + addProcessArgs([ + "-tb", + "target", + "-pr", + "https://github.com/owner/reponame/pull/2368", + "--labels", + "cherry-pick :cherries:, original-label", + "--inherit-labels", + ]); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(1); + expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", + base: "target", + title: "[target] PR Title", + body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"), + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + labels: ["cherry-pick :cherries:", "original-label"], + } + ); + }); + + test("set custom lables without inheritance", async () => { + addProcessArgs([ + "-tb", + "target", + "-pr", + "https://github.com/owner/reponame/pull/2368", + "--labels", + "first-label, second-label ", + ]); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(1); + expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", + base: "target", + title: "[target] PR Title", + body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"), + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + labels: ["first-label", "second-label"], } ); }); @@ -454,6 +550,7 @@ describe("cli runner", () => { body: "New Body Prefix - New Body", reviewers: [], assignees: ["user3", "user4"], + labels: ["cli github cherry pick :cherries:", "original-label"], } ); }); diff --git a/test/service/runner/cli-gitlab-runner.test.ts b/test/service/runner/cli-gitlab-runner.test.ts index c8b9292..657a5fd 100644 --- a/test/service/runner/cli-gitlab-runner.test.ts +++ b/test/service/runner/cli-gitlab-runner.test.ts @@ -21,6 +21,8 @@ const GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { "reviewers": [], "assignees": ["user3", "user4"], "inheritReviewers": false, + "labels": ["cli gitlab cherry pick :cherries:"], + "inheritLabels": true, }; jest.mock("axios", () => { @@ -171,6 +173,7 @@ describe("cli runner", () => { body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"), reviewers: ["superuser"], assignees: [], + labels: [], } ); }); @@ -224,6 +227,7 @@ describe("cli runner", () => { body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), reviewers: ["superuser"], assignees: [], + labels: [], } ); }); @@ -278,6 +282,7 @@ describe("cli runner", () => { body: "New Body Prefix - New Body", reviewers: ["user1", "user2"], assignees: ["user3", "user4"], + labels: [], } ); }); @@ -330,6 +335,98 @@ describe("cli runner", () => { body: "New Body Prefix - New Body", reviewers: [], assignees: ["user3", "user4"], + labels: [], + } + ); + }); + + test("set custom labels with inheritance", async () => { + addProcessArgs([ + "-tb", + "target", + "-pr", + "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1", + "--labels", + "cherry-pick :cherries:, another-label", + "--inherit-labels", + ]); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + // 0 occurrences as the mr is already merged and the owner is the same for + // both source and target repositories + expect(GitCLIService.prototype.fetch).toBeCalledTimes(0); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({ + owner: "superuser", + repo: "backporting-example", + head: "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e", + base: "target", + title: "[target] Update test.txt", + 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"], + } + ); + }); + + test("set custom labels without inheritance", async () => { + addProcessArgs([ + "-tb", + "target", + "-pr", + "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1", + "--labels", + "cherry-pick :cherries:, another-label", + ]); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + // 0 occurrences as the mr is already merged and the owner is the same for + // both source and target repositories + expect(GitCLIService.prototype.fetch).toBeCalledTimes(0); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({ + owner: "superuser", + repo: "backporting-example", + head: "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e", + base: "target", + title: "[target] Update test.txt", + body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), + reviewers: ["superuser"], + assignees: [], + labels: ["cherry-pick :cherries:", "another-label"], } ); }); @@ -370,6 +467,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"], } ); }); diff --git a/test/service/runner/gha-github-runner.test.ts b/test/service/runner/gha-github-runner.test.ts index ab2236e..9b17fee 100644 --- a/test/service/runner/gha-github-runner.test.ts +++ b/test/service/runner/gha-github-runner.test.ts @@ -21,6 +21,8 @@ const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT = { "reviewers": [], "assignees": ["user3", "user4"], "inheritReviewers": false, + "labels": ["gha github cherry pick :cherries:"], + "inheritLabels": true, }; @@ -117,6 +119,7 @@ describe("gha runner", () => { body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"), reviewers: ["gh-user", "that-s-a-user"], assignees: [], + labels: [], } ); }); @@ -165,6 +168,7 @@ describe("gha runner", () => { body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/4444"), reviewers: ["gh-user"], assignees: [], + labels: [], } ); }); @@ -210,6 +214,7 @@ describe("gha runner", () => { body: "New Body Prefix - New Body", reviewers: ["user1", "user2"], assignees: ["user3", "user4"], + labels: [], } ); }); @@ -256,6 +261,91 @@ describe("gha runner", () => { body: "New Body Prefix - New Body", reviewers: [], assignees: ["user3", "user4"], + labels: [], + } + ); + }); + + test("set custom labels with inheritance", async () => { + spyGetInput({ + "target-branch": "target", + "pull-request": "https://github.com/owner/reponame/pull/2368", + "labels": "cherry-pick :cherries:, another-label", + "inherit-labels": "true", + }); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(1); + expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", + base: "target", + title: "[target] PR Title", + body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"), + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + labels: ["cherry-pick :cherries:", "another-label", "original-label"], + } + ); + }); + + test("set custom labels without inheritance", async () => { + spyGetInput({ + "target-branch": "target", + "pull-request": "https://github.com/owner/reponame/pull/2368", + "labels": "cherry-pick :cherries:, another-label", + "inherit-labels": "false", + }); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(1); + expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368"); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", + base: "target", + title: "[target] PR Title", + body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"), + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + labels: ["cherry-pick :cherries:", "another-label"], } ); }); @@ -294,6 +384,7 @@ describe("gha runner", () => { body: "New Body Prefix - New Body", reviewers: [], assignees: ["user3", "user4"], + labels: ["gha github cherry pick :cherries:", "original-label"], } ); }); diff --git a/test/service/runner/gha-gitlab-runner.test.ts b/test/service/runner/gha-gitlab-runner.test.ts index 0443342..e1d755c 100644 --- a/test/service/runner/gha-gitlab-runner.test.ts +++ b/test/service/runner/gha-gitlab-runner.test.ts @@ -21,6 +21,8 @@ const GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { "reviewers": [], "assignees": ["user3", "user4"], "inheritReviewers": false, + "labels": ["gha gitlab cherry pick :cherries:"], + "inheritLabels": true, }; jest.mock("axios", () => { @@ -128,6 +130,7 @@ describe("gha runner", () => { body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"), reviewers: ["superuser"], assignees: [], + labels: [], } ); }); @@ -175,6 +178,7 @@ describe("gha runner", () => { body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), reviewers: ["superuser"], assignees: [], + labels: [], } ); }); @@ -220,6 +224,7 @@ describe("gha runner", () => { body: "New Body Prefix - New Body", reviewers: ["user1", "user2"], assignees: ["user3", "user4"], + labels: [], } ); }); @@ -266,6 +271,88 @@ describe("gha runner", () => { body: "New Body Prefix - New Body", reviewers: [], assignees: ["user3", "user4"], + labels: [], + } + ); + }); + + test("set custom labels with inheritance", async () => { + spyGetInput({ + "target-branch": "target", + "pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1", + "labels": "cherry-pick :cherries:, another-label", + "inherit-labels": "true", + }); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(0); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({ + owner: "superuser", + repo: "backporting-example", + head: "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e", + base: "target", + title: "[target] Update test.txt", + 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"], + } + ); + }); + + test("set custom labels without inheritance", async () => { + spyGetInput({ + "target-branch": "target", + "pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1", + "labels": "cherry-pick :cherries:, another-label", + }); + + await runner.execute(); + + const cwd = process.cwd() + "/bp"; + + expect(GitCLIService.prototype.clone).toBeCalledTimes(1); + expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitCLIService.prototype.fetch).toBeCalledTimes(0); + + expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1); + expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitCLIService.prototype.push).toBeCalledTimes(1); + expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({ + owner: "superuser", + repo: "backporting-example", + head: "bp-target-ebb1eca696c42fd067658bd9b5267709f78ef38e", + base: "target", + title: "[target] Update test.txt", + body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), + reviewers: ["superuser"], + assignees: [], + labels: ["cherry-pick :cherries:", "another-label"], } ); }); @@ -305,6 +392,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"], } ); }); diff --git a/test/support/mock/github-data.ts b/test/support/mock/github-data.ts index fc13e32..a5f0f60 100644 --- a/test/support/mock/github-data.ts +++ b/test/support/mock/github-data.ts @@ -91,7 +91,15 @@ export const mergedPullRequestFixture = { ], "labels": [ - + { + "id": 4901021057, + "node_id": "LA_kwDOImgs2354988AAAABJB-lgQ", + "url": "https://api.github.com/repos/owner/reponame/labels/original-label", + "name": "original-label", + "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 7df3263..d1af227 100644 --- a/test/support/mock/gitlab-data.ts +++ b/test/support/mock/gitlab-data.ts @@ -241,7 +241,7 @@ export const MERGED_SQUASHED_MR = { "source_project_id":76316, "target_project_id":76316, "labels":[ - + "gitlab-original-label" ], "draft":false, "work_in_progress":false,