30 Commits

Author SHA1 Message Date
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
58 changed files with 5542 additions and 2308 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,22 +1,27 @@
**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>

View File

@@ -13,9 +13,9 @@ 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:

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@v3
- uses: ArtiomTr/jest-coverage-report-action@v2
with:
test-script: npm test

View File

@@ -3,14 +3,22 @@
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 }}

3
.gitignore vendored
View File

@@ -13,4 +13,5 @@ build/
.npmrc
# temporary files created during tests
*test*.json
*test*.json
bp

View File

@@ -1,5 +1,55 @@
# Changelog
## [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)

View File

@@ -14,10 +14,6 @@
---
## :bangbang: Starting from v4 git-backporting has been moved under @kiegroup organization :bangbang:
---
**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.
@@ -35,7 +31,7 @@ Table of content
## Who is this tool for?
`git-backporting` is a tool that provides capabilities to *backport* pull requests (on *GitHub*) and merge requests (on *GitLab*) in an automated way.
`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.
@@ -43,14 +39,14 @@ Therefore this tools is for anybody who is working on projects where they have t
## CLI tool
> All instructions provided below pertain to version `v4` of the tool. If you wish to use an earlier version, we strongly encourage you to consider migrating to version `v4` as there are no valid reason to keep using older versions. Please refer to [Migrating to v4](#migrating-to-v4) section for comprehensive details on how to properly migrate to version `v4`.
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 @kie/git-backporting
```
> **NOTE**: if you want to download version 3 or older you must use the older package name `@lampajr/bper` instead of `@kie/git-backporting`.
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 the simply run the following command:
```bash
@@ -64,8 +60,6 @@ $ git-backporting -tb develop -pr https://github.com/kiegroup/git-backporting-ex
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.
> **NOTE**: if you are still using version 3 or older you must use the older CLI tool name `bper` instead of `git-backporting`.
### Requirements
* Node 16 or higher, more details on Node can be found [here](https://nodejs.org/en).
@@ -100,10 +94,10 @@ This tool comes with some inputs that allow users to override the default behavi
|---------------|----------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
| Version | -V, --version | - | Current version of the tool | |
| Help | -h, --help | - | Display the help message | |
| Target Branch | -tb, --target-branch | N | Branch where the changes must be backported to | |
| Target Branches | -tb, --target-branch | N | Comma separated list of branches where the changes must be backported to | |
| Pull Request | -pr, --pull-request | N | Original pull request url, the one that must be backported, e.g., https://github.com/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 | `GITHUB_TOKEN`, `GITLAB_TOKEN` or a `repo` scoped [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) | "" |
| 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 User | -gu, --git-user | N | Local git user name | "GitHub" |
| Git Email | -ge, --git-email | N | Local git user email | "noreply@github.com" |
@@ -113,16 +107,28 @@ This tool comes with some inputs that allow users to override the default behavi
| 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, if it exceeds 250 chars it will be truncated | bp-{target-branch}-{sha1}...{shaN} |
| 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 | If provided the backporting will try to backport all pull request commits without squashing | false |
| 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" |
| Additional comments | --comments | N | Semicolon separated list of additional comments to be posted to the backported pull request | [] |
| Dry Run | -d, --dry-run | N | If enabled the tool does not push nor create anything remotely, use this to skip PR creation | false |
> **NOTE**: `pull request` and `target branch` are *mandatory*, they must be provided as CLI options or as part of the configuration file (if used).
#### Authorization token
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.
@@ -144,6 +150,8 @@ Right now **Git Backporting** supports the following git management services:
* ***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
@@ -197,6 +205,9 @@ on:
- closed
- labeled
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
backporting:
name: "Backporting"
@@ -219,7 +230,6 @@ jobs:
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).
@@ -235,7 +245,7 @@ For a complete description of all inputs see [Inputs section](#inputs).
## Migrating to v4
From version `v4.0.0` the project has been moved under [@kiegroup](https://github.com/kiegroup) organization. During this migration we changed some things that you should be aware of. I'll try to summarize them in the following table:
From version `v4` the project has been moved under [@kiegroup](https://github.com/kiegroup) organization. During this migration we changed some things that you should be aware of. I'll try to summarize them in the following table:
> **NOTE**: these changes did not affect the tool features.
@@ -246,9 +256,9 @@ From version `v4.0.0` the project has been moved under [@kiegroup](https://githu
| NPM package | @kie/git-backporting | @lampajr/bper |
| CLI tool | git-backporting | bper |
So everytime you would use older version keep in mind of these changes.
So everytime you would use older version keep in mind that these changes are madnatory to make the tool working.
> **REMARK**: since from capabilities point of view `v3.1.1` and `v4.0.0` are equivalent we would recommend to directly start using `v4`.
> **NOTE**: Versions `v3.1.1` and `v4.0.0` offer identical features; the only distinction lies in the project's renaming and organization movement.
## Development
@@ -311,4 +321,4 @@ Every change must be submitted through a *GitHub* pull request (PR). Backporting
## 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

@@ -5,7 +5,7 @@ inputs:
description: "URL of the pull request to backport, e.g., https://github.com/kiegroup/git-backporting/pull/1"
required: false
target-branch:
description: "Branch where the pull request must be backported to"
description: "Comma separated list of branches where the pull request must be backported to"
required: false
config-file:
description: "Path to a file containing the json configuration for this tool, the object must match the Args interface"
@@ -15,7 +15,7 @@ inputs:
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-user:
@@ -29,14 +29,14 @@ inputs:
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>`"
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"
@@ -67,9 +67,12 @@ inputs:
description: "Cherry-pick merge strategy option"
required: false
default: "theirs"
comments:
description: "Semicolon separated list of additional comments to be posted to the backported pull request"
required: false
runs:
using: node16
using: node20
main: dist/gha/index.js
branding:

718
dist/cli/index.js vendored

File diff suppressed because it is too large Load Diff

713
dist/gha/index.js vendored

File diff suppressed because it is too large Load Diff

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@v3
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
}
}

