├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── release.yml │ └── build.yml ├── test ├── fixtures │ └── files │ │ ├── file1.txt │ │ └── file2.txt ├── index_publish.test.js ├── index.test.js ├── publish.test.js └── verify.test.js ├── .gitmodules ├── .eslintrc.yml ├── .editorconfig ├── index.js ├── .releaserc ├── lib ├── get-error.js ├── resolve-config.js ├── definitions │ └── errors.js ├── publish.js └── verify.js ├── LICENSE ├── .gitignore ├── package.json ├── CHANGELOG.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: asbiin 2 | -------------------------------------------------------------------------------- /test/fixtures/files/file1.txt: -------------------------------------------------------------------------------- 1 | Upload file content -------------------------------------------------------------------------------- /test/fixtures/files/file2.txt: -------------------------------------------------------------------------------- 1 | Upload file content -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "semantic-release-github"] 2 | path = semantic-release-github 3 | url = https://github.com/semantic-release/github 4 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - standard 3 | - prettier 4 | 5 | plugins: 6 | - prettier 7 | 8 | rules: 9 | prettier/prettier: 2 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | insert_final_newline = true 3 | trim_trailing_whitespace = true 4 | end_of_line = lf 5 | charset = utf-8 6 | indent_size = 2 7 | indent_style = space 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Configuration:** 20 | - semantic-release and plugins version 21 | - semantic-release configuration 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const verify = require('./lib/verify'); 2 | const publishCall = require('./lib/publish'); 3 | 4 | let verified; 5 | 6 | async function verifyConditions(pluginConfig, context) { 7 | await verify(pluginConfig, context); 8 | verified = true; 9 | } 10 | 11 | async function publish(pluginConfig, context) { 12 | if (!verified) { 13 | await verifyConditions(pluginConfig, context); 14 | } 15 | 16 | await publishCall(pluginConfig, context); 17 | } 18 | 19 | module.exports = { 20 | verifyConditions, 21 | publish, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: github-actions 6 | directory: "/" 7 | schedule: 8 | interval: weekly 9 | time: "11:00" 10 | labels: 11 | - actions 12 | - dependencies 13 | - auto-squash 14 | 15 | # Maintain dependencies for npm/yarn 16 | - package-ecosystem: npm 17 | directory: "/" 18 | schedule: 19 | interval: weekly 20 | time: "11:00" 21 | open-pull-requests-limit: 10 22 | versioning-strategy: lockfile-only 23 | labels: 24 | - javascript 25 | - dependencies 26 | - auto-squash 27 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main", 4 | "next", 5 | "next-major", 6 | {"name": "beta", "prerelease": true}, 7 | {"name": "alpha", "prerelease": true} 8 | ], 9 | "plugins": [ 10 | ["@semantic-release/commit-analyzer", { "preset": "conventionalcommits" }], 11 | ["@semantic-release/release-notes-generator", { "preset": "conventionalcommits" }], 12 | "@semantic-release/npm", 13 | [ 14 | "@semantic-release/changelog", 15 | { 16 | "changelogFile": "CHANGELOG.md" 17 | } 18 | ], 19 | [ 20 | "@semantic-release/github", 21 | { 22 | "assets": ["CHANGELOG.md"] 23 | } 24 | ], 25 | [ 26 | "semantic-release-github-pullrequest", 27 | { 28 | "assets": ["CHANGELOG.md"], 29 | "labels": [ 30 | "semantic-release", 31 | "auto-squash" 32 | ] 33 | } 34 | ] 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /lib/get-error.js: -------------------------------------------------------------------------------- 1 | const { replace } = require('lodash'); 2 | const SemanticReleaseError = require('@semantic-release/error'); 3 | const ERROR_DEFINITIONS = require('./definitions/errors'); 4 | const ERROR_DEFINITIONS_BASE = require('@semantic-release/github/lib/definitions/errors'); 5 | const pkgBase = require('@semantic-release/github/package.json'); 6 | const [homepageBase] = pkgBase.homepage.split('#'); 7 | const pkg = require('../package.json'); 8 | const [homepage] = pkg.homepage.split('#'); 9 | 10 | module.exports = (code, ctx = {}) => { 11 | let error; 12 | if (ERROR_DEFINITIONS[code] !== undefined) { 13 | error = ERROR_DEFINITIONS[code](ctx); 14 | } else { 15 | error = ERROR_DEFINITIONS_BASE[code](ctx); 16 | error.details = replace(error.details, `${homepageBase}/blob/master/`, `${homepage}/blob/main/`); 17 | } 18 | const { message, details } = error; 19 | return new SemanticReleaseError(message, code, details); 20 | }; 21 | -------------------------------------------------------------------------------- /lib/resolve-config.js: -------------------------------------------------------------------------------- 1 | const { isNil, castArray } = require('lodash'); 2 | const resolveConfig = require('@semantic-release/github/lib/resolve-config'); 3 | 4 | module.exports = (pluginConfig, { env }) => { 5 | const { githubSha, assets, branch, pullrequestTitle, baseRef } = pluginConfig; 6 | const { githubToken, githubUrl, githubApiPathPrefix, proxy, labels } = resolveConfig(pluginConfig, { env }); 7 | return { 8 | githubToken: env.GH_TOKEN_RELEASE || githubToken, 9 | githubUrl, 10 | githubApiPathPrefix, 11 | proxy, 12 | labels, 13 | githubSha: githubSha || env.GH_SHA || env.GITHUB_SHA, 14 | assets: assets ? castArray(assets) : assets, 15 | branch: isNil(branch) 16 | ? `semantic-release-pr<%= nextRelease.version ? \`-\${nextRelease.version}\` : "" %>` 17 | : branch, 18 | pullrequestTitle: isNil(pullrequestTitle) 19 | ? `chore(release): update release<%= nextRelease.version ? \` \${nextRelease.version}\` : "" %>` 20 | : pullrequestTitle, 21 | baseRef: isNil(baseRef) ? 'main' : baseRef, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alexis Saettler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - next-major 9 | - beta 10 | - alpha 11 | 12 | workflow_dispatch: 13 | 14 | env: 15 | node-version: 16 16 | 17 | jobs: 18 | semantic: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 # Get all tags 25 | 26 | - name: Use Node.js ${{ env.node-version }} 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: ${{ env.node-version }} 30 | 31 | - name: Semantic Release 32 | uses: cycjimmy/semantic-release-action@v3 33 | id: semantic 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | GH_TOKEN_RELEASE: ${{ secrets.GH_TOKEN_RELEASE }} 38 | with: 39 | semantic_version: 18 40 | extra_plugins: | 41 | conventional-changelog-conventionalcommits@5.0.0 42 | @semantic-release/changelog@6 43 | @semantic-release/exec@6 44 | ${{ github.workspace }} 45 | 46 | - name: New release published 47 | if: steps.semantic.outputs.new_release_published == 'true' 48 | run: echo ${{ steps.semantic.outputs.new_release_version }} 49 | -------------------------------------------------------------------------------- /lib/definitions/errors.js: -------------------------------------------------------------------------------- 1 | const { inspect } = require('util'); 2 | const { isString } = require('lodash'); 3 | const pkg = require('../../package.json'); 4 | const [homepage] = pkg.homepage.split('#'); 5 | const stringify = (object) => 6 | isString(object) ? object : inspect(object, { breakLength: Number.POSITIVE_INFINITY, depth: 2, maxArrayLength: 5 }); 7 | const linkify = (file) => `${homepage}/blob/main/${file}`; 8 | 9 | module.exports = { 10 | EINVALIDPULLREQUESTTITLE: ({ pullrequestTitle }) => ({ 11 | message: 'Invalid `pullrequestTitle` option.', 12 | details: `The [pullrequestTitle option](${linkify( 13 | 'README.md#pullrequestTitle' 14 | )}) if defined, must be a non empty \`String\`. 15 | 16 | Your configuration for the \`pullrequestTitle\` option is \`${stringify(pullrequestTitle)}\`.`, 17 | }), 18 | EINVALIDBRANCH: ({ branch }) => ({ 19 | message: 'Invalid `branch` option.', 20 | details: `The [branch option](${linkify('README.md#branch')}) if defined, must be a non empty \`String\`. 21 | 22 | Your configuration for the \`branch\` option is \`${stringify(branch)}\`.`, 23 | }), 24 | EINVALIDBASEREF: ({ baseRef }) => ({ 25 | message: 'Invalid `baseRef` option.', 26 | details: `The [baseRef option](${linkify('README.md#baseRef')}) if defined, must be a non empty \`String\`. 27 | 28 | Your configuration for the \`baseRef\` option is \`${stringify(baseRef)}\`.`, 29 | }), 30 | EMISSINGSHA: ({ githubSha }) => ({ 31 | message: 'Invalid Sha.', 32 | details: `The [GitHub sha](${linkify( 33 | 'README.md#github-sha' 34 | )}) configured in the \`GH_SHA\` or \`GITHUB_SHA\` environment variable must be a valid commit sha-1. 35 | 36 | Please make sure to set the \`GH_SHA\` or \`GITHUB_SHA\` environment variable in your CI with the exact value of the commit sha-1 to use as a base reference. 37 | 38 | Your configuration for the \`githubSha\` option is \`${stringify(githubSha)}\`.`, 39 | }), 40 | }; 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | release: 9 | types: [created] 10 | 11 | env: 12 | default-node-version: 16 13 | 14 | 15 | jobs: 16 | tests: 17 | runs-on: ubuntu-latest 18 | name: Test (Node ${{ matrix.node-version }}) 19 | 20 | strategy: 21 | matrix: 22 | node-version: [14, 15, 16, 17, 18] 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | submodules: true 28 | 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | - name: npm install 34 | uses: bahmutov/npm-install@v1 35 | 36 | - name: run tests 37 | run: npm run test 38 | 39 | - name: Set version parameter 40 | id: version 41 | run: | 42 | version=$(git tag --points-at HEAD) 43 | test -z "$version" && version="main" 44 | echo "value=$version" >> $GITHUB_OUTPUT 45 | 46 | - name: SonarCloud Scan 47 | if: matrix.node-version == env.default-node-version && env.SONAR_TOKEN != '' 48 | uses: SonarSource/sonarcloud-github-action@v2.0.2 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 52 | with: 53 | args: | 54 | -Dsonar.organization=asbiin-github 55 | -Dsonar.projectKey=asbiin_semantic-release-github-pullrequest 56 | -Dsonar.projectVersion=${{ steps.version.outputs.value }} 57 | -Dsonar.sources=index.js,lib/ 58 | -Dsonar.tests=test/ 59 | -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info 60 | 61 | lint: 62 | runs-on: ubuntu-latest 63 | needs: tests 64 | name: Lint (Node ${{ matrix.node-version }}) 65 | 66 | strategy: 67 | matrix: 68 | node-version: [14, 15, 16, 17, 18] 69 | 70 | steps: 71 | - uses: actions/checkout@v4 72 | 73 | - name: Use Node.js ${{ matrix.node-version }} 74 | uses: actions/setup-node@v3 75 | with: 76 | node-version: ${{ matrix.node-version }} 77 | 78 | - name: npm install 79 | uses: bahmutov/npm-install@v1 80 | 81 | - name: run lint 82 | run: npm run lint 83 | 84 | - name: run lockfile-lint 85 | run: npx lockfile-lint --path package-lock.json 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semantic-release-github-pullrequest", 3 | "description": "semantic-release plugin to create a github pullrequest", 4 | "version": "0.0.0-development", 5 | "engines": { 6 | "node": ">=14" 7 | }, 8 | "author": "Alexis Saettler ", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/asbiin/semantic-release-github-pullrequest.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/asbiin/semantic-release-github-pullrequest/issues" 16 | }, 17 | "homepage": "https://github.com/asbiin/semantic-release-github-pullrequest#readme", 18 | "keywords": [ 19 | "semantic-release", 20 | "version", 21 | "github", 22 | "pullrequest", 23 | "publish", 24 | "push" 25 | ], 26 | "scripts": { 27 | "lint": "eslint lib index.js", 28 | "prettier": "prettier --write --list-different '**/*.js?(on)'", 29 | "semantic-release": "semantic-release", 30 | "pretest": "npm run lint", 31 | "test": "nyc ava -v" 32 | }, 33 | "ava": { 34 | "files": [ 35 | "test/**/*.test.js" 36 | ] 37 | }, 38 | "files": [ 39 | "lib", 40 | "index.js" 41 | ], 42 | "main": "index.js", 43 | "dependencies": { 44 | "@octokit/rest": "^18.1.0", 45 | "@semantic-release/error": "^3.0.0", 46 | "@semantic-release/github": "^8.0.2", 47 | "aggregate-error": "^3.1.0", 48 | "clear-module": "^4.1.2", 49 | "execa": "^5.1.1", 50 | "fs-extra": "^10.0.0", 51 | "js-base64": "^3.6.0", 52 | "lodash": "^4.17.20", 53 | "util": "^0.12.3" 54 | }, 55 | "devDependencies": { 56 | "ava": "^3.15.0", 57 | "bottleneck": "^2.19.5", 58 | "eslint": "^7.19.0", 59 | "eslint-config-prettier": "^7.2.0", 60 | "eslint-config-standard": "^16.0.2", 61 | "eslint-plugin-import": "^2.22.1", 62 | "eslint-plugin-node": "^11.1.0", 63 | "eslint-plugin-prettier": "^4.0.0", 64 | "eslint-plugin-promise": "^5.0.0", 65 | "http-proxy-agent": "^5.0.0", 66 | "https-proxy-agent": "^5.0.0", 67 | "nock": "^13.0.7", 68 | "nyc": "^15.1.0", 69 | "p-retry": "^4.3.0", 70 | "prettier": "^2.2.1", 71 | "proxy": "^1.0.2", 72 | "proxyquire": "^2.1.3", 73 | "server-destroy": "^1.0.1", 74 | "sinon": "^12.0.1", 75 | "url-join": "^4.0.1" 76 | }, 77 | "peerDependencies": { 78 | "semantic-release": ">=18.0.0" 79 | }, 80 | "nyc": { 81 | "include": [ 82 | "lib/**/*.js", 83 | "index.js" 84 | ], 85 | "exclude": [ 86 | "semantic-release-github/**" 87 | ], 88 | "reporter": [ 89 | "lcov", 90 | "text" 91 | ], 92 | "all": true 93 | }, 94 | "prettier": { 95 | "printWidth": 120, 96 | "singleQuote": true, 97 | "bracketSpacing": true, 98 | "trailingComma": "es5", 99 | "semi": true 100 | }, 101 | "publishConfig": { 102 | "access": "public" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.3.0](https://github.com/asbiin/semantic-release-github-pullrequest/compare/v1.2.1...v1.3.0) (2021-12-29) 2 | 3 | 4 | ### Features 5 | 6 | * Supports monorepo ([#87](https://github.com/asbiin/semantic-release-github-pullrequest/issues/87)) ([b08c1ff](https://github.com/asbiin/semantic-release-github-pullrequest/commit/b08c1ffa33418287c5a9392a0414fb5a0f230282)) 7 | 8 | ## [1.2.1](https://github.com/asbiin/semantic-release-github-pullrequest/compare/v1.2.0...v1.2.1) (2021-12-20) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * fix branch creation process ([#99](https://github.com/asbiin/semantic-release-github-pullrequest/issues/99)) ([e83313b](https://github.com/asbiin/semantic-release-github-pullrequest/commit/e83313b500d2d74ddd01b20fac13769302eb4713)) 14 | * fix peer dependency ([#98](https://github.com/asbiin/semantic-release-github-pullrequest/issues/98)) ([5fa7baa](https://github.com/asbiin/semantic-release-github-pullrequest/commit/5fa7baad1eea9a348f085939a476c09671a52527)) 15 | 16 | 17 | # [1.2.0](https://github.com/asbiin/semantic-release-github-pullrequest/compare/v1.1.2...v1.2.0) (2021-12-19) 18 | 19 | 20 | ### Features 21 | 22 | * unlock semantic-release peerDependencies >18 ([#95](https://github.com/asbiin/semantic-release-github-pullrequest/issues/95)) ([4450ce5](https://github.com/asbiin/semantic-release-github-pullrequest/commit/4450ce59aa04dfa92ed82f0caf61e51ed5d733aa)) 23 | 24 | 25 | ## [1.1.2](https://github.com/asbiin/semantic-release-github-pullrequest/compare/v1.1.1...v1.1.2) (2021-04-23) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * allow proxy to be false ([#34](https://github.com/asbiin/semantic-release-github-pullrequest/issues/34)) ([f87745c](https://github.com/asbiin/semantic-release-github-pullrequest/commit/f87745c3008758807c80ed5a2f4e2e5443a91ac8)) 31 | 32 | ## [1.1.1](https://github.com/asbiin/semantic-release-github-pullrequest/compare/v1.1.0...v1.1.1) (2021-02-10) 33 | 34 | 35 | ### Performance Improvements 36 | 37 | * use 'publish' semantic-relase action ([#12](https://github.com/asbiin/semantic-release-github-pullrequest/issues/12)) ([2b940f1](https://github.com/asbiin/semantic-release-github-pullrequest/commit/2b940f1da02f2d35a222a4e2332bca1d5fd6d55d)) 38 | 39 | # [1.1.0](https://github.com/asbiin/semantic-release-github-pullrequest/compare/v1.0.1...v1.1.0) (2021-02-10) 40 | 41 | 42 | ### Features 43 | 44 | * extend semantic-release/github for resolveConfig and getError ([#10](https://github.com/asbiin/semantic-release-github-pullrequest/issues/10)) ([9a7acc2](https://github.com/asbiin/semantic-release-github-pullrequest/commit/9a7acc25cc435724ba0be24ae8463603687a2ba6)) 45 | 46 | ## [1.0.1](https://github.com/asbiin/semantic-release-github-pullrequest/compare/v1.0.0...v1.0.1) (2021-02-07) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * fix pr number return ([#2](https://github.com/asbiin/semantic-release-github-pullrequest/issues/2)) ([3f3228c](https://github.com/asbiin/semantic-release-github-pullrequest/commit/3f3228c9cc7e5a2084ad084dcf8a3a5530761443)) 52 | 53 | # 1.0.0 (2021-02-07) 54 | 55 | 56 | ### Features 57 | 58 | * first version ([7a6a7a1](https://github.com/asbiin/semantic-release-github-pullrequest/commit/7a6a7a1f00a2a297ff392630355fdc256c6de341)) 59 | -------------------------------------------------------------------------------- /test/index_publish.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const nock = require('nock'); 3 | const {stub} = require('sinon'); 4 | const proxyquire = require('proxyquire'); 5 | const clearModule = require('clear-module'); 6 | const {authenticate} = require('../semantic-release-github/test/helpers/mock-github'); 7 | const rateLimit = require('../semantic-release-github/test/helpers/rate-limit'); 8 | const path = require('path'); 9 | 10 | const cwd = 'test/fixtures/files'; 11 | const client = proxyquire( 12 | '@semantic-release/github/lib/get-client', proxyquire('@semantic-release/github/lib/get-client', { 13 | '@semantic-release/github/lib/definitions/rate-limit': rateLimit, 14 | })); 15 | const index = proxyquire('..', { 16 | './lib/verify': proxyquire('../lib/verify', {'@semantic-release/github/lib/get-client': client}), 17 | './lib/publish': proxyquire('../lib/publish', {'@semantic-release/github/lib/get-client': client}), 18 | }); 19 | 20 | test.beforeEach((t) => { 21 | // Clear npm cache to refresh the module state 22 | clearModule('..'); 23 | t.context.m = index; 24 | // Stub the logger 25 | t.context.log = stub(); 26 | t.context.error = stub(); 27 | t.context.logger = {log: t.context.log, error: t.context.error}; 28 | }); 29 | 30 | test.afterEach.always(() => { 31 | // Clear nock 32 | nock.cleanAll(); 33 | }); 34 | 35 | test.serial('Run publish with 1 file', async (t) => { 36 | const owner = 'test_user'; 37 | const repo = 'test_repo'; 38 | const branch = 'refs/heads/semantic-release-pr-1.0.0'; 39 | const env = { GITHUB_TOKEN: 'github_token', GITHUB_SHA: '12345' }; 40 | const assets = ['file1.txt']; 41 | const pluginConfig = { assets }; 42 | const options = { branch: 'main', repositoryUrl: `https://github.com/${owner}/${repo}.git` }; 43 | const nextRelease = { version: '1.0.0' }; 44 | const file1Path = path.join(cwd, 'file1.txt'); 45 | 46 | const github = authenticate(env) 47 | .get(`/repos/${owner}/${repo}`) 48 | .reply(200, { permissions: { push: true } }) 49 | .get(`/repos/${owner}/${repo}/git/commits/12345`) 50 | .reply(200) 51 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"${branch}","sha":"12345"}`) 52 | .reply(201, { ref: branch }) 53 | .get(`/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}?ref=${branch}`) 54 | .reply(302, { sha: '123' }) 55 | .put( 56 | `/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}`, 57 | `{"message":"chore(release): update release 1.0.0","content":"VXBsb2FkIGZpbGUgY29udGVudA==","sha":"123","branch":"${branch}"}` 58 | ) 59 | .reply(200, {}) 60 | .post( 61 | `/repos/${owner}/${repo}/pulls`, 62 | `{"head":"${branch}","base":"main","title":"chore(release): update release 1.0.0"}` 63 | ) 64 | .reply(200, { number: 1, html_url: `https://github.com/${owner}/${repo}/pull/1` }) 65 | .put(`/repos/${owner}/${repo}/issues/1/labels`, '{"labels":["semantic-release"]}') 66 | .reply(200, {}); 67 | 68 | await t.notThrowsAsync(index.publish(pluginConfig, { env, cwd, options, nextRelease, logger: t.context.logger })); 69 | 70 | t.true(t.context.log.calledWith("Creating branch '%s'", 'semantic-release-pr-1.0.0')); 71 | t.true(t.context.log.calledWith('Creating a pull request for version %s', '1.0.0')); 72 | t.true(t.context.log.calledWith("Upload file '%s'", file1Path)); 73 | t.true(t.context.log.calledWith('Pull Request created: %s', `https://github.com/${owner}/${repo}/pull/1`)); 74 | t.true(github.isDone()); 75 | }); 76 | -------------------------------------------------------------------------------- /lib/publish.js: -------------------------------------------------------------------------------- 1 | const { template } = require('lodash'); 2 | const resolveConfig = require('./resolve-config'); 3 | const parseGithubUrl = require('@semantic-release/github/lib/parse-github-url'); 4 | const { Base64 } = require('js-base64'); 5 | const path = require('path'); 6 | const getClient = require('@semantic-release/github/lib/get-client'); 7 | const { readFile } = require('fs-extra'); 8 | const AggregateError = require('aggregate-error'); 9 | const execa = require('execa'); 10 | 11 | module.exports = async (pluginConfig, context) => { 12 | const { 13 | cwd, 14 | options: { repositoryUrl }, 15 | nextRelease, 16 | logger, 17 | } = context; 18 | const { 19 | githubToken, 20 | githubUrl, 21 | githubApiPathPrefix, 22 | githubSha, 23 | proxy, 24 | assets, 25 | branch, 26 | pullrequestTitle, 27 | labels, 28 | baseRef, 29 | } = resolveConfig(pluginConfig, context); 30 | const { owner, repo } = parseGithubUrl(repositoryUrl); 31 | let absoluteRepoPath = './'; 32 | try { 33 | absoluteRepoPath = (await execa('git', ['rev-parse', '--show-toplevel'])).stdout; 34 | } catch (e) { 35 | logger.log( 36 | "Unable to determine repository root path with `git`. Falling back to '%s'. Received error %s", 37 | absoluteRepoPath, 38 | e.message 39 | ); 40 | } 41 | 42 | const github = getClient({ githubToken, githubUrl, githubApiPathPrefix, proxy }); 43 | 44 | logger.log('Creating a pull request for version %s', nextRelease.version); 45 | 46 | const pullrequestTitleExt = template(pullrequestTitle)(context); 47 | 48 | const branchExt = template(branch)(context); 49 | let newBranch = branchExt; 50 | 51 | // Change branch name if it already exist 52 | let i = 0; 53 | let ref = ''; 54 | while (true) { 55 | if (i > 10) { 56 | throw new AggregateError(["No free branch available, try to delete branches or define a 'branch' option."]); 57 | } 58 | 59 | try { 60 | ref = `refs/heads/${newBranch}`; 61 | // Create new branch 62 | logger.log("Creating branch '%s'", newBranch); 63 | await github.git.createRef({ 64 | owner, 65 | repo, 66 | ref, 67 | sha: githubSha, 68 | }); 69 | break; 70 | } catch (error) { 71 | logger.log("Branch '%s' not created (error %d)", newBranch, error.status); 72 | newBranch = `${branchExt}-${++i}`; 73 | } 74 | } 75 | 76 | await Promise.all( 77 | assets.map(async (filePath) => { 78 | const absoluteFilePath = path.resolve(cwd, filePath); 79 | const uploadPath = path.relative(absoluteRepoPath, absoluteFilePath); 80 | 81 | // Get current file's sha 82 | let commitSha = ''; 83 | try { 84 | const { 85 | data: { sha }, 86 | } = await github.repos.getContent({ 87 | owner, 88 | repo, 89 | path: uploadPath, 90 | ref, 91 | }); 92 | commitSha = sha; 93 | } catch (error) { 94 | if (error.status === 404) { 95 | // ignore error 96 | } 97 | } 98 | 99 | const content = await readFile(absoluteFilePath); 100 | const contentEncoded = Base64.encode(content); 101 | 102 | logger.log("Upload file '%s'", uploadPath); 103 | 104 | // Update file's content 105 | await github.repos.createOrUpdateFileContents({ 106 | owner, 107 | repo, 108 | path: uploadPath, 109 | message: pullrequestTitleExt, 110 | content: contentEncoded, 111 | sha: commitSha, 112 | branch: ref, 113 | }); 114 | }) 115 | ); 116 | 117 | // Create a pull request 118 | const { 119 | data: { number, html_url: htmlUrl }, 120 | } = await github.pulls.create({ 121 | owner, 122 | repo, 123 | head: ref, 124 | base: baseRef, 125 | title: pullrequestTitleExt, 126 | }); 127 | 128 | // Add labels 129 | if (labels !== false && labels.length > 0) { 130 | await github.issues.setLabels({ 131 | owner, 132 | repo, 133 | issue_number: number, 134 | labels, 135 | }); 136 | } 137 | 138 | logger.log('Pull Request created: %s', htmlUrl); 139 | return { number, html_url: htmlUrl }; 140 | }; 141 | -------------------------------------------------------------------------------- /lib/verify.js: -------------------------------------------------------------------------------- 1 | const { isString, isPlainObject, isNil, isArray, isNumber } = require('lodash'); 2 | const urlJoin = require('url-join'); 3 | const AggregateError = require('aggregate-error'); 4 | const parseGithubUrl = require('@semantic-release/github/lib/parse-github-url'); 5 | const resolveConfig = require('./resolve-config'); 6 | const getClient = require('@semantic-release/github/lib/get-client'); 7 | const getError = require('./get-error'); 8 | 9 | const isNonEmptyString = (value) => isString(value) && value.trim(); 10 | const isStringOrStringArray = (value) => 11 | isNonEmptyString(value) || (isArray(value) && value.every((string) => isNonEmptyString(string))); 12 | const isArrayOf = (validator) => (array) => isArray(array) && array.every((value) => validator(value)); 13 | const canBeDisabled = (validator) => (value) => value === false || validator(value); 14 | 15 | const VALIDATORS = { 16 | proxy: canBeDisabled( 17 | (proxy) => isNonEmptyString(proxy) || (isPlainObject(proxy) && isNonEmptyString(proxy.host) && isNumber(proxy.port)) 18 | ), 19 | assets: isArrayOf( 20 | (asset) => isStringOrStringArray(asset) || (isPlainObject(asset) && isStringOrStringArray(asset.path)) 21 | ), 22 | pullrequestTitle: canBeDisabled(isNonEmptyString), 23 | branch: canBeDisabled(isNonEmptyString), 24 | baseRef: canBeDisabled(isNonEmptyString), 25 | labels: canBeDisabled(isArrayOf(isNonEmptyString)), 26 | }; 27 | 28 | module.exports = async (pluginConfig, context) => { 29 | const { 30 | env, 31 | options: { repositoryUrl }, 32 | logger, 33 | } = context; 34 | const { githubToken, githubUrl, githubApiPathPrefix, githubSha, proxy, ...options } = resolveConfig( 35 | pluginConfig, 36 | context 37 | ); 38 | 39 | const errors = Object.entries({ ...options, proxy }).reduce( 40 | (errors, [option, value]) => 41 | !isNil(value) && !VALIDATORS[option](value) 42 | ? [...errors, getError(`EINVALID${option.toUpperCase()}`, { [option]: value })] 43 | : errors, 44 | [] 45 | ); 46 | 47 | if (githubUrl) { 48 | logger.log('Verify GitHub authentication (%s)', urlJoin(githubUrl, githubApiPathPrefix)); 49 | } else { 50 | logger.log('Verify GitHub authentication'); 51 | } 52 | 53 | const { repo, owner } = parseGithubUrl(repositoryUrl); 54 | if (!owner || !repo) { 55 | errors.push(getError('EINVALIDGITHUBURL')); 56 | } else if (githubToken && !errors.find(({ code }) => code === 'EINVALIDPROXY')) { 57 | const github = getClient({ githubToken, githubUrl, githubApiPathPrefix, proxy }); 58 | 59 | let verified = false; 60 | 61 | if (!env.GITHUB_ACTION) { 62 | try { 63 | const { 64 | data: { 65 | permissions: { push }, 66 | }, 67 | } = await github.repos.get({ repo, owner }); 68 | if (!push) { 69 | // If authenticated as GitHub App installation, `push` will always be false. 70 | // We send another request to check if current authentication is an installation. 71 | // Note: we cannot check if the installation has all required permissions, it's 72 | // up to the user to make sure it has 73 | if (await github.request('HEAD /installation/repositories', { per_page: 1 }).catch(() => false)) { 74 | verified = true; 75 | } else { 76 | errors.push(getError('EGHNOPERMISSION', { owner, repo })); 77 | } 78 | } else { 79 | verified = true; 80 | } 81 | } catch (error) { 82 | if (error.status === 401) { 83 | errors.push(getError('EINVALIDGHTOKEN', { owner, repo })); 84 | } else if (error.status === 404) { 85 | errors.push(getError('EMISSINGREPO', { owner, repo })); 86 | } else { 87 | throw error; 88 | } 89 | } 90 | } 91 | 92 | if (verified) { 93 | try { 94 | await github.git.getCommit({ repo, owner, commit_sha: githubSha }); 95 | } catch (error) { 96 | if (error.status === 404) { 97 | errors.push(getError('EMISSINGSHA', { githubSha })); 98 | } else { 99 | throw error; 100 | } 101 | } 102 | } 103 | } 104 | 105 | if (!githubToken) { 106 | errors.push(getError('ENOGHTOKEN', { owner, repo })); 107 | } 108 | 109 | if (errors.length > 0) { 110 | throw new AggregateError(errors); 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const nock = require('nock'); 3 | const {stub} = require('sinon'); 4 | const proxyquire = require('proxyquire'); 5 | const clearModule = require('clear-module'); 6 | const {authenticate} = require('../semantic-release-github/test/helpers/mock-github'); 7 | const rateLimit = require('../semantic-release-github/test/helpers/rate-limit'); 8 | 9 | const cwd = 'test/fixtures/files'; 10 | const client = proxyquire( 11 | '@semantic-release/github/lib/get-client', proxyquire('@semantic-release/github/lib/get-client', { 12 | '@semantic-release/github/lib/definitions/rate-limit': rateLimit, 13 | })); 14 | const index = proxyquire('..', { 15 | './lib/verify': proxyquire('../lib/verify', {'@semantic-release/github/lib/get-client': client}), 16 | './lib/publish': proxyquire('../lib/publish', {'@semantic-release/github/lib/get-client': client}), 17 | }); 18 | 19 | test.beforeEach((t) => { 20 | // Clear npm cache to refresh the module state 21 | clearModule('..'); 22 | t.context.m = index; 23 | // Stub the logger 24 | t.context.log = stub(); 25 | t.context.error = stub(); 26 | t.context.logger = {log: t.context.log, error: t.context.error}; 27 | }); 28 | 29 | test.afterEach.always(() => { 30 | // Clear nock 31 | nock.cleanAll(); 32 | }); 33 | 34 | test.serial('Verify GitHub auth', async (t) => { 35 | const owner = 'test_user'; 36 | const repo = 'test_repo'; 37 | const env = {GITHUB_TOKEN: 'github_token', GITHUB_SHA: 123}; 38 | const options = {repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git`}; 39 | const github = authenticate(env) 40 | .get(`/repos/${owner}/${repo}`) 41 | .reply(200, { permissions: { push: true } }) 42 | .get(`/repos/${owner}/${repo}/git/commits/123`) 43 | .reply(200); 44 | 45 | await t.notThrowsAsync(index.verifyConditions({}, {cwd, env, options, logger: t.context.logger})); 46 | 47 | t.true(github.isDone()); 48 | }); 49 | 50 | test.serial('Verify GitHub auth with publish options', async (t) => { 51 | const owner = 'test_user'; 52 | const repo = 'test_repo'; 53 | const env = {GITHUB_TOKEN: 'github_token', GITHUB_SHA: 123}; 54 | const options = { 55 | publish: {path: '@semantic-release/github'}, 56 | repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git`, 57 | }; 58 | const github = authenticate(env) 59 | .get(`/repos/${owner}/${repo}`) 60 | .reply(200, { permissions: { push: true } }) 61 | .get(`/repos/${owner}/${repo}/git/commits/123`) 62 | .reply(200); 63 | 64 | await t.notThrowsAsync(index.verifyConditions({}, {cwd, env, options, logger: t.context.logger})); 65 | 66 | t.true(github.isDone()); 67 | }); 68 | 69 | test.serial('Verify GitHub auth and assets config', async (t) => { 70 | const owner = 'test_user'; 71 | const repo = 'test_repo'; 72 | const env = {GITHUB_TOKEN: 'github_token', GITHUB_SHA: 123}; 73 | const assets = [ 74 | {path: 'lib/file.js'}, 75 | 'file.js', 76 | ['dist/**'], 77 | ['dist/**', '!dist/*.js'], 78 | {path: ['dist/**', '!dist/*.js']}, 79 | ]; 80 | const options = { 81 | publish: [{path: '@semantic-release/npm'}], 82 | repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git`, 83 | }; 84 | const github = authenticate(env) 85 | .get(`/repos/${owner}/${repo}`) 86 | .reply(200, { permissions: { push: true } }) 87 | .get(`/repos/${owner}/${repo}/git/commits/123`) 88 | .reply(200); 89 | 90 | await t.notThrowsAsync(index.verifyConditions({assets}, {cwd, env, options, logger: t.context.logger})); 91 | 92 | t.true(github.isDone()); 93 | }); 94 | 95 | test.serial('Throw SemanticReleaseError if invalid config', async (t) => { 96 | const env = {}; 97 | const assets = [{wrongProperty: 'lib/file.js'}]; 98 | const pullrequestTitle = 42; 99 | const branch = 42; 100 | const baseRef = 42; 101 | const labels = 42; 102 | const options = { 103 | publish: [ 104 | {path: '@semantic-release/npm'}, 105 | {path: 'semantic-release-github-pullrequest', assets, pullrequestTitle, branch, baseRef, labels}, 106 | ], 107 | repositoryUrl: 'invalid_url', 108 | }; 109 | 110 | const errors = [ 111 | ...(await t.throwsAsync(index.verifyConditions({}, {cwd, env, options, logger: t.context.logger}))), 112 | ]; 113 | 114 | t.is(errors.length, 2); 115 | t.is(errors[0].name, 'SemanticReleaseError'); 116 | t.is(errors[0].code, 'EINVALIDGITHUBURL'); 117 | t.is(errors[1].name, 'SemanticReleaseError'); 118 | t.is(errors[1].code, 'ENOGHTOKEN'); 119 | }); 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # semantic-release-github-pullrequest 2 | 3 | This plugin is a plugin for [**semantic-release**](https://github.com/semantic-release/semantic-release). 4 | 5 | It automatically creates a pull request containing changes for any files you want to publish in your repository, like release notes of your newly published release. 6 | 7 | [![npm](https://img.shields.io/npm/v/semantic-release-github-pullrequest.svg?style=flat-square)](https://www.npmjs.com/package/semantic-release-github-pullrequest) 8 | [![npm](https://img.shields.io/npm/dm/semantic-release-github-pullrequest.svg?style=flat-square)](https://www.npmjs.com/package/semantic-release-github-pullrequest) 9 | [![Build](https://img.shields.io/github/workflow/status/asbiin/semantic-release-github-pullrequest/Build%20and%20test/main)](https://github.com/asbiin/semantic-release-github-pullrequest/actions?query=workflow%3A%22Build+and+test%22) 10 | [![Code coverage](https://img.shields.io/sonar/coverage/asbiin_semantic-release-github-pullrequest?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/project/activity?custom_metrics=coverage&graph=custom&id=asbiin_semantic-release-github-pullrequest) 11 | [![Lines of Code](https://img.shields.io/tokei/lines/github/asbiin/semantic-release-github-pullrequest)](https://sonarcloud.io/dashboard?id=asbiin_semantic-release-github-pullrequest) 12 | [![License](https://img.shields.io/github/license/asbiin/semantic-release-github-pullrequest)](https://opensource.org/licenses/MIT) 13 | 14 | | Step | Description | 15 | | ------------------ | ------------------------------------------------------------------------------------------------ | 16 | | `verifyConditions` | Verify that all needed configuration is present. | 17 | | `publish` | Create a branch to upload all assets and create the pull request on the base branch on GitHub. | 18 | 19 | 20 | ## Install 21 | 22 | Add the plugin to your npm-project: 23 | 24 | ```console 25 | npm install semantic-release-github-pullrequest -D 26 | ``` 27 | 28 | ## Usage 29 | 30 | The plugin can be configured in the [**semantic-release** configuration file](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#configuration): 31 | 32 | ```json 33 | { 34 | "plugins": [ 35 | "@semantic-release/commit-analyzer", 36 | "@semantic-release/release-notes-generator", 37 | [ 38 | "semantic-release-github-pullrequest", { 39 | "assets": ["CHANGELOG.md"], 40 | "baseRef": "main" 41 | } 42 | ] 43 | ] 44 | } 45 | ``` 46 | 47 | With this example, a GitHub pull request will be created, with the content of `CHANGELOG.md` file, on the `main` branch. 48 | 49 | ## Configuration 50 | 51 | ### GitHub authentication 52 | 53 | The GitHub authentication configuration is **required** and can be set via [environment variables](#environment-variables). 54 | 55 | Follow the [Creating a personal access token for the command line](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line) documentation to obtain an authentication token. The token has to be made available in your CI environment via the `GH_TOKEN_RELEASE` or `GH_TOKEN` environment variable. The user associated with the token must have push permission to the repository. 56 | 57 | When creating the token, the **minimum required scopes** are: 58 | 59 | - [`repo`](https://github.com/settings/tokens/new?scopes=repo) for a private repository 60 | - [`public_repo`](https://github.com/settings/tokens/new?scopes=public_repo) for a public repository 61 | 62 | _Note on GitHub Actions:_ You can use the default token which is provided in the secret _GITHUB_TOKEN_. However [no workflows will be triggered in the Pull Request](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#triggering-new-workflows-using-a-personal-access-token), providing it to be merged. 63 | You can use `GH_TOKEN` or `GITHUB_TOKEN` with the secret _GITHUB_TOKEN_ to create the release, and use `GH_TOKEN_RELEASE` with this plugin to create the Pull Request. 64 | 65 | ### Environment variables 66 | 67 | | Variable | Description | 68 | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | 69 | | `GH_TOKEN_RELEASE`, `GH_TOKEN` or `GITHUB_TOKEN` | **Required.** The token used to authenticate with GitHub. | 70 | | `GITHUB_API_URL` or `GH_URL` or `GITHUB_URL` | The GitHub Enterprise endpoint. | 71 | | `GH_PREFIX` or `GITHUB_PREFIX` | The GitHub Enterprise API prefix. | 72 | | `GH_SHA` or `GITHUB_SHA` | The commit sha reference to create the new branch for the pull request. On GitHub Actions, this variable is automatically set. | 73 | 74 | ### Options 75 | 76 | | Option | Description | Default | 77 | | :-------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------- | 78 | | `githubUrl` | The GitHub Enterprise endpoint. | `GH_URL` or `GITHUB_URL` environment variable. | 79 | | `githubApiPathPrefix` | The GitHub Enterprise API prefix. | `GH_PREFIX` or `GITHUB_PREFIX` environment variable. | 80 | | `proxy` | The proxy to use to access the GitHub API. Set to `false` to disable usage of proxy. See [proxy](#proxy). | `HTTP_PROXY` environment variable. | 81 | | `assets` | **Required.**. An array of files to upload to the release. See [assets](#assets). | - | 82 | | `branch` | Name of the branch that will be created in the repository. | `semantic-release-pr<%= nextRelease.version ? \`-\${nextRelease.version}\` : "" %>` | 83 | | `pullrequestTitle` | Title for the pull request. This title will also be used for all commit created to upload the assets. See [pullrequestTitle](#pullrequestTitle). | `chore(release): update release<%= nextRelease.version ? \` \${nextRelease.version}\` : "" %>` | 84 | | `labels` | The [labels](https://help.github.com/articles/about-labels) to add to the pull request created. Set to `false` to not add any label. | `['semantic-release']` | 85 | | `baseRef` | The base branch used to create the pull request (usually `main` or `master`). | `main` | 86 | 87 | 88 | 89 | #### proxy 90 | 91 | Can be `false`, a proxy URL or an `Object` with the following properties: 92 | 93 | | Property | Description | Default | 94 | |---------------|----------------------------------------------------------------|--------------------------------------| 95 | | `host` | **Required.** Proxy host to connect to. | - | 96 | | `port` | **Required.** Proxy port to connect to. | File name extracted from the `path`. | 97 | | `secureProxy` | If `true`, then use TLS to connect to the proxy. | `false` | 98 | | `headers` | Additional HTTP headers to be sent on the HTTP CONNECT method. | - | 99 | 100 | See [node-https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent#new-httpsproxyagentobject-options) and [node-http-proxy-agent](https://github.com/TooTallNate/node-http-proxy-agent) for additional details. 101 | 102 | ##### proxy examples 103 | 104 | `'http://168.63.76.32:3128'`: use the proxy running on host `168.63.76.32` and port `3128` for each GitHub API request. 105 | `{host: '168.63.76.32', port: 3128, headers: {Foo: 'bar'}}`: use the proxy running on host `168.63.76.32` and port `3128` for each GitHub API request, setting the `Foo` header value to `bar`. 106 | 107 | #### assets 108 | 109 | Can be a [glob](https://github.com/isaacs/node-glob#glob-primer) or and `Array` of 110 | [globs](https://github.com/isaacs/node-glob#glob-primer) and `Object`s with the following properties: 111 | 112 | | Property | Description | Default | 113 | | -------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------ | 114 | | `path` | **Required.** A [glob](https://github.com/isaacs/node-glob#glob-primer) to identify the files to upload. | - | 115 | | `name` | The name of the downloadable file on the GitHub release. | File name extracted from the `path`. | 116 | | `label` | Short description of the file displayed on the GitHub release. | - | 117 | 118 | Each entry in the `assets` `Array` is globbed individually. A [glob](https://github.com/isaacs/node-glob#glob-primer) 119 | can be a `String` (`"dist/**/*.js"` or `"dist/mylib.js"`) or an `Array` of `String`s that will be globbed together 120 | (`["dist/**", "!**/*.css"]`). 121 | 122 | If a directory is configured, all the files under this directory and its children will be included. 123 | 124 | The `name` and `label` for each assets are generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: 125 | 126 | | Parameter | Description | 127 | |---------------|-------------------------------------------------------------------------------------| 128 | | `branch` | The branch from which the release is done. | 129 | | `lastRelease` | `Object` with `version`, `gitTag` and `gitHead` of the last release. | 130 | | `nextRelease` | `Object` with `version`, `gitTag`, `gitHead` and `notes` of the release being done. | 131 | | `commits` | `Array` of commit `Object`s with `hash`, `subject`, `body` `message` and `author`. | 132 | 133 | **Note**: If a file has a match in `assets` it will be included even if it also has a match in `.gitignore`. 134 | 135 | #### pullrequestTitle 136 | 137 | The title of the pull request is generated with [Lodash template](https://lodash.com/docs#template). The following variables are available: 138 | 139 | | Parameter | Description | 140 | |---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 141 | | `branch` | `Object` with `name`, `type`, `channel`, `range` and `prerelease` properties of the branch from which the release is done. | 142 | | `lastRelease` | `Object` with `version`, `channel`, `gitTag` and `gitHead` of the last release. | 143 | | `nextRelease` | `Object` with `version`, `channel`, `gitTag`, `gitHead` and `notes` of the release being done. | 144 | | `releases` | `Array` with a release `Object`s for each release published, with optional release data such as `name` and `url`. | 145 | -------------------------------------------------------------------------------- /test/publish.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const nock = require('nock'); 3 | const { stub } = require('sinon'); 4 | const proxyquire = require('proxyquire'); 5 | const { authenticate } = require('../semantic-release-github/test/helpers/mock-github'); 6 | const rateLimit = require('../semantic-release-github/test/helpers/rate-limit'); 7 | const path = require('path'); 8 | 9 | /* eslint camelcase: ["error", {properties: "never"}] */ 10 | 11 | const cwd = 'test/fixtures/files'; 12 | const publish = proxyquire('../lib/publish', { 13 | '@semantic-release/github/lib/get-client': proxyquire('@semantic-release/github/lib/get-client', { 14 | '@semantic-release/github/lib/definitions/rate-limit': rateLimit, 15 | }), 16 | }); 17 | 18 | test.beforeEach((t) => { 19 | // Mock logger 20 | t.context.log = stub(); 21 | t.context.error = stub(); 22 | t.context.logger = { log: t.context.log, error: t.context.error }; 23 | }); 24 | 25 | test.afterEach.always(() => { 26 | // Clear nock 27 | nock.cleanAll(); 28 | }); 29 | 30 | test.serial('Create PR with 1 file but git binary does not exist on file system', async (t) => { 31 | const publish = proxyquire('../lib/publish', { 32 | '@semantic-release/github/lib/get-client': proxyquire('@semantic-release/github/lib/get-client', { 33 | '@semantic-release/github/lib/definitions/rate-limit': rateLimit, 34 | }), 35 | execa: function () { 36 | throw new Error('error executing git binary'); 37 | }, 38 | }); 39 | const owner = 'test_user'; 40 | const repo = 'test_repo'; 41 | const branch = 'refs/heads/semantic-release-pr-1.0.0'; 42 | const env = { GITHUB_TOKEN: 'github_token', GITHUB_SHA: '12345' }; 43 | const assets = ['file1.txt']; 44 | const pluginConfig = { assets }; 45 | const options = { branch: 'main', repositoryUrl: `https://github.com/${owner}/${repo}.git` }; 46 | const nextRelease = { version: '1.0.0' }; 47 | const file1Path = path.join(cwd, 'file1.txt'); 48 | const github = authenticate(env) 49 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"${branch}","sha":"12345"}`) 50 | .reply(201, { ref: branch }) 51 | .get(`/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}?ref=${branch}`) 52 | .reply(302, { sha: '123' }) 53 | .put( 54 | `/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}`, 55 | `{"message":"chore(release): update release 1.0.0","content":"VXBsb2FkIGZpbGUgY29udGVudA==","sha":"123","branch":"${branch}"}` 56 | ) 57 | .reply(200, {}) 58 | .post( 59 | `/repos/${owner}/${repo}/pulls`, 60 | `{"head":"${branch}","base":"main","title":"chore(release): update release 1.0.0"}` 61 | ) 62 | .reply(200, { number: 1, html_url: `https://github.com/${owner}/${repo}/pull/1` }) 63 | .put(`/repos/${owner}/${repo}/issues/1/labels`, '{"labels":["semantic-release"]}') 64 | .reply(200, {}); 65 | 66 | await t.notThrowsAsync(publish(pluginConfig, { env, cwd, options, nextRelease, logger: t.context.logger })); 67 | 68 | t.true( 69 | t.context.log.calledWith( 70 | "Unable to determine repository root path with `git`. Falling back to '%s'. Received error %s", 71 | './', 72 | 'error executing git binary' 73 | ) 74 | ); 75 | t.true(t.context.log.calledWith("Creating branch '%s'", 'semantic-release-pr-1.0.0')); 76 | t.true(t.context.log.calledWith('Creating a pull request for version %s', '1.0.0')); 77 | t.true(t.context.log.calledWith("Upload file '%s'", file1Path)); 78 | t.true(t.context.log.calledWith('Pull Request created: %s', `https://github.com/${owner}/${repo}/pull/1`)); 79 | t.true(github.isDone()); 80 | }); 81 | 82 | test.serial('Create PR with 1 file', async (t) => { 83 | const owner = 'test_user'; 84 | const repo = 'test_repo'; 85 | const branch = 'refs/heads/semantic-release-pr-1.0.0'; 86 | const env = { GITHUB_TOKEN: 'github_token', GITHUB_SHA: '12345' }; 87 | const assets = ['file1.txt']; 88 | const pluginConfig = { assets }; 89 | const options = { branch: 'main', repositoryUrl: `https://github.com/${owner}/${repo}.git` }; 90 | const nextRelease = { version: '1.0.0' }; 91 | const file1Path = path.join(cwd, 'file1.txt'); 92 | const github = authenticate(env) 93 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"${branch}","sha":"12345"}`) 94 | .reply(201, { ref: branch }) 95 | .get(`/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}?ref=${branch}`) 96 | .reply(302, { sha: '123' }) 97 | .put( 98 | `/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}`, 99 | `{"message":"chore(release): update release 1.0.0","content":"VXBsb2FkIGZpbGUgY29udGVudA==","sha":"123","branch":"${branch}"}` 100 | ) 101 | .reply(200, {}) 102 | .post( 103 | `/repos/${owner}/${repo}/pulls`, 104 | `{"head":"${branch}","base":"main","title":"chore(release): update release 1.0.0"}` 105 | ) 106 | .reply(200, { number: 1, html_url: `https://github.com/${owner}/${repo}/pull/1` }) 107 | .put(`/repos/${owner}/${repo}/issues/1/labels`, '{"labels":["semantic-release"]}') 108 | .reply(200, {}); 109 | 110 | await t.notThrowsAsync(publish(pluginConfig, { env, cwd, options, nextRelease, logger: t.context.logger })); 111 | 112 | t.true(t.context.log.calledWith("Creating branch '%s'", 'semantic-release-pr-1.0.0')); 113 | t.true(t.context.log.calledWith('Creating a pull request for version %s', '1.0.0')); 114 | t.true(t.context.log.calledWith("Upload file '%s'", file1Path)); 115 | t.true(t.context.log.calledWith('Pull Request created: %s', `https://github.com/${owner}/${repo}/pull/1`)); 116 | t.true(github.isDone()); 117 | }); 118 | 119 | test.serial('Create PR with 1 new file', async (t) => { 120 | const owner = 'test_user'; 121 | const repo = 'test_repo'; 122 | const branch = 'refs/heads/semantic-release-pr-1.0.0'; 123 | const env = { GITHUB_TOKEN: 'github_token', GITHUB_SHA: '12345' }; 124 | const assets = ['file1.txt']; 125 | const pluginConfig = { assets }; 126 | const options = { branch: 'main', repositoryUrl: `https://github.com/${owner}/${repo}.git` }; 127 | const nextRelease = { version: '1.0.0' }; 128 | const file1Path = path.join(cwd, 'file1.txt'); 129 | const github = authenticate(env) 130 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"${branch}","sha":"12345"}`) 131 | .reply(201, { ref: branch }) 132 | .get(`/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}?ref=${branch}`) 133 | .times(4) 134 | .reply(404) 135 | .put( 136 | `/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}`, 137 | `{"message":"chore(release): update release 1.0.0","content":"VXBsb2FkIGZpbGUgY29udGVudA==","sha":"","branch":"${branch}"}` 138 | ) 139 | .reply(201, {}) 140 | .post( 141 | `/repos/${owner}/${repo}/pulls`, 142 | `{"head":"${branch}","base":"main","title":"chore(release): update release 1.0.0"}` 143 | ) 144 | .reply(200, { number: 1, html_url: `https://github.com/${owner}/${repo}/pull/1` }) 145 | .put(`/repos/${owner}/${repo}/issues/1/labels`, '{"labels":["semantic-release"]}') 146 | .reply(200, {}); 147 | 148 | await t.notThrowsAsync(publish(pluginConfig, { env, cwd, options, nextRelease, logger: t.context.logger })); 149 | 150 | t.true(t.context.log.calledWith("Creating branch '%s'", 'semantic-release-pr-1.0.0')); 151 | t.true(t.context.log.calledWith('Creating a pull request for version %s', '1.0.0')); 152 | t.true(t.context.log.calledWith("Upload file '%s'", file1Path)); 153 | t.true(t.context.log.calledWith('Pull Request created: %s', `https://github.com/${owner}/${repo}/pull/1`)); 154 | t.true(github.isDone()); 155 | }); 156 | 157 | test.serial('Create PR with 2 files', async (t) => { 158 | const owner = 'test_user'; 159 | const repo = 'test_repo'; 160 | const branch = 'refs/heads/semantic-release-pr-1.0.0'; 161 | const env = { GITHUB_TOKEN: 'github_token', GITHUB_SHA: '12345' }; 162 | const assets = ['file1.txt', 'file2.txt']; 163 | const pluginConfig = { assets }; 164 | const options = { branch: 'main', repositoryUrl: `https://github.com/${owner}/${repo}.git` }; 165 | const nextRelease = { version: '1.0.0' }; 166 | const file1Path = path.join(cwd, 'file1.txt'); 167 | const file2Path = path.join(cwd, 'file2.txt'); 168 | const github = authenticate(env) 169 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"${branch}","sha":"12345"}`) 170 | .reply(201, { ref: branch }) 171 | .get(`/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}?ref=${branch}`) 172 | .reply(302, { sha: '123' }) 173 | .put( 174 | `/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}`, 175 | `{"message":"chore(release): update release 1.0.0","content":"VXBsb2FkIGZpbGUgY29udGVudA==","sha":"123","branch":"${branch}"}` 176 | ) 177 | .reply(200, {}) 178 | .get(`/repos/${owner}/${repo}/contents/${encodeURIComponent(file2Path)}?ref=${branch}`) 179 | .reply(302, { sha: '456' }) 180 | .put( 181 | `/repos/${owner}/${repo}/contents/${encodeURIComponent(file2Path)}`, 182 | `{"message":"chore(release): update release 1.0.0","content":"VXBsb2FkIGZpbGUgY29udGVudA==","sha":"456","branch":"${branch}"}` 183 | ) 184 | .reply(200, {}) 185 | .post( 186 | `/repos/${owner}/${repo}/pulls`, 187 | `{"head":"${branch}","base":"main","title":"chore(release): update release 1.0.0"}` 188 | ) 189 | .reply(200, { number: 1, html_url: `https://github.com/${owner}/${repo}/pull/1` }) 190 | .put(`/repos/${owner}/${repo}/issues/1/labels`, '{"labels":["semantic-release"]}') 191 | .reply(200, {}); 192 | 193 | await t.notThrowsAsync(publish(pluginConfig, { env, cwd, options, nextRelease, logger: t.context.logger })); 194 | 195 | t.true(t.context.log.calledWith("Creating branch '%s'", 'semantic-release-pr-1.0.0')); 196 | t.true(t.context.log.calledWith('Creating a pull request for version %s', '1.0.0')); 197 | t.true(t.context.log.calledWith("Upload file '%s'", file1Path)); 198 | t.true(t.context.log.calledWith("Upload file '%s'", file2Path)); 199 | t.true(t.context.log.calledWith('Pull Request created: %s', `https://github.com/${owner}/${repo}/pull/1`)); 200 | t.true(github.isDone()); 201 | }); 202 | 203 | test.serial('Create PR with pullrequest title', async (t) => { 204 | const owner = 'test_user'; 205 | const repo = 'test_repo'; 206 | const branch = 'refs/heads/semantic-release-pr-1.0.0'; 207 | const env = { GITHUB_TOKEN: 'github_token', GITHUB_SHA: '12345' }; 208 | const assets = ['file1.txt']; 209 | const pluginConfig = { assets, pullrequestTitle: 'my title' }; 210 | const options = { branch: 'main', repositoryUrl: `https://github.com/${owner}/${repo}.git` }; 211 | const nextRelease = { version: '1.0.0' }; 212 | const file1Path = path.join(cwd, 'file1.txt'); 213 | const github = authenticate(env) 214 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"${branch}","sha":"12345"}`) 215 | .reply(201, { ref: branch }) 216 | .get(`/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}?ref=${branch}`) 217 | .reply(302, { sha: '123' }) 218 | .put( 219 | `/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}`, 220 | `{"message":"my title","content":"VXBsb2FkIGZpbGUgY29udGVudA==","sha":"123","branch":"${branch}"}` 221 | ) 222 | .reply(200, {}) 223 | .post(`/repos/${owner}/${repo}/pulls`, `{"head":"${branch}","base":"main","title":"my title"}`) 224 | .reply(200, { number: 1, html_url: `https://github.com/${owner}/${repo}/pull/1` }) 225 | .put(`/repos/${owner}/${repo}/issues/1/labels`, '{"labels":["semantic-release"]}') 226 | .reply(200, {}); 227 | 228 | await t.notThrowsAsync(publish(pluginConfig, { env, cwd, options, nextRelease, logger: t.context.logger })); 229 | 230 | t.true(t.context.log.calledWith("Creating branch '%s'", 'semantic-release-pr-1.0.0')); 231 | t.true(t.context.log.calledWith('Creating a pull request for version %s', '1.0.0')); 232 | t.true(t.context.log.calledWith("Upload file '%s'", file1Path)); 233 | t.true(t.context.log.calledWith('Pull Request created: %s', `https://github.com/${owner}/${repo}/pull/1`)); 234 | t.true(github.isDone()); 235 | }); 236 | 237 | test.serial('Create PR with labels', async (t) => { 238 | const owner = 'test_user'; 239 | const repo = 'test_repo'; 240 | const branch = 'refs/heads/semantic-release-pr-1.0.0'; 241 | const env = { GITHUB_TOKEN: 'github_token', GITHUB_SHA: '12345' }; 242 | const assets = ['file1.txt']; 243 | const pluginConfig = { assets, labels: ['mylabel'] }; 244 | const options = { branch: 'main', repositoryUrl: `https://github.com/${owner}/${repo}.git` }; 245 | const nextRelease = { version: '1.0.0' }; 246 | const file1Path = path.join(cwd, 'file1.txt'); 247 | const github = authenticate(env) 248 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"${branch}","sha":"12345"}`) 249 | .reply(201, { ref: branch }) 250 | .get(`/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}?ref=${branch}`) 251 | .reply(302, { sha: '123' }) 252 | .put( 253 | `/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}`, 254 | `{"message":"chore(release): update release 1.0.0","content":"VXBsb2FkIGZpbGUgY29udGVudA==","sha":"123","branch":"${branch}"}` 255 | ) 256 | .reply(200, {}) 257 | .post( 258 | `/repos/${owner}/${repo}/pulls`, 259 | `{"head":"${branch}","base":"main","title":"chore(release): update release 1.0.0"}` 260 | ) 261 | .reply(200, { number: 1, html_url: `https://github.com/${owner}/${repo}/pull/1` }) 262 | .put(`/repos/${owner}/${repo}/issues/1/labels`, '{"labels":["mylabel"]}') 263 | .reply(200, {}); 264 | 265 | await t.notThrowsAsync(publish(pluginConfig, { env, cwd, options, nextRelease, logger: t.context.logger })); 266 | 267 | t.true(t.context.log.calledWith("Creating branch '%s'", 'semantic-release-pr-1.0.0')); 268 | t.true(t.context.log.calledWith('Creating a pull request for version %s', '1.0.0')); 269 | t.true(t.context.log.calledWith("Upload file '%s'", file1Path)); 270 | t.true(t.context.log.calledWith('Pull Request created: %s', `https://github.com/${owner}/${repo}/pull/1`)); 271 | t.true(github.isDone()); 272 | }); 273 | 274 | test.serial('Create PR with branch', async (t) => { 275 | const owner = 'test_user'; 276 | const repo = 'test_repo'; 277 | const branch = 'refs/heads/newbranch'; 278 | const env = { GITHUB_TOKEN: 'github_token', GITHUB_SHA: '12345' }; 279 | const assets = ['file1.txt']; 280 | const pluginConfig = { assets, baseRef: 'base', branch: 'newbranch' }; 281 | const options = { branch: 'main', repositoryUrl: `https://github.com/${owner}/${repo}.git` }; 282 | const nextRelease = { version: '1.0.0' }; 283 | const file1Path = path.join(cwd, 'file1.txt'); 284 | const github = authenticate(env) 285 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"${branch}","sha":"12345"}`) 286 | .reply(201, { ref: branch }) 287 | .get(`/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}?ref=${branch}`) 288 | .reply(302, { sha: '123' }) 289 | .put( 290 | `/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}`, 291 | `{"message":"chore(release): update release 1.0.0","content":"VXBsb2FkIGZpbGUgY29udGVudA==","sha":"123","branch":"${branch}"}` 292 | ) 293 | .reply(200, {}) 294 | .post( 295 | `/repos/${owner}/${repo}/pulls`, 296 | `{"head":"${branch}","base":"base","title":"chore(release): update release 1.0.0"}` 297 | ) 298 | .reply(200, { number: 1, html_url: `https://github.com/${owner}/${repo}/pull/1` }) 299 | .put(`/repos/${owner}/${repo}/issues/1/labels`, '{"labels":["semantic-release"]}') 300 | .reply(200, {}); 301 | 302 | await t.notThrowsAsync(publish(pluginConfig, { env, cwd, options, nextRelease, logger: t.context.logger })); 303 | 304 | t.true(t.context.log.calledWith("Creating branch '%s'", 'newbranch')); 305 | t.true(t.context.log.calledWith('Creating a pull request for version %s', '1.0.0')); 306 | t.true(t.context.log.calledWith("Upload file '%s'", file1Path)); 307 | t.true(t.context.log.calledWith('Pull Request created: %s', `https://github.com/${owner}/${repo}/pull/1`)); 308 | t.true(github.isDone()); 309 | }); 310 | 311 | test.serial('Create PR with branch already exist', async (t) => { 312 | const owner = 'test_user'; 313 | const repo = 'test_repo'; 314 | const branch = 'refs/heads/newbranch-1'; 315 | const env = { GITHUB_TOKEN: 'github_token', GITHUB_SHA: '12345' }; 316 | const assets = ['file1.txt']; 317 | const pluginConfig = { assets, baseRef: 'base', branch: 'newbranch' }; 318 | const options = { branch: 'main', repositoryUrl: `https://github.com/${owner}/${repo}.git` }; 319 | const nextRelease = { version: '1.0.0' }; 320 | const file1Path = path.join(cwd, 'file1.txt'); 321 | const github = authenticate(env) 322 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"refs/heads/newbranch","sha":"12345"}`) 323 | .reply(401) 324 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"${branch}","sha":"12345"}`) 325 | .reply(201, { ref: branch }) 326 | .get(`/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}?ref=${branch}`) 327 | .reply(302, { sha: '123' }) 328 | .put( 329 | `/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}`, 330 | `{"message":"chore(release): update release 1.0.0","content":"VXBsb2FkIGZpbGUgY29udGVudA==","sha":"123","branch":"${branch}"}` 331 | ) 332 | .reply(200, {}) 333 | .post( 334 | `/repos/${owner}/${repo}/pulls`, 335 | `{"head":"${branch}","base":"base","title":"chore(release): update release 1.0.0"}` 336 | ) 337 | .reply(200, { number: 1, html_url: `https://github.com/${owner}/${repo}/pull/1` }) 338 | .put(`/repos/${owner}/${repo}/issues/1/labels`, '{"labels":["semantic-release"]}') 339 | .reply(200, {}); 340 | 341 | await t.notThrowsAsync(publish(pluginConfig, { env, cwd, options, nextRelease, logger: t.context.logger })); 342 | 343 | t.true(t.context.log.calledWith("Branch '%s' not created (error %d)", 'newbranch', 401)); 344 | t.true(t.context.log.calledWith("Creating branch '%s'", 'newbranch-1')); 345 | t.true(t.context.log.calledWith('Creating a pull request for version %s', '1.0.0')); 346 | t.true(t.context.log.calledWith("Upload file '%s'", file1Path)); 347 | t.true(t.context.log.calledWith('Pull Request created: %s', `https://github.com/${owner}/${repo}/pull/1`)); 348 | t.true(github.isDone()); 349 | }); 350 | 351 | test.serial('Create PR with default branch already exist', async (t) => { 352 | const owner = 'test_user'; 353 | const repo = 'test_repo'; 354 | const branch = 'refs/heads/semantic-release-pr-1.0.0-1'; 355 | const env = { GITHUB_TOKEN: 'github_token', GITHUB_SHA: '12345' }; 356 | const assets = ['file1.txt']; 357 | const pluginConfig = { assets, baseRef: 'base' }; 358 | const options = { branch: 'main', repositoryUrl: `https://github.com/${owner}/${repo}.git` }; 359 | const nextRelease = { version: '1.0.0' }; 360 | const file1Path = path.join(cwd, 'file1.txt'); 361 | const github = authenticate(env) 362 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"refs/heads/semantic-release-pr-1.0.0","sha":"12345"}`) 363 | .reply(401) 364 | .post(`/repos/${owner}/${repo}/git/refs`, `{"ref":"${branch}","sha":"12345"}`) 365 | .reply(201, { ref: branch }) 366 | .get(`/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}?ref=${branch}`) 367 | .reply(302, { sha: '123' }) 368 | .put( 369 | `/repos/${owner}/${repo}/contents/${encodeURIComponent(file1Path)}`, 370 | `{"message":"chore(release): update release 1.0.0","content":"VXBsb2FkIGZpbGUgY29udGVudA==","sha":"123","branch":"${branch}"}` 371 | ) 372 | .reply(200, {}) 373 | .post( 374 | `/repos/${owner}/${repo}/pulls`, 375 | `{"head":"${branch}","base":"base","title":"chore(release): update release 1.0.0"}` 376 | ) 377 | .reply(200, { number: 1, html_url: `https://github.com/${owner}/${repo}/pull/1` }) 378 | .put(`/repos/${owner}/${repo}/issues/1/labels`, '{"labels":["semantic-release"]}') 379 | .reply(200, {}); 380 | 381 | await t.notThrowsAsync(publish(pluginConfig, { env, cwd, options, nextRelease, logger: t.context.logger })); 382 | 383 | t.true(t.context.log.calledWith("Branch '%s' not created (error %d)", 'semantic-release-pr-1.0.0', 401)); 384 | t.true(t.context.log.calledWith("Creating branch '%s'", 'semantic-release-pr-1.0.0-1')); 385 | t.true(t.context.log.calledWith('Creating a pull request for version %s', '1.0.0')); 386 | t.true(t.context.log.calledWith("Upload file '%s'", file1Path)); 387 | t.true(t.context.log.calledWith('Pull Request created: %s', `https://github.com/${owner}/${repo}/pull/1`)); 388 | t.true(github.isDone()); 389 | }); 390 | -------------------------------------------------------------------------------- /test/verify.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const nock = require('nock'); 3 | const { stub } = require('sinon'); 4 | const proxyquire = require('proxyquire'); 5 | const { authenticate } = require('../semantic-release-github/test/helpers/mock-github'); 6 | const rateLimit = require('../semantic-release-github/test/helpers/rate-limit'); 7 | 8 | /* eslint camelcase: ["error", {properties: "never"}] */ 9 | 10 | const verify = proxyquire('../lib/verify', { 11 | '@semantic-release/github/lib/get-client': proxyquire('@semantic-release/github/lib/get-client', { 12 | '@semantic-release/github/lib/definitions/rate-limit': rateLimit, 13 | }), 14 | }); 15 | 16 | test.beforeEach((t) => { 17 | // Mock logger 18 | t.context.log = stub(); 19 | t.context.error = stub(); 20 | t.context.logger = { log: t.context.log, error: t.context.error }; 21 | }); 22 | 23 | test.afterEach.always(() => { 24 | // Clear nock 25 | nock.cleanAll(); 26 | }); 27 | 28 | test.serial('Verify package, token and repository access', async (t) => { 29 | const owner = 'test_user'; 30 | const repo = 'test_repo'; 31 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 32 | const proxy = 'https://localhost'; 33 | const assets = ['file.txt']; 34 | const pullrequestTitle = 'Test pull request title'; 35 | const branch = 'test-branch'; 36 | const baseRef = 'baseRef'; 37 | const labels = ['semantic-release']; 38 | const github = authenticate(env) 39 | .get(`/repos/${owner}/${repo}`) 40 | .reply(200, { permissions: { push: true } }) 41 | .get(`/repos/${owner}/${repo}/git/commits/123`) 42 | .reply(200); 43 | 44 | await t.notThrowsAsync( 45 | verify( 46 | { proxy, assets, pullrequestTitle, branch, baseRef, labels }, 47 | { env, options: { repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git` }, logger: t.context.logger } 48 | ) 49 | ); 50 | t.true(github.isDone()); 51 | }); 52 | 53 | test.serial( 54 | 'Verify package, token and repository access with "proxy", "asset", "pullrequestTitle", "branch", "baseRef" and "label" set to "null"', 55 | async (t) => { 56 | const owner = 'test_user'; 57 | const repo = 'test_repo'; 58 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 59 | const proxy = null; 60 | const assets = null; 61 | const pullrequestTitle = null; 62 | const branch = null; 63 | const baseRef = null; 64 | const labels = null; 65 | const github = authenticate(env) 66 | .get(`/repos/${owner}/${repo}`) 67 | .reply(200, { permissions: { push: true } }) 68 | .get(`/repos/${owner}/${repo}/git/commits/123`) 69 | .reply(200); 70 | 71 | await t.notThrowsAsync( 72 | verify( 73 | { proxy, assets, pullrequestTitle, branch, baseRef, labels }, 74 | { 75 | env, 76 | options: { repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git` }, 77 | logger: t.context.logger, 78 | } 79 | ) 80 | ); 81 | t.true(github.isDone()); 82 | } 83 | ); 84 | 85 | test.serial('Verify package, token and repository access and custom URL with prefix', async (t) => { 86 | const owner = 'test_user'; 87 | const repo = 'test_repo'; 88 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 89 | const githubUrl = 'https://othertesturl.com:9090'; 90 | const githubApiPathPrefix = 'prefix'; 91 | const github = authenticate(env, { githubUrl, githubApiPathPrefix }) 92 | .get(`/repos/${owner}/${repo}`) 93 | .reply(200, { permissions: { push: true } }) 94 | .get(`/repos/${owner}/${repo}/git/commits/123`) 95 | .reply(200); 96 | 97 | await t.notThrowsAsync( 98 | verify( 99 | { githubUrl, githubApiPathPrefix }, 100 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 101 | ) 102 | ); 103 | 104 | t.true(github.isDone()); 105 | t.deepEqual(t.context.log.args[0], ['Verify GitHub authentication (%s)', 'https://othertesturl.com:9090/prefix']); 106 | }); 107 | 108 | test.serial('Verify package, token and repository access and custom URL without prefix', async (t) => { 109 | const owner = 'test_user'; 110 | const repo = 'test_repo'; 111 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 112 | const githubUrl = 'https://othertesturl.com:9090'; 113 | const github = authenticate(env, { githubUrl }) 114 | .get(`/repos/${owner}/${repo}`) 115 | .reply(200, { permissions: { push: true } }) 116 | .get(`/repos/${owner}/${repo}/git/commits/123`) 117 | .reply(200); 118 | 119 | await t.notThrowsAsync( 120 | verify( 121 | { githubUrl }, 122 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 123 | ) 124 | ); 125 | 126 | t.true(github.isDone()); 127 | t.deepEqual(t.context.log.args[0], ['Verify GitHub authentication (%s)', 'https://othertesturl.com:9090']); 128 | }); 129 | 130 | test.serial('Verify package, token and repository access and shorthand repositoryUrl URL', async (t) => { 131 | const owner = 'test_user'; 132 | const repo = 'test_repo'; 133 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 134 | const githubUrl = 'https://othertesturl.com:9090'; 135 | const github = authenticate(env, { githubUrl }) 136 | .get(`/repos/${owner}/${repo}`) 137 | .reply(200, { permissions: { push: true } }) 138 | .get(`/repos/${owner}/${repo}/git/commits/123`) 139 | .reply(200); 140 | 141 | await t.notThrowsAsync( 142 | verify({ githubUrl }, { env, options: { repositoryUrl: `github:${owner}/${repo}` }, logger: t.context.logger }) 143 | ); 144 | 145 | t.true(github.isDone()); 146 | t.deepEqual(t.context.log.args[0], ['Verify GitHub authentication (%s)', 'https://othertesturl.com:9090']); 147 | }); 148 | 149 | test.serial('Verify package, token and repository with environment variables', async (t) => { 150 | const owner = 'test_user'; 151 | const repo = 'test_repo'; 152 | const env = { 153 | GH_URL: 'https://othertesturl.com:443', 154 | GH_TOKEN: 'github_token', 155 | GH_PREFIX: 'prefix', 156 | GITHUB_SHA: 123, 157 | HTTP_PROXY: 'https://localhost', 158 | }; 159 | const github = authenticate(env) 160 | .get(`/repos/${owner}/${repo}`) 161 | .reply(200, { permissions: { push: true } }) 162 | .get(`/repos/${owner}/${repo}/git/commits/123`) 163 | .reply(200); 164 | 165 | await t.notThrowsAsync( 166 | verify( 167 | {}, 168 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 169 | ) 170 | ); 171 | 172 | t.true(github.isDone()); 173 | t.deepEqual(t.context.log.args[0], ['Verify GitHub authentication (%s)', 'https://othertesturl.com:443/prefix']); 174 | }); 175 | 176 | test.serial('Verify package, token and repository access with alternative environment varialbes', async (t) => { 177 | const owner = 'test_user'; 178 | const repo = 'test_repo'; 179 | const env = { 180 | GITHUB_URL: 'https://othertesturl.com:443', 181 | GITHUB_TOKEN: 'github_token', 182 | GITHUB_PREFIX: 'prefix', 183 | GITHUB_SHA: 123, 184 | }; 185 | const github = authenticate(env) 186 | .get(`/repos/${owner}/${repo}`) 187 | .reply(200, { permissions: { push: true } }) 188 | .get(`/repos/${owner}/${repo}/git/commits/123`) 189 | .reply(200); 190 | 191 | await t.notThrowsAsync( 192 | verify( 193 | {}, 194 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 195 | ) 196 | ); 197 | t.true(github.isDone()); 198 | }); 199 | 200 | test.serial('Verify "githubSha" is a real commit sha', async (t) => { 201 | const owner = 'test_user'; 202 | const repo = 'test_repo'; 203 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 204 | const proxy = 'https://localhost'; 205 | const assets = ['file.txt']; 206 | const pullrequestTitle = 'Test pull request title'; 207 | const branch = 'test-branch'; 208 | const baseRef = 'baseRef'; 209 | const labels = ['semantic-release']; 210 | const github = authenticate(env) 211 | .get(`/repos/${owner}/${repo}`) 212 | .reply(200, { permissions: { push: true } }) 213 | .get(`/repos/${owner}/${repo}/git/commits/123`) 214 | .reply(200); 215 | 216 | await t.notThrowsAsync( 217 | verify( 218 | { proxy, assets, pullrequestTitle, branch, baseRef, labels }, 219 | { env, options: { repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git` }, logger: t.context.logger } 220 | ) 221 | ); 222 | t.true(github.isDone()); 223 | }); 224 | 225 | test.serial('Throw SemanticReleaseError if "githubSha" is not a real commit', async (t) => { 226 | const owner = 'test_user'; 227 | const repo = 'test_repo'; 228 | const env = { GH_TOKEN: 'github_token' }; 229 | const github = authenticate(env) 230 | .get(`/repos/${owner}/${repo}`) 231 | .reply(200, { permissions: { push: true } }) 232 | .get(`/repos/${owner}/${repo}/git/commits/123`) 233 | .times(4) 234 | .reply(404); 235 | 236 | const [error, ...errors] = await t.throwsAsync( 237 | verify( 238 | { githubSha: 123 }, 239 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 240 | ) 241 | ); 242 | 243 | t.is(errors.length, 0); 244 | t.is(error.name, 'SemanticReleaseError'); 245 | t.is(error.code, 'EMISSINGSHA'); 246 | t.true(github.isDone()); 247 | }); 248 | 249 | test.serial('Verify "proxy" is a String', async (t) => { 250 | const owner = 'test_user'; 251 | const repo = 'test_repo'; 252 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 253 | const proxy = 'https://locahost'; 254 | const github = authenticate(env) 255 | .get(`/repos/${owner}/${repo}`) 256 | .reply(200, { permissions: { push: true } }) 257 | .get(`/repos/${owner}/${repo}/git/commits/123`) 258 | .reply(200); 259 | 260 | await t.notThrowsAsync( 261 | verify( 262 | { proxy }, 263 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 264 | ) 265 | ); 266 | 267 | t.true(github.isDone()); 268 | }); 269 | 270 | test.serial('Verify "proxy" is an object with "host" and "port" properties', async (t) => { 271 | const owner = 'test_user'; 272 | const repo = 'test_repo'; 273 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 274 | const proxy = { host: 'locahost', port: 80 }; 275 | const github = authenticate(env) 276 | .get(`/repos/${owner}/${repo}`) 277 | .reply(200, { permissions: { push: true } }) 278 | .get(`/repos/${owner}/${repo}/git/commits/123`) 279 | .reply(200); 280 | 281 | await t.notThrowsAsync( 282 | verify( 283 | { proxy }, 284 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 285 | ) 286 | ); 287 | 288 | t.true(github.isDone()); 289 | }); 290 | 291 | test.serial('Verify "assets" is a String', async (t) => { 292 | const owner = 'test_user'; 293 | const repo = 'test_repo'; 294 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 295 | const assets = 'file2.js'; 296 | const github = authenticate(env) 297 | .get(`/repos/${owner}/${repo}`) 298 | .reply(200, { permissions: { push: true } }) 299 | .get(`/repos/${owner}/${repo}/git/commits/123`) 300 | .reply(200); 301 | 302 | await t.notThrowsAsync( 303 | verify( 304 | { assets }, 305 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 306 | ) 307 | ); 308 | 309 | t.true(github.isDone()); 310 | }); 311 | 312 | test.serial('Verify "assets" is an Object with a path property', async (t) => { 313 | const owner = 'test_user'; 314 | const repo = 'test_repo'; 315 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 316 | const assets = { path: 'file2.js' }; 317 | const github = authenticate(env) 318 | .get(`/repos/${owner}/${repo}`) 319 | .reply(200, { permissions: { push: true } }) 320 | .get(`/repos/${owner}/${repo}/git/commits/123`) 321 | .reply(200); 322 | 323 | await t.notThrowsAsync( 324 | verify( 325 | { assets }, 326 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 327 | ) 328 | ); 329 | 330 | t.true(github.isDone()); 331 | }); 332 | 333 | test.serial('Verify "assets" is an Array of Object with a path property', async (t) => { 334 | const owner = 'test_user'; 335 | const repo = 'test_repo'; 336 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 337 | const assets = [{ path: 'file1.js' }, { path: 'file2.js' }]; 338 | const github = authenticate(env) 339 | .get(`/repos/${owner}/${repo}`) 340 | .reply(200, { permissions: { push: true } }) 341 | .get(`/repos/${owner}/${repo}/git/commits/123`) 342 | .reply(200); 343 | 344 | await t.notThrowsAsync( 345 | verify( 346 | { assets }, 347 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 348 | ) 349 | ); 350 | 351 | t.true(github.isDone()); 352 | }); 353 | 354 | test.serial('Verify "assets" is an Array of glob Arrays', async (t) => { 355 | const owner = 'test_user'; 356 | const repo = 'test_repo'; 357 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 358 | const assets = [['dist/**', '!**/*.js'], 'file2.js']; 359 | const github = authenticate(env) 360 | .get(`/repos/${owner}/${repo}`) 361 | .reply(200, { permissions: { push: true } }) 362 | .get(`/repos/${owner}/${repo}/git/commits/123`) 363 | .reply(200); 364 | 365 | await t.notThrowsAsync( 366 | verify( 367 | { assets }, 368 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 369 | ) 370 | ); 371 | 372 | t.true(github.isDone()); 373 | }); 374 | 375 | test.serial('Verify "assets" is an Array of Object with a glob Arrays in path property', async (t) => { 376 | const owner = 'test_user'; 377 | const repo = 'test_repo'; 378 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 379 | const assets = [{ path: ['dist/**', '!**/*.js'] }, { path: 'file2.js' }]; 380 | const github = authenticate(env) 381 | .get(`/repos/${owner}/${repo}`) 382 | .reply(200, { permissions: { push: true } }) 383 | .get(`/repos/${owner}/${repo}/git/commits/123`) 384 | .reply(200); 385 | 386 | await t.notThrowsAsync( 387 | verify( 388 | { assets }, 389 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 390 | ) 391 | ); 392 | 393 | t.true(github.isDone()); 394 | }); 395 | 396 | test.serial('Verify "labels" is a String', async (t) => { 397 | const owner = 'test_user'; 398 | const repo = 'test_repo'; 399 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 400 | const labels = 'semantic-release'; 401 | const github = authenticate(env) 402 | .get(`/repos/${owner}/${repo}`) 403 | .reply(200, { permissions: { push: true } }) 404 | .get(`/repos/${owner}/${repo}/git/commits/123`) 405 | .reply(200); 406 | 407 | await t.notThrowsAsync( 408 | verify( 409 | { labels }, 410 | { env, options: { repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git` }, logger: t.context.logger } 411 | ) 412 | ); 413 | 414 | t.true(github.isDone()); 415 | }); 416 | 417 | // https://github.com/asbiin/semantic-github-pullrequest/issues/182 418 | test.serial('Verify if run in GitHub Action', async (t) => { 419 | const owner = 'test_user'; 420 | const repo = 'test_repo'; 421 | const env = { 422 | GITHUB_TOKEN: 'v1.1234567890123456789012345678901234567890', 423 | GITHUB_ACTION: 'Release', 424 | GITHUB_SHA: 123, 425 | }; 426 | const proxy = 'https://localhost'; 427 | const assets = [{ path: 'lib/file.js' }, 'file.js']; 428 | const successComment = 'Test comment'; 429 | const failTitle = 'Test title'; 430 | const failComment = 'Test comment'; 431 | const labels = ['semantic-release']; 432 | 433 | authenticate(env).get(`/repos/${owner}/${repo}/git/commits/123`).reply(200); 434 | 435 | await t.notThrowsAsync( 436 | verify( 437 | { proxy, assets, successComment, failTitle, failComment, labels }, 438 | { env, options: { repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git` }, logger: t.context.logger } 439 | ) 440 | ); 441 | }); 442 | 443 | test('Throw SemanticReleaseError for missing github token', async (t) => { 444 | const [error, ...errors] = await t.throwsAsync( 445 | verify( 446 | {}, 447 | { 448 | env: {}, 449 | options: { repositoryUrl: 'https://github.com/asbiin/semantic-github-pullrequest.git' }, 450 | logger: t.context.logger, 451 | } 452 | ) 453 | ); 454 | 455 | t.is(errors.length, 0); 456 | t.is(error.name, 'SemanticReleaseError'); 457 | t.is(error.code, 'ENOGHTOKEN'); 458 | }); 459 | 460 | test.serial('Throw SemanticReleaseError for invalid token', async (t) => { 461 | const owner = 'test_user'; 462 | const repo = 'test_repo'; 463 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 464 | const github = authenticate(env).get(`/repos/${owner}/${repo}`).reply(401); 465 | 466 | const [error, ...errors] = await t.throwsAsync( 467 | verify({}, { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger }) 468 | ); 469 | 470 | t.is(errors.length, 0); 471 | t.is(error.name, 'SemanticReleaseError'); 472 | t.is(error.code, 'EINVALIDGHTOKEN'); 473 | t.true(github.isDone()); 474 | }); 475 | 476 | test('Throw SemanticReleaseError for invalid repositoryUrl', async (t) => { 477 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 478 | 479 | const [error, ...errors] = await t.throwsAsync( 480 | verify({}, { env, options: { repositoryUrl: 'invalid_url' }, logger: t.context.logger }) 481 | ); 482 | 483 | t.is(errors.length, 0); 484 | t.is(error.name, 'SemanticReleaseError'); 485 | t.is(error.code, 'EINVALIDGITHUBURL'); 486 | }); 487 | 488 | test.serial( 489 | "Throw SemanticReleaseError if token doesn't have the push permission on the repository and it's not a Github installation token", 490 | async (t) => { 491 | const owner = 'test_user'; 492 | const repo = 'test_repo'; 493 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 494 | const github = authenticate(env) 495 | .get(`/repos/${owner}/${repo}`) 496 | .reply(200, { permissions: { push: false } }) 497 | .head('/installation/repositories') 498 | .query({ per_page: 1 }) 499 | .reply(403); 500 | 501 | const [error, ...errors] = await t.throwsAsync( 502 | verify( 503 | {}, 504 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 505 | ) 506 | ); 507 | 508 | t.is(errors.length, 0); 509 | t.is(error.name, 'SemanticReleaseError'); 510 | t.is(error.code, 'EGHNOPERMISSION'); 511 | t.true(github.isDone()); 512 | } 513 | ); 514 | 515 | test.serial( 516 | "Do not throw SemanticReleaseError if token doesn't have the push permission but it is a Github installation token", 517 | async (t) => { 518 | const owner = 'test_user'; 519 | const repo = 'test_repo'; 520 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 521 | const github = authenticate(env) 522 | .get(`/repos/${owner}/${repo}`) 523 | .reply(200, { permissions: { push: false } }) 524 | .head('/installation/repositories') 525 | .query({ per_page: 1 }) 526 | .reply(200) 527 | .get(`/repos/${owner}/${repo}/git/commits/123`) 528 | .reply(200); 529 | 530 | await t.notThrowsAsync( 531 | verify( 532 | {}, 533 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 534 | ) 535 | ); 536 | 537 | t.true(github.isDone()); 538 | } 539 | ); 540 | 541 | test.serial("Throw SemanticReleaseError if the repository doesn't exist", async (t) => { 542 | const owner = 'test_user'; 543 | const repo = 'test_repo'; 544 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 545 | const github = authenticate(env).get(`/repos/${owner}/${repo}`).times(4).reply(404); 546 | 547 | const [error, ...errors] = await t.throwsAsync( 548 | verify({}, { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger }) 549 | ); 550 | 551 | t.is(errors.length, 0); 552 | t.is(error.name, 'SemanticReleaseError'); 553 | t.is(error.code, 'EMISSINGREPO'); 554 | t.true(github.isDone()); 555 | }); 556 | 557 | test.serial('Throw error if github return any other errors', async (t) => { 558 | const owner = 'test_user'; 559 | const repo = 'test_repo'; 560 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 561 | const github = authenticate(env).get(`/repos/${owner}/${repo}`).reply(500); 562 | 563 | const error = await t.throwsAsync( 564 | verify({}, { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger }) 565 | ); 566 | 567 | t.is(error.status, 500); 568 | t.true(github.isDone()); 569 | }); 570 | 571 | test('Throw SemanticReleaseError if "proxy" option is not a String or an Object', async (t) => { 572 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 573 | const proxy = 42; 574 | 575 | const [error, ...errors] = await t.throwsAsync( 576 | verify( 577 | { proxy }, 578 | { 579 | env, 580 | options: { repositoryUrl: 'https://github.com/asbiin/semantic-github-pullrequest.git' }, 581 | logger: t.context.logger, 582 | } 583 | ) 584 | ); 585 | 586 | t.is(errors.length, 0); 587 | t.is(error.name, 'SemanticReleaseError'); 588 | t.is(error.code, 'EINVALIDPROXY'); 589 | }); 590 | 591 | test('Throw SemanticReleaseError if "proxy" option is an Object with invalid properties', async (t) => { 592 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 593 | const proxy = { host: 42 }; 594 | 595 | const [error, ...errors] = await t.throwsAsync( 596 | verify( 597 | { proxy }, 598 | { 599 | env, 600 | options: { repositoryUrl: 'https://github.com/asbiin/semantic-github-pullrequest.git' }, 601 | logger: t.context.logger, 602 | } 603 | ) 604 | ); 605 | 606 | t.is(errors.length, 0); 607 | t.is(error.name, 'SemanticReleaseError'); 608 | t.is(error.code, 'EINVALIDPROXY'); 609 | }); 610 | 611 | test.serial('Throw SemanticReleaseError if "assets" option is not a String or an Array of Objects', async (t) => { 612 | const owner = 'test_user'; 613 | const repo = 'test_repo'; 614 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 615 | const assets = 42; 616 | const github = authenticate(env) 617 | .get(`/repos/${owner}/${repo}`) 618 | .reply(200, { permissions: { push: true } }) 619 | .get(`/repos/${owner}/${repo}/git/commits/123`) 620 | .reply(200); 621 | 622 | const [error, ...errors] = await t.throwsAsync( 623 | verify( 624 | { assets }, 625 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 626 | ) 627 | ); 628 | 629 | t.is(errors.length, 0); 630 | t.is(error.name, 'SemanticReleaseError'); 631 | t.is(error.code, 'EINVALIDASSETS'); 632 | t.true(github.isDone()); 633 | }); 634 | 635 | test.serial('Throw SemanticReleaseError if "assets" option is an Array with invalid elements', async (t) => { 636 | const owner = 'test_user'; 637 | const repo = 'test_repo'; 638 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 639 | const assets = ['file.js', 42]; 640 | const github = authenticate(env) 641 | .get(`/repos/${owner}/${repo}`) 642 | .reply(200, { permissions: { push: true } }) 643 | .get(`/repos/${owner}/${repo}/git/commits/123`) 644 | .reply(200); 645 | 646 | const [error, ...errors] = await t.throwsAsync( 647 | verify( 648 | { assets }, 649 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 650 | ) 651 | ); 652 | 653 | t.is(errors.length, 0); 654 | t.is(error.name, 'SemanticReleaseError'); 655 | t.is(error.code, 'EINVALIDASSETS'); 656 | t.true(github.isDone()); 657 | }); 658 | 659 | test.serial('Throw SemanticReleaseError if "assets" option is an Object missing the "path" property', async (t) => { 660 | const owner = 'test_user'; 661 | const repo = 'test_repo'; 662 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 663 | const assets = { name: 'file.js' }; 664 | const github = authenticate(env) 665 | .get(`/repos/${owner}/${repo}`) 666 | .reply(200, { permissions: { push: true } }) 667 | .get(`/repos/${owner}/${repo}/git/commits/123`) 668 | .reply(200); 669 | 670 | const [error, ...errors] = await t.throwsAsync( 671 | verify( 672 | { assets }, 673 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 674 | ) 675 | ); 676 | 677 | t.is(errors.length, 0); 678 | t.is(error.name, 'SemanticReleaseError'); 679 | t.is(error.code, 'EINVALIDASSETS'); 680 | t.true(github.isDone()); 681 | }); 682 | 683 | test.serial( 684 | 'Throw SemanticReleaseError if "assets" option is an Array with objects missing the "path" property', 685 | async (t) => { 686 | const owner = 'test_user'; 687 | const repo = 'test_repo'; 688 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 689 | const assets = [{ path: 'lib/file.js' }, { name: 'file.js' }]; 690 | const github = authenticate(env) 691 | .get(`/repos/${owner}/${repo}`) 692 | .reply(200, { permissions: { push: true } }) 693 | .get(`/repos/${owner}/${repo}/git/commits/123`) 694 | .reply(200); 695 | 696 | const [error, ...errors] = await t.throwsAsync( 697 | verify( 698 | { assets }, 699 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 700 | ) 701 | ); 702 | 703 | t.is(errors.length, 0); 704 | t.is(error.name, 'SemanticReleaseError'); 705 | t.is(error.code, 'EINVALIDASSETS'); 706 | t.true(github.isDone()); 707 | } 708 | ); 709 | 710 | test.serial('Throw SemanticReleaseError if "pullrequestTitle" option is not a String', async (t) => { 711 | const owner = 'test_user'; 712 | const repo = 'test_repo'; 713 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 714 | const pullrequestTitle = 123; 715 | const github = authenticate(env) 716 | .get(`/repos/${owner}/${repo}`) 717 | .reply(200, { permissions: { push: true } }) 718 | .get(`/repos/${owner}/${repo}/git/commits/123`) 719 | .reply(200); 720 | 721 | const [error, ...errors] = await t.throwsAsync( 722 | verify( 723 | { pullrequestTitle }, 724 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 725 | ) 726 | ); 727 | 728 | t.is(errors.length, 0); 729 | t.is(error.name, 'SemanticReleaseError'); 730 | t.is(error.code, 'EINVALIDPULLREQUESTTITLE'); 731 | t.true(github.isDone()); 732 | }); 733 | 734 | test.serial('Throw SemanticReleaseError if "pullrequestTitle" option is an empty String', async (t) => { 735 | const owner = 'test_user'; 736 | const repo = 'test_repo'; 737 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 738 | const pullrequestTitle = ''; 739 | const github = authenticate(env) 740 | .get(`/repos/${owner}/${repo}`) 741 | .reply(200, { permissions: { push: true } }) 742 | .get(`/repos/${owner}/${repo}/git/commits/123`) 743 | .reply(200); 744 | 745 | const [error, ...errors] = await t.throwsAsync( 746 | verify( 747 | { pullrequestTitle }, 748 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 749 | ) 750 | ); 751 | 752 | t.is(errors.length, 0); 753 | t.is(error.name, 'SemanticReleaseError'); 754 | t.is(error.code, 'EINVALIDPULLREQUESTTITLE'); 755 | t.true(github.isDone()); 756 | }); 757 | 758 | test.serial('Throw SemanticReleaseError if "pullrequestTitle" option is a whitespace String', async (t) => { 759 | const owner = 'test_user'; 760 | const repo = 'test_repo'; 761 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 762 | const pullrequestTitle = ' \n \r '; 763 | const github = authenticate(env) 764 | .get(`/repos/${owner}/${repo}`) 765 | .reply(200, { permissions: { push: true } }) 766 | .get(`/repos/${owner}/${repo}/git/commits/123`) 767 | .reply(200); 768 | 769 | const [error, ...errors] = await t.throwsAsync( 770 | verify( 771 | { pullrequestTitle }, 772 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 773 | ) 774 | ); 775 | 776 | t.is(errors.length, 0); 777 | t.is(error.name, 'SemanticReleaseError'); 778 | t.is(error.code, 'EINVALIDPULLREQUESTTITLE'); 779 | t.true(github.isDone()); 780 | }); 781 | 782 | test.serial('Throw SemanticReleaseError if "branch" option is not a String', async (t) => { 783 | const owner = 'test_user'; 784 | const repo = 'test_repo'; 785 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 786 | const branch = 123; 787 | const github = authenticate(env) 788 | .get(`/repos/${owner}/${repo}`) 789 | .reply(200, { permissions: { push: true } }) 790 | .get(`/repos/${owner}/${repo}/git/commits/123`) 791 | .reply(200); 792 | 793 | const [error, ...errors] = await t.throwsAsync( 794 | verify( 795 | { branch }, 796 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 797 | ) 798 | ); 799 | 800 | t.is(errors.length, 0); 801 | t.is(error.name, 'SemanticReleaseError'); 802 | t.is(error.code, 'EINVALIDBRANCH'); 803 | t.true(github.isDone()); 804 | }); 805 | 806 | test.serial('Throw SemanticReleaseError if "branch" option is an empty String', async (t) => { 807 | const owner = 'test_user'; 808 | const repo = 'test_repo'; 809 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 810 | const branch = ''; 811 | const github = authenticate(env) 812 | .get(`/repos/${owner}/${repo}`) 813 | .reply(200, { permissions: { push: true } }) 814 | .get(`/repos/${owner}/${repo}/git/commits/123`) 815 | .reply(200); 816 | 817 | const [error, ...errors] = await t.throwsAsync( 818 | verify( 819 | { branch }, 820 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 821 | ) 822 | ); 823 | 824 | t.is(errors.length, 0); 825 | t.is(error.name, 'SemanticReleaseError'); 826 | t.is(error.code, 'EINVALIDBRANCH'); 827 | t.true(github.isDone()); 828 | }); 829 | 830 | test.serial('Throw SemanticReleaseError if "branch" option is a whitespace String', async (t) => { 831 | const owner = 'test_user'; 832 | const repo = 'test_repo'; 833 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 834 | const branch = ' \n \r '; 835 | const github = authenticate(env) 836 | .get(`/repos/${owner}/${repo}`) 837 | .reply(200, { permissions: { push: true } }) 838 | .get(`/repos/${owner}/${repo}/git/commits/123`) 839 | .reply(200); 840 | 841 | const [error, ...errors] = await t.throwsAsync( 842 | verify( 843 | { branch }, 844 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 845 | ) 846 | ); 847 | 848 | t.is(errors.length, 0); 849 | t.is(error.name, 'SemanticReleaseError'); 850 | t.is(error.code, 'EINVALIDBRANCH'); 851 | t.true(github.isDone()); 852 | }); 853 | 854 | test.serial('Throw SemanticReleaseError if "baseRef" option is not a String', async (t) => { 855 | const owner = 'test_user'; 856 | const repo = 'test_repo'; 857 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 858 | const baseRef = 42; 859 | const github = authenticate(env) 860 | .get(`/repos/${owner}/${repo}`) 861 | .reply(200, { permissions: { push: true } }) 862 | .get(`/repos/${owner}/${repo}/git/commits/123`) 863 | .reply(200); 864 | 865 | const [error, ...errors] = await t.throwsAsync( 866 | verify( 867 | { baseRef }, 868 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 869 | ) 870 | ); 871 | 872 | t.is(errors.length, 0); 873 | t.is(error.name, 'SemanticReleaseError'); 874 | t.is(error.code, 'EINVALIDBASEREF'); 875 | t.true(github.isDone()); 876 | }); 877 | 878 | test.serial('Throw SemanticReleaseError if "baseRef" option is an empty String', async (t) => { 879 | const owner = 'test_user'; 880 | const repo = 'test_repo'; 881 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 882 | const baseRef = ''; 883 | const github = authenticate(env) 884 | .get(`/repos/${owner}/${repo}`) 885 | .reply(200, { permissions: { push: true } }) 886 | .get(`/repos/${owner}/${repo}/git/commits/123`) 887 | .reply(200); 888 | 889 | const [error, ...errors] = await t.throwsAsync( 890 | verify( 891 | { baseRef }, 892 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 893 | ) 894 | ); 895 | 896 | t.is(errors.length, 0); 897 | t.is(error.name, 'SemanticReleaseError'); 898 | t.is(error.code, 'EINVALIDBASEREF'); 899 | t.true(github.isDone()); 900 | }); 901 | 902 | test.serial('Throw SemanticReleaseError if "baseRef" option is a whitespace String', async (t) => { 903 | const owner = 'test_user'; 904 | const repo = 'test_repo'; 905 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 906 | const baseRef = ' \n \r '; 907 | const github = authenticate(env) 908 | .get(`/repos/${owner}/${repo}`) 909 | .reply(200, { permissions: { push: true } }) 910 | .get(`/repos/${owner}/${repo}/git/commits/123`) 911 | .reply(200); 912 | 913 | const [error, ...errors] = await t.throwsAsync( 914 | verify( 915 | { baseRef }, 916 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 917 | ) 918 | ); 919 | 920 | t.is(errors.length, 0); 921 | t.is(error.name, 'SemanticReleaseError'); 922 | t.is(error.code, 'EINVALIDBASEREF'); 923 | t.true(github.isDone()); 924 | }); 925 | 926 | test.serial('Throw SemanticReleaseError if "labels" option is not a String or an Array of String', async (t) => { 927 | const owner = 'test_user'; 928 | const repo = 'test_repo'; 929 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 930 | const labels = 42; 931 | const github = authenticate(env) 932 | .get(`/repos/${owner}/${repo}`) 933 | .reply(200, { permissions: { push: true } }) 934 | .get(`/repos/${owner}/${repo}/git/commits/123`) 935 | .reply(200); 936 | 937 | const [error, ...errors] = await t.throwsAsync( 938 | verify( 939 | { labels }, 940 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 941 | ) 942 | ); 943 | 944 | t.is(errors.length, 0); 945 | t.is(error.name, 'SemanticReleaseError'); 946 | t.is(error.code, 'EINVALIDLABELS'); 947 | t.true(github.isDone()); 948 | }); 949 | 950 | test.serial('Throw SemanticReleaseError if "labels" option is an Array with invalid elements', async (t) => { 951 | const owner = 'test_user'; 952 | const repo = 'test_repo'; 953 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 954 | const labels = ['label1', 42]; 955 | const github = authenticate(env) 956 | .get(`/repos/${owner}/${repo}`) 957 | .reply(200, { permissions: { push: true } }) 958 | .get(`/repos/${owner}/${repo}/git/commits/123`) 959 | .reply(200); 960 | 961 | const [error, ...errors] = await t.throwsAsync( 962 | verify( 963 | { labels }, 964 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 965 | ) 966 | ); 967 | 968 | t.is(errors.length, 0); 969 | t.is(error.name, 'SemanticReleaseError'); 970 | t.is(error.code, 'EINVALIDLABELS'); 971 | t.true(github.isDone()); 972 | }); 973 | 974 | test.serial('Throw SemanticReleaseError if "labels" option is a whitespace String', async (t) => { 975 | const owner = 'test_user'; 976 | const repo = 'test_repo'; 977 | const env = { GH_TOKEN: 'github_token', GITHUB_SHA: 123 }; 978 | const labels = ' \n \r '; 979 | const github = authenticate(env) 980 | .get(`/repos/${owner}/${repo}`) 981 | .reply(200, { permissions: { push: true } }) 982 | .get(`/repos/${owner}/${repo}/git/commits/123`) 983 | .reply(200); 984 | 985 | const [error, ...errors] = await t.throwsAsync( 986 | verify( 987 | { labels }, 988 | { env, options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, logger: t.context.logger } 989 | ) 990 | ); 991 | 992 | t.is(errors.length, 0); 993 | t.is(error.name, 'SemanticReleaseError'); 994 | t.is(error.code, 'EINVALIDLABELS'); 995 | t.true(github.isDone()); 996 | }); 997 | --------------------------------------------------------------------------------