├── .githooks └── pre-commit ├── .github └── workflows │ ├── PR-CI.yml │ └── master-CI.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ └── getInputs.test.ts ├── action.yml ├── dist └── index.js ├── dprint.json ├── jest.config.js ├── package.json ├── src ├── getInputs.ts └── index.ts ├── tsconfig.json ├── types └── mock-env │ └── index.d.ts ├── webpack.config.js └── yarn.lock /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This commit hook checks whether we ran `yarn build` when committed TypeScript files. 4 | # For GitHub actions to work, we need to check the compiled JavaScript into VCS. 5 | # 6 | # This script can yield false positives in cases where you only make stylistic changes to the TypeScript code that don't result in changes to the compiled JavaScript code. 7 | # It is your responsibility as a developer to then commit the changes with `git commit --no-verify` and simply skip this commit hook. 8 | 9 | TS_FILES=$(git diff --staged --name-only | grep -c '.ts') 10 | DIST_MODIFIED=$(git diff --staged --name-only | grep -c dist/index.js) 11 | 12 | if [ $TS_FILES -gt 0 ] && [ $DIST_MODIFIED -eq 0 ] ; then 13 | echo "You modified TypeScript files but apparently did not run 'yarn build'". 14 | exit 1; 15 | fi 16 | -------------------------------------------------------------------------------- /.github/workflows/PR-CI.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: dprint/check@v1.5 11 | - uses: actions/setup-node@v1 12 | - run: yarn install 13 | - run: yarn test 14 | -------------------------------------------------------------------------------- /.github/workflows/master-CI.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: dprint/check@v1.5 14 | - uses: actions/setup-node@v1 15 | - run: yarn install 16 | - run: yarn test 17 | 18 | create-pr: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - run: | 23 | git config user.email noreply@github.com 24 | git config user.name "GitHub actions" 25 | git checkout -b feature/test/${{ github.sha }} 26 | echo date > current-date.txt 27 | git add current-date.txt 28 | git commit --message "Some changes" 29 | git push origin feature/test/${{ github.sha }} 30 | - uses: ./ 31 | id: create-pr 32 | with: 33 | head: feature/test/${{ github.sha }} 34 | title: Testing commit ${{ github.sha }} 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Validate that PR exists 37 | run: curl --fail https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.create-pr.outputs.number }} 38 | - name: Close PR 39 | run: | 40 | curl \ 41 | -X PATCH \ 42 | -H 'Accept: application/vnd.github.v3+json' \ 43 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 44 | -H "Content-Type: application/json" \ 45 | https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.create-pr.outputs.number }} \ 46 | -d '{"state":"closed"}' 47 | if: always() 48 | 49 | create-draft-pr: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v2 53 | - run: | 54 | git config user.email noreply@github.com 55 | git config user.name "GitHub actions" 56 | git checkout -b feature/test/${{ github.sha }}-draft 57 | echo date > current-date.txt 58 | git add current-date.txt 59 | git commit --message "Some changes" 60 | git push origin feature/test/${{ github.sha }}-draft 61 | - uses: ./ 62 | id: create-draft-pr 63 | with: 64 | head: feature/test/${{ github.sha }}-draft 65 | title: Testing commit ${{ github.sha }} 66 | draft: true 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | - name: Validate that PR exists 69 | run: | 70 | curl \ 71 | -H 'Accept: application/vnd.github.shadow-cat-preview+json' \ 72 | https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.create-draft-pr.outputs.number }} \ 73 | --fail \ 74 | - name: Close PR 75 | run: | 76 | curl \ 77 | -X PATCH \ 78 | -H 'Accept: application/vnd.github.v3+json' \ 79 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 80 | -H "Content-Type: application/json" \ 81 | https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.create-draft-pr.outputs.number }} \ 82 | -d '{"state":"closed"}' 83 | if: always() 84 | 85 | create-pr-with-reviewers: 86 | runs-on: ubuntu-latest 87 | steps: 88 | - uses: actions/checkout@v2 89 | - run: | 90 | git config user.email noreply@github.com 91 | git config user.name "GitHub actions" 92 | git checkout -b feature/test/${{ github.sha }}-reviewers 93 | echo date > current-date.txt 94 | git add current-date.txt 95 | git commit --message "Some changes" 96 | git push origin feature/test/${{ github.sha }}-reviewers 97 | - uses: ./ 98 | id: create-pr-with-reviewers 99 | with: 100 | head: feature/test/${{ github.sha }}-reviewers 101 | title: Testing commit ${{ github.sha }} 102 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | reviewers: ${{ github.actor }} 104 | - name: Validate that PR exists 105 | run: curl --fail https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.create-pr-with-reviewers.outputs.number }} 106 | - name: Close PR 107 | run: | 108 | curl \ 109 | -X PATCH \ 110 | -H 'Accept: application/vnd.github.v3+json' \ 111 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 112 | -H "Content-Type: application/json" \ 113 | https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.create-pr-with-reviewers.outputs.number }} \ 114 | -d '{"state":"closed"}' 115 | if: always() 116 | 117 | create-pr-with-labels: 118 | runs-on: ubuntu-latest 119 | steps: 120 | - uses: actions/checkout@v2 121 | - run: | 122 | git config user.email noreply@github.com 123 | git config user.name "GitHub actions" 124 | git checkout -b feature/test/${{ github.sha }}-labels 125 | echo date > current-date.txt 126 | git add current-date.txt 127 | git commit --message "Some changes" 128 | git push origin feature/test/${{ github.sha }}-labels 129 | - uses: ./ 130 | id: create-pr-with-labels 131 | with: 132 | head: feature/test/${{ github.sha }}-labels 133 | title: Testing commit ${{ github.sha }} 134 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 135 | labels: label1,label2 136 | - name: Validate that PR exists 137 | run: curl --fail https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.create-pr-with-labels.outputs.number }} 138 | - name: Close PR 139 | run: | 140 | curl \ 141 | -X PATCH \ 142 | -H 'Accept: application/vnd.github.v3+json' \ 143 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 144 | -H "Content-Type: application/json" \ 145 | https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.create-pr-with-labels.outputs.number }} \ 146 | -d '{"state":"closed"}' 147 | if: always() 148 | 149 | do-not-fail-on-existing_pr: 150 | runs-on: ubuntu-latest 151 | steps: 152 | - uses: actions/checkout@v2 153 | - run: | 154 | git config user.email noreply@github.com 155 | git config user.name "GitHub actions" 156 | git checkout -b feature/test/${{ github.sha }}-idempotent-case 157 | echo date > current-date.txt 158 | git add current-date.txt 159 | git commit --message "Some changes" 160 | git push origin feature/test/${{ github.sha }}-idempotent-case 161 | - uses: ./ 162 | id: create-pr 163 | with: 164 | head: feature/test/${{ github.sha }}-idempotent-case 165 | title: Testing commit ${{ github.sha }} 166 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 167 | - name: Validate that PR exists 168 | run: curl --fail https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.create-pr.outputs.number }} 169 | - uses: ./ 170 | id: second-create-pr 171 | with: 172 | head: feature/test/${{ github.sha }}-idempotent-case 173 | title: Testing commit ${{ github.sha }} 174 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 175 | - name: Validate that only first PR is created 176 | run: | 177 | if [ "${{ steps.create-pr.outputs.created }}" != "true" ] 178 | then 179 | echo "First PR is not created: ${{ steps.create-pr.outputs.created }}" 180 | exit 1 181 | fi 182 | if [ "${{ steps.second-create-pr.outputs.created }}" != "false" ] 183 | then 184 | echo "Second PR is created: ${{ steps.second-create-pr.outputs.created }}" 185 | exit 1 186 | fi 187 | - name: Close PR 188 | run: | 189 | curl \ 190 | -X PATCH \ 191 | -H 'Accept: application/vnd.github.v3+json' \ 192 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 193 | -H "Content-Type: application/json" \ 194 | https://api.github.com/repos/${{ github.repository }}/pulls/${{ steps.create-pr.outputs.number }} \ 195 | -d '{"state":"closed"}' 196 | if: always() 197 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # Ignore built ts files 98 | __tests__/runner/* 99 | lib/**/* 100 | 101 | # IntelliJ 102 | .idea 103 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.4.0] - 2024-05-06 11 | 12 | ### Changed 13 | 14 | - Execute action using `node20` instead of `node16` which is now deprecated. 15 | 16 | ## [1.3.1] - 2023-06-28 17 | 18 | ### Fixed 19 | 20 | - Deprecation warning for `set-output` command from `actions/core` dependency. 21 | 22 | ## [1.3.0] - 2022-12-12 23 | 24 | ### Added 25 | 26 | - Support for adding team reviewers by setting `team_reviewers` 27 | - Support for adding assignees by setting `assignees` 28 | 29 | ### Changed 30 | 31 | - Execute action using `node16` instead of `node12` which is now deprecated. 32 | 33 | ## [1.2.0] - 2021-07-26 34 | 35 | ### Added 36 | 37 | - Support for adding labels by setting `labels` 38 | - Check for existing pull request and `created` action output 39 | 40 | ## [1.1.0] - 2021-06-16 41 | 42 | ### Added 43 | 44 | - Git hook to make sure we always run `yarn build` before committing any Typescript changes. This should prevent dist/index.js from getting out of date. 45 | - Support for setting a proxy using the `HTTPS_PROXY` environment variable 46 | - Support for GitHub Enterprise by reading `process.env.GITHUB_REPOSITORY` 47 | 48 | ### Fixed 49 | 50 | - action.yml suggested to use `github-token` as the input where as in reality, we are looking for an input `github_token` (note the underscore!) 51 | 52 | ## [1.0.0] - 2020-02-15 53 | 54 | ### Added 55 | 56 | - Initial release! 57 | 58 | [Unreleased]: https://github.com/thomaseizinger/create-pull-request/compare/1.4.0...HEAD 59 | [1.4.0]: https://github.com/thomaseizinger/create-pull-request/compare/1.3.1...1.4.0 60 | [1.3.1]: https://github.com/thomaseizinger/create-pull-request/compare/1.3.0...1.3.1 61 | [1.3.0]: https://github.com/thomaseizinger/create-pull-request/compare/1.2.0...1.3.0 62 | [1.2.0]: https://github.com/thomaseizinger/create-pull-request/compare/1.1.0...1.2.0 63 | [1.1.0]: https://github.com/thomaseizinger/create-pull-request/compare/1.0.0...1.1.0 64 | [1.0.0]: https://github.com/thomaseizinger/create-pull-request/compare/92284b92aff90f2100e022ed93d6e485240e8a36...1.0.0 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for wanting to contribute! 4 | 5 | This action has a set of integration tests that are unfortunately a bit tricky to run due to how GitHub handles permissions. 6 | 7 | There are two sets of workflows: 8 | 9 | 1. Basic checks which run on every PR. 10 | 2. Integration tests which run only on the master branch. 11 | 12 | To make it easier to merge contributions, please: 13 | 14 | - Make pull requests from _your fork's master branch_. 15 | - Enable actions on your fork. 16 | 17 | This allows the integration tests to run on your fork, making it possible to see that a change works before merging it into `master` here. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Thomas Eizinger 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create pull request 2 | 3 | Does what it says on the tin - creates a pull request, nothing else. 4 | 5 | ## Usage 6 | 7 | ```yaml 8 | name: "Create PR" 9 | on: push 10 | 11 | jobs: 12 | create-pr-on-push: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@1.0.0 16 | 17 | - name: Create pull request 18 | uses: thomaseizinger/create-pull-request@master 19 | with: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | head: ${{ github.ref }} 22 | base: master 23 | title: "An automatically created PR!" 24 | ``` 25 | 26 | ## Advanced usage 27 | 28 | To get an idea of all inputs that are supported, have a look at [this file](./src/getInputs.ts) or the [tests](./__tests__/getInputs.test.ts). 29 | 30 | For self-hosted runners behind a corporate proxy, set the https_proxy environment variable. 31 | 32 | ```yaml 33 | - name: Create pull request 34 | uses: thomaseizinger/create-pull-request@master 35 | with: 36 | github_token: ${{ secrets.GITHUB_TOKEN }} 37 | head: ${{ github.ref }} 38 | base: master 39 | title: "An automatically created PR!" 40 | env: 41 | https_proxy: http://: 42 | ``` 43 | 44 | ### Action outputs 45 | 46 | The following outputs can be set by action 47 | 48 | - `number` - Number of the created pull request. 49 | - `html_url` - URL of the created pull request. 50 | - `created` - 'true' if pull request was successfully created, 'false' if pull request existed already. 51 | -------------------------------------------------------------------------------- /__tests__/getInputs.test.ts: -------------------------------------------------------------------------------- 1 | import { morph } from 'mock-env'; 2 | import { getInputs } from '../src/getInputs'; 3 | 4 | const MANDATORY_INPUTS = { 5 | INPUT_HEAD: 'refs/heads/feature/test', 6 | INPUT_TITLE: 'My test pull request', 7 | GITHUB_REPOSITORY: 'foo/bar', 8 | }; 9 | 10 | it('should default base to master', function() { 11 | const inputs = morph(getInputs, { 12 | ...MANDATORY_INPUTS, 13 | }); 14 | 15 | expect(inputs).toHaveProperty('base', 'master'); 16 | }); 17 | 18 | it('should parse "false" for draft as false', function() { 19 | const inputs = morph(getInputs, { 20 | ...MANDATORY_INPUTS, 21 | INPUT_DRAFT: 'false', 22 | }); 23 | 24 | expect(inputs).toHaveProperty('draft', false); 25 | }); 26 | 27 | it('should parse "true" for draft as true', function() { 28 | const inputs = morph(getInputs, { 29 | ...MANDATORY_INPUTS, 30 | INPUT_DRAFT: 'true', 31 | }); 32 | 33 | expect(inputs).toHaveProperty('draft', true); 34 | }); 35 | 36 | it('should include body if given', function() { 37 | const inputs = morph(getInputs, { 38 | ...MANDATORY_INPUTS, 39 | INPUT_BODY: 'Fixes #42', 40 | }); 41 | 42 | expect(inputs).toHaveProperty('body', 'Fixes #42'); 43 | }); 44 | 45 | it('should parse owner and repo', function() { 46 | const inputs = morph(getInputs, { 47 | ...MANDATORY_INPUTS, 48 | }); 49 | 50 | expect(inputs).toHaveProperty('owner', 'foo'); 51 | expect(inputs).toHaveProperty('repo', 'bar'); 52 | }); 53 | 54 | it('should parse owner and repo for input repository', function() { 55 | const inputs = morph(getInputs, { 56 | ...MANDATORY_INPUTS, 57 | INPUT_REPOSITORY: 'my/repository', 58 | }); 59 | 60 | expect(inputs).toHaveProperty('owner', 'my'); 61 | expect(inputs).toHaveProperty('repo', 'repository'); 62 | }); 63 | 64 | it('should default to empty list of reviewers', function() { 65 | const inputs = morph(getInputs, { 66 | ...MANDATORY_INPUTS, 67 | }); 68 | 69 | expect(inputs).toHaveProperty('reviewers', []); 70 | }); 71 | 72 | it('should split reviewers by comma', function() { 73 | const inputs = morph(getInputs, { 74 | ...MANDATORY_INPUTS, 75 | INPUT_REVIEWERS: 'thomaseizinger,bonomat', 76 | }); 77 | 78 | expect(inputs).toHaveProperty('reviewers', ['thomaseizinger', 'bonomat']); 79 | }); 80 | 81 | it('should trim reviewer names', function() { 82 | const inputs = morph(getInputs, { 83 | ...MANDATORY_INPUTS, 84 | INPUT_REVIEWERS: 'd4nte, bonomat, luckysori', 85 | }); 86 | 87 | expect(inputs).toHaveProperty('reviewers', ['d4nte', 'bonomat', 'luckysori']); 88 | }); 89 | 90 | it('should default to empty list of labels', function() { 91 | const inputs = morph(getInputs, { 92 | ...MANDATORY_INPUTS, 93 | }); 94 | 95 | expect(inputs).toHaveProperty('labels', []); 96 | }); 97 | 98 | it('should split labels by comma', function() { 99 | const inputs = morph(getInputs, { 100 | ...MANDATORY_INPUTS, 101 | INPUT_LABELS: 'label1,label2', 102 | }); 103 | 104 | expect(inputs).toHaveProperty('labels', ['label1', 'label2']); 105 | }); 106 | 107 | it('should trim labels', function() { 108 | const inputs = morph(getInputs, { 109 | ...MANDATORY_INPUTS, 110 | INPUT_LABELS: 'label1, label2, label3', 111 | }); 112 | 113 | expect(inputs).toHaveProperty('labels', ['label1', 'label2', 'label3']); 114 | }); 115 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Just Create A Pull Request' 2 | description: 'Does what it says on the tin, create a pull request, easy and simple.' 3 | author: 'Thomas Eizinger' 4 | branding: 5 | icon: git-pull-request 6 | color: white 7 | inputs: 8 | github_token: 9 | description: 'A GitHub API token' 10 | required: true 11 | head: 12 | description: 'The head ref that should be pulled into base.' 13 | required: true 14 | title: 15 | description: 'The title of the pull request.' 16 | required: true 17 | base: 18 | description: 'The base branch for the pull request. Defaults to master.' 19 | required: false 20 | draft: 21 | description: 'Whether this should be a draft PR.' 22 | required: false 23 | body: 24 | description: 'The body of the pull request.' 25 | required: false 26 | assignees: 27 | description: 'A comma-separated list of GitHub logins that should be assigned to this PR.' 28 | required: false 29 | reviewers: 30 | description: 'A comma-separated list of GitHub logins that should review this PR.' 31 | required: false 32 | team_reviewers: 33 | description: 'A comma-separated list of GitHub team slugs that should review this PR.' 34 | required: false 35 | labels: 36 | description: 'A comma-separated list of labels to be set on this PR.' 37 | required: false 38 | repository: 39 | description: 'The repository in which the PR shall be created.' 40 | required: false 41 | outputs: 42 | number: 43 | description: 'Pull Request number' 44 | html_url: 45 | description: 'URL that Pull Request may be reviewed at' 46 | runs: 47 | using: 'node20' 48 | main: 'dist/index.js' 49 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dprint.dev/schemas/v0.json", 3 | "incremental": true, 4 | "typescript": { 5 | "quoteStyle": "preferSingle", 6 | "indentWidth": 2 7 | }, 8 | "json": { 9 | }, 10 | "markdown": { 11 | }, 12 | "includes": ["**/*.{ts,tsx,js,jsx,cjs,mjs,json,md}"], 13 | "excludes": [ 14 | "**/node_modules", 15 | "**/*-lock.json", 16 | "dist/**" 17 | ], 18 | "plugins": [ 19 | "https://plugins.dprint.dev/typescript-0.46.0.wasm", 20 | "https://plugins.dprint.dev/json-0.12.0.wasm", 21 | "https://plugins.dprint.dev/markdown-0.8.0.wasm" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest', 9 | }, 10 | verbose: true, 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-pull-request", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "A GitHub action for creating pull requests.", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "postinstall": "git config core.hooksPath .githooks", 9 | "build": "webpack --mode production", 10 | "compile": "tsc", 11 | "test": "jest" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/thomaseizinger/create-pull-request.git" 16 | }, 17 | "author": "Thomas Eizinger", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@actions/core": "^1.10.0", 21 | "@octokit/action": "^1.3.0", 22 | "https-proxy-agent": "^5.0.0" 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "^24.0.23", 26 | "@types/node": "^20.12.7", 27 | "jest": "^24.9.0", 28 | "jest-circus": "^25.1.0", 29 | "mock-env": "^0.2.0", 30 | "ts-jest": "^24.2.0", 31 | "ts-loader": "^6.2.1", 32 | "typescript": "^3.7.3", 33 | "webpack": "^4.41.2", 34 | "webpack-cli": "^3.3.10" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/getInputs.ts: -------------------------------------------------------------------------------- 1 | import { getInput } from '@actions/core/lib/core'; 2 | import { 3 | IssuesAddAssigneesParams, 4 | IssuesAddLabelsParams, 5 | PullsCreateParams, 6 | PullsCreateReviewRequestParams, 7 | } from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/rest-endpoint-methods-types'; 8 | 9 | type Inputs = 10 | & PullsCreateParams 11 | & Required< 12 | Omit 13 | > 14 | & Required< 15 | Omit 16 | > 17 | & Required< 18 | Omit 19 | >; 20 | 21 | export function getInputs(): Inputs { 22 | const head = getInput('head', { required: true }); 23 | const title = getInput('title', { required: true }); 24 | const base = getInput('base') || 'master'; 25 | const draft = getInput('draft') ? JSON.parse(getInput('draft')) : undefined; 26 | const body = getInput('body') || undefined; 27 | const assignees = getInput('assignees'); 28 | const reviewers = getInput('reviewers'); 29 | const team_reviewers = getInput('team_reviewers'); 30 | const labels = getInput('labels'); 31 | const repository = getInput('repository'); 32 | 33 | const githubRepository = repository || process.env.GITHUB_REPOSITORY; 34 | 35 | if (!githubRepository) { 36 | throw new Error('GITHUB_REPOSITORY is not set'); 37 | } 38 | 39 | const [owner, repo] = githubRepository.split('/'); 40 | 41 | return { 42 | head, 43 | base, 44 | title, 45 | draft, 46 | body, 47 | owner, 48 | repo, 49 | assignees: assignees 50 | ? assignees.split(',').map(assignee => assignee.trim()) 51 | : [], 52 | reviewers: reviewers 53 | ? reviewers.split(',').map(reviewer => reviewer.trim()) 54 | : [], 55 | team_reviewers: team_reviewers 56 | ? team_reviewers.split(',').map(reviewer => reviewer.trim()) 57 | : [], 58 | labels: labels 59 | ? labels.split(',').map(label => label.trim()) 60 | : [], 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { setFailed, setOutput } from '@actions/core'; 2 | import { OctokitOptions } from '@octokit/core/dist-types/types'; 3 | import { Octokit } from '@octokit/action'; 4 | import { HttpsProxyAgent } from 'https-proxy-agent'; 5 | import { getInputs } from './getInputs'; 6 | 7 | async function run(): Promise { 8 | try { 9 | const { reviewers, team_reviewers, assignees, labels, ...pullParams } = getInputs(); 10 | 11 | const options: OctokitOptions = {}; 12 | options.baseUrl = process.env.GITHUB_API_URL; 13 | 14 | const proxy = process.env.https_proxy || process.env.HTTPS_PROXY; 15 | if (proxy) { 16 | options.request = { 17 | agent: new HttpsProxyAgent(proxy), 18 | }; 19 | } 20 | 21 | const octokit = new Octokit(options); 22 | const pullRequest = await octokit.pulls.create(pullParams); 23 | const pullNumber = pullRequest.data.number; 24 | const htmlUrl = pullRequest.data.html_url; 25 | 26 | if (reviewers.length > 0 || team_reviewers.length > 0) { 27 | await octokit.pulls.createReviewRequest({ 28 | owner: pullParams.owner, 29 | repo: pullParams.repo, 30 | pull_number: pullNumber, 31 | reviewers, 32 | team_reviewers, 33 | }); 34 | } 35 | 36 | if (assignees.length > 0) { 37 | await octokit.issues.addAssignees({ 38 | owner: pullParams.owner, 39 | repo: pullParams.repo, 40 | issue_number: pullNumber, 41 | assignees, 42 | }); 43 | } 44 | 45 | if (labels.length > 0) { 46 | await octokit.issues.addLabels({ 47 | owner: pullParams.owner, 48 | repo: pullParams.repo, 49 | issue_number: pullNumber, 50 | labels: labels, 51 | }); 52 | } 53 | 54 | setOutput('number', pullNumber.toString()); 55 | setOutput('html_url', htmlUrl); 56 | setOutput('created', 'true'); 57 | } catch (error) { 58 | if (error.message && error.message.includes('A pull request already exists')) { 59 | setOutput('created', 'false'); 60 | } else { 61 | setFailed(error.message); 62 | } 63 | } 64 | } 65 | 66 | run(); 67 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "CommonJS", 5 | "strict": true, 6 | "noEmit": true, 7 | "noImplicitAny": true, 8 | "typeRoots": [ 9 | "node_modules/@types", 10 | "./types" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /types/mock-env/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'mock-env' { 2 | function morph(callback: () => T, vars: object, toRemove?: string[]): void; 3 | } 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.ts', 5 | target: 'node', 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.tsx?$/, 10 | use: { 11 | loader: 'ts-loader', 12 | options: { 13 | compilerOptions: { 14 | noEmit: false, 15 | }, 16 | }, 17 | }, 18 | exclude: /node_modules/, 19 | }, 20 | ], 21 | }, 22 | resolve: { 23 | extensions: ['.tsx', '.ts', '.js'], 24 | // see https://stackoverflow.com/a/59267337/2489334 25 | alias: { 26 | 'universal-user-agent': path.resolve(__dirname, 'node_modules/universal-user-agent/dist-node/index.js'), 27 | }, 28 | }, 29 | output: { 30 | filename: 'index.js', 31 | path: path.resolve(__dirname, 'dist'), 32 | }, 33 | }; 34 | --------------------------------------------------------------------------------