Compare commits

..

91 commits
v2.2.1 ... main

Author SHA1 Message Date
github-actions[bot]
7ff4fce545
chore: release v4.8.5 (#156)
Some checks failed
main ci / ubuntu-latest - node 16 (push) Has been cancelled
main ci / ubuntu-latest - node 18 (push) Has been cancelled
main ci / ubuntu-latest - node 20 (push) Has been cancelled
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2025-04-15 10:57:49 +02:00
Andrea Lamparelli
c9a7375bf9
build(deps): upgrade release-it to v18 (#153) 2025-04-15 10:42:44 +02:00
Andrea Lamparelli
d74a787035
fix(#151): fix gitlab post comments url (#152)
Some checks are pending
main ci / ubuntu-latest - node 20 (push) Waiting to run
main ci / ubuntu-latest - node 16 (push) Waiting to run
main ci / ubuntu-latest - node 18 (push) Waiting to run
2025-04-14 17:48:09 +02:00
Andrea Lamparelli
3a9d367b48
build(deps): audit fix (#150)
Some checks failed
main ci / ubuntu-latest - node 16 (push) Has been cancelled
main ci / ubuntu-latest - node 18 (push) Has been cancelled
main ci / ubuntu-latest - node 20 (push) Has been cancelled
2025-03-24 20:23:36 +01:00
github-actions[bot]
b9ed3ac959
chore: release v4.8.4 (#147)
Some checks failed
main ci / ubuntu-latest - node 16 (push) Has been cancelled
main ci / ubuntu-latest - node 18 (push) Has been cancelled
main ci / ubuntu-latest - node 20 (push) Has been cancelled
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2024-11-02 13:31:20 +01:00
Andrea Lamparelli
3deee59d4c
fix: abort conflicting cherry-pick before starting new one (#146)
Some checks failed
main ci / ubuntu-latest - node 16 (push) Has been cancelled
main ci / ubuntu-latest - node 18 (push) Has been cancelled
main ci / ubuntu-latest - node 20 (push) Has been cancelled
Signed-off-by: Andrea Lamparelli <a.lamparelli95@gmail.com>
2024-10-28 11:51:36 +01:00
Andrea Lamparelli
2b4b429356
build(deps): audit run (#143)
Some checks failed
main ci / ubuntu-latest - node 16 (push) Has been cancelled
main ci / ubuntu-latest - node 18 (push) Has been cancelled
main ci / ubuntu-latest - node 20 (push) Has been cancelled
Signed-off-by: Andrea Lamparelli <a.lamparelli95@gmail.com>
2024-10-10 11:16:48 +02:00
dependabot[bot]
31eabaf84a
build(deps): bump cookie and express (#142)
Bumps [cookie](https://github.com/jshttp/cookie) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `cookie` from 0.6.0 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1)

Updates `express` from 4.21.0 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.0...4.21.1)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-10 11:07:36 +02:00
github-actions[bot]
a14014e89e
chore: release v4.8.3 (#141)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2024-10-10 10:58:28 +02:00
Ratchanan Srirattanamet
b4d0481c56
fix: auto-no-squash inference for GitLab (#140)
Some checks failed
main ci / ubuntu-latest - node 16 (push) Has been cancelled
main ci / ubuntu-latest - node 18 (push) Has been cancelled
main ci / ubuntu-latest - node 20 (push) Has been cancelled
When a GitLab MR is not squashed, `squash_commit_sha` will be `null`,
not `undefined`. Update `inferSquash()` to account for this.
2024-10-07 14:40:31 +02:00
github-actions[bot]
c3daf80306
chore: release v4.8.2 (#139)
Some checks are pending
main ci / ubuntu-latest - node 16 (push) Waiting to run
main ci / ubuntu-latest - node 18 (push) Waiting to run
main ci / ubuntu-latest - node 20 (push) Waiting to run
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2024-10-07 11:21:34 +02:00
Andrea Lamparelli
6d6592f958
ci: upgrade github actions (#138)
Some checks failed
main ci / ubuntu-latest - node 16 (push) Has been cancelled
main ci / ubuntu-latest - node 18 (push) Has been cancelled
main ci / ubuntu-latest - node 20 (push) Has been cancelled
Signed-off-by: Andrea Lamparelli <a.lamparelli95@gmail.com>
2024-10-03 20:04:40 +02:00
Ratchanan Srirattanamet
e2d73d050c
fix: cherry-pick order on GitLab by reversing commmit list only once (#137)
GitLabClient already handle commit order reversing, so re-handle it in
Runner causes cherry-pick order on GitLab to be wrong.

Remove commit order handling from Runner, and instead handle difference
between GitHub and Codeberg inside GitHubClient.

Now, since the default of --bp-branch-name takes the commit list from
{GitHub,GitLab}Client directly, that means backporting branch name
on Codeberg will also be changed to have commits in the correct order
too (old to new, in line with GitHub and GitLab), which is IMO a nice
bonus.
2024-10-03 19:39:03 +02:00
Ratchanan Srirattanamet
1e8358bb2c
fix: handle Codeberg returning null entry in requested_reviewers (#136)
Codeberg can return null as part of requested_reviewers (presumably
because that user has been deleted). Handle that case, and also for
assignees since we're at it.
2024-10-03 19:21:14 +02:00
dependabot[bot]
fe22142b85
build(deps): bump serve-static and express (#134)
Some checks failed
main ci / ubuntu-latest - node 16 (push) Has been cancelled
main ci / ubuntu-latest - node 18 (push) Has been cancelled
main ci / ubuntu-latest - node 20 (push) Has been cancelled
Bumps [serve-static](https://github.com/expressjs/serve-static) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `serve-static` from 1.15.0 to 1.16.2
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

---
updated-dependencies:
- dependency-name: serve-static
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-17 16:59:26 +02:00
dependabot[bot]
26a4c5dfd2
build(deps): bump axios from 1.6.0 to 1.7.4 (#133)
Bumps [axios](https://github.com/axios/axios) from 1.6.0 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.0...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-27 09:53:44 +02:00
github-actions[bot]
c5d7f0ea56
chore: release v4.8.1 (#132)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2024-07-16 19:27:31 +02:00
Andrea Lamparelli
cb3473d7c9
fix(gh130): apply commits in the correct order on github (#131) 2024-07-16 17:49:23 +02:00
dependabot[bot]
da6431b114
build(deps-dev): bump braces from 3.0.2 to 3.0.3 (#129)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-21 14:05:07 +02:00
github-actions[bot]
c22286f85e
chore: release v4.8.0 (#128)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2024-04-11 08:44:11 +02:00
Andrea Lamparelli
2bb7f73112
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>
2024-04-10 23:01:16 +02:00
Earl Warren
6042bcc40b
feat: auto-detect the value of the no-squash option (#118)
The auto-no-squash option is added to:

* backport all the commits when the pull/merge request has been merged
* backport the squashed commit otherwise

It is equivalent to dynamically adjust the value of the no-squash
option, depending on the context.

The no-squash option is kept for backward compatibility for a single
use case: backporting the merged commit instead of backporting the
commits of the pull/merge request request.

Detecting if a pull/merge request was squashed or not depends on the
underlying forge:

* Forgejo / GitHub: use the API to count the number of parents
* GitLab: if the squash_commit_sha is set, the merge request was
  squashed

If the pull/merge request is open, always backport all the commits it
contains.

Fixes: https://github.com/kiegroup/git-backporting/issues/113

Co-authored-by: Andrea Lamparelli <a.lamparelli95@gmail.com>
2024-04-08 18:51:13 +02:00
github-actions[bot]
fc5dba6703
chore: release v4.7.1 (#122)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2024-04-03 10:32:37 +02:00
Earl Warren
e6f86f8f83
fix: gha input is target-branch-pattern, not target-reg-exp (#120) 2024-04-03 08:44:29 +02:00
github-actions[bot]
0a07bf30c8
chore: release v4.7.0 (#117)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2024-04-02 11:23:43 +02:00
Earl Warren
6d9b9db590
fix: return GitHub no-squash commits in order (#115)
Fixes: https://github.com/kiegroup/git-backporting/issues/114
2024-04-02 11:17:41 +02:00
Earl Warren
fe6be83074
feat: add --cherry-pick-options to add to all cherry-pick run (#116)
Fixes: https://github.com/kiegroup/git-backporting/issues/111
2024-04-02 10:34:48 +02:00
Andrea Lamparelli
53cc505f17
feat(gh75): extract target branched from pr labels (#112) 2024-03-30 19:19:17 +01:00
Earl Warren
b2e2e271b9
docs: explain the strategy equivalent to the cherry-pick defaults (#110) 2024-03-30 11:04:32 +01:00
Andrea Lamparelli
ee7a87f26f
docs: remove migrating section (#109) 2024-03-29 09:39:39 +01:00
dependabot[bot]
b30ba6021a
build(deps-dev): bump express from 4.18.2 to 4.19.2 (#108)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-28 19:51:30 +01:00
github-actions[bot]
ffe625d8b3
chore: release v4.6.0 (#107)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2024-03-25 09:30:30 +01:00
Earl Warren
80a0b554f0
feat: add --git-client to explicitly set the type of forge (#106)
codeberg is running Forgejo and it may not be possible to infer that
from the URL alone. The same is true for GitHub Enterprise Server.
2024-03-23 17:13:14 +01:00
dependabot[bot]
646d8fe41c
build(deps): bump follow-redirects from 1.15.4 to 1.15.6 (#104)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-22 16:06:11 +01:00
Earl Warren
a36b740991
docs: fix typos (#105)
Signed-off-by: Earl Warren <contact@earl-warren.org>
2024-03-22 16:04:20 +01:00
github-actions[bot]
f4a2683189
chore: release v4.5.2 (#103)
* chore: release v4.5.2

* Update CHANGELOG.md

---------

Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
Co-authored-by: Andrea Lamparelli <a.lamparelli95@gmail.com>
2024-03-08 12:51:25 +01:00
Andrea Lamparelli
c8ede8d4e2
build: upgrade to node20 for gha (#102) 2024-03-07 09:35:08 +01:00
github-actions[bot]
bce5dd4f99
chore: release v4.5.1 (#101)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2024-02-23 15:29:56 +01:00
Andrea Lamparelli
c57fca6bd6
fix(gh-96): fix git token parsing (#98) 2024-02-23 15:13:34 +01:00
Andrea Lamparelli
2c5c54654d
build: skip ci when not required (#100) 2024-02-23 11:50:56 +01:00
Andrea Lamparelli
5afc464268
chore: add issue templates and improve pr template (#99) 2024-02-23 11:44:47 +01:00
Ratchanan Srirattanamet
9bcd6e6b55
fix: --auth when --git-user contains space (#95)
Since --git-user is a user-facing name, it's common to include a space
in it. As such, it's not suitable to use as a username in a Git remote
URL.

GitLab documented that it doesn't (yet?) check for username [1], and
from my testing GitHub doesn't seem to care either. So just use an
arbitrary name as a username.

[1] https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html
2024-02-23 10:30:18 +01:00
Andrea Lamparelli
d4dc510af1
build: upgrade release-it (#94) 2024-02-20 14:41:12 +01:00
Ratchanan Srirattanamet
300fa91a8a
fix: --no-squash on single-commit GitLab MR (#93)
Due to off-by-one error in GitLabMapper, --no-squash was not effective
on an MR containing single commit. Fix by using the same condition as
GitHubMapper.
2024-02-20 14:24:08 +01:00
dependabot[bot]
b7df1f80dc
build(deps): bump follow-redirects from 1.15.2 to 1.15.4 (#92)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-11 10:01:35 +01:00
github-actions[bot]
204ebd4376
chore: release v4.5.0 (#89)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2023-12-10 23:18:55 +01:00
Andrea Lamparelli
70da575afc
feat(gh-85): take git tokens from environment (#88) 2023-12-10 22:05:53 +01:00
github-actions[bot]
aac73bf7c5
chore: release v4.4.1 (#87)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2023-12-05 16:25:50 +01:00
Shyim
ed32d2275b
fix: namespace parsing in gitlab (#84)
* fix: namespace parsing in gitlab

* test: add test for nested namespace

---------

Co-authored-by: Andrea Lamparelli <a.lamparelli95@gmail.com>
2023-12-05 16:08:49 +01:00
dependabot[bot]
e7c9b4795b
build(deps): bump axios from 1.4.0 to 1.6.0 (#83)
Bumps [axios](https://github.com/axios/axios) from 1.4.0 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.4.0...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-11 22:33:56 +01:00
dependabot[bot]
2db9eb3ef2
build(deps-dev): bump @babel/traverse from 7.22.5 to 7.23.2 (#82)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.5 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 11:56:17 +02:00
github-actions[bot]
4313be48e7
chore: release v4.4.0 (#81)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2023-08-18 13:24:39 +02:00
Andrea Lamparelli
9f0fbc0b2f
feat: integrate with codeberg (#80) 2023-08-18 13:15:38 +02:00
Andrea Lamparelli
eecbff34b7
chore: update README (#79) 2023-08-18 13:11:20 +02:00
Andrea Lamparelli
5fc72e127b
feat(issue-77): handle multiple target branches (#78)
fix: https://github.com/kiegroup/git-backporting/issues/77

This enhancement allow users to backport the same change to multiple
branches with one single tool invocation
2023-08-03 21:57:11 +02:00
github-actions[bot]
c19a56a9ad
chore: release v4.3.0 (#74)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2023-07-27 17:45:18 +02:00
Andrea Lamparelli
29589a63b5
fix: gha skip whitespace trim on body (#73)
this also added a github workflow example
2023-07-27 17:38:45 +02:00
Andrea Lamparelli
fa43ffc1dc
fix: preserve new lines in body and comments (#72) 2023-07-27 15:35:23 +02:00
Andrea Lamparelli
a402fa4525
Merge pull request #71 from lampajr/issue-70_post_comments
feat(issue-70): additional pr comments
2023-07-27 14:26:12 +02:00
Andrea Lamparelli
bed7e29ddc feat(issue-70): additional pr comments 2023-07-27 12:36:42 +02:00
Andrea Lamparelli
10a46551ee refactor: move backport data generation to configs parser 2023-07-27 11:26:39 +02:00
Andrea Lamparelli
e13d1fbf00 docs: update readme 2023-07-20 15:45:17 +02:00
Andrea Lamparelli
9766bdb67b
build(CVE-2023-37466): update release-it to 16.1.3 (#68)
Fix CVE-2023-37466: vm2 Sandbox Escape vulnerability
2023-07-20 15:19:11 +02:00
Andrea Lamparelli
cee5e32d0e
ci: automated pull request test coverage report (#66) 2023-07-20 10:21:22 +02:00
Andrea Lamparelli
a8db0755a8
refactor: updated logging messages (#65) 2023-07-20 10:05:07 +02:00
github-actions[bot]
e29dae5073
chore: release v4.2.0 (#64)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2023-07-13 10:10:47 +02:00
Andrea Lamparelli
265955dda7
feat(issue-62): make cherry-pick strategy configurable (#63)
fix https://github.com/kiegroup/git-backporting/issues/62
2023-07-12 13:50:59 +02:00
Andrea Lamparelli
ead1322c0f
fix(issue-57): truncate the bp branch name (#58)
fix: https://github.com/kiegroup/git-backporting/issues/57

This pr will ensure that if the provided/generated backport branch name
exceede the maximum branch name length set for git, which is 250 chars,
it truncates that name to 250 chars exactly.

In order to include as much commits as possible the branch name will
contain by default the shortened version of all commits
2023-07-11 22:46:21 +02:00
Andrea Lamparelli
91782505ce
test: fix process.argv reset during cli tests (#60) 2023-07-11 22:43:22 +02:00
Shubh Bapna
49a7350406
perf: use concurrent promises instead of awaiting them one by one (#59) 2023-07-11 22:15:52 +02:00
github-actions[bot]
8c010b43e4
chore: release v4.1.0 (#56)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2023-07-11 14:59:14 +02:00
Andrea Lamparelli
c4dbb26c1d
feat(issue-54): backport pr commits without squash (#55)
* feat(issue-54): backport pr commits without squash

fix https://github.com/kiegroup/git-backporting/issues/54

* feat(issue-54): fixed readme
2023-07-11 11:22:01 +02:00
Andrea Lamparelli
a737aa7c4c
fix(issue-52): use pull request github api url as source (#53)
fix https://github.com/kiegroup/git-backporting/issues/52
2023-07-10 15:18:51 +02:00
Andrea Lamparelli
fcc01673f4
feat(issue-41): set and inherit labels (#48)
fix https://github.com/kiegroup/git-backporting/issues/41
2023-07-10 08:49:11 +02:00
Andrea Lamparelli
f923f7f4c2 test: fix gitlab data 2023-07-09 21:31:30 +02:00
Andrea Lamparelli
573a83f114
chore: set public access in publishConfig (#47) 2023-07-06 17:22:35 +02:00
Andrea Lamparelli
15b6dd63ec
refactor: transfer to kiegroup organization (#46)
* refactor: moved to @kiegroup organization

Starting from v4 the package/project has been moved under @kiegroup
organization.

* chore: release v4.0.0

* refactor: bper renamed to git-backporting
2023-07-06 14:19:04 +02:00
github-actions[bot]
1343ba5ec0
chore: release v3.1.1 (#45)
Co-authored-by: Create or Update Pull Request Action <create-or-update-pull-request@users.noreply.github.com>
2023-07-06 11:21:04 +02:00
Andrea Lamparelli
841ddb9125 build: automate release preparation 2023-07-06 11:15:38 +02:00
Andrea Lamparelli
729b380b05 build: fix release workflow 2023-07-06 00:20:50 +02:00
Andrea Lamparelli
7dd2f7270c
chore: release v3.1.0 (#43) 2023-07-05 23:49:25 +02:00
Andrea Lamparelli
23e552c38c build: package release automation 2023-07-05 23:45:38 +02:00
Andrea Lamparelli
5ead31f606
feat: config file as option (#42)
Fix https://github.com/lampajr/backporting/issues/37

This enhancement required a huge refactoring where all arguments
defaults have been centralized in one single place ArgsParser#parse
2023-07-05 22:11:23 +02:00
Andrea Lamparelli
a88adeec35 docs: updated readme 2023-07-03 18:15:59 +02:00
Andrea Lamparelli
06518b7971 build: moved @octokit/webhooks-types to dev dependencies 2023-07-02 19:29:52 +02:00
Andrea Lamparelli
85d1d38237 chore: release v3.0.0 2023-07-02 00:42:39 +02:00
Andrea Lamparelli
c3a09d6514
build: apply auit fix (#40) 2023-07-02 00:12:03 +02:00
Andrea Lamparelli
107f5e52d6
feat: integrate tool with gitlab service (#39)
* feat: integrate tool with gitlab service

Fix https://github.com/lampajr/backporting/issues/30
2023-07-02 00:05:17 +02:00
Andrea Lamparelli
8a007941d1 docs: removed badge 2023-06-28 12:04:21 +02:00
Andrea Lamparelli
2a411ed5a0 docs: fix readme 2023-06-28 12:00:46 +02:00
Andrea Lamparelli
6869becb3e fix: removed 'powered by..' pr body suffix 2023-06-28 11:56:36 +02:00
75 changed files with 37165 additions and 8370 deletions

23
.github/ISSUE_TEMPLATE/bug.md vendored Normal file
View file

@ -0,0 +1,23 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Do this '...'
2. Do that '....'
3. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional context**
Add any other context about the problem here.

8
.github/ISSUE_TEMPLATE/chore.md vendored Normal file
View file

@ -0,0 +1,8 @@
---
name: Chore issue
about: General purpose issues related to chores, project management, etc.
title: ''
labels: 'chore'
assignees: ''
---

20
.github/ISSUE_TEMPLATE/feature.md vendored Normal file
View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'feature'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,20 +1,58 @@
**Thank you for submitting this pull request**
fix _(please add the issue ID if it exists)_
fixes _(please add the issue ID if it exists)_
### Referenced pull requests
## Description
<!--- Describe your changes in detail -->
<!-- Add URLs of all referenced pull requests if they exist. This is only required when making
changes that span multiple kiegroup repositories and depend on each other. -->
<!-- Example:
- https://github.com/kiegroup/droolsjbpm-build-bootstrap/pull/1234
- https://github.com/kiegroup/drools/pull/3000
- https://github.com/kiegroup/optaplanner/pull/899
- etc.
-->
## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
### Checklist
- [ ] Tests added if applicable.
- [ ] Documentation updated if applicable.
### Merge criteria:
<!--- This PR will be merged by any repository approver when it meets all the points in the checklist -->
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [ ] The commits and have meaningful messages; the author will squash them _after approval_ or will ask to merge with squash.
- [ ] Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious).
- [ ] The developer has manually tested the changes and verified that the changes work
> **Note:** `dist/cli/index.js` and `dist/gha/index.js` are automatically generated by git hooks and gh workflows.
<details>
<summary>
First time here?
</summary>
This project follows [git conventional commits](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13) pattern, therefore the commits should have the following format:
```
<type>(<optional scope>): <subject>
empty separator line
<optional body>
empty separator line
<optional footer>
```
Where the type must be one of `[build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test]`
> **NOTE**: if you are still in a `work in progress` branch and you want to push your changes remotely, consider adding `--no-verify` for both `commit` and `push`, e.g., `git push origin <feat-branch> --no-verify` - this could become useful to push changes where there are still tests failures. Once the pull request is ready, please `amend` the commit and force-push it to keep following the adopted git commit standard.
</details>
<details>
<summary>
How to prepare for a new release?
</summary>
There is no need to manually update `package.json` version and `CHANGELOG.md` information. This process has been automated in [Prepare Release](./workflows/prepare-release.yml) *Github* workflow.
Therefore whenever enough changes are merged into the `main` branch, one of the maintainers will trigger this workflow that will automatically update `version` and `changelog` based on the commits on the git tree.
More details can be found in [package release](https://github.com/kiegroup/git-backporting/blob/main/README.md#package-release) section of the README.
</details>

View file

@ -13,15 +13,15 @@ jobs:
name: ${{ matrix.os }} - node ${{ matrix.node-version }}
strategy:
matrix:
node-version: [16, 18]
node-version: [16, 18, 20]
os: [ubuntu-latest]
fail-fast: true
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci

22
.github/workflows/coverage.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: 'coverage report'
on:
pull_request_target:
branches:
- main
paths-ignore:
- 'LICENSE*'
- '**.gitignore'
- '**.md'
- '**.txt'
- '.github/ISSUE_TEMPLATE/**'
- '.github/dependabot.yml'
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ArtiomTr/jest-coverage-report-action@v2
with:
test-script: npm test

50
.github/workflows/prepare-release.yml vendored Normal file
View file

@ -0,0 +1,50 @@
# this workflow prepare the project for the next release, it will update changelog and version based on the previous commits.
# after that it will open a pull request for this change
name: "prepare release"
on:
workflow_dispatch:
inputs:
options:
description: 'Additional release-it options'
required: false
default: ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
jobs:
prepare-release:
name: Prepare release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 18
- name: Git config
run: |
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
- name: Npm config
run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
- run: npm ci
- name: Compute next version
run: |
next_version=$(npx release-it --release-version --no-git.requireCleanWorkingDir)
echo "NEXT_VERSION=${next_version}" >> $GITHUB_ENV
- name: Prepare the release changes
run: npm run release:prepare -- --ci --no-git.commit ${{ github.event.inputs.options }}
- name: Create Pull Request
uses: gr2m/create-or-update-pull-request-action@v1.x
with:
title: "chore: release v${{ env.NEXT_VERSION }}"
body: >
Creating changes for the next release.
branch: release/v${{ env.NEXT_VERSION }}
commit-message: "chore: release v${{ env.NEXT_VERSION }}"
reviewers: lampajr

View file

@ -3,22 +3,30 @@
name: "pull request check"
on: pull_request
on:
pull_request:
paths-ignore:
- 'LICENSE*'
- '**.gitignore'
- '**.md'
- '**.txt'
- '.github/ISSUE_TEMPLATE/**'
- '.github/dependabot.yml'
jobs:
build:
name: ${{ matrix.os }} - node ${{ matrix.node-version }}
strategy:
matrix:
node-version: [16, 18]
node-version: [16, 18, 20]
os: [ubuntu-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci

37
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,37 @@
name: "release package"
on:
workflow_dispatch:
inputs:
options:
description: 'Additional release-it options'
required: false
default: ''
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
jobs:
release:
name: Release package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 18
- name: Git config
run: |
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
- name: Npm config
run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
- run: npm ci
# the version/changelog must be already updated on main branch using "npm run release:prepare"
# or check prepare-release.yml workflow
- name: New version release
run: npm run release -- --ci --no-increment --no-git.commit ${{ github.event.inputs.options }}

7
.gitignore vendored
View file

@ -6,7 +6,12 @@ test/**/_temp/**/*
yarn.lock
coverage/
test-report.xml
report.json
.idea/
.vscode/
build/
# dist/
.npmrc
# temporary files created during tests
*test*.json
bp

View file

@ -1,37 +1,212 @@
# Changelog
## [2.2.1](https://github.com/lampajr/backporting/compare/v2.2.0...v2.2.1) (2023-06-27)
## <small>4.8.5 (2025-04-15)</small>
* build(deps): audit fix (#150) ([3a9d367](https://github.com/kiegroup/git-backporting/commit/3a9d367)), closes [#150](https://github.com/kiegroup/git-backporting/issues/150)
* build(deps): upgrade release-it to v18 (#153) ([c9a7375](https://github.com/kiegroup/git-backporting/commit/c9a7375)), closes [#153](https://github.com/kiegroup/git-backporting/issues/153)
* fix(#151): fix gitlab post comments url (#152) ([d74a787](https://github.com/kiegroup/git-backporting/commit/d74a787)), closes [#152](https://github.com/kiegroup/git-backporting/issues/152)
## [4.8.4](https://github.com/kiegroup/git-backporting/compare/v4.8.3...v4.8.4) (2024-11-02)
### Bug Fixes
* fix gha no-inherit-reviewers input ([8b586cc](https://github.com/lampajr/backporting/commit/8b586ccdfe0e6b90ed41ea8a5eecdbc24893fe25))
* abort conflicting cherry-pick before starting new one ([#146](https://github.com/kiegroup/git-backporting/issues/146)) ([3deee59](https://github.com/kiegroup/git-backporting/commit/3deee59d4c3b726ae131b5974af4618dd5e7d1d2))
## [2.2.0](https://github.com/lampajr/backporting/compare/v2.1.0...v2.2.0) (2023-06-22)
### Features
* override backporting pr fields ([#38](https://github.com/lampajr/backporting/issues/38)) ([a32e8cd](https://github.com/lampajr/backporting/commit/a32e8cd34c757358668fe8f88f6d1733d3fa8391))
## [2.1.0](https://github.com/lampajr/backporting/compare/v2.0.1...v2.1.0) (2023-06-20)
### Features
* **issue-17:** override backporting pr data ([#29](https://github.com/lampajr/backporting/issues/29)) ([941beda](https://github.com/lampajr/backporting/commit/941beda208e4a8c1577bd4d39299fbbfbf569c06))
## [4.8.3](https://github.com/kiegroup/git-backporting/compare/v4.8.2...v4.8.3) (2024-10-10)
### Bug Fixes
* gha input parser ([95b35aa](https://github.com/lampajr/backporting/commit/95b35aa4efb86e2bc4990d920feec1ec5c4eb8e4))
* auto-no-squash inference for GitLab ([#140](https://github.com/kiegroup/git-backporting/issues/140)) ([b4d0481](https://github.com/kiegroup/git-backporting/commit/b4d0481c5649115367f1ae0724d12d55b6b86e10))
## [2.0.1](https://github.com/lampajr/backporting/compare/v2.0.0...v2.0.1) (2023-01-05)
## [4.8.2](https://github.com/kiegroup/git-backporting/compare/v4.8.1...v4.8.2) (2024-10-07)
## [2.0.0](https://github.com/lampajr/backporting/compare/v1.0.0...v2.0.0) (2023-01-05)
### Bug Fixes
* cherry-pick order on GitLab by reversing commmit list only once ([#137](https://github.com/kiegroup/git-backporting/issues/137)) ([e2d73d0](https://github.com/kiegroup/git-backporting/commit/e2d73d050c8387c0858877ac3c56c565bacaf4f9))
* handle Codeberg returning null entry in requested_reviewers ([#136](https://github.com/kiegroup/git-backporting/issues/136)) ([1e8358b](https://github.com/kiegroup/git-backporting/commit/1e8358bb2c461c56cf86e82bec4d71284866b13b))
## [4.8.1](https://github.com/kiegroup/git-backporting/compare/v4.8.0...v4.8.1) (2024-07-16)
### Bug Fixes
* **gh130:** apply commits in the correct order on github ([#131](https://github.com/kiegroup/git-backporting/issues/131)) ([cb3473d](https://github.com/kiegroup/git-backporting/commit/cb3473d7c9de66cb7bec188f08538e134cdc4bc0))
## [4.8.0](https://github.com/kiegroup/git-backporting/compare/v4.7.1...v4.8.0) (2024-04-11)
### Features
* backport still open pull requests ([b3936e0](https://github.com/lampajr/backporting/commit/b3936e019a19976281c5e2582904264e974b8b42))
* pull request backporting ([b3936e0](https://github.com/lampajr/backporting/commit/b3936e019a19976281c5e2582904264e974b8b42))
* auto-detect the value of the no-squash option ([#118](https://github.com/kiegroup/git-backporting/issues/118)) ([6042bcc](https://github.com/kiegroup/git-backporting/commit/6042bcc40ba83593a23dfe4d92cf50655a05b1cd))
* implement error notification as pr comment ([#124](https://github.com/kiegroup/git-backporting/issues/124)) ([2bb7f73](https://github.com/kiegroup/git-backporting/commit/2bb7f731127e099d1f196e6785e992589f7c4940))
## [4.7.1](https://github.com/kiegroup/git-backporting/compare/v4.7.0...v4.7.1) (2024-04-03)
### Bug Fixes
* gha input is target-branch-pattern, not target-reg-exp ([#120](https://github.com/kiegroup/git-backporting/issues/120)) ([e6f86f8](https://github.com/kiegroup/git-backporting/commit/e6f86f8f839bc86adf36fa0d3c8dcad6cab2f92e))
## [4.7.0](https://github.com/kiegroup/git-backporting/compare/v4.6.0...v4.7.0) (2024-04-02)
### Features
* add --cherry-pick-options to add to all cherry-pick run ([#116](https://github.com/kiegroup/git-backporting/issues/116)) ([fe6be83](https://github.com/kiegroup/git-backporting/commit/fe6be83074476d91c1b038fd7f03c4868e96f113))
* **gh75:** extract target branched from pr labels ([#112](https://github.com/kiegroup/git-backporting/issues/112)) ([53cc505](https://github.com/kiegroup/git-backporting/commit/53cc505f17630fb30daa70f75895323325cc0c7d))
### Bug Fixes
* return GitHub no-squash commits in order ([#115](https://github.com/kiegroup/git-backporting/issues/115)) ([6d9b9db](https://github.com/kiegroup/git-backporting/commit/6d9b9db590f9713e2b056bcc8e20fc3f3c70618b))
## [4.6.0](https://github.com/kiegroup/git-backporting/compare/v4.5.2...v4.6.0) (2024-03-25)
### Features
* add --git-client to explicitly set the type of forge ([#106](https://github.com/kiegroup/git-backporting/issues/106)) ([80a0b55](https://github.com/kiegroup/git-backporting/commit/80a0b554f0c1920a178e28bd678f709581a1b224))
## [4.5.2](https://github.com/kiegroup/git-backporting/compare/v4.5.1...v4.5.2) (2024-03-08)
### Improvements
* upgrade to node20 for gha ([c8ede8d](https://github.com/kiegroup/git-backporting/commit/c8ede8d4e2428cb3f4dc2d727f24b37e5781cbb1))
## [4.5.1](https://github.com/kiegroup/git-backporting/compare/v4.5.0...v4.5.1) (2024-02-23)
### Bug Fixes
* --auth when --git-user contains space ([#95](https://github.com/kiegroup/git-backporting/issues/95)) ([9bcd6e6](https://github.com/kiegroup/git-backporting/commit/9bcd6e6b5547974c45ade756b623eb385bb76019))
* --no-squash on single-commit GitLab MR ([#93](https://github.com/kiegroup/git-backporting/issues/93)) ([300fa91](https://github.com/kiegroup/git-backporting/commit/300fa91a8ae065b7f6f6370882b10929bbde6309))
* **gh-96:** fix git token parsing ([#98](https://github.com/kiegroup/git-backporting/issues/98)) ([c57fca6](https://github.com/kiegroup/git-backporting/commit/c57fca6bd6b9c241c11ad1f728cc9bc0acfd7f88))
## [4.5.0](https://github.com/kiegroup/git-backporting/compare/v4.4.1...v4.5.0) (2023-12-10)
### Features
* **gh-85:** take git tokens from environment ([#88](https://github.com/kiegroup/git-backporting/issues/88)) ([70da575](https://github.com/kiegroup/git-backporting/commit/70da575afce603190eafed927637922a37fbd087))
## [4.4.1](https://github.com/kiegroup/git-backporting/compare/v4.4.0...v4.4.1) (2023-12-05)
### Bug Fixes
* namespace parsing in gitlab ([#84](https://github.com/kiegroup/git-backporting/issues/84)) ([ed32d22](https://github.com/kiegroup/git-backporting/commit/ed32d2275b6008d31e456c41beecd536eceb23dc))
## [4.4.0](https://github.com/kiegroup/git-backporting/compare/v4.3.0...v4.4.0) (2023-08-18)
### Features
* integrate with codeberg ([#80](https://github.com/kiegroup/git-backporting/issues/80)) ([9f0fbc0](https://github.com/kiegroup/git-backporting/commit/9f0fbc0b2fd8d449207660323be87f6d2fa8c017))
* **issue-77:** handle multiple target branches ([#78](https://github.com/kiegroup/git-backporting/issues/78)) ([5fc72e1](https://github.com/kiegroup/git-backporting/commit/5fc72e127bedb3177f4e17ff1182827c78154ef1))
## [4.3.0](https://github.com/kiegroup/git-backporting/compare/v4.2.0...v4.3.0) (2023-07-27)
### Features
* **issue-70:** additional pr comments ([bed7e29](https://github.com/kiegroup/git-backporting/commit/bed7e29ddc1ba5498faa2c7cc33ec3b127947dcf))
### Bug Fixes
* gha skip whitespace trim on body ([#73](https://github.com/kiegroup/git-backporting/issues/73)) ([29589a6](https://github.com/kiegroup/git-backporting/commit/29589a63b503b30820a13a442de533239dec06f4))
* preserve new lines in body and comments ([#72](https://github.com/kiegroup/git-backporting/issues/72)) ([fa43ffc](https://github.com/kiegroup/git-backporting/commit/fa43ffc1dc5572a06309c28e93ee6ab5fba14780))
## [4.2.0](https://github.com/kiegroup/git-backporting/compare/v4.1.0...v4.2.0) (2023-07-13)
### Features
* **issue-62:** make cherry-pick strategy configurable ([#63](https://github.com/kiegroup/git-backporting/issues/63)) ([265955d](https://github.com/kiegroup/git-backporting/commit/265955dda77a8191fd1f64517fec20e8d5f8c5b4))
### Bug Fixes
* **issue-57:** truncate the bp branch name ([#58](https://github.com/kiegroup/git-backporting/issues/58)) ([ead1322](https://github.com/kiegroup/git-backporting/commit/ead1322c0f6bb5de96c487e8f6b6565734144853))
### Performance Improvements
* use concurrent promises instead of awaiting them one by one ([#59](https://github.com/kiegroup/git-backporting/issues/59)) ([49a7350](https://github.com/kiegroup/git-backporting/commit/49a73504066396ca2a074f55bb23815e13ae462e))
## [4.1.0](https://github.com/kiegroup/git-backporting/compare/v4.0.0...v4.1.0) (2023-07-11)
### Features
* **issue-41:** set and inherit labels ([#48](https://github.com/kiegroup/git-backporting/issues/48)) ([fcc0167](https://github.com/kiegroup/git-backporting/commit/fcc01673f4bc9aa2786faf6dfd503a29e5ca0cd9))
* **issue-54:** backport pr commits without squash ([#55](https://github.com/kiegroup/git-backporting/issues/55)) ([c4dbb26](https://github.com/kiegroup/git-backporting/commit/c4dbb26c1d9d266ed86f3f0d6016b8cff7743f8b))
### Bug Fixes
* **issue-52:** use pull request github api url as source ([#53](https://github.com/kiegroup/git-backporting/issues/53)) ([a737aa7](https://github.com/kiegroup/git-backporting/commit/a737aa7c4c66983de358b8472121ab918de922e3))
## [4.0.0](https://github.com/kiegroup/git-backporting/compare/v3.0.0...v4.0.0) (2023-07-06)
Project moved under @kiegroup organization.
## [3.1.1](https://github.com/kiegroup/git-backporting/compare/v3.1.0...v3.1.1) (2023-07-06)
## [3.1.0](https://github.com/kiegroup/git-backporting/compare/v3.0.0...v3.1.0) (2023-07-05)
### Features
* config file as option ([#42](https://github.com/kiegroup/git-backporting/issues/42)) ([5ead31f](https://github.com/kiegroup/git-backporting/commit/5ead31f606b585ecdf7ed2e9de8ebd841b935898))
## [3.0.0](https://github.com/kiegroup/git-backporting/compare/v2.2.1...v3.0.0) (2023-07-01)
### Features
* integrate tool with gitlab service ([#39](https://github.com/kiegroup/git-backporting/issues/39)) ([107f5e5](https://github.com/kiegroup/git-backporting/commit/107f5e52d663157145aa14f6cf7fa4d6704cb844))
### Bug Fixes
* removed 'powered by..' pr body suffix ([6869bec](https://github.com/kiegroup/git-backporting/commit/6869becb3e5979b24f6fe29bf38141e15c1bdc66))
## [2.2.1](https://github.com/kiegroup/git-backporting/compare/v2.2.0...v2.2.1) (2023-06-27)
### Bug Fixes
* fix gha no-inherit-reviewers input ([8b586cc](https://github.com/kiegroup/git-backporting/commit/8b586ccdfe0e6b90ed41ea8a5eecdbc24893fe25))
## [2.2.0](https://github.com/kiegroup/git-backporting/compare/v2.1.0...v2.2.0) (2023-06-22)
### Features
* override backporting pr fields ([#38](https://github.com/kiegroup/git-backporting/issues/38)) ([a32e8cd](https://github.com/kiegroup/git-backporting/commit/a32e8cd34c757358668fe8f88f6d1733d3fa8391))
## [2.1.0](https://github.com/kiegroup/git-backporting/compare/v2.0.1...v2.1.0) (2023-06-20)
### Features
* **issue-17:** override backporting pr data ([#29](https://github.com/kiegroup/git-backporting/issues/29)) ([941beda](https://github.com/kiegroup/git-backporting/commit/941beda208e4a8c1577bd4d39299fbbfbf569c06))
### Bug Fixes
* gha input parser ([95b35aa](https://github.com/kiegroup/git-backporting/commit/95b35aa4efb86e2bc4990d920feec1ec5c4eb8e4))
## [2.0.1](https://github.com/kiegroup/git-backporting/compare/v2.0.0...v2.0.1) (2023-01-05)
## [2.0.0](https://github.com/kiegroup/git-backporting/compare/v1.0.0...v2.0.0) (2023-01-05)
### Features
* backport still open pull requests ([b3936e0](https://github.com/kiegroup/git-backporting/commit/b3936e019a19976281c5e2582904264e974b8b42))
* pull request backporting ([b3936e0](https://github.com/kiegroup/git-backporting/commit/b3936e019a19976281c5e2582904264e974b8b42))

228
README.md
View file

@ -1,62 +1,121 @@
<h1 align="center">
BPER: Git Backporter </br>
Git Backporting </br>
:outbox_tray: :inbox_tray:
</h1>
<p align="center">
<a href="https://github.com/lampajr/backporting">
<img alt="ci checks status" src="https://github.com/lampajr/backporting/actions/workflows/ci.yml/badge.svg">
<a href="https://github.com/kiegroup/git-backporting">
<img alt="ci checks status" src="https://github.com/kiegroup/git-backporting/actions/workflows/ci.yml/badge.svg">
</a>
<a href="https://badge.fury.io/js/@lampajr%2Fbper">
<img alt="npm version" src="https://badge.fury.io/js/@lampajr%2Fbper.svg">
</a>
<a href="https://snyk.io/test/github/lampajr/backporting?targetFile=package.json">
<img alt="vulnerabilities" src="https://snyk.io/test/github/lampajr/backporting/badge.svg?targetFile=package.json">
<a href="https://badge.fury.io/js/@kie%2Fgit-backporting">
<img alt="npm version" src="https://badge.fury.io/js/@kie%2Fgit-backporting.svg">
</a>
</p>
---
**BPer** is a [NodeJS](https://nodejs.org/) command line tool that provides capabilities to *backport* [1] pull requests in an automated way. This tool also comes with a predefined GitHub action in order to make CI/CD integration easier for all users.
**Git Backporting** is a [NodeJS](https://nodejs.org/) command line tool that provides capabilities to *backport* pull requests (on *GitHub*) and merge requests (on *GitLab*) in an automated way. This tool also comes with a predefined *GitHub* action in order to make CI/CD integration easier for all users.
[1] *backporting* is an action aiming to move a change (usually a commit) from a branch (usually the main one) to another one, which is generally referring to a still maintained release branch. Keeping it simple: it is about to move a specific change or a set of them from one branch to another.
Table of content
----------------
* **[Usage](#usage)**
* **[GitHub Action](#github-action)**
* **[Limitations](#limitations)**
* **[Contributions](#contributing)**
* **[Who is this tool for](#who-is-this-tool-for)**
* **[CLI tool](#cli-tool)**
* **[GitHub action](#github-action)**
* **[Future works](#future-works)**
* **[Development](#development)**
* **[Contributing](#contributing)**
* **[License](#license)**
## Who is this tool for?
## Usage
`git-backporting` is a fully configurable tool that provides capabilities to *backport* pull requests (on *GitHub*) and merge requests (on *GitLab*) in an automated way.
> *What is backporting?* - backporting is an action aiming to move a change (usually a commit) from a branch (usually the main one) to another one, which is generally referring to a still maintained release branch. Keeping it simple: it is about to move a specific change or a set of them from one branch to another.
Therefore this tools is for anybody who is working on projects where they have to maintain multiple active branches/versions at the same time. If you are actively cherry-picking many changes from your main branch to other ones, and you mainly do changes through pull requests or merge requests, maybe this tool may be right for you.
## CLI tool
> All instructions provided below pertain to version `v4` of the tool. If you wish to use an earlier version, please refer to the documentation from the corresponding tag/release.
This tool is released on the [public npm registry](https://www.npmjs.com/), therefore it can be easily installed using `npm`:
```bash
$ npm install -g @lampajr/bper
$ npm install -g @kie/git-backporting
```
Then it can be used as any other command line tool:
Then you just have to choose the pull request (or merge request on *Gitlab*) that you would like to backport and the target branch and then simply run the following command:
```bash
$ bper -tb <branch> -pr <pull-request-url> -a <github-token> [-f <your-folder>]
$ git-backporting -tb <branch> -pr <pull-request-url> -a <git-token>
```
A real example could be the following one:
```bash
$ git-backporting -tb develop -pr https://github.com/kiegroup/git-backporting-example/pull/47 -a *****
```
This is the easiest invocation where you let the tool set / compute most of the backported pull request data. Obviously most of that data can be overridden with appropriate tool options, more details can be found in the [inputs](#inputs) section.
### Requirements
* Node 16 or higher, more details on Node can be found [here](https://nodejs.org/en).
* Git, see [how to install](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) if you need help.
### How it works?
It works in this way: given the provided `pull/merge request` it infers the server API to use (either *Github* or *Gitlab* for now) and retrieves the corresponding pull request object (original pull/merge request to be backported into another branch).
After that it clones the corresponding git repository, check out in the provided `target branch` and create a new branch from that (name automatically generated if not provided as option).
By default the tool will try to cherry-pick the single squashed/merged commit into the newly created branch. The `--no-squash` and `--auto-no-squash` options control this behavior according the following table.
| No squash | Auto no squash |Behavior|
|---|---|---|
| unset/false | unset/false | cherry-pick a single commit, squashed or merged |
| set/true | unset/false | cherry-pick all commits found in the the original pull/merge request|
| (ignored) | set/true | cherry-pick all commits if the original pull/merge request was merged, a single commit if it was squashed |
Based on the original pull request, creates a new one containing the backporting to the target branch. Note that most of these information can be overridden with appropriate CLI options or GHA inputs.
#### cherry-pick strategy
The default cherry-pick strategy is `recursive` with `theirs` option for automatic conflicts resolution. Therefore, by default, all commits are cherry-picked using the following git-equivalent command:
```bash
$ git cherry-pick -m 1 --strategy=recursive --strategy-option=theirs <sha>
```
From version `v4.2.0` both can be configured via the `strategy` or `strategy-option` inputs if using the action and the `--strategy` or `--strategy-option` arguments if using the CLI.
The [default strategy](https://git-scm.com/docs/git-merge#Documentation/git-merge.txt--sltstrategygt) of the `git-cherry-pick` command is different from the defaults of `git-backporting`.
```bash
$ git cherry-pick -m 1 <sha>
```
is the same as:
```bash
$ git cherry-pick -m 1 --strategy=ort --strategy-option=find-renames <sha>
```
If there is a conflict the backport will fail and require manual intervention.
> **NOTE**: If there are any conflicts, the tool will block the process and exit signalling the failure as there are still no ways to interactively resolve them. In these cases a manual cherry-pick is needed, or alternatively users could manually resume the process in the cloned repository (here the user will have to resolve the conflicts, push the branch and create the pull request - all manually).
### Inputs
This toold comes with some inputs that allow users to override the default behavior, here the full list of available inputs:
This tool comes with some inputs that allow users to override the default behavior, here the full list of available inputs:
| **Name** | **Command** | **Required** | **Description** | **Default** |
|---------------|----------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
| 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 | |
| Auth | -a, --auth | N | `GITHUB_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 where the repo will be checked out, e.g., /tmp/folder | {cwd}/bp |
| Target Branches | -tb, --target-branch | N | Comma separated list of branches where the changes must be backported to | |
| Target Branches Pattern | -tbp, --target-branch-pattern | N | Regular expression pattern to extract target branch(es) from pr labels. The branches will be extracted from the pattern's required `target` named capturing group, e.g., `^backport (?<target>([^ ]+))$` | |
| Pull Request | -pr, --pull-request | N | Original pull request url, the one that must be backported, e.g., https://github.com/kiegroup/git-backporting/pull/1 | |
| Configuration File | -cf, --config-file | N | Configuration file, in JSON format, containing all options to be overridded, note that if provided all other CLI options will be ignored | |
| Auth | -a, --auth | N | Git access/authorization token, if provided all token env variables will be ignored. See [auth token](#authorization-token) section for more details | "" |
| Folder | -f, --folder | N | Local folder full name of the repository that will be checked out, e.g., /tmp/folder | {cwd}/bp |
| Git Client | --git-client | N | Git client type <github|gitlab|codeberg>, if not set it is infered from pull-request
| Git User | -gu, --git-user | N | Local git user name | "GitHub" |
| Git Email | -ge, --git-email | N | Local git user email | "noreply@github.com" |
| Title | --title | N | Backporting pull request title | "{original-pr-title}" |
@ -65,16 +124,62 @@ This toold comes with some inputs that allow users to override the default behav
| Reviewers | --reviewers | N | Backporting pull request comma-separated reviewers list | [] |
| Assignees | --assignes | N | Backporting pull request comma-separated assignees list | [] |
| No Reviewers Inheritance | --no-inherit-reviewers | N | Considered only if reviewers is empty, if true keep reviewers as empty list, otherwise inherit from original pull request | false |
| Backport Branch Name | --bp-branch-name | N | Name of the backporting pull request branch | bp-{target-branch}-{sha} |
| Backport Branch Names | --bp-branch-name | N | Comma separated lists of the backporting pull request branch names, if they exceeds 250 chars they will be truncated | bp-{target-branch}-{sha1}...{shaN} |
| Labels | --labels | N | Provide custom labels to be added to the backporting pull request | [] |
| Inherit labels | --inherit-labels | N | If enabled inherit lables from the original pull request | false |
| No squash | --no-squash | N | 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. | |
| Auto no squash | --auto-no-squash | N | If the pull request was merged or is open, backport all commits. If the pull request commits were squashed, backport the squashed commit. | |
| Strategy | --strategy | N | Cherry pick merging strategy, see [git-merge](https://git-scm.com/docs/git-merge#_merge_strategies) doc for all possible values | "recursive" |
| 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).
## GitHub Action
#### Authorization token
This action can be used in any GitHub workflow, below you can find a simple example of manually triggered workflow backporting a specific pull request (provided as input).
Since version `4.5.0` we introduced a new feature that allows user to provide the git access token through environment variables. These env variables are taken into consideration only if the `--auth/-a` is not provided as argument/input.
Here the supported list of env variables:
- `GITHUB_TOKEN`: this is checked only if backporting on Github platform.
- `GITLAB_TOKEN`: this is checked only if backporting on Gitlab platform.
- `CODEBERG_TOKEN`: this is checked only if backporting on Codeberg platform.
- `GIT_TOKEN`: this is considered if none of the previous envs are set.
> **NOTE**: if `--auth` argument is provided, all env variables will be ignored even if not empty.
#### Configuration file example
This is an example of a configuration file that can be used.
```json
{
"pullRequest": "https://gitlab.com/<namespace>/<repo>/-/merge_requests/1",
"targetBranch": "old",
"folder": "/tmp/my-folder",
"title": "Override Title",
"auth": "*****"
}
```
Keep in mind that its structure 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 **Git Backporting** supports the following git management services:
* ***GITHUB***: Introduced since the first release of this tool (version `1.0.0`). The interaction with this system is performed using [*octokit*](https://octokit.github.io/rest.js) client library.
* ***GITLAB***: This has been introduced since version `3.0.0`, it works for both public and private *GitLab* servers. The interaction with this service is performed using plain [*axios*](https://axios-http.com) requests. The *gitlab* api version that is used to make requests is `v4`, at the moment there is no possibility to override it.
* ***CODEBERG***: Introduced since version `4.4.0`, it works for public [codeberg.org](https://codeberg.org/) platform. Thanks to the api compatibility with GitHub, the interaction with this service is performed using using [*octokit*](https://octokit.github.io/rest.js) client library.
> **NOTE**: by default, all gitlab requests are performed setting `rejectUnauthorized=false`, planning to make this configurable too.
## GitHub action
This action can be used in any *GitHub* workflow, below you can find a simple example of manually triggered workflow backporting a specific pull request (provided as input).
```yml
name: Pull Request Backporting using BPer
name: Pull Request Backporting using Git Backporting
on:
workflow_dispatch:
@ -99,7 +204,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Backporting
uses: lampajr/backporting@main
uses: kiegroup/git-backporting@main
with:
target-branch: ${{ inputs.targetBranch }}
pull-request: ${{ inputs.pullRequest }}
@ -112,7 +217,7 @@ You can also use this action with other events - you'll just need to specify `ta
For example, this configuration creates a pull request against branch `v1` once the current one is merged, provided that the label `backport-v1` is applied:
```yaml
name: Pull Request Backporting using BPer
name: Pull Request Backporting using Git Backporting
on:
pull_request_target:
@ -120,6 +225,9 @@ on:
- closed
- labeled
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
backporting:
name: "Backporting"
@ -138,37 +246,69 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Backporting
uses: lampajr/backporting@main
uses: kiegroup/git-backporting@main
with:
target-branch: v1
pull-request: ${{ github.event.pull_request.url }}
auth: ${{ secrets.GITHUB_TOKEN }}
```
For a complete description of all inputs see [Inputs section](#inputs).
## Limitations
## Future works
**BPer** is in development mode, this means that it has many limitations right now. I'll try to summarize the most importan ones:
**Git Backporting** is still in development mode, this means that there are still many future works and extension that can be implemented. I'll try to summarize the most important ones:
- You can backport pull requests only.
- It only works for [GitHub](https://github.com/).
- Integrated in GitHub Actions CI/CD only.
- Provide a way to backport single commit (or a set of them) if no original pull request is present.
- Integrate this tool with other git management services (like Bitbucket) to make it as generic as possible.
- Integrate it into other CI/CD services like gitlab CI.
- Provide some reusable *GitHub* workflows.
Based on these limitations, the next **Future Works** could be the following:
- Provide a way to backport single commit too (or a set of them), even if no original pull request is present.
- Integrate this tool with other git management services (like GitLab and Bitbucket) to make it as generic as possible.
- Provide some reusable GitHub workflows.
## Development
### Package release
The release of this package is entirely based on [release-it](https://github.com/release-it/release-it) tool. I created some useful scripts that can make the release itself quite easy.
#### Automatic release
The first step is to prepare the changes for the next release, this is done by running:
```bash
$ npm run release:prepare:all
```
> NOTE: running locally this requires `npm login`, please consider using `.github/workflows/prepare-release.yml` if you don't have permission on the npm package.
This script performs the following steps:
1. Automatically computes the next version based on the last commits
2. Create a new branch `release/v${computed_version}`
3. Apply all changes, like version and changelog upgrade
4. Commit those changes: `chore: release v${compute_version}`
After that you should just push the new branch and open the pull request.
> NOTE: if you don't want to run this preparation from you local environment, there is already a workflow that does all these steps, including the pull request. See [Prepare release](.github/workflows/prepare-release.yml) workflow.
Once the release preparion pull request got merged, you can run [Release package](.github/workflows/release.yml) workflow that automatically performs the release itself, including npm publishing, git tag and github release.
#### Manual release
In case we would like to perform a manual release, it would be enough to open a pull request changing the following items:
- Package version inside the `package.json`
- Provide exhaustive changelog information inside `CHANGELOG.md`
- Commit like `chore: release v<version>`
Once the release preparion pull request got merged, run [Release package](.github/workflows/release.yml) workflow.
## Contributing
This is an open source project, and you are more than welcome to contribute :heart:!
Every change must be submitted through a GitHub pull request (PR). Backporting uses continuous integration (CI). The CI runs checks against your branch after you submit the PR to ensure that your PR doesnt introduce errors. If the CI identifies a potential problem, our friendly PR maintainers will help you resolve it.
Every change must be submitted through a *GitHub* pull request (PR). Backporting uses continuous integration (CI). The CI runs checks against your branch after you submit the PR to ensure that your PR doesnt introduce errors. If the CI identifies a potential problem, our friendly PR maintainers will help you resolve it.
> **Note**: this project follows [git-conventional-commits](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13) standards, thanks to the [commit-msg hook](./.husky/commit-msg) you are not allowed to use commits that do not follow those standards.
1. Fork it (https://github.com/lampajr/backporting).
1. Fork it (https://github.com/kiegroup/git-backporting).
2. Create your feature branch: (git checkout -b feature).
@ -180,6 +320,8 @@ Every change must be submitted through a GitHub pull request (PR). Backporting u
> **Note**: you don't need to take care about typescript compilation and minifycation, there are automated [git hooks](./.husky) taking care of that!
**Hint**: if you are still in a `work in progress` branch and you want to push your changes remotely, consider adding `--no-verify` for both `commit` and `push`, e.g., `git push origin <feat-branch> --no-verify`
## License
Backporting (BPer) open source project is licensed under the [MIT](./LICENSE) license.
Git backporting open source project is licensed under the [MIT](./LICENSE) license.

View file

@ -1,53 +1,122 @@
name: "Backporting GitHub Action"
description: "GitHub action providing an automated way to backport pull requests from one branch to another"
description: GitHub action providing an automated way to backport pull requests from one branch to another
inputs:
pull-request:
description: >
URL of the pull request to backport, e.g., "https://github.com/kiegroup/git-backporting/pull/1"
required: false
target-branch:
description: >
Comma separated list of branches where the pull request must be backported to
required: false
target-branch-pattern:
description: >
Regular expression pattern to extract target branch(es) from pr labels.
The branches will be extracted from the pattern's required `target` named capturing group,
for instance "^backport (?<target>([^ ]+))$"
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"
description: >
If enabled the tool does not create any pull request nor push anything remotely
required: false
default: "false"
auth:
description: "GITHUB_TOKEN or a `repo` scoped Personal Access Token (PAT)"
description: >
GITHUB_TOKEN or a `repo` scoped Personal Access Token (PAT),
if not provided will look for existing env variables like GITHUB_TOKEN
default: ${{ github.token }}
required: false
git-client:
description: >
Git client type <github|gitlab|codeberg>, if not set it is infered from pull-request
required: false
git-user:
description: "Local git user name"
description: Local git user name
default: "GitHub"
required: false
git-email:
description: "Local git user email"
description: Local git user email
default: "noreply@github.com"
required: false
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
body:
description: "Backporting PR body. Default is the original PR body prefixed by `backport: <original-pr-link>`"
description: >
Backporting PR title. Default is the original PR title prefixed by the target branch
required: false
body-prefix:
description: "Backporting PR body prefix. Default is `backport: <original-pr-link>`"
description: >
Backporting PR body prefix. Default is `Backport: <original-pr-link>`
required: false
body:
description: >
Backporting PR body. Default is the original PR body
required: false
bp-branch-name:
description: "Backporting PR branch name. Default is auto-generated from commit"
description: >
Comma separated list of backporting PR branch names.
Default is auto-generated from commit and target branches
required: false
reviewers:
description: "Comma separated list of reviewers for the backporting pull request"
description: >
Comma separated list of reviewers for the backporting pull request
required: false
assignees:
description: "Comma separated list of reviewers for the backporting pull request"
description: >
Comma separated list of reviewers for the backporting pull request
required: false
no-inherit-reviewers:
description: "Considered only if reviewers is empty, if true keep reviewers as empty list, otherwise inherit from original pull request"
description: >
Considered only if reviewers is empty, if true keep reviewers as empty list,
otherwise inherit from original pull request
required: false
default: "false"
labels:
description: >
Comma separated list of labels to be assigned to the backported pull request
required: false
inherit-labels:
description: >
If true the backported pull request will inherit labels from the original one
required: false
default: "false"
no-squash:
description: >
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.
required: false
auto-no-squash:
description: >
If the pull request was merged or is open, backport all commits.
If the pull request commits were squashed, backport the squashed commit.
required: false
strategy:
description: Cherry-pick merge strategy
required: false
default: "recursive"
strategy-option:
description: Cherry-pick merge strategy option
required: false
default: "theirs"
cherry-pick-options:
description: >
Additional cherry-pick options
required: false
comments:
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: node16
using: node20
main: dist/gha/index.js
branding:

8510
dist/cli/index.js vendored

File diff suppressed because one or more lines are too long

8463
dist/gha/index.js vendored

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,53 @@
name: Automated Backporting on PR merge using Git Backporting
on:
pull_request_target:
types: [closed, labeled]
branches:
- main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
compute-targets:
if: ${{ github.event.pull_request.state == 'closed' && github.event.pull_request.merged }}
runs-on: ubuntu-latest
outputs:
target-branches: ${{ steps.set-targets.outputs.targets }}
env:
LABELS: ${{ toJSON(github.event.pull_request.labels) }}
steps:
- name: Set target branches
id: set-targets
uses: kiegroup/kie-ci/.ci/actions/parse-labels@main
with:
labels: ${LABELS}
backporting:
if: ${{ github.event.pull_request.state == 'closed' && github.event.pull_request.merged && needs.compute-targets.outputs.target-branches != '[]' }}
name: "[${{ matrix.target-branch }}] - Backporting"
runs-on: ubuntu-latest
needs: compute-targets
strategy:
matrix:
target-branch: ${{ fromJSON(needs.compute-targets.outputs.target-branches) }}
fail-fast: true
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Backporting
uses: ./
with:
dry-run: true
pull-request: ${{ github.event.pull_request.html_url }}
target-branch: ${{ matrix.target-branch }}
auth: "${{ env.GITHUB_TOKEN }}"
title: "[${{ matrix.target-branch }}] ${{ github.event.pull_request.title }}"
body-prefix: "**Backport:** ${{ github.event.pull_request.html_url }}\r\n\r\n**Note**: comment 'ok to test' to properly launch Jenkins jobs\r\n\r\n"
body: "${{ github.event.pull_request.body }}"
labels: "cherry-pick :cherries:"
inherit-labels: false
bp-branch-name: "${{ matrix.target-branch }}_${{ github.event.pull_request.head.ref }}"

View file

@ -0,0 +1,79 @@
{
"pull_request": {
"url": "https://api.github.com/repos/lampajr/backporting-example/pulls/66",
"html_url": "https://github.com/lampajr/backporting-example/pull/66",
"diff_url": "https://github.com/lampajr/backporting-example/pull/66.diff",
"patch_url": "https://github.com/lampajr/backporting-example/pull/66.patch",
"issue_url": "https://api.github.com/repos/lampajr/backporting-example/issues/66",
"number": 66,
"state": "closed",
"title": "Feature1: multiple changes",
"user": {
"login": "lampajr"
},
"body": "This is the body of multiple change",
"merge_commit_sha": "0bcaa01cdd509ca434e123d2e2b9ce7f66234bd7",
"assignee": null,
"assignees": [
],
"requested_reviewers": [
],
"requested_teams": [
],
"labels": [
{
"name": "backport-develop",
"color": "AB975B",
"default": false,
"description": ""
}
],
"head": {
"label": "lampajr:feature1",
"ref": "feature1",
"sha": "69e49388ea2ca9be272b188a9271806d487bf01e",
"user": {
"login": "lampajr"
},
"repo": {
"name": "backporting-example",
"full_name": "lampajr/backporting-example",
"owner": {
"login": "lampajr"
},
"html_url": "https://github.com/lampajr/backporting-example",
"clone_url": "https://github.com/lampajr/backporting-example.git"
}
},
"base": {
"label": "lampajr:main",
"ref": "main",
"sha": "c85b8fcdb741814b3e90e6e5729455cf46ff26ea",
"user": {
"login": "lampajr"
},
"repo": {
"name": "backporting-example",
"full_name": "lampajr/backporting-example",
"owner": {
"login": "lampajr"
},
"html_url": "https://github.com/lampajr/backporting-example",
"description": "Playground repository for automated backporting testing",
"url": "https://api.github.com/repos/lampajr/backporting-example",
"issues_url": "https://api.github.com/repos/lampajr/backporting-example/issues{/number}",
"pulls_url": "https://api.github.com/repos/lampajr/backporting-example/pulls{/number}",
"clone_url": "https://github.com/lampajr/backporting-example.git"
}
},
"merged": true,
"merged_by": {
"login": "lampajr"
},
"comments": 0,
"commits": 2
}
}

2
mise.toml Normal file
View file

@ -0,0 +1,2 @@
[tools]
node = "20"

11887
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,20 @@
{
"name": "@lampajr/bper",
"version": "2.2.1",
"description": "BPer is a tool to execute automatic git backporting.",
"name": "@kie/git-backporting",
"version": "4.8.5",
"description": "Git backporting is a tool to execute automatic pull request git backporting.",
"author": "",
"license": "MIT",
"private": false,
"main": "./dist/gha/index.js",
"bin": {
"bper": "./dist/cli/index.js"
"git-backporting": "./dist/cli/index.js"
},
"files": [
"dist/cli/index.js"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"prepare": "husky install",
"clean": "rm -rf ./build ./dist",
@ -20,33 +23,39 @@
"package:cli": "ncc build ./build/src/bin/cli.js -o dist/cli",
"package:gha": "ncc build ./build/src/bin/gha.js -o dist/gha",
"build": "npm run clean && npm run compile && npm run package",
"test": "jest",
"test": "jest --silent",
"test:report": "npm test -- --coverage --testResultsProcessor=jest-sonar-reporter",
"lint": "eslint . --ext .ts",
"lint:fix": "npm run lint -- --fix",
"ts-node": "ts-node",
"postversion": "npm run build && git add dist && rm -rf build",
"release": "release-it"
"release": "release-it",
"release:branch": "git checkout -b release/$(release-it --release-version) main",
"release:prepare": "release-it --no-npm.publish --no-github.release --no-git.push --no-git.tag --no-git.requireUpstream",
"release:prepare:all": "npm run release:branch && npm run release:prepare"
},
"repository": {
"type": "git",
"url": "git+https://github.com/lampajr/backporting.git"
"url": "git+https://github.com/kiegroup/git-backporting.git"
},
"keywords": [
"backporting",
"pull-requests",
"merge-requests",
"github-action",
"cherry-pick"
],
"bugs": {
"url": "https://github.com/lampajr/backporting/issues"
"url": "https://github.com/kiegroup/git-backporting/issues"
},
"homepage": "https://github.com/lampajr/backporting#readme",
"homepage": "https://github.com/kiegroup/git-backporting#readme",
"devDependencies": {
"@commitlint/cli": "^17.4.0",
"@commitlint/config-conventional": "^17.4.0",
"@gitbeaker/rest": "^39.1.0",
"@kie/mock-github": "^1.1.0",
"@release-it/conventional-changelog": "^5.1.1",
"@octokit/webhooks-types": "^6.8.0",
"@release-it/conventional-changelog": "^10.0.0",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^29.2.4",
"@types/node": "^18.11.17",
@ -55,11 +64,11 @@
"@vercel/ncc": "^0.36.0",
"eslint": "^8.30.0",
"husky": "^8.0.2",
"jest": "^29.3.1",
"jest": "^29.0.0",
"jest-sonar-reporter": "^2.0.0",
"release-it": "^15.6.0",
"release-it": "^18.1.2",
"semver": "^7.3.8",
"ts-jest": "^29.0.3",
"ts-jest": "^29.0.0",
"ts-node": "^10.8.1",
"tsc-alias": "^1.8.2",
"tsconfig-paths": "^4.1.0",
@ -68,9 +77,10 @@
"dependencies": {
"@actions/core": "^1.10.0",
"@octokit/rest": "^18.12.0",
"@octokit/webhooks-types": "^6.8.0",
"axios": "^1.4.0",
"commander": "^9.3.0",
"fs-extra": "^11.1.0",
"https": "^1.0.0",
"simple-git": "^3.15.1"
}
}

View file

@ -4,7 +4,52 @@ 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();
if (!args.pullRequest) {
throw new Error("Missing option: pull request must be provided");
}
// validate and fill with defaults
if ((!args.targetBranch || args.targetBranch.trim().length == 0) && !args.targetBranchPattern) {
throw new Error("Missing option: target branch(es) or target regular expression must be provided");
}
return {
pullRequest: args.pullRequest,
targetBranch: args.targetBranch,
targetBranchPattern: args.targetBranchPattern,
dryRun: this.getOrDefault(args.dryRun, false),
auth: this.getOrDefault(args.auth),
folder: this.getOrDefault(args.folder),
gitClient: this.getOrDefault(args.gitClient),
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),
labels: this.getOrDefault(args.labels, []),
inheritLabels: this.getOrDefault(args.inheritLabels, false),
squash: this.getOrDefault(args.squash, true),
autoNoSquash: this.getOrDefault(args.autoNoSquash, false),
strategy: this.getOrDefault(args.strategy),
strategyOption: this.getOrDefault(args.strategyOption),
cherryPickOptions: this.getOrDefault(args.cherryPickOptions),
comments: this.getOrDefault(args.comments),
enableErrorNotification: this.getOrDefault(args.enableErrorNotification, false),
};
}
}

View file

@ -0,0 +1,56 @@
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);
}
/**
* Return the input only if it is not a blank or null string, otherwise returns undefined
* @param key input key
* @returns the value or undefined
*/
export function getOrUndefined(value: string): string | undefined {
return value !== "" ? value : undefined;
}
// get rid of inner spaces too
export function getAsCleanedCommaSeparatedList(value: string): string[] | undefined {
// trim the value
const trimmed: string = value.trim();
return trimmed !== "" ? trimmed.replace(/\s/g, "").split(",") : undefined;
}
// preserve inner spaces
export function getAsCommaSeparatedList(value: string): string[] | undefined {
// trim the value
const trimmed: string = value.trim();
return trimmed !== "" ? trimmed.split(",").map(v => v.trim()) : undefined;
}
export function getAsSemicolonSeparatedList(value: string): string[] | undefined {
// trim the value
const trimmed: string = value.trim();
return trimmed !== "" ? trimmed.split(";").map(v => v.trim()) : undefined;
}
export function getAsBooleanOrUndefined(value: string): boolean | undefined {
const trimmed = value.trim();
return trimmed !== "" ? trimmed.toLowerCase() === "true" : undefined;
}

View file

@ -1,19 +1,32 @@
/**
* Input arguments
* Tool's input arguments interface
*/
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
// NOTE: keep targetBranch as singular and of type string for backward compatibilities
targetBranch?: string, // comma separated list of branches on the target repo where the change should be backported to
targetBranchPattern?: string, // regular expression to extract target branch(es) from pull request labels
pullRequest: string, // url of the pull request to backport
dryRun?: boolean, // if enabled do not push anything remotely
auth?: string, // git service auth, like github token
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'
gitClient?: string, // git client
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 <original-pr-link>`
bpBranchName?: string, // backport pr branch name, default computed from commit
// NOTE: keep bpBranchName as singular and of type string for backward compatibilities
bpBranchName?: string, // comma separated list of backport pr branch names, default computed from commit and target branches
reviewers?: string[], // backport pr reviewers
assignees?: string[], // backport pr assignees
inheritReviewers: boolean, // if true and reviewers == [] then inherit reviewers from original pr
inheritReviewers?: boolean, // if true and reviewers == [] then inherit reviewers from original pr
labels?: string[], // backport pr labels
inheritLabels?: boolean, // if true inherit labels from original pr
squash?: boolean,
autoNoSquash?: boolean,
strategy?: string, // cherry-pick merge strategy
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
}

View file

@ -2,56 +2,82 @@ 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 { getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, getAsSemicolonSeparatedList, readConfigFile } from "@bp/service/args/args-utils";
function commaSeparatedList(value: string, _prev: unknown): string[] {
// remove all whitespaces
const cleanedValue: string = value.trim();
return cleanedValue !== "" ? cleanedValue.replace(/\s/g, "").split(",") : [];
}
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>", "branch where changes must be backported to.")
.requiredOption("-pr, --pull-request <pr-url>", "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 <auth>", "git service authentication string, e.g., github token.", "")
.option("-gu, --git-user <git-user>", "local git user name, default is 'GitHub'.", "GitHub")
.option("-ge, --git-email <git-email>", "local git user email, default is 'noreply@github.com'.", "noreply@github.com")
.option("-f, --folder <folder>", "local folder where the repo will be checked out, e.g., /tmp/folder.", undefined)
.option("--title <bp-title>", "backport pr title, default original pr title prefixed by target branch.", undefined)
.option("--body <bp-body>", "backport pr title, default original pr body prefixed by bodyPrefix.", undefined)
.option("--body-prefix <bp-body-prefix>", "backport pr body prefix, default `backport <original-pr-link>`.", undefined)
.option("--bp-branch-name <bp-branch-name>", "backport pr branch name, default auto-generated by the commit.", undefined)
.option("--reviewers <reviewers>", "comma separated list of reviewers for the backporting pull request.", commaSeparatedList, [])
.option("--assignees <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 <branches>", "comma separated list of branches where changes must be backported to")
.option("-tbp, --target-branch-pattern <pattern>", "regular expression pattern to extract target branch(es) from pr labels, the branches will be extracted from the pattern's required `target` named capturing group")
.option("-pr, --pull-request <pr-url>", "pull request url, e.g., https://github.com/kiegroup/git-backporting/pull/1")
.option("-d, --dry-run", "if enabled the tool does not create any pull request nor push anything remotely")
.option("-a, --auth <auth>", "git authentication string, if not provided fallback by looking for existing env variables like GITHUB_TOKEN")
.option("--git-client <github|gitlab|codeberg>", "git client type, if not set it is infered from --pull-request")
.option("-gu, --git-user <git-user>", "local git user name, default is 'GitHub'")
.option("-ge, --git-email <git-email>", "local git user email, default is 'noreply@github.com'")
.option("-f, --folder <folder>", "local folder where the repo will be checked out, e.g., /tmp/folder")
.option("--title <bp-title>", "backport pr title, default original pr title prefixed by target branch")
.option("--body <bp-body>", "backport pr title, default original pr body prefixed by bodyPrefix")
.option("--body-prefix <bp-body-prefix>", "backport pr body prefix, default `backport <original-pr-link>`")
.option("--bp-branch-name <bp-branch-names>", "comma separated list of backport pr branch names, default auto-generated by the commit and target branch")
.option("--reviewers <reviewers>", "comma separated list of reviewers for the backporting pull request", getAsCleanedCommaSeparatedList)
.option("--assignees <assignees>", "comma separated list of assignees for the backporting pull request", getAsCleanedCommaSeparatedList)
.option("--no-inherit-reviewers", "if provided and reviewers option is empty then inherit them from original pull request")
.option("--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("--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");
}
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,
targetBranchPattern: opts.targetBranchPattern,
folder: opts.folder,
gitClient: opts.gitClient,
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,
labels: opts.labels,
inheritLabels: opts.inheritLabels,
squash: opts.squash,
autoNoSquash: opts.autoNoSquash,
strategy: opts.strategy,
strategyOption: opts.strategyOption,
cherryPickOptions: opts.cherryPickOptions,
comments: opts.comments,
enableErrorNotification: opts.enableErrNotification,
};
}
return args;
}
}

View file

@ -1,52 +1,47 @@
import ArgsParser from "@bp/service/args/args-parser";
import { Args } from "@bp/service/args/args.types";
import { getInput } from "@actions/core";
import { getAsBooleanOrUndefined, getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, getAsSemicolonSeparatedList, getOrUndefined, 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
* @param key input key
* @returns the value or undefined
*/
public getOrUndefined(key: string): string | undefined {
const value = getInput(key);
return value !== "" ? value : undefined;
}
readArgs(): Args {
const configFile = getOrUndefined(getInput("config-file"));
public getOrDefault(key: string, defaultValue: string): string {
const value = getInput(key);
return value !== "" ? value : defaultValue;
}
let args: Args;
if (configFile) {
args = readConfigFile(configFile);
} else {
args = {
dryRun: getAsBooleanOrUndefined(getInput("dry-run")),
auth: getOrUndefined(getInput("auth")),
pullRequest: getInput("pull-request"),
targetBranch: getOrUndefined(getInput("target-branch")),
targetBranchPattern: getOrUndefined(getInput("target-branch-pattern")),
folder: getOrUndefined(getInput("folder")),
gitClient: getOrUndefined(getInput("git-client")),
gitUser: getOrUndefined(getInput("git-user")),
gitEmail: getOrUndefined(getInput("git-email")),
title: getOrUndefined(getInput("title")),
body: getOrUndefined(getInput("body", { trimWhitespace: false })),
bodyPrefix: getOrUndefined(getInput("body-prefix", { trimWhitespace: false })),
bpBranchName: getOrUndefined(getInput("bp-branch-name")),
reviewers: getAsCleanedCommaSeparatedList(getInput("reviewers")),
assignees: getAsCleanedCommaSeparatedList(getInput("assignees")),
inheritReviewers: !getAsBooleanOrUndefined(getInput("no-inherit-reviewers")),
labels: getAsCommaSeparatedList(getInput("labels")),
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")),
};
}
public getAsCommaSeparatedList(key: string): string[] {
// trim the value
const value: string = (getInput(key) ?? "").trim();
return value !== "" ? value.replace(/\s/g, "").split(",") : [];
}
public getAsBooleanOrDefault(key: string, defaultValue: boolean): boolean {
const value = getInput(key).trim();
return value !== "" ? value.toLowerCase() === "true" : defaultValue;
}
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),
};
return args;
}
}

View file

@ -9,7 +9,7 @@ import LoggerServiceFactory from "../logger/logger-service-factory";
*/
export default abstract class ConfigsParser {
private readonly logger: LoggerService;
protected readonly logger: LoggerService;
constructor() {
this.logger = LoggerServiceFactory.getLogger();
@ -24,12 +24,12 @@ import LoggerServiceFactory from "../logger/logger-service-factory";
// if pr is opened check if the there exists one single commit
if (configs.originalPullRequest.state == "open") {
this.logger.warn("Trying to backport an open pull request!");
this.logger.warn("Trying to backport an open pull request");
}
// if PR is closed and not merged log a warning
// if PR is closed and not merged throw an error
if (configs.originalPullRequest.state == "closed" && !configs.originalPullRequest.merged) {
throw new Error("Provided pull request is closed and not merged!");
throw new Error("Provided pull request is closed and not merged");
}
return Promise.resolve(configs);

View file

@ -1,22 +1,43 @@
import { GitPullRequest } from "@bp/service/git/git.types";
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
*/
export interface Configs {
dryRun: boolean,
auth: string,
auth?: string,
git: LocalGit,
folder: string,
targetBranch: string,
mergeStrategy?: string, // cherry-pick merge strategy
mergeStrategyOption?: string, // cherry-pick merge strategy option
cherryPickOptions?: string, // additional cherry-pick options
originalPullRequest: GitPullRequest,
backportPullRequest: GitPullRequest,
backportPullRequests: BackportPullRequest[],
errorNotification: ErrorNotification,
}
export enum AuthTokenId {
// github specific token
GITHUB_TOKEN = "GITHUB_TOKEN",
// gitlab specific token
GITLAB_TOKEN = "GITLAB_TOKEN",
// codeberg specific token
CODEBERG_TOKEN = "CODEBERG_TOKEN",
// generic git token
GIT_TOKEN = "GIT_TOKEN",
}

View file

@ -1,34 +1,68 @@
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 GitService from "@bp/service/git/git-service";
import GitServiceFactory from "@bp/service/git/git-service-factory";
import { GitPullRequest } from "@bp/service/git/git.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";
export default class PullRequestConfigsParser extends ConfigsParser {
private gitService: GitService;
private gitClient: GitClient;
constructor() {
super();
this.gitService = GitServiceFactory.getService();
this.gitClient = GitClientFactory.getClient();
}
public async parse(args: Args): Promise<Configs> {
const pr: GitPullRequest = await this.gitService.getPullRequestFromUrl(args.pullRequest);
let pr: GitPullRequest;
if (args.autoNoSquash) {
args.squash = undefined;
}
try {
pr = await this.gitClient.getPullRequestFromUrl(args.pullRequest, args.squash);
} catch(error) {
this.logger.error("Something went wrong retrieving pull request");
throw error;
}
const folder: string = args.folder ?? this.getDefaultFolder();
return {
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,
let targetBranches: string[] = [];
if (args.targetBranchPattern) {
// parse labels to extract target branch(es)
targetBranches = this.getTargetBranchesFromLabels(args.targetBranchPattern, pr.labels);
if (targetBranches.length === 0) {
throw new Error(`Unable to extract target branches with regular expression "${args.targetBranchPattern}"`);
}
} else {
// target branch must be provided if targetRegExp is missing
targetBranches = [...new Set(getAsCommaSeparatedList(args.targetBranch!)!)];
}
const bpBranchNames: string[] = [...new Set(args.bpBranchName ? (getAsCleanedCommaSeparatedList(args.bpBranchName) ?? []) : [])];
if (bpBranchNames.length > 1 && bpBranchNames.length != targetBranches.length) {
throw new Error(`The number of backport branch names, if provided, must match the number of target branches or just one, provided ${bpBranchNames.length} branch names instead`);
}
return {
dryRun: args.dryRun!,
auth: args.auth, // this has been already pre-processed before parsing configs
folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`,
mergeStrategy: args.strategy,
mergeStrategyOption: args.strategyOption,
cherryPickOptions: args.cherryPickOptions,
originalPullRequest: pr,
backportPullRequests: this.generateBackportPullRequestsData(pr, args, targetBranches, bpBranchNames),
git: {
user: args.gitUser ?? this.gitClient.getDefaultGitUser(),
email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(),
},
errorNotification: {
enabled: args.enableErrorNotification ?? false,
message: this.getDefaultErrorComment(),
},
};
}
@ -36,6 +70,38 @@ 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
* @param pattern reg exp pattern to extract target branch from label name
* @param labels list of labels to check
* @returns list of target branches
*/
private getTargetBranchesFromLabels(pattern: string, labels: string[]): string[] {
this.logger.debug(`Extracting branches from [${labels}] using ${pattern}`);
const regExp = new RegExp(pattern);
const branches: string[] = [];
for (const l of labels) {
const result = regExp.exec(l);
if (result?.groups) {
const { target } = result.groups;
if (target){
branches.push(target);
}
}
}
return [...new Set(branches)];
}
/**
* Create a backport pull request starting from the target branch and
* the original pr to be backported
@ -43,7 +109,13 @@ export default class PullRequestConfigsParser extends ConfigsParser {
* @param targetBranch target branch where the backport should be applied
* @returns {GitPullRequest}
*/
private getDefaultBackportPullRequest(originalPullRequest: GitPullRequest, args: Args): GitPullRequest {
private generateBackportPullRequestsData(
originalPullRequest: GitPullRequest,
args: Args,
targetBranches: string[],
bpBranchNames: string[]
): BackportPullRequest[] {
const reviewers = args.reviewers ?? [];
if (reviewers.length == 0 && args.inheritReviewers) {
// inherit only if args.reviewers is empty and args.inheritReviewers set to true
@ -54,19 +126,45 @@ export default class PullRequestConfigsParser extends ConfigsParser {
}
const bodyPrefix = args.bodyPrefix ?? `**Backport:** ${originalPullRequest.htmlUrl}\r\n\r\n`;
const body = args.body ?? `${originalPullRequest.body}\r\n\r\nPowered by [BPer](https://github.com/lampajr/backporting).`;
const body = bodyPrefix + (args.body ?? `${originalPullRequest.body}`);
return {
author: args.gitUser,
title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`,
body: `${bodyPrefix}${body}`,
reviewers: [...new Set(reviewers)],
assignees: [...new Set(args.assignees)],
targetRepo: originalPullRequest.targetRepo,
sourceRepo: originalPullRequest.targetRepo,
branchName: args.bpBranchName,
// nCommits: 0, // not needed, but required by the
// commits: [] // not needed
};
const labels = args.labels ?? [];
if (args.inheritLabels) {
labels.push(...originalPullRequest.labels);
}
return targetBranches.map((tb, idx) => {
// if there multiple branch names take the corresponding one, otherwise get the the first one if it exists
let backportBranch = bpBranchNames.length > 1 ? bpBranchNames[idx] : bpBranchNames[0];
if (backportBranch === undefined || backportBranch.trim() === "") {
// for each commit takes the first 7 chars that are enough to uniquely identify them in most of the projects
const concatenatedCommits: string = originalPullRequest.commits!.map(c => c.slice(0, 7)).join("-");
backportBranch = `bp-${tb}-${concatenatedCommits}`;
} else if (bpBranchNames.length == 1 && targetBranches.length > 1) {
// multiple targets and single custom backport branch name we need to differentiate branch names
// so append "-${tb}" to the provided name
backportBranch = backportBranch + `-${tb}`;
}
if (backportBranch.length > 250) {
this.logger.warn(`Backport branch (length=${backportBranch.length}) exceeded the max length of 250 chars, branch name truncated!`);
backportBranch = backportBranch.slice(0, 250);
}
return {
owner: originalPullRequest.targetRepo.owner,
repo: originalPullRequest.targetRepo.project,
head: backportBranch,
base: tb,
title: args.title ?? `[${tb}] ${originalPullRequest.title}`,
// preserve new line chars
body: body.replace(/\\n/g, "\n").replace(/\\r/g, "\r"),
reviewers: [...new Set(reviewers)],
assignees: [...new Set(args.assignees)],
labels: [...new Set(labels)],
comments: args.comments?.map(c => c.replace(/\\n/g, "\n").replace(/\\r/g, "\r")) ?? [],
};
}) as BackportPullRequest[];
}
}

View file

@ -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;
@ -32,11 +32,12 @@ export default class GitCLIService {
/**
* Update the provided remote URL by adding the auth token if not empty
* @param remoteURL remote link, e.g., https://github.com/lampajr/backporting-example.git
* @param remoteURL remote link, e.g., https://github.com/kiegroup/git-backporting-example.git
*/
private remoteWithAuth(remoteURL: string): string {
if (this.auth && this.gitData.user) {
return remoteURL.replace("://", `://${this.gitData.user}:${this.auth}@`);
if (this.auth) {
// Anything will work as a username.
return remoteURL.replace("://", `://token:${this.auth}@`);
}
// return remote as it is
@ -60,12 +61,25 @@ export default class GitCLIService {
* @param branch branch which should be cloned
*/
async clone(from: string, to: string, branch: string): Promise<void> {
this.logger.info(`Cloning repository ${from} to ${to}.`);
this.logger.info(`Cloning repository ${from} to ${to}`);
if (!fs.existsSync(to)) {
await simpleGit().clone(this.remoteWithAuth(from), to, ["--quiet", "--shallow-submodules", "--no-tags", "--branch", branch]);
} else {
this.logger.warn(`Folder ${to} already exist. Won't clone`);
return;
}
this.logger.info(`Folder ${to} already exist. Won't clone`);
// ensure the working tree is properly reset - no stale changes
// from previous (failed) backport
const ongoingCherryPick = await this.anyConflict(to);
if (ongoingCherryPick) {
this.logger.warn("Found previously failed cherry-pick, aborting it");
await this.git(to).raw(["cherry-pick", "--abort"]);
}
// checkout to the proper branch
this.logger.info(`Checking out branch ${branch}`);
await this.git(to).checkout(branch);
}
/**
@ -74,7 +88,7 @@ export default class GitCLIService {
* @param newBranch new branch name
*/
async createLocalBranch(cwd: string, newBranch: string): Promise<void> {
this.logger.info(`Creating branch ${newBranch}.`);
this.logger.info(`Creating branch ${newBranch}`);
await this.git(cwd).checkoutLocalBranch(newBranch);
}
@ -85,7 +99,7 @@ export default class GitCLIService {
* @param remoteName [optional] name of the remote, by default 'fork' is used
*/
async addRemote(cwd: string, remote: string, remoteName = "fork"): Promise<void> {
this.logger.info(`Adding new remote ${remote}.`);
this.logger.info(`Adding new remote ${remote}`);
await this.git(cwd).addRemote(remoteName, this.remoteWithAuth(remote));
}
@ -96,7 +110,7 @@ export default class GitCLIService {
* @param remote [optional] the remote to fetch, by default origin
*/
async fetch(cwd: string, branch: string, remote = "origin"): Promise<void> {
this.logger.info(`Fetching ${remote} ${branch}.`);
this.logger.info(`Fetching ${remote} ${branch}`);
await this.git(cwd).fetch(remote, branch, ["--quiet"]);
}
@ -105,9 +119,40 @@ export default class GitCLIService {
* @param cwd repository in which the sha should be cherry picked to
* @param sha commit sha
*/
async cherryPick(cwd: string, sha: string): Promise<void> {
this.logger.info(`Cherry picking ${sha}.`);
await this.git(cwd).raw(["cherry-pick", "-m", "1", "--strategy=recursive", "--strategy-option=theirs", sha]);
async cherryPick(cwd: string, sha: string, strategy = "recursive", strategyOption = "theirs", cherryPickOptions: string | undefined): Promise<void> {
this.logger.info(`Cherry picking ${sha}`);
let options = ["cherry-pick", "-m", "1", `--strategy=${strategy}`, `--strategy-option=${strategyOption}`];
if (cherryPickOptions !== undefined) {
options = options.concat(cherryPickOptions.split(" "));
}
options.push(sha);
this.logger.debug(`Cherry picking command git ${options}`);
try {
await this.git(cwd).raw(options);
} catch(error) {
const diff = await this.git(cwd).diff();
if (diff) {
throw new Error(`${error}\r\nShowing git diff:\r\n` + diff);
}
throw error;
}
}
/**
* Check whether there are some conflicts in the current working directory
* which means there is an ongoing cherry-pick that did not complete successfully
* @param cwd repository in which the check should be performed
* @return true if there is some conflict, false otherwise
*/
async anyConflict(cwd: string): Promise<boolean> {
const status = await this.git(cwd).status();
if (status.conflicted.length > 0) {
this.logger.debug(`Found conflicts in branch ${status.current}`);
return true;
}
return false;
}
/**
@ -117,7 +162,7 @@ export default class GitCLIService {
* @param remote [optional] remote to which the branch should be pushed to, by default 'origin'
*/
async push(cwd: string, branch: string, remote = "origin", force = false): Promise<void> {
this.logger.info(`Pushing ${branch} to ${remote}.`);
this.logger.info(`Pushing ${branch} to ${remote}`);
const options = ["--quiet"];
if (force) {

View file

@ -0,0 +1,61 @@
import GitClient from "@bp/service/git/git-client";
import { GitClientType } from "@bp/service/git/git.types";
import GitHubService from "@bp/service/git/github/github-client";
import LoggerService from "@bp/service/logger/logger-service";
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import GitLabClient from "./gitlab/gitlab-client";
/**
* Singleton git service factory class
*/
export default class GitClientFactory {
private static logger: LoggerService = LoggerServiceFactory.getLogger();
private static instance?: GitClient;
// this method assumes there already exists a singleton client instance, otherwise it will fail
public static getClient(): GitClient {
if (!GitClientFactory.instance) {
throw new Error("You must call `getOrCreate` method first");
}
return GitClientFactory.instance;
}
/**
* Initialize the singleton git management service
* @param type git management service type
* @param authToken authentication token, like github/gitlab token
*/
public static getOrCreate(type: GitClientType, authToken: string | undefined, apiUrl: string): GitClient {
if (GitClientFactory.instance) {
GitClientFactory.logger.warn("Git service already initialized");
return GitClientFactory.instance;
}
this.logger.debug(`Setting up ${type} client: apiUrl=${apiUrl}, token=****`);
switch(type) {
case GitClientType.GITHUB:
GitClientFactory.instance = new GitHubService(authToken, apiUrl);
break;
case GitClientType.GITLAB:
GitClientFactory.instance = new GitLabClient(authToken, apiUrl);
break;
case GitClientType.CODEBERG:
GitClientFactory.instance = new GitHubService(authToken, apiUrl, true);
break;
default:
throw new Error(`Invalid git service type received: ${type}`);
}
return GitClientFactory.instance;
}
// this is used for testing purposes
public static reset(): void {
GitClientFactory.logger.warn("Resetting git service");
GitClientFactory.instance = undefined;
}
}

View file

@ -0,0 +1,54 @@
import { BackportPullRequest, GitClientType, GitPullRequest } from "@bp/service/git/git.types";
/**
* Git management service interface, which provides a common API for interacting
* with several git management services like GitHub, Gitlab or Bitbucket.
*/
export default interface GitClient {
/**
* @returns {GitClientType} specific git client enum type
*/
getClientType(): GitClientType
// READ
getDefaultGitUser(): string;
getDefaultGitEmail(): string;
/**
* Get a pull request object from the underneath git service
* @param owner repository's owner
* @param repo repository's name
* @param prNumber pull request number
* @param squash if true keep just one single commit, otherwise get the full list
* @returns {Promise<PullRequest>}
*/
getPullRequest(owner: string, repo: string, prNumber: number, squash: boolean | undefined): Promise<GitPullRequest>;
/**
* Get a pull request object from the underneath git service
* @param prUrl pull request html url
* @param squash if true keep just one single commit, otherwise get the full list
* @returns {Promise<PullRequest>}
*/
getPullRequestFromUrl(prUrl: string, squash: boolean | undefined): Promise<GitPullRequest>;
// WRITE
/**
* Create a new pull request on the underneath git service
* @param backport backport pull request data
* @returns {Promise<string>} the pull request url
*/
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>;
}

View file

@ -0,0 +1,21 @@
import { GitPullRequest, GitRepoState, GitRepository } from "@bp/service/git/git.types";
/**
* Generic git client response mapper
*
* PR - full pull request schema type
* S - pull request state type
*/
export default interface GitResponseMapper<PR, S> {
mapPullRequest(
pr: PR,
commits?: string[],
): Promise<GitPullRequest>;
mapGitState(state: S): GitRepoState;
mapSourceRepo(pull: PR): Promise<GitRepository>;
mapTargetRepo (pull: PR): Promise<GitRepository>;
}

View file

@ -1,43 +0,0 @@
import GitService from "@bp/service/git/git-service";
import { GitServiceType } from "@bp/service/git/git.types";
import GitHubService from "@bp/service/git/github/github-service";
import LoggerService from "@bp/service/logger/logger-service";
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
/**
* Singleton git service factory class
*/
export default class GitServiceFactory {
private static logger: LoggerService = LoggerServiceFactory.getLogger();
private static instance?: GitService;
public static getService(): GitService {
if (!GitServiceFactory.instance) {
throw new Error("You must call `init` method first!");
}
return GitServiceFactory.instance;
}
/**
* Initialize the singleton git management service
* @param type git management service type
* @param auth authentication, like github token
*/
public static getOrCreate(type: GitServiceType, auth: string): void {
if (GitServiceFactory.instance) {
GitServiceFactory.logger.warn("Git service already initialized!");
return;
}
switch(type) {
case GitServiceType.GITHUB:
GitServiceFactory.instance = new GitHubService(auth);
break;
default:
throw new Error(`Invalid git service type received: ${type}`);
}
}
}

View file

@ -1,34 +0,0 @@
import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
/**
* Git management service interface, which provides a common API for interacting
* with several git management services like GitHub, Gitlab or Bitbucket.
*/
export default interface GitService {
// READ
/**
* Get a pull request object from the underneath git service
* @param owner repository's owner
* @param repo repository's name
* @param prNumber pull request number
* @returns {Promise<PullRequest>}
*/
getPullRequest(owner: string, repo: string, prNumber: number): Promise<GitPullRequest>;
/**
* Get a pull request object from the underneath git service
* @param prUrl pull request html url
* @returns {Promise<PullRequest>}
*/
getPullRequestFromUrl(prUrl: string): Promise<GitPullRequest>;
// WRITE
/**
* Create a new pull request on the underneath git service
* @param backport backport pull request data
*/
createPullRequest(backport: BackportPullRequest): Promise<void>;
}

104
src/service/git/git-util.ts Normal file
View file

@ -0,0 +1,104 @@
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { GitClientType } from "@bp/service/git/git.types";
import { AuthTokenId } from "@bp/service/configs/configs.types";
const PUBLIC_GITHUB_URL = "https://github.com";
const PUBLIC_GITHUB_API = "https://api.github.com";
/**
* Infer the remote GIT service to interact with based on the provided
* pull request URL
* @param prUrl provided pull request URL
* @returns {GitClientType}
*/
export const inferGitClient = (prUrl: string): GitClientType => {
const stdPrUrl = prUrl.toLowerCase().trim();
if (stdPrUrl.includes(GitClientType.GITHUB.toString())) {
return GitClientType.GITHUB;
} else if (stdPrUrl.includes(GitClientType.GITLAB.toString())) {
return GitClientType.GITLAB;
} else if (stdPrUrl.includes(GitClientType.CODEBERG.toString())) {
return GitClientType.CODEBERG;
}
throw new Error(`Remote git service not recognized from pr url: ${prUrl}`);
};
/**
* Infer the host git service from the pull request url
* @param prUrl pull/merge request url
* @param apiVersion the api version, ignored in case of public github
* @returns api URL like https://api.github.com or https://gitlab.com/api/v4
*/
export const inferGitApiUrl = (prUrl: string, apiVersion = "v4"): string => {
const url = new URL(prUrl);
const baseUrl = `${url.protocol}//${url.host}`;
if (baseUrl.includes(PUBLIC_GITHUB_URL) || baseUrl.includes(PUBLIC_GITHUB_API)) {
return PUBLIC_GITHUB_API;
}
return `${baseUrl}/api/${apiVersion}`;
};
/**
* Infer the value of the squash option
* @param open true if the pull/merge request is still open
* @param squash_commit undefined or null if the pull/merge request was merged, the sha of the squashed commit if it was squashed
* @returns true if a single commit must be cherry-picked, false if all merged commits must be cherry-picked
*/
export const inferSquash = (open: boolean, squash_commit: string | undefined | null): boolean => {
const logger = LoggerServiceFactory.getLogger();
if (open) {
logger.debug("cherry-pick all commits because they have not been merged (or squashed) in the base branch yet");
return false;
} else {
if (squash_commit) {
logger.debug(`cherry-pick the squashed commit ${squash_commit}`);
return true;
} else {
logger.debug("cherry-pick the merged commit(s)");
return false;
}
}
};
/**
* Retrieve the git token from env variable, the default is taken from GIT_TOKEN env.
* All specific git env variable have precedence and override the default one.
* @param gitType
* @returns tuple where
* - the first element is the corresponding env value
* - the second element is true if the value is not undefined nor empty
*/
export const getGitTokenFromEnv = (gitType: GitClientType): string | undefined => {
let [token] = getEnv(AuthTokenId.GIT_TOKEN);
let [specToken, specOk]: [string | undefined, boolean] = [undefined, false];
if (GitClientType.GITHUB == gitType) {
[specToken, specOk] = getEnv(AuthTokenId.GITHUB_TOKEN);
} else if (GitClientType.GITLAB == gitType) {
[specToken, specOk] = getEnv(AuthTokenId.GITLAB_TOKEN);
} else if (GitClientType.CODEBERG == gitType) {
[specToken, specOk] = getEnv(AuthTokenId.CODEBERG_TOKEN);
}
if (specOk) {
token = specToken;
}
return token;
};
/**
* Get process env variable given the input key string
* @param key
* @returns tuple where
* - the first element is the corresponding env value
* - the second element is true if the value is not undefined nor empty
*/
export const getEnv = (key: string): [string | undefined, boolean] => {
const val = process.env[key];
return [val, val !== undefined && val !== ""];
};

View file

@ -1,19 +1,20 @@
export interface GitPullRequest {
number?: number,
author: string,
url?: string,
url: string,
htmlUrl?: string,
state?: "open" | "closed",
state?: GitRepoState,
merged?: boolean,
mergedBy?: string,
title: string,
body: string,
reviewers: string[],
assignees: string[],
labels: string[],
targetRepo: GitRepository,
sourceRepo: GitRepository,
nCommits?: number, // number of commits in the pr
commits?: string[], // merge commit or last one
nCommits: number, // number of commits in the pr
commits: string[], // merge commit or last one
branchName?: string,
}
@ -32,9 +33,20 @@ export interface BackportPullRequest {
body: string, // pr body
reviewers: string[], // pr list of reviewers
assignees: string[], // pr list of assignees
branchName?: string,
labels: string[], // pr list of assigned labels
comments: string[], // pr list of additional comments
// branchName?: string,
}
export enum GitServiceType {
GITHUB = "github"
export enum GitClientType {
GITHUB = "github",
GITLAB = "gitlab",
CODEBERG = "codeberg",
}
export enum GitRepoState {
OPEN = "open",
CLOSED = "closed",
LOCKED = "locked", // just on gitlab
MERGED = "merged", // just on gitlab
}

View file

@ -0,0 +1,204 @@
import GitClient from "@bp/service/git/git-client";
import { inferSquash } from "@bp/service/git/git-util";
import { BackportPullRequest, GitClientType, GitPullRequest } from "@bp/service/git/git.types";
import GitHubMapper from "@bp/service/git/github/github-mapper";
import OctokitFactory from "@bp/service/git/github/octokit-factory";
import LoggerService from "@bp/service/logger/logger-service";
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { Octokit } from "@octokit/rest";
import { PullRequest } from "@octokit/webhooks-types";
export default class GitHubClient implements GitClient {
private logger: LoggerService;
private apiUrl: string;
private isForCodeberg: boolean;
private octokit: Octokit;
private mapper: GitHubMapper;
constructor(token: string | undefined, apiUrl: string, isForCodeberg = false) {
this.apiUrl = apiUrl;
this.isForCodeberg = isForCodeberg;
this.logger = LoggerServiceFactory.getLogger();
this.octokit = OctokitFactory.getOctokit(token, this.apiUrl);
this.mapper = new GitHubMapper();
}
getClientType(): GitClientType {
return this.isForCodeberg ? GitClientType.CODEBERG : GitClientType.GITHUB;
}
// READ
getDefaultGitUser(): string {
return this.apiUrl.includes(GitClientType.CODEBERG.toString()) ? "Codeberg" : "GitHub";
}
getDefaultGitEmail(): string {
return "noreply@github.com";
}
async getPullRequest(owner: string, repo: string, prNumber: number, squash: boolean | undefined): Promise<GitPullRequest> {
this.logger.debug(`Fetching pull request ${owner}/${repo}/${prNumber}`);
const { data } = await this.octokit.rest.pulls.get({
owner: owner,
repo: repo,
pull_number: prNumber,
});
if (squash === undefined) {
let commit_sha: string | undefined = undefined;
const open: boolean = data.state == "open";
if (!open) {
const commit = await this.octokit.rest.git.getCommit({
owner: owner,
repo: repo,
commit_sha: (data.merge_commit_sha as string),
});
if (commit.data.parents.length === 1) {
commit_sha = (data.merge_commit_sha as string);
}
}
squash = inferSquash(open, commit_sha);
}
const commits: string[] = [];
if (!squash) {
// fetch all commits
try {
const { data } = await this.octokit.rest.pulls.listCommits({
owner: owner,
repo: repo,
pull_number: prNumber,
});
commits.push(...data.map(c => c.sha));
if (this.isForCodeberg) {
// For some reason, even though Codeberg advertises API compatibility
// with GitHub, it returns commits in reversed order.
commits.reverse();
}
} catch(error) {
throw new Error(`Failed to retrieve commits for pull request n. ${prNumber}`);
}
}
return this.mapper.mapPullRequest(data as PullRequest, commits);
}
async getPullRequestFromUrl(prUrl: string, squash: boolean | undefined): Promise<GitPullRequest> {
const { owner, project, id } = this.extractPullRequestData(prUrl);
return this.getPullRequest(owner, project, id, squash);
}
// WRITE
async createPullRequest(backport: BackportPullRequest): Promise<string> {
this.logger.info(`Creating pull request ${backport.head} -> ${backport.base}`);
this.logger.info(`${JSON.stringify(backport, null, 2)}`);
const { data } = await this.octokit.pulls.create({
owner: backport.owner,
repo: backport.repo,
head: backport.head,
base: backport.base,
title: backport.title,
body: backport.body,
});
if (!data) {
throw new Error("Pull request creation failed");
}
const promises = [];
if (backport.labels.length > 0) {
promises.push(
this.octokit.issues.addLabels({
owner: backport.owner,
repo: backport.repo,
issue_number: (data as PullRequest).number,
labels: backport.labels,
}).catch(error => this.logger.error(`Error setting labels: ${error}`))
);
}
if (backport.reviewers.length > 0) {
promises.push(
this.octokit.pulls.requestReviewers({
owner: backport.owner,
repo: backport.repo,
pull_number: (data as PullRequest).number,
reviewers: backport.reviewers,
}).catch(error => this.logger.error(`Error requesting reviewers: ${error}`))
);
}
if (backport.assignees.length > 0) {
promises.push(
this.octokit.issues.addAssignees({
owner: backport.owner,
repo: backport.repo,
issue_number: (data as PullRequest).number,
assignees: backport.assignees,
}).catch(error => this.logger.error(`Error setting assignees: ${error}`))
);
}
if (backport.comments.length > 0) {
backport.comments.forEach(c => {
promises.push(
this.octokit.issues.createComment({
owner: backport.owner,
repo: backport.repo,
issue_number: (data as PullRequest).number,
body: c,
}).catch(error => this.logger.error(`Error posting comment: ${error}`))
);
});
}
await Promise.all(promises);
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
/**
* Extract repository owner and project from the pull request url
* @param prUrl pull request url
* @returns {{owner: string, project: string}}
*/
private extractPullRequestData(prUrl: string): {owner: string, project: string, id: number} {
const elems: string[] = prUrl.split("/");
return {
owner: elems[elems.length - 4],
project: elems[elems.length - 3],
id: parseInt(prUrl.substring(prUrl.lastIndexOf("/") + 1, prUrl.length)),
};
}
}

View file

@ -1,9 +1,19 @@
import { GitPullRequest } from "@bp/service/git/git.types";
import { GitPullRequest, GitRepoState, GitRepository } from "@bp/service/git/git.types";
import { PullRequest, User } from "@octokit/webhooks-types";
import GitResponseMapper from "@bp/service/git/git-mapper";
export default class GitHubMapper {
export default class GitHubMapper implements GitResponseMapper<PullRequest, "open" | "closed"> {
mapPullRequest(pr: PullRequest): GitPullRequest {
mapGitState(state: "open" | "closed"): GitRepoState {
switch (state) {
case "open":
return GitRepoState.OPEN;
default:
return GitRepoState.CLOSED;
}
}
async mapPullRequest(pr: PullRequest, commits?: string[]): Promise<GitPullRequest> {
return {
number: pr.number,
author: pr.user.login,
@ -11,24 +21,38 @@ export default class GitHubMapper {
htmlUrl: pr.html_url,
title: pr.title,
body: pr.body ?? "",
state: pr.state,
state: this.mapGitState(pr.state), // TODO fix using custom mapper
merged: pr.merged ?? false,
mergedBy: pr.merged_by?.login,
reviewers: pr.requested_reviewers.filter(r => "login" in r).map((r => (r as User)?.login)),
assignees: pr.assignees.filter(r => "login" in r).map(r => r.login),
sourceRepo: {
owner: pr.head.repo.full_name.split("/")[0],
project: pr.head.repo.full_name.split("/")[1],
cloneUrl: pr.head.repo.clone_url
},
targetRepo: {
owner: pr.base.repo.full_name.split("/")[0],
project: pr.base.repo.full_name.split("/")[1],
cloneUrl: pr.base.repo.clone_url
},
reviewers: pr.requested_reviewers?.filter(r => r && "login" in r).map((r => (r as User)?.login)) ?? [],
assignees: pr.assignees?.filter(r => r && "login" in r).map(r => r.login) ?? [],
labels: pr.labels?.map(l => l.name) ?? [],
sourceRepo: await this.mapSourceRepo(pr),
targetRepo: await this.mapTargetRepo(pr),
nCommits: pr.commits,
// if pr is open use latest commit sha otherwise use merge_commit_sha
commits: pr.state === "open" ? [pr.head.sha] : [pr.merge_commit_sha as string]
// if commits is provided use them, otherwise fetch the single sha representing the whole pr
commits: (commits && commits.length > 0) ? commits : this.getSha(pr),
};
}
private getSha(pr: PullRequest) {
// if pr is open use latest commit sha otherwise use merge_commit_sha
return pr.state === "open" ? [pr.head.sha] : [pr.merge_commit_sha as string];
}
async mapSourceRepo(pr: PullRequest): Promise<GitRepository> {
return Promise.resolve({
owner: pr.head.repo.full_name.split("/")[0],
project: pr.head.repo.full_name.split("/")[1],
cloneUrl: pr.head.repo.clone_url
});
}
async mapTargetRepo(pr: PullRequest): Promise<GitRepository> {
return Promise.resolve({
owner: pr.base.repo.full_name.split("/")[0],
project: pr.base.repo.full_name.split("/")[1],
cloneUrl: pr.base.repo.clone_url
});
}
}

View file

@ -1,96 +0,0 @@
import GitService from "@bp/service/git/git-service";
import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
import GitHubMapper from "@bp/service/git/github/github-mapper";
import OctokitFactory from "@bp/service/git/github/octokit-factory";
import LoggerService from "@bp/service/logger/logger-service";
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { Octokit } from "@octokit/rest";
import { PullRequest } from "@octokit/webhooks-types";
export default class GitHubService implements GitService {
private logger: LoggerService;
private octokit: Octokit;
private mapper: GitHubMapper;
constructor(token: string) {
this.logger = LoggerServiceFactory.getLogger();
this.octokit = OctokitFactory.getOctokit(token);
this.mapper = new GitHubMapper();
}
// READ
async getPullRequest(owner: string, repo: string, prNumber: number): Promise<GitPullRequest> {
this.logger.info(`Getting pull request ${owner}/${repo}/${prNumber}.`);
const { data } = await this.octokit.rest.pulls.get({
owner: owner,
repo: repo,
pull_number: prNumber
});
return this.mapper.mapPullRequest(data as PullRequest);
}
async getPullRequestFromUrl(prUrl: string): Promise<GitPullRequest> {
const {owner, project} = this.getRepositoryFromPrUrl(prUrl);
return this.getPullRequest(owner, project, parseInt(prUrl.substring(prUrl.lastIndexOf("/") + 1, prUrl.length)));
}
// WRITE
async createPullRequest(backport: BackportPullRequest): Promise<void> {
this.logger.info(`Creating pull request ${backport.head} -> ${backport.base}.`);
this.logger.info(`${JSON.stringify(backport, null, 2)}`);
const { data } = await this.octokit.pulls.create({
owner: backport.owner,
repo: backport.repo,
head: backport.head,
base: backport.base,
title: backport.title,
body: backport.body
});
if (backport.reviewers.length > 0) {
try {
await this.octokit.pulls.requestReviewers({
owner: backport.owner,
repo: backport.repo,
pull_number: (data as PullRequest).number,
reviewers: backport.reviewers,
});
} catch (error) {
this.logger.error(`Error requesting reviewers: ${error}`);
}
}
if (backport.assignees.length > 0) {
try {
await this.octokit.issues.addAssignees({
owner: backport.owner,
repo: backport.repo,
issue_number: (data as PullRequest).number,
assignees: backport.assignees,
});
} catch (error) {
this.logger.error(`Error setting assignees: ${error}`);
}
}
}
// UTILS
/**
* Extract repository owner and project from the pull request url
* @param prUrl pull request url
* @returns {{owner: string, project: string}}
*/
private getRepositoryFromPrUrl(prUrl: string): {owner: string, project: string} {
const elems: string[] = prUrl.split("/");
return {
owner: elems[elems.length - 4],
project: elems[elems.length - 3]
};
}
}

View file

@ -10,12 +10,12 @@ export default class OctokitFactory {
private static logger: LoggerService = LoggerServiceFactory.getLogger();
private static octokit?: Octokit;
public static getOctokit(token: string): Octokit {
public static getOctokit(token: string | undefined, apiUrl: string): Octokit {
if (!OctokitFactory.octokit) {
OctokitFactory.logger.info("Creating octokit instance.");
OctokitFactory.octokit = new Octokit({
auth: token,
userAgent: "lampajr/backporting"
userAgent: "kiegroup/git-backporting",
baseUrl: apiUrl
});
}

View file

@ -0,0 +1,239 @@
import LoggerService from "@bp/service/logger/logger-service";
import GitClient from "@bp/service/git/git-client";
import { inferSquash } from "@bp/service/git/git-util";
import { GitPullRequest, BackportPullRequest, GitClientType } from "@bp/service/git/git.types";
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { CommitSchema, MergeRequestSchema, UserSchema } from "@gitbeaker/rest";
import GitLabMapper from "@bp/service/git/gitlab/gitlab-mapper";
import axios, { Axios } from "axios";
import https from "https";
export default class GitLabClient implements GitClient {
private readonly logger: LoggerService;
private readonly apiUrl: string;
private readonly mapper: GitLabMapper;
private readonly client: Axios;
constructor(token: string | undefined, apiUrl: string, rejectUnauthorized = false) {
this.logger = LoggerServiceFactory.getLogger();
this.apiUrl = apiUrl;
this.client = axios.create({
baseURL: this.apiUrl,
headers: {
Authorization: token ? `Bearer ${token}` : "",
"User-Agent": "kiegroup/git-backporting",
},
httpsAgent: new https.Agent({
rejectUnauthorized
})
});
this.mapper = new GitLabMapper(this.client);
}
getClientType(): GitClientType {
return GitClientType.GITLAB;
}
getDefaultGitUser(): string {
return "Gitlab";
}
getDefaultGitEmail(): string {
return "noreply@gitlab.com";
}
// READ
// example: <host>/api/v4/projects/<namespace>%2Fbackporting-example/merge_requests/1
async getPullRequest(namespace: string, repo: string, mrNumber: number, squash: boolean | undefined): Promise<GitPullRequest> {
const projectId = this.getProjectId(namespace, repo);
const url = `/projects/${projectId}/merge_requests/${mrNumber}`;
this.logger.debug(`Fetching pull request ${url}`);
const { data } = await this.client.get(`${url}`);
if (squash === undefined) {
squash = inferSquash(data.state === "opened", data.squash_commit_sha);
}
const commits: string[] = [];
if (!squash) {
// fetch all commits
try {
const { data } = await this.client.get(`/projects/${projectId}/merge_requests/${mrNumber}/commits`);
// gitlab returns them in reverse order
commits.push(...(data as CommitSchema[]).map(c => c.id).reverse());
} catch(error) {
throw new Error(`Failed to retrieve commits for merge request n. ${mrNumber}`);
}
}
return this.mapper.mapPullRequest(data as MergeRequestSchema, commits);
}
getPullRequestFromUrl(mrUrl: string, squash: boolean | undefined): Promise<GitPullRequest> {
const { namespace, project, id } = this.extractMergeRequestData(mrUrl);
return this.getPullRequest(namespace, project, id, squash);
}
// WRITE
async createPullRequest(backport: BackportPullRequest): Promise<string> {
this.logger.info(`Creating pull request ${backport.head} -> ${backport.base}`);
this.logger.info(`${JSON.stringify(backport, null, 2)}`);
const projectId = this.getProjectId(backport.owner, backport.repo);
const { data } = await this.client.post(`/projects/${projectId}/merge_requests`, {
source_branch: backport.head,
target_branch: backport.base,
title: backport.title,
description: backport.body,
reviewer_ids: [],
assignee_ids: [],
});
const mr = data as MergeRequestSchema;
const promises = [];
// labels
if (backport.labels.length > 0) {
this.logger.info("Setting labels: " + backport.labels);
promises.push(
this.client.put(`/projects/${projectId}/merge_requests/${mr.iid}`, {
labels: backport.labels.join(","),
}).catch(error => this.logger.warn("Failure trying to update labels. " + error))
);
}
// comments
if (backport.comments.length > 0) {
this.logger.info("Posting comments: " + backport.comments);
backport.comments.forEach(c => {
promises.push(
this.client.post(`/projects/${projectId}/merge_requests/${mr.iid}/notes`, {
body: c,
}).catch(error => this.logger.warn("Failure trying to post comment. " + error))
);
});
}
// reviewers
const reviewerIds = await Promise.all(backport.reviewers.map(async r => {
this.logger.debug("Retrieving user: " + r);
return this.getUser(r).then(user => user.id).catch(
() => {
this.logger.warn(`Failed to retrieve reviewer ${r}`);
return undefined;
}
);
}));
if (reviewerIds.length > 0) {
this.logger.info("Setting reviewers: " + reviewerIds);
promises.push(
this.client.put(`/projects/${projectId}/merge_requests/${mr.iid}`, {
reviewer_ids: reviewerIds.filter(r => r !== undefined),
}).catch(error => this.logger.warn("Failure trying to update reviewers. " + error))
);
}
// assignees
const assigneeIds = await Promise.all(backport.assignees.map(async a => {
this.logger.debug("Retrieving user: " + a);
return this.getUser(a).then(user => user.id).catch(
() => {
this.logger.warn(`Failed to retrieve assignee ${a}`);
return undefined;
}
);
}));
if (assigneeIds.length > 0) {
this.logger.info("Setting assignees: " + assigneeIds);
promises.push(
this.client.put(`/projects/${projectId}/merge_requests/${mr.iid}`, {
assignee_ids: assigneeIds.filter(a => a !== undefined),
}).catch(error => this.logger.warn("Failure trying to update assignees. " + error))
);
}
await Promise.all(promises);
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}/merge_requests/${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
* @returns UserSchema
*/
private async getUser(username: string): Promise<UserSchema> {
const { data } = await this.client.get(`/users?username=${username}`);
const users = data as UserSchema[];
if (users.length > 1) {
throw new Error("Too many users found with username=" + username);
}
if (users.length == 0) {
throw new Error("User " + username + " not found");
}
return users[0];
}
/**
* Extract repository namespace, project and mr number from the merge request url
* example: <host>/<namespace>/backporting-example/-/merge_requests/1
* note: "-/" could be omitted
* @param mrUrl merge request url
* @returns {{owner: string, project: string}}
*/
private extractMergeRequestData(mrUrl: string): {namespace: string, project: string, id: number} {
const { pathname } = new URL(mrUrl);
const elems: string[] = pathname.substring(1).replace("/-/", "/").split("/");
let namespace = "";
for (let i = 0; i < elems.length - 3; i++) {
namespace += elems[i] + "/";
}
namespace = namespace.substring(0, namespace.length - 1);
return {
namespace: namespace,
project: elems[elems.length - 3],
id: parseInt(mrUrl.substring(mrUrl.lastIndexOf("/") + 1, mrUrl.length)),
};
}
private getProjectId(namespace: string, repo: string) {
// e.g., <namespace>%2F<repo>
return encodeURIComponent(`${namespace}/${repo}`);
}
}

View file

@ -0,0 +1,88 @@
import { GitPullRequest, GitRepoState, GitRepository } from "@bp/service/git/git.types";
import GitResponseMapper from "@bp/service/git/git-mapper";
import { MergeRequestSchema, ProjectSchema } from "@gitbeaker/rest";
import { Axios } from "axios";
export default class GitLabMapper implements GitResponseMapper<MergeRequestSchema, string> {
private readonly client;
// needs client to perform additional requests
constructor(client: Axios) {
this.client = client;
}
mapGitState(state: string): GitRepoState {
switch (state) {
case "opened":
return GitRepoState.OPEN;
case "closed":
return GitRepoState.CLOSED;
case "merged":
return GitRepoState.MERGED;
default:
return GitRepoState.LOCKED;
}
}
async mapPullRequest(mr: MergeRequestSchema, commits?: string[]): Promise<GitPullRequest> {
return {
number: mr.iid,
author: mr.author.username,
url: mr.web_url,
htmlUrl: mr.web_url,
title: mr.title,
body: mr.description,
state: this.mapGitState(mr.state),
merged: this.isMerged(mr),
mergedBy: mr.merged_by?.username,
reviewers: mr.reviewers?.map((r => r.username)) ?? [],
assignees: mr.assignees?.map((r => r.username)) ?? [],
labels: mr.labels ?? [],
sourceRepo: await this.mapSourceRepo(mr),
targetRepo: await this.mapTargetRepo(mr),
// if commits list is provided use that as source
nCommits: (commits && commits.length > 0) ? commits.length : 1,
commits: (commits && commits.length > 0) ? commits : this.getSha(mr)
};
}
private getSha(mr: MergeRequestSchema) {
// if mr is merged, use merge_commit_sha otherwise use sha
// what is the difference between sha and diff_refs.head_sha?
return this.isMerged(mr) ? [mr.squash_commit_sha ? mr.squash_commit_sha : mr.merge_commit_sha as string] : [mr.sha];
}
async mapSourceRepo(mr: MergeRequestSchema): Promise<GitRepository> {
const project: ProjectSchema = await this.getProject(mr.source_project_id);
return {
owner: project.namespace.full_path, // or just proj.path?
project: project.path,
cloneUrl: project.http_url_to_repo,
};
}
async mapTargetRepo(mr: MergeRequestSchema): Promise<GitRepository> {
const project: ProjectSchema = await this.getProject(mr.target_project_id);
return {
owner: project.namespace.full_path, // or just proj.path?
project: project.path,
cloneUrl: project.http_url_to_repo,
};
}
private isMerged(mr: MergeRequestSchema) {
return this.mapGitState(mr.state) === GitRepoState.MERGED;
}
private async getProject(projectId: number): Promise<ProjectSchema> {
const { data } = await this.client.get(`/projects/${projectId}`);
if (!data) {
throw new Error(`Project ${projectId} not found`);
}
return data as ProjectSchema;
}
}

View file

@ -3,30 +3,50 @@ import LoggerService from "@bp/service/logger/logger-service";
export default class ConsoleLoggerService implements LoggerService {
private readonly logger;
private readonly logger: Logger;
private readonly verbose: boolean;
private context?: string;
constructor() {
constructor(verbose = true) {
this.logger = new Logger();
this.verbose = verbose;
}
setContext(newContext: string) {
this.context = newContext;
}
getContext(): string | undefined {
return this.context;
}
clearContext() {
this.context = undefined;
}
trace(message: string): void {
this.logger.log("[TRACE]", message);
this.logger.log("TRACE", this.fromContext(message));
}
debug(message: string): void {
this.logger.log("[DEBUG]", message);
if (this.verbose) {
this.logger.log("DEBUG", this.fromContext(message));
}
}
info(message: string): void {
this.logger.log("[INFO]", message);
this.logger.log("INFO", this.fromContext(message));
}
warn(message: string): void {
this.logger.log("[WARN]", message);
this.logger.log("WARN", this.fromContext(message));
}
error(message: string): void {
this.logger.log("[ERROR]", message);
this.logger.log("ERROR", this.fromContext(message));
}
private fromContext(msg: string): string {
return this.context ? `[${this.context}] ${msg}` : msg;
}
}

View file

@ -3,6 +3,12 @@
*/
export default interface LoggerService {
setContext(newContext: string): void;
getContext(): string | undefined;
clearContext(): void;
trace(message: string): void;
debug(message: string): void;

View file

@ -6,7 +6,7 @@
log(prefix: string, ...str: string[]) {
// eslint-disable-next-line no-console
console.log.apply(console, [prefix, ...str]);
console.log.apply(console, [`[${prefix.padEnd(5)}]`, ...str]);
}
emptyLine() {

View 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);
};

View file

@ -3,11 +3,19 @@ import { Args } from "@bp/service/args/args.types";
import { Configs } from "@bp/service/configs/configs.types";
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
import GitCLIService from "@bp/service/git/git-cli";
import GitService from "@bp/service/git/git-service";
import GitServiceFactory from "@bp/service/git/git-service-factory";
import { BackportPullRequest, GitPullRequest, GitServiceType } from "@bp/service/git/git.types";
import GitClient from "@bp/service/git/git-client";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { BackportPullRequest, GitClientType, GitPullRequest } from "@bp/service/git/git.types";
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;
gitClientApi: GitClient;
gitCli: GitCLIService;
}
/**
* Main runner implementation, it implements the core logic flow
@ -22,22 +30,6 @@ export default class Runner {
this.argsParser = parser;
}
/**
* Infer the remote GIT service to interact with based on the provided
* pull request URL
* @param prUrl provided pull request URL
* @returns {GitServiceType}
*/
private inferRemoteGitService(prUrl: string): GitServiceType {
const stdPrUrl = prUrl.toLowerCase().trim();
if (stdPrUrl.includes(GitServiceType.GITHUB.toString())) {
return GitServiceType.GITHUB;
}
throw new Error(`Remote GIT service not recognixed from PR url: ${prUrl}`);
}
/**
* Entry point invoked by the command line or gha
*/
@ -45,13 +37,13 @@ export default class Runner {
try {
await this.execute();
this.logger.info("Process succeeded!");
this.logger.info("Process succeeded");
process.exit(0);
} catch (error) {
this.logger.error(`${error}`);
this.logger.info("Process failed!");
this.logger.info("Process failed");
process.exit(1);
}
}
@ -65,61 +57,118 @@ export default class Runner {
const args: Args = this.argsParser.parse();
if (args.dryRun) {
this.logger.warn("Dry run enabled!");
this.logger.warn("Dry run enabled");
}
// 2. init git service
GitServiceFactory.getOrCreate(this.inferRemoteGitService(args.pullRequest), args.auth);
const gitApi: GitService = GitServiceFactory.getService();
let gitClientType: GitClientType;
if (args.gitClient === undefined) {
gitClientType = inferGitClient(args.pullRequest);
} else {
gitClientType = args.gitClient as GitClientType;
}
// the api version is ignored in case of github
const apiUrl = inferGitApiUrl(args.pullRequest, gitClientType === GitClientType.CODEBERG ? "v1" : undefined);
const token = this.fetchToken(args, gitClientType);
const gitApi: GitClient = GitClientFactory.getOrCreate(gitClientType, token, apiUrl);
// 3. parse configs
this.logger.debug("Parsing configs..");
args.auth = token; // override auth
const configs: Configs = await new PullRequestConfigsParser().parseAndValidate(args);
const originalPR: GitPullRequest = configs.originalPullRequest;
const backportPR: GitPullRequest = configs.backportPullRequest;
const backportPRs: BackportPullRequest[] = configs.backportPullRequests;
// start local git operations
const git: GitCLIService = new GitCLIService(configs.auth, configs.git);
const failures: string[] = [];
// we need sequential backporting as they will operate on the same folder
// avoid cloning the same repo multiple times
for(const pr of backportPRs) {
try {
await this.executeBackport(configs, pr, {
gitClientType: gitClientType,
gitClientApi: gitApi,
gitCli: git,
});
} catch(error) {
this.logger.error(`Something went wrong backporting to ${pr.base}: ${error}`);
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);
}
}
if (failures.length > 0) {
throw new Error(`Failure occurred during one of the backports: [${failures.join(" ; ")}]`);
}
}
/**
* Fetch the GIT token from the provided Args obj, if not empty, otherwise fallback
* to the environment variables.
* @param args input arguments
* @param gitType git client type
* @returns the provided or fetched token, or undefined if not set anywhere
*/
fetchToken(args: Args, gitType: GitClientType): string | undefined {
let token = args.auth;
if (token === undefined) {
// try to fetch the auth from env variable
this.logger.info("Auth argument not provided, checking available tokens from env..");
token = getGitTokenFromEnv(gitType);
if (!token) {
this.logger.info("Git token not found in the environment");
}
}
return token;
}
async executeBackport(configs: Configs, backportPR: BackportPullRequest, git: Git): Promise<void> {
this.logger.setContext(backportPR.base);
const originalPR: GitPullRequest = configs.originalPullRequest;
// 4. clone the repository
await git.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, configs.targetBranch);
this.logger.debug("Cloning repo..");
await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base);
// 5. create new branch from target one and checkout
const backportBranch = backportPR.branchName ?? `bp-${configs.targetBranch}-${originalPR.commits!.join("-")}`;
await git.createLocalBranch(configs.folder, backportBranch);
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") {
await git.fetch(configs.folder, `pull/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`);
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}`);
}
// 7. apply all changes to the new branch
for (const sha of originalPR.commits!) {
await git.cherryPick(configs.folder, sha);
this.logger.debug("Cherry picking commits..");
for (const sha of originalPR.commits) {
await git.gitCli.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption, configs.cherryPickOptions);
}
const backport: BackportPullRequest = {
owner: originalPR.targetRepo.owner,
repo: originalPR.targetRepo.project,
head: backportBranch,
base: configs.targetBranch,
title: backportPR.title,
body: backportPR.body,
reviewers: backportPR.reviewers,
assignees: backportPR.assignees,
};
if (!configs.dryRun) {
// 8. push the new branch to origin
await git.push(configs.folder, backportBranch);
await git.gitCli.push(configs.folder, backportPR.head);
// 9. create pull request new branch -> target branch (using octokit)
await gitApi.createPullRequest(backport);
const prUrl = await git.gitClientApi.createPullRequest(backportPR);
this.logger.info(`Pull request created: ${prUrl}`);
} else {
this.logger.warn("Pull request creation and remote push skipped!");
this.logger.info(`${JSON.stringify(backport, null, 2)}`);
this.logger.warn("Pull request creation and remote push skipped");
this.logger.info(`${JSON.stringify(backportPR, null, 2)}`);
}
}
}
this.logger.clearContext();
}
}

View file

@ -0,0 +1,78 @@
import { getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, getOrUndefined, parseArgs, readConfigFile } from "@bp/service/args/args-utils";
import { createTestFile, expectArrayEqual, removeTestFile, spyGetInput } from "../../support/utils";
import { getInput } from "@actions/core";
const RANDOM_CONFIG_FILE_CONTENT_PATHNAME = "./args-utils-test-random-config-file.json";
const RANDOM_CONFIG_FILE_CONTENT = {
"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 <original-pr-link>",
"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);
});
test("gha getOrUndefined", () => {
spyGetInput({
"present": "value",
"empty": "",
});
expect(getOrUndefined(getInput("empty"))).toStrictEqual(undefined);
expect(getOrUndefined(getInput("present"))).toStrictEqual("value");
});
test("gha getAsCleanedCommaSeparatedList", () => {
spyGetInput({
"present": "value1, value2 , value3",
"empty": "",
"blank": " ",
"inner": " inner spaces ",
});
expectArrayEqual(getAsCleanedCommaSeparatedList(getInput("present"))!, ["value1", "value2", "value3"]);
expect(getAsCleanedCommaSeparatedList(getInput("empty"))).toStrictEqual(undefined);
expect(getAsCleanedCommaSeparatedList(getInput("blank"))).toStrictEqual(undefined);
expect(getAsCleanedCommaSeparatedList(getInput("inner"))).toStrictEqual(["innerspaces"]);
});
test("gha getAsCommaSeparatedList", () => {
spyGetInput({
"present": "value1, value2 , value3",
"empty": "",
"blank": " ",
"inner": " inner spaces ",
});
expectArrayEqual(getAsCommaSeparatedList(getInput("present"))!, ["value1", "value2", "value3"]);
expect(getAsCommaSeparatedList(getInput("empty"))).toStrictEqual(undefined);
expect(getAsCommaSeparatedList(getInput("blank"))).toStrictEqual(undefined);
expectArrayEqual(getAsCommaSeparatedList(getInput("inner"))!, ["inner spaces"]);
});
});

View file

@ -1,16 +1,55 @@
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",
"gitClient": "codeberg",
"gitUser": "YourGitUser",
"gitEmail": "your-email@example.com",
"title": "Backport: Original PR Title",
"body": "Backport: Original PR Body",
"bodyPrefix": "backport <original-pr-link>",
"bpBranchName": "backport-branch-name",
"reviewers": ["reviewer1", "reviewer2"],
"assignees": ["assignee1", "assignee2"],
"inheritReviewers": true,
"labels": ["cherry-pick :cherries:"],
"inheritLabels": 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
parser = new CLIArgsParser();
// reset process.env variables
resetProcessArgs();
// create a fresh new instance every time
parser = new CLIArgsParser();
});
test("valid execution [default, short]", () => {
@ -23,9 +62,10 @@ 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.gitClient).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");
@ -36,6 +76,46 @@ describe("cli args parser", () => {
expect(args.reviewers).toEqual([]);
expect(args.assignees).toEqual([]);
expect(args.inheritReviewers).toEqual(true);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
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]", () => {
addProcessArgs([
"-cf",
SIMPLE_CONFIG_FILE_CONTENT_PATHNAME,
]);
const args: Args = parser.parse();
expect(args.dryRun).toEqual(false);
expect(args.auth).toEqual(undefined);
expect(args.gitClient).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);
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]", () => {
@ -48,9 +128,10 @@ 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.gitClient).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");
@ -61,6 +142,42 @@ describe("cli args parser", () => {
expect(args.reviewers).toEqual([]);
expect(args.assignees).toEqual([]);
expect(args.inheritReviewers).toEqual(true);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
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.gitClient).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);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
test("valid execution [override, short]", () => {
@ -81,6 +198,7 @@ describe("cli args parser", () => {
const args: Args = parser.parse();
expect(args.dryRun).toEqual(true);
expect(args.auth).toEqual("bearer-token");
expect(args.gitClient).toEqual(undefined);
expect(args.gitUser).toEqual("Me");
expect(args.gitEmail).toEqual("me@email.com");
expect(args.folder).toEqual(undefined);
@ -93,6 +211,12 @@ describe("cli args parser", () => {
expect(args.reviewers).toEqual([]);
expect(args.assignees).toEqual([]);
expect(args.inheritReviewers).toEqual(true);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
test("valid execution [override, long]", () => {
@ -104,6 +228,8 @@ describe("cli args parser", () => {
"target",
"--pull-request",
"https://localhost/whatever/pulls/1",
"--git-client",
"codeberg",
"--git-user",
"Me",
"--git-email",
@ -121,11 +247,15 @@ describe("cli args parser", () => {
"--assignees",
" pippo,pluto, paperino",
"--no-inherit-reviewers",
"--labels",
"cherry-pick :cherries:, another spaced label",
"--inherit-labels",
]);
const args: Args = parser.parse();
expect(args.dryRun).toEqual(true);
expect(args.auth).toEqual("bearer-token");
expect(args.gitClient).toEqual("codeberg");
expect(args.gitUser).toEqual("Me");
expect(args.gitEmail).toEqual("me@email.com");
expect(args.folder).toEqual(undefined);
@ -135,9 +265,277 @@ 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);
expectArrayEqual(args.labels!, ["cherry-pick :cherries:", "another spaced label"]);
expect(args.inheritLabels).toEqual(true);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
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.gitClient).toEqual("codeberg");
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 <original-pr-link>");
expect(args.bpBranchName).toEqual("backport-branch-name");
expectArrayEqual(args.reviewers!, ["reviewer1", "reviewer2"]);
expectArrayEqual(args.assignees!,["assignee1", "assignee2"]);
expect(args.inheritReviewers).toEqual(true);
expectArrayEqual(args.labels!, ["cherry-pick :cherries:"]);
expect(args.inheritLabels).toEqual(true);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
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-client",
"github",
"--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",
"--labels",
"cherry-pick :cherries:, another spaced label",
"--inherit-labels",
]);
const args: Args = parser.parse();
expect(args.dryRun).toEqual(true);
expect(args.auth).toEqual("your-git-service-auth-token");
expect(args.gitClient).toEqual("codeberg");
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 <original-pr-link>");
expect(args.bpBranchName).toEqual("backport-branch-name");
expectArrayEqual(args.reviewers!, ["reviewer1", "reviewer2"]);
expectArrayEqual(args.assignees!,["assignee1", "assignee2"]);
expect(args.inheritReviewers).toEqual(true);
expectArrayEqual(args.labels!, ["cherry-pick :cherries:"]);
expect(args.inheritLabels).toEqual(true);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
test("override squash to false", () => {
addProcessArgs([
"--target-branch",
"target",
"--pull-request",
"https://localhost/whatever/pulls/1",
"--no-squash"
]);
const args: Args = parser.parse();
expect(args.dryRun).toEqual(false);
expect(args.auth).toEqual(undefined);
expect(args.gitClient).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);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(false);
});
test("override cherry pick strategies and options", () => {
addProcessArgs([
"--target-branch",
"target",
"--pull-request",
"https://localhost/whatever/pulls/1",
"--strategy",
"ort",
"--strategy-option",
"ours",
"--cherry-pick-options",
"--allow-empty -x",
]);
const args: Args = parser.parse();
expect(args.dryRun).toEqual(false);
expect(args.auth).toEqual(undefined);
expect(args.gitClient).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);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual("ort");
expect(args.strategyOption).toEqual("ours");
expect(args.cherryPickOptions).toEqual("--allow-empty -x");
});
test("additional pr comments", () => {
addProcessArgs([
"--target-branch",
"target",
"--pull-request",
"https://localhost/whatever/pulls/1",
"--comments",
"first comment;second comment",
]);
const args: Args = parser.parse();
expect(args.dryRun).toEqual(false);
expect(args.auth).toEqual(undefined);
expect(args.gitClient).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);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expectArrayEqual(args.comments!,["first comment", "second comment"]);
});
test("valid execution with multiple branches", () => {
addProcessArgs([
"-tb",
"target, old",
"-pr",
"https://localhost/whatever/pulls/1"
]);
const args: Args = parser.parse();
expect(args.dryRun).toEqual(false);
expect(args.auth).toEqual(undefined);
expect(args.gitClient).toEqual(undefined);
expect(args.gitUser).toEqual(undefined);
expect(args.gitEmail).toEqual(undefined);
expect(args.folder).toEqual(undefined);
expect(args.targetBranch).toEqual("target, old");
expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1");
expect(args.title).toEqual(undefined);
expect(args.body).toEqual(undefined);
expect(args.bodyPrefix).toEqual(undefined);
expect(args.bpBranchName).toEqual(undefined);
expect(args.reviewers).toEqual([]);
expect(args.assignees).toEqual([]);
expect(args.inheritReviewers).toEqual(true);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
test("invalid execution with empty target branch", () => {
addProcessArgs([
"-tb",
" ",
"-pr",
"https://localhost/whatever/pulls/1"
]);
expect(() => parser.parse()).toThrowError("Missing option: target branch(es) or target regular expression must be provided");
});
test("invalid execution with missing mandatory target branch", () => {
addProcessArgs([
"-pr",
"https://localhost/whatever/pulls/1"
]);
expect(() => parser.parse()).toThrowError("Missing option: target branch(es) or target regular expression must be provided");
});
test("invalid execution with missing mandatory pull request", () => {
addProcessArgs([
"-tb",
"target",
]);
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);
});
});

View file

@ -1,47 +1,53 @@
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 <original-pr-link>",
"bpBranchName": "backport-branch-name",
"reviewers": ["reviewer1", "reviewer2"],
"assignees": ["assignee1", "assignee2"],
"inheritReviewers": true,
"labels": ["cherry-pick :cherries:"],
"inheritLabels": 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();
});
afterEach(() => {
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",
"empty": "",
});
expect(parser.getOrUndefined("empty")).toStrictEqual(undefined);
expect(parser.getOrUndefined("present")).toStrictEqual("value");
});
test("getAsCommaSeparatedList", () => {
spyGetInput({
"present": "value1, value2 , value3",
"empty": "",
"blank": " ",
});
expectArrayEqual(parser.getAsCommaSeparatedList("present")!, ["value1", "value2", "value3"]);
expect(parser.getAsCommaSeparatedList("empty")).toStrictEqual([]);
expect(parser.getAsCommaSeparatedList("blank")).toStrictEqual([]);
});
test("valid execution [default]", () => {
spyGetInput({
"target-branch": "target",
@ -50,9 +56,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");
@ -61,6 +67,12 @@ describe("gha args parser", () => {
expect(args.reviewers).toEqual([]);
expect(args.assignees).toEqual([]);
expect(args.inheritReviewers).toEqual(true);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
test("valid execution [override]", () => {
@ -78,6 +90,8 @@ describe("gha args parser", () => {
"reviewers": "al , john, jack",
"assignees": " pippo,pluto, paperino",
"no-inherit-reviewers": "true",
"labels": "cherry-pick :cherries:, another spaced label",
"inherit-labels": "true"
});
const args: Args = parser.parse();
@ -92,9 +106,228 @@ 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);
expectArrayEqual(args.labels!, ["cherry-pick :cherries:", "another spaced label"]);
expect(args.inheritLabels).toEqual(true);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
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);
expectArrayEqual(args.labels!, []);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
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",
"labels": "cherry-pick :cherries:, another spaced label",
"inherit-labels": "false"
});
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 <original-pr-link>");
expect(args.bpBranchName).toEqual("backport-branch-name");
expectArrayEqual(args.reviewers!, ["reviewer1", "reviewer2"]);
expectArrayEqual(args.assignees!,["assignee1", "assignee2"]);
expect(args.inheritReviewers).toEqual(true);
expectArrayEqual(args.labels!, ["cherry-pick :cherries:"]);
expect(args.inheritLabels).toEqual(true);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
test("override squash to false", () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://localhost/whatever/pulls/1",
"no-squash": "true",
});
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.reviewers).toEqual([]);
expect(args.assignees).toEqual([]);
expect(args.inheritReviewers).toEqual(true);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(false);
});
test("override cherry pick strategy", () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://localhost/whatever/pulls/1",
"strategy": "ort",
"strategy-option": "ours",
});
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.reviewers).toEqual([]);
expect(args.assignees).toEqual([]);
expect(args.inheritReviewers).toEqual(true);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual("ort");
expect(args.strategyOption).toEqual("ours");
expect(args.cherryPickOptions).toEqual(undefined);
});
test("additional pr comments", () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://localhost/whatever/pulls/1",
"comments": "first comment;second comment",
});
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.reviewers).toEqual([]);
expect(args.assignees).toEqual([]);
expect(args.inheritReviewers).toEqual(true);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expectArrayEqual(args.comments!,["first comment", "second comment"]);
});
test("valid execution with multiple branches", () => {
spyGetInput({
"target-branch": "target,old",
"pull-request": "https://localhost/whatever/pulls/1"
});
const args: Args = parser.parse();
expect(args.dryRun).toEqual(false);
expect(args.auth).toEqual(undefined);
expect(args.gitUser).toEqual(undefined);
expect(args.gitEmail).toEqual(undefined);
expect(args.folder).toEqual(undefined);
expect(args.targetBranch).toEqual("target,old");
expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1");
expect(args.title).toEqual(undefined);
expect(args.body).toEqual(undefined);
expect(args.reviewers).toEqual([]);
expect(args.assignees).toEqual([]);
expect(args.inheritReviewers).toEqual(true);
expect(args.labels).toEqual([]);
expect(args.inheritLabels).toEqual(false);
expect(args.squash).toEqual(true);
expect(args.strategy).toEqual(undefined);
expect(args.strategyOption).toEqual(undefined);
expect(args.cherryPickOptions).toEqual(undefined);
});
test("invalid execution with empty target branch", () => {
spyGetInput({
"target-branch": " ",
"pull-request": "https://localhost/whatever/pulls/1"
});
expect(() => parser.parse()).toThrowError("Missing option: target branch(es) or target regular expression must be provided");
});
test("invalid execution with missing mandatory target branch", () => {
spyGetInput({
"pull-request": "https://localhost/whatever/pulls/1"
});
expect(() => parser.parse()).toThrowError("Missing option: target branch(es) or target regular expression must be provided");
});
test("invalid execution with missin mandatory pull request", () => {
spyGetInput({
"target-branch": "target,old",
});
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);
});
});

View file

@ -0,0 +1,493 @@
import { Args } from "@bp/service/args/args.types";
import { Configs } from "@bp/service/configs/configs.types";
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
import { mockGitHubClient } from "../../../support/mock/git-client-mock-support";
import { resetProcessArgs } from "../../../support/utils";
import { MERGED_PR_FIXTURE, REPO, TARGET_OWNER, MULT_COMMITS_PR_FIXTURE } from "../../../support/mock/github-data";
import GitHubMapper from "@bp/service/git/github/github-mapper";
import GitHubClient from "@bp/service/git/github/github-client";
jest.spyOn(GitHubMapper.prototype, "mapPullRequest");
jest.spyOn(GitHubClient.prototype, "getPullRequest");
describe("github pull request config parser", () => {
const mergedPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`;
const multipleCommitsPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MULT_COMMITS_PR_FIXTURE.number}`;
let configParser: PullRequestConfigsParser;
beforeAll(() => {
GitClientFactory.reset();
GitClientFactory.getOrCreate(GitClientType.GITHUB, "whatever", "http://localhost/api/v3");
});
beforeEach(() => {
// reset process.env variables
resetProcessArgs();
// mock octokit
mockGitHubClient("http://localhost/api/v3");
// create a fresh new instance every time
configParser = new PullRequestConfigsParser();
});
test("multiple backports", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, undefined);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "owner",
repo: "reponame",
head: "bp-v1-28f63db",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v2-28f63db",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v3-28f63db",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
])
);
});
test("multiple backports ignore duplicates", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, undefined);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "owner",
repo: "reponame",
head: "bp-v1-28f63db",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v2-28f63db",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v3-28f63db",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
])
);
});
test("multiple backports with custom branch name", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom-branch",
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, undefined);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "owner",
repo: "reponame",
head: "custom-branch-v1",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "custom-branch-v2",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "custom-branch-v3",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
])
);
});
test("multiple backports with multiple custom branch names", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom-branch1, custom-branch2, custom-branch3",
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, undefined);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "owner",
repo: "reponame",
head: "custom-branch1",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "custom-branch2",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "custom-branch3",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
])
);
});
test("multiple backports with incorrect number of bp branch names", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom-branch1, custom-branch2",
};
await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("The number of backport branch names, if provided, must match the number of target branches or just one, provided 2 branch names instead");
});
test("multiple backports and multiple commits", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: multipleCommitsPRUrl,
targetBranch: "v4, v5, v6",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
squash: false,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 8632, false);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"]);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "owner",
repo: "reponame",
head: "bp-v4-0404fb9-11da4e3",
base: "v4",
title: "[v4] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v5-0404fb9-11da4e3",
base: "v5",
title: "[v5] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v6-0404fb9-11da4e3",
base: "v6",
title: "[v6] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
},
])
);
});
test("multiple extracted branches and multiple commits", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: multipleCommitsPRUrl,
targetBranchPattern: "^backport (?<target>([^ ]+))$",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
squash: false,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 8632, false);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"]);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "owner",
repo: "reponame",
head: "bp-v1-0404fb9-11da4e3",
base: "v1",
title: "[v1] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v2-0404fb9-11da4e3",
base: "v2",
title: "[v2] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
},
{
owner: "owner",
repo: "reponame",
head: "bp-v3-0404fb9-11da4e3",
base: "v3",
title: "[v3] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
},
])
);
});
});

View file

@ -0,0 +1,968 @@
import { Args } from "@bp/service/args/args.types";
import { Configs } from "@bp/service/configs/configs.types";
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
import { mockGitHubClient } from "../../../support/mock/git-client-mock-support";
import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../../support/utils";
import { MERGED_PR_FIXTURE, OPEN_PR_FIXTURE, NOT_MERGED_PR_FIXTURE, REPO, TARGET_OWNER, MULT_COMMITS_PR_FIXTURE } from "../../../support/mock/github-data";
import CLIArgsParser from "@bp/service/args/cli/cli-args-parser";
import GitHubMapper from "@bp/service/git/github/github-mapper";
import GitHubClient from "@bp/service/git/github/github-client";
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/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`,
};
const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT_PATHNAME = "./github-pr-configs-parser-complex-pr-merged.json";
const GITHUB_MERGED_PR_COMPLEX_CONFIG_FILE_CONTENT = {
"dryRun": false,
"auth": "my-auth-token",
"pullRequest": `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.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
"labels": ["cherry-pick :cherries:"],
"inheritLabels": true,
};
jest.spyOn(GitHubMapper.prototype, "mapPullRequest");
jest.spyOn(GitHubClient.prototype, "getPullRequest");
describe("github pull request config parser", () => {
const mergedPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`;
const openPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${OPEN_PR_FIXTURE.number}`;
const notMergedPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${NOT_MERGED_PR_FIXTURE.number}`;
const multipleCommitsPRUrl = `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MULT_COMMITS_PR_FIXTURE.number}`;
let argsParser: CLIArgsParser;
let configParser: PullRequestConfigsParser;
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(() => {
// reset process.env variables
resetProcessArgs();
// mock octokit
mockGitHubClient("http://localhost/api/v3");
// create a fresh new instance every time
argsParser = new CLIArgsParser();
configParser = new PullRequestConfigsParser();
});
test("parse configs from pull request", async () => {
const args: Args = {
dryRun: false,
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: 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.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
expect(configs.auth).toEqual(undefined);
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
labels: ["backport prod"],
targetRepo: {
owner: "owner",
project: "reponame",
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.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "bp-prod-28f63db",
base: "prod",
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: [],
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 () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: mergedPRUrl,
targetBranch: "prod",
folder: "/tmp/test",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.folder).toEqual("/tmp/test");
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
});
test("still open pull request", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: openPRUrl,
targetBranch: "prod",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 4444, undefined);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"]);
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
expect(configs.originalPullRequest).toEqual({
number: 4444,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/4444",
htmlUrl: "https://github.com/owner/reponame/pull/4444",
state: "open",
merged: false,
mergedBy: undefined,
title: "PR Title",
body: "Please review and merge",
reviewers: ["gh-user"],
assignees: [],
labels: [],
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: ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"],
});
});
test("closed pull request", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: notMergedPRUrl,
targetBranch: "prod",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged");
});
test("override backport pr data inheriting reviewers", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: [],
inheritReviewers: true,
bpBranchName: "custom-branch"
};
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.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
labels: ["backport prod"],
targetRepo: {
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
},
sourceRepo: {
owner: "fork",
project: "reponame",
cloneUrl: "https://github.com/fork/reponame.git"
},
bpBranchName: undefined,
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "custom-branch",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
});
test("override backport with empty bp branch name", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: [],
inheritReviewers: true,
bpBranchName: " "
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, undefined);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "bp-prod-28f63db",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
});
test("override backport pr reviewers and assignees", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
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
};
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.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
labels: ["backport prod"],
targetRepo: {
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
},
sourceRepo: {
owner: "fork",
project: "reponame",
cloneUrl: "https://github.com/fork/reponame.git"
},
bpBranchName: undefined,
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "bp-prod-28f63db",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
labels: [],
comments: [],
});
});
test("override backport pr empty reviewers", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
};
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.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
labels: ["backport prod"],
targetRepo: {
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
},
sourceRepo: {
owner: "fork",
project: "reponame",
cloneUrl: "https://github.com/fork/reponame.git"
},
bpBranchName: undefined,
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "bp-prod-28f63db",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
});
});
test("override backport pr custom labels with duplicates", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: ["custom-label", "backport prod"], // also include the one inherited
inheritLabels: 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.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
labels: ["backport prod"],
targetRepo: {
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
},
sourceRepo: {
owner: "fork",
project: "reponame",
cloneUrl: "https://github.com/fork/reponame.git"
},
bpBranchName: undefined,
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "bp-prod-28f63db",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: ["custom-label", "backport prod"],
comments: [],
});
});
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(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
expect(configs.auth).toEqual(undefined);
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
labels: ["backport prod"],
targetRepo: {
owner: "owner",
project: "reponame",
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.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "bp-prod-28f63db",
base: "prod",
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: [],
labels: [],
comments: [],
});
});
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(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, true);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("my-auth-token");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
labels: ["backport prod"],
targetRepo: {
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
},
sourceRepo: {
owner: "fork",
project: "reponame",
cloneUrl: "https://github.com/fork/reponame.git"
},
bpBranchName: undefined,
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "bp-prod-28f63db",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
labels: ["cherry-pick :cherries:", "backport prod"],
comments: [],
});
});
test("parse configs from pull request without squashing with multiple commits", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: multipleCommitsPRUrl,
targetBranch: "prod",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
squash: false,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 8632, false);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"]);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 8632,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/8632",
htmlUrl: "https://github.com/owner/reponame/pull/8632",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
labels: [
"backport v1",
"backport v2",
"backport v3",
],
targetRepo: {
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
},
sourceRepo: {
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
},
nCommits: 2,
commits: ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"]
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "bp-prod-0404fb9-11da4e3",
base: "prod",
title: "[prod] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
});
test("override backport pr with additional comments", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: ["First comment", "Second comment"],
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, undefined);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
labels: ["backport prod"],
targetRepo: {
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
},
sourceRepo: {
owner: "fork",
project: "reponame",
cloneUrl: "https://github.com/fork/reponame.git"
},
bpBranchName: undefined,
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "bp-prod-28f63db",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: ["First comment", "Second comment"],
});
});
test("no extracted target branches from pr labels due to wrong group name", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranchPattern: "^backport (?<wrong>([^ ]+))$",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: ["First comment", "Second comment"],
};
await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("Unable to extract target branches with regular expression");
});
test("extract target branches from pr labels", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranchPattern: "^backport (?<target>([^ ]+))$",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: ["First comment", "Second comment"],
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitHubClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.getPullRequest).toBeCalledWith("owner", "reponame", 2368, undefined);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
labels: ["backport prod"],
targetRepo: {
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
},
sourceRepo: {
owner: "fork",
project: "reponame",
cloneUrl: "https://github.com/fork/reponame.git"
},
bpBranchName: undefined,
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "owner",
repo: "reponame",
head: "bp-prod-28f63db",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: ["First comment", "Second comment"],
});
});
test("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."
});
});
});

View file

@ -0,0 +1,353 @@
import { Args } from "@bp/service/args/args.types";
import { Configs } from "@bp/service/configs/configs.types";
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
import { getAxiosMocked } from "../../../support/mock/git-client-mock-support";
import { MERGED_SQUASHED_MR } from "../../../support/mock/gitlab-data";
import GitLabClient from "@bp/service/git/gitlab/gitlab-client";
import GitLabMapper from "@bp/service/git/gitlab/gitlab-mapper";
jest.spyOn(GitLabMapper.prototype, "mapPullRequest");
jest.spyOn(GitLabClient.prototype, "getPullRequest");
jest.mock("axios", () => {
return {
create: jest.fn(() => ({
get: getAxiosMocked,
})),
};
});
describe("gitlab merge request config parser", () => {
const mergedPRUrl = `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${MERGED_SQUASHED_MR.iid}`;
let configParser: PullRequestConfigsParser;
beforeAll(() => {
GitClientFactory.reset();
GitClientFactory.getOrCreate(GitClientType.GITLAB, "whatever", "my.gitlab.host.com");
});
beforeEach(() => {
configParser = new PullRequestConfigsParser();
});
test("multiple backports", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v1-ebb1eca",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v2-ebb1eca",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v3-ebb1eca",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
])
);
});
test("multiple backports ignore duplicates", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3, v1",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v1-ebb1eca",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v2-ebb1eca",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "bp-v3-ebb1eca",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
])
);
});
test("multiple backports with custom branch name", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom-branch",
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "superuser",
repo: "backporting-example",
head: "custom-branch-v1",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "custom-branch-v2",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "custom-branch-v3",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
])
);
});
test("multiple backports with multiple custom branch names", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom1, custom2, custom3",
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.backportPullRequests.length).toEqual(3);
expect(configs.backportPullRequests).toEqual(
expect.arrayContaining([
{
owner: "superuser",
repo: "backporting-example",
head: "custom1",
base: "v1",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "custom2",
base: "v2",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
},
{
owner: "superuser",
repo: "backporting-example",
head: "custom3",
base: "v3",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
])
);
});
test("multiple backports with incorrect number of bp branch names", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "v1, v2, v3",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: [],
bpBranchName: "custom-branch1, custom-branch2, custom-branch2, custom-branch3, custom-branch4",
};
await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("The number of backport branch names, if provided, must match the number of target branches or just one, provided 4 branch names instead");
});
});

View file

@ -0,0 +1,911 @@
import { Args } from "@bp/service/args/args.types";
import { Configs } from "@bp/service/configs/configs.types";
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
import { getAxiosMocked } from "../../../support/mock/git-client-mock-support";
import { 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, resetEnvTokens, spyGetInput } from "../../../support/utils";
import GitLabClient from "@bp/service/git/gitlab/gitlab-client";
import GitLabMapper from "@bp/service/git/gitlab/gitlab-mapper";
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,
"labels": ["cherry-pick :cherries:"],
"inheritLabels": true,
};
jest.spyOn(GitLabMapper.prototype, "mapPullRequest");
jest.spyOn(GitLabClient.prototype, "getPullRequest");
jest.mock("axios", () => {
return {
create: jest.fn(() => ({
get: getAxiosMocked,
})),
};
});
describe("gitlab merge request config parser", () => {
const mergedPRUrl = `https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/${MERGED_SQUASHED_MR.iid}`;
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 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(() => {
// reset env tokens
resetEnvTokens();
argsParser = new GHAArgsParser();
configParser = new PullRequestConfigsParser();
});
test("parse configs from merge request", async () => {
const args: Args = {
dryRun: false,
auth: undefined,
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Gitlab",
gitEmail: "noreply@gitlab.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Gitlab",
email: "noreply@gitlab.com"
});
expect(configs.auth).toEqual(undefined);
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
labels: ["backport-prod"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-ebb1eca",
base: "prod",
title: "[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: [],
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 () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: mergedPRUrl,
targetBranch: "prod",
folder: "/tmp/test",
gitUser: "Gitlab",
gitEmail: "noreply@gitlab.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.folder).toEqual("/tmp/test");
expect(configs.git).toEqual({
user: "Gitlab",
email: "noreply@gitlab.com"
});
});
test("still open pull request", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: openPRUrl,
targetBranch: "prod",
gitUser: "Gitlab",
gitEmail: "noreply@gitlab.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 2, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.git).toEqual({
user: "Gitlab",
email: "noreply@gitlab.com"
});
expect(configs.originalPullRequest).toEqual({
number: 2,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
state: "open",
merged: false,
mergedBy: undefined,
title: "Update test.txt opened",
body: "Still opened mr body",
reviewers: ["superuser"],
assignees: ["superuser"],
labels: [],
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,
nCommits: 1,
// taken from mr.sha
commits: ["9e15674ebd48e05c6e428a1fa31dbb60a778d644"]
});
});
test("closed pull request", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: notMergedPRUrl,
targetBranch: "prod",
gitUser: "Gitlab",
gitEmail: "noreply@gitlab.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
squash: true,
};
await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged");
});
test("override backport pr data inheriting reviewers", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: [],
inheritReviewers: true,
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
labels: ["backport-prod"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-ebb1eca",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
});
});
test("override backport pr reviewers and assignees", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
inheritReviewers: true, // not taken into account
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
labels: ["backport-prod"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-ebb1eca",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
labels: [],
comments: [],
});
});
test("override backport pr empty reviewers", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
labels: ["backport-prod"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-ebb1eca",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
});
});
test("override backport pr custom labels with duplicates", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: ["custom-label", "backport-prod"], // also include the one inherited
inheritLabels: true,
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
labels: ["backport-prod"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-ebb1eca",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: ["custom-label", "backport-prod"],
comments: [],
});
});
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(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Gitlab",
email: "noreply@gitlab.com"
});
expect(configs.auth).toEqual(undefined);
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
labels: ["backport-prod"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-ebb1eca",
base: "prod",
title: "[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: [],
labels: [],
comments: [],
});
});
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(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("my-token");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
labels: ["backport-prod"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-ebb1eca",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: ["cherry-pick :cherries:", "backport-prod"],
comments: [],
});
});
test("still open pull request without squash", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: openPRUrl,
targetBranch: "prod",
gitUser: "Gitlab",
gitEmail: "noreply@gitlab.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
squash: false,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 2, false);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), ["e4dd336a4a20f394df6665994df382fb1d193a11", "974519f65c9e0ed65277cd71026657a09fca05e7"]);
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.git).toEqual({
user: "Gitlab",
email: "noreply@gitlab.com"
});
expect(configs.originalPullRequest).toEqual({
number: 2,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
state: "open",
merged: false,
mergedBy: undefined,
title: "Update test.txt opened",
body: "Still opened mr body",
reviewers: ["superuser"],
assignees: ["superuser"],
labels: [],
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,
nCommits: 2,
commits: ["e4dd336a4a20f394df6665994df382fb1d193a11", "974519f65c9e0ed65277cd71026657a09fca05e7"]
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-e4dd336-974519f",
base: "prod",
title: "[prod] Update test.txt opened",
body: "**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2\r\n\r\nStill opened mr body",
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
});
});
test("override backport pr with additional comments", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: ["First comment", "Second comment"],
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
labels: ["backport-prod"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-ebb1eca",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: ["First comment", "Second comment"],
});
});
test("extract target branches from pr labels", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranchPattern: "^backport-(?<target>([^ ]+))$",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
labels: [],
inheritLabels: false,
comments: ["First comment", "Second comment"],
squash: true,
};
const configs: Configs = await configParser.parseAndValidate(args);
expect(GitLabClient.prototype.getPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.getPullRequest).toBeCalledWith("superuser", "backporting-example", 1, true);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitLabMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
author: "superuser",
url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
htmlUrl: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
state: "merged",
merged: true,
mergedBy: "superuser",
title: "Update test.txt",
body: "This is the body",
reviewers: ["superuser1", "superuser2"],
assignees: ["superuser"],
labels: ["backport-prod"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequests.length).toEqual(1);
expect(configs.backportPullRequests[0]).toEqual({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-ebb1eca",
base: "prod",
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: ["First comment", "Second comment"],
});
});
test("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.",
});
});
});

View file

@ -1,440 +0,0 @@
import { Args } from "@bp/service/args/args.types";
import { Configs } from "@bp/service/configs/configs.types";
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
import GitServiceFactory from "@bp/service/git/git-service-factory";
import { GitServiceType } from "@bp/service/git/git.types";
import { setupMoctokit } from "../../../support/moctokit/moctokit-support";
import { mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, repo, targetOwner } from "../../../support/moctokit/moctokit-data";
describe("pull request config parser", () => {
const mergedPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`;
const openPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${openPullRequestFixture.number}`;
const notMergedPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${notMergedPullRequestFixture.number}`;
let parser: PullRequestConfigsParser;
beforeAll(() => {
GitServiceFactory.getOrCreate(GitServiceType.GITHUB, "whatever");
});
beforeEach(() => {
setupMoctokit();
parser = new PullRequestConfigsParser();
});
afterEach(() => {
jest.clearAllMocks();
});
test("parse configs from pull request", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
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\r\n\r\nPowered by [BPer](https://github.com/lampajr/backporting).",
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("override folder", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: mergedPRUrl,
targetBranch: "prod",
folder: "/tmp/test",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await parser.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: "GitHub",
email: "noreply@github.com"
});
});
test("override author", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
});
test("still open pull request", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: openPRUrl,
targetBranch: "prod",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.git).toEqual({
user: "GitHub",
email: "noreply@github.com"
});
expect(configs.originalPullRequest).toEqual({
number: 4444,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/4444",
htmlUrl: "https://github.com/owner/reponame/pull/4444",
state: "open",
merged: false,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["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,
// taken from head.sha
commits: ["91748965051fae1330ad58d15cf694e103267c87"]
});
});
test("closed pull request", async () => {
const args: Args = {
dryRun: true,
auth: "whatever",
pullRequest: notMergedPRUrl,
targetBranch: "prod",
gitUser: "GitHub",
gitEmail: "noreply@github.com",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
expect(async () => await parser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!");
});
test("override backport pr data inherting reviewers", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: [],
inheritReviewers: true,
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
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: ["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("override backport pr reviewers and assignees", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
inheritReviewers: true, // not taken into account
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
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,
});
});
test("override backport pr empty reviewers", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "Me",
gitEmail: "me@email.com",
title: "New Title",
body: "New Body",
bodyPrefix: "New Body Prefix -",
reviewers: [],
assignees: ["user3", "user4"],
inheritReviewers: false,
};
const configs: Configs = await parser.parseAndValidate(args);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
targetRepo: {
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
},
sourceRepo: {
owner: "fork",
project: "reponame",
cloneUrl: "https://github.com/fork/reponame.git"
},
bpBranchName: undefined,
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
title: "New Title",
body: "New Body Prefix -New Body",
reviewers: [],
assignees: ["user3", "user4"],
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,
});
});
});

View file

@ -114,4 +114,31 @@ describe("git cli service", () => {
const output = spawnSync("git", ["cherry", "-v"], { cwd }).stdout.toString();
expect(output.includes(expressionToTest)).toBe(false);
});
test("git clone on already created repo", async () => {
await git.clone("remote", cwd, "tbranch");
// use rev-parse to double check the current branch is the expected one
const post = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd }).stdout.toString().trim();
expect(post).toEqual("tbranch");
});
test("git clone set url with auth correctly for API token", async () => {
const git2 = new GitCLIService("api-token", {
user: "Backporting bot",
email: "bot@example.com",
});
const cwd2 = `${__dirname}/test-api-token`;
try {
await git2.clone(`file://${cwd}`, cwd2, "main");
const remoteURL = spawnSync("git", ["remote", "get-url", "origin"], { cwd: cwd2 }).stdout.toString().trim();
expect(remoteURL).toContain("api-token");
expect(remoteURL).not.toContain("Backporting bot");
} finally {
fs.rmSync(cwd2, { recursive: true, force: true });
}
});
});

View file

@ -0,0 +1,45 @@
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
import GitHubClient from "@bp/service/git/github/github-client";
import GitLabClient from "@bp/service/git/gitlab/gitlab-client";
describe("git client factory test", () => {
beforeEach(() => {
// reset git service
GitClientFactory.reset();
});
test("correctly create github client", () => {
const client = GitClientFactory.getOrCreate(GitClientType.GITHUB, "auth", "apiurl");
expect(client).toBeInstanceOf(GitHubClient);
});
test("correctly create gitlab client", () => {
const client = GitClientFactory.getOrCreate(GitClientType.GITLAB, "auth", "apiurl");
expect(client).toBeInstanceOf(GitLabClient);
});
test("correctly create codeberg client", () => {
const client = GitClientFactory.getOrCreate(GitClientType.CODEBERG, "auth", "apiurl");
expect(client).toBeInstanceOf(GitHubClient);
});
test("check get service github", () => {
const create = GitClientFactory.getOrCreate(GitClientType.GITHUB, "auth", "apiurl");
const get = GitClientFactory.getClient();
expect(create).toStrictEqual(get);
});
test("check get service gitlab", () => {
const create = GitClientFactory.getOrCreate(GitClientType.GITLAB, "auth", "apiurl");
const get = GitClientFactory.getClient();
expect(create).toStrictEqual(get);
});
test("check get service codeberg", () => {
const create = GitClientFactory.getOrCreate(GitClientType.CODEBERG, "auth", "apiurl");
const get = GitClientFactory.getClient();
expect(create).toStrictEqual(get);
});
});

View file

@ -0,0 +1,64 @@
import { inferGitApiUrl, inferGitClient, inferSquash } from "@bp/service/git/git-util";
import { GitClientType } from "@bp/service/git/git.types";
describe("check git utilities", () => {
test("check infer gitlab api", ()=> {
expect(inferGitApiUrl("https://my.gitlab.awesome.com/superuser/backporting-example/-/merge_requests/4")).toStrictEqual("https://my.gitlab.awesome.com/api/v4");
});
test("check infer gitlab api with different version", ()=> {
expect(inferGitApiUrl("http://my.gitlab.awesome.com/superuser/backporting-example/-/merge_requests/4", "v2")).toStrictEqual("http://my.gitlab.awesome.com/api/v2");
});
test("check infer github api", ()=> {
expect(inferGitApiUrl("https://github.com/superuser/backporting-example/pull/4")).toStrictEqual("https://api.github.com");
});
test("check infer custom github api", ()=> {
expect(inferGitApiUrl("http://github.acme-inc.com/superuser/backporting-example/pull/4")).toStrictEqual("http://github.acme-inc.com/api/v4");
});
test("check infer custom github api with different version", ()=> {
expect(inferGitApiUrl("http://github.acme-inc.com/superuser/backporting-example/pull/4", "v3")).toStrictEqual("http://github.acme-inc.com/api/v3");
});
test("check infer github api from github api url", ()=> {
expect(inferGitApiUrl("https://api.github.com/repos/owner/repo/pulls/1")).toStrictEqual("https://api.github.com");
});
test("check infer codeberg api", ()=> {
expect(inferGitApiUrl("https://codeberg.org/lampajr/backporting-example/pulls/1", "v1")).toStrictEqual("https://codeberg.org/api/v1");
});
test("check infer codeberg api", ()=> {
expect(inferGitApiUrl("https://codeberg.org/lampajr/backporting-example/pulls/1", undefined)).toStrictEqual("https://codeberg.org/api/v4");
});
test("check infer github client", ()=> {
expect(inferGitClient("https://github.com/superuser/backporting-example/pull/4")).toStrictEqual(GitClientType.GITHUB);
});
test("check infer gitlab client", ()=> {
expect(inferGitClient("https://my.gitlab.awesome.com/superuser/backporting-example/-/merge_requests/4")).toStrictEqual(GitClientType.GITLAB);
});
test("not recognized git client type", ()=> {
expect(() => inferGitClient("https://not.recognized/superuser/backporting-example/-/merge_requests/4")).toThrowError("Remote git service not recognized from pr url: https://not.recognized/superuser/backporting-example/-/merge_requests/4");
});
test("check infer github client using github api", ()=> {
expect(inferGitClient("https://api.github.com/repos/owner/repo/pulls/1")).toStrictEqual(GitClientType.GITHUB);
});
test("check infer codeberg client", ()=> {
expect(inferGitClient("https://codeberg.org/lampajr/backporting-example/pulls/1")).toStrictEqual(GitClientType.CODEBERG);
});
test("check inferSquash", ()=> {
expect(inferSquash(true, undefined)).toStrictEqual(false);
expect(inferSquash(false, "SHA")).toStrictEqual(true);
expect(inferSquash(false, undefined)).toStrictEqual(false);
expect(inferSquash(false, null)).toStrictEqual(false);
});
});

View file

@ -0,0 +1,41 @@
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitPullRequest, GitClientType } from "@bp/service/git/git.types";
import GitHubClient from "@bp/service/git/github/github-client";
import { MERGED_PR_FIXTURE, REPO, TARGET_OWNER } from "../../../support/mock/github-data";
import { mockGitHubClient } from "../../../support/mock/git-client-mock-support";
describe("github service", () => {
let gitClient: GitHubClient;
beforeAll(() => {
// init git service
GitClientFactory.reset();
GitClientFactory.getOrCreate(GitClientType.GITHUB, "whatever", "http://localhost/api/v3");
});
beforeEach(() => {
// mock github api calls
mockGitHubClient("http://localhost/api/v3");
gitClient = GitClientFactory.getClient() as GitHubClient;
});
test("get pull request: success", async () => {
const res: GitPullRequest = await gitClient.getPullRequest(TARGET_OWNER, REPO, MERGED_PR_FIXTURE.number, true);
expect(res.sourceRepo).toEqual({
owner: "fork",
project: "reponame",
cloneUrl: "https://github.com/fork/reponame.git"
});
expect(res.targetRepo).toEqual({
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
});
expect(res.title).toBe("PR Title");
expect(res.commits!.length).toBe(1);
expect(res.commits).toEqual(["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"]);
});
});

View file

@ -1,40 +0,0 @@
import GitServiceFactory from "@bp/service/git/git-service-factory";
import { GitPullRequest, GitServiceType } from "@bp/service/git/git.types";
import GitHubService from "@bp/service/git/github/github-service";
import { mergedPullRequestFixture, repo, targetOwner } from "../../../support/moctokit/moctokit-data";
import { setupMoctokit } from "../../../support/moctokit/moctokit-support";
describe("github service", () => {
let gitService: GitHubService;
beforeAll(() => {
// init git service
GitServiceFactory.getOrCreate(GitServiceType.GITHUB, "whatever");
});
beforeEach(() => {
// mock github api calls
setupMoctokit();
gitService = GitServiceFactory.getService() as GitHubService;
});
test("get pull request: success", async () => {
const res: GitPullRequest = await gitService.getPullRequest(targetOwner, repo, mergedPullRequestFixture.number);
expect(res.sourceRepo).toEqual({
owner: "fork",
project: "reponame",
cloneUrl: "https://github.com/fork/reponame.git"
});
expect(res.targetRepo).toEqual({
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
});
expect(res.title).toBe("PR Title");
expect(res.commits!.length).toBe(1);
expect(res.commits).toEqual(["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"]);
});
});

View file

@ -0,0 +1,351 @@
import GitClientFactory from "@bp/service/git/git-client-factory";
import { NEW_GITLAB_MR_ID, SECOND_NEW_GITLAB_MR_ID, getAxiosMocked, postAxiosMocked, putAxiosMocked } from "../../../support/mock/git-client-mock-support";
import { BackportPullRequest, GitClientType, GitPullRequest } from "@bp/service/git/git.types";
import GitLabClient from "@bp/service/git/gitlab/gitlab-client";
import axios from "axios";
jest.mock("axios");
const axiosSpy = axios.create as jest.Mock;
let axiosInstanceSpy: {[key: string]: jest.Func};
function setupAxiosSpy() {
const getSpy = jest.fn(getAxiosMocked);
const postSpy = jest.fn(postAxiosMocked);
const putSpy = jest.fn(putAxiosMocked);
const axiosInstance = {
get: getSpy,
post: postSpy,
put: putSpy,
};
axiosSpy.mockImplementation(() => (axiosInstance));
return axiosInstance;
}
describe("github service", () => {
let gitClient: GitLabClient;
beforeEach(() => {
axiosInstanceSpy = setupAxiosSpy();
GitClientFactory.reset();
gitClient = GitClientFactory.getOrCreate(GitClientType.GITLAB, "whatever", "apiUrl") as GitLabClient;
});
test("get merged pull request", async () => {
const res: GitPullRequest = await gitClient.getPullRequest("superuser", "backporting-example", 1, true);
// check content
expect(res.sourceRepo).toEqual({
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
});
expect(res.targetRepo).toEqual({
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
});
expect(res.title).toBe("Update test.txt");
expect(res.commits!.length).toBe(1);
expect(res.commits).toEqual(["ebb1eca696c42fd067658bd9b5267709f78ef38e"]);
// check axios invocation
expect(axiosInstanceSpy.get).toBeCalledTimes(3); // merge request and 2 repos
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/1");
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/76316");
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/76316");
});
test("get open pull request", async () => {
const res: GitPullRequest = await gitClient.getPullRequest("superuser", "backporting-example", 2, true);
expect(res.sourceRepo).toEqual({
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
});
expect(res.targetRepo).toEqual({
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
});
expect(res.title).toBe("Update test.txt opened");
expect(res.commits!.length).toBe(1);
expect(res.commits).toEqual(["9e15674ebd48e05c6e428a1fa31dbb60a778d644"]);
// check axios invocation
expect(axiosInstanceSpy.get).toBeCalledTimes(3); // merge request and 2 repos
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/2");
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/76316");
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/76316");
});
test("create backport pull request without reviewers and assignees", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch",
reviewers: [],
assignees: [],
labels: [],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(1);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(0); // no reviewers nor assignees
expect(axiosInstanceSpy.put).toBeCalledTimes(0); // no reviewers nor assignees
});
test("create backport pull request with reviewers", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch",
reviewers: ["superuser", "invalid"],
assignees: [],
labels: [],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(1);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(2); // just reviewers, one invalid
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=superuser");
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=invalid");
expect(axiosInstanceSpy.put).toBeCalledTimes(1); // just reviewers
expect(axiosInstanceSpy.put).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + NEW_GITLAB_MR_ID, {
reviewer_ids: [14041],
});
});
test("create backport pull request with assignees", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch",
reviewers: [],
assignees: ["superuser", "invalid"],
labels: [],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(1);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(2); // just assignees, one invalid
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=superuser");
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=invalid");
expect(axiosInstanceSpy.put).toBeCalledTimes(1); // just assignees
expect(axiosInstanceSpy.put).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + NEW_GITLAB_MR_ID, {
assignee_ids: [14041],
});
});
test("create backport pull request with failure assigning reviewers", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch-2",
reviewers: ["superuser", "invalid"],
assignees: [],
labels: [],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + SECOND_NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(1);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch-2",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(2); // just reviewers, one invalid
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=superuser");
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=invalid");
expect(axiosInstanceSpy.put).toBeCalledTimes(1); // just reviewers
expect(axiosInstanceSpy.put).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + SECOND_NEW_GITLAB_MR_ID, {
reviewer_ids: [14041],
});
});
test("create backport pull request with failure assigning assignees", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch-2",
reviewers: [],
assignees: ["superuser", "invalid"],
labels: [],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + SECOND_NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(1);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch-2",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(2); // just assignees, one invalid
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=superuser");
expect(axiosInstanceSpy.get).toBeCalledWith("/users?username=invalid");
expect(axiosInstanceSpy.put).toBeCalledTimes(1); // just assignees
expect(axiosInstanceSpy.put).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + SECOND_NEW_GITLAB_MR_ID, {
assignee_ids: [14041],
});
});
test("create backport pull request with custom labels", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch-2",
reviewers: [],
assignees: [],
labels: ["label1", "label2"],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + SECOND_NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(1);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch-2",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(0);
expect(axiosInstanceSpy.put).toBeCalledTimes(1); // just labels
expect(axiosInstanceSpy.put).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + SECOND_NEW_GITLAB_MR_ID, {
labels: "label1,label2",
});
});
test("create backport pull request with post comments", async () => {
const backport: BackportPullRequest = {
title: "Backport Title",
body: "Backport Body",
owner: "superuser",
repo: "backporting-example",
base: "old/branch",
head: "bp-branch-2",
reviewers: [],
assignees: [],
labels: [],
comments: ["this is first comment", "this is second comment"],
};
const url: string = await gitClient.createPullRequest(backport);
expect(url).toStrictEqual("https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + SECOND_NEW_GITLAB_MR_ID);
// check axios invocation
expect(axiosInstanceSpy.post).toBeCalledTimes(3); // also comments
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests", expect.objectContaining({
source_branch: "bp-branch-2",
target_branch: "old/branch",
title: "Backport Title",
description: "Backport Body",
reviewer_ids: [],
assignee_ids: [],
}));
expect(axiosInstanceSpy.get).toBeCalledTimes(0);
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + SECOND_NEW_GITLAB_MR_ID + "/notes", {
body: "this is first comment",
});
expect(axiosInstanceSpy.post).toBeCalledWith("/projects/superuser%2Fbackporting-example/merge_requests/" + SECOND_NEW_GITLAB_MR_ID + "/notes", {
body: "this is second comment",
});
});
test("get pull request for nested namespaces", async () => {
const res: GitPullRequest = await gitClient.getPullRequestFromUrl("https://my.gitlab.host.com/mysuperorg/6/mysuperproduct/mysuperunit/backporting-example/-/merge_requests/4", true);
// check content
expect(res.sourceRepo).toEqual({
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/mysuperorg/6/mysuperproduct/mysuperunit/backporting-example.git"
});
expect(res.targetRepo).toEqual({
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/mysuperorg/6/mysuperproduct/mysuperunit/backporting-example.git"
});
expect(res.title).toBe("Update test.txt");
expect(res.commits!.length).toBe(1);
expect(res.commits).toEqual(["ebb1eca696c42fd067658bd9b5267709f78ef38e"]);
// check axios invocation
expect(axiosInstanceSpy.get).toBeCalledTimes(3); // merge request and 2 repos
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/mysuperorg%2F6%2Fmysuperproduct%2Fmysuperunit%2Fbackporting-example/merge_requests/4");
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/1645");
expect(axiosInstanceSpy.get).toBeCalledWith("/projects/1645");
});
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,705 @@
import ArgsParser from "@bp/service/args/args-parser";
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, createTestFile, removeTestFile, resetEnvTokens, resetProcessArgs } from "../../support/utils";
import { getAxiosMocked } from "../../support/mock/git-client-mock-support";
import { MERGED_SQUASHED_MR } from "../../support/mock/gitlab-data";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
import { AuthTokenId } from "@bp/service/configs/configs.types";
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,
"labels": ["cli gitlab cherry pick :cherries:"],
"inheritLabels": true,
};
jest.mock("axios", () => {
return {
create: () => ({
get: getAxiosMocked,
post: () => ({
data: {
iid: 1, // FIXME: I am not testing this atm
}
}),
put: async () => undefined,
}),
};
});
jest.mock("@bp/service/git/git-cli");
jest.spyOn(GitLabClient.prototype, "createPullRequest");
jest.spyOn(GitLabClient.prototype, "createPullRequestComment");
jest.spyOn(GitClientFactory, "getOrCreate");
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(() => {
// reset process.env variables
resetProcessArgs();
// reset git env tokens
resetEnvTokens();
// create CLI arguments parser
parser = new CLIArgsParser();
// create runner
runner = new Runner(parser);
});
describe("cli runner", () => {
test("with dry run", async () => {
addProcessArgs([
"-d",
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-9e15674");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644", undefined, undefined, undefined);
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 () => {
addProcessArgs([
"-d",
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"-f",
"folder"
]);
await runner.execute();
const cwd = process.cwd() + "/folder";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-9e15674");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644", undefined, undefined, undefined);
expect(GitCLIService.prototype.addRemote).toBeCalledTimes(0);
expect(GitCLIService.prototype.addRemote).toBeCalledTimes(0);
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(0);
});
test("without dry run", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-9e15674");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-9e15674");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-9e15674",
base: "target",
title: "[target] Update test.txt opened",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"),
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
test("closed and not merged pull request", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/3"
]);
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 () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-ebb1eca");
// 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", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-ebb1eca",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"),
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
expect(GitLabClient.prototype.createPullRequestComment).toBeCalledTimes(0);
});
test("override backporting pr data", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"--title",
"New Title",
"--body",
"New Body",
"--body-prefix",
"New Body Prefix - ",
"--bp-branch-name",
"bp_branch_name",
"--reviewers",
"user1,user2",
"--assignees",
"user3,user4"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp_branch_name");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
);
});
test("set empty reviewers", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"--title",
"New Title",
"--body",
"New Body",
"--body-prefix",
"New Body Prefix - ",
"--bp-branch-name",
"bp_branch_name",
"--no-inherit-reviewers",
"--assignees",
"user3,user4",
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp_branch_name");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
);
});
test("set custom labels with inheritance", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
"--labels",
"cherry-pick :cherries:, another-label",
"--inherit-labels",
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-ebb1eca");
// 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", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-ebb1eca",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"),
reviewers: ["superuser"],
assignees: [],
labels: ["cherry-pick :cherries:", "another-label", "backport-prod"],
comments: [],
}
);
});
test("set custom labels without inheritance", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
"--labels",
"cherry-pick :cherries:, another-label",
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-ebb1eca");
// 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", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-ebb1eca",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"),
reviewers: ["superuser"],
assignees: [],
labels: ["cherry-pick :cherries:", "another-label"],
comments: [],
}
);
});
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(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, "my-token", "https://my.gitlab.host.com/api/v4");
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-ebb1eca");
// 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", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-prod-ebb1eca");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-ebb1eca",
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"],
labels: ["cli gitlab cherry pick :cherries:", "backport-prod"],
comments: [],
}
);
});
test("single commit without squash", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
"--no-squash",
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-e4dd336");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "e4dd336a4a20f394df6665994df382fb1d193a11", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-e4dd336");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-e4dd336",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"),
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
test("multiple commits without squash", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"--no-squash",
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-e4dd336-974519f");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(2);
expect(GitCLIService.prototype.cherryPick).toHaveBeenNthCalledWith(1, cwd, "e4dd336a4a20f394df6665994df382fb1d193a11", undefined, undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toHaveBeenNthCalledWith(2, cwd, "974519f65c9e0ed65277cd71026657a09fca05e7", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-e4dd336-974519f");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-e4dd336-974519f",
base: "target",
title: "[target] Update test.txt opened",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"),
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
test("merged MR with --auto-no-squash", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/5",
"--auto-no-squash",
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-e4dd336");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "e4dd336a4a20f394df6665994df382fb1d193a11", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-e4dd336");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-e4dd336",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/5"),
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
test("auth using GITLAB_TOKEN takes precedence over GIT_TOKEN env variable", async () => {
process.env[AuthTokenId.GIT_TOKEN] = "mygittoken";
process.env[AuthTokenId.GITLAB_TOKEN] = "mygitlabtoken";
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"
]);
await runner.execute();
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, "mygitlabtoken", "https://my.gitlab.host.com/api/v4");
// Not interested in all subsequent calls, already tested in other test cases
});
test("auth arg takes precedence over GITLAB_TOKEN", async () => {
process.env[AuthTokenId.GITLAB_TOKEN] = "mygitlabtoken";
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"-a",
"mytoken"
]);
await runner.execute();
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, "mytoken", "https://my.gitlab.host.com/api/v4");
// Not interested in all subsequent calls, already tested in other test cases
});
test("ignore env variables related to other git platforms", async () => {
process.env[AuthTokenId.GITHUB_TOKEN] = "mygithubtoken";
process.env[AuthTokenId.CODEBERG_TOKEN] = "mycodebergtoken";
addProcessArgs([
"-tb",
"target",
"-pr",
"https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"
]);
await runner.execute();
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
// Not interested in all subsequent calls, already tested in other test cases
});
});

View file

@ -1,393 +0,0 @@
import ArgsParser from "@bp/service/args/args-parser";
import Runner from "@bp/service/runner/runner";
import GitCLIService from "@bp/service/git/git-cli";
import GitHubService from "@bp/service/git/github/github-service";
import CLIArgsParser from "@bp/service/args/cli/cli-args-parser";
import { addProcessArgs, resetProcessArgs } from "../../support/utils";
import { setupMoctokit } from "../../support/moctokit/moctokit-support";
jest.mock("@bp/service/git/git-cli");
jest.spyOn(GitHubService.prototype, "createPullRequest");
let parser: ArgsParser;
let runner: Runner;
beforeEach(() => {
setupMoctokit();
// create CLI arguments parser
parser = new CLIArgsParser();
// create runner
runner = new Runner(parser);
});
afterEach(() => {
jest.clearAllMocks();
// reset process.env variables
resetProcessArgs();
});
describe("cli runner", () => {
test("with dry run", async () => {
addProcessArgs([
"-d",
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/2368"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
});
test("overriding author", async () => {
addProcessArgs([
"-d",
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/2368"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
});
test("with relative folder", async () => {
addProcessArgs([
"-d",
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/2368",
"-f",
"folder"
]);
await runner.execute();
const cwd = process.cwd() + "/folder";
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.addRemote).toBeCalledTimes(0);
expect(GitCLIService.prototype.addRemote).toBeCalledTimes(0);
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
});
test("with absolute folder", async () => {
addProcessArgs([
"-d",
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/2368",
"-f",
"/tmp/folder"
]);
await runner.execute();
const cwd = "/tmp/folder";
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
});
test("without dry run", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/2368"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
}
);
});
test("same owner", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/8632"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(0);
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/8632"),
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
}
);
});
test("closed and not merged pull request", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/6666"
]);
expect(async () => await runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!");
});
test("open pull request", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/4444"
]);
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-91748965051fae1330ad58d15cf694e103267c87");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/4444/head:pr/4444");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "91748965051fae1330ad58d15cf694e103267c87");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-91748965051fae1330ad58d15cf694e103267c87");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-91748965051fae1330ad58d15cf694e103267c87",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/4444"),
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
}
);
});
test("override backporting pr data", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/2368",
"--title",
"New Title",
"--body",
"New Body",
"--body-prefix",
"New Body Prefix - ",
"--bp-branch-name",
"bp_branch_name",
"--reviewers",
"user1,user2",
"--assignees",
"user3,user4"
]);
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(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
}
);
});
test("set empty reviewers", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/2368",
"--title",
"New Title",
"--body",
"New Body",
"--body-prefix",
"New Body Prefix - ",
"--bp-branch-name",
"bp_branch_name",
"--no-inherit-reviewers",
"--assignees",
"user3,user4",
]);
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(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.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"],
}
);
});
});

View file

@ -0,0 +1,838 @@
import ArgsParser from "@bp/service/args/args-parser";
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 { createTestFile, removeTestFile, resetEnvTokens, spyGetInput } from "../../support/utils";
import { mockGitHubClient } from "../../support/mock/git-client-mock-support";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
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,
"labels": ["gha github cherry pick :cherries:"],
"inheritLabels": true,
};
jest.mock("@bp/service/git/git-cli");
jest.spyOn(GitHubClient.prototype, "createPullRequest");
jest.spyOn(GitHubClient.prototype, "createPullRequestComment");
jest.spyOn(GitClientFactory, "getOrCreate");
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(() => {
// reset git env tokens
resetEnvTokens();
mockGitHubClient();
// create GHA arguments parser
parser = new GHAArgsParser();
// create runner
runner = new Runner(parser);
});
describe("gha runner", () => {
test("with dry run", async () => {
spyGetInput({
"dry-run": "true",
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/2368"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db");
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", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(0);
expect(GitHubClient.prototype.createPullRequestComment).toBeCalledTimes(0);
});
test("without dry run", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/2368"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db");
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", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("closed and not merged pull request", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/6666"
});
await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged");
});
test("open pull request", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/4444"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-9174896");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/4444/head:pr/4444");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "91748965051fae1330ad58d15cf694e103267c87", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-9174896");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-9174896",
base: "target",
title: "[target] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/4444\r\n\r\nPlease review and merge",
reviewers: ["gh-user"],
assignees: [],
labels: [],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("override backporting pr data", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/2368",
"title": "New Title",
"body": "New Body",
"body-prefix": "New Body Prefix\\r\\n\\r\\n",
"bp-branch-name": "bp_branch_name",
"reviewers": "user1, user2",
"assignees": "user3, user4",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "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", undefined, undefined, undefined);
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\r\n\r\nNew Body",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("set empty reviewers", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/2368",
"title": "New Title",
"body": "New Body",
"body-prefix": "New Body Prefix - ",
"bp-branch-name": "bp_branch_name",
"reviewers": "",
"assignees": "user3, user4",
"no-inherit-reviewers": "true",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "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", undefined, undefined, undefined);
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"],
labels: [],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("set custom labels with inheritance", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/2368",
"labels": "cherry-pick :cherries:, another-label",
"inherit-labels": "true",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db");
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", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db",
base: "target",
title: "[target] 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: ["cherry-pick :cherries:", "another-label", "backport prod"],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("set custom labels without inheritance", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/2368",
"labels": "cherry-pick :cherries:, another-label",
"inherit-labels": "false",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db");
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", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db",
base: "target",
title: "[target] 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: ["cherry-pick :cherries:", "another-label"],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
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(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, "my-auth-token", "https://api.github.com");
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", undefined, undefined, undefined);
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"],
labels: ["gha github cherry pick :cherries:", "backport prod"],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
// to check: https://github.com/kiegroup/git-backporting/issues/52
test("using github api url instead of html one", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://api.github.com/repos/owner/reponame/pulls/2368"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db");
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", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("multiple commits pr", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://api.github.com/repos/owner/reponame/pulls/8632",
"no-squash": "true",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-0404fb9-11da4e3");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(0);
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(2);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "0404fb922ab75c3a8aecad5c97d9af388df04695", undefined, undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toHaveBeenLastCalledWith(cwd, "11da4e38aa3e577ffde6d546f1c52e53b04d3151", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-0404fb9-11da4e3");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-0404fb9-11da4e3",
base: "target",
title: "[target] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/8632\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("using github api url and different strategy", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://api.github.com/repos/owner/reponame/pulls/2368",
"strategy": "ort",
"strategy-option": "ours",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db");
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", "ort", "ours", undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("using github api url and additional cherry-pick options", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://api.github.com/repos/owner/reponame/pulls/2368",
"cherry-pick-options": "-x --allow-empty",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db");
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", undefined, undefined, "-x --allow-empty");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("additional pr comments", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/2368",
"comments": "first comment; second comment",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db");
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", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db",
base: "target",
title: "[target] 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: ["first comment", "second comment"],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("with multiple target branches", async () => {
spyGetInput({
"target-branch": "v1, v2, v3",
"pull-request": "https://github.com/owner/reponame/pull/2368",
"folder": "/tmp/folder",
});
await runner.execute();
const cwd = "/tmp/folder";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(3);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v1-28f63db");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v2-28f63db");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-v3-28f63db");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(3);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined, 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, "bp-v1-28f63db");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v2-28f63db");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-v3-28f63db");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-v1-28f63db",
base: "v1",
title: "[v1] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-v2-28f63db",
base: "v2",
title: "[v2] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-v3-28f63db",
base: "v3",
title: "[v3] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(3);
});
test("with multiple target branches and single custom bp branch", async () => {
spyGetInput({
"target-branch": "v1, v2, v3",
"pull-request": "https://github.com/owner/reponame/pull/2368",
"folder": "/tmp/folder",
"bp-branch-name": "custom"
});
await runner.execute();
const cwd = "/tmp/folder";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(3);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-v1");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-v2");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-v3");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(3);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined, 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-v1");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-v2");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-v3");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-v1",
base: "v1",
title: "[v1] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-v2",
base: "v2",
title: "[v2] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-v3",
base: "v3",
title: "[v3] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(3);
});
test("explicitly set git client", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://api.github.com/repos/owner/reponame/pulls/2368",
"git-client": "codeberg",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.CODEBERG, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
});
});

View file

@ -0,0 +1,564 @@
import ArgsParser from "@bp/service/args/args-parser";
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 { createTestFile, removeTestFile, resetEnvTokens, spyGetInput } from "../../support/utils";
import { getAxiosMocked } from "../../support/mock/git-client-mock-support";
import { MERGED_SQUASHED_MR } from "../../support/mock/gitlab-data";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
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,
"labels": ["gha gitlab cherry pick :cherries:"],
"inheritLabels": true,
};
jest.mock("axios", () => {
return {
create: () => ({
get: getAxiosMocked,
post: () => ({
data: {
iid: 1, // FIXME: I am not testing this atm
}
}),
put: async () => undefined, // make it async so that .catch doesn't throw an error
}),
};
});
jest.mock("@bp/service/git/git-cli");
jest.spyOn(GitLabClient.prototype, "createPullRequest");
jest.spyOn(GitLabClient.prototype, "createPullRequestComment");
jest.spyOn(GitClientFactory, "getOrCreate");
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(() => {
// reset git env tokens
resetEnvTokens();
// create GHA arguments parser
parser = new GHAArgsParser();
// create runner
runner = new Runner(parser);
});
describe("gha runner", () => {
test("with dry run", async () => {
spyGetInput({
"dry-run": "true",
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-9e15674");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(0);
expect(GitLabClient.prototype.createPullRequestComment).toBeCalledTimes(0);
});
test("without dry run", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-9e15674");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-9e15674");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-9e15674",
base: "target",
title: "[target] Update test.txt opened",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"),
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
test("closed and not merged pull request", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/3"
});
await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged");
});
test("merged pull request", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-ebb1eca");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(0);
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "ebb1eca696c42fd067658bd9b5267709f78ef38e", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-ebb1eca",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"),
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
test("override backporting pr data", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"title": "New Title",
"body": "New Body",
"body-prefix": "New Body Prefix - ",
"bp-branch-name": "bp_branch_name",
"reviewers": "user1, user2",
"assignees": "user3, user4",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp_branch_name");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
);
});
test("set empty reviewers", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"title": "New Title",
"body": "New Body",
"body-prefix": "New Body Prefix - ",
"bp-branch-name": "bp_branch_name",
"reviewers": "",
"assignees": "user3, user4",
"no-inherit-reviewers": "true",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp_branch_name");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "9e15674ebd48e05c6e428a1fa31dbb60a778d644", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp_branch_name");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
);
});
test("set custom labels with inheritance", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
"labels": "cherry-pick :cherries:, another-label",
"inherit-labels": "true",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-ebb1eca");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(0);
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "ebb1eca696c42fd067658bd9b5267709f78ef38e", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-ebb1eca",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"),
reviewers: ["superuser"],
assignees: [],
labels: ["cherry-pick :cherries:", "another-label", "backport-prod"],
comments: [],
}
);
});
test("set custom labels without inheritance", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
"labels": "cherry-pick :cherries:, another-label",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-ebb1eca");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(0);
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "ebb1eca696c42fd067658bd9b5267709f78ef38e", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-ebb1eca");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-ebb1eca",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"),
reviewers: ["superuser"],
assignees: [],
labels: ["cherry-pick :cherries:", "another-label"],
comments: [],
}
);
});
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(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, "my-token", "https://my.gitlab.host.com/api/v4");
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-ebb1eca");
// 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", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-prod-ebb1eca");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-prod-ebb1eca",
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"],
labels: ["gha gitlab cherry pick :cherries:", "backport-prod"],
comments: [],
}
);
});
test("single commit without squash", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1",
"no-squash": "true",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-e4dd336");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "e4dd336a4a20f394df6665994df382fb1d193a11", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-e4dd336");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-e4dd336",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/1"),
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
test("multiple commits without squash", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2",
"no-squash": "true",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-e4dd336-974519f");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "merge-requests/2/head:pr/2");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(2);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "e4dd336a4a20f394df6665994df382fb1d193a11", undefined, undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "974519f65c9e0ed65277cd71026657a09fca05e7", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-e4dd336-974519f");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-e4dd336-974519f",
base: "target",
title: "[target] Update test.txt opened",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/2"),
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
test("merged MR with auto-no-squash", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/5",
"auto-no-squash": "true",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITLAB, undefined, "https://my.gitlab.host.com/api/v4");
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://my.gitlab.host.com/superuser/backporting-example.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-e4dd336");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "e4dd336a4a20f394df6665994df382fb1d193a11", undefined, undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-e4dd336");
expect(GitLabClient.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitLabClient.prototype.createPullRequest).toBeCalledWith({
owner: "superuser",
repo: "backporting-example",
head: "bp-target-e4dd336",
base: "target",
title: "[target] Update test.txt",
body: expect.stringContaining("**Backport:** https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/5"),
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
});

View file

@ -1,234 +0,0 @@
import ArgsParser from "@bp/service/args/args-parser";
import Runner from "@bp/service/runner/runner";
import GitCLIService from "@bp/service/git/git-cli";
import GitHubService from "@bp/service/git/github/github-service";
import GHAArgsParser from "@bp/service/args/gha/gha-args-parser";
import { spyGetInput } from "../../support/utils";
import { setupMoctokit } from "../../support/moctokit/moctokit-support";
jest.mock("@bp/service/git/git-cli");
jest.spyOn(GitHubService.prototype, "createPullRequest");
let parser: ArgsParser;
let runner: Runner;
beforeEach(() => {
setupMoctokit();
// create GHA arguments parser
parser = new GHAArgsParser();
// create runner
runner = new Runner(parser);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("gha runner", () => {
test("with dry run", async () => {
spyGetInput({
"dry-run": "true",
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/2368"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
});
test("without dry run", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/2368"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
}
);
});
test("closed and not merged pull request", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/6666"
});
expect(async () => await runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!");
});
test("open pull request", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/4444"
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-91748965051fae1330ad58d15cf694e103267c87");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/4444/head:pr/4444");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "91748965051fae1330ad58d15cf694e103267c87");
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-91748965051fae1330ad58d15cf694e103267c87");
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp-target-91748965051fae1330ad58d15cf694e103267c87",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/4444"),
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
}
);
});
test("override backporting pr data", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/2368",
"title": "New Title",
"body": "New Body",
"body-prefix": "New Body Prefix - ",
"bp-branch-name": "bp_branch_name",
"reviewers": "user1, user2",
"assignees": "user3, user4",
});
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(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
}
);
});
test("set empty reviewers", async () => {
spyGetInput({
"target-branch": "target",
"pull-request": "https://github.com/owner/reponame/pull/2368",
"title": "New Title",
"body": "New Body",
"body-prefix": "New Body Prefix - ",
"bp-branch-name": "bp_branch_name",
"reviewers": "",
"assignees": "user3, user4",
"no-inherit-reviewers": "true",
});
await runner.execute();
const cwd = process.cwd() + "/bp";
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp_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(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
expect(GitHubService.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"],
}
);
});
});

View 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}}");
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,394 @@
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { Moctokit } from "@kie/mock-github";
import { TARGET_OWNER, REPO, MERGED_PR_FIXTURE, OPEN_PR_FIXTURE, NOT_MERGED_PR_FIXTURE, NOT_FOUND_PR_NUMBER, MULT_COMMITS_PR_FIXTURE, MULT_COMMITS_PR_COMMITS, NEW_PR_URL, NEW_PR_NUMBER, GITHUB_GET_COMMIT } from "./github-data";
import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, NESTED_NAMESPACE_MR, OPEN_MR, OPEN_PR_COMMITS, PROJECT_EXAMPLE, NESTED_PROJECT_EXAMPLE, SUPERUSER, MERGED_SQUASHED_MR_COMMITS, MERGED_NOT_SQUASHED_MR, MERGED_NOT_SQUASHED_MR_COMMITS } from "./gitlab-data";
import { CB_TARGET_OWNER, CB_REPO, CB_MERGED_PR_FIXTURE, CB_OPEN_PR_FIXTURE, CB_NOT_MERGED_PR_FIXTURE, CB_NOT_FOUND_PR_NUMBER, CB_MULT_COMMITS_PR_FIXTURE, CB_MULT_COMMITS_PR_COMMITS, CB_NEW_PR_URL, CB_NEW_PR_NUMBER, CODEBERG_GET_COMMIT } from "./codeberg-data";
// high number, for each test we are not expecting
// to send more than 3 reqs per api endpoint
const REPEAT = 20;
const logger = LoggerServiceFactory.getLogger();
// AXIOS
export const getAxiosMocked = (url: string) => {
let data = undefined;
// gitlab
if (url.endsWith("merge_requests/1")) {
data = MERGED_SQUASHED_MR;
} else if (url.endsWith("merge_requests/2")) {
data = OPEN_MR;
} else if (url.endsWith("merge_requests/3")) {
data = CLOSED_NOT_MERGED_MR;
} else if (url.endsWith("merge_requests/4")) {
data = NESTED_NAMESPACE_MR;
} else if (url.endsWith("merge_requests/5")) {
data = MERGED_NOT_SQUASHED_MR;
} else if (url.endsWith("projects/76316")) {
data = PROJECT_EXAMPLE;
} else if (url.endsWith("projects/1645")) {
data = NESTED_PROJECT_EXAMPLE;
} else if (url.endsWith("users?username=superuser")) {
data = [SUPERUSER];
} else if (url.endsWith("merge_requests/1/commits")) {
data = MERGED_SQUASHED_MR_COMMITS;
} else if (url.endsWith("merge_requests/2/commits")) {
data = OPEN_PR_COMMITS;
} else if (url.endsWith("merge_requests/5/commits")) {
data = MERGED_NOT_SQUASHED_MR_COMMITS;
}
return {
data,
status: data ? 200 : 404,
};
};
export const NEW_GITLAB_MR_ID = 999;
export const SECOND_NEW_GITLAB_MR_ID = 1000;
export const postAxiosMocked = async (url: string, data?: {source_branch: string,}) => {
let responseData = undefined;
// gitlab
if (url.includes("notes")) {
// creating comments
responseData = {
// we do not need the whole response
iid: NEW_GITLAB_MR_ID,
};
} else if (data?.source_branch === "bp-branch") {
responseData = {
// we do not need the whole response
iid: NEW_GITLAB_MR_ID,
web_url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + NEW_GITLAB_MR_ID
};
} else if (data?.source_branch === "bp-branch-2") {
responseData = {
// we do not need the whole response
iid: SECOND_NEW_GITLAB_MR_ID,
web_url: "https://my.gitlab.host.com/superuser/backporting-example/-/merge_requests/" + SECOND_NEW_GITLAB_MR_ID
};
}
return {
data: responseData,
status: responseData ? 200 : 404,
};
};
export const putAxiosMocked = async (url: string, _data?: unknown) => {
const responseData = undefined;
// gitlab
if (url.endsWith(`merge_requests/${NEW_GITLAB_MR_ID}`)) {
return {
data: {
iid: NEW_GITLAB_MR_ID,
},
status: responseData ? 200 : 404,
};
}
throw new Error("Error updating merge request: " + url);
};
// GITHUB - OCTOKIT
export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit => {
logger.debug("Setting up moctokit..");
const mock = new Moctokit(apiUrl);
// setup the mock requests here
// valid requests
mock.rest.pulls
.get({
owner: TARGET_OWNER,
repo: REPO,
pull_number: MERGED_PR_FIXTURE.number
})
.reply({
status: 200,
data: MERGED_PR_FIXTURE
});
mock.rest.pulls
.get({
owner: TARGET_OWNER,
repo: REPO,
pull_number: MULT_COMMITS_PR_FIXTURE.number
})
.reply({
status: 200,
data: MULT_COMMITS_PR_FIXTURE
});
mock.rest.pulls
.get({
owner: TARGET_OWNER,
repo: REPO,
pull_number: OPEN_PR_FIXTURE.number
})
.reply({
status: 200,
data: OPEN_PR_FIXTURE
});
mock.rest.pulls
.get({
owner: TARGET_OWNER,
repo: REPO,
pull_number: NOT_MERGED_PR_FIXTURE.number
})
.reply({
status: 200,
data: NOT_MERGED_PR_FIXTURE
});
mock.rest.pulls
.listCommits({
owner: TARGET_OWNER,
repo: REPO,
pull_number: MULT_COMMITS_PR_FIXTURE.number
})
.reply({
status: 200,
data: MULT_COMMITS_PR_COMMITS
});
mock.rest.pulls
.listCommits({
owner: TARGET_OWNER,
repo: REPO,
pull_number: OPEN_PR_FIXTURE.number
})
.reply({
status: 200,
data: MULT_COMMITS_PR_COMMITS
});
mock.rest.pulls
.create()
.reply({
repeat: REPEAT,
status: 201,
data: {
number: NEW_PR_NUMBER,
html_url: NEW_PR_URL,
}
});
mock.rest.pulls
.requestReviewers()
.reply({
repeat: REPEAT,
status: 201,
data: MERGED_PR_FIXTURE
});
mock.rest.issues
.addAssignees()
.reply({
repeat: REPEAT,
status: 201,
data: {}
});
mock.rest.issues
.addLabels()
.reply({
repeat: REPEAT,
status: 200,
data: {}
});
mock.rest.issues
.createComment()
.reply({
repeat: REPEAT,
status: 201,
data: {}
});
mock.rest.git
.getCommit({
owner: TARGET_OWNER,
repo: REPO,
commit_sha: "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc",
})
.reply({
status: 200,
data: GITHUB_GET_COMMIT,
});
// invalid requests
mock.rest.pulls
.get({
owner: TARGET_OWNER,
repo: REPO,
pull_number: NOT_FOUND_PR_NUMBER
})
.reply({
repeat: REPEAT,
status: 404,
data: {
message: "Not found"
}
});
return mock;
};
// CODEBERG - OCTOKIT
export const mockCodebergClient = (apiUrl = "https://codeberg.org/api/v1"): Moctokit => {
logger.debug("Setting up moctokit..");
const mock = new Moctokit(apiUrl);
// setup the mock requests here
// valid requests
mock.rest.pulls
.get({
owner: CB_TARGET_OWNER,
repo: CB_REPO,
pull_number: CB_MERGED_PR_FIXTURE.number
})
.reply({
status: 200,
data: CB_MERGED_PR_FIXTURE
});
mock.rest.pulls
.get({
owner: CB_TARGET_OWNER,
repo: CB_REPO,
pull_number: CB_MULT_COMMITS_PR_FIXTURE.number
})
.reply({
status: 200,
data: CB_MULT_COMMITS_PR_FIXTURE
});
mock.rest.pulls
.get({
owner: CB_TARGET_OWNER,
repo: CB_REPO,
pull_number: CB_OPEN_PR_FIXTURE.number
})
.reply({
status: 200,
data: CB_OPEN_PR_FIXTURE
});
mock.rest.pulls
.get({
owner: CB_TARGET_OWNER,
repo: CB_REPO,
pull_number: CB_NOT_MERGED_PR_FIXTURE.number
})
.reply({
status: 200,
data: CB_NOT_MERGED_PR_FIXTURE
});
mock.rest.pulls
.listCommits({
owner: CB_TARGET_OWNER,
repo: CB_REPO,
pull_number: CB_MULT_COMMITS_PR_FIXTURE.number
})
.reply({
status: 200,
data: CB_MULT_COMMITS_PR_COMMITS
});
mock.rest.pulls
.listCommits({
owner: CB_TARGET_OWNER,
repo: CB_REPO,
pull_number: CB_OPEN_PR_FIXTURE.number
})
.reply({
status: 200,
data: CB_MULT_COMMITS_PR_COMMITS
});
mock.rest.pulls
.create()
.reply({
repeat: REPEAT,
status: 201,
data: {
number: CB_NEW_PR_NUMBER,
html_url: CB_NEW_PR_URL,
}
});
mock.rest.pulls
.requestReviewers()
.reply({
repeat: REPEAT,
status: 201,
data: CB_MERGED_PR_FIXTURE
});
mock.rest.issues
.addAssignees()
.reply({
repeat: REPEAT,
status: 201,
data: {}
});
mock.rest.issues
.addLabels()
.reply({
repeat: REPEAT,
status: 200,
data: {}
});
mock.rest.issues
.createComment()
.reply({
repeat: REPEAT,
status: 201,
data: {}
});
mock.rest.git
.getCommit({
owner: CB_TARGET_OWNER,
repo: CB_REPO,
commit_sha: "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc",
})
.reply({
status: 200,
data: CODEBERG_GET_COMMIT,
});
// invalid requests
mock.rest.pulls
.get({
owner: CB_TARGET_OWNER,
repo: CB_REPO,
pull_number: CB_NOT_FOUND_PR_NUMBER
})
.reply({
repeat: REPEAT,
status: 404,
data: {
message: "Not found"
}
});
return mock;
};

View file

@ -1,9 +1,11 @@
export const targetOwner = "owner";
export const sourceOwner = "fork";
export const repo = "reponame";
export const notFoundPullRequestNumber = 1;
export const TARGET_OWNER = "owner";
export const SOURCE_OWNER = "fork";
export const REPO = "reponame";
export const NOT_FOUND_PR_NUMBER = 1;
export const NEW_PR_URL = "new_pr_url";
export const NEW_PR_NUMBER = 9999;
export const mergedPullRequestFixture = {
export const MERGED_PR_FIXTURE = {
"url": "https://api.github.com/repos/owner/reponame/pulls/2368",
"id": 1137188271,
"node_id": "PR_kwDOABTq6s5DyB2v",
@ -91,7 +93,15 @@ export const mergedPullRequestFixture = {
],
"labels": [
{
"id": 4901021057,
"node_id": "LA_kwDOImgs2354988AAAABJB-lgQ",
"url": "https://api.github.com/repos/owner/reponame/labels/backport-prod",
"name": "backport prod",
"color": "AB975B",
"default": false,
"description": ""
}
],
"milestone": null,
"draft": false,
@ -466,7 +476,7 @@ export const mergedPullRequestFixture = {
"changed_files": 2
};
export const openPullRequestFixture = {
export const OPEN_PR_FIXTURE = {
"url": "https://api.github.com/repos/owner/reponame/pulls/4444",
"id": 1137188271,
"node_id": "PR_kwDOABTq6s5DyB2v",
@ -880,26 +890,7 @@ export const openPullRequestFixture = {
"mergeable": null,
"rebaseable": null,
"mergeable_state": "unknown",
"merged_by": {
"login": "that-s-a-user",
"id": 17157711,
"node_id": "MDQ6VXNlcjE3MTU3NzEx",
"avatar_url": "https://avatars.githubusercontent.com/u/17157711?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/that-s-a-user",
"html_url": "https://github.com/that-s-a-user",
"followers_url": "https://api.github.com/users/that-s-a-user/followers",
"following_url": "https://api.github.com/users/that-s-a-user/following{/other_user}",
"gists_url": "https://api.github.com/users/that-s-a-user/gists{/gist_id}",
"starred_url": "https://api.github.com/users/that-s-a-user/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/that-s-a-user/subscriptions",
"organizations_url": "https://api.github.com/users/that-s-a-user/orgs",
"repos_url": "https://api.github.com/users/that-s-a-user/repos",
"events_url": "https://api.github.com/users/that-s-a-user/events{/privacy}",
"received_events_url": "https://api.github.com/users/that-s-a-user/received_events",
"type": "User",
"site_admin": false
},
"merged_by": {},
"comments": 0,
"review_comments": 0,
"maintainer_can_modify": false,
@ -909,7 +900,7 @@ export const openPullRequestFixture = {
"changed_files": 2
};
export const notMergedPullRequestFixture = {
export const NOT_MERGED_PR_FIXTURE = {
"url": "https://api.github.com/repos/owner/reponame/pulls/6666",
"id": 1137188271,
"node_id": "PR_kwDOABTq6s5DyB2v",
@ -1352,7 +1343,7 @@ export const notMergedPullRequestFixture = {
"changed_files": 2
};
export const sameOwnerPullRequestFixture = {
export const MULT_COMMITS_PR_FIXTURE = {
"url": "https://api.github.com/repos/owner/reponame/pulls/8632",
"id": 1137188271,
"node_id": "PR_kwDOABTq6s5DyB2v",
@ -1440,7 +1431,33 @@ export const sameOwnerPullRequestFixture = {
],
"labels": [
{
"id": 4901021057,
"node_id": "LA_kwDOImgs2354988AAAABJB-lgQ",
"url": "https://api.github.com/repos/owner/reponame/labels/backport-v1",
"name": "backport v1",
"color": "AB975B",
"default": false,
"description": ""
},
{
"id": 4901021057,
"node_id": "LA_kwDOImgs2354988AAAABJB-lgQ",
"url": "https://api.github.com/repos/owner/reponame/labels/backport-v2",
"name": "backport v2",
"color": "AB975B",
"default": false,
"description": ""
},
{
"id": 4901021057,
"node_id": "LA_kwDOImgs2354988AAAABJB-lgQ",
"url": "https://api.github.com/repos/owner/reponame/labels/backport-v3",
"name": "backport v3",
"color": "AB975B",
"default": false,
"description": ""
}
],
"milestone": null,
"draft": false,
@ -1813,4 +1830,173 @@ export const sameOwnerPullRequestFixture = {
"additions": 2,
"deletions": 2,
"changed_files": 2
};
};
export const GITHUB_GET_COMMIT = {
"parents": [
{
"sha": "SHA"
}
]
};
export const MULT_COMMITS_PR_COMMITS = [
{
"sha": "0404fb922ab75c3a8aecad5c97d9af388df04695",
"node_id": "C_kwDOImgs99oAKDA0MDRmYjkyMmFiNzVjM2E4YWVjYWQ1Yzk3ZDlhZjM4OGRmMDQ2OTU",
"commit": {
"author": {
"name": "owner",
"email": "owner@email.com",
"date": "2023-07-06T13:46:30Z"
},
"committer": {
"name": "GitHub",
"email": "noreply@github.com",
"date": "2023-07-06T13:46:30Z"
},
"message": "Update file1.txt",
"tree": {
"sha": "50be1d7031b02a2ae609f432f2a1e0f818d827b2",
"url": "https://api.github.com/repos/owner/reponame/git/trees/50be1d7031b02a2ae609f432f2a1e0f818d827b2"
},
"url": "https://api.github.com/repos/owner/reponame/git/commits/0404fb922ab75c3a8aecad5c97d9af388df04695",
"comment_count": 0,
"verification": {
"verified": true,
"reason": "valid",
"signature": "-----BEGIN PGP SIGNATURE-----\n\nno-signature=\n=fivd\n-----END PGP SIGNATURE-----\n",
"payload": "tree 50be1d7031b02a2ae609f432f2a1e0f818d827b2\nparent c85b8fcdb741814b3e90e6e5729455cf46ff26ea\nauthor Owner <owner@email.com> 1688651190 +0200\ncommitter GitHub <noreply@github.com> 1688651190 +0200\n\nUpdate file1.txt"
}
},
"url": "https://api.github.com/repos/owner/reponame/commits/0404fb922ab75c3a8aecad5c97d9af388df04695",
"html_url": "https://github.com/owner/reponame/commit/0404fb922ab75c3a8aecad5c97d9af388df04695",
"comments_url": "https://api.github.com/repos/owner/reponame/commits/0404fb922ab75c3a8aecad5c97d9af388df04695/comments",
"author": {
"login": "owner",
"id": 26715795,
"node_id": "MDQ6VXNlcjI2NzE1Nzk1",
"avatar_url": "https://avatars.githubusercontent.com/u/26715795?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/owner",
"html_url": "https://github.com/owner",
"followers_url": "https://api.github.com/users/owner/followers",
"following_url": "https://api.github.com/users/owner/following{/other_user}",
"gists_url": "https://api.github.com/users/owner/gists{/gist_id}",
"starred_url": "https://api.github.com/users/owner/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/owner/subscriptions",
"organizations_url": "https://api.github.com/users/owner/orgs",
"repos_url": "https://api.github.com/users/owner/repos",
"events_url": "https://api.github.com/users/owner/events{/privacy}",
"received_events_url": "https://api.github.com/users/owner/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "web-flow",
"id": 19864447,
"node_id": "MDQ6VXNlcjE5ODY0NDQ3",
"avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/web-flow",
"html_url": "https://github.com/web-flow",
"followers_url": "https://api.github.com/users/web-flow/followers",
"following_url": "https://api.github.com/users/web-flow/following{/other_user}",
"gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}",
"starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/web-flow/subscriptions",
"organizations_url": "https://api.github.com/users/web-flow/orgs",
"repos_url": "https://api.github.com/users/web-flow/repos",
"events_url": "https://api.github.com/users/web-flow/events{/privacy}",
"received_events_url": "https://api.github.com/users/web-flow/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "c85b8fcdb741814b3e90e6e5729455cf46ff26ea",
"url": "https://api.github.com/repos/owner/reponame/commits/c85b8fcdb741814b3e90e6e5729455cf46ff26ea",
"html_url": "https://github.com/owner/reponame/commit/c85b8fcdb741814b3e90e6e5729455cf46ff26ea"
}
]
},
{
"sha": "11da4e38aa3e577ffde6d546f1c52e53b04d3151",
"node_id": "C_kwDOImgs99oAKDExZGE0ZTM4YWEzZTU3N2ZmZGU2ZDU0NmYxYzUyZTUzYjA0ZDMxNTE",
"commit": {
"author": {
"name": "Owner",
"email": "owner@email.com",
"date": "2023-07-10T13:23:44Z"
},
"committer": {
"name": "GitHub",
"email": "noreply@github.com",
"date": "2023-07-10T13:23:44Z"
},
"message": "Update file2.txt",
"tree": {
"sha": "fdd16fb791eef26fd84c3bfa34fd89eb1f7a85be",
"url": "https://api.github.com/repos/owner/reponame/git/trees/fdd16fb791eef26fd84c3bfa34fd89eb1f7a85be"
},
"url": "https://api.github.com/repos/owner/reponame/git/commits/11da4e38aa3e577ffde6d546f1c52e53b04d3151",
"comment_count": 0,
"verification": {
"verified": true,
"reason": "valid",
"signature": "-----BEGIN PGP SIGNATURE-----\n\nno-signature\n=//hm\n-----END PGP SIGNATURE-----\n",
"payload": "tree fdd16fb791eef26fd84c3bfa34fd89eb1f7a85be\nparent 0404fb922ab75c3a8aecad5c97d9af388df04695\nauthor Owner <owner@email.com> 1688995424 +0200\ncommitter GitHub <noreply@github.com> 1688995424 +0200\n\nUpdate file2.txt"
}
},
"url": "https://api.github.com/repos/owner/reponame/commits/11da4e38aa3e577ffde6d546f1c52e53b04d3151",
"html_url": "https://github.com/owner/reponame/commit/11da4e38aa3e577ffde6d546f1c52e53b04d3151",
"comments_url": "https://api.github.com/repos/owner/reponame/commits/11da4e38aa3e577ffde6d546f1c52e53b04d3151/comments",
"author": {
"login": "owner",
"id": 26715795,
"node_id": "MDQ6VXNlcjI2NzE1Nzk1",
"avatar_url": "https://avatars.githubusercontent.com/u/26715795?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/owner",
"html_url": "https://github.com/owner",
"followers_url": "https://api.github.com/users/owner/followers",
"following_url": "https://api.github.com/users/owner/following{/other_user}",
"gists_url": "https://api.github.com/users/owner/gists{/gist_id}",
"starred_url": "https://api.github.com/users/owner/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/owner/subscriptions",
"organizations_url": "https://api.github.com/users/owner/orgs",
"repos_url": "https://api.github.com/users/owner/repos",
"events_url": "https://api.github.com/users/owner/events{/privacy}",
"received_events_url": "https://api.github.com/users/owner/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "web-flow",
"id": 19864447,
"node_id": "MDQ6VXNlcjE5ODY0NDQ3",
"avatar_url": "https://avatars.githubusercontent.com/u/19864447?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/web-flow",
"html_url": "https://github.com/web-flow",
"followers_url": "https://api.github.com/users/web-flow/followers",
"following_url": "https://api.github.com/users/web-flow/following{/other_user}",
"gists_url": "https://api.github.com/users/web-flow/gists{/gist_id}",
"starred_url": "https://api.github.com/users/web-flow/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/web-flow/subscriptions",
"organizations_url": "https://api.github.com/users/web-flow/orgs",
"repos_url": "https://api.github.com/users/web-flow/repos",
"events_url": "https://api.github.com/users/web-flow/events{/privacy}",
"received_events_url": "https://api.github.com/users/web-flow/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "0404fb922ab75c3a8aecad5c97d9af388df04695",
"url": "https://api.github.com/repos/owner/reponame/commits/0404fb922ab75c3a8aecad5c97d9af388df04695",
"html_url": "https://github.com/owner/reponame/commit/0404fb922ab75c3a8aecad5c97d9af388df04695"
}
]
}
];

File diff suppressed because it is too large Load diff

View file

@ -1,89 +0,0 @@
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { Moctokit } from "@kie/mock-github";
import { targetOwner, repo, mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, notFoundPullRequestNumber, sameOwnerPullRequestFixture } from "./moctokit-data";
const logger = LoggerServiceFactory.getLogger();
export const setupMoctokit = (): Moctokit => {
logger.debug("Setting up moctokit.");
const mock = new Moctokit();
// setup the mock requests here
// valid requests
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: mergedPullRequestFixture.number
})
.reply({
status: 200,
data: mergedPullRequestFixture
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: sameOwnerPullRequestFixture.number
})
.reply({
status: 200,
data: sameOwnerPullRequestFixture
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: openPullRequestFixture.number
})
.reply({
status: 200,
data: openPullRequestFixture
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: notMergedPullRequestFixture.number
})
.reply({
status: 200,
data: notMergedPullRequestFixture
});
mock.rest.pulls
.create()
.reply({
status: 201,
data: mergedPullRequestFixture
});
mock.rest.pulls
.requestReviewers()
.reply({
status: 201,
data: mergedPullRequestFixture
});
// invalid requests
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: notFoundPullRequestNumber
})
.reply({
status: 404,
data: {
message: "Not found"
}
});
return mock;
};

View file

@ -1,4 +1,6 @@
import * as core from "@actions/core";
import { AuthTokenId } from "@bp/service/configs/configs.types";
import * as fs from "fs";
export const addProcessArgs = (args: string[]) => {
process.argv = [...process.argv, ...args];
@ -8,6 +10,13 @@ export const resetProcessArgs = () => {
process.argv = ["node", "backporting"];
};
export const resetEnvTokens = () => {
delete process.env[AuthTokenId.GITHUB_TOKEN];
delete process.env[AuthTokenId.GITLAB_TOKEN];
delete process.env[AuthTokenId.CODEBERG_TOKEN];
delete process.env[AuthTokenId.GIT_TOKEN];
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const spyGetInput = (obj: any) => {
const mock = jest.spyOn(core, "getInput");
@ -24,4 +33,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);
};