2707
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@kie/git-backporting",
"version": "4.2.0",
"version": "4.5.2",
"description": "Git backporting is a tool to execute automatic pull request git backporting.",
"author": "",
"license": "MIT",
@@ -54,7 +54,8 @@
"@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": "^7.0.0",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^29.2.4",
"@types/node": "^18.11.17",
@@ -65,14 +66,13 @@
"husky": "^8.0.2",
"jest": "^29.0.0",
"jest-sonar-reporter": "^2.0.0",
"release-it": "^15.6.0",
"release-it": "^16.1.3",
"semver": "^7.3.8",
"ts-jest": "^29.0.0",
"ts-node": "^10.8.1",
"tsc-alias": "^1.8.2",
"tsconfig-paths": "^4.1.0",
"typescript": "^4.9.3",
"@octokit/webhooks-types": "^6.8.0"
"typescript": "^4.9.3"
},
"dependencies": {
"@actions/core": "^1.10.0",

View File

@@ -17,8 +17,8 @@ export default abstract class ArgsParser {
const args = this.readArgs();
// validate and fill with defaults
if (!args.pullRequest || !args.targetBranch) {
throw new Error("Missing option: pull request and target branch must be provided");
if (!args.pullRequest || !args.targetBranch || args.targetBranch.trim().length == 0) {
throw new Error("Missing option: pull request and target branches must be provided");
}
return {
@@ -41,6 +41,7 @@ export default abstract class ArgsParser {
squash: this.getOrDefault(args.squash, true),
strategy: this.getOrDefault(args.strategy),
strategyOption: this.getOrDefault(args.strategyOption),
comments: this.getOrDefault(args.comments)
};
}
}

View File

@@ -44,6 +44,12 @@ export function getAsCommaSeparatedList(value: string): string[] | undefined {
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 getAsBooleanOrDefault(value: string): boolean | undefined {
const trimmed = value.trim();
return trimmed !== "" ? trimmed.toLowerCase() === "true" : undefined;

View File

@@ -1,8 +1,9 @@
/**
* Input arguments
* Tool's input arguments interface
*/
export interface Args {
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
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
@@ -12,7 +13,8 @@ export interface Args {
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
@@ -21,4 +23,5 @@ export interface Args {
squash?: boolean, // if false use squashed/merged commit otherwise backport all commits as part of the pr
strategy?: string, // cherry-pick merge strategy
strategyOption?: string, // cherry-pick merge strategy option
comments?: string[], // additional comments to be posted
}

View File

@@ -2,7 +2,7 @@ import ArgsParser from "@bp/service/args/args-parser";
import { Args } from "@bp/service/args/args.types";
import { Command } from "commander";
import { name, version, description } from "@bp/../package.json";
import { getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, readConfigFile } from "@bp/service/args/args-utils";
import { getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, getAsSemicolonSeparatedList, readConfigFile } from "@bp/service/args/args-utils";
export default class CLIArgsParser extends ArgsParser {
@@ -10,17 +10,17 @@ export default class CLIArgsParser extends ArgsParser {
return new Command(name)
.version(version)
.description(description)
.option("-tb, --target-branch <branch>", "branch where changes must be backported to")
.option("-tb, --target-branch <branches>", "comma separated list of branches where changes must be backported to")
.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 service authentication string, e.g., github token")
.option("-a, --auth <auth>", "git authentication string, if not provided fallback by looking for existing env variables like GITHUB_TOKEN")
.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-name>", "backport pr branch name, default auto-generated by the commit")
.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")
@@ -29,6 +29,7 @@ export default class CLIArgsParser extends ArgsParser {
.option("--no-squash", "if provided the tool will backport all commits as part of the pull request")
.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("--comments <comments>", "semicolon separated list of additional comments to be posted to the backported pull request", getAsSemicolonSeparatedList)
.option("-cf, --config-file <config-file>", "configuration file containing all valid options, the json must match Args interface");
}
@@ -62,6 +63,7 @@ export default class CLIArgsParser extends ArgsParser {
squash: opts.squash,
strategy: opts.strategy,
strategyOption: opts.strategyOption,
comments: opts.comments,
};
}

View File

@@ -1,7 +1,7 @@
import ArgsParser from "@bp/service/args/args-parser";
import { Args } from "@bp/service/args/args.types";
import { getInput } from "@actions/core";
import { getAsBooleanOrDefault, getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, getOrUndefined, readConfigFile } from "@bp/service/args/args-utils";
import { getAsBooleanOrDefault, getAsCleanedCommaSeparatedList, getAsCommaSeparatedList, getAsSemicolonSeparatedList, getOrUndefined, readConfigFile } from "@bp/service/args/args-utils";
export default class GHAArgsParser extends ArgsParser {
@@ -21,8 +21,8 @@ export default class GHAArgsParser extends ArgsParser {
gitUser: getOrUndefined(getInput("git-user")),
gitEmail: getOrUndefined(getInput("git-email")),
title: getOrUndefined(getInput("title")),
body: getOrUndefined(getInput("body")),
bodyPrefix: getOrUndefined(getInput("body-prefix")),
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")),
@@ -32,6 +32,7 @@ export default class GHAArgsParser extends ArgsParser {
squash: !getAsBooleanOrDefault(getInput("no-squash")),
strategy: getOrUndefined(getInput("strategy")),
strategyOption: getOrUndefined(getInput("strategy-option")),
comments: getAsSemicolonSeparatedList(getInput("comments")),
};
}

View File

@@ -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,6 +1,6 @@
import { GitPullRequest } from "@bp/service/git/git.types";
import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
export interface LocalGit {
user: string, // local git user
@@ -15,10 +15,19 @@ export interface Configs {
auth?: string,
git: LocalGit,
folder: string,
targetBranch: string,
mergeStrategy?: string, // cherry-pick merge strategy
mergeStrategyOption?: string, // cherry-pick merge strategy option
originalPullRequest: GitPullRequest,
backportPullRequest: GitPullRequest,
backportPullRequests: BackportPullRequest[],
}
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,9 +1,10 @@
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 GitClient from "@bp/service/git/git-client";
import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitPullRequest } from "@bp/service/git/git.types";
import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
export default class PullRequestConfigsParser extends ConfigsParser {
@@ -25,15 +26,21 @@ export default class PullRequestConfigsParser extends ConfigsParser {
const folder: string = args.folder ?? this.getDefaultFolder();
const targetBranches: string[] = [...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,
auth: args.auth, // this has been already pre-processed before parsing configs
folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`,
targetBranch: args.targetBranch,
mergeStrategy: args.strategy,
mergeStrategyOption: args.strategyOption,
originalPullRequest: pr,
backportPullRequest: this.getDefaultBackportPullRequest(pr, args),
backportPullRequests: this.generateBackportPullRequestsData(pr, args, targetBranches, bpBranchNames),
git: {
user: args.gitUser ?? this.gitClient.getDefaultGitUser(),
email: args.gitEmail ?? this.gitClient.getDefaultGitEmail(),
@@ -52,7 +59,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
@@ -63,23 +76,45 @@ export default class PullRequestConfigsParser extends ConfigsParser {
}
const bodyPrefix = args.bodyPrefix ?? `**Backport:** ${originalPullRequest.htmlUrl}\r\n\r\n`;
const body = args.body ?? `${originalPullRequest.body}`;
const body = bodyPrefix + (args.body ?? `${originalPullRequest.body}`);
const labels = args.labels ?? [];
if (args.inheritLabels) {
labels.push(...originalPullRequest.labels);
}
return {
author: args.gitUser ?? this.gitClient.getDefaultGitUser(),
title: args.title ?? `[${args.targetBranch}] ${originalPullRequest.title}`,
body: `${bodyPrefix}${body}`,
reviewers: [...new Set(reviewers)],
assignees: [...new Set(args.assignees)],
labels: [...new Set(labels)],
targetRepo: originalPullRequest.targetRepo,
sourceRepo: originalPullRequest.targetRepo,
branchName: args.bpBranchName,
};
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

@@ -35,8 +35,9 @@ export default class GitCLIService {
* @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,16 @@ 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`);
// checkout to the proper branch
this.logger.info(`Checking out branch ${branch}`);
await this.git(to).checkout(branch);
}
/**
@@ -74,7 +79,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 +90,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 +101,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"]);
}
@@ -106,7 +111,7 @@ export default class GitCLIService {
* @param sha commit sha
*/
async cherryPick(cwd: string, sha: string, strategy = "recursive", strategyOption = "theirs"): Promise<void> {
this.logger.info(`Cherry picking ${sha}.`);
this.logger.info(`Cherry picking ${sha}`);
const options = ["cherry-pick", "-m", "1", `--strategy=${strategy}`, `--strategy-option=${strategyOption}`, sha];
try {
@@ -128,7 +133,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

@@ -13,9 +13,10 @@ 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!");
throw new Error("You must call `getOrCreate` method first");
}
return GitClientFactory.instance;
@@ -29,7 +30,7 @@ export default class GitClientFactory {
public static getOrCreate(type: GitClientType, authToken: string | undefined, apiUrl: string): GitClient {
if (GitClientFactory.instance) {
GitClientFactory.logger.warn("Git service already initialized!");
GitClientFactory.logger.warn("Git service already initialized");
return GitClientFactory.instance;
}
@@ -42,6 +43,9 @@ export default class GitClientFactory {
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}`);
}
@@ -49,8 +53,9 @@ export default class GitClientFactory {
return GitClientFactory.instance;
}
// this is used for testing purposes
public static reset(): void {
GitClientFactory.logger.warn("Resetting git service!");
GitClientFactory.logger.warn("Resetting git service");
GitClientFactory.instance = undefined;
}
}

View File

@@ -1,4 +1,4 @@
import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
import { BackportPullRequest, GitClientType, GitPullRequest } from "@bp/service/git/git.types";
/**
* Git management service interface, which provides a common API for interacting
@@ -6,6 +6,11 @@ import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
*/
export default interface GitClient {
/**
* @returns {GitClientType} specific git client enum type
*/
getClientType(): GitClientType
// READ
getDefaultGitUser(): string;
@@ -38,4 +43,5 @@ import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
* @returns {Promise<string>} the pull request url
*/
createPullRequest(backport: BackportPullRequest): Promise<string>;
}

View File

@@ -1,4 +1,5 @@
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";
@@ -16,6 +17,8 @@ export const inferGitClient = (prUrl: string): GitClientType => {
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}`);
@@ -36,4 +39,42 @@ export const inferGitApiUrl = (prUrl: string, apiVersion = "v4"): string => {
}
return `${baseUrl}/api/${apiVersion}`;
};
/**
* 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

@@ -13,8 +13,8 @@ export interface GitPullRequest {
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,
}
@@ -34,12 +34,14 @@ export interface BackportPullRequest {
reviewers: string[], // pr list of reviewers
assignees: string[], // pr list of assignees
labels: string[], // pr list of assigned labels
branchName?: string,
comments: string[], // pr list of additional comments
// branchName?: string,
}
export enum GitClientType {
GITHUB = "github",
GITLAB = "gitlab",
CODEBERG = "codeberg",
}
export enum GitRepoState {

View File

@@ -1,5 +1,5 @@
import GitClient from "@bp/service/git/git-client";
import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
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";
@@ -11,20 +11,26 @@ 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) {
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 "GitHub";
return this.apiUrl.includes(GitClientType.CODEBERG.toString()) ? "Codeberg" : "GitHub";
}
getDefaultGitEmail(): string {
@@ -32,7 +38,7 @@ export default class GitHubClient implements GitClient {
}
async getPullRequest(owner: string, repo: string, prNumber: number, squash = true): Promise<GitPullRequest> {
this.logger.info(`Getting pull request ${owner}/${repo}/${prNumber}.`);
this.logger.debug(`Fetching pull request ${owner}/${repo}/${prNumber}`);
const { data } = await this.octokit.rest.pulls.get({
owner: owner,
repo: repo,
@@ -66,7 +72,7 @@ export default class GitHubClient implements GitClient {
// WRITE
async createPullRequest(backport: BackportPullRequest): Promise<string> {
this.logger.info(`Creating pull request ${backport.head} -> ${backport.base}.`);
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({
@@ -117,6 +123,19 @@ export default class GitHubClient implements GitClient {
);
}
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;

View File

@@ -24,9 +24,9 @@ export default class GitHubMapper implements GitResponseMapper<PullRequest, "ope
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),
labels: pr.labels.map(l => l.name),
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) ?? [],
labels: pr.labels?.map(l => l.name) ?? [],
sourceRepo: await this.mapSourceRepo(pr),
targetRepo: await this.mapTargetRepo(pr),
nCommits: pr.commits,

View File

@@ -12,7 +12,6 @@ export default class OctokitFactory {
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: "kiegroup/git-backporting",

View File

@@ -1,6 +1,6 @@
import LoggerService from "@bp/service/logger/logger-service";
import GitClient from "@bp/service/git/git-client";
import { GitPullRequest, BackportPullRequest } from "@bp/service/git/git.types";
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";
@@ -30,6 +30,10 @@ export default class GitLabClient implements GitClient {
this.mapper = new GitLabMapper(this.client);
}
getClientType(): GitClientType {
return GitClientType.GITLAB;
}
getDefaultGitUser(): string {
return "Gitlab";
}
@@ -69,7 +73,7 @@ export default class GitLabClient implements GitClient {
// WRITE
async createPullRequest(backport: BackportPullRequest): Promise<string> {
this.logger.info(`Creating pull request ${backport.head} -> ${backport.base}.`);
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);
@@ -96,6 +100,18 @@ export default class GitLabClient implements GitClient {
);
}
// 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);
@@ -141,7 +157,6 @@ export default class GitLabClient implements GitClient {
return mr.web_url;
}
/**
* Retrieve a gitlab user given its username
* @param username
@@ -170,9 +185,18 @@ export default class GitLabClient implements GitClient {
* @returns {{owner: string, project: string}}
*/
private extractMergeRequestData(mrUrl: string): {namespace: string, project: string, id: number} {
const elems: string[] = mrUrl.replace("/-/", "/").split("/");
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: elems[elems.length - 4],
namespace: namespace,
project: elems[elems.length - 3],
id: parseInt(mrUrl.substring(mrUrl.lastIndexOf("/") + 1, mrUrl.length)),
};

View File

@@ -41,8 +41,8 @@ export default class GitLabMapper implements GitResponseMapper<MergeRequestSchem
sourceRepo: await this.mapSourceRepo(mr),
targetRepo: await this.mapTargetRepo(mr),
// if commits list is provided use that as source
nCommits: (commits && commits.length > 1) ? commits.length : 1,
commits: (commits && commits.length > 1) ? commits : this.getSha(mr)
nCommits: (commits && commits.length > 0) ? commits.length : 1,
commits: (commits && commits.length > 0) ? commits : this.getSha(mr)
};
}

View File

@@ -5,32 +5,44 @@ export default class ConsoleLoggerService implements LoggerService {
private readonly logger: Logger;
private readonly verbose: boolean;
private context?: string;
constructor(verbose = true) {
this.logger = new Logger();
this.verbose = verbose;
}
setContext(newContext: string) {
this.context = newContext;
}
clearContext() {
this.context = undefined;
}
trace(message: string): void {
this.logger.log("[TRACE]", message);
this.logger.log("TRACE", this.fromContext(message));
}
debug(message: string): void {
if (this.verbose) {
this.logger.log("[DEBUG]", message);
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,10 @@
*/
export default interface LoggerService {
setContext(newContext: string): void;
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

@@ -8,7 +8,13 @@ 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 } from "@bp/service/git/git-util";
import { inferGitClient, inferGitApiUrl, getGitTokenFromEnv } from "@bp/service/git/git-util";
interface Git {
gitClientType: GitClientType;
gitClientApi: GitClient;
gitCli: GitCLIService;
}
/**
* Main runner implementation, it implements the core logic flow
@@ -30,13 +36,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);
}
}
@@ -50,82 +56,108 @@ 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
const gitClientType: GitClientType = inferGitClient(args.pullRequest);
// right now the apiVersion is set to v4
const apiUrl = inferGitApiUrl(args.pullRequest);
const gitApi: GitClient = GitClientFactory.getOrCreate(gitClientType, args.auth, apiUrl);
// 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}`);
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
this.logger.debug("Cloning repo..");
await git.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, configs.targetBranch);
await git.gitCli.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, backportPR.base);
// 5. create new branch from target one and checkout
this.logger.debug("Creating local branch..");
let backportBranch = backportPR.branchName;
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 = originalPR.commits!.map(c => c.slice(0, 7)).join("-");
backportBranch = `bp-${configs.targetBranch}-${concatenatedCommits}`;
}
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);
}
await git.createLocalBranch(configs.folder, backportBranch);
await git.gitCli.createLocalBranch(configs.folder, backportPR.head);
// 6. fetch pull request remote if source owner != target owner or pull request still open
if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner ||
configs.originalPullRequest.state === "open") {
this.logger.debug("Fetching pull request remote..");
const prefix = gitClientType === GitClientType.GITHUB ? "pull" : "merge-requests"; // default is for gitlab
await git.fetch(configs.folder, `${prefix}/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`);
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
this.logger.debug("Cherry picking commits..");
for (const sha of originalPR.commits!) {
await git.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption);
await git.gitCli.cherryPick(configs.folder, sha, configs.mergeStrategy, configs.mergeStrategyOption);
}
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,
labels: backportPR.labels,
};
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)
const prUrl = 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

@@ -402,4 +402,94 @@ describe("cli args parser", () => {
expect(args.strategy).toEqual("ort");
expect(args.strategyOption).toEqual("ours");
});
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.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.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);
});
test("invalid execution with empty target branch", () => {
addProcessArgs([
"-tb",
" ",
"-pr",
"https://localhost/whatever/pulls/1"
]);
expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided");
});
test("invalid execution with missing mandatory target branch", () => {
addProcessArgs([
"-pr",
"https://localhost/whatever/pulls/1"
]);
expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided");
});
test("invalid execution with missin mandatory pull request", () => {
addProcessArgs([
"-tb",
"target",
]);
expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided");
});
});

View File

@@ -236,4 +236,82 @@ describe("gha args parser", () => {
expect(args.strategy).toEqual("ort");
expect(args.strategyOption).toEqual("ours");
});
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);
});
test("invalid execution with empty target branch", () => {
spyGetInput({
"target-branch": " ",
"pull-request": "https://localhost/whatever/pulls/1"
});
expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches 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: pull request and target branches must be provided");
});
test("invalid execution with missin mandatory pull request", () => {
spyGetInput({
"target-branch": "target,old",
});
expect(() => parser.parse()).toThrowError("Missing option: pull request and target branches must be provided");
});
});

View File

@@ -0,0 +1,422 @@
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, true);
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, true);
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, true);
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, true);
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: "v1, v2, v3",
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

@@ -5,7 +5,7 @@ import GitClientFactory from "@bp/service/git/git-client-factory";
import { GitClientType } from "@bp/service/git/git.types";
import { mockGitHubClient } from "../../../support/mock/git-client-mock-support";
import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../../support/utils";
import { mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, repo, targetOwner, multipleCommitsPullRequestFixture } from "../../../support/mock/github-data";
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";
@@ -13,14 +13,14 @@ 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/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`,
"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/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`,
"pullRequest": `https://github.com/${TARGET_OWNER}/${REPO}/pull/${MERGED_PR_FIXTURE.number}`,
"targetBranch": "prod",
"gitUser": "Me",
"gitEmail": "me@email.com",
@@ -39,10 +39,10 @@ jest.spyOn(GitHubClient.prototype, "getPullRequest");
describe("github 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}`;
const multipleCommitsPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${multipleCommitsPullRequestFixture.number}`;
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;
@@ -77,7 +77,6 @@ describe("github pull request config parser", () => {
test("parse configs from pull request", async () => {
const args: Args = {
dryRun: false,
auth: "",
pullRequest: mergedPRUrl,
targetBranch: "prod",
gitUser: "GitHub",
@@ -99,8 +98,7 @@ describe("github pull request config parser", () => {
user: "GitHub",
email: "noreply@github.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.auth).toEqual(undefined);
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
@@ -128,26 +126,18 @@ describe("github pull request config parser", () => {
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"]
});
expect(configs.backportPullRequest).toEqual({
author: "GitHub",
url: undefined,
htmlUrl: undefined,
title: "[prod] PR Title",
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: [],
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,
comments: [],
});
});
@@ -169,7 +159,6 @@ describe("github pull request config parser", () => {
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",
@@ -199,7 +188,6 @@ describe("github pull request config parser", () => {
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"
@@ -247,7 +235,7 @@ describe("github pull request config parser", () => {
inheritReviewers: true,
};
await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!");
await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged");
});
test("override backport pr data inheriting reviewers", async () => {
@@ -264,6 +252,7 @@ describe("github pull request config parser", () => {
reviewers: [],
assignees: [],
inheritReviewers: true,
bpBranchName: "custom-branch"
};
const configs: Configs = await configParser.parseAndValidate(args);
@@ -279,7 +268,6 @@ describe("github pull request config parser", () => {
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,
@@ -308,26 +296,60 @@ describe("github pull request config parser", () => {
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
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: [],
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,
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, true);
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: [],
});
});
@@ -360,7 +382,6 @@ describe("github pull request config parser", () => {
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,
@@ -389,26 +410,18 @@ describe("github pull request config parser", () => {
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
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: [],
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,
comments: [],
});
});
@@ -441,7 +454,6 @@ describe("github pull request config parser", () => {
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,
@@ -470,26 +482,18 @@ describe("github pull request config parser", () => {
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
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: [],
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,
comments: [],
});
});
@@ -524,7 +528,6 @@ describe("github pull request config parser", () => {
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,
@@ -553,26 +556,18 @@ describe("github pull request config parser", () => {
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
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", "original-label"],
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,
comments: [],
});
});
@@ -596,7 +591,6 @@ describe("github pull request config parser", () => {
email: "noreply@github.com"
});
expect(configs.auth).toEqual(undefined);
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
@@ -624,26 +618,18 @@ describe("github pull request config parser", () => {
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"]
});
expect(configs.backportPullRequest).toEqual({
author: "GitHub",
url: undefined,
htmlUrl: undefined,
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: [],
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,
comments: [],
});
});
@@ -667,7 +653,6 @@ describe("github pull request config parser", () => {
email: "me@email.com"
});
expect(configs.auth).toEqual("my-auth-token");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
@@ -696,26 +681,18 @@ describe("github pull request config parser", () => {
nCommits: 2,
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"],
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
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:", "original-label"],
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,
comments: [],
});
});
@@ -746,7 +723,6 @@ describe("github pull request config parser", () => {
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: 8632,
@@ -774,26 +750,93 @@ describe("github pull request config parser", () => {
nCommits: 2,
commits: ["0404fb922ab75c3a8aecad5c97d9af388df04695", "11da4e38aa3e577ffde6d546f1c52e53b04d3151"]
});
expect(configs.backportPullRequest).toEqual({
author: "GitHub",
url: undefined,
htmlUrl: undefined,
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, true);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledTimes(1);
expect(GitHubMapper.prototype.mapPullRequest).toBeCalledWith(expect.anything(), []);
expect(configs.dryRun).toEqual(false);
expect(configs.git).toEqual({
user: "Me",
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 2368,
author: "gh-user",
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
htmlUrl: "https://github.com/owner/reponame/pull/2368",
state: "closed",
merged: true,
mergedBy: "that-s-a-user",
title: "PR Title",
body: "Please review and merge",
reviewers: ["requested-gh-user", "gh-user"],
assignees: [],
labels: ["original-label"],
targetRepo: {
owner: "owner",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
},
sourceRepo: {
owner: "owner",
owner: "fork",
project: "reponame",
cloneUrl: "https://github.com/owner/reponame.git"
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"],
});
});
});

View File

@@ -0,0 +1,349 @@
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: [],
};
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: [],
};
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"
};
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"
};
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

@@ -6,7 +6,7 @@ import { GitClientType } from "@bp/service/git/git.types";
import { getAxiosMocked } from "../../../support/mock/git-client-mock-support";
import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, OPEN_MR } from "../../../support/mock/gitlab-data";
import GHAArgsParser from "@bp/service/args/gha/gha-args-parser";
import { createTestFile, removeTestFile, spyGetInput } from "../../../support/utils";
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";
@@ -70,6 +70,9 @@ describe("gitlab merge request config parser", () => {
});
beforeEach(() => {
// reset env tokens
resetEnvTokens();
argsParser = new GHAArgsParser();
configParser = new PullRequestConfigsParser();
});
@@ -100,7 +103,6 @@ describe("gitlab merge request config parser", () => {
email: "noreply@gitlab.com"
});
expect(configs.auth).toEqual(undefined);
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
@@ -128,26 +130,18 @@ describe("gitlab merge request config parser", () => {
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequest).toEqual({
author: "Gitlab",
url: undefined,
htmlUrl: undefined,
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: [],
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,
comments: [],
});
});
@@ -175,7 +169,6 @@ describe("gitlab merge request config parser", () => {
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual("/tmp/test");
expect(configs.git).toEqual({
user: "Gitlab",
@@ -205,7 +198,6 @@ describe("gitlab merge request config parser", () => {
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.git).toEqual({
user: "Gitlab",
email: "noreply@gitlab.com"
@@ -253,7 +245,7 @@ describe("gitlab merge request config parser", () => {
inheritReviewers: true,
};
await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!");
await expect(() => configParser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged");
});
test("override backport pr data inheriting reviewers", async () => {
@@ -285,7 +277,6 @@ describe("gitlab merge request config parser", () => {
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
@@ -313,26 +304,18 @@ describe("gitlab merge request config parser", () => {
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
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: [],
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,
comments: [],
});
});
@@ -365,7 +348,6 @@ describe("gitlab merge request config parser", () => {
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
@@ -393,26 +375,18 @@ describe("gitlab merge request config parser", () => {
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
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: [],
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,
comments: [],
});
});
@@ -445,7 +419,6 @@ describe("gitlab merge request config parser", () => {
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
@@ -473,26 +446,18 @@ describe("gitlab merge request config parser", () => {
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
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: [],
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,
comments: [],
});
});
@@ -527,7 +492,6 @@ describe("gitlab merge request config parser", () => {
email: "me@email.com"
});
expect(configs.auth).toEqual("");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
@@ -555,26 +519,18 @@ describe("gitlab merge request config parser", () => {
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
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", "gitlab-original-label"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
bpBranchName: undefined,
comments: [],
});
});
@@ -597,7 +553,6 @@ describe("gitlab merge request config parser", () => {
email: "noreply@gitlab.com"
});
expect(configs.auth).toEqual(undefined);
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
@@ -625,26 +580,18 @@ describe("gitlab merge request config parser", () => {
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequest).toEqual({
author: "Gitlab",
url: undefined,
htmlUrl: undefined,
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: [],
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,
comments: [],
});
});
@@ -667,7 +614,6 @@ describe("gitlab merge request config parser", () => {
email: "me@email.com"
});
expect(configs.auth).toEqual("my-token");
expect(configs.targetBranch).toEqual("prod");
expect(configs.folder).toEqual(process.cwd() + "/bp");
expect(configs.originalPullRequest).toEqual({
number: 1,
@@ -695,26 +641,18 @@ describe("gitlab merge request config parser", () => {
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.backportPullRequest).toEqual({
author: "Me",
url: undefined,
htmlUrl: undefined,
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:", "gitlab-original-label"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
bpBranchName: undefined,
comments: [],
});
});
@@ -741,7 +679,6 @@ describe("gitlab merge request config parser", () => {
expect(configs.dryRun).toEqual(true);
expect(configs.auth).toEqual("whatever");
expect(configs.targetBranch).toEqual("prod");
expect(configs.git).toEqual({
user: "Gitlab",
email: "noreply@gitlab.com"
@@ -773,5 +710,92 @@ describe("gitlab merge request config parser", () => {
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"],
};
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: ["gitlab-original-label"],
targetRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
sourceRepo: {
owner: "superuser",
project: "backporting-example",
cloneUrl: "https://my.gitlab.host.com/superuser/backporting-example.git"
},
nCommits: 1,
commits: ["ebb1eca696c42fd067658bd9b5267709f78ef38e"]
});
expect(configs.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"],
});
});
});

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

@@ -20,6 +20,11 @@ describe("git client factory test", () => {
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();
@@ -31,4 +36,10 @@ describe("git client factory test", () => {
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

@@ -23,6 +23,18 @@ describe("check git utilities", () => {
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);
});
@@ -39,7 +51,7 @@ describe("check git utilities", () => {
expect(inferGitClient("https://api.github.com/repos/owner/repo/pulls/1")).toStrictEqual(GitClientType.GITHUB);
});
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 client", ()=> {
expect(inferGitClient("https://codeberg.org/lampajr/backporting-example/pulls/1")).toStrictEqual(GitClientType.CODEBERG);
});
});

View File

@@ -1,7 +1,7 @@
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 { mergedPullRequestFixture, repo, targetOwner } from "../../../support/mock/github-data";
import { MERGED_PR_FIXTURE, REPO, TARGET_OWNER } from "../../../support/mock/github-data";
import { mockGitHubClient } from "../../../support/mock/git-client-mock-support";
describe("github service", () => {
@@ -22,7 +22,7 @@ describe("github service", () => {
});
test("get pull request: success", async () => {
const res: GitPullRequest = await gitClient.getPullRequest(targetOwner, repo, mergedPullRequestFixture.number);
const res: GitPullRequest = await gitClient.getPullRequest(TARGET_OWNER, REPO, MERGED_PR_FIXTURE.number);
expect(res.sourceRepo).toEqual({
owner: "fork",
project: "reponame",

View File

@@ -89,6 +89,7 @@ describe("github service", () => {
reviewers: [],
assignees: [],
labels: [],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
@@ -119,6 +120,7 @@ describe("github service", () => {
reviewers: ["superuser", "invalid"],
assignees: [],
labels: [],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
@@ -154,6 +156,7 @@ describe("github service", () => {
reviewers: [],
assignees: ["superuser", "invalid"],
labels: [],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
@@ -189,6 +192,7 @@ describe("github service", () => {
reviewers: ["superuser", "invalid"],
assignees: [],
labels: [],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
@@ -224,6 +228,7 @@ describe("github service", () => {
reviewers: [],
assignees: ["superuser", "invalid"],
labels: [],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
@@ -259,6 +264,7 @@ describe("github service", () => {
reviewers: [],
assignees: [],
labels: ["label1", "label2"],
comments: [],
};
const url: string = await gitClient.createPullRequest(backport);
@@ -280,4 +286,66 @@ describe("github service", () => {
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");
// 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");
});
});

View File

@@ -3,10 +3,11 @@ import Runner from "@bp/service/runner/runner";
import GitCLIService from "@bp/service/git/git-cli";
import GitHubClient from "@bp/service/git/github/github-client";
import CLIArgsParser from "@bp/service/args/cli/cli-args-parser";
import { addProcessArgs, createTestFile, removeTestFile, resetProcessArgs } from "../../support/utils";
import { addProcessArgs, createTestFile, removeTestFile, resetEnvTokens, resetProcessArgs } 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";
import { BackportPullRequest, GitClientType } from "@bp/service/git/git.types";
import { AuthTokenId } from "@bp/service/configs/configs.types";
const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT_PATHNAME = "./cli-github-runner-pr-merged-with-overrides.json";
const GITHUB_MERGED_PR_W_OVERRIDES_CONFIG_FILE_CONTENT = {
@@ -48,6 +49,9 @@ beforeEach(() => {
// reset process.env variables
resetProcessArgs();
// reset git env tokens
resetEnvTokens();
// mock octokit
mockGitHubClient();
@@ -229,12 +233,14 @@ describe("cli runner", () => {
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
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("same owner", async () => {
@@ -273,12 +279,14 @@ describe("cli runner", () => {
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/8632"),
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("closed and not merged pull request", async () => {
@@ -289,7 +297,7 @@ describe("cli runner", () => {
"https://github.com/owner/reponame/pull/6666"
]);
await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!");
await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged");
});
test("open pull request", async () => {
@@ -329,12 +337,14 @@ describe("cli runner", () => {
head: "bp-target-9174896",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/4444"),
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 () => {
@@ -348,7 +358,7 @@ describe("cli runner", () => {
"--body",
"New Body",
"--body-prefix",
"New Body Prefix - ",
"New Body Prefix\\r\\n\\r\\n",
"--bp-branch-name",
"bp_branch_name",
"--reviewers",
@@ -386,12 +396,14 @@ describe("cli runner", () => {
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
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 () => {
@@ -446,8 +458,10 @@ describe("cli runner", () => {
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("set custom labels with inheritance", async () => {
@@ -490,15 +504,17 @@ describe("cli runner", () => {
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: ["cherry-pick :cherries:", "original-label"],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("set custom lables without inheritance", async () => {
test("set custom labels without inheritance", async () => {
addProcessArgs([
"-tb",
"target",
@@ -537,12 +553,14 @@ describe("cli runner", () => {
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
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: ["first-label", "second-label"],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("using config file with overrides", async () => {
@@ -584,8 +602,10 @@ describe("cli runner", () => {
reviewers: [],
assignees: ["user3", "user4"],
labels: ["cli github cherry pick :cherries:", "original-label"],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
// to check: https://github.com/kiegroup/git-backporting/issues/52
@@ -626,12 +646,14 @@ describe("cli runner", () => {
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
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 () => {
@@ -672,12 +694,14 @@ describe("cli runner", () => {
head: "bp-target-0404fb9-11da4e3",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/8632"),
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("too long bp branch name", async () => {
@@ -724,12 +748,14 @@ describe("cli runner", () => {
head: truncatedBranch,
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
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 with different strategy", async () => {
@@ -774,11 +800,367 @@ describe("cli runner", () => {
head: "bp-target-0404fb9-11da4e3",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/8632"),
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("additional pr comments", async () => {
addProcessArgs([
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/8632",
"--comments",
"first comment; second comment",
"--body",
"New body"
]);
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(0);
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", 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/8632\r\n\r\nNew body",
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 () => {
addProcessArgs([
"-tb",
"v1, v2, v3",
"-pr",
"https://github.com/owner/reponame/pull/2368",
"-f",
"/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);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", 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 multiple bp names", async () => {
addProcessArgs([
"-tb",
"v1, v2, v3",
"-pr",
"https://github.com/owner/reponame/pull/2368",
"-f",
"/tmp/folder",
"--bp-branch-name",
"custom1, custom1, custom2, custom3",
]);
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, "custom1");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom2");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom3");
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);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(3);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom1");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom2");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom3");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom1",
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: "custom2",
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: "custom3",
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 one failure", async () => {
jest.spyOn(GitHubClient.prototype, "createPullRequest").mockImplementation((_backport: BackportPullRequest) => {
throw new Error("Mocked error");
});
addProcessArgs([
"-tb",
"v1, v2, v3",
"-pr",
"https://github.com/owner/reponame/pull/2368",
"-f",
"/tmp/folder",
"--bp-branch-name",
"custom-failure-head",
]);
await expect(() => runner.execute()).rejects.toThrowError("Failure occurred during one of the backports: [Error: Mocked error ; Error: Mocked error ; Error: Mocked error]");
const cwd = "/tmp/folder";
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
expect(GitCLIService.prototype.clone).toBeCalledTimes(3);
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v1");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v2");
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "v3");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(3);
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v1");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v2");
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "custom-failure-head-v3");
expect(GitCLIService.prototype.fetch).toBeCalledTimes(3);
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(3);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.push).toBeCalledTimes(3);
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v1");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v2");
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "custom-failure-head-v3");
expect(GitHubClient.prototype.createPullRequest).toBeCalledTimes(3);
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-failure-head-v1",
base: "v1",
title: "[v1] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-failure-head-v2",
base: "v2",
title: "[v2] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toBeCalledWith({
owner: "owner",
repo: "reponame",
head: "custom-failure-head-v3",
base: "v3",
title: "[v3] PR Title",
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: [],
comments: [],
});
expect(GitHubClient.prototype.createPullRequest).toThrowError();
});
test("auth using GITHUB_TOKEN takes precedence over GIT_TOKEN env variable", async () => {
process.env[AuthTokenId.GIT_TOKEN] = "mygittoken";
process.env[AuthTokenId.GITHUB_TOKEN] = "mygithubtoken";
addProcessArgs([
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/8632"
]);
await runner.execute();
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, "mygithubtoken", "https://api.github.com");
// Not interested in all subsequent calls, already tested in other test cases
});
test("auth arg takes precedence over GITHUB_TOKEN", async () => {
process.env[AuthTokenId.GITHUB_TOKEN] = "mygithubtoken";
addProcessArgs([
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/8632",
"-a",
"mytoken"
]);
await runner.execute();
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, "mytoken", "https://api.github.com");
// 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.GITLAB_TOKEN] = "mygitlabtoken";
process.env[AuthTokenId.CODEBERG_TOKEN] = "mycodebergtoken";
addProcessArgs([
"-tb",
"target",
"-pr",
"https://github.com/owner/reponame/pull/8632"
]);
await runner.execute();
expect(GitClientFactory.getOrCreate).toBeCalledTimes(1);
expect(GitClientFactory.getOrCreate).toBeCalledWith(GitClientType.GITHUB, undefined, "https://api.github.com");
// Not interested in all subsequent calls, already tested in other test cases
});
});

View File

@@ -3,11 +3,12 @@ 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, resetProcessArgs } from "../../support/utils";
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 = {
@@ -63,6 +64,9 @@ beforeEach(() => {
// reset process.env variables
resetProcessArgs();
// reset git env tokens
resetEnvTokens();
// create CLI arguments parser
parser = new CLIArgsParser();
@@ -181,6 +185,7 @@ describe("cli runner", () => {
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
@@ -193,7 +198,7 @@ describe("cli runner", () => {
"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!");
await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged");
});
test("merged pull request", async () => {
@@ -238,6 +243,7 @@ describe("cli runner", () => {
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
@@ -296,6 +302,7 @@ describe("cli runner", () => {
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
);
});
@@ -352,6 +359,7 @@ describe("cli runner", () => {
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
);
});
@@ -401,6 +409,7 @@ describe("cli runner", () => {
reviewers: ["superuser"],
assignees: [],
labels: ["cherry-pick :cherries:", "another-label", "gitlab-original-label"],
comments: [],
}
);
});
@@ -449,6 +458,7 @@ describe("cli runner", () => {
reviewers: ["superuser"],
assignees: [],
labels: ["cherry-pick :cherries:", "another-label"],
comments: [],
}
);
});
@@ -493,6 +503,51 @@ describe("cli runner", () => {
reviewers: [],
assignees: ["user3", "user4"],
labels: ["cli gitlab cherry pick :cherries:", "gitlab-original-label"],
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);
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: [],
}
);
});
@@ -540,7 +595,63 @@ describe("cli runner", () => {
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

@@ -3,7 +3,7 @@ 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, spyGetInput } from "../../support/utils";
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";
@@ -46,6 +46,9 @@ afterAll(() => {
});
beforeEach(() => {
// reset git env tokens
resetEnvTokens();
mockGitHubClient();
// create GHA arguments parser
@@ -121,12 +124,14 @@ describe("gha runner", () => {
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
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 () => {
@@ -135,7 +140,7 @@ describe("gha runner", () => {
"pull-request": "https://github.com/owner/reponame/pull/6666"
});
await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!");
await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged");
});
test("open pull request", async () => {
@@ -173,12 +178,14 @@ describe("gha runner", () => {
head: "bp-target-9174896",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/4444"),
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 () => {
@@ -187,7 +194,7 @@ describe("gha runner", () => {
"pull-request": "https://github.com/owner/reponame/pull/2368",
"title": "New Title",
"body": "New Body",
"body-prefix": "New Body Prefix - ",
"body-prefix": "New Body Prefix\\r\\n\\r\\n",
"bp-branch-name": "bp_branch_name",
"reviewers": "user1, user2",
"assignees": "user3, user4",
@@ -222,12 +229,14 @@ describe("gha runner", () => {
head: "bp_branch_name",
base: "target",
title: "New Title",
body: "New Body Prefix - New Body",
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 () => {
@@ -276,8 +285,10 @@ describe("gha runner", () => {
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("set custom labels with inheritance", async () => {
@@ -317,12 +328,14 @@ describe("gha runner", () => {
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge",
reviewers: ["gh-user", "that-s-a-user"],
assignees: [],
labels: ["cherry-pick :cherries:", "another-label", "original-label"],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
test("set custom labels without inheritance", async () => {
@@ -362,12 +375,14 @@ describe("gha runner", () => {
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
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 () => {
@@ -408,8 +423,10 @@ describe("gha runner", () => {
reviewers: [],
assignees: ["user3", "user4"],
labels: ["gha github cherry pick :cherries:", "original-label"],
comments: [],
}
);
expect(GitHubClient.prototype.createPullRequest).toReturnTimes(1);
});
// to check: https://github.com/kiegroup/git-backporting/issues/52
@@ -448,12 +465,14 @@ describe("gha runner", () => {
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
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 () => {
@@ -492,12 +511,14 @@ describe("gha runner", () => {
head: "bp-target-0404fb9-11da4e3",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/8632"),
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 () => {
@@ -537,11 +558,214 @@ describe("gha runner", () => {
head: "bp-target-28f63db",
base: "target",
title: "[target] PR Title",
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
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);
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);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", 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);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", undefined, undefined);
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc", 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);
});
});

View File

@@ -3,7 +3,7 @@ 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, spyGetInput } from "../../support/utils";
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";
@@ -59,6 +59,9 @@ afterAll(() => {
});
beforeEach(() => {
// reset git env tokens
resetEnvTokens();
// create GHA arguments parser
parser = new GHAArgsParser();
@@ -136,6 +139,7 @@ describe("gha runner", () => {
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
@@ -146,7 +150,7 @@ describe("gha runner", () => {
"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!");
await expect(() => runner.execute()).rejects.toThrow("Provided pull request is closed and not merged");
});
test("merged pull request", async () => {
@@ -187,6 +191,7 @@ describe("gha runner", () => {
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});
@@ -236,6 +241,7 @@ describe("gha runner", () => {
reviewers: ["user1", "user2"],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
);
});
@@ -286,6 +292,7 @@ describe("gha runner", () => {
reviewers: [],
assignees: ["user3", "user4"],
labels: [],
comments: [],
}
);
});
@@ -330,6 +337,7 @@ describe("gha runner", () => {
reviewers: ["superuser"],
assignees: [],
labels: ["cherry-pick :cherries:", "another-label", "gitlab-original-label"],
comments: [],
}
);
});
@@ -373,6 +381,7 @@ describe("gha runner", () => {
reviewers: ["superuser"],
assignees: [],
labels: ["cherry-pick :cherries:", "another-label"],
comments: [],
}
);
});
@@ -416,6 +425,49 @@ describe("gha runner", () => {
reviewers: [],
assignees: ["user3", "user4"],
labels: ["gha gitlab cherry pick :cherries:", "gitlab-original-label"],
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);
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: [],
}
);
});
@@ -461,6 +513,7 @@ describe("gha runner", () => {
reviewers: ["superuser"],
assignees: [],
labels: [],
comments: [],
}
);
});

View File

@@ -1,7 +1,11 @@
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
import { Moctokit } from "@kie/mock-github";
import { targetOwner, repo, mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, notFoundPullRequestNumber, multipleCommitsPullRequestFixture, multipleCommitsPullRequestCommits } from "./github-data";
import { CLOSED_NOT_MERGED_MR, MERGED_SQUASHED_MR, OPEN_MR, OPEN_PR_COMMITS, PROJECT_EXAMPLE, SUPERUSER} from "./gitlab-data";
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 } 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 } from "./gitlab-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();
@@ -18,10 +22,16 @@ export const getAxiosMocked = (url: string) => {
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("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;
}
@@ -34,18 +44,24 @@ export const getAxiosMocked = (url: string) => {
export const NEW_GITLAB_MR_ID = 999;
export const SECOND_NEW_GITLAB_MR_ID = 1000;
export const postAxiosMocked = (_url: string, data?: {source_branch: string,}) => {
export const postAxiosMocked = async (url: string, data?: {source_branch: string,}) => {
let responseData = undefined;
// gitlab
if (data?.source_branch === "bp-branch") {
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
};
} if (data?.source_branch === "bp-branch-2") {
} else if (data?.source_branch === "bp-branch-2") {
responseData = {
// we do not need the whole response
iid: SECOND_NEW_GITLAB_MR_ID,
@@ -79,7 +95,7 @@ export const putAxiosMocked = async (url: string, _data?: unknown) => {
// GITHUB - OCTOKIT
export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit => {
logger.debug("Setting up moctokit.");
logger.debug("Setting up moctokit..");
const mock = new Moctokit(apiUrl);
@@ -88,76 +104,82 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit =>
// valid requests
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: mergedPullRequestFixture.number
owner: TARGET_OWNER,
repo: REPO,
pull_number: MERGED_PR_FIXTURE.number
})
.reply({
status: 200,
data: mergedPullRequestFixture
data: MERGED_PR_FIXTURE
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: multipleCommitsPullRequestFixture.number
owner: TARGET_OWNER,
repo: REPO,
pull_number: MULT_COMMITS_PR_FIXTURE.number
})
.reply({
status: 200,
data: multipleCommitsPullRequestFixture
data: MULT_COMMITS_PR_FIXTURE
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: openPullRequestFixture.number
owner: TARGET_OWNER,
repo: REPO,
pull_number: OPEN_PR_FIXTURE.number
})
.reply({
status: 200,
data: openPullRequestFixture
data: OPEN_PR_FIXTURE
});
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: notMergedPullRequestFixture.number
owner: TARGET_OWNER,
repo: REPO,
pull_number: NOT_MERGED_PR_FIXTURE.number
})
.reply({
status: 200,
data: notMergedPullRequestFixture
data: NOT_MERGED_PR_FIXTURE
});
mock.rest.pulls
.listCommits({
owner: targetOwner,
repo: repo,
pull_number: multipleCommitsPullRequestFixture.number
owner: TARGET_OWNER,
repo: REPO,
pull_number: MULT_COMMITS_PR_FIXTURE.number
})
.reply({
status: 200,
data: multipleCommitsPullRequestCommits
data: MULT_COMMITS_PR_COMMITS
});
mock.rest.pulls
.create()
.reply({
repeat: REPEAT,
status: 201,
data: mergedPullRequestFixture
data: {
number: NEW_PR_NUMBER,
html_url: NEW_PR_URL,
}
});
mock.rest.pulls
.requestReviewers()
.reply({
repeat: REPEAT,
status: 201,
data: mergedPullRequestFixture
data: MERGED_PR_FIXTURE
});
mock.rest.issues
.addAssignees()
.reply({
repeat: REPEAT,
status: 201,
data: {}
});
@@ -165,23 +187,33 @@ export const mockGitHubClient = (apiUrl = "https://api.github.com"): Moctokit =>
mock.rest.issues
.addLabels()
.reply({
repeat: REPEAT,
status: 200,
data: {}
});
mock.rest.issues
.createComment()
.reply({
repeat: REPEAT,
status: 201,
data: {}
});
// invalid requests
mock.rest.pulls
.get({
owner: targetOwner,
repo: repo,
pull_number: notFoundPullRequestNumber
owner: TARGET_OWNER,
repo: REPO,
pull_number: 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",
@@ -474,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",
@@ -898,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",
@@ -1341,7 +1343,7 @@ export const notMergedPullRequestFixture = {
"changed_files": 2
};
export const multipleCommitsPullRequestFixture = {
export const MULT_COMMITS_PR_FIXTURE = {
"url": "https://api.github.com/repos/owner/reponame/pulls/8632",
"id": 1137188271,
"node_id": "PR_kwDOABTq6s5DyB2v",
@@ -1804,7 +1806,7 @@ export const multipleCommitsPullRequestFixture = {
"changed_files": 2
};
export const multipleCommitsPullRequestCommits = [
export const MULT_COMMITS_PR_COMMITS = [
{
"sha": "0404fb922ab75c3a8aecad5c97d9af388df04695",
"node_id": "C_kwDOImgs99oAKDA0MDRmYjkyMmFiNzVjM2E4YWVjYWQ1Yzk3ZDlhZjM4OGRmMDQ2OTU",

View File

@@ -161,6 +161,166 @@ export const PROJECT_EXAMPLE = {
}
};
export const NESTED_PROJECT_EXAMPLE = {
"id":1645,
"description":null,
"name":"Backporting Example",
"name_with_namespace":"Super User / Backporting Example",
"path":"backporting-example",
"path_with_namespace":"mysuperorg/6/mysuperproduct/mysuperunit/backporting-example",
"created_at":"2023-06-23T13:45:15.121Z",
"default_branch":"main",
"tag_list":[
],
"topics":[
],
"ssh_url_to_repo":"git@my.gitlab.host.com:mysuperorg/6/mysuperproduct/mysuperunit/backporting-example.git",
"http_url_to_repo":"https://my.gitlab.host.com/mysuperorg/6/mysuperproduct/mysuperunit/backporting-example.git",
"web_url":"https://my.gitlab.host.com/mysuperorg/6/mysuperproduct/mysuperunit/backporting-example",
"readme_url":"https://my.gitlab.host.com/mysuperorg/6/mysuperproduct/mysuperunit/backporting-example/-/blob/main/README.md",
"forks_count":0,
"avatar_url":null,
"star_count":0,
"last_activity_at":"2023-06-28T14:05:42.596Z",
"namespace":{
"id":70747,
"name":"Super User",
"path":"superuser",
"kind":"user",
"full_path":"superuser",
"parent_id":null,
"avatar_url":"/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"_links":{
"self":"https://my.gitlab.host.com/api/v4/projects/1645",
"issues":"https://my.gitlab.host.com/api/v4/projects/1645/issues",
"merge_requests":"https://my.gitlab.host.com/api/v4/projects/1645/merge_requests",
"repo_branches":"https://my.gitlab.host.com/api/v4/projects/1645/repository/branches",
"labels":"https://my.gitlab.host.com/api/v4/projects/1645/labels",
"events":"https://my.gitlab.host.com/api/v4/projects/1645/events",
"members":"https://my.gitlab.host.com/api/v4/projects/1645/members",
"cluster_agents":"https://my.gitlab.host.com/api/v4/projects/1645/cluster_agents"
},
"packages_enabled":true,
"empty_repo":false,
"archived":false,
"visibility":"private",
"owner":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"resolve_outdated_diff_discussions":false,
"container_expiration_policy":{
"cadence":"1d",
"enabled":false,
"keep_n":10,
"older_than":"90d",
"name_regex":".*",
"name_regex_keep":null,
"next_run_at":"2023-06-24T13:45:15.167Z"
},
"issues_enabled":true,
"merge_requests_enabled":true,
"wiki_enabled":true,
"jobs_enabled":true,
"snippets_enabled":true,
"container_registry_enabled":true,
"service_desk_enabled":false,
"service_desk_address":null,
"can_create_merge_request_in":true,
"issues_access_level":"enabled",
"repository_access_level":"enabled",
"merge_requests_access_level":"enabled",
"forking_access_level":"enabled",
"wiki_access_level":"enabled",
"builds_access_level":"enabled",
"snippets_access_level":"enabled",
"pages_access_level":"private",
"analytics_access_level":"enabled",
"container_registry_access_level":"enabled",
"security_and_compliance_access_level":"private",
"releases_access_level":"enabled",
"environments_access_level":"enabled",
"feature_flags_access_level":"enabled",
"infrastructure_access_level":"enabled",
"monitor_access_level":"enabled",
"emails_disabled":null,
"shared_runners_enabled":true,
"lfs_enabled":true,
"creator_id":14041,
"import_url":null,
"import_type":null,
"import_status":"none",
"import_error":null,
"open_issues_count":0,
"description_html":"",
"updated_at":"2023-06-28T14:05:42.596Z",
"ci_default_git_depth":20,
"ci_forward_deployment_enabled":true,
"ci_job_token_scope_enabled":false,
"ci_separated_caches":true,
"ci_allow_fork_pipelines_to_run_in_parent_project":true,
"build_git_strategy":"fetch",
"keep_latest_artifact":true,
"restrict_user_defined_variables":false,
"runners_token":"TOKEN",
"runner_token_expiration_interval":null,
"group_runners_enabled":true,
"auto_cancel_pending_pipelines":"enabled",
"build_timeout":3600,
"auto_devops_enabled":false,
"auto_devops_deploy_strategy":"continuous",
"ci_config_path":"",
"public_jobs":true,
"shared_with_groups":[
],
"only_allow_merge_if_pipeline_succeeds":false,
"allow_merge_on_skipped_pipeline":null,
"request_access_enabled":true,
"only_allow_merge_if_all_discussions_are_resolved":false,
"remove_source_branch_after_merge":true,
"printing_merge_request_link_enabled":true,
"merge_method":"merge",
"squash_option":"default_off",
"enforce_auth_checks_on_uploads":true,
"suggestion_commit_message":null,
"merge_commit_template":null,
"squash_commit_template":null,
"issue_branch_template":null,
"autoclose_referenced_issues":true,
"approvals_before_merge":0,
"mirror":false,
"external_authorization_classification_label":null,
"marked_for_deletion_at":null,
"marked_for_deletion_on":null,
"requirements_enabled":false,
"requirements_access_level":"enabled",
"security_and_compliance_enabled":true,
"compliance_frameworks":[
],
"issues_template":null,
"merge_requests_template":null,
"merge_pipelines_enabled":false,
"merge_trains_enabled":false,
"allow_pipeline_trigger_approve_deployment":false,
"permissions":{
"project_access":{
"access_level":50,
"notification_level":3
},
"group_access":null
}
};
export const MERGED_SQUASHED_MR = {
"id":807106,
"iid":1,
@@ -529,6 +689,29 @@ export const CLOSED_NOT_MERGED_MR = {
}
};
export const MERGED_SQUASHED_MR_COMMITS = [
{
"id":"e4dd336a4a20f394df6665994df382fb1d193a11",
"short_id":"e4dd336a",
"created_at":"2023-06-29T09:59:10.000Z",
"parent_ids":[
],
"title":"Add new file",
"message":"Add new file",
"author_name":"Super User",
"author_email":"superuser@email.com",
"authored_date":"2023-06-29T09:59:10.000Z",
"committer_name":"Super User",
"committer_email":"superuser@email.com",
"committed_date":"2023-06-29T09:59:10.000Z",
"trailers":{
},
"web_url":"https://gitlab.com/superuser/backporting-example/-/commit/e4dd336a4a20f394df6665994df382fb1d193a11"
},
];
export const OPEN_PR_COMMITS = [
{
"id":"974519f65c9e0ed65277cd71026657a09fca05e7",
@@ -580,3 +763,138 @@ export const SUPERUSER = {
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
};
export const NESTED_NAMESPACE_MR = {
"id":807106,
"iid":1,
"project_id":1645,
"title":"Update test.txt",
"description":"This is the body",
"state":"merged",
"created_at":"2023-06-28T14:32:40.943Z",
"updated_at":"2023-06-28T14:37:12.108Z",
"merged_by":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"merge_user":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"merged_at":"2023-06-28T14:37:11.667Z",
"closed_by":null,
"closed_at":null,
"target_branch":"main",
"source_branch":"feature",
"user_notes_count":0,
"upvotes":0,
"downvotes":0,
"author":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"assignees":[
{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
}
],
"assignee":{
"id":14041,
"username":"superuser",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
"reviewers":[
{
"id":1404188,
"username":"superuser1",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
},
{
"id":1404199,
"username":"superuser2",
"name":"Super User",
"state":"active",
"avatar_url":"https://my.gitlab.host.com/uploads/-/system/user/avatar/14041/avatar.png",
"web_url":"https://my.gitlab.host.com/superuser"
}
],
"source_project_id":1645,
"target_project_id":1645,
"labels":[
"gitlab-original-label"
],
"draft":false,
"work_in_progress":false,
"milestone":null,
"merge_when_pipeline_succeeds":false,
"merge_status":"can_be_merged",
"detailed_merge_status":"not_open",
"sha":"9e15674ebd48e05c6e428a1fa31dbb60a778d644",
"merge_commit_sha":"4d369c3e9a8d1d5b7e56c892a8ab2a7666583ac3",
"squash_commit_sha":"ebb1eca696c42fd067658bd9b5267709f78ef38e",
"discussion_locked":null,
"should_remove_source_branch":true,
"force_remove_source_branch":true,
"reference":"!2",
"references":{
"short":"!2",
"relative":"!2",
"full":"superuser/backporting-example!2"
},
"web_url":"https://my.gitlab.host.com/mysuperorg/6/mysuperproduct/mysuperunit/backporting-example/-/merge_requests/4",
"time_stats":{
"time_estimate":0,
"total_time_spent":0,
"human_time_estimate":null,
"human_total_time_spent":null
},
"squash":true,
"squash_on_merge":true,
"task_completion_status":{
"count":0,
"completed_count":0
},
"has_conflicts":false,
"blocking_discussions_resolved":true,
"approvals_before_merge":null,
"subscribed":true,
"changes_count":"1",
"latest_build_started_at":null,
"latest_build_finished_at":null,
"first_deployed_to_production_at":null,
"pipeline":null,
"head_pipeline":null,
"diff_refs":{
"base_sha":"2c553a0c4c133a51806badce5fa4842b7253cb3b",
"head_sha":"9e15674ebd48e05c6e428a1fa31dbb60a778d644",
"start_sha":"2c553a0c4c133a51806badce5fa4842b7253cb3b"
},
"merge_error":null,
"first_contribution":false,
"user":{
"can_merge":true
}
};

View File

@@ -1,4 +1,5 @@
import * as core from "@actions/core";
import { AuthTokenId } from "@bp/service/configs/configs.types";
import * as fs from "fs";
export const addProcessArgs = (args: string[]) => {
@@ -9,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");