mirror of
https://code.forgejo.org/actions/git-backporting.git
synced 2025-02-22 10:35:43 -05:00
feat: pull request backporting
feat: backport still open pull requests
This commit is contained in:
commit
b3936e019a
53 changed files with 48467 additions and 0 deletions
3
.commitlintrc.json
Normal file
3
.commitlintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
}
|
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
build
|
||||
dist
|
51
.eslintrc
Normal file
51
.eslintrc
Normal file
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": [
|
||||
"error",
|
||||
{
|
||||
"fixToUnknown": true
|
||||
}
|
||||
],
|
||||
"curly": "error",
|
||||
"no-empty": "error",
|
||||
"no-console": "error",
|
||||
"no-alert": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"varsIgnorePattern": "^_",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-fallthrough": "off",
|
||||
"arrow-parens": [
|
||||
"error",
|
||||
"as-needed"
|
||||
]
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2020": true
|
||||
}
|
||||
}
|
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
version: 2
|
||||
updates:
|
||||
# Enable version updates for npm
|
||||
- package-ecosystem: 'npm'
|
||||
# Look for `package.json` and `lock` files in the `root` directory
|
||||
directory: '/'
|
||||
# Check the npm registry for updates every day (weekdays)
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
# consider only security updates
|
||||
open-pull-requests-limit: 0
|
20
.github/pull_request_template.md
vendored
Normal file
20
.github/pull_request_template.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
**Thank you for submitting this pull request**
|
||||
|
||||
fix _(please add the issue ID if it exists)_
|
||||
|
||||
### Referenced pull requests
|
||||
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
### Checklist
|
||||
- [ ] Tests added if applicable.
|
||||
- [ ] Documentation updated if applicable.
|
||||
|
||||
> **Note:** `dist/cli/index.js` and `dist/gha/index.js` are automatically generated by git hooks and gh workflows.
|
27
.github/workflows/ci.yml
vendored
Normal file
27
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: "Main CI"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16]
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: true
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm test
|
24
.github/workflows/pull-request.yml
vendored
Normal file
24
.github/workflows/pull-request.yml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: "Pull Request Checks"
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16]
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm test
|
14
.github/workflows/release-please.yml
vendored
Normal file
14
.github/workflows/release-please.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
name: release-please
|
||||
jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: google-github-actions/release-please-action@v3
|
||||
with:
|
||||
release-type: node
|
||||
package-name: "@lampajr/bper"
|
19
.github/workflows/release.yml
vendored
Normal file
19
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
# This workflow create a new tag and then publish it to NPM.
|
||||
|
||||
name: "Publish Package"
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: "Publish"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm install && npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
.env
|
||||
node_modules/
|
||||
*.code-workspace
|
||||
locally_execution/**/*
|
||||
test/**/_temp/**/*
|
||||
yarn.lock
|
||||
coverage/
|
||||
test-report.xml
|
||||
.idea/
|
||||
.vscode/
|
||||
build/
|
||||
# dist/
|
4
.husky/commit-msg
Executable file
4
.husky/commit-msg
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx commitlint --edit $1
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run lint && npm run build && git add dist && rm -rf build
|
4
.husky/pre-push
Executable file
4
.husky/pre-push
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run lint && npm run test
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 GitHub, Inc. and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
135
README.md
Normal file
135
README.md
Normal file
|
@ -0,0 +1,135 @@
|
|||
<h1 align="center">
|
||||
BPER: Git Backporter </br>
|
||||
:outbox_tray: :inbox_tray:
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/lampajr/backporting">
|
||||
<img alt="CI Checks Status" src="https://github.com/lampajr/backporting/actions/workflows/ci.yml/badge.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
**BPer** is a [NodeJS](https://nodejs.org/) command line tool that provides capabilities to *backport* [1] pull requests in an automated way. This tool also comes with a predefined GitHub action in order to make CI/CD integration easier for all users.
|
||||
|
||||
[1] *backporting* is an action aiming to move a change (usually a commit) from a branch (usually the main one) to another one, which is generally referring to a still maintained release branch. Keeping it simple: it is about to move a specific change or a set of them from one branch to another.
|
||||
|
||||
Table of content
|
||||
----------------
|
||||
|
||||
* **[Usage](#usage)**
|
||||
* **[GitHub Action](#github-action)**
|
||||
* **[Limitations](#limitations)**
|
||||
* **[Contributions](#contributing)**
|
||||
* **[License](#license)**
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
This tool is released on the [public npm registry](https://www.npmjs.com/), therefore it can be easily installed using `npm`:
|
||||
|
||||
```bash
|
||||
$ npm install -g @lampajr/bper
|
||||
```
|
||||
|
||||
Then it can be used as any other command line tool:
|
||||
|
||||
```bash
|
||||
$ bper -tb <branch> -pr <pull-request-url> -a <github-token> [-f <your-folder>]
|
||||
```
|
||||
|
||||
### Inputs
|
||||
|
||||
This toold comes with some inputs that allow users to override the default behavior, here the full list of available inputs:
|
||||
|
||||
| **Name** | **Command** | **Required** | **Description** | **Default** |
|
||||
|---------------|----------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
|
||||
| Version | -V, --version | - | Current version of the tool | |
|
||||
| Help | -h, --help | - | Display the help message | |
|
||||
| Target Branch | -tb, --target-branch | Y | Branch where the changes must be backported to | |
|
||||
| Pull Request | -pr, --pull-request | Y | Original pull request url, the one that must be backported, e.g., https://github.com/lampajr/backporting/pull/1 | |
|
||||
| Auth | -a, --auth | N | `GITHUB_TOKEN` or a `repo` scoped [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) | "" |
|
||||
| Folder | -f, --folder | N | Local folder where the repo will be checked out, e.g., /tmp/folder | {cwd}/bp |
|
||||
| Dry Run | -d, --dry-run | N | If enabled the tool does not push nor create anything remotely, use this to skip PR creation | false |
|
||||
|
||||
## GitHub Action
|
||||
|
||||
This action can be used in any GitHub workflow, below you can find a simple example of manually triggered workflow backporting a specific pull request (provided as input).
|
||||
|
||||
```yml
|
||||
name: Pull Request Backporting using BPer
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
targetBranch:
|
||||
description: 'Target branch'
|
||||
required: true
|
||||
type: string
|
||||
pullRequest:
|
||||
description: 'Pull request'
|
||||
required: true
|
||||
type: string
|
||||
dryRun:
|
||||
description: 'Dry run'
|
||||
required: false
|
||||
default: "true"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
backporting:
|
||||
name: "Backporting"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Backporting
|
||||
uses: lampajr/backporting@main
|
||||
with:
|
||||
target-branch: ${{ inputs.targetBranch }}
|
||||
pull-request: ${{ inputs.pullRequest }}
|
||||
auth: ${{ secrets.GITHUB_TOKEN }}
|
||||
dry-run: ${{ inputs.dryRun }}
|
||||
```
|
||||
|
||||
You can also use this action with other events - you'll just need to specify `target-branch` and `pull-request` params.
|
||||
|
||||
For a complete description of all inputs see [Inputs section](#inputs).
|
||||
|
||||
## Limitations
|
||||
|
||||
**BPer** is in development mode, this means that it has many limitations right now. I'll try to summarize the most importan ones:
|
||||
|
||||
- No way to override backporting pull request fields like body, reviewers and so on.
|
||||
- You can backport pull requests only.
|
||||
- It only works for [GitHub](https://github.com/).
|
||||
- Integrated in GitHub Actions CI/CD only.
|
||||
|
||||
Based on these limitations, the next **Future Works** could be the following:
|
||||
- Give users the possibility to override/customize the backporting pull request.
|
||||
- Provide a way to backport single commit too (or a set of them), even if no original pull request is present.
|
||||
- Integrate this tool with other git management services (like GitLab and Bitbucket) to make it as generic as possible.
|
||||
- Provide some reusable GitHub workflows.
|
||||
|
||||
## Contributing
|
||||
|
||||
This is an open source project, and you are more than welcome to contribute :heart:!
|
||||
|
||||
Every change must be submitted through a GitHub pull request (PR). Backporting uses continuous integration (CI). The CI runs checks against your branch after you submit the PR to ensure that your PR doesn’t introduce errors. If the CI identifies a potential problem, our friendly PR maintainers will help you resolve it.
|
||||
|
||||
> **Note**: this project follows [git-conventional-commits](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13) standards, thanks to the [commit-msg hook](./.husky/commit-msg) you are not allowed to use commits that do not follow those standards.
|
||||
|
||||
1. Fork it (https://github.com/lampajr/backporting).
|
||||
|
||||
2. Create your feature branch: (git checkout -b feature).
|
||||
|
||||
3. Commit your changes with a comment: (git commit -am 'Add some feature').
|
||||
|
||||
4. Push to the branch to GitHub: (git push origin feature).
|
||||
|
||||
5. Create a new pull request against `main` branch.
|
||||
|
||||
> **Note**: you don't need to take care about typescript compilation and minifycation, there are automated [git hooks](./.husky) taking care of that!
|
||||
|
||||
## License
|
||||
|
||||
Backporting (BPer) open source project is licensed under the [MIT](./LICENSE) license.
|
25
action.yml
Normal file
25
action.yml
Normal file
|
@ -0,0 +1,25 @@
|
|||
name: "Backporting GitHub Action"
|
||||
description: "GitHub action providing an automated way to backport pull requests from one branch to another"
|
||||
inputs:
|
||||
dry-run:
|
||||
description: "If enabled the tool does not create any pull request nor push anything remotely."
|
||||
required: false
|
||||
default: "false"
|
||||
auth:
|
||||
description: "GITHUB_TOKEN or a `repo` scoped Personal Access Token (PAT)."
|
||||
default: ${{ github.token }}
|
||||
required: false
|
||||
pull-request:
|
||||
description: "URL of the pull request to backport, e.g., https://github.com/lampajr/backporting/pull/1."
|
||||
required: true
|
||||
target-branch:
|
||||
description: "Branch where the pull request must be backported to."
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: node16
|
||||
main: dist/gha/index.js
|
||||
|
||||
branding:
|
||||
icon: 'git-merge'
|
||||
color: 'blue'
|
16133
dist/cli/index.js
vendored
Executable file
16133
dist/cli/index.js
vendored
Executable file
File diff suppressed because one or more lines are too long
15562
dist/gha/index.js
vendored
Executable file
15562
dist/gha/index.js
vendored
Executable file
File diff suppressed because one or more lines are too long
16
jest.config.ts
Normal file
16
jest.config.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import type { Config } from "@jest/types";
|
||||
// Sync object
|
||||
const jestConfig: Config.InitialOptions = {
|
||||
verbose: true,
|
||||
transform: {
|
||||
"^.+\\.tsx?$": "ts-jest",
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"^@bp/(.*)$": "<rootDir>/src/$1",
|
||||
},
|
||||
clearMocks: true,
|
||||
resetMocks: true,
|
||||
modulePathIgnorePatterns: ["<rootDir>/build/", "<rootDir>/dist/"],
|
||||
coveragePathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/test/", "<rootDir>/build/", "<rootDir>/dist/"]
|
||||
};
|
||||
export default jestConfig;
|
12557
package-lock.json
generated
Normal file
12557
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
75
package.json
Normal file
75
package.json
Normal file
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"name": "@lampajr/bper",
|
||||
"version": "1.0.5",
|
||||
"description": "BPer is a tool to execute automatic git backporting.",
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"main": "./dist/gha/index.js",
|
||||
"bin": {
|
||||
"bper": "./dist/cli/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/cli/index.js"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"clean": "rm -rf ./build ./dist",
|
||||
"compile": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
||||
"package": "npm run package:cli && npm run package:gha",
|
||||
"package:cli": "ncc build ./build/src/bin/cli.js -o dist/cli",
|
||||
"package:gha": "ncc build ./build/src/bin/gha.js -o dist/gha",
|
||||
"build": "npm run clean && npm run compile && npm run package",
|
||||
"test": "jest",
|
||||
"test:report": "npm test -- --coverage --testResultsProcessor=jest-sonar-reporter",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"ts-node": "ts-node",
|
||||
"preversion": "npm install && npm test",
|
||||
"postversion": "git push && git push --tags"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lampajr/backporting.git"
|
||||
},
|
||||
"keywords": [
|
||||
"backporting",
|
||||
"pull-requests",
|
||||
"github-action",
|
||||
"cherry-pick"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/lampajr/backporting/issues"
|
||||
},
|
||||
"homepage": "https://github.com/lampajr/backporting#readme",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.4.0",
|
||||
"@commitlint/config-conventional": "^17.4.0",
|
||||
"@kie/mock-github": "^0.1.2",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/jest": "^29.2.4",
|
||||
"@types/node": "^18.11.17",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.0",
|
||||
"@typescript-eslint/parser": "^5.47.0",
|
||||
"@vercel/ncc": "^0.36.0",
|
||||
"eslint": "^8.30.0",
|
||||
"husky": "^8.0.2",
|
||||
"jest": "^29.3.1",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"semver": "^7.3.8",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.8.1",
|
||||
"tsc-alias": "^1.8.2",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@octokit/types": "^8.0.0",
|
||||
"@octokit/webhooks-types": "^6.8.0",
|
||||
"commander": "^9.3.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"simple-git": "^3.15.1"
|
||||
}
|
||||
}
|
12
src/bin/cli.ts
Normal file
12
src/bin/cli.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import CLIArgsParser from "@bp/service/args/cli/cli-args-parser";
|
||||
import Runner from "@bp/service/runner/runner";
|
||||
|
||||
// create CLI arguments parser
|
||||
const parser = new CLIArgsParser();
|
||||
|
||||
// create runner
|
||||
const runner = new Runner(parser);
|
||||
|
||||
runner.run();
|
12
src/bin/gha.ts
Normal file
12
src/bin/gha.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import GHAArgsParser from "@bp/service/args/gha/gha-args-parser";
|
||||
import Runner from "@bp/service/runner/runner";
|
||||
|
||||
// create CLI arguments parser
|
||||
const parser = new GHAArgsParser();
|
||||
|
||||
// create runner
|
||||
const runner = new Runner(parser);
|
||||
|
||||
runner.run();
|
10
src/service/args/args-parser.ts
Normal file
10
src/service/args/args-parser.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Args } from "@bp/service/args/args.types";
|
||||
|
||||
/**
|
||||
* Abstract arguments parser interface in charge to parse inputs and
|
||||
* produce a common Args object
|
||||
*/
|
||||
export default interface ArgsParser {
|
||||
|
||||
parse(): Args;
|
||||
}
|
11
src/service/args/args.types.ts
Normal file
11
src/service/args/args.types.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Input arguments
|
||||
*/
|
||||
export interface Args {
|
||||
dryRun: boolean, // if enabled do not push anything remotely
|
||||
auth: string, // git service auth, like github token
|
||||
targetBranch: string, // branch on the target repo where the change should be backported to
|
||||
pullRequest: string, // url of the pull request to backport
|
||||
folder?: string, // local folder where the repositories should be cloned
|
||||
author?: string, // backport pr author, default taken from pr
|
||||
}
|
34
src/service/args/cli/cli-args-parser.ts
Normal file
34
src/service/args/cli/cli-args-parser.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
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";
|
||||
|
||||
|
||||
export default class CLIArgsParser implements ArgsParser {
|
||||
|
||||
private getCommand(): Command {
|
||||
return new Command(name)
|
||||
.version(version)
|
||||
.description(description)
|
||||
.requiredOption("-tb, --target-branch <branch>", "branch where changes must be backported to.")
|
||||
.requiredOption("-pr, --pull-request <pr url>", "pull request url, e.g., https://github.com/lampajr/backporting/pull/1.")
|
||||
.option("-d, --dry-run", "if enabled the tool does not create any pull request nor push anything remotely", false)
|
||||
.option("-a, --auth <auth>", "git service authentication string, e.g., github token.", "")
|
||||
.option("-f, --folder <folder>", "local folder where the repo will be checked out, e.g., /tmp/folder.", undefined);
|
||||
}
|
||||
|
||||
parse(): Args {
|
||||
const opts = this.getCommand()
|
||||
.parse()
|
||||
.opts();
|
||||
|
||||
return {
|
||||
dryRun: opts.dryRun,
|
||||
auth: opts.auth,
|
||||
pullRequest: opts.pullRequest,
|
||||
targetBranch: opts.targetBranch,
|
||||
folder: opts.folder
|
||||
};
|
||||
}
|
||||
|
||||
}
|
17
src/service/args/gha/gha-args-parser.ts
Normal file
17
src/service/args/gha/gha-args-parser.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import ArgsParser from "@bp/service/args/args-parser";
|
||||
import { Args } from "@bp/service/args/args.types";
|
||||
import { getInput } from "@actions/core";
|
||||
|
||||
export default class GHAArgsParser implements ArgsParser {
|
||||
|
||||
parse(): Args {
|
||||
return {
|
||||
dryRun: getInput("dry-run") === "true",
|
||||
auth: getInput("auth") ? getInput("auth") : "",
|
||||
pullRequest: getInput("pull-request"),
|
||||
targetBranch: getInput("target-branch"),
|
||||
folder: getInput("folder") !== "" ? getInput("folder") : undefined
|
||||
};
|
||||
}
|
||||
|
||||
}
|
37
src/service/configs/configs-parser.ts
Normal file
37
src/service/configs/configs-parser.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Args } from "@bp/service/args/args.types";
|
||||
import { Configs } from "@bp/service/configs/configs.types";
|
||||
import LoggerService from "../logger/logger-service";
|
||||
import LoggerServiceFactory from "../logger/logger-service-factory";
|
||||
|
||||
/**
|
||||
* Abstract configuration parser class in charge to parse
|
||||
* Args and produces a common Configs object
|
||||
*/
|
||||
export default abstract class ConfigsParser {
|
||||
|
||||
private readonly logger: LoggerService;
|
||||
|
||||
constructor() {
|
||||
this.logger = LoggerServiceFactory.getLogger();
|
||||
}
|
||||
|
||||
abstract parse(args: Args): Promise<Configs>;
|
||||
|
||||
async parseAndValidate(args: Args): Promise<Configs> {
|
||||
const configs: Configs = await this.parse(args);
|
||||
|
||||
// apply validation, throw errors if something is wrong
|
||||
|
||||
// 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!");
|
||||
}
|
||||
|
||||
// if PR is closed and not merged log a warning
|
||||
if (configs.originalPullRequest.state == "closed" && !configs.originalPullRequest.merged) {
|
||||
throw new Error("Provided pull request is closed and not merged!");
|
||||
}
|
||||
|
||||
return Promise.resolve(configs);
|
||||
}
|
||||
}
|
17
src/service/configs/configs.types.ts
Normal file
17
src/service/configs/configs.types.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
|
||||
import { GitPullRequest } from "@bp/service/git/git.types";
|
||||
|
||||
/**
|
||||
* Internal configuration object
|
||||
*/
|
||||
export interface Configs {
|
||||
dryRun: boolean,
|
||||
auth: string,
|
||||
author: string, // author of the backport pr
|
||||
folder: string,
|
||||
targetBranch: string,
|
||||
originalPullRequest: GitPullRequest,
|
||||
backportPullRequest: GitPullRequest
|
||||
}
|
||||
|
61
src/service/configs/pullrequest/pr-configs-parser.ts
Normal file
61
src/service/configs/pullrequest/pr-configs-parser.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { Args } from "@bp/service/args/args.types";
|
||||
import ConfigsParser from "@bp/service/configs/configs-parser";
|
||||
import { Configs } from "@bp/service/configs/configs.types";
|
||||
import GitService from "@bp/service/git/git-service";
|
||||
import GitServiceFactory from "@bp/service/git/git-service-factory";
|
||||
import { GitPullRequest } from "@bp/service/git/git.types";
|
||||
|
||||
export default class PullRequestConfigsParser extends ConfigsParser {
|
||||
|
||||
private gitService: GitService;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.gitService = GitServiceFactory.getService();
|
||||
}
|
||||
|
||||
public async parse(args: Args): Promise<Configs> {
|
||||
const pr: GitPullRequest = await this.gitService.getPullRequestFromUrl(args.pullRequest);
|
||||
const folder: string = args.folder ?? this.getDefaultFolder();
|
||||
|
||||
return {
|
||||
dryRun: args.dryRun,
|
||||
auth: args.auth,
|
||||
author: args.author ?? pr.author,
|
||||
folder: `${folder.startsWith("/") ? "" : process.cwd() + "/"}${args.folder ?? this.getDefaultFolder()}`,
|
||||
targetBranch: args.targetBranch,
|
||||
originalPullRequest: pr,
|
||||
backportPullRequest: this.getDefaultBackportPullRequest(pr, args.targetBranch)
|
||||
};
|
||||
}
|
||||
|
||||
private getDefaultFolder() {
|
||||
return "bp";
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default backport pull request starting from the target branch and
|
||||
* the original pr to be backported
|
||||
* @param originalPullRequest original pull request
|
||||
* @param targetBranch target branch where the backport should be applied
|
||||
* @returns {GitPullRequest}
|
||||
*/
|
||||
private getDefaultBackportPullRequest(originalPullRequest: GitPullRequest, targetBranch: string): GitPullRequest {
|
||||
const reviewers = [];
|
||||
reviewers.push(originalPullRequest.author);
|
||||
if (originalPullRequest.mergedBy) {
|
||||
reviewers.push(originalPullRequest.mergedBy);
|
||||
}
|
||||
|
||||
return {
|
||||
author: originalPullRequest.author,
|
||||
title: `[${targetBranch}] ${originalPullRequest.title}`,
|
||||
body: `**Backport:** ${originalPullRequest.htmlUrl}\r\n\r\n${originalPullRequest.body}\r\n\r\nPowered by [BPer](https://github.com/lampajr/backporting).`,
|
||||
reviewers: [...new Set(reviewers)],
|
||||
targetRepo: originalPullRequest.targetRepo,
|
||||
sourceRepo: originalPullRequest.targetRepo,
|
||||
nCommits: 0, // TODO: needed?
|
||||
commits: [] // TODO needed?
|
||||
};
|
||||
}
|
||||
}
|
128
src/service/git/git-cli.ts
Normal file
128
src/service/git/git-cli.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
import LoggerService from "@bp/service/logger/logger-service";
|
||||
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
|
||||
import simpleGit, { SimpleGit } from "simple-git";
|
||||
import fs from "fs";
|
||||
|
||||
/**
|
||||
* Command line git commands executor service
|
||||
*/
|
||||
export default class GitCLIService {
|
||||
|
||||
private readonly logger: LoggerService;
|
||||
private readonly auth: string;
|
||||
private readonly author: string;
|
||||
|
||||
constructor(auth: string, author: string) {
|
||||
this.logger = LoggerServiceFactory.getLogger();
|
||||
this.auth = auth;
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a pre-configured SimpleGit instance able to execute commands from current
|
||||
* directory or the provided one
|
||||
* @param cwd [optional] current working directory
|
||||
* @returns {SimpleGit}
|
||||
*/
|
||||
private git(cwd?: string): SimpleGit {
|
||||
const gitConfig = { ...(cwd ? { baseDir: cwd } : {})};
|
||||
return simpleGit(gitConfig).addConfig("user.name", this.author).addConfig("user.email", "noreply@github.com");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the provided remote URL by adding the auth token if not empty
|
||||
* @param remoteURL remote link, e.g., https://github.com/lampajr/backporting-example.git
|
||||
*/
|
||||
private remoteWithAuth(remoteURL: string): string {
|
||||
if (this.auth && this.author) {
|
||||
return remoteURL.replace("://", `://${this.author}:${this.auth}@`);
|
||||
}
|
||||
|
||||
// return remote as it is
|
||||
return remoteURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the git version
|
||||
* @returns {Promise<string | undefined>}
|
||||
*/
|
||||
async version(cwd: string): Promise<string | undefined> {
|
||||
const rawOutput = await this.git(cwd).raw("version");
|
||||
const match = rawOutput.match(/(\d+\.\d+(\.\d+)?)/);
|
||||
return match ? match[1] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a git repository
|
||||
* @param from url or path from which the repository should be cloned from
|
||||
* @param to location at which the repository should be cloned at
|
||||
* @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}.`);
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new branch starting from the current one and checkout in it
|
||||
* @param cwd repository in which createBranch should be performed
|
||||
* @param newBranch new branch name
|
||||
*/
|
||||
async createLocalBranch(cwd: string, newBranch: string): Promise<void> {
|
||||
this.logger.info(`Creating branch ${newBranch}.`);
|
||||
await this.git(cwd).checkoutLocalBranch(newBranch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new remote to the current repository
|
||||
* @param cwd repository in which addRemote should be performed
|
||||
* @param remote remote git link
|
||||
* @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}.`);
|
||||
await this.git(cwd).addRemote(remoteName, this.remoteWithAuth(remote));
|
||||
}
|
||||
|
||||
/**
|
||||
* Git fetch from a particular branch
|
||||
* @param cwd repository in which fetch should be performed
|
||||
* @param branch fetch from the given branch
|
||||
* @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}.`);
|
||||
await this.git(cwd).fetch(remote, branch, ["--quiet"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cherry-pick a specific sha
|
||||
* @param cwd repository in which the sha should be cherry picked to
|
||||
* @param sha commit sha
|
||||
*/
|
||||
async cherryPick(cwd: string, sha: string): Promise<void> {
|
||||
this.logger.info(`Cherry picking ${sha}.`);
|
||||
await this.git(cwd).raw(["cherry-pick", "-m", "1", "--strategy=recursive", "--strategy-option=theirs", sha]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a branch to a remote
|
||||
* @param cwd repository in which the push should be performed
|
||||
* @param branch branch to be pushed
|
||||
* @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}.`);
|
||||
|
||||
const options = ["--quiet"];
|
||||
if (force) {
|
||||
options.push("--force-with-lease");
|
||||
}
|
||||
await this.git(cwd).push(remote, branch, options);
|
||||
}
|
||||
|
||||
}
|
43
src/service/git/git-service-factory.ts
Normal file
43
src/service/git/git-service-factory.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import GitService from "@bp/service/git/git-service";
|
||||
import { GitServiceType } from "@bp/service/git/git.types";
|
||||
import GitHubService from "@bp/service/git/github/github-service";
|
||||
import LoggerService from "@bp/service/logger/logger-service";
|
||||
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
|
||||
|
||||
/**
|
||||
* Singleton git service factory class
|
||||
*/
|
||||
export default class GitServiceFactory {
|
||||
|
||||
private static logger: LoggerService = LoggerServiceFactory.getLogger();
|
||||
private static instance?: GitService;
|
||||
|
||||
public static getService(): GitService {
|
||||
if (!GitServiceFactory.instance) {
|
||||
throw new Error("You must call `init` method first!");
|
||||
}
|
||||
|
||||
return GitServiceFactory.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the singleton git management service
|
||||
* @param type git management service type
|
||||
* @param auth authentication, like github token
|
||||
*/
|
||||
public static init(type: GitServiceType, auth: string): void {
|
||||
|
||||
if (GitServiceFactory.instance) {
|
||||
GitServiceFactory.logger.warn("Git service already initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
case GitServiceType.GITHUB:
|
||||
GitServiceFactory.instance = new GitHubService(auth);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid git service type received: ${type}`);
|
||||
}
|
||||
}
|
||||
}
|
34
src/service/git/git-service.ts
Normal file
34
src/service/git/git-service.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
|
||||
|
||||
/**
|
||||
* Git management service interface, which provides a common API for interacting
|
||||
* with several git management services like GitHub, Gitlab or Bitbucket.
|
||||
*/
|
||||
export default interface GitService {
|
||||
|
||||
// READ
|
||||
|
||||
/**
|
||||
* Get a pull request object from the underneath git service
|
||||
* @param owner repository's owner
|
||||
* @param repo repository's name
|
||||
* @param prNumber pull request number
|
||||
* @returns {Promise<PullRequest>}
|
||||
*/
|
||||
getPullRequest(owner: string, repo: string, prNumber: number): Promise<GitPullRequest>;
|
||||
|
||||
/**
|
||||
* Get a pull request object from the underneath git service
|
||||
* @param prUrl pull request html url
|
||||
* @returns {Promise<PullRequest>}
|
||||
*/
|
||||
getPullRequestFromUrl(prUrl: string): Promise<GitPullRequest>;
|
||||
|
||||
// WRITE
|
||||
|
||||
/**
|
||||
* Create a new pull request on the underneath git service
|
||||
* @param backport backport pull request data
|
||||
*/
|
||||
createPullRequest(backport: BackportPullRequest): Promise<void>;
|
||||
}
|
36
src/service/git/git.types.ts
Normal file
36
src/service/git/git.types.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
export interface GitPullRequest {
|
||||
number?: number,
|
||||
author: string,
|
||||
url?: string,
|
||||
htmlUrl?: string,
|
||||
state?: "open" | "closed",
|
||||
merged?: boolean,
|
||||
mergedBy?: string,
|
||||
title: string,
|
||||
body: string,
|
||||
reviewers: string[],
|
||||
targetRepo: GitRepository,
|
||||
sourceRepo: GitRepository,
|
||||
nCommits: number, // number of commits in the pr
|
||||
commits: string[] // merge commit or last one
|
||||
}
|
||||
|
||||
export interface GitRepository {
|
||||
owner: string,
|
||||
project: string,
|
||||
cloneUrl: string
|
||||
}
|
||||
|
||||
export interface BackportPullRequest {
|
||||
owner: string, // repository's owner
|
||||
repo: string, // repository's name
|
||||
head: string, // name of the source branch
|
||||
base: string, // name of the target branch
|
||||
title: string, // pr title
|
||||
body: string, // pr body
|
||||
reviewers: string[] // pr list of reviewers
|
||||
}
|
||||
|
||||
export enum GitServiceType {
|
||||
GITHUB = "github"
|
||||
}
|
33
src/service/git/github/github-mapper.ts
Normal file
33
src/service/git/github/github-mapper.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { GitPullRequest } from "@bp/service/git/git.types";
|
||||
import { PullRequest, User } from "@octokit/webhooks-types";
|
||||
|
||||
export default class GitHubMapper {
|
||||
|
||||
mapPullRequest(pr: PullRequest): GitPullRequest {
|
||||
return {
|
||||
number: pr.number,
|
||||
author: pr.user.login,
|
||||
url: pr.url,
|
||||
htmlUrl: pr.html_url,
|
||||
title: pr.title,
|
||||
body: pr.body ?? "",
|
||||
state: pr.state,
|
||||
merged: pr.merged ?? false,
|
||||
mergedBy: pr.merged_by?.login,
|
||||
reviewers: pr.requested_reviewers.filter(r => "login" in r).map((r => (r as User)?.login)),
|
||||
sourceRepo: {
|
||||
owner: pr.head.repo.full_name.split("/")[0],
|
||||
project: pr.head.repo.full_name.split("/")[1],
|
||||
cloneUrl: pr.head.repo.clone_url
|
||||
},
|
||||
targetRepo: {
|
||||
owner: pr.base.repo.full_name.split("/")[0],
|
||||
project: pr.base.repo.full_name.split("/")[1],
|
||||
cloneUrl: pr.base.repo.clone_url
|
||||
},
|
||||
nCommits: pr.commits,
|
||||
// if pr is open use latest commit sha otherwise use merge_commit_sha
|
||||
commits: pr.state === "open" ? [pr.head.sha] : [pr.merge_commit_sha as string]
|
||||
};
|
||||
}
|
||||
}
|
83
src/service/git/github/github-service.ts
Normal file
83
src/service/git/github/github-service.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import GitService from "@bp/service/git/git-service";
|
||||
import { BackportPullRequest, GitPullRequest } from "@bp/service/git/git.types";
|
||||
import GitHubMapper from "@bp/service/git/github/github-mapper";
|
||||
import OctokitFactory from "@bp/service/git/github/octokit-factory";
|
||||
import LoggerService from "@bp/service/logger/logger-service";
|
||||
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { PullRequest } from "@octokit/webhooks-types";
|
||||
|
||||
export default class GitHubService implements GitService {
|
||||
|
||||
private logger: LoggerService;
|
||||
private octokit: Octokit;
|
||||
private mapper: GitHubMapper;
|
||||
|
||||
constructor(token: string) {
|
||||
this.logger = LoggerServiceFactory.getLogger();
|
||||
this.octokit = OctokitFactory.getOctokit(token);
|
||||
this.mapper = new GitHubMapper();
|
||||
}
|
||||
|
||||
// READ
|
||||
|
||||
async getPullRequest(owner: string, repo: string, prNumber: number): Promise<GitPullRequest> {
|
||||
this.logger.info(`Getting pull request ${owner}/${repo}/${prNumber}.`);
|
||||
const { data } = await this.octokit.rest.pulls.get({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
return this.mapper.mapPullRequest(data as PullRequest);
|
||||
}
|
||||
|
||||
async getPullRequestFromUrl(prUrl: string): Promise<GitPullRequest> {
|
||||
const {owner, project} = this.getRepositoryFromPrUrl(prUrl);
|
||||
return this.getPullRequest(owner, project, parseInt(prUrl.substring(prUrl.lastIndexOf("/") + 1, prUrl.length)));
|
||||
}
|
||||
|
||||
// WRITE
|
||||
|
||||
async createPullRequest(backport: BackportPullRequest): Promise<void> {
|
||||
this.logger.info(`Creating pull request ${backport.head} -> ${backport.base}.`);
|
||||
this.logger.info(`${JSON.stringify(backport, null, 2)}`);
|
||||
|
||||
const { data } = await this.octokit.pulls.create({
|
||||
owner: backport.owner,
|
||||
repo: backport.repo,
|
||||
head: backport.head,
|
||||
base: backport.base,
|
||||
title: backport.title,
|
||||
body: backport.body
|
||||
});
|
||||
|
||||
if (backport.reviewers.length > 0) {
|
||||
try {
|
||||
await this.octokit.pulls.requestReviewers({
|
||||
owner: backport.owner,
|
||||
repo: backport.repo,
|
||||
pull_number: (data as PullRequest).number,
|
||||
reviewers: backport.reviewers
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Error requesting reviewers: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UTILS
|
||||
|
||||
/**
|
||||
* Extract repository owner and project from the pull request url
|
||||
* @param prUrl pull request url
|
||||
* @returns {{owner: string, project: string}}
|
||||
*/
|
||||
private getRepositoryFromPrUrl(prUrl: string): {owner: string, project: string} {
|
||||
const elems: string[] = prUrl.split("/");
|
||||
return {
|
||||
owner: elems[elems.length - 4],
|
||||
project: elems[elems.length - 3]
|
||||
};
|
||||
}
|
||||
}
|
24
src/service/git/github/octokit-factory.ts
Normal file
24
src/service/git/github/octokit-factory.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import LoggerService from "@bp/service/logger/logger-service";
|
||||
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
|
||||
/**
|
||||
* Singleton factory class for {Octokit} instance
|
||||
*/
|
||||
export default class OctokitFactory {
|
||||
|
||||
private static logger: LoggerService = LoggerServiceFactory.getLogger();
|
||||
private static octokit?: Octokit;
|
||||
|
||||
public static getOctokit(token: string): Octokit {
|
||||
if (!OctokitFactory.octokit) {
|
||||
OctokitFactory.logger.info("Creating octokit instance.");
|
||||
OctokitFactory.octokit = new Octokit({
|
||||
auth: token,
|
||||
userAgent: "lampajr/backporting"
|
||||
});
|
||||
}
|
||||
|
||||
return OctokitFactory.octokit;
|
||||
}
|
||||
}
|
32
src/service/logger/console-logger-service.ts
Normal file
32
src/service/logger/console-logger-service.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import Logger from "@bp/service/logger/logger";
|
||||
import LoggerService from "@bp/service/logger/logger-service";
|
||||
|
||||
export default class ConsoleLoggerService implements LoggerService {
|
||||
|
||||
private readonly logger;
|
||||
|
||||
constructor() {
|
||||
this.logger = new Logger();
|
||||
}
|
||||
|
||||
trace(message: string): void {
|
||||
this.logger.log("[TRACE]", message);
|
||||
}
|
||||
|
||||
debug(message: string): void {
|
||||
this.logger.log("[DEBUG]", message);
|
||||
}
|
||||
|
||||
info(message: string): void {
|
||||
this.logger.log("[INFO]", message);
|
||||
}
|
||||
|
||||
warn(message: string): void {
|
||||
this.logger.log("[WARN]", message);
|
||||
}
|
||||
|
||||
error(message: string): void {
|
||||
this.logger.log("[ERROR]", message);
|
||||
}
|
||||
|
||||
}
|
18
src/service/logger/logger-service-factory.ts
Normal file
18
src/service/logger/logger-service-factory.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import ConsoleLoggerService from "@bp/service/logger/console-logger-service";
|
||||
import LoggerService from "@bp/service/logger/logger-service";
|
||||
|
||||
/**
|
||||
* Singleton factory class
|
||||
*/
|
||||
export default class LoggerServiceFactory {
|
||||
|
||||
private static instance?: LoggerService;
|
||||
|
||||
public static getLogger(): LoggerService {
|
||||
if (!LoggerServiceFactory.instance) {
|
||||
LoggerServiceFactory.instance = new ConsoleLoggerService();
|
||||
}
|
||||
|
||||
return LoggerServiceFactory.instance;
|
||||
}
|
||||
}
|
15
src/service/logger/logger-service.ts
Normal file
15
src/service/logger/logger-service.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Logger service interface providing the most commong logging functionalities
|
||||
*/
|
||||
export default interface LoggerService {
|
||||
|
||||
trace(message: string): void;
|
||||
|
||||
debug(message: string): void;
|
||||
|
||||
info(message: string): void;
|
||||
|
||||
warn(message: string): void;
|
||||
|
||||
error(message: string): void;
|
||||
}
|
15
src/service/logger/logger.ts
Normal file
15
src/service/logger/logger.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
|
||||
/**
|
||||
* Common logger class based on the console.log functionality
|
||||
*/
|
||||
export default class Logger {
|
||||
|
||||
log(prefix: string, ...str: string[]) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log.apply(console, [prefix, ...str]);
|
||||
}
|
||||
|
||||
emptyLine() {
|
||||
this.log("", "");
|
||||
}
|
||||
}
|
126
src/service/runner/runner.ts
Normal file
126
src/service/runner/runner.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
import ArgsParser from "@bp/service/args/args-parser";
|
||||
import { Args } from "@bp/service/args/args.types";
|
||||
import { Configs } from "@bp/service/configs/configs.types";
|
||||
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
|
||||
import GitCLIService from "@bp/service/git/git-cli";
|
||||
import GitService from "@bp/service/git/git-service";
|
||||
import GitServiceFactory from "@bp/service/git/git-service-factory";
|
||||
import { BackportPullRequest, GitPullRequest, GitServiceType } from "@bp/service/git/git.types";
|
||||
import LoggerService from "@bp/service/logger/logger-service";
|
||||
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
|
||||
|
||||
/**
|
||||
* Main runner implementation, it implements the core logic flow
|
||||
*/
|
||||
export default class Runner {
|
||||
|
||||
private logger: LoggerService;
|
||||
private argsParser: ArgsParser;
|
||||
|
||||
constructor(parser: ArgsParser) {
|
||||
this.logger = LoggerServiceFactory.getLogger();
|
||||
this.argsParser = parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer the remote GIT service to interact with based on the provided
|
||||
* pull request URL
|
||||
* @param prUrl provided pull request URL
|
||||
* @returns {GitServiceType}
|
||||
*/
|
||||
private inferRemoteGitService(prUrl: string): GitServiceType {
|
||||
const stdPrUrl = prUrl.toLowerCase().trim();
|
||||
|
||||
if (stdPrUrl.includes(GitServiceType.GITHUB.toString())) {
|
||||
return GitServiceType.GITHUB;
|
||||
}
|
||||
|
||||
throw new Error(`Remote GIT service not recognixed from PR url: ${prUrl}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point invoked by the command line or gha
|
||||
*/
|
||||
async run(): Promise<void> {
|
||||
this.logger.info("Starting process.");
|
||||
|
||||
try {
|
||||
await this.execute();
|
||||
|
||||
this.logger.info("Process succeeded!");
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`${error}`);
|
||||
|
||||
this.logger.info("Process failed!");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core logic
|
||||
*/
|
||||
async execute(): Promise<void>{
|
||||
|
||||
// 1. parse args
|
||||
const args: Args = this.argsParser.parse();
|
||||
|
||||
if (args.dryRun) {
|
||||
this.logger.warn("Dry run enabled!");
|
||||
}
|
||||
|
||||
// 2. init git service
|
||||
GitServiceFactory.init(this.inferRemoteGitService(args.pullRequest), args.auth);
|
||||
const gitApi: GitService = GitServiceFactory.getService();
|
||||
|
||||
// 3. parse configs
|
||||
const configs: Configs = await new PullRequestConfigsParser().parseAndValidate(args);
|
||||
const originalPR: GitPullRequest = configs.originalPullRequest;
|
||||
const backportPR: GitPullRequest = configs.backportPullRequest;
|
||||
|
||||
// start local git operations
|
||||
const git: GitCLIService = new GitCLIService(configs.auth, configs.author);
|
||||
|
||||
// 4. clone the repository
|
||||
await git.clone(configs.originalPullRequest.targetRepo.cloneUrl, configs.folder, configs.targetBranch);
|
||||
|
||||
// 5. create new branch from target one and checkout
|
||||
const backportBranch = `bp-${configs.targetBranch}-${originalPR.commits.join("-")}`;
|
||||
await git.createLocalBranch(configs.folder, backportBranch);
|
||||
|
||||
// 6. fetch pull request remote if source owner != target owner or pull request still open
|
||||
if (configs.originalPullRequest.sourceRepo.owner !== configs.originalPullRequest.targetRepo.owner ||
|
||||
configs.originalPullRequest.state === "open") {
|
||||
await git.fetch(configs.folder, `pull/${configs.originalPullRequest.number}/head:pr/${configs.originalPullRequest.number}`);
|
||||
}
|
||||
|
||||
// 7. apply all changes to the new branch
|
||||
for (const sha of originalPR.commits) {
|
||||
await git.cherryPick(configs.folder, sha);
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
if (!configs.dryRun) {
|
||||
// 8. push the new branch to origin
|
||||
await git.push(configs.folder, backportBranch);
|
||||
|
||||
// 9. create pull request new branch -> target branch (using octokit)
|
||||
await gitApi.createPullRequest(backport);
|
||||
|
||||
} else {
|
||||
this.logger.warn("Pull request creation and remote push skipped!");
|
||||
this.logger.info(`${JSON.stringify(backport, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
90
test/service/args/cli/cli-args-parser.test.ts
Normal file
90
test/service/args/cli/cli-args-parser.test.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { Args } from "@bp/service/args/args.types";
|
||||
import CLIArgsParser from "@bp/service/args/cli/cli-args-parser";
|
||||
import { addProcessArgs, resetProcessArgs } from "../../../support/utils";
|
||||
|
||||
describe("cli args parser", () => {
|
||||
let parser: CLIArgsParser;
|
||||
|
||||
beforeEach(() => {
|
||||
// create a fresh new instance every time
|
||||
parser = new CLIArgsParser();
|
||||
|
||||
// reset process.env variables
|
||||
resetProcessArgs();
|
||||
});
|
||||
|
||||
test("valid execution [default, short]", () => {
|
||||
addProcessArgs([
|
||||
"-tb",
|
||||
"target",
|
||||
"-pr",
|
||||
"https://localhost/whatever/pulls/1"
|
||||
]);
|
||||
|
||||
const args: Args = parser.parse();
|
||||
expect(args.dryRun).toEqual(false);
|
||||
expect(args.auth).toEqual("");
|
||||
expect(args.author).toEqual(undefined);
|
||||
expect(args.folder).toEqual(undefined);
|
||||
expect(args.targetBranch).toEqual("target");
|
||||
expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1");
|
||||
});
|
||||
|
||||
test("valid execution [default, long]", () => {
|
||||
addProcessArgs([
|
||||
"--target-branch",
|
||||
"target",
|
||||
"--pull-request",
|
||||
"https://localhost/whatever/pulls/1"
|
||||
]);
|
||||
|
||||
const args: Args = parser.parse();
|
||||
expect(args.dryRun).toEqual(false);
|
||||
expect(args.auth).toEqual("");
|
||||
expect(args.author).toEqual(undefined);
|
||||
expect(args.folder).toEqual(undefined);
|
||||
expect(args.targetBranch).toEqual("target");
|
||||
expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1");
|
||||
});
|
||||
|
||||
test("valid execution [override, short]", () => {
|
||||
addProcessArgs([
|
||||
"-d",
|
||||
"-a",
|
||||
"bearer-token",
|
||||
"-tb",
|
||||
"target",
|
||||
"-pr",
|
||||
"https://localhost/whatever/pulls/1"
|
||||
]);
|
||||
|
||||
const args: Args = parser.parse();
|
||||
expect(args.dryRun).toEqual(true);
|
||||
expect(args.auth).toEqual("bearer-token");
|
||||
expect(args.author).toEqual(undefined);
|
||||
expect(args.folder).toEqual(undefined);
|
||||
expect(args.targetBranch).toEqual("target");
|
||||
expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1");
|
||||
});
|
||||
|
||||
test("valid execution [override, long]", () => {
|
||||
addProcessArgs([
|
||||
"--dry-run",
|
||||
"--auth",
|
||||
"bearer-token",
|
||||
"--target-branch",
|
||||
"target",
|
||||
"--pull-request",
|
||||
"https://localhost/whatever/pulls/1"
|
||||
]);
|
||||
|
||||
const args: Args = parser.parse();
|
||||
expect(args.dryRun).toEqual(true);
|
||||
expect(args.auth).toEqual("bearer-token");
|
||||
expect(args.author).toEqual(undefined);
|
||||
expect(args.folder).toEqual(undefined);
|
||||
expect(args.targetBranch).toEqual("target");
|
||||
expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1");
|
||||
});
|
||||
|
||||
});
|
50
test/service/args/gha/gha-args-parser.test.ts
Normal file
50
test/service/args/gha/gha-args-parser.test.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { Args } from "@bp/service/args/args.types";
|
||||
import GHAArgsParser from "@bp/service/args/gha/gha-args-parser";
|
||||
import { spyGetInput } from "../../../support/utils";
|
||||
|
||||
describe("gha args parser", () => {
|
||||
let parser: GHAArgsParser;
|
||||
|
||||
beforeEach(() => {
|
||||
// create a fresh new instance every time
|
||||
parser = new GHAArgsParser();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
||||
test("valid execution [default]", () => {
|
||||
spyGetInput({
|
||||
"target-branch": "target",
|
||||
"pull-request": "https://localhost/whatever/pulls/1"
|
||||
});
|
||||
|
||||
const args: Args = parser.parse();
|
||||
expect(args.dryRun).toEqual(false);
|
||||
expect(args.auth).toEqual("");
|
||||
expect(args.author).toEqual(undefined);
|
||||
expect(args.folder).toEqual(undefined);
|
||||
expect(args.targetBranch).toEqual("target");
|
||||
expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1");
|
||||
});
|
||||
|
||||
test("valid execution [override]", () => {
|
||||
spyGetInput({
|
||||
"dry-run": "true",
|
||||
"auth": "bearer-token",
|
||||
"target-branch": "target",
|
||||
"pull-request": "https://localhost/whatever/pulls/1"
|
||||
});
|
||||
|
||||
const args: Args = parser.parse();
|
||||
expect(args.dryRun).toEqual(true);
|
||||
expect(args.auth).toEqual("bearer-token");
|
||||
expect(args.author).toEqual(undefined);
|
||||
expect(args.folder).toEqual(undefined);
|
||||
expect(args.targetBranch).toEqual("target");
|
||||
expect(args.pullRequest).toEqual("https://localhost/whatever/pulls/1");
|
||||
});
|
||||
|
||||
});
|
177
test/service/configs/pullrequest/pr-configs-parser.test.ts
Normal file
177
test/service/configs/pullrequest/pr-configs-parser.test.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
import { Args } from "@bp/service/args/args.types";
|
||||
import { Configs } from "@bp/service/configs/configs.types";
|
||||
import PullRequestConfigsParser from "@bp/service/configs/pullrequest/pr-configs-parser";
|
||||
import GitServiceFactory from "@bp/service/git/git-service-factory";
|
||||
import { GitServiceType } from "@bp/service/git/git.types";
|
||||
import { setupMoctokit } from "../../../support/moctokit/moctokit-support";
|
||||
import { mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, repo, targetOwner } from "../../../support/moctokit/moctokit-data";
|
||||
|
||||
describe("pull request config parser", () => {
|
||||
|
||||
const mergedPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${mergedPullRequestFixture.number}`;
|
||||
const openPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${openPullRequestFixture.number}`;
|
||||
const notMergedPRUrl = `https://github.com/${targetOwner}/${repo}/pull/${notMergedPullRequestFixture.number}`;
|
||||
|
||||
let parser: PullRequestConfigsParser;
|
||||
|
||||
beforeAll(() => {
|
||||
GitServiceFactory.init(GitServiceType.GITHUB, "whatever");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setupMoctokit();
|
||||
|
||||
parser = new PullRequestConfigsParser();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("parse configs from pull request", async () => {
|
||||
const args: Args = {
|
||||
dryRun: false,
|
||||
auth: "",
|
||||
pullRequest: mergedPRUrl,
|
||||
targetBranch: "prod"
|
||||
};
|
||||
|
||||
const configs: Configs = await parser.parseAndValidate(args);
|
||||
|
||||
expect(configs.dryRun).toEqual(false);
|
||||
expect(configs.author).toEqual("gh-user");
|
||||
expect(configs.auth).toEqual("");
|
||||
expect(configs.targetBranch).toEqual("prod");
|
||||
expect(configs.folder).toEqual(process.cwd() + "/bp");
|
||||
expect(configs.originalPullRequest).toEqual({
|
||||
number: 2368,
|
||||
author: "gh-user",
|
||||
url: "https://api.github.com/repos/owner/reponame/pulls/2368",
|
||||
htmlUrl: "https://github.com/owner/reponame/pull/2368",
|
||||
state: "closed",
|
||||
merged: true,
|
||||
mergedBy: "that-s-a-user",
|
||||
title: "PR Title",
|
||||
body: "Please review and merge",
|
||||
reviewers: ["requested-gh-user", "gh-user"],
|
||||
targetRepo: {
|
||||
owner: "owner",
|
||||
project: "reponame",
|
||||
cloneUrl: "https://github.com/owner/reponame.git"
|
||||
},
|
||||
sourceRepo: {
|
||||
owner: "fork",
|
||||
project: "reponame",
|
||||
cloneUrl: "https://github.com/fork/reponame.git"
|
||||
},
|
||||
nCommits: 2,
|
||||
commits: ["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"]
|
||||
});
|
||||
expect(configs.backportPullRequest).toEqual({
|
||||
author: "gh-user",
|
||||
url: undefined,
|
||||
htmlUrl: undefined,
|
||||
title: "[prod] PR Title",
|
||||
body: "**Backport:** https://github.com/owner/reponame/pull/2368\r\n\r\nPlease review and merge\r\n\r\nPowered by [BPer](https://github.com/lampajr/backporting).",
|
||||
reviewers: ["gh-user", "that-s-a-user"],
|
||||
targetRepo: {
|
||||
owner: "owner",
|
||||
project: "reponame",
|
||||
cloneUrl: "https://github.com/owner/reponame.git"
|
||||
},
|
||||
sourceRepo: {
|
||||
owner: "owner",
|
||||
project: "reponame",
|
||||
cloneUrl: "https://github.com/owner/reponame.git"
|
||||
},
|
||||
nCommits: 0,
|
||||
commits: []
|
||||
});
|
||||
});
|
||||
|
||||
test("override folder", async () => {
|
||||
const args: Args = {
|
||||
dryRun: true,
|
||||
auth: "whatever",
|
||||
pullRequest: mergedPRUrl,
|
||||
targetBranch: "prod",
|
||||
folder: "/tmp/test"
|
||||
};
|
||||
|
||||
const configs: Configs = await parser.parseAndValidate(args);
|
||||
|
||||
expect(configs.dryRun).toEqual(true);
|
||||
expect(configs.auth).toEqual("whatever");
|
||||
expect(configs.targetBranch).toEqual("prod");
|
||||
expect(configs.folder).toEqual("/tmp/test");
|
||||
});
|
||||
|
||||
test("override author", async () => {
|
||||
const args: Args = {
|
||||
dryRun: true,
|
||||
auth: "whatever",
|
||||
pullRequest: mergedPRUrl,
|
||||
targetBranch: "prod",
|
||||
author: "another-user"
|
||||
};
|
||||
|
||||
const configs: Configs = await parser.parseAndValidate(args);
|
||||
|
||||
expect(configs.dryRun).toEqual(true);
|
||||
expect(configs.auth).toEqual("whatever");
|
||||
expect(configs.targetBranch).toEqual("prod");
|
||||
expect(configs.author).toEqual("another-user");
|
||||
});
|
||||
|
||||
test("still open pull request", async () => {
|
||||
const args: Args = {
|
||||
dryRun: true,
|
||||
auth: "whatever",
|
||||
pullRequest: openPRUrl,
|
||||
targetBranch: "prod"
|
||||
};
|
||||
|
||||
const configs: Configs = await parser.parseAndValidate(args);
|
||||
|
||||
expect(configs.dryRun).toEqual(true);
|
||||
expect(configs.auth).toEqual("whatever");
|
||||
expect(configs.targetBranch).toEqual("prod");
|
||||
expect(configs.author).toEqual("gh-user");
|
||||
expect(configs.originalPullRequest).toEqual({
|
||||
number: 4444,
|
||||
author: "gh-user",
|
||||
url: "https://api.github.com/repos/owner/reponame/pulls/4444",
|
||||
htmlUrl: "https://github.com/owner/reponame/pull/4444",
|
||||
state: "open",
|
||||
merged: false,
|
||||
mergedBy: "that-s-a-user",
|
||||
title: "PR Title",
|
||||
body: "Please review and merge",
|
||||
reviewers: ["gh-user"],
|
||||
targetRepo: {
|
||||
owner: "owner",
|
||||
project: "reponame",
|
||||
cloneUrl: "https://github.com/owner/reponame.git"
|
||||
},
|
||||
sourceRepo: {
|
||||
owner: "fork",
|
||||
project: "reponame",
|
||||
cloneUrl: "https://github.com/fork/reponame.git"
|
||||
},
|
||||
nCommits: 2,
|
||||
// taken from head.sha
|
||||
commits: ["91748965051fae1330ad58d15cf694e103267c87"]
|
||||
});
|
||||
});
|
||||
|
||||
test("closed pull request", async () => {
|
||||
const args: Args = {
|
||||
dryRun: true,
|
||||
auth: "whatever",
|
||||
pullRequest: notMergedPRUrl,
|
||||
targetBranch: "prod"
|
||||
};
|
||||
|
||||
expect(async () => await parser.parseAndValidate(args)).rejects.toThrow("Provided pull request is closed and not merged!");
|
||||
});
|
||||
});
|
114
test/service/git/git-cli.test.ts
Normal file
114
test/service/git/git-cli.test.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
import GitCLIService from "@bp/service/git/git-cli";
|
||||
import { FileState, GitActionTypes, MockGithub } from "@kie/mock-github";
|
||||
import { spawnSync } from "child_process";
|
||||
import { assert } from "console";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
let git: GitCLIService;
|
||||
let cwd: string;
|
||||
let currentBranch: string;
|
||||
let pushedBranches: string[];
|
||||
let localBranches: string[];
|
||||
let files: FileState[];
|
||||
|
||||
const mockGithub = new MockGithub(
|
||||
{
|
||||
repo: {
|
||||
repoA: {
|
||||
pushedBranches: ["sbranch", "tbranch"],
|
||||
localBranches: ["lbranch"],
|
||||
currentBranch: "main",
|
||||
history: [
|
||||
{
|
||||
action: GitActionTypes.PUSH,
|
||||
branch: "main",
|
||||
},
|
||||
{
|
||||
action: GitActionTypes.PUSH,
|
||||
branch: "sbranch",
|
||||
},
|
||||
{
|
||||
action: GitActionTypes.PUSH,
|
||||
branch: "tbranch",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
path.join(__dirname, "setup-cli")
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
//setup
|
||||
await mockGithub.setup();
|
||||
cwd = mockGithub.repo.getPath("repoA")!;
|
||||
currentBranch = mockGithub.repo.getBranchState("repoA")!.currentBranch;
|
||||
pushedBranches = mockGithub.repo.getBranchState("repoA")!.pushedBranches;
|
||||
localBranches = mockGithub.repo.getBranchState("repoA")!.localBranches;
|
||||
files = (await mockGithub.repo.getFileSystemState("repoA"))!;
|
||||
|
||||
//make sure the setup is correct to run this test suite
|
||||
assert(
|
||||
pushedBranches.length > 1,
|
||||
"your configuration must have a repository with pushed branches other than main"
|
||||
);
|
||||
assert(
|
||||
localBranches.length > 0,
|
||||
"your configuration must have a repository with local branches i.e. not pushed branches"
|
||||
);
|
||||
assert(
|
||||
files.length > 0,
|
||||
"your configuration needs at least 1 file committed to some branch which is not the current branch"
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mockGithub.teardown();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// create a fresh instance of git before each test
|
||||
git = new GitCLIService("", "author");
|
||||
});
|
||||
|
||||
describe("git cli service", () => {
|
||||
test("version", async () => {
|
||||
const result = await git.version(cwd);
|
||||
const actualVersion = spawnSync("git", ["version"]).stdout.toString();
|
||||
const match = actualVersion.match(/(\d+\.\d+(\.\d+)?)/);
|
||||
if (match) {
|
||||
expect(result).toEqual(match[1]);
|
||||
} else {
|
||||
expect(result).toBe(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test("fetch", async () => {
|
||||
await expect(git.fetch(cwd, currentBranch)).resolves.not.toThrowError();
|
||||
});
|
||||
|
||||
test("local branch", async () => {
|
||||
await expect(git.createLocalBranch(cwd, "new-local-branch")).resolves.not.toThrowError();
|
||||
|
||||
// use rev-parse to double check the current branch is the new one
|
||||
const output = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd }).stdout.toString().trim();
|
||||
expect(output).toEqual("new-local-branch");
|
||||
});
|
||||
|
||||
test("push local branch", async () => {
|
||||
const expressionToTest = "GIT_CHERRY_SHOULD_NOT_INCLUDE_THIS_MSG";
|
||||
// create file to push
|
||||
fs.writeFileSync(path.join(cwd, "test-push"), "testing git push");
|
||||
|
||||
// add and commit the file
|
||||
spawnSync("git", ["add", "."], { cwd });
|
||||
spawnSync("git", ["commit", "-m", expressionToTest], { cwd });
|
||||
|
||||
await git.push(cwd, currentBranch, "origin", false);
|
||||
|
||||
// use git cherry to verify this commit was pushed
|
||||
const output = spawnSync("git", ["cherry", "-v"], { cwd }).stdout.toString();
|
||||
expect(output.includes(expressionToTest)).toBe(false);
|
||||
});
|
||||
});
|
40
test/service/git/github/github-service.test.ts
Normal file
40
test/service/git/github/github-service.test.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import GitServiceFactory from "@bp/service/git/git-service-factory";
|
||||
import { GitPullRequest, GitServiceType } from "@bp/service/git/git.types";
|
||||
import GitHubService from "@bp/service/git/github/github-service";
|
||||
import { mergedPullRequestFixture, repo, targetOwner } from "../../../support/moctokit/moctokit-data";
|
||||
import { setupMoctokit } from "../../../support/moctokit/moctokit-support";
|
||||
|
||||
describe("github service", () => {
|
||||
|
||||
let gitService: GitHubService;
|
||||
|
||||
beforeAll(() => {
|
||||
// init git service
|
||||
GitServiceFactory.init(GitServiceType.GITHUB, "whatever");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// mock github api calls
|
||||
setupMoctokit();
|
||||
|
||||
gitService = GitServiceFactory.getService() as GitHubService;
|
||||
});
|
||||
|
||||
test("get pull request: success", async () => {
|
||||
const res: GitPullRequest = await gitService.getPullRequest(targetOwner, repo, mergedPullRequestFixture.number);
|
||||
expect(res.sourceRepo).toEqual({
|
||||
owner: "fork",
|
||||
project: "reponame",
|
||||
cloneUrl: "https://github.com/fork/reponame.git"
|
||||
});
|
||||
expect(res.targetRepo).toEqual({
|
||||
owner: "owner",
|
||||
project: "reponame",
|
||||
cloneUrl: "https://github.com/owner/reponame.git"
|
||||
});
|
||||
expect(res.title).toBe("PR Title");
|
||||
expect(res.commits.length).toBe(1);
|
||||
expect(res.commits).toEqual(["28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc"]);
|
||||
});
|
||||
|
||||
});
|
285
test/service/runner/cli-runner.test.ts
Normal file
285
test/service/runner/cli-runner.test.ts
Normal file
|
@ -0,0 +1,285 @@
|
|||
import ArgsParser from "@bp/service/args/args-parser";
|
||||
import Runner from "@bp/service/runner/runner";
|
||||
import GitCLIService from "@bp/service/git/git-cli";
|
||||
import GitHubService from "@bp/service/git/github/github-service";
|
||||
import CLIArgsParser from "@bp/service/args/cli/cli-args-parser";
|
||||
import { addProcessArgs, resetProcessArgs } from "../../support/utils";
|
||||
import { setupMoctokit } from "../../support/moctokit/moctokit-support";
|
||||
|
||||
jest.mock("@bp/service/git/git-cli");
|
||||
jest.spyOn(GitHubService.prototype, "createPullRequest");
|
||||
|
||||
let parser: ArgsParser;
|
||||
let runner: Runner;
|
||||
|
||||
beforeEach(() => {
|
||||
setupMoctokit();
|
||||
|
||||
// create CLI arguments parser
|
||||
parser = new CLIArgsParser();
|
||||
|
||||
// create runner
|
||||
runner = new Runner(parser);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// reset process.env variables
|
||||
resetProcessArgs();
|
||||
});
|
||||
|
||||
describe("cli runner", () => {
|
||||
test("with dry run", async () => {
|
||||
addProcessArgs([
|
||||
"-d",
|
||||
"-tb",
|
||||
"target",
|
||||
"-pr",
|
||||
"https://github.com/owner/reponame/pull/2368"
|
||||
]);
|
||||
|
||||
await runner.execute();
|
||||
|
||||
const cwd = process.cwd() + "/bp";
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("overriding author", async () => {
|
||||
addProcessArgs([
|
||||
"-d",
|
||||
"-tb",
|
||||
"target",
|
||||
"-pr",
|
||||
"https://github.com/owner/reponame/pull/2368"
|
||||
]);
|
||||
|
||||
await runner.execute();
|
||||
|
||||
const cwd = process.cwd() + "/bp";
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("with relative folder", async () => {
|
||||
addProcessArgs([
|
||||
"-d",
|
||||
"-tb",
|
||||
"target",
|
||||
"-pr",
|
||||
"https://github.com/owner/reponame/pull/2368",
|
||||
"-f",
|
||||
"folder"
|
||||
]);
|
||||
|
||||
await runner.execute();
|
||||
|
||||
const cwd = process.cwd() + "/folder";
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.addRemote).toBeCalledTimes(0);
|
||||
expect(GitCLIService.prototype.addRemote).toBeCalledTimes(0);
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("with absolute folder", async () => {
|
||||
addProcessArgs([
|
||||
"-d",
|
||||
"-tb",
|
||||
"target",
|
||||
"-pr",
|
||||
"https://github.com/owner/reponame/pull/2368",
|
||||
"-f",
|
||||
"/tmp/folder"
|
||||
]);
|
||||
|
||||
await runner.execute();
|
||||
|
||||
const cwd = "/tmp/folder";
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("without dry run", async () => {
|
||||
addProcessArgs([
|
||||
"-tb",
|
||||
"target",
|
||||
"-pr",
|
||||
"https://github.com/owner/reponame/pull/2368"
|
||||
]);
|
||||
|
||||
await runner.execute();
|
||||
|
||||
const cwd = process.cwd() + "/bp";
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
|
||||
owner: "owner",
|
||||
repo: "reponame",
|
||||
head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc",
|
||||
base: "target",
|
||||
title: "[target] PR Title",
|
||||
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
|
||||
reviewers: ["gh-user", "that-s-a-user"]
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("same owner", async () => {
|
||||
addProcessArgs([
|
||||
"-tb",
|
||||
"target",
|
||||
"-pr",
|
||||
"https://github.com/owner/reponame/pull/8632"
|
||||
]);
|
||||
|
||||
await runner.execute();
|
||||
|
||||
const cwd = process.cwd() + "/bp";
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(0);
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
|
||||
owner: "owner",
|
||||
repo: "reponame",
|
||||
head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc",
|
||||
base: "target",
|
||||
title: "[target] PR Title",
|
||||
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/8632"),
|
||||
reviewers: ["gh-user", "that-s-a-user"]
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("closed and not merged pull request", async () => {
|
||||
addProcessArgs([
|
||||
"-tb",
|
||||
"target",
|
||||
"-pr",
|
||||
"https://github.com/owner/reponame/pull/6666"
|
||||
]);
|
||||
|
||||
expect(async () => await runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!");
|
||||
});
|
||||
|
||||
test("open pull request", async () => {
|
||||
addProcessArgs([
|
||||
"-tb",
|
||||
"target",
|
||||
"-pr",
|
||||
"https://github.com/owner/reponame/pull/4444"
|
||||
]);
|
||||
|
||||
await runner.execute();
|
||||
|
||||
const cwd = process.cwd() + "/bp";
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-91748965051fae1330ad58d15cf694e103267c87");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/4444/head:pr/4444");
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "91748965051fae1330ad58d15cf694e103267c87");
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-91748965051fae1330ad58d15cf694e103267c87");
|
||||
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
|
||||
owner: "owner",
|
||||
repo: "reponame",
|
||||
head: "bp-target-91748965051fae1330ad58d15cf694e103267c87",
|
||||
base: "target",
|
||||
title: "[target] PR Title",
|
||||
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/4444"),
|
||||
reviewers: ["gh-user", "that-s-a-user"]
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
141
test/service/runner/gha-runner.test.ts
Normal file
141
test/service/runner/gha-runner.test.ts
Normal file
|
@ -0,0 +1,141 @@
|
|||
import ArgsParser from "@bp/service/args/args-parser";
|
||||
import Runner from "@bp/service/runner/runner";
|
||||
import GitCLIService from "@bp/service/git/git-cli";
|
||||
import GitHubService from "@bp/service/git/github/github-service";
|
||||
import GHAArgsParser from "@bp/service/args/gha/gha-args-parser";
|
||||
import { spyGetInput } from "../../support/utils";
|
||||
import { setupMoctokit } from "../../support/moctokit/moctokit-support";
|
||||
|
||||
jest.mock("@bp/service/git/git-cli");
|
||||
jest.spyOn(GitHubService.prototype, "createPullRequest");
|
||||
|
||||
let parser: ArgsParser;
|
||||
let runner: Runner;
|
||||
|
||||
beforeEach(() => {
|
||||
setupMoctokit();
|
||||
|
||||
// create GHA arguments parser
|
||||
parser = new GHAArgsParser();
|
||||
|
||||
// create runner
|
||||
runner = new Runner(parser);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("gha runner", () => {
|
||||
test("with dry run", async () => {
|
||||
spyGetInput({
|
||||
"dry-run": "true",
|
||||
"target-branch": "target",
|
||||
"pull-request": "https://github.com/owner/reponame/pull/2368"
|
||||
});
|
||||
|
||||
await runner.execute();
|
||||
|
||||
const cwd = process.cwd() + "/bp";
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(0);
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("without dry run", async () => {
|
||||
spyGetInput({
|
||||
"target-branch": "target",
|
||||
"pull-request": "https://github.com/owner/reponame/pull/2368"
|
||||
});
|
||||
|
||||
await runner.execute();
|
||||
|
||||
const cwd = process.cwd() + "/bp";
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/2368/head:pr/2368");
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc");
|
||||
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
|
||||
owner: "owner",
|
||||
repo: "reponame",
|
||||
head: "bp-target-28f63db774185f4ec4b57cd9aaeb12dbfb4c9ecc",
|
||||
base: "target",
|
||||
title: "[target] PR Title",
|
||||
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/2368"),
|
||||
reviewers: ["gh-user", "that-s-a-user"]
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("closed and not merged pull request", async () => {
|
||||
spyGetInput({
|
||||
"target-branch": "target",
|
||||
"pull-request": "https://github.com/owner/reponame/pull/6666"
|
||||
});
|
||||
|
||||
expect(async () => await runner.execute()).rejects.toThrow("Provided pull request is closed and not merged!");
|
||||
});
|
||||
|
||||
test("open pull request", async () => {
|
||||
spyGetInput({
|
||||
"target-branch": "target",
|
||||
"pull-request": "https://github.com/owner/reponame/pull/4444"
|
||||
});
|
||||
|
||||
await runner.execute();
|
||||
|
||||
const cwd = process.cwd() + "/bp";
|
||||
|
||||
expect(GitCLIService.prototype.clone).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.clone).toBeCalledWith("https://github.com/owner/reponame.git", cwd, "target");
|
||||
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.createLocalBranch).toBeCalledWith(cwd, "bp-target-91748965051fae1330ad58d15cf694e103267c87");
|
||||
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.fetch).toBeCalledWith(cwd, "pull/4444/head:pr/4444");
|
||||
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.cherryPick).toBeCalledWith(cwd, "91748965051fae1330ad58d15cf694e103267c87");
|
||||
|
||||
expect(GitCLIService.prototype.push).toBeCalledTimes(1);
|
||||
expect(GitCLIService.prototype.push).toBeCalledWith(cwd, "bp-target-91748965051fae1330ad58d15cf694e103267c87");
|
||||
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledTimes(1);
|
||||
expect(GitHubService.prototype.createPullRequest).toBeCalledWith({
|
||||
owner: "owner",
|
||||
repo: "reponame",
|
||||
head: "bp-target-91748965051fae1330ad58d15cf694e103267c87",
|
||||
base: "target",
|
||||
title: "[target] PR Title",
|
||||
body: expect.stringContaining("**Backport:** https://github.com/owner/reponame/pull/4444"),
|
||||
reviewers: ["gh-user", "that-s-a-user"]
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
1816
test/support/moctokit/moctokit-data.ts
Normal file
1816
test/support/moctokit/moctokit-data.ts
Normal file
File diff suppressed because it is too large
Load diff
89
test/support/moctokit/moctokit-support.ts
Normal file
89
test/support/moctokit/moctokit-support.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import LoggerServiceFactory from "@bp/service/logger/logger-service-factory";
|
||||
import { Moctokit } from "@kie/mock-github";
|
||||
import { targetOwner, repo, mergedPullRequestFixture, openPullRequestFixture, notMergedPullRequestFixture, notFoundPullRequestNumber, sameOwnerPullRequestFixture } from "./moctokit-data";
|
||||
|
||||
const logger = LoggerServiceFactory.getLogger();
|
||||
|
||||
export const setupMoctokit = (): Moctokit => {
|
||||
logger.debug("Setting up moctokit.");
|
||||
|
||||
const mock = new Moctokit();
|
||||
|
||||
// setup the mock requests here
|
||||
|
||||
// valid requests
|
||||
mock.rest.pulls
|
||||
.get({
|
||||
owner: targetOwner,
|
||||
repo: repo,
|
||||
pull_number: mergedPullRequestFixture.number
|
||||
})
|
||||
.reply({
|
||||
status: 200,
|
||||
data: mergedPullRequestFixture
|
||||
});
|
||||
|
||||
mock.rest.pulls
|
||||
.get({
|
||||
owner: targetOwner,
|
||||
repo: repo,
|
||||
pull_number: sameOwnerPullRequestFixture.number
|
||||
})
|
||||
.reply({
|
||||
status: 200,
|
||||
data: sameOwnerPullRequestFixture
|
||||
});
|
||||
|
||||
mock.rest.pulls
|
||||
.get({
|
||||
owner: targetOwner,
|
||||
repo: repo,
|
||||
pull_number: openPullRequestFixture.number
|
||||
})
|
||||
.reply({
|
||||
status: 200,
|
||||
data: openPullRequestFixture
|
||||
});
|
||||
|
||||
mock.rest.pulls
|
||||
.get({
|
||||
owner: targetOwner,
|
||||
repo: repo,
|
||||
pull_number: notMergedPullRequestFixture.number
|
||||
})
|
||||
.reply({
|
||||
status: 200,
|
||||
data: notMergedPullRequestFixture
|
||||
});
|
||||
|
||||
mock.rest.pulls
|
||||
.create()
|
||||
.reply({
|
||||
status: 201,
|
||||
data: mergedPullRequestFixture
|
||||
});
|
||||
|
||||
mock.rest.pulls
|
||||
.requestReviewers()
|
||||
.reply({
|
||||
status: 201,
|
||||
data: mergedPullRequestFixture
|
||||
});
|
||||
|
||||
|
||||
// invalid requests
|
||||
mock.rest.pulls
|
||||
.get({
|
||||
owner: targetOwner,
|
||||
repo: repo,
|
||||
pull_number: notFoundPullRequestNumber
|
||||
})
|
||||
.reply({
|
||||
status: 404,
|
||||
data: {
|
||||
message: "Not found"
|
||||
}
|
||||
});
|
||||
|
||||
return mock;
|
||||
};
|
17
test/support/utils.ts
Normal file
17
test/support/utils.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import * as core from "@actions/core";
|
||||
|
||||
export const addProcessArgs = (args: string[]) => {
|
||||
process.argv = [...process.argv, ...args];
|
||||
};
|
||||
|
||||
export const resetProcessArgs = () => {
|
||||
process.argv = ["node", "backporting"];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const spyGetInput = (obj: any) => {
|
||||
const mock = jest.spyOn(core, "getInput");
|
||||
mock.mockImplementation((name: string) : string => {
|
||||
return obj[name];
|
||||
});
|
||||
};
|
130
tsconfig.json
Normal file
130
tsconfig.json
Normal file
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES2020",
|
||||
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs",
|
||||
/* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"baseUrl": "./",
|
||||
/* Specify the base directory to resolve non-relative module names. */
|
||||
"paths": {
|
||||
"@bp/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
/* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
"resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./build",
|
||||
/* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
"importHelpers": false,
|
||||
/* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true,
|
||||
/* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
/* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true,
|
||||
/* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true
|
||||
/* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"ts-node": {
|
||||
// Do not forget to `npm install --save-dev tsconfig-paths`
|
||||
"require": ["tsconfig-paths/register"]
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"test"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build",
|
||||
"dist"
|
||||
]
|
||||
}
|
Loading…
Add table
Reference in a new issue