diff --git a/README.md b/README.md index ac1a568..0552d18 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,9 @@ This tool comes with some inputs that allow users to override the default behavi |---------------|----------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------| | Version | -V, --version | - | Current version of the tool | | | Help | -h, --help | - | Display the help message | | -| Target Branch | -tb, --target-branch | Y | Branch where the changes must be backported to | | -| Pull Request | -pr, --pull-request | Y | Original pull request url, the one that must be backported, e.g., https://github.com/lampajr/backporting/pull/1 | | +| Target Branch | -tb, --target-branch | N | Branch where the changes must be backported to | | +| Pull Request | -pr, --pull-request | N | Original pull request url, the one that must be backported, e.g., https://github.com/lampajr/backporting/pull/1 | | +| Configuration File | -cf, --config-file | N | Configuration file, in JSON format, containing all options to be overridded, note that if provided all other CLI options will be ignored | | | Auth | -a, --auth | N | `GITHUB_TOKEN`, `GITLAB_TOKEN` or a `repo` scoped [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) | "" | | Folder | -f, --folder | N | Local folder full name of the repository that will be checked out, e.g., /tmp/folder | {cwd}/bp | | Git User | -gu, --git-user | N | Local git user name | "GitHub" | @@ -90,6 +91,22 @@ This tool comes with some inputs that allow users to override the default behavi | Backport Branch Name | --bp-branch-name | N | Name of the backporting pull request branch | bp-{target-branch}-{sha} | | 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). + +#### Configuration file example + +This is an example of a configuration file that can be used. +```json +{ + "pullRequest": "https://gitlab.com///-/merge_requests/1", + "targetBranch": "old", + "folder": "/tmp/my-folder", + "title": "Override Title", + "auth": "*****" +} +``` +Keep in mind that its structue MUST match the [Args](src/service/args/args.types.ts) interface, which is actually a camel-case version of the CLI options. + ## Supported git services Right now **bper** supports the following git management services: diff --git a/action.yml b/action.yml index 68001c9..56ab98d 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,15 @@ name: "Backporting GitHub Action" 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/lampajr/backporting/pull/1" + required: false + target-branch: + description: "Branch where the pull request must be backported to" + required: false + config-file: + description: "Path to a file containing the json configuration for this tool, the object must match the Args interface" + required: false dry-run: description: "If enabled the tool does not create any pull request nor push anything remotely" required: false @@ -17,12 +26,6 @@ inputs: description: "Local git user email" default: "noreply@github.com" required: false - pull-request: - description: "URL of the pull request to backport, e.g., https://github.com/lampajr/backporting/pull/1" - required: true - target-branch: - description: "Branch where the pull request must be backported to" - required: true title: description: "Backporting PR title. Default is the original PR title prefixed by the target branch" required: false diff --git a/dist/cli/index.js b/dist/cli/index.js index d614739..ffe50a3 100755 --- a/dist/cli/index.js +++ b/dist/cli/index.js @@ -22,59 +22,172 @@ runner.run(); /***/ }), -/***/ 7938: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { +/***/ 3025: +/***/ ((__unused_webpack_module, exports) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); +/** + * Abstract arguments parser interface in charge to parse inputs and + * produce a common Args object + */ +class ArgsParser { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getOrDefault(parsedValue, defaultValue) { + return parsedValue === undefined ? defaultValue : parsedValue; + } + parse() { + const args = this.readArgs(); + // validate and fill with defaults + if (!args.pullRequest || !args.targetBranch) { + throw new Error("Missing option: pull request and target branch must be provided"); + } + return { + pullRequest: args.pullRequest, + targetBranch: args.targetBranch, + dryRun: this.getOrDefault(args.dryRun, false), + auth: this.getOrDefault(args.auth), + folder: this.getOrDefault(args.folder), + gitUser: this.getOrDefault(args.gitUser), + gitEmail: this.getOrDefault(args.gitEmail), + title: this.getOrDefault(args.title), + body: this.getOrDefault(args.body), + bodyPrefix: this.getOrDefault(args.bodyPrefix), + bpBranchName: this.getOrDefault(args.bpBranchName), + reviewers: this.getOrDefault(args.reviewers, []), + assignees: this.getOrDefault(args.assignees, []), + inheritReviewers: this.getOrDefault(args.inheritReviewers, true), + }; + } +} +exports["default"] = ArgsParser; + + +/***/ }), + +/***/ 8048: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.readConfigFile = exports.parseArgs = void 0; +const fs = __importStar(__nccwpck_require__(7147)); +/** + * Parse the input configuation string as json object and + * return it as Args + * @param configFileContent + * @returns {Args} + */ +function parseArgs(configFileContent) { + return JSON.parse(configFileContent); +} +exports.parseArgs = parseArgs; +/** + * Read a configuration file in json format anf parse it as {Args} + * @param pathToFile Full path name of the config file, e.g., /tmp/dir/config-file.json + * @returns {Args} + */ +function readConfigFile(pathToFile) { + const asString = fs.readFileSync(pathToFile, "utf-8"); + return parseArgs(asString); +} +exports.readConfigFile = readConfigFile; + + +/***/ }), + +/***/ 7938: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +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 { +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) - .requiredOption("-tb, --target-branch ", "branch where changes must be backported to.") - .requiredOption("-pr, --pull-request ", "pull request url, e.g., https://github.com/lampajr/backporting/pull/1.") - .option("-d, --dry-run", "if enabled the tool does not create any pull request nor push anything remotely", false) - .option("-a, --auth ", "git service authentication string, e.g., github token.", "") - .option("-gu, --git-user ", "local git user name, default is 'GitHub'.", "GitHub") - .option("-ge, --git-email ", "local git user email, default is 'noreply@github.com'.", "noreply@github.com") - .option("-f, --folder ", "local folder where the repo will be checked out, e.g., /tmp/folder.", undefined) - .option("--title ", "backport pr title, default original pr title prefixed by target branch.", undefined) - .option("--body ", "backport pr title, default original pr body prefixed by bodyPrefix.", undefined) - .option("--body-prefix ", "backport pr body prefix, default `backport `.", undefined) - .option("--bp-branch-name ", "backport pr branch name, default auto-generated by the commit.", undefined) - .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("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request", true); + .option("-tb, --target-branch ", "branch where changes must be backported to.") + .option("-pr, --pull-request ", "pull request url, e.g., https://github.com/lampajr/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("--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."); } - parse() { + readArgs() { const opts = this.getCommand() .parse() .opts(); - return { - dryRun: opts.dryRun, - auth: opts.auth, - pullRequest: opts.pullRequest, - targetBranch: opts.targetBranch, - folder: opts.folder, - gitUser: opts.gitUser, - gitEmail: opts.gitEmail, - title: opts.title, - body: opts.body, - bodyPrefix: opts.bodyPrefix, - bpBranchName: opts.bpBranchName, - reviewers: opts.reviewers, - assignees: opts.assignees, - inheritReviewers: opts.inheritReviewers, - }; + let args; + if (opts.configFile) { + // if config file is set ignore all other options + args = (0, args_utils_1.readConfigFile)(opts.configFile); + } + else { + args = { + dryRun: opts.dryRun, + auth: opts.auth, + pullRequest: opts.pullRequest, + targetBranch: opts.targetBranch, + folder: opts.folder, + gitUser: opts.gitUser, + gitEmail: opts.gitEmail, + title: opts.title, + body: opts.body, + bodyPrefix: opts.bodyPrefix, + bpBranchName: opts.bpBranchName, + reviewers: opts.reviewers, + assignees: opts.assignees, + inheritReviewers: opts.inheritReviewers, + }; + } + return args; } } exports["default"] = CLIArgsParser; @@ -133,12 +246,12 @@ const git_client_factory_1 = __importDefault(__nccwpck_require__(8550)); class PullRequestConfigsParser extends configs_parser_1.default { constructor() { super(); - this.gitService = git_client_factory_1.default.getClient(); + this.gitClient = git_client_factory_1.default.getClient(); } async parse(args) { let pr; try { - pr = await this.gitService.getPullRequestFromUrl(args.pullRequest); + pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest); } catch (error) { this.logger.error("Something went wrong retrieving pull request"); @@ -153,8 +266,8 @@ class PullRequestConfigsParser extends configs_parser_1.default { originalPullRequest: pr, backportPullRequest: this.getDefaultBackportPullRequest(pr, args), git: { - user: args.gitUser, - email: args.gitEmail, + user: args.gitUser ?? this.gitClient.getDefaultGitUser(), + email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(), } }; } @@ -180,7 +293,7 @@ class PullRequestConfigsParser extends configs_parser_1.default { const bodyPrefix = args.bodyPrefix ?? `**Backport:** ${originalPullRequest.htmlUrl}\r\n\r\n`; const body = args.body ?? `${originalPullRequest.body}`; return { - author: args.gitUser, + author: args.gitUser ?? this.gitClient.getDefaultGitUser(), title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, body: `${bodyPrefix}${body}`, reviewers: [...new Set(reviewers)], @@ -466,6 +579,12 @@ class GitHubClient { this.mapper = new github_mapper_1.default(); } // READ + getDefaultGitUser() { + return "GitHub"; + } + getDefaultGitEmail() { + return "noreply@github.com"; + } async getPullRequest(owner, repo, prNumber) { this.logger.info(`Getting pull request ${owner}/${repo}/${prNumber}.`); const { data } = await this.octokit.rest.pulls.get({ @@ -651,7 +770,7 @@ class GitLabClient { this.client = axios_1.default.create({ baseURL: this.apiUrl, headers: { - Authorization: `Bearer ${token}`, + Authorization: token ? `Bearer ${token}` : "", "User-Agent": "lampajr/backporting", }, httpsAgent: new https_1.default.Agent({ @@ -660,6 +779,12 @@ class GitLabClient { }); this.mapper = new gitlab_mapper_1.default(this.client); } + getDefaultGitUser() { + return "Gitlab"; + } + getDefaultGitEmail() { + return "noreply@gitlab.com"; + } // READ // example: /api/v4/projects/alampare%2Fbackporting-example/merge_requests/1 async getPullRequest(namespace, repo, mrNumber) { diff --git a/dist/gha/index.js b/dist/gha/index.js index 61af54f..dfe5967 100755 --- a/dist/gha/index.js +++ b/dist/gha/index.js @@ -22,14 +22,118 @@ runner.run(); /***/ }), -/***/ 7283: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { +/***/ 3025: +/***/ ((__unused_webpack_module, exports) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); +/** + * Abstract arguments parser interface in charge to parse inputs and + * produce a common Args object + */ +class ArgsParser { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getOrDefault(parsedValue, defaultValue) { + return parsedValue === undefined ? defaultValue : parsedValue; + } + parse() { + const args = this.readArgs(); + // validate and fill with defaults + if (!args.pullRequest || !args.targetBranch) { + throw new Error("Missing option: pull request and target branch must be provided"); + } + return { + pullRequest: args.pullRequest, + targetBranch: args.targetBranch, + dryRun: this.getOrDefault(args.dryRun, false), + auth: this.getOrDefault(args.auth), + folder: this.getOrDefault(args.folder), + gitUser: this.getOrDefault(args.gitUser), + gitEmail: this.getOrDefault(args.gitEmail), + title: this.getOrDefault(args.title), + body: this.getOrDefault(args.body), + bodyPrefix: this.getOrDefault(args.bodyPrefix), + bpBranchName: this.getOrDefault(args.bpBranchName), + reviewers: this.getOrDefault(args.reviewers, []), + assignees: this.getOrDefault(args.assignees, []), + inheritReviewers: this.getOrDefault(args.inheritReviewers, true), + }; + } +} +exports["default"] = ArgsParser; + + +/***/ }), + +/***/ 8048: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.readConfigFile = exports.parseArgs = void 0; +const fs = __importStar(__nccwpck_require__(7147)); +/** + * Parse the input configuation string as json object and + * return it as Args + * @param configFileContent + * @returns {Args} + */ +function parseArgs(configFileContent) { + return JSON.parse(configFileContent); +} +exports.parseArgs = parseArgs; +/** + * Read a configuration file in json format anf parse it as {Args} + * @param pathToFile Full path name of the config file, e.g., /tmp/dir/config-file.json + * @returns {Args} + */ +function readConfigFile(pathToFile) { + const asString = fs.readFileSync(pathToFile, "utf-8"); + return parseArgs(asString); +} +exports.readConfigFile = readConfigFile; + + +/***/ }), + +/***/ 7283: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const args_parser_1 = __importDefault(__nccwpck_require__(3025)); const core_1 = __nccwpck_require__(2186); -class GHAArgsParser { +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 @@ -39,36 +143,40 @@ class GHAArgsParser { const value = (0, core_1.getInput)(key); return value !== "" ? value : undefined; } - getOrDefault(key, defaultValue) { - const value = (0, core_1.getInput)(key); - return value !== "" ? value : defaultValue; - } getAsCommaSeparatedList(key) { // trim the value const value = ((0, core_1.getInput)(key) ?? "").trim(); - return value !== "" ? value.replace(/\s/g, "").split(",") : []; + return value !== "" ? value.replace(/\s/g, "").split(",") : undefined; } - getAsBooleanOrDefault(key, defaultValue) { + getAsBooleanOrDefault(key) { const value = (0, core_1.getInput)(key).trim(); - return value !== "" ? value.toLowerCase() === "true" : defaultValue; + return value !== "" ? value.toLowerCase() === "true" : undefined; } - parse() { - return { - dryRun: this.getAsBooleanOrDefault("dry-run", false), - auth: (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.getOrDefault("git-user", "GitHub"), - gitEmail: this.getOrDefault("git-email", "noreply@github.com"), - 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", false), - }; + readArgs() { + const configFile = this.getOrUndefined("config-file"); + let args; + if (configFile) { + args = (0, args_utils_1.readConfigFile)(configFile); + } + else { + args = { + dryRun: this.getAsBooleanOrDefault("dry-run"), + auth: this.getOrUndefined("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"), + }; + } + return args; } } exports["default"] = GHAArgsParser; @@ -127,12 +235,12 @@ const git_client_factory_1 = __importDefault(__nccwpck_require__(8550)); class PullRequestConfigsParser extends configs_parser_1.default { constructor() { super(); - this.gitService = git_client_factory_1.default.getClient(); + this.gitClient = git_client_factory_1.default.getClient(); } async parse(args) { let pr; try { - pr = await this.gitService.getPullRequestFromUrl(args.pullRequest); + pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest); } catch (error) { this.logger.error("Something went wrong retrieving pull request"); @@ -147,8 +255,8 @@ class PullRequestConfigsParser extends configs_parser_1.default { originalPullRequest: pr, backportPullRequest: this.getDefaultBackportPullRequest(pr, args), git: { - user: args.gitUser, - email: args.gitEmail, + user: args.gitUser ?? this.gitClient.getDefaultGitUser(), + email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(), } }; } @@ -174,7 +282,7 @@ class PullRequestConfigsParser extends configs_parser_1.default { const bodyPrefix = args.bodyPrefix ?? `**Backport:** ${originalPullRequest.htmlUrl}\r\n\r\n`; const body = args.body ?? `${originalPullRequest.body}`; return { - author: args.gitUser, + author: args.gitUser ?? this.gitClient.getDefaultGitUser(), title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, body: `${bodyPrefix}${body}`, reviewers: [...new Set(reviewers)], @@ -460,6 +568,12 @@ class GitHubClient { this.mapper = new github_mapper_1.default(); } // READ + getDefaultGitUser() { + return "GitHub"; + } + getDefaultGitEmail() { + return "noreply@github.com"; + } async getPullRequest(owner, repo, prNumber) { this.logger.info(`Getting pull request ${owner}/${repo}/${prNumber}.`); const { data } = await this.octokit.rest.pulls.get({ @@ -645,7 +759,7 @@ class GitLabClient { this.client = axios_1.default.create({ baseURL: this.apiUrl, headers: { - Authorization: `Bearer ${token}`, + Authorization: token ? `Bearer ${token}` : "", "User-Agent": "lampajr/backporting", }, httpsAgent: new https_1.default.Agent({ @@ -654,6 +768,12 @@ class GitLabClient { }); this.mapper = new gitlab_mapper_1.default(this.client); } + getDefaultGitUser() { + return "Gitlab"; + } + getDefaultGitEmail() { + return "noreply@gitlab.com"; + } // READ // example: /api/v4/projects/alampare%2Fbackporting-example/merge_requests/1 async getPullRequest(namespace, repo, mrNumber) { diff --git a/src/service/args/args-parser.ts b/src/service/args/args-parser.ts index 8b44d04..e2bddb9 100644 --- a/src/service/args/args-parser.ts +++ b/src/service/args/args-parser.ts @@ -4,7 +4,38 @@ import { Args } from "@bp/service/args/args.types"; * Abstract arguments parser interface in charge to parse inputs and * produce a common Args object */ -export default interface ArgsParser { +export default abstract class ArgsParser { - parse(): Args; + abstract readArgs(): Args; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private getOrDefault(parsedValue: any, defaultValue?: any) { + return parsedValue === undefined ? defaultValue : parsedValue; + } + + public parse(): Args { + const args = this.readArgs(); + + // validate and fill with defaults + if (!args.pullRequest || !args.targetBranch) { + throw new Error("Missing option: pull request and target branch must be provided"); + } + + return { + pullRequest: args.pullRequest, + targetBranch: args.targetBranch, + dryRun: this.getOrDefault(args.dryRun, false), + auth: this.getOrDefault(args.auth), + folder: this.getOrDefault(args.folder), + gitUser: this.getOrDefault(args.gitUser), + gitEmail: this.getOrDefault(args.gitEmail), + title: this.getOrDefault(args.title), + body: this.getOrDefault(args.body), + bodyPrefix: this.getOrDefault(args.bodyPrefix), + bpBranchName: this.getOrDefault(args.bpBranchName), + reviewers: this.getOrDefault(args.reviewers, []), + assignees: this.getOrDefault(args.assignees, []), + inheritReviewers: this.getOrDefault(args.inheritReviewers, true), + }; + } } \ No newline at end of file diff --git a/src/service/args/args-utils.ts b/src/service/args/args-utils.ts new file mode 100644 index 0000000..b449fd4 --- /dev/null +++ b/src/service/args/args-utils.ts @@ -0,0 +1,22 @@ +import { Args } from "@bp/service/args/args.types"; +import * as fs from "fs"; + +/** + * Parse the input configuation string as json object and + * return it as Args + * @param configFileContent + * @returns {Args} + */ +export function parseArgs(configFileContent: string): Args { + return JSON.parse(configFileContent) as Args; +} + +/** + * Read a configuration file in json format anf parse it as {Args} + * @param pathToFile Full path name of the config file, e.g., /tmp/dir/config-file.json + * @returns {Args} + */ +export function readConfigFile(pathToFile: string): Args { + const asString: string = fs.readFileSync(pathToFile, "utf-8"); + return parseArgs(asString); +} \ No newline at end of file diff --git a/src/service/args/args.types.ts b/src/service/args/args.types.ts index 9751ee6..6279136 100644 --- a/src/service/args/args.types.ts +++ b/src/service/args/args.types.ts @@ -2,18 +2,18 @@ * Input arguments */ export interface Args { - dryRun: boolean, // if enabled do not push anything remotely - auth: string, // git service auth, like github token targetBranch: string, // branch on the target repo where the change should be backported to pullRequest: string, // url of the pull request to backport + dryRun?: boolean, // if enabled do not push anything remotely + auth?: string, // git service auth, like github token folder?: string, // local folder where the repositories should be cloned - gitUser: string, // local git user, default 'GitHub' - gitEmail: string, // local git email, default 'noreply@github.com' + gitUser?: string, // local git user, default 'GitHub' + gitEmail?: string, // local git email, default 'noreply@github.com' title?: string, // backport pr title, default original pr title prefixed by target branch body?: string, // backport pr title, default original pr body prefixed by bodyPrefix bodyPrefix?: string, // backport pr body prefix, default `backport ` bpBranchName?: string, // backport pr branch name, default computed from commit reviewers?: string[], // backport pr reviewers assignees?: string[], // backport pr assignees - inheritReviewers: boolean, // if true and reviewers == [] then inherit reviewers from original pr + inheritReviewers?: boolean, // if true and reviewers == [] then inherit reviewers 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 cb20d9a..1e90cd8 100644 --- a/src/service/args/cli/cli-args-parser.ts +++ b/src/service/args/cli/cli-args-parser.ts @@ -2,6 +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 @@ -9,49 +10,58 @@ function commaSeparatedList(value: string, _prev: unknown): string[] { return cleanedValue !== "" ? cleanedValue.replace(/\s/g, "").split(",") : []; } -export default class CLIArgsParser implements ArgsParser { +export default class CLIArgsParser extends ArgsParser { private getCommand(): Command { return new Command(name) .version(version) .description(description) - .requiredOption("-tb, --target-branch ", "branch where changes must be backported to.") - .requiredOption("-pr, --pull-request ", "pull request url, e.g., https://github.com/lampajr/backporting/pull/1.") - .option("-d, --dry-run", "if enabled the tool does not create any pull request nor push anything remotely", false) - .option("-a, --auth ", "git service authentication string, e.g., github token.", "") - .option("-gu, --git-user ", "local git user name, default is 'GitHub'.", "GitHub") - .option("-ge, --git-email ", "local git user email, default is 'noreply@github.com'.", "noreply@github.com") - .option("-f, --folder ", "local folder where the repo will be checked out, e.g., /tmp/folder.", undefined) - .option("--title ", "backport pr title, default original pr title prefixed by target branch.", undefined) - .option("--body ", "backport pr title, default original pr body prefixed by bodyPrefix.", undefined) - .option("--body-prefix ", "backport pr body prefix, default `backport `.", undefined) - .option("--bp-branch-name ", "backport pr branch name, default auto-generated by the commit.", undefined) - .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("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request", true); + .option("-tb, --target-branch ", "branch where changes must be backported to.") + .option("-pr, --pull-request ", "pull request url, e.g., https://github.com/lampajr/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("--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."); } - parse(): Args { + readArgs(): Args { const opts = this.getCommand() .parse() .opts(); - return { - dryRun: opts.dryRun, - auth: opts.auth, - pullRequest: opts.pullRequest, - targetBranch: opts.targetBranch, - folder: opts.folder, - gitUser: opts.gitUser, - gitEmail: opts.gitEmail, - title: opts.title, - body: opts.body, - bodyPrefix: opts.bodyPrefix, - bpBranchName: opts.bpBranchName, - reviewers: opts.reviewers, - assignees: opts.assignees, - inheritReviewers: opts.inheritReviewers, - }; + let args: Args; + if (opts.configFile) { + // if config file is set ignore all other options + args = readConfigFile(opts.configFile); + } else { + args = { + dryRun: opts.dryRun, + auth: opts.auth, + pullRequest: opts.pullRequest, + targetBranch: opts.targetBranch, + folder: opts.folder, + gitUser: opts.gitUser, + gitEmail: opts.gitEmail, + title: opts.title, + body: opts.body, + bodyPrefix: opts.bodyPrefix, + bpBranchName: opts.bpBranchName, + reviewers: opts.reviewers, + assignees: opts.assignees, + inheritReviewers: opts.inheritReviewers, + }; + } + + return args; } } \ No newline at end of file diff --git a/src/service/args/gha/gha-args-parser.ts b/src/service/args/gha/gha-args-parser.ts index 0ab2ee9..74debbd 100644 --- a/src/service/args/gha/gha-args-parser.ts +++ b/src/service/args/gha/gha-args-parser.ts @@ -1,8 +1,9 @@ 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"; -export default class GHAArgsParser implements ArgsParser { +export default class GHAArgsParser extends ArgsParser { /** * Return the input only if it is not a blank or null string, otherwise returns undefined @@ -14,39 +15,43 @@ export default class GHAArgsParser implements ArgsParser { return value !== "" ? value : undefined; } - public getOrDefault(key: string, defaultValue: string): string { - const value = getInput(key); - return value !== "" ? value : defaultValue; - } - - public getAsCommaSeparatedList(key: string): string[] { + public getAsCommaSeparatedList(key: string): string[] | undefined { // trim the value const value: string = (getInput(key) ?? "").trim(); - return value !== "" ? value.replace(/\s/g, "").split(",") : []; + return value !== "" ? value.replace(/\s/g, "").split(",") : undefined; } - public getAsBooleanOrDefault(key: string, defaultValue: boolean): boolean { + private getAsBooleanOrDefault(key: string): boolean | undefined { const value = getInput(key).trim(); - return value !== "" ? value.toLowerCase() === "true" : defaultValue; + return value !== "" ? value.toLowerCase() === "true" : undefined; } - parse(): Args { - return { - dryRun: this.getAsBooleanOrDefault("dry-run", false), - auth: getInput("auth"), - pullRequest: getInput("pull-request"), - targetBranch: getInput("target-branch"), - folder: this.getOrUndefined("folder"), - gitUser: this.getOrDefault("git-user", "GitHub"), - gitEmail: this.getOrDefault("git-email", "noreply@github.com"), - 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", false), - }; + readArgs(): Args { + const configFile = this.getOrUndefined("config-file"); + + let args: Args; + if (configFile) { + args = readConfigFile(configFile); + } else { + args = { + dryRun: this.getAsBooleanOrDefault("dry-run"), + auth: this.getOrUndefined("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"), + }; + } + + return args; } } \ No newline at end of file diff --git a/src/service/configs/configs.types.ts b/src/service/configs/configs.types.ts index cb3c0f1..ff28a78 100644 --- a/src/service/configs/configs.types.ts +++ b/src/service/configs/configs.types.ts @@ -12,7 +12,7 @@ export interface LocalGit { */ export interface Configs { dryRun: boolean, - auth: string, + auth?: string, git: LocalGit, folder: string, targetBranch: string, diff --git a/src/service/configs/pullrequest/pr-configs-parser.ts b/src/service/configs/pullrequest/pr-configs-parser.ts index a8e6004..f54ba5e 100644 --- a/src/service/configs/pullrequest/pr-configs-parser.ts +++ b/src/service/configs/pullrequest/pr-configs-parser.ts @@ -7,17 +7,17 @@ import { GitPullRequest } from "@bp/service/git/git.types"; export default class PullRequestConfigsParser extends ConfigsParser { - private gitService: GitClient; + private gitClient: GitClient; constructor() { super(); - this.gitService = GitClientFactory.getClient(); + this.gitClient = GitClientFactory.getClient(); } public async parse(args: Args): Promise { let pr: GitPullRequest; try { - pr = await this.gitService.getPullRequestFromUrl(args.pullRequest); + pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest); } catch(error) { this.logger.error("Something went wrong retrieving pull request"); throw error; @@ -26,15 +26,15 @@ export default class PullRequestConfigsParser extends ConfigsParser { const folder: string = args.folder ?? this.getDefaultFolder(); return { - dryRun: args.dryRun, + dryRun: args.dryRun!, auth: args.auth, folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`, targetBranch: args.targetBranch, originalPullRequest: pr, backportPullRequest: this.getDefaultBackportPullRequest(pr, args), git: { - user: args.gitUser, - email: args.gitEmail, + user: args.gitUser ?? this.gitClient.getDefaultGitUser(), + email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(), } }; } @@ -64,7 +64,7 @@ export default class PullRequestConfigsParser extends ConfigsParser { const body = args.body ?? `${originalPullRequest.body}`; return { - author: args.gitUser, + author: args.gitUser ?? this.gitClient.getDefaultGitUser(), title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`, body: `${bodyPrefix}${body}`, reviewers: [...new Set(reviewers)], diff --git a/src/service/git/git-cli.ts b/src/service/git/git-cli.ts index 4781aff..0061fd5 100644 --- a/src/service/git/git-cli.ts +++ b/src/service/git/git-cli.ts @@ -10,10 +10,10 @@ import { LocalGit } from "@bp/service/configs/configs.types"; export default class GitCLIService { private readonly logger: LoggerService; - private readonly auth: string; + private readonly auth: string | undefined; private readonly gitData: LocalGit; - constructor(auth: string, gitData: LocalGit) { + constructor(auth: string | undefined, gitData: LocalGit) { this.logger = LoggerServiceFactory.getLogger(); this.auth = auth; this.gitData = gitData; diff --git a/src/service/git/git-client-factory.ts b/src/service/git/git-client-factory.ts index 44bf7e8..890863a 100644 --- a/src/service/git/git-client-factory.ts +++ b/src/service/git/git-client-factory.ts @@ -26,7 +26,7 @@ export default class GitClientFactory { * @param type git management service type * @param authToken authentication token, like github/gitlab token */ - public static getOrCreate(type: GitClientType, authToken: string, apiUrl: string): GitClient { + public static getOrCreate(type: GitClientType, authToken: string | undefined, apiUrl: string): GitClient { if (GitClientFactory.instance) { GitClientFactory.logger.warn("Git service already initialized!"); diff --git a/src/service/git/git-client.ts b/src/service/git/git-client.ts index 557d2f8..c1ff0ea 100644 --- a/src/service/git/git-client.ts +++ b/src/service/git/git-client.ts @@ -7,6 +7,10 @@ import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types"; export default interface GitClient { // READ + + getDefaultGitUser(): string; + + getDefaultGitEmail(): string; /** * Get a pull request object from the underneath git service diff --git a/src/service/git/github/github-client.ts b/src/service/git/github/github-client.ts index 3d7e89d..6f79c0c 100644 --- a/src/service/git/github/github-client.ts +++ b/src/service/git/github/github-client.ts @@ -14,7 +14,7 @@ export default class GitHubClient implements GitClient { private octokit: Octokit; private mapper: GitHubMapper; - constructor(token: string, apiUrl: string) { + constructor(token: string | undefined, apiUrl: string) { this.apiUrl = apiUrl; this.logger = LoggerServiceFactory.getLogger(); this.octokit = OctokitFactory.getOctokit(token, this.apiUrl); @@ -23,6 +23,14 @@ export default class GitHubClient implements GitClient { // READ + getDefaultGitUser(): string { + return "GitHub"; + } + + getDefaultGitEmail(): string { + return "noreply@github.com"; + } + async getPullRequest(owner: string, repo: string, prNumber: number): Promise { this.logger.info(`Getting pull request ${owner}/${repo}/${prNumber}.`); const { data } = await this.octokit.rest.pulls.get({ diff --git a/src/service/git/github/octokit-factory.ts b/src/service/git/github/octokit-factory.ts index 7ea69ff..cad08d9 100644 --- a/src/service/git/github/octokit-factory.ts +++ b/src/service/git/github/octokit-factory.ts @@ -10,7 +10,7 @@ export default class OctokitFactory { private static logger: LoggerService = LoggerServiceFactory.getLogger(); private static octokit?: Octokit; - public static getOctokit(token: string, apiUrl: string): Octokit { + public static getOctokit(token: string | undefined, apiUrl: string): Octokit { if (!OctokitFactory.octokit) { OctokitFactory.logger.info("Creating octokit instance."); OctokitFactory.octokit = new Octokit({ diff --git a/src/service/git/gitlab/gitlab-client.ts b/src/service/git/gitlab/gitlab-client.ts index abe3062..5a76da6 100644 --- a/src/service/git/gitlab/gitlab-client.ts +++ b/src/service/git/gitlab/gitlab-client.ts @@ -14,13 +14,13 @@ export default class GitLabClient implements GitClient { private readonly mapper: GitLabMapper; private readonly client: Axios; - constructor(token: string, apiUrl: string, rejectUnauthorized = false) { + constructor(token: string | undefined, apiUrl: string, rejectUnauthorized = false) { this.logger = LoggerServiceFactory.getLogger(); this.apiUrl = apiUrl; this.client = axios.create({ baseURL: this.apiUrl, headers: { - Authorization: `Bearer ${token}`, + Authorization: token ? `Bearer ${token}` : "", "User-Agent": "lampajr/backporting", }, httpsAgent: new https.Agent({ @@ -30,6 +30,14 @@ export default class GitLabClient implements GitClient { this.mapper = new GitLabMapper(this.client); } + getDefaultGitUser(): string { + return "Gitlab"; + } + + getDefaultGitEmail(): string { + return "noreply@gitlab.com"; + } + // READ // example: /api/v4/projects/alampare%2Fbackporting-example/merge_requests/1 diff --git a/test/service/args/args-utils.test.ts b/test/service/args/args-utils.test.ts new file mode 100644 index 0000000..57f5f30 --- /dev/null +++ b/test/service/args/args-utils.test.ts @@ -0,0 +1,42 @@ +import { parseArgs, readConfigFile } from "@bp/service/args/args-utils"; +import { createTestFile, removeTestFile } from "../../support/utils"; + +const RANDOM_CONFIG_FILE_CONTENT_PATHNAME = "./args-utils-test-random-config-file.json"; +const RANDOM_CONFIG_FILE_CONTENT = { + "dryRun": true, + "auth": "your-git-service-auth-token", + "targetBranch": "target-branch-name", + "pullRequest": "https://github.com/user/repo/pull/123", + "folder": "/path/to/local/folder", + "gitUser": "YourGitUser", + "gitEmail": "your-email@example.com", + "title": "Backport: Original PR Title", + "body": "Backport: Original PR Body", + "bodyPrefix": "backport ", + "bpBranchName": "backport-branch-name", + "reviewers": ["reviewer1", "reviewer2"], + "assignees": ["assignee1", "assignee2"], + "inheritReviewers": true, +}; + + +describe("args utils test suite", () => { + beforeAll(() => { + // create a temporary file + createTestFile(RANDOM_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(RANDOM_CONFIG_FILE_CONTENT)); + }); + + afterAll(() => { + // clean up all temporary files + removeTestFile(RANDOM_CONFIG_FILE_CONTENT_PATHNAME); + }); + + test("check parseArgs function", () => { + const asString = JSON.stringify(RANDOM_CONFIG_FILE_CONTENT); + expect(parseArgs(asString)).toStrictEqual(RANDOM_CONFIG_FILE_CONTENT); + }); + + test("check readConfigFile function", () => { + expect(readConfigFile(RANDOM_CONFIG_FILE_CONTENT_PATHNAME)).toStrictEqual(RANDOM_CONFIG_FILE_CONTENT); + }); +}); \ 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 ba2f59c..ff09dd0 100644 --- a/test/service/args/cli/cli-args-parser.test.ts +++ b/test/service/args/cli/cli-args-parser.test.ts @@ -1,9 +1,45 @@ import { Args } from "@bp/service/args/args.types"; import CLIArgsParser from "@bp/service/args/cli/cli-args-parser"; -import { addProcessArgs, resetProcessArgs, expectArrayEqual } from "../../../support/utils"; +import { addProcessArgs, resetProcessArgs, expectArrayEqual, createTestFile, removeTestFile } from "../../../support/utils"; + +export const SIMPLE_CONFIG_FILE_CONTENT_PATHNAME = "./cli-args-parser-test-simple-config-file-pulls-1.json"; +export const SIMPLE_CONFIG_FILE_CONTENT = { + "targetBranch": "target", + "pullRequest": "https://localhost/whatever/pulls/1", +}; + +const RANDOM_CONFIG_FILE_CONTENT_PATHNAME = "./cli-args-parser-test-random-config-file.json"; +const RANDOM_CONFIG_FILE_CONTENT = { + "dryRun": true, + "auth": "your-git-service-auth-token", + "targetBranch": "target-branch-name", + "pullRequest": "https://github.com/user/repo/pull/123", + "folder": "/path/to/local/folder", + "gitUser": "YourGitUser", + "gitEmail": "your-email@example.com", + "title": "Backport: Original PR Title", + "body": "Backport: Original PR Body", + "bodyPrefix": "backport ", + "bpBranchName": "backport-branch-name", + "reviewers": ["reviewer1", "reviewer2"], + "assignees": ["assignee1", "assignee2"], + "inheritReviewers": true, +}; describe("cli args parser", () => { let parser: CLIArgsParser; + + beforeAll(() => { + // create a temporary file + createTestFile(SIMPLE_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(SIMPLE_CONFIG_FILE_CONTENT)); + createTestFile(RANDOM_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(RANDOM_CONFIG_FILE_CONTENT)); + }); + + afterAll(() => { + // clean up all temporary files + removeTestFile(SIMPLE_CONFIG_FILE_CONTENT_PATHNAME); + removeTestFile(RANDOM_CONFIG_FILE_CONTENT_PATHNAME); + }); beforeEach(() => { // create a fresh new instance every time @@ -23,9 +59,32 @@ describe("cli args parser", () => { const args: Args = parser.parse(); expect(args.dryRun).toEqual(false); - expect(args.auth).toEqual(""); - expect(args.gitUser).toEqual("GitHub"); - expect(args.gitEmail).toEqual("noreply@github.com"); + expect(args.auth).toEqual(undefined); + expect(args.gitUser).toEqual(undefined); + expect(args.gitEmail).toEqual(undefined); + expect(args.folder).toEqual(undefined); + expect(args.targetBranch).toEqual("target"); + expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1"); + expect(args.title).toEqual(undefined); + expect(args.body).toEqual(undefined); + expect(args.bodyPrefix).toEqual(undefined); + expect(args.bpBranchName).toEqual(undefined); + expect(args.reviewers).toEqual([]); + expect(args.assignees).toEqual([]); + expect(args.inheritReviewers).toEqual(true); + }); + + test("with config file [default, short]", () => { + addProcessArgs([ + "-cf", + SIMPLE_CONFIG_FILE_CONTENT_PATHNAME, + ]); + + const args: Args = parser.parse(); + expect(args.dryRun).toEqual(false); + expect(args.auth).toEqual(undefined); + expect(args.gitUser).toEqual(undefined); + expect(args.gitEmail).toEqual(undefined); expect(args.folder).toEqual(undefined); expect(args.targetBranch).toEqual("target"); expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1"); @@ -48,9 +107,32 @@ describe("cli args parser", () => { const args: Args = parser.parse(); expect(args.dryRun).toEqual(false); - expect(args.auth).toEqual(""); - expect(args.gitUser).toEqual("GitHub"); - expect(args.gitEmail).toEqual("noreply@github.com"); + expect(args.auth).toEqual(undefined); + expect(args.gitUser).toEqual(undefined); + expect(args.gitEmail).toEqual(undefined); + expect(args.folder).toEqual(undefined); + expect(args.targetBranch).toEqual("target"); + expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1"); + expect(args.title).toEqual(undefined); + expect(args.body).toEqual(undefined); + expect(args.bodyPrefix).toEqual(undefined); + expect(args.bpBranchName).toEqual(undefined); + expect(args.reviewers).toEqual([]); + expect(args.assignees).toEqual([]); + expect(args.inheritReviewers).toEqual(true); + }); + + test("with config file [default, long]", () => { + addProcessArgs([ + "--config-file", + SIMPLE_CONFIG_FILE_CONTENT_PATHNAME, + ]); + + const args: Args = parser.parse(); + expect(args.dryRun).toEqual(false); + expect(args.auth).toEqual(undefined); + expect(args.gitUser).toEqual(undefined); + expect(args.gitEmail).toEqual(undefined); expect(args.folder).toEqual(undefined); expect(args.targetBranch).toEqual("target"); expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1"); @@ -135,9 +217,78 @@ describe("cli args parser", () => { expect(args.body).toEqual("New Body"); expect(args.bodyPrefix).toEqual("New Body Prefix"); expect(args.bpBranchName).toEqual("bp_branch_name"); - expectArrayEqual(["al", "john", "jack"], args.reviewers!); - expectArrayEqual(["pippo", "pluto", "paperino"], args.assignees!); + expectArrayEqual(args.reviewers!, ["al", "john", "jack"]); + expectArrayEqual(args.assignees!, ["pippo", "pluto", "paperino"]); expect(args.inheritReviewers).toEqual(false); }); + test("override using config file", () => { + addProcessArgs([ + "--config-file", + RANDOM_CONFIG_FILE_CONTENT_PATHNAME, + ]); + + const args: Args = parser.parse(); + expect(args.dryRun).toEqual(true); + expect(args.auth).toEqual("your-git-service-auth-token"); + expect(args.gitUser).toEqual("YourGitUser"); + expect(args.gitEmail).toEqual("your-email@example.com"); + expect(args.folder).toEqual("/path/to/local/folder"); + expect(args.targetBranch).toEqual("target-branch-name"); + expect(args.pullRequest).toEqual("https://github.com/user/repo/pull/123"); + expect(args.title).toEqual("Backport: Original PR Title"); + expect(args.body).toEqual("Backport: Original PR Body"); + expect(args.bodyPrefix).toEqual("backport "); + expect(args.bpBranchName).toEqual("backport-branch-name"); + expectArrayEqual(args.reviewers!, ["reviewer1", "reviewer2"]); + expectArrayEqual(args.assignees!,["assignee1", "assignee2"]); + expect(args.inheritReviewers).toEqual(true); + }); + + test("ignore custom option when config file is set", () => { + addProcessArgs([ + "--config-file", + RANDOM_CONFIG_FILE_CONTENT_PATHNAME, + "--dry-run", + "--auth", + "bearer-token", + "--target-branch", + "target", + "--pull-request", + "https://localhost/whatever/pulls/1", + "--git-user", + "Me", + "--git-email", + "me@email.com", + "--title", + "New Title", + "--body", + "New Body", + "--body-prefix", + "New Body Prefix", + "--bp-branch-name", + "bp_branch_name", + "--reviewers", + "al , john, jack", + "--assignees", + " pippo,pluto, paperino", + "--no-inherit-reviewers", + ]); + + const args: Args = parser.parse(); + expect(args.dryRun).toEqual(true); + expect(args.auth).toEqual("your-git-service-auth-token"); + expect(args.gitUser).toEqual("YourGitUser"); + expect(args.gitEmail).toEqual("your-email@example.com"); + expect(args.folder).toEqual("/path/to/local/folder"); + expect(args.targetBranch).toEqual("target-branch-name"); + expect(args.pullRequest).toEqual("https://github.com/user/repo/pull/123"); + expect(args.title).toEqual("Backport: Original PR Title"); + expect(args.body).toEqual("Backport: Original PR Body"); + expect(args.bodyPrefix).toEqual("backport "); + expect(args.bpBranchName).toEqual("backport-branch-name"); + expectArrayEqual(args.reviewers!, ["reviewer1", "reviewer2"]); + expectArrayEqual(args.assignees!,["assignee1", "assignee2"]); + expect(args.inheritReviewers).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 701ac03..130f7d5 100644 --- a/test/service/args/gha/gha-args-parser.test.ts +++ b/test/service/args/gha/gha-args-parser.test.ts @@ -1,10 +1,46 @@ import { Args } from "@bp/service/args/args.types"; import GHAArgsParser from "@bp/service/args/gha/gha-args-parser"; -import { spyGetInput, expectArrayEqual } from "../../../support/utils"; +import { spyGetInput, expectArrayEqual, removeTestFile, createTestFile } from "../../../support/utils"; + +const SIMPLE_CONFIG_FILE_CONTENT_PATHNAME = "./gha-args-parser-test-simple-config-file-pulls-1.json"; +const SIMPLE_CONFIG_FILE_CONTENT = { + "targetBranch": "target", + "pullRequest": "https://localhost/whatever/pulls/1", +}; + +const RANDOM_CONFIG_FILE_CONTENT_PATHNAME = "./gha-args-parser-test-random-config-file.json"; +const RANDOM_CONFIG_FILE_CONTENT = { + "dryRun": true, + "auth": "your-git-service-auth-token", + "targetBranch": "target-branch-name", + "pullRequest": "https://github.com/user/repo/pull/123", + "folder": "/path/to/local/folder", + "gitUser": "YourGitUser", + "gitEmail": "your-email@example.com", + "title": "Backport: Original PR Title", + "body": "Backport: Original PR Body", + "bodyPrefix": "backport ", + "bpBranchName": "backport-branch-name", + "reviewers": ["reviewer1", "reviewer2"], + "assignees": ["assignee1", "assignee2"], + "inheritReviewers": true, +}; describe("gha args parser", () => { let parser: GHAArgsParser; + beforeAll(() => { + // create a temporary file + createTestFile(SIMPLE_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(SIMPLE_CONFIG_FILE_CONTENT)); + createTestFile(RANDOM_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(RANDOM_CONFIG_FILE_CONTENT)); + }); + + afterAll(() => { + // clean up all temporary files + removeTestFile(SIMPLE_CONFIG_FILE_CONTENT_PATHNAME); + removeTestFile(RANDOM_CONFIG_FILE_CONTENT_PATHNAME); + }); + beforeEach(() => { // create a fresh new instance every time parser = new GHAArgsParser(); @@ -14,14 +50,6 @@ describe("gha args parser", () => { jest.clearAllMocks(); }); - test("getOrDefault", () => { - spyGetInput({ - "present": "value" - }); - expect(parser.getOrDefault("not-present", "default")).toStrictEqual("default"); - expect(parser.getOrDefault("present", "default")).toStrictEqual("value"); - }); - test("getOrUndefined", () => { spyGetInput({ "present": "value", @@ -38,8 +66,8 @@ describe("gha args parser", () => { "blank": " ", }); expectArrayEqual(parser.getAsCommaSeparatedList("present")!, ["value1", "value2", "value3"]); - expect(parser.getAsCommaSeparatedList("empty")).toStrictEqual([]); - expect(parser.getAsCommaSeparatedList("blank")).toStrictEqual([]); + expect(parser.getAsCommaSeparatedList("empty")).toStrictEqual(undefined); + expect(parser.getAsCommaSeparatedList("blank")).toStrictEqual(undefined); }); test("valid execution [default]", () => { @@ -50,9 +78,9 @@ describe("gha args parser", () => { const args: Args = parser.parse(); expect(args.dryRun).toEqual(false); - expect(args.auth).toEqual(""); - expect(args.gitUser).toEqual("GitHub"); - expect(args.gitEmail).toEqual("noreply@github.com"); + expect(args.auth).toEqual(undefined); + expect(args.gitUser).toEqual(undefined); + expect(args.gitEmail).toEqual(undefined); expect(args.folder).toEqual(undefined); expect(args.targetBranch).toEqual("target"); expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1"); @@ -92,9 +120,65 @@ describe("gha args parser", () => { expect(args.body).toEqual("New Body"); expect(args.bodyPrefix).toEqual("New Body Prefix"); expect(args.bpBranchName).toEqual("bp_branch_name"); - expectArrayEqual(["al", "john", "jack"], args.reviewers!); - expectArrayEqual(["pippo", "pluto", "paperino"], args.assignees!); + expectArrayEqual(args.reviewers!, ["al", "john", "jack"]); + expectArrayEqual(args.assignees!, ["pippo", "pluto", "paperino"]); expect(args.inheritReviewers).toEqual(false); }); + test("using config file", () => { + spyGetInput({ + "config-file": SIMPLE_CONFIG_FILE_CONTENT_PATHNAME, + }); + + const args: Args = parser.parse(); + expect(args.dryRun).toEqual(false); + expect(args.auth).toEqual(undefined); + expect(args.gitUser).toEqual(undefined); + expect(args.gitEmail).toEqual(undefined); + expect(args.folder).toEqual(undefined); + expect(args.targetBranch).toEqual("target"); + expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1"); + expect(args.title).toEqual(undefined); + expect(args.body).toEqual(undefined); + expect(args.bodyPrefix).toEqual(undefined); + expect(args.bpBranchName).toEqual(undefined); + expect(args.reviewers).toEqual([]); + expect(args.assignees).toEqual([]); + expect(args.inheritReviewers).toEqual(true); + }); + + test("ignore custom options when using config file", () => { + spyGetInput({ + "config-file": RANDOM_CONFIG_FILE_CONTENT_PATHNAME, + "dry-run": "true", + "auth": "bearer-token", + "target-branch": "target", + "pull-request": "https://localhost/whatever/pulls/1", + "git-user": "Me", + "git-email": "me@email.com", + "title": "New Title", + "body": "New Body", + "body-prefix": "New Body Prefix", + "bp-branch-name": "bp_branch_name", + "reviewers": "al , john, jack", + "assignees": " pippo,pluto, paperino", + "no-inherit-reviewers": "true", + }); + + const args: Args = parser.parse(); + expect(args.dryRun).toEqual(true); + expect(args.auth).toEqual("your-git-service-auth-token"); + expect(args.gitUser).toEqual("YourGitUser"); + expect(args.gitEmail).toEqual("your-email@example.com"); + expect(args.folder).toEqual("/path/to/local/folder"); + expect(args.targetBranch).toEqual("target-branch-name"); + expect(args.pullRequest).toEqual("https://github.com/user/repo/pull/123"); + expect(args.title).toEqual("Backport: Original PR Title"); + expect(args.body).toEqual("Backport: Original PR Body"); + expect(args.bodyPrefix).toEqual("backport "); + expect(args.bpBranchName).toEqual("backport-branch-name"); + expectArrayEqual(args.reviewers!, ["reviewer1", "reviewer2"]); + expectArrayEqual(args.assignees!,["assignee1", "assignee2"]); + expect(args.inheritReviewers).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 d1ad6e8..5f5d8ae 100644 --- a/test/service/configs/pullrequest/github-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/github-pr-configs-parser.test.ts @@ -4,7 +4,31 @@ import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs import GitClientFactory from "@bp/service/git/git-client-factory"; import { GitClientType } from "@bp/service/git/git.types"; import { mockGitHubClient } from "../../../support/mock/git-client-mock-support"; +import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../../support/utils"; import { mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, repo, targetOwner } from "../../../support/mock/github-data"; +import CLIArgsParser from "@bp/service/args/cli/cli-args-parser"; + +const GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME = "./github-pr-configs-parser-simple-pr-merged.json"; +const GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT = { + "targetBranch": "prod", + "pullRequest": `https://github.com/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`, +}; + +const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME = "./github-pr-configs-parser-complex-pr-merged.json"; +const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { + "dryRun": false, + "auth": "my-auth-token", + "pullRequest": `https://github.com/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`, + "targetBranch": "prod", + "gitUser": "Me", + "gitEmail": "me@email.com", + "title": "New Title", + "body": "New Body", + "bodyPrefix": "New Body Prefix -", + "reviewers": ["user1", "user2"], + "assignees": ["user3", "user4"], + "inheritReviewers": true, // not taken into account +}; describe("github pull request config parser", () => { @@ -12,17 +36,33 @@ describe("github pull request config parser", () => { const openPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${openPullRequestFixture.number}`; const notMergedPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${notMergedPullRequestFixture.number}`; - let parser: PullRequestConfigsParser; + let argsParser: CLIArgsParser; + let configParser: PullRequestConfigsParser; beforeAll(() => { + // create a temporary file + createTestFile(GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT)); + createTestFile(GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT)); + GitClientFactory.reset(); GitClientFactory.getOrCreate(GitClientType.GITHUB, "whatever", "http://localhost/api/v3"); }); + afterAll(() => { + // clean up all temporary files + removeTestFile(GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME); + removeTestFile(GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME); + }); + beforeEach(() => { mockGitHubClient("http://localhost/api/v3"); - parser = new PullRequestConfigsParser(); + // reset process.env variables + resetProcessArgs(); + + // create a fresh new instance every time + argsParser = new CLIArgsParser(); + configParser = new PullRequestConfigsParser(); }); afterEach(() => { @@ -42,7 +82,7 @@ describe("github pull request config parser", () => { inheritReviewers: true, }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(false); expect(configs.git).toEqual({ @@ -113,7 +153,7 @@ describe("github pull request config parser", () => { inheritReviewers: true, }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(true); expect(configs.auth).toEqual("whatever"); @@ -138,7 +178,7 @@ describe("github pull request config parser", () => { inheritReviewers: true, }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(true); expect(configs.auth).toEqual("whatever"); @@ -189,7 +229,7 @@ describe("github pull request config parser", () => { inheritReviewers: true, }; - expect(async () => await parser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!"); + expect(async () => await configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!"); }); @@ -209,7 +249,7 @@ describe("github pull request config parser", () => { inheritReviewers: true, }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(false); expect(configs.git).toEqual({ @@ -283,7 +323,7 @@ describe("github pull request config parser", () => { inheritReviewers: true, // not taken into account }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(false); expect(configs.git).toEqual({ @@ -357,7 +397,7 @@ describe("github pull request config parser", () => { inheritReviewers: false, }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(false); expect(configs.git).toEqual({ @@ -414,4 +454,133 @@ describe("github pull request config parser", () => { bpBranchName: undefined, }); }); + + test("using simple config file", async () => { + addProcessArgs([ + "-cf", + GITHUB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME, + ]); + + const args: Args = argsParser.parse(); + const configs: Configs = await configParser.parseAndValidate(args); + + expect(configs.dryRun).toEqual(false); + expect(configs.git).toEqual({ + user: "GitHub", + email: "noreply@github.com" + }); + expect(configs.auth).toEqual(undefined); + expect(configs.targetBranch).toEqual("prod"); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.originalPullRequest).toEqual({ + number: 2368, + 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: [], + targetRepo: { + owner: "owner", + project: "reponame", + cloneUrl: "https://github.com/owner/reponame.git" + }, + sourceRepo: { + owner: "fork", + project: "reponame", + cloneUrl: "https://github.com/fork/reponame.git" + }, + nCommits: 2, + commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"] + }); + expect(configs.backportPullRequest).toEqual({ + author: "GitHub", + url: undefined, + htmlUrl: undefined, + title: "[prod] PR Title", + body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge", + reviewers: ["gh-user", "that-s-a-user"], + assignees: [], + 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("using complex config file", async () => { + addProcessArgs([ + "-cf", + GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME, + ]); + + const args: Args = argsParser.parse(); + 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("my-auth-token"); + 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: [], + 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: ["user1", "user2"], + assignees: ["user3", "user4"], + 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, + }); + }); }); \ No newline at end of file diff --git a/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts b/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts index 3abe97c..6049da5 100644 --- a/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts +++ b/test/service/configs/pullrequest/gitlab-pr-configs-parser.test.ts @@ -5,6 +5,31 @@ import GitClientFactory from "@bp/service/git/git-client-factory"; import { GitClientType } from "@bp/service/git/git.types"; import { getAxiosMocked } from "../../../support/mock/git-client-mock-support"; import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, OPEN_MR } from "../../../support/mock/gitlab-data"; +import GHAArgsParser from "@bp/service/args/gha/gha-args-parser"; +import { createTestFile, removeTestFile, spyGetInput } from "../../../support/utils"; + +const GITLAB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME = "./gitlab-pr-configs-parser-simple-pr-merged.json"; +const GITLAB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT = { + "targetBranch": "prod", + "pullRequest": `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${MERGED_SQUASHED_MR.iid}`, +}; + +const GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME = "./gitlab-pr-configs-parser-complex-pr-merged.json"; +const GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { + "dryRun": false, + "auth": "my-token", + "pullRequest": `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${MERGED_SQUASHED_MR.iid}`, + "targetBranch": "prod", + "gitUser": "Me", + "gitEmail": "me@email.com", + "title": "New Title", + "body": "New Body", + "bodyPrefix": "New Body Prefix -", + "reviewers": [], + "assignees": ["user3", "user4"], + "inheritReviewers": false, +}; + jest.mock("axios", () => { return { @@ -20,15 +45,27 @@ describe("gitlab merge request config parser", () => { const openPRUrl = `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${OPEN_MR.iid}`; const notMergedPRUrl = `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${CLOSED_NOT_MERGED_MR.iid}`; - let parser: PullRequestConfigsParser; + let argsParser: GHAArgsParser; + let configParser: PullRequestConfigsParser; beforeAll(() => { + // create a temporary file + createTestFile(GITLAB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(GITLAB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT)); + createTestFile(GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT)); + GitClientFactory.reset(); GitClientFactory.getOrCreate(GitClientType.GITLAB, "whatever", "my.gitlab.host.com"); }); + + afterAll(() => { + // clean up all temporary files + removeTestFile(GITLAB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME); + removeTestFile(GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME); + }); beforeEach(() => { - parser = new PullRequestConfigsParser(); + argsParser = new GHAArgsParser(); + configParser = new PullRequestConfigsParser(); }); afterEach(() => { @@ -38,24 +75,24 @@ describe("gitlab merge request config parser", () => { test("parse configs from merge request", async () => { const args: Args = { dryRun: false, - auth: "", + auth: undefined, pullRequest: mergedPRUrl, targetBranch: "prod", - gitUser: "GitLab", + gitUser: "Gitlab", gitEmail: "noreply@gitlab.com", reviewers: [], assignees: [], inheritReviewers: true, }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(false); expect(configs.git).toEqual({ - user: "GitLab", + user: "Gitlab", email: "noreply@gitlab.com" }); - expect(configs.auth).toEqual(""); + expect(configs.auth).toEqual(undefined); expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual(process.cwd() + "/bp"); expect(configs.originalPullRequest).toEqual({ @@ -84,7 +121,7 @@ describe("gitlab merge request config parser", () => { commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"] }); expect(configs.backportPullRequest).toEqual({ - author: "GitLab", + author: "Gitlab", url: undefined, htmlUrl: undefined, title: "[prod] Update test.txt", @@ -113,21 +150,21 @@ describe("gitlab merge request config parser", () => { pullRequest: mergedPRUrl, targetBranch: "prod", folder: "/tmp/test", - gitUser: "GitLab", + gitUser: "Gitlab", gitEmail: "noreply@gitlab.com", reviewers: [], assignees: [], inheritReviewers: true, }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(true); expect(configs.auth).toEqual("whatever"); expect(configs.targetBranch).toEqual("prod"); expect(configs.folder).toEqual("/tmp/test"); expect(configs.git).toEqual({ - user: "GitLab", + user: "Gitlab", email: "noreply@gitlab.com" }); }); @@ -138,20 +175,20 @@ describe("gitlab merge request config parser", () => { auth: "whatever", pullRequest: openPRUrl, targetBranch: "prod", - gitUser: "GitLab", + gitUser: "Gitlab", gitEmail: "noreply@gitlab.com", reviewers: [], assignees: [], inheritReviewers: true, }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(true); expect(configs.auth).toEqual("whatever"); expect(configs.targetBranch).toEqual("prod"); expect(configs.git).toEqual({ - user: "GitLab", + user: "Gitlab", email: "noreply@gitlab.com" }); expect(configs.originalPullRequest).toEqual({ @@ -189,14 +226,14 @@ describe("gitlab merge request config parser", () => { auth: "whatever", pullRequest: notMergedPRUrl, targetBranch: "prod", - gitUser: "GitLab", + gitUser: "Gitlab", gitEmail: "noreply@gitlab.com", reviewers: [], assignees: [], inheritReviewers: true, }; - expect(async () => await parser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!"); + expect(async () => await configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!"); }); test("override backport pr data inherting reviewers", async () => { @@ -215,7 +252,7 @@ describe("gitlab merge request config parser", () => { inheritReviewers: true, }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(false); expect(configs.git).toEqual({ @@ -288,7 +325,7 @@ describe("gitlab merge request config parser", () => { inheritReviewers: true, // not taken into account }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(false); expect(configs.git).toEqual({ @@ -361,7 +398,7 @@ describe("gitlab merge request config parser", () => { inheritReviewers: false, }; - const configs: Configs = await parser.parseAndValidate(args); + const configs: Configs = await configParser.parseAndValidate(args); expect(configs.dryRun).toEqual(false); expect(configs.git).toEqual({ @@ -417,4 +454,131 @@ describe("gitlab merge request config parser", () => { bpBranchName: undefined, }); }); + + + test("using simple config file", async () => { + spyGetInput({ + "config-file": GITLAB_MERGED_PR_SIMPLE_CONFIG_FILE_CONTENT_PATHNAME, + }); + + const args: Args = argsParser.parse(); + const configs: Configs = await configParser.parseAndValidate(args); + + expect(configs.dryRun).toEqual(false); + expect(configs.git).toEqual({ + user: "Gitlab", + email: "noreply@gitlab.com" + }); + expect(configs.auth).toEqual(undefined); + expect(configs.targetBranch).toEqual("prod"); + expect(configs.folder).toEqual(process.cwd() + "/bp"); + expect(configs.originalPullRequest).toEqual({ + number: 1, + 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"], + 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: "Gitlab", + url: undefined, + htmlUrl: undefined, + title: "[prod] Update test.txt", + body: "**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1\r\n\r\nThis is the body", + reviewers: ["superuser"], + assignees: [], + 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 complex config file", async () => { + spyGetInput({ + "config-file": GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME, + }); + + const args: Args = argsParser.parse(); + 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("my-token"); + 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"], + 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"], + 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, + }); + }); }); \ 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 9a5b94b..0b7a752 100644 --- a/test/service/runner/cli-github-runner.test.ts +++ b/test/service/runner/cli-github-runner.test.ts @@ -3,15 +3,42 @@ import Runner from "@bp/service/runner/runner"; import GitCLIService from "@bp/service/git/git-cli"; import GitHubClient from "@bp/service/git/github/github-client"; import CLIArgsParser from "@bp/service/args/cli/cli-args-parser"; -import { addProcessArgs, resetProcessArgs } from "../../support/utils"; +import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../support/utils"; import { mockGitHubClient } from "../../support/mock/git-client-mock-support"; +const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME = "./cli-github-runner-pr-merged-with-overrides.json"; +const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT = { + "dryRun": false, + "auth": "my-auth-token", + "pullRequest": "https://github.com/owner/reponame/pull/2368", + "targetBranch": "target", + "gitUser": "Me", + "gitEmail": "me@email.com", + "title": "New Title", + "body": "New Body", + "bodyPrefix": "New Body Prefix - ", + "bpBranchName": "bp_branch_name", + "reviewers": [], + "assignees": ["user3", "user4"], + "inheritReviewers": false, +}; + jest.mock("@bp/service/git/git-cli"); jest.spyOn(GitHubClient.prototype, "createPullRequest"); let parser: ArgsParser; let runner: Runner; +beforeAll(() => { + // create a temporary file + createTestFile(GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT)); +}); + +afterAll(() => { + // clean up all temporary files + removeTestFile(GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME); +}); + beforeEach(() => { mockGitHubClient(); @@ -391,4 +418,43 @@ describe("cli runner", () => { } ); }); + + test("using config file with overrides", async () => { + addProcessArgs([ + "--config-file", + GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME, + ]); + + 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_branch_name"); + + 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_branch_name"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp_branch_name", + base: "target", + title: "New Title", + body: "New Body Prefix - New Body", + reviewers: [], + assignees: ["user3", "user4"], + } + ); + }); }); \ 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 81bbe93..c8b9292 100644 --- a/test/service/runner/cli-gitlab-runner.test.ts +++ b/test/service/runner/cli-gitlab-runner.test.ts @@ -3,8 +3,25 @@ import Runner from "@bp/service/runner/runner"; import GitCLIService from "@bp/service/git/git-cli"; import GitLabClient from "@bp/service/git/gitlab/gitlab-client"; import CLIArgsParser from "@bp/service/args/cli/cli-args-parser"; -import { addProcessArgs, resetProcessArgs } from "../../support/utils"; +import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../support/utils"; import { getAxiosMocked } from "../../support/mock/git-client-mock-support"; +import { MERGED_SQUASHED_MR } from "../../support/mock/gitlab-data"; + +const GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME = "./cli-gitlab-runner-pr-merged-with-overrides.json"; +const GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { + "dryRun": false, + "auth": "my-token", + "pullRequest": `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${MERGED_SQUASHED_MR.iid}`, + "targetBranch": "prod", + "gitUser": "Me", + "gitEmail": "me@email.com", + "title": "New Title", + "body": "New Body", + "bodyPrefix": `**This is a backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${MERGED_SQUASHED_MR.iid}`, + "reviewers": [], + "assignees": ["user3", "user4"], + "inheritReviewers": false, +}; jest.mock("axios", () => { return { @@ -27,6 +44,16 @@ jest.spyOn(GitLabClient.prototype, "createPullRequest"); let parser: ArgsParser; let runner: Runner; +beforeAll(() => { + // create a temporary file + createTestFile(GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT)); +}); + +afterAll(() => { + // clean up all temporary files + removeTestFile(GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME); +}); + beforeEach(() => { // create CLI arguments parser parser = new CLIArgsParser(); @@ -306,4 +333,44 @@ describe("cli runner", () => { } ); }); + + test("using config file with overrides", async () => { + addProcessArgs([ + "--config-file", + GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME, + ]); + + 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, "prod"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-prod-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-prod-ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({ + owner: "superuser", + repo: "backporting-example", + head: "bp-prod-ebb1eca696c42fd067658bd9b5267709f78ef38e", + base: "prod", + title: "New Title", + body: expect.stringContaining("**This is a backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), + reviewers: [], + assignees: ["user3", "user4"], + } + ); + }); }); \ No newline at end of file diff --git a/test/service/runner/gha-github-runner.test.ts b/test/service/runner/gha-github-runner.test.ts index 4772601..ab2236e 100644 --- a/test/service/runner/gha-github-runner.test.ts +++ b/test/service/runner/gha-github-runner.test.ts @@ -3,15 +3,43 @@ import Runner from "@bp/service/runner/runner"; import GitCLIService from "@bp/service/git/git-cli"; import GitHubClient from "@bp/service/git/github/github-client"; import GHAArgsParser from "@bp/service/args/gha/gha-args-parser"; -import { spyGetInput } from "../../support/utils"; +import { createTestFile, removeTestFile, spyGetInput } from "../../support/utils"; import { mockGitHubClient } from "../../support/mock/git-client-mock-support"; +const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME = "./gha-github-runner-pr-merged-with-overrides.json"; +const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT = { + "dryRun": false, + "auth": "my-auth-token", + "pullRequest": "https://github.com/owner/reponame/pull/2368", + "targetBranch": "target", + "gitUser": "Me", + "gitEmail": "me@email.com", + "title": "New Title", + "body": "New Body", + "bodyPrefix": "New Body Prefix - ", + "bpBranchName": "bp_branch_name", + "reviewers": [], + "assignees": ["user3", "user4"], + "inheritReviewers": false, +}; + + jest.mock("@bp/service/git/git-cli"); jest.spyOn(GitHubClient.prototype, "createPullRequest"); let parser: ArgsParser; let runner: Runner; +beforeAll(() => { + // create a temporary file + createTestFile(GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT)); +}); + +afterAll(() => { + // clean up all temporary files + removeTestFile(GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME); +}); + beforeEach(() => { mockGitHubClient(); @@ -231,4 +259,42 @@ describe("gha runner", () => { } ); }); + + test("using config file with overrides", async () => { + spyGetInput({ + "config-file": GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME, + }); + + 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_branch_name"); + + 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_branch_name"); + + expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({ + owner: "owner", + repo: "reponame", + head: "bp_branch_name", + base: "target", + title: "New Title", + body: "New Body Prefix - New Body", + reviewers: [], + assignees: ["user3", "user4"], + } + ); + }); }); \ No newline at end of file diff --git a/test/service/runner/gha-gitlab-runner.test.ts b/test/service/runner/gha-gitlab-runner.test.ts index 78a3ce7..0443342 100644 --- a/test/service/runner/gha-gitlab-runner.test.ts +++ b/test/service/runner/gha-gitlab-runner.test.ts @@ -3,8 +3,25 @@ import Runner from "@bp/service/runner/runner"; import GitCLIService from "@bp/service/git/git-cli"; import GitLabClient from "@bp/service/git/gitlab/gitlab-client"; import GHAArgsParser from "@bp/service/args/gha/gha-args-parser"; -import { spyGetInput } from "../../support/utils"; +import { createTestFile, removeTestFile, spyGetInput } from "../../support/utils"; import { getAxiosMocked } from "../../support/mock/git-client-mock-support"; +import { MERGED_SQUASHED_MR } from "../../support/mock/gitlab-data"; + +const GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME = "./gha-gitlab-runner-pr-merged-with-overrides.json"; +const GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = { + "dryRun": false, + "auth": "my-token", + "pullRequest": `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${MERGED_SQUASHED_MR.iid}`, + "targetBranch": "prod", + "gitUser": "Me", + "gitEmail": "me@email.com", + "title": "New Title", + "body": "New Body", + "bodyPrefix": `**This is a backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${MERGED_SQUASHED_MR.iid}`, + "reviewers": [], + "assignees": ["user3", "user4"], + "inheritReviewers": false, +}; jest.mock("axios", () => { return { @@ -26,6 +43,16 @@ jest.spyOn(GitLabClient.prototype, "createPullRequest"); let parser: ArgsParser; let runner: Runner; +beforeAll(() => { + // create a temporary file + createTestFile(GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME, JSON.stringify(GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT)); +}); + +afterAll(() => { + // clean up all temporary files + removeTestFile(GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME); +}); + beforeEach(() => { // create GHA arguments parser parser = new GHAArgsParser(); @@ -242,4 +269,43 @@ describe("gha runner", () => { } ); }); + + test("using config file with overrides", async () => { + spyGetInput({ + "config-file": GITLAB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME, + }); + + 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, "prod"); + + expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1); + expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-prod-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-prod-ebb1eca696c42fd067658bd9b5267709f78ef38e"); + + expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1); + expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({ + owner: "superuser", + repo: "backporting-example", + head: "bp-prod-ebb1eca696c42fd067658bd9b5267709f78ef38e", + base: "prod", + title: "New Title", + body: expect.stringContaining("**This is a backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"), + reviewers: [], + assignees: ["user3", "user4"], + } + ); + }); }); \ No newline at end of file diff --git a/test/support/utils.ts b/test/support/utils.ts index 8be9212..693610d 100644 --- a/test/support/utils.ts +++ b/test/support/utils.ts @@ -1,4 +1,5 @@ import * as core from "@actions/core"; +import * as fs from "fs"; export const addProcessArgs = (args: string[]) => { process.argv = [...process.argv, ...args]; @@ -24,4 +25,21 @@ export const spyGetInput = (obj: any) => { */ export const expectArrayEqual = (actual: unknown[], expected: unknown[]) => { expect(actual.sort()).toEqual(expected.sort()); +}; + +/** + * Create a test file given the full pathname + * @param pathname full file pathname e.g, /tmp/dir/filename.json + * @param content what must be written in the file + */ +export const createTestFile = (pathname: string, content: string) => { + fs.writeFileSync(pathname, content); +}; + +/** + * Remove a file located at pathname + * @param pathname full file pathname e.g, /tmp/dir/filename.json + */ +export const removeTestFile = (pathname: string) => { + fs.rmSync(pathname); }; \ No newline at end of file