mirror of
https://code.forgejo.org/actions/git-backporting.git
synced 2025-02-22 02:25:44 -05:00
feat: implement error notification as pr comment (#124)
* feat: implement error notification as pr comment * Update action.yml Co-authored-by: Earl Warren <109468362+earl-warren@users.noreply.github.com> * feat: implement gitlab client and surround with try catch * docs: add error notification enablment in the doc * feat: disable comment if dry-run * feat: update the default comment on error --------- Co-authored-by: Earl Warren <109468362+earl-warren@users.noreply.github.com>
This commit is contained in:
parent
6042bcc40b
commit
2bb7f73112
28 changed files with 594 additions and 39 deletions
|
@ -133,6 +133,7 @@ This tool comes with some inputs that allow users to override the default behavi
|
|||
| Strategy Option | --strategy-option | N | Cherry pick merging strategy option, see [git-merge](https://git-scm.com/docs/git-merge#_merge_strategies) doc for all possible values | "theirs" |
|
||||
| Cherry-pick Options | --cherry-pick-options | N | Additional cherry-pick options, see [git-cherry-pick](https://git-scm.com/docs/git-cherry-pick) doc for all possible values | "theirs" |
|
||||
| Additional comments | --comments | N | Semicolon separated list of additional comments to be posted to the backported pull request | [] |
|
||||
| Enable error notification | --enable-err-notification | N | If true, enable the error notification as comment on the original pull request | false |
|
||||
| Dry Run | -d, --dry-run | N | If enabled the tool does not push nor create anything remotely, use this to skip PR creation | false |
|
||||
|
||||
> **NOTE**: `pull request` and (`target branch` or `target branch pattern`) are *mandatory*, they must be provided as CLI options or as part of the configuration file (if used).
|
||||
|
|
|
@ -109,6 +109,11 @@ inputs:
|
|||
description: >
|
||||
Semicolon separated list of additional comments to be posted to the backported pull request
|
||||
required: false
|
||||
enable-err-notification:
|
||||
description: >
|
||||
If true, enable the error notification as comment on the original pull request
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: node20
|
||||
|
|
114
dist/cli/index.js
vendored
114
dist/cli/index.js
vendored
|
@ -70,7 +70,8 @@ class ArgsParser {
|
|||
strategy: this.getOrDefault(args.strategy),
|
||||
strategyOption: this.getOrDefault(args.strategyOption),
|
||||
cherryPickOptions: this.getOrDefault(args.cherryPickOptions),
|
||||
comments: this.getOrDefault(args.comments)
|
||||
comments: this.getOrDefault(args.comments),
|
||||
enableErrorNotification: this.getOrDefault(args.enableErrorNotification, false),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +109,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.getAsBooleanOrDefault = exports.getAsSemicolonSeparatedList = exports.getAsCommaSeparatedList = exports.getAsCleanedCommaSeparatedList = exports.getOrUndefined = exports.readConfigFile = exports.parseArgs = void 0;
|
||||
exports.getAsBooleanOrUndefined = exports.getAsSemicolonSeparatedList = exports.getAsCommaSeparatedList = exports.getAsCleanedCommaSeparatedList = exports.getOrUndefined = exports.readConfigFile = exports.parseArgs = void 0;
|
||||
const fs = __importStar(__nccwpck_require__(7147));
|
||||
/**
|
||||
* Parse the input configuation string as json object and
|
||||
|
@ -159,11 +160,11 @@ function getAsSemicolonSeparatedList(value) {
|
|||
return trimmed !== "" ? trimmed.split(";").map(v => v.trim()) : undefined;
|
||||
}
|
||||
exports.getAsSemicolonSeparatedList = getAsSemicolonSeparatedList;
|
||||
function getAsBooleanOrDefault(value) {
|
||||
function getAsBooleanOrUndefined(value) {
|
||||
const trimmed = value.trim();
|
||||
return trimmed !== "" ? trimmed.toLowerCase() === "true" : undefined;
|
||||
}
|
||||
exports.getAsBooleanOrDefault = getAsBooleanOrDefault;
|
||||
exports.getAsBooleanOrUndefined = getAsBooleanOrUndefined;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
@ -204,12 +205,13 @@ class CLIArgsParser extends args_parser_1.default {
|
|||
.option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request")
|
||||
.option("--labels <labels>", "comma separated list of labels to be assigned to the backported pull request", args_utils_1.getAsCommaSeparatedList)
|
||||
.option("--inherit-labels", "if true the backported pull request will inherit labels from the original one")
|
||||
.option("--no-squash", "Backport all commits found in the pull request. The default behavior is to only backport the first commit that was merged in the base branch")
|
||||
.option("--auto-no-squash", "If the pull request was merged or is open, backport all commits. If the pull request commits were squashed, backport the squashed commit.")
|
||||
.option("--no-squash", "backport all commits found in the pull request. The default behavior is to only backport the first commit that was merged in the base branch")
|
||||
.option("--auto-no-squash", "if the pull request was merged or is open, backport all commits. If the pull request commits were squashed, backport the squashed commit.")
|
||||
.option("--strategy <strategy>", "cherry-pick merge strategy, default to 'recursive'", undefined)
|
||||
.option("--strategy-option <strategy-option>", "cherry-pick merge strategy option, default to 'theirs'")
|
||||
.option("--cherry-pick-options <options>", "additional cherry-pick options")
|
||||
.option("--comments <comments>", "semicolon separated list of additional comments to be posted to the backported pull request", args_utils_1.getAsSemicolonSeparatedList)
|
||||
.option("--enable-err-notification", "if true, enable the error notification as comment on the original pull request")
|
||||
.option("-cf, --config-file <config-file>", "configuration file containing all valid options, the json must match Args interface");
|
||||
}
|
||||
readArgs() {
|
||||
|
@ -247,6 +249,7 @@ class CLIArgsParser extends args_parser_1.default {
|
|||
strategyOption: opts.strategyOption,
|
||||
cherryPickOptions: opts.cherryPickOptions,
|
||||
comments: opts.comments,
|
||||
enableErrorNotification: opts.enableErrNotification,
|
||||
};
|
||||
}
|
||||
return args;
|
||||
|
@ -300,7 +303,9 @@ exports["default"] = ConfigsParser;
|
|||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.AuthTokenId = void 0;
|
||||
exports.AuthTokenId = exports.MESSAGE_TARGET_BRANCH_PLACEHOLDER = exports.MESSAGE_ERROR_PLACEHOLDER = void 0;
|
||||
exports.MESSAGE_ERROR_PLACEHOLDER = "{{error}}";
|
||||
exports.MESSAGE_TARGET_BRANCH_PLACEHOLDER = "{{target-branch}}";
|
||||
var AuthTokenId;
|
||||
(function (AuthTokenId) {
|
||||
// github specific token
|
||||
|
@ -327,6 +332,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
const args_utils_1 = __nccwpck_require__(8048);
|
||||
const configs_parser_1 = __importDefault(__nccwpck_require__(5799));
|
||||
const configs_types_1 = __nccwpck_require__(4753);
|
||||
const git_client_factory_1 = __importDefault(__nccwpck_require__(8550));
|
||||
class PullRequestConfigsParser extends configs_parser_1.default {
|
||||
constructor() {
|
||||
|
@ -374,12 +380,20 @@ class PullRequestConfigsParser extends configs_parser_1.default {
|
|||
git: {
|
||||
user: args.gitUser ?? this.gitClient.getDefaultGitUser(),
|
||||
email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(),
|
||||
}
|
||||
},
|
||||
errorNotification: {
|
||||
enabled: args.enableErrorNotification ?? false,
|
||||
message: this.getDefaultErrorComment(),
|
||||
},
|
||||
};
|
||||
}
|
||||
getDefaultFolder() {
|
||||
return "bp";
|
||||
}
|
||||
getDefaultErrorComment() {
|
||||
// TODO: fetch from arg or set default with placeholder {{error}}
|
||||
return `The backport to \`${configs_types_1.MESSAGE_TARGET_BRANCH_PLACEHOLDER}\` failed. Check the latest run for more details.`;
|
||||
}
|
||||
/**
|
||||
* Parse the provided labels and return a list of target branches
|
||||
* obtained by applying the provided pattern as regular expression extractor
|
||||
|
@ -934,6 +948,26 @@ class GitHubClient {
|
|||
await Promise.all(promises);
|
||||
return data.html_url;
|
||||
}
|
||||
async createPullRequestComment(prUrl, comment) {
|
||||
let commentUrl = undefined;
|
||||
try {
|
||||
const { owner, project, id } = this.extractPullRequestData(prUrl);
|
||||
const { data } = await this.octokit.issues.createComment({
|
||||
owner: owner,
|
||||
repo: project,
|
||||
issue_number: id,
|
||||
body: comment
|
||||
});
|
||||
if (!data) {
|
||||
throw new Error("Pull request comment creation failed");
|
||||
}
|
||||
commentUrl = data.url;
|
||||
}
|
||||
catch (error) {
|
||||
this.logger.error(`Error creating comment on pull request ${prUrl}: ${error}`);
|
||||
}
|
||||
return commentUrl;
|
||||
}
|
||||
// UTILS
|
||||
/**
|
||||
* Extract repository owner and project from the pull request url
|
||||
|
@ -1093,7 +1127,7 @@ class GitLabClient {
|
|||
const projectId = this.getProjectId(namespace, repo);
|
||||
const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}`);
|
||||
if (squash === undefined) {
|
||||
squash = (0, git_util_1.inferSquash)(data.state == "opened", data.squash_commit_sha);
|
||||
squash = (0, git_util_1.inferSquash)(data.state === "opened", data.squash_commit_sha);
|
||||
}
|
||||
const commits = [];
|
||||
if (!squash) {
|
||||
|
@ -1175,6 +1209,25 @@ class GitLabClient {
|
|||
await Promise.all(promises);
|
||||
return mr.web_url;
|
||||
}
|
||||
// https://docs.gitlab.com/ee/api/notes.html#create-new-issue-note
|
||||
async createPullRequestComment(mrUrl, comment) {
|
||||
const commentUrl = undefined;
|
||||
try {
|
||||
const { namespace, project, id } = this.extractMergeRequestData(mrUrl);
|
||||
const projectId = this.getProjectId(namespace, project);
|
||||
const { data } = await this.client.post(`/projects/${projectId}/issues/${id}/notes`, {
|
||||
body: comment,
|
||||
});
|
||||
if (!data) {
|
||||
throw new Error("Merge request comment creation failed");
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
this.logger.error(`Error creating comment on merge request ${mrUrl}: ${error}`);
|
||||
}
|
||||
return commentUrl;
|
||||
}
|
||||
// UTILS
|
||||
/**
|
||||
* Retrieve a gitlab user given its username
|
||||
* @param username
|
||||
|
@ -1322,6 +1375,9 @@ class ConsoleLoggerService {
|
|||
setContext(newContext) {
|
||||
this.context = newContext;
|
||||
}
|
||||
getContext() {
|
||||
return this.context;
|
||||
}
|
||||
clearContext() {
|
||||
this.context = undefined;
|
||||
}
|
||||
|
@ -1398,6 +1454,39 @@ class Logger {
|
|||
exports["default"] = Logger;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 9632:
|
||||
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
||||
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.injectTargetBranch = exports.injectError = void 0;
|
||||
const configs_types_1 = __nccwpck_require__(4753);
|
||||
/**
|
||||
* Inject the error message in the provided `message`.
|
||||
* This is injected in place of the MESSAGE_ERROR_PLACEHOLDER placeholder
|
||||
* @param message string that needs to be updated
|
||||
* @param errMsg the error message that needs to be injected
|
||||
*/
|
||||
const injectError = (message, errMsg) => {
|
||||
return message.replace(configs_types_1.MESSAGE_ERROR_PLACEHOLDER, errMsg);
|
||||
};
|
||||
exports.injectError = injectError;
|
||||
/**
|
||||
* Inject the target branch into the provided `message`.
|
||||
* This is injected in place of the MESSAGE_TARGET_BRANCH_PLACEHOLDER placeholder
|
||||
* @param message string that needs to be updated
|
||||
* @param targetBranch the target branch to inject
|
||||
* @returns
|
||||
*/
|
||||
const injectTargetBranch = (message, targetBranch) => {
|
||||
return message.replace(configs_types_1.MESSAGE_TARGET_BRANCH_PLACEHOLDER, targetBranch);
|
||||
};
|
||||
exports.injectTargetBranch = injectTargetBranch;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 8810:
|
||||
|
@ -1415,6 +1504,7 @@ const git_client_factory_1 = __importDefault(__nccwpck_require__(8550));
|
|||
const git_types_1 = __nccwpck_require__(750);
|
||||
const logger_service_factory_1 = __importDefault(__nccwpck_require__(8936));
|
||||
const git_util_1 = __nccwpck_require__(9080);
|
||||
const runner_util_1 = __nccwpck_require__(9632);
|
||||
/**
|
||||
* Main runner implementation, it implements the core logic flow
|
||||
*/
|
||||
|
@ -1479,6 +1569,12 @@ class Runner {
|
|||
}
|
||||
catch (error) {
|
||||
this.logger.error(`Something went wrong backporting to ${pr.base}: ${error}`);
|
||||
if (!configs.dryRun && configs.errorNotification.enabled && configs.errorNotification.message.length > 0) {
|
||||
// notify the failure as comment in the original pull request
|
||||
let comment = (0, runner_util_1.injectError)(configs.errorNotification.message, error);
|
||||
comment = (0, runner_util_1.injectTargetBranch)(comment, pr.base);
|
||||
await gitApi.createPullRequestComment(configs.originalPullRequest.url, comment);
|
||||
}
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
|
|
119
dist/gha/index.js
vendored
119
dist/gha/index.js
vendored
|
@ -70,7 +70,8 @@ class ArgsParser {
|
|||
strategy: this.getOrDefault(args.strategy),
|
||||
strategyOption: this.getOrDefault(args.strategyOption),
|
||||
cherryPickOptions: this.getOrDefault(args.cherryPickOptions),
|
||||
comments: this.getOrDefault(args.comments)
|
||||
comments: this.getOrDefault(args.comments),
|
||||
enableErrorNotification: this.getOrDefault(args.enableErrorNotification, false),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +109,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.getAsBooleanOrDefault = exports.getAsSemicolonSeparatedList = exports.getAsCommaSeparatedList = exports.getAsCleanedCommaSeparatedList = exports.getOrUndefined = exports.readConfigFile = exports.parseArgs = void 0;
|
||||
exports.getAsBooleanOrUndefined = exports.getAsSemicolonSeparatedList = exports.getAsCommaSeparatedList = exports.getAsCleanedCommaSeparatedList = exports.getOrUndefined = exports.readConfigFile = exports.parseArgs = void 0;
|
||||
const fs = __importStar(__nccwpck_require__(7147));
|
||||
/**
|
||||
* Parse the input configuation string as json object and
|
||||
|
@ -159,11 +160,11 @@ function getAsSemicolonSeparatedList(value) {
|
|||
return trimmed !== "" ? trimmed.split(";").map(v => v.trim()) : undefined;
|
||||
}
|
||||
exports.getAsSemicolonSeparatedList = getAsSemicolonSeparatedList;
|
||||
function getAsBooleanOrDefault(value) {
|
||||
function getAsBooleanOrUndefined(value) {
|
||||
const trimmed = value.trim();
|
||||
return trimmed !== "" ? trimmed.toLowerCase() === "true" : undefined;
|
||||
}
|
||||
exports.getAsBooleanOrDefault = getAsBooleanOrDefault;
|
||||
exports.getAsBooleanOrUndefined = getAsBooleanOrUndefined;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
@ -189,7 +190,7 @@ class GHAArgsParser extends args_parser_1.default {
|
|||
}
|
||||
else {
|
||||
args = {
|
||||
dryRun: (0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("dry-run")),
|
||||
dryRun: (0, args_utils_1.getAsBooleanOrUndefined)((0, core_1.getInput)("dry-run")),
|
||||
auth: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("auth")),
|
||||
pullRequest: (0, core_1.getInput)("pull-request"),
|
||||
targetBranch: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("target-branch")),
|
||||
|
@ -204,15 +205,16 @@ class GHAArgsParser extends args_parser_1.default {
|
|||
bpBranchName: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("bp-branch-name")),
|
||||
reviewers: (0, args_utils_1.getAsCleanedCommaSeparatedList)((0, core_1.getInput)("reviewers")),
|
||||
assignees: (0, args_utils_1.getAsCleanedCommaSeparatedList)((0, core_1.getInput)("assignees")),
|
||||
inheritReviewers: !(0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("no-inherit-reviewers")),
|
||||
inheritReviewers: !(0, args_utils_1.getAsBooleanOrUndefined)((0, core_1.getInput)("no-inherit-reviewers")),
|
||||
labels: (0, args_utils_1.getAsCommaSeparatedList)((0, core_1.getInput)("labels")),
|
||||
inheritLabels: (0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("inherit-labels")),
|
||||
squash: !(0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("no-squash")),
|
||||
autoNoSquash: (0, args_utils_1.getAsBooleanOrDefault)((0, core_1.getInput)("auto-no-squash")),
|
||||
inheritLabels: (0, args_utils_1.getAsBooleanOrUndefined)((0, core_1.getInput)("inherit-labels")),
|
||||
squash: !(0, args_utils_1.getAsBooleanOrUndefined)((0, core_1.getInput)("no-squash")),
|
||||
autoNoSquash: (0, args_utils_1.getAsBooleanOrUndefined)((0, core_1.getInput)("auto-no-squash")),
|
||||
strategy: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("strategy")),
|
||||
strategyOption: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("strategy-option")),
|
||||
cherryPickOptions: (0, args_utils_1.getOrUndefined)((0, core_1.getInput)("cherry-pick-options")),
|
||||
comments: (0, args_utils_1.getAsSemicolonSeparatedList)((0, core_1.getInput)("comments")),
|
||||
enableErrorNotification: (0, args_utils_1.getAsBooleanOrUndefined)((0, core_1.getInput)("enable-err-notification")),
|
||||
};
|
||||
}
|
||||
return args;
|
||||
|
@ -266,7 +268,9 @@ exports["default"] = ConfigsParser;
|
|||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.AuthTokenId = void 0;
|
||||
exports.AuthTokenId = exports.MESSAGE_TARGET_BRANCH_PLACEHOLDER = exports.MESSAGE_ERROR_PLACEHOLDER = void 0;
|
||||
exports.MESSAGE_ERROR_PLACEHOLDER = "{{error}}";
|
||||
exports.MESSAGE_TARGET_BRANCH_PLACEHOLDER = "{{target-branch}}";
|
||||
var AuthTokenId;
|
||||
(function (AuthTokenId) {
|
||||
// github specific token
|
||||
|
@ -293,6 +297,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
const args_utils_1 = __nccwpck_require__(8048);
|
||||
const configs_parser_1 = __importDefault(__nccwpck_require__(5799));
|
||||
const configs_types_1 = __nccwpck_require__(4753);
|
||||
const git_client_factory_1 = __importDefault(__nccwpck_require__(8550));
|
||||
class PullRequestConfigsParser extends configs_parser_1.default {
|
||||
constructor() {
|
||||
|
@ -340,12 +345,20 @@ class PullRequestConfigsParser extends configs_parser_1.default {
|
|||
git: {
|
||||
user: args.gitUser ?? this.gitClient.getDefaultGitUser(),
|
||||
email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(),
|
||||
}
|
||||
},
|
||||
errorNotification: {
|
||||
enabled: args.enableErrorNotification ?? false,
|
||||
message: this.getDefaultErrorComment(),
|
||||
},
|
||||
};
|
||||
}
|
||||
getDefaultFolder() {
|
||||
return "bp";
|
||||
}
|
||||
getDefaultErrorComment() {
|
||||
// TODO: fetch from arg or set default with placeholder {{error}}
|
||||
return `The backport to \`${configs_types_1.MESSAGE_TARGET_BRANCH_PLACEHOLDER}\` failed. Check the latest run for more details.`;
|
||||
}
|
||||
/**
|
||||
* Parse the provided labels and return a list of target branches
|
||||
* obtained by applying the provided pattern as regular expression extractor
|
||||
|
@ -900,6 +913,26 @@ class GitHubClient {
|
|||
await Promise.all(promises);
|
||||
return data.html_url;
|
||||
}
|
||||
async createPullRequestComment(prUrl, comment) {
|
||||
let commentUrl = undefined;
|
||||
try {
|
||||
const { owner, project, id } = this.extractPullRequestData(prUrl);
|
||||
const { data } = await this.octokit.issues.createComment({
|
||||
owner: owner,
|
||||
repo: project,
|
||||
issue_number: id,
|
||||
body: comment
|
||||
});
|
||||
if (!data) {
|
||||
throw new Error("Pull request comment creation failed");
|
||||
}
|
||||
commentUrl = data.url;
|
||||
}
|
||||
catch (error) {
|
||||
this.logger.error(`Error creating comment on pull request ${prUrl}: ${error}`);
|
||||
}
|
||||
return commentUrl;
|
||||
}
|
||||
// UTILS
|
||||
/**
|
||||
* Extract repository owner and project from the pull request url
|
||||
|
@ -1059,7 +1092,7 @@ class GitLabClient {
|
|||
const projectId = this.getProjectId(namespace, repo);
|
||||
const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}`);
|
||||
if (squash === undefined) {
|
||||
squash = (0, git_util_1.inferSquash)(data.state == "opened", data.squash_commit_sha);
|
||||
squash = (0, git_util_1.inferSquash)(data.state === "opened", data.squash_commit_sha);
|
||||
}
|
||||
const commits = [];
|
||||
if (!squash) {
|
||||
|
@ -1141,6 +1174,25 @@ class GitLabClient {
|
|||
await Promise.all(promises);
|
||||
return mr.web_url;
|
||||
}
|
||||
// https://docs.gitlab.com/ee/api/notes.html#create-new-issue-note
|
||||
async createPullRequestComment(mrUrl, comment) {
|
||||
const commentUrl = undefined;
|
||||
try {
|
||||
const { namespace, project, id } = this.extractMergeRequestData(mrUrl);
|
||||
const projectId = this.getProjectId(namespace, project);
|
||||
const { data } = await this.client.post(`/projects/${projectId}/issues/${id}/notes`, {
|
||||
body: comment,
|
||||
});
|
||||
if (!data) {
|
||||
throw new Error("Merge request comment creation failed");
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
this.logger.error(`Error creating comment on merge request ${mrUrl}: ${error}`);
|
||||
}
|
||||
return commentUrl;
|
||||
}
|
||||
// UTILS
|
||||
/**
|
||||
* Retrieve a gitlab user given its username
|
||||
* @param username
|
||||
|
@ -1288,6 +1340,9 @@ class ConsoleLoggerService {
|
|||
setContext(newContext) {
|
||||
this.context = newContext;
|
||||
}
|
||||
getContext() {
|
||||
return this.context;
|
||||
}
|
||||
clearContext() {
|
||||
this.context = undefined;
|
||||
}
|
||||
|
@ -1364,6 +1419,39 @@ class Logger {
|
|||
exports["default"] = Logger;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 9632:
|
||||
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
||||
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.injectTargetBranch = exports.injectError = void 0;
|
||||
const configs_types_1 = __nccwpck_require__(4753);
|
||||
/**
|
||||
* Inject the error message in the provided `message`.
|
||||
* This is injected in place of the MESSAGE_ERROR_PLACEHOLDER placeholder
|
||||
* @param message string that needs to be updated
|
||||
* @param errMsg the error message that needs to be injected
|
||||
*/
|
||||
const injectError = (message, errMsg) => {
|
||||
return message.replace(configs_types_1.MESSAGE_ERROR_PLACEHOLDER, errMsg);
|
||||
};
|
||||
exports.injectError = injectError;
|
||||
/**
|
||||
* Inject the target branch into the provided `message`.
|
||||
* This is injected in place of the MESSAGE_TARGET_BRANCH_PLACEHOLDER placeholder
|
||||
* @param message string that needs to be updated
|
||||
* @param targetBranch the target branch to inject
|
||||
* @returns
|
||||
*/
|
||||
const injectTargetBranch = (message, targetBranch) => {
|
||||
return message.replace(configs_types_1.MESSAGE_TARGET_BRANCH_PLACEHOLDER, targetBranch);
|
||||
};
|
||||
exports.injectTargetBranch = injectTargetBranch;
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 8810:
|
||||
|
@ -1381,6 +1469,7 @@ const git_client_factory_1 = __importDefault(__nccwpck_require__(8550));
|
|||
const git_types_1 = __nccwpck_require__(750);
|
||||
const logger_service_factory_1 = __importDefault(__nccwpck_require__(8936));
|
||||
const git_util_1 = __nccwpck_require__(9080);
|
||||
const runner_util_1 = __nccwpck_require__(9632);
|
||||
/**
|
||||
* Main runner implementation, it implements the core logic flow
|
||||
*/
|
||||
|
@ -1445,6 +1534,12 @@ class Runner {
|
|||
}
|
||||
catch (error) {
|
||||
this.logger.error(`Something went wrong backporting to ${pr.base}: ${error}`);
|
||||
if (!configs.dryRun && configs.errorNotification.enabled && configs.errorNotification.message.length > 0) {
|
||||
// notify the failure as comment in the original pull request
|
||||
let comment = (0, runner_util_1.injectError)(configs.errorNotification.message, error);
|
||||
comment = (0, runner_util_1.injectTargetBranch)(comment, pr.base);
|
||||
await gitApi.createPullRequestComment(configs.originalPullRequest.url, comment);
|
||||
}
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,8 @@ export default abstract class ArgsParser {
|
|||
strategy: this.getOrDefault(args.strategy),
|
||||
strategyOption: this.getOrDefault(args.strategyOption),
|
||||
cherryPickOptions: this.getOrDefault(args.cherryPickOptions),
|
||||
comments: this.getOrDefault(args.comments)
|
||||
comments: this.getOrDefault(args.comments),
|
||||
enableErrorNotification: this.getOrDefault(args.enableErrorNotification, false),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -50,7 +50,7 @@ export function getAsSemicolonSeparatedList(value: string): string[] | undefined
|
|||
return trimmed !== "" ? trimmed.split(";").map(v => v.trim()) : undefined;
|
||||
}
|
||||
|
||||
export function getAsBooleanOrDefault(value: string): boolean | undefined {
|
||||
export function getAsBooleanOrUndefined(value: string): boolean | undefined {
|
||||
const trimmed = value.trim();
|
||||
return trimmed !== "" ? trimmed.toLowerCase() === "true" : undefined;
|
||||
}
|
|
@ -28,4 +28,5 @@ export interface Args {
|
|||
strategyOption?: string, // cherry-pick merge strategy option
|
||||
cherryPickOptions?: string, // additional cherry-pick options
|
||||
comments?: string[], // additional comments to be posted
|
||||
enableErrorNotification?: boolean, // enable the error notification on original pull request
|
||||
}
|
|
@ -28,12 +28,13 @@ export default class CLIArgsParser extends ArgsParser {
|
|||
.option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request")
|
||||
.option("--labels <labels>", "comma separated list of labels to be assigned to the backported pull request", getAsCommaSeparatedList)
|
||||
.option("--inherit-labels", "if true the backported pull request will inherit labels from the original one")
|
||||
.option("--no-squash", "Backport all commits found in the pull request. The default behavior is to only backport the first commit that was merged in the base branch")
|
||||
.option("--auto-no-squash", "If the pull request was merged or is open, backport all commits. If the pull request commits were squashed, backport the squashed commit.")
|
||||
.option("--no-squash", "backport all commits found in the pull request. The default behavior is to only backport the first commit that was merged in the base branch")
|
||||
.option("--auto-no-squash", "if the pull request was merged or is open, backport all commits. If the pull request commits were squashed, backport the squashed commit.")
|
||||
.option("--strategy <strategy>", "cherry-pick merge strategy, default to 'recursive'", undefined)
|
||||
.option("--strategy-option <strategy-option>", "cherry-pick merge strategy option, default to 'theirs'")
|
||||
.option("--cherry-pick-options <options>", "additional cherry-pick options")
|
||||
.option("--comments <comments>", "semicolon separated list of additional comments to be posted to the backported pull request", getAsSemicolonSeparatedList)
|
||||
.option("--enable-err-notification", "if true, enable the error notification as comment on the original pull request")
|
||||
.option("-cf, --config-file <config-file>", "configuration file containing all valid options, the json must match Args interface");
|
||||
}
|
||||
|
||||
|
@ -72,6 +73,7 @@ export default class CLIArgsParser extends ArgsParser {
|
|||
strategyOption: opts.strategyOption,
|
||||
cherryPickOptions: opts.cherryPickOptions,
|
||||
comments: opts.comments,
|
||||
enableErrorNotification: opts.enableErrNotification,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import ArgsParser from "@bp/service/args/args-parser";
|
||||
import { Args } from "@bp/service/args/args.types";
|
||||
import { getInput } from "@actions/core";
|
||||
import { getAsBooleanOrDefault, getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, getAsSemicolonSeparatedList, getOrUndefined, readConfigFile } from "@bp/service/args/args-utils";
|
||||
import { getAsBooleanOrUndefined, getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, getAsSemicolonSeparatedList, getOrUndefined, readConfigFile } from "@bp/service/args/args-utils";
|
||||
|
||||
export default class GHAArgsParser extends ArgsParser {
|
||||
|
||||
|
@ -13,7 +13,7 @@ export default class GHAArgsParser extends ArgsParser {
|
|||
args = readConfigFile(configFile);
|
||||
} else {
|
||||
args = {
|
||||
dryRun: getAsBooleanOrDefault(getInput("dry-run")),
|
||||
dryRun: getAsBooleanOrUndefined(getInput("dry-run")),
|
||||
auth: getOrUndefined(getInput("auth")),
|
||||
pullRequest: getInput("pull-request"),
|
||||
targetBranch: getOrUndefined(getInput("target-branch")),
|
||||
|
@ -28,15 +28,16 @@ export default class GHAArgsParser extends ArgsParser {
|
|||
bpBranchName: getOrUndefined(getInput("bp-branch-name")),
|
||||
reviewers: getAsCleanedCommaSeparatedList(getInput("reviewers")),
|
||||
assignees: getAsCleanedCommaSeparatedList(getInput("assignees")),
|
||||
inheritReviewers: !getAsBooleanOrDefault(getInput("no-inherit-reviewers")),
|
||||
inheritReviewers: !getAsBooleanOrUndefined(getInput("no-inherit-reviewers")),
|
||||
labels: getAsCommaSeparatedList(getInput("labels")),
|
||||
inheritLabels: getAsBooleanOrDefault(getInput("inherit-labels")),
|
||||
squash: !getAsBooleanOrDefault(getInput("no-squash")),
|
||||
autoNoSquash: getAsBooleanOrDefault(getInput("auto-no-squash")),
|
||||
inheritLabels: getAsBooleanOrUndefined(getInput("inherit-labels")),
|
||||
squash: !getAsBooleanOrUndefined(getInput("no-squash")),
|
||||
autoNoSquash: getAsBooleanOrUndefined(getInput("auto-no-squash")),
|
||||
strategy: getOrUndefined(getInput("strategy")),
|
||||
strategyOption: getOrUndefined(getInput("strategy-option")),
|
||||
cherryPickOptions: getOrUndefined(getInput("cherry-pick-options")),
|
||||
comments: getAsSemicolonSeparatedList(getInput("comments")),
|
||||
enableErrorNotification: getAsBooleanOrUndefined(getInput("enable-err-notification")),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,19 @@
|
|||
|
||||
import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
|
||||
|
||||
export const MESSAGE_ERROR_PLACEHOLDER = "{{error}}";
|
||||
export const MESSAGE_TARGET_BRANCH_PLACEHOLDER = "{{target-branch}}";
|
||||
|
||||
export interface LocalGit {
|
||||
user: string, // local git user
|
||||
email: string, // local git email
|
||||
}
|
||||
|
||||
export interface ErrorNotification {
|
||||
enabled: boolean, // if the error notification is enabled
|
||||
message: string, // notification message, placeholder {{error}} will be replaced with actual error
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal configuration object
|
||||
*/
|
||||
|
@ -20,6 +28,7 @@ export interface Configs {
|
|||
cherryPickOptions?: string, // additional cherry-pick options
|
||||
originalPullRequest: GitPullRequest,
|
||||
backportPullRequests: BackportPullRequest[],
|
||||
errorNotification: ErrorNotification,
|
||||
}
|
||||
|
||||
export enum AuthTokenId {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getAsCleanedCommaSeparatedList, getAsCommaSeparatedList } from "@bp/service/args/args-utils";
|
||||
import { Args } from "@bp/service/args/args.types";
|
||||
import ConfigsParser from "@bp/service/configs/configs-parser";
|
||||
import { Configs } from "@bp/service/configs/configs.types";
|
||||
import { Configs, MESSAGE_TARGET_BRANCH_PLACEHOLDER } from "@bp/service/configs/configs.types";
|
||||
import GitClient from "@bp/service/git/git-client";
|
||||
import GitClientFactory from "@bp/service/git/git-client-factory";
|
||||
import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
|
||||
|
@ -58,7 +58,11 @@ export default class PullRequestConfigsParser extends ConfigsParser {
|
|||
git: {
|
||||
user: args.gitUser ?? this.gitClient.getDefaultGitUser(),
|
||||
email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(),
|
||||
}
|
||||
},
|
||||
errorNotification: {
|
||||
enabled: args.enableErrorNotification ?? false,
|
||||
message: this.getDefaultErrorComment(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -66,6 +70,11 @@ export default class PullRequestConfigsParser extends ConfigsParser {
|
|||
return "bp";
|
||||
}
|
||||
|
||||
private getDefaultErrorComment(): string {
|
||||
// TODO: fetch from arg or set default with placeholder {{error}}
|
||||
return `The backport to \`${MESSAGE_TARGET_BRANCH_PLACEHOLDER}\` failed. Check the latest run for more details.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the provided labels and return a list of target branches
|
||||
* obtained by applying the provided pattern as regular expression extractor
|
||||
|
|
|
@ -44,4 +44,11 @@ import { BackportPullRequest, GitClientType, GitPullRequest } from "@bp/service/
|
|||
*/
|
||||
createPullRequest(backport: BackportPullRequest): Promise<string>;
|
||||
|
||||
/**
|
||||
* Create a new comment on the provided pull request
|
||||
* @param prUrl pull request's URL
|
||||
* @param comment comment body
|
||||
*/
|
||||
createPullRequestComment(prUrl: string, comment: string): Promise<string | undefined>;
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
export interface GitPullRequest {
|
||||
number?: number,
|
||||
author: string,
|
||||
url?: string,
|
||||
url: string,
|
||||
htmlUrl?: string,
|
||||
state?: GitRepoState,
|
||||
merged?: boolean,
|
||||
|
|
|
@ -158,6 +158,29 @@ export default class GitHubClient implements GitClient {
|
|||
return data.html_url;
|
||||
}
|
||||
|
||||
async createPullRequestComment(prUrl: string, comment: string): Promise<string | undefined> {
|
||||
let commentUrl: string | undefined = undefined;
|
||||
try {
|
||||
const { owner, project, id } = this.extractPullRequestData(prUrl);
|
||||
const { data } = await this.octokit.issues.createComment({
|
||||
owner: owner,
|
||||
repo: project,
|
||||
issue_number: id,
|
||||
body: comment
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
throw new Error("Pull request comment creation failed");
|
||||
}
|
||||
|
||||
commentUrl = data.url;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating comment on pull request ${prUrl}: ${error}`);
|
||||
}
|
||||
|
||||
return commentUrl;
|
||||
}
|
||||
|
||||
// UTILS
|
||||
|
||||
/**
|
||||
|
|
|
@ -162,6 +162,29 @@ export default class GitLabClient implements GitClient {
|
|||
return mr.web_url;
|
||||
}
|
||||
|
||||
// https://docs.gitlab.com/ee/api/notes.html#create-new-issue-note
|
||||
async createPullRequestComment(mrUrl: string, comment: string): Promise<string | undefined> {
|
||||
const commentUrl: string | undefined = undefined;
|
||||
try{
|
||||
const { namespace, project, id } = this.extractMergeRequestData(mrUrl);
|
||||
const projectId = this.getProjectId(namespace, project);
|
||||
|
||||
const { data } = await this.client.post(`/projects/${projectId}/issues/${id}/notes`, {
|
||||
body: comment,
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
throw new Error("Merge request comment creation failed");
|
||||
}
|
||||
} catch(error) {
|
||||
this.logger.error(`Error creating comment on merge request ${mrUrl}: ${error}`);
|
||||
}
|
||||
|
||||
return commentUrl;
|
||||
}
|
||||
|
||||
// UTILS
|
||||
|
||||
/**
|
||||
* Retrieve a gitlab user given its username
|
||||
* @param username
|
||||
|
|
|
@ -16,6 +16,10 @@ export default class ConsoleLoggerService implements LoggerService {
|
|||
this.context = newContext;
|
||||
}
|
||||
|
||||
getContext(): string | undefined {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
clearContext() {
|
||||
this.context = undefined;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ export default interface LoggerService {
|
|||
|
||||
setContext(newContext: string): void;
|
||||
|
||||
getContext(): string | undefined;
|
||||
|
||||
clearContext(): void;
|
||||
|
||||
trace(message: string): void;
|
||||
|
|
22
src/service/runner/runner-util.ts
Normal file
22
src/service/runner/runner-util.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { MESSAGE_ERROR_PLACEHOLDER, MESSAGE_TARGET_BRANCH_PLACEHOLDER } from "@bp/service/configs/configs.types";
|
||||
|
||||
/**
|
||||
* Inject the error message in the provided `message`.
|
||||
* This is injected in place of the MESSAGE_ERROR_PLACEHOLDER placeholder
|
||||
* @param message string that needs to be updated
|
||||
* @param errMsg the error message that needs to be injected
|
||||
*/
|
||||
export const injectError = (message: string, errMsg: string): string => {
|
||||
return message.replace(MESSAGE_ERROR_PLACEHOLDER, errMsg);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inject the target branch into the provided `message`.
|
||||
* This is injected in place of the MESSAGE_TARGET_BRANCH_PLACEHOLDER placeholder
|
||||
* @param message string that needs to be updated
|
||||
* @param targetBranch the target branch to inject
|
||||
* @returns
|
||||
*/
|
||||
export const injectTargetBranch = (message: string, targetBranch: string): string => {
|
||||
return message.replace(MESSAGE_TARGET_BRANCH_PLACEHOLDER, targetBranch);
|
||||
};
|
|
@ -9,6 +9,7 @@ import { BackportPullRequest, GitClientType, GitPullRequest } from "@bp/service/
|
|||
import LoggerService from "@bp/service/logger/logger-service";
|
||||
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
|
||||
import { inferGitClient, inferGitApiUrl, getGitTokenFromEnv } from "@bp/service/git/git-util";
|
||||
import { injectError, injectTargetBranch } from "./runner-util";
|
||||
|
||||
interface Git {
|
||||
gitClientType: GitClientType;
|
||||
|
@ -92,6 +93,12 @@ export default class Runner {
|
|||
});
|
||||
} catch(error) {
|
||||
this.logger.error(`Something went wrong backporting to ${pr.base}: ${error}`);
|
||||
if (!configs.dryRun && configs.errorNotification.enabled && configs.errorNotification.message.length > 0) {
|
||||
// notify the failure as comment in the original pull request
|
||||
let comment = injectError(configs.errorNotification.message, error as string);
|
||||
comment = injectTargetBranch(comment, pr.base);
|
||||
await gitApi.createPullRequestComment(configs.originalPullRequest.url, comment);
|
||||
}
|
||||
failures.push(error as string);
|
||||
}
|
||||
}
|
||||
|
@ -133,13 +140,12 @@ export default class Runner {
|
|||
|
||||
// 5. create new branch from target one and checkout
|
||||
this.logger.debug("Creating local branch..");
|
||||
|
||||
await git.gitCli.createLocalBranch(configs.folder, backportPR.head);
|
||||
|
||||
// 6. fetch pull request remote if source owner != target owner or pull request still open
|
||||
if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner ||
|
||||
configs.originalPullRequest.state === "open") {
|
||||
this.logger.debug("Fetching pull request remote..");
|
||||
this.logger.debug("Fetching pull request remote..");
|
||||
const prefix = git.gitClientType === GitClientType.GITLAB ? "merge-requests" : "pull" ; // default is for gitlab
|
||||
await git.gitCli.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`);
|
||||
}
|
||||
|
@ -165,4 +171,4 @@ export default class Runner {
|
|||
|
||||
this.logger.clearContext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,9 +79,11 @@ describe("cli args parser", () => {
|
|||
expect(args.labels).toEqual([]);
|
||||
expect(args.inheritLabels).toEqual(false);
|
||||
expect(args.squash).toEqual(true);
|
||||
expect(args.autoNoSquash).toEqual(false);
|
||||
expect(args.strategy).toEqual(undefined);
|
||||
expect(args.strategyOption).toEqual(undefined);
|
||||
expect(args.cherryPickOptions).toEqual(undefined);
|
||||
expect(args.enableErrorNotification).toEqual(false);
|
||||
});
|
||||
|
||||
test("with config file [default, short]", () => {
|
||||
|
@ -109,9 +111,11 @@ describe("cli args parser", () => {
|
|||
expect(args.labels).toEqual([]);
|
||||
expect(args.inheritLabels).toEqual(false);
|
||||
expect(args.squash).toEqual(true);
|
||||
expect(args.autoNoSquash).toEqual(false);
|
||||
expect(args.strategy).toEqual(undefined);
|
||||
expect(args.strategyOption).toEqual(undefined);
|
||||
expect(args.cherryPickOptions).toEqual(undefined);
|
||||
expect(args.enableErrorNotification).toEqual(false);
|
||||
});
|
||||
|
||||
test("valid execution [default, long]", () => {
|
||||
|
@ -521,4 +525,17 @@ describe("cli args parser", () => {
|
|||
|
||||
expect(() => parser.parse()).toThrowError("Missing option: pull request must be provided");
|
||||
});
|
||||
|
||||
test("enable error notification flag", () => {
|
||||
addProcessArgs([
|
||||
"-tb",
|
||||
"target, old",
|
||||
"-pr",
|
||||
"https://localhost/whatever/pulls/1",
|
||||
"--enable-err-notification",
|
||||
]);
|
||||
|
||||
const args: Args = parser.parse();
|
||||
expect(args.enableErrorNotification).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -295,7 +295,6 @@ describe("gha args parser", () => {
|
|||
expect(args.cherryPickOptions).toEqual(undefined);
|
||||
});
|
||||
|
||||
|
||||
test("invalid execution with empty target branch", () => {
|
||||
spyGetInput({
|
||||
"target-branch": " ",
|
||||
|
@ -320,4 +319,15 @@ describe("gha args parser", () => {
|
|||
|
||||
expect(() => parser.parse()).toThrowError("Missing option: pull request must be provided");
|
||||
});
|
||||
|
||||
test("enable error notification flag", () => {
|
||||
spyGetInput({
|
||||
"target-branch": "target,old",
|
||||
"pull-request": "https://localhost/whatever/pulls/1",
|
||||
"enable-err-notification": "true"
|
||||
});
|
||||
|
||||
const args: Args = parser.parse();
|
||||
expect(args.enableErrorNotification).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -139,6 +139,10 @@ describe("github pull request config parser", () => {
|
|||
labels: [],
|
||||
comments: [],
|
||||
});
|
||||
expect(configs.errorNotification).toEqual({
|
||||
enabled: false,
|
||||
message: "The backport to `{{target-branch}}` failed. Check the latest run for more details."
|
||||
});
|
||||
});
|
||||
|
||||
test("override folder", async () => {
|
||||
|
@ -939,4 +943,26 @@ describe("github pull request config parser", () => {
|
|||
comments: ["First comment", "Second comment"],
|
||||
});
|
||||
});
|
||||
|
||||
test("enable error notification message", async () => {
|
||||
const args: Args = {
|
||||
dryRun: false,
|
||||
auth: "",
|
||||
pullRequest: mergedPRUrl,
|
||||
targetBranch: "prod",
|
||||
enableErrorNotification: true,
|
||||
};
|
||||
|
||||
const configs: Configs = await configParser.parseAndValidate(args);
|
||||
|
||||
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
|
||||
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, undefined);
|
||||
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
|
||||
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
|
||||
|
||||
expect(configs.errorNotification).toEqual({
|
||||
"enabled": true,
|
||||
"message": "The backport to `{{target-branch}}` failed. Check the latest run for more details."
|
||||
});
|
||||
});
|
||||
});
|
|
@ -144,6 +144,10 @@ describe("gitlab merge request config parser", () => {
|
|||
labels: [],
|
||||
comments: [],
|
||||
});
|
||||
expect(configs.errorNotification).toEqual({
|
||||
"enabled": false,
|
||||
"message": "The backport to `{{target-branch}}` failed. Check the latest run for more details."
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
@ -882,4 +886,26 @@ describe("gitlab merge request config parser", () => {
|
|||
comments: ["First comment", "Second comment"],
|
||||
});
|
||||
});
|
||||
|
||||
test("enable error notification message", async () => {
|
||||
const args: Args = {
|
||||
dryRun: false,
|
||||
auth: "",
|
||||
pullRequest: mergedPRUrl,
|
||||
targetBranch: "prod",
|
||||
enableErrorNotification: true,
|
||||
};
|
||||
|
||||
const configs: Configs = await configParser.parseAndValidate(args);
|
||||
|
||||
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
|
||||
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, undefined);
|
||||
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
|
||||
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
|
||||
|
||||
expect(configs.errorNotification).toEqual({
|
||||
"enabled": true,
|
||||
"message": "The backport to `{{target-branch}}` failed. Check the latest run for more details.",
|
||||
});
|
||||
});
|
||||
});
|
|
@ -30,6 +30,7 @@ const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT = {
|
|||
|
||||
jest.mock("@bp/service/git/git-cli");
|
||||
jest.spyOn(GitHubClient.prototype, "createPullRequest");
|
||||
jest.spyOn(GitHubClient.prototype, "createPullRequestComment");
|
||||
jest.spyOn(GitClientFactory, "getOrCreate");
|
||||
|
||||
let parser: ArgsParser;
|
||||
|
@ -94,6 +95,7 @@ describe("cli runner", () => {
|
|||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
|
||||
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(0);
|
||||
expect(GitHubClient.prototype.createPullRequestComment).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("overriding author", async () => {
|
||||
|
@ -287,6 +289,7 @@ describe("cli runner", () => {
|
|||
}
|
||||
);
|
||||
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
|
||||
expect(GitHubClient.prototype.createPullRequestComment).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("closed and not merged pull request", async () => {
|
||||
|
@ -1156,6 +1159,7 @@ describe("cli runner", () => {
|
|||
comments: [],
|
||||
});
|
||||
expect(GitHubClient.prototype.createPullRequest).toThrowError();
|
||||
expect(GitHubClient.prototype.createPullRequestComment).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("auth using GITHUB_TOKEN takes precedence over GIT_TOKEN env variable", async () => {
|
||||
|
@ -1231,4 +1235,142 @@ describe("cli runner", () => {
|
|||
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "prod");
|
||||
});
|
||||
});
|
||||
|
||||
test("with multiple target branches, one failure and error notification enabled", async () => {
|
||||
jest.spyOn(GitHubClient.prototype, "createPullRequest").mockImplementation((backport: BackportPullRequest) => {
|
||||
throw new Error(`Mocked error: ${backport.base}`);
|
||||
});
|
||||
|
||||
addProcessArgs([
|
||||
"-tb",
|
||||
"v1, v2, v3",
|
||||
"-pr",
|
||||
"https://github.com/owner/reponame/pull/2368",
|
||||
"-f",
|
||||
"/tmp/folder",
|
||||
"--bp-branch-name",
|
||||
"custom-failure-head",
|
||||
"--enable-err-notification",
|
||||
]);
|
||||
|
||||
await expect(() => runner.execute()).rejects.toThrowError("Failure occurred during one of the backports: [Error: Mocked error: v1 ; Error: Mocked error: v2 ; Error: Mocked error: v3]");
|
||||
|
||||
const cwd = "/tmp/folder";
|
||||
|
||||
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
|
||||
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(3);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1");
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2");
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v1");
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v2");
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v3");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(3);
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined, undefined);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined, undefined);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined, undefined);
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(3);
|
||||
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v1");
|
||||
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v2");
|
||||
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v3");
|
||||
|
||||
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3);
|
||||
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
|
||||
owner: "owner",
|
||||
repo: "reponame",
|
||||
head: "custom-failure-head-v1",
|
||||
base: "v1",
|
||||
title: "[v1] PR Title",
|
||||
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
|
||||
reviewers: ["gh-user", "that-s-a-user"],
|
||||
assignees: [],
|
||||
labels: [],
|
||||
comments: [],
|
||||
});
|
||||
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
|
||||
owner: "owner",
|
||||
repo: "reponame",
|
||||
head: "custom-failure-head-v2",
|
||||
base: "v2",
|
||||
title: "[v2] PR Title",
|
||||
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
|
||||
reviewers: ["gh-user", "that-s-a-user"],
|
||||
assignees: [],
|
||||
labels: [],
|
||||
comments: [],
|
||||
});
|
||||
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
|
||||
owner: "owner",
|
||||
repo: "reponame",
|
||||
head: "custom-failure-head-v3",
|
||||
base: "v3",
|
||||
title: "[v3] PR Title",
|
||||
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
|
||||
reviewers: ["gh-user", "that-s-a-user"],
|
||||
assignees: [],
|
||||
labels: [],
|
||||
comments: [],
|
||||
});
|
||||
expect(GitHubClient.prototype.createPullRequest).toThrowError();
|
||||
expect(GitHubClient.prototype.createPullRequestComment).toBeCalledTimes(3);
|
||||
expect(GitHubClient.prototype.createPullRequestComment).toBeCalledWith("https://api.github.com/repos/owner/reponame/pulls/2368", "The backport to `v1` failed. Check the latest run for more details.");
|
||||
expect(GitHubClient.prototype.createPullRequestComment).toBeCalledWith("https://api.github.com/repos/owner/reponame/pulls/2368", "The backport to `v2` failed. Check the latest run for more details.");
|
||||
expect(GitHubClient.prototype.createPullRequestComment).toBeCalledWith("https://api.github.com/repos/owner/reponame/pulls/2368", "The backport to `v3` failed. Check the latest run for more details.");
|
||||
});
|
||||
|
||||
test("with some failures and dry run enabled", async () => {
|
||||
jest.spyOn(GitCLIService.prototype, "cherryPick").mockImplementation((cwd: string, sha: string) => {
|
||||
throw new Error(`Forced error: ${sha}`);
|
||||
});
|
||||
|
||||
addProcessArgs([
|
||||
"-tb",
|
||||
"v1, v2, v3",
|
||||
"-pr",
|
||||
"https://github.com/owner/reponame/pull/2368",
|
||||
"-f",
|
||||
"/tmp/folder",
|
||||
"--bp-branch-name",
|
||||
"custom-failure-head",
|
||||
"--enable-err-notification",
|
||||
"--dry-run",
|
||||
]);
|
||||
|
||||
await expect(() => runner.execute()).rejects.toThrowError("Failure occurred during one of the backports: [Error: Forced error: 28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc ; Error: Forced error: 28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc ; Error: Forced error: 28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc]");
|
||||
|
||||
const cwd = "/tmp/folder";
|
||||
|
||||
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
|
||||
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(3);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1");
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2");
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v1");
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v2");
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v3");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(3);
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3);
|
||||
expect(GitCLIService.prototype.cherryPick).toThrowError();
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
|
||||
|
||||
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(0);
|
||||
expect(GitHubClient.prototype.createPullRequestComment).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -44,6 +44,7 @@ jest.mock("axios", () => {
|
|||
|
||||
jest.mock("@bp/service/git/git-cli");
|
||||
jest.spyOn(GitLabClient.prototype, "createPullRequest");
|
||||
jest.spyOn(GitLabClient.prototype, "createPullRequestComment");
|
||||
jest.spyOn(GitClientFactory, "getOrCreate");
|
||||
|
||||
|
||||
|
@ -105,6 +106,7 @@ describe("cli runner", () => {
|
|||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
|
||||
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(0);
|
||||
expect(GitLabClient.prototype.createPullRequestComment).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("dry run with relative folder", async () => {
|
||||
|
@ -199,6 +201,7 @@ describe("cli runner", () => {
|
|||
]);
|
||||
|
||||
await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged");
|
||||
expect(GitLabClient.prototype.createPullRequestComment).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("merged pull request", async () => {
|
||||
|
@ -246,6 +249,7 @@ describe("cli runner", () => {
|
|||
comments: [],
|
||||
}
|
||||
);
|
||||
expect(GitLabClient.prototype.createPullRequestComment).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT = {
|
|||
|
||||
jest.mock("@bp/service/git/git-cli");
|
||||
jest.spyOn(GitHubClient.prototype, "createPullRequest");
|
||||
jest.spyOn(GitHubClient.prototype, "createPullRequestComment");
|
||||
jest.spyOn(GitClientFactory, "getOrCreate");
|
||||
|
||||
let parser: ArgsParser;
|
||||
|
@ -87,6 +88,7 @@ describe("gha runner", () => {
|
|||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
|
||||
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(0);
|
||||
expect(GitHubClient.prototype.createPullRequestComment).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("without dry run", async () => {
|
||||
|
|
|
@ -43,6 +43,7 @@ jest.mock("axios", () => {
|
|||
|
||||
jest.mock("@bp/service/git/git-cli");
|
||||
jest.spyOn(GitLabClient.prototype, "createPullRequest");
|
||||
jest.spyOn(GitLabClient.prototype, "createPullRequestComment");
|
||||
jest.spyOn(GitClientFactory, "getOrCreate");
|
||||
|
||||
let parser: ArgsParser;
|
||||
|
@ -98,6 +99,7 @@ describe("gha runner", () => {
|
|||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
|
||||
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(0);
|
||||
expect(GitLabClient.prototype.createPullRequestComment).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("without dry run", async () => {
|
||||
|
|
19
test/service/runner/runner-util.test.ts
Normal file
19
test/service/runner/runner-util.test.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { injectError, injectTargetBranch } from "@bp/service/runner/runner-util";
|
||||
|
||||
describe("check runner utilities", () => {
|
||||
test("properly inject error message", () => {
|
||||
expect(injectError("Original message: {{error}}", "to inject")).toStrictEqual("Original message: to inject");
|
||||
});
|
||||
|
||||
test("missing error placeholder in the original message", () => {
|
||||
expect(injectError("Original message: {{wrong}}", "to inject")).toStrictEqual("Original message: {{wrong}}");
|
||||
});
|
||||
|
||||
test("properly inject target branch into message", () => {
|
||||
expect(injectTargetBranch("Original message: {{target-branch}}", "to inject")).toStrictEqual("Original message: to inject");
|
||||
});
|
||||
|
||||
test("missing target branch placeholder in the original message", () => {
|
||||
expect(injectTargetBranch("Original message: {{wrong}}", "to inject")).toStrictEqual("Original message: {{wrong}}");
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue