├── .github └── workflows │ └── test.yml ├── action.yml ├── .eslintrc ├── README.md ├── .gitignore ├── package.json ├── CHANGELOG.md └── index.js /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Action self-test 4 | 5 | on: 6 | pull_request: 7 | branches: [ '*' ] 8 | 9 | jobs: 10 | test-action: 11 | name: Create review app 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Test the action locally 16 | uses: ./ 17 | with: 18 | github_token: ${{ secrets.GITHUB_TOKEN }} 19 | github_label: Review App 20 | heroku_api_token: ${{ secrets.HEROKU_API_TOKEN }} 21 | heroku_pipeline_id: ${{ secrets.HEROKU_PIPELINE_ID }} 22 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Heroku Review App 2 | 3 | description: Create a Heroku review app when a PR is raised by someone with write or admin access 4 | 5 | branding: 6 | icon: box 7 | color: blue 8 | 9 | inputs: 10 | github_token: 11 | required: true 12 | description: Github access token 13 | github_label: 14 | required: false 15 | description: Label to apply to PRs (or trigger builds when added to PR) 16 | default: '' 17 | should_comment_pull_request: 18 | required: false 19 | description: Whether to comment on the PR 20 | default: false 21 | heroku_api_token: 22 | required: true 23 | description: Heroku API token 24 | heroku_pipeline_id: 25 | required: true 26 | description: The UUID of your herkou pipeline to create review app within 27 | 28 | runs: 29 | using: node12 30 | main: dist/index.js 31 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "es-beautifier" 4 | ], 5 | "extends": ["eslint:recommended", "plugin:es-beautifier/standard"], 6 | "parserOptions": { 7 | "ecmaVersion": 2018, 8 | "sourceType": "script" 9 | }, 10 | "env": { 11 | "es6": true, 12 | "node": true, 13 | "jest": true 14 | }, 15 | "globals": {}, 16 | "rules": { 17 | "quotes": [ 18 | 2, 19 | "single" 20 | ], 21 | "no-undef": 2, 22 | "no-unused-vars": 2, 23 | "one-var": [ 24 | 2, 25 | { 26 | "initialized": "never", 27 | "uninitialized": "never" 28 | } 29 | ], 30 | "curly": 2, 31 | "camelcase": 0, 32 | "eqeqeq": 2, 33 | "indent": [ 34 | 2, 35 | 2, 36 | { 37 | "SwitchCase": 1 38 | } 39 | ], 40 | "lines-around-comment": [ 41 | "error", { 42 | "allowBlockStart": true 43 | } 44 | ], 45 | "no-use-before-define": [ 46 | 2, 47 | { 48 | "functions": false 49 | } 50 | ], 51 | "max-len": [ 52 | 2, 53 | { 54 | "code": 200, 55 | "ignoreComments": true 56 | } 57 | ], 58 | "max-statements-per-line": 2, 59 | "new-cap": ["error", { 60 | "properties": false 61 | }], 62 | "no-eq-null": 2, 63 | "no-var": 2, 64 | "one-var-declaration-per-line": 2, 65 | "prefer-const": 2, 66 | "semi": 2, 67 | "spaced-comment": ["error", "always", { 68 | "exceptions": ["-"] 69 | }] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heroku Review Application 2 | 3 | Create a Heroku review app when a PR is raised by someone with write or admin access 4 | 5 | ## Usage 6 | 7 | ```yaml 8 | # in .github/workflows/review-app.yml 9 | name: Heroku Review App 10 | on: 11 | pull_request: 12 | types: [opened, reopened, synchronize, labeled, closed] 13 | pull_request_target: 14 | types: [opened, reopened, synchronize, labeled, closed] 15 | 16 | jobs: 17 | heroku-review-application: 18 | name: Heroku Review App 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Heroku Review Application 22 | uses: matmar10/pr-heroku-review-app@master 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | github_label: Review App 26 | should_comment_pull_request: true 27 | heroku_api_token: ${{ secrets.HEROKU_API_TOKEN }} 28 | heroku_pipeline_id: b3db2bf0-081c-49a5-afa8-4f6a2443ad75 29 | ``` 30 | 31 | ## Available Configuration 32 | 33 | ### Inputs 34 | 35 | - **github_token** - Github API access token; needs scope to add label to issue 36 | - **github_label** - Text of what label should be added to each PR. If this label is added, it triggers a new build 37 | - **should_comment_pull_request** - If true, a comment will be added to the PR when the review app is deployed 38 | - **heroku_api_token** - Heroku API Token; generate this under your personal settings in Heroku 39 | - **heroku_pipeline_id** - Pipeline ID configured to use review apps. You can get this from the URL in your browser. 40 | 41 | ## TODO / Roadmap 42 | 43 | - [ ] Export info about the created build to be used later 44 | 45 | # Acknowledgments 46 | 47 | Thanks to [Michael Heap](https://github.com/mheap) for his [original rough implementation](https://github.com/mheap/github-action-pr-heroku-review-app) from which this version is inspired. 48 | -------------------------------------------------------------------------------- /.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 | .secrets 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # local debugging files 108 | test.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-action-heroku-review-app", 3 | "version": "0.2.0", 4 | "description": "Automatically deploy a Heroku review app for each PR & ensure the review app stays up to date.", 5 | "author": "Matthew J Martin ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:matmar10/heroku-review-app.git" 10 | }, 11 | "main": "index.js", 12 | "scripts": { 13 | "build": "ncc build index.js", 14 | "release": "release-it --ci", 15 | "prerelease": "npm run build", 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "dependencies": { 19 | "@actions/core": "^1.6.0", 20 | "@actions/github": "^5.0.1", 21 | "actions-toolkit": "^4.0.0", 22 | "heroku-client": "^3.1.0" 23 | }, 24 | "devDependencies": { 25 | "@release-it/conventional-changelog": "^4.2.2", 26 | "@zeit/ncc": "^0.22.3", 27 | "dotenv": "^16.0.0", 28 | "eslint": "^8.12.0", 29 | "eslint-plugin-es-beautifier": "^1.0.1", 30 | "release-it": "^14.14.0" 31 | }, 32 | "release-it": { 33 | "ci": true, 34 | "plugins": { 35 | "@release-it/conventional-changelog": { 36 | "infile": "CHANGELOG.md", 37 | "preset": { 38 | "name": "conventionalcommits", 39 | "types": [ 40 | { 41 | "type": "feat", 42 | "section": "Features" 43 | }, 44 | { 45 | "type": "fix", 46 | "section": "Bug Fixes" 47 | }, 48 | { 49 | "type": "perf", 50 | "section": "Performance Improvements" 51 | }, 52 | { 53 | "type": "revert", 54 | "section": "Reverts" 55 | }, 56 | { 57 | "type": "docs", 58 | "section": "Documentation" 59 | }, 60 | { 61 | "type": "style", 62 | "section": "Styles" 63 | }, 64 | { 65 | "type": "refactor", 66 | "section": "Code Refactoring" 67 | }, 68 | { 69 | "type": "test", 70 | "section": "Tests" 71 | }, 72 | { 73 | "type": "build", 74 | "section": "Build System" 75 | }, 76 | { 77 | "type": "ci", 78 | "section": "Continuous Integration" 79 | } 80 | ] 81 | } 82 | } 83 | }, 84 | "git": { 85 | "commit": true, 86 | "tag": true, 87 | "push": true, 88 | "requireUpstream": false 89 | }, 90 | "github": { 91 | "release": true 92 | }, 93 | "npm": { 94 | "publish": true 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 0.2.0 (2022-04-25) 4 | 5 | 6 | ### Features 7 | 8 | * output the created/updated app id and web URL ([6b7712f](https://github.com/matmar10/heroku-review-app/commit/6b7712fa9db7316b9ffb3e3046e8f2300b53753d)) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * add missing distribution files ([5d5e2b3](https://github.com/matmar10/heroku-review-app/commit/5d5e2b36845ce0850a3ba23cb91595fe248e635e)) 14 | * fetch tarbell via github archive API & update debugging output ([8728158](https://github.com/matmar10/heroku-review-app/commit/87281588400b0795cb553185d2bf7d16f68bb965)) 15 | * incorrect annotation methods for github core ([f363de3](https://github.com/matmar10/heroku-review-app/commit/f363de38f05b07ed66eafce07305826aff5760ac)) 16 | * invalid event name check crashing ([fbaec4b](https://github.com/matmar10/heroku-review-app/commit/fbaec4b10489a831c1f223476d669617432f68f2)) 17 | * missing pipeline ID due to referenceing env variable not action input ([e311d7e](https://github.com/matmar10/heroku-review-app/commit/e311d7e102d6faca92b777b666857fe25e77dde0)) 18 | * only check for label if PR event is labeled ([6c6b601](https://github.com/matmar10/heroku-review-app/commit/6c6b601ac530ec3bed2939e16fedb78b906ad746)) 19 | * use find app to check build status ([25a1133](https://github.com/matmar10/heroku-review-app/commit/25a1133c094cc6c975ea0c7e2069f5050fc03c6c)) 20 | * use find app to check build status ([882e58e](https://github.com/matmar10/heroku-review-app/commit/882e58ef0c9187fee187a19031f6d36d40c11437)) 21 | * wait for seconds not milliseconds ([e30ac39](https://github.com/matmar10/heroku-review-app/commit/e30ac398d1ddc6744c5b6ee1bdd0b4334f8dc94a)) 22 | 23 | 24 | ### Reverts 25 | 26 | * start from fresh version history ([d55537d](https://github.com/matmar10/heroku-review-app/commit/d55537dacfd91942fedb4fb14fc7eee3aee994fa)) 27 | 28 | 29 | ### Code Refactoring 30 | 31 | * use latest octokit ([9c9aee8](https://github.com/matmar10/heroku-review-app/commit/9c9aee8d23bbd1e491af58be9d7e0d603232939e)) 32 | 33 | 34 | ### Tests 35 | 36 | * allow writing test files using dotenv configuration ([0248d83](https://github.com/matmar10/heroku-review-app/commit/0248d8346eda87b21701859410f7df1431acf9e0)) 37 | * rename base URL for API tests to disambiguate ([434bac4](https://github.com/matmar10/heroku-review-app/commit/434bac4beffaf37331706d07ccdfe16475eaab22)) 38 | 39 | 40 | ### Documentation 41 | 42 | * add github action branding & unique title ([9c12a4a](https://github.com/matmar10/heroku-review-app/commit/9c12a4a119580819b24373b859d19c5cc6037f0c)) 43 | * unfork since code has completely diverged use new repo name ([17519df](https://github.com/matmar10/heroku-review-app/commit/17519df8d7823c04b8eebec8c3fb7247b6860619)) 44 | * update config section to use inputs not env variables ([a0ac403](https://github.com/matmar10/heroku-review-app/commit/a0ac403b165ab629ee34eb16c340c14292ecd0e4)) 45 | * update readme example to self-reference; add roadmap ([adf7e7d](https://github.com/matmar10/heroku-review-app/commit/adf7e7da8757278f1a30bed4b0424706d07bcd28)) 46 | 47 | 48 | ### Continuous Integration 49 | 50 | * scope publicly to avoid paid since this is public ([262c955](https://github.com/matmar10/heroku-review-app/commit/262c955d73aec24ea01288e84698434986750222)) 51 | * update npm name to be unique ([0a6d780](https://github.com/matmar10/heroku-review-app/commit/0a6d78024effebbc4d3178a377f945548335549e))### [1.0.3](https://github.com/matmar10/pr-heroku-review-app/compare/v1.0.1...v1.0.3) (2022-04-06) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * add missing distribution files ([5d5e2b3](https://github.com/matmar10/pr-heroku-review-app/commit/5d5e2b36845ce0850a3ba23cb91595fe248e635e)) 57 | * fetch tarbell via github archive API & update debugging output ([8728158](https://github.com/matmar10/pr-heroku-review-app/commit/87281588400b0795cb553185d2bf7d16f68bb965)) 58 | * incorrect annotation methods for github core ([f363de3](https://github.com/matmar10/pr-heroku-review-app/commit/f363de38f05b07ed66eafce07305826aff5760ac)) 59 | * invalid event name check crashing ([fbaec4b](https://github.com/matmar10/pr-heroku-review-app/commit/fbaec4b10489a831c1f223476d669617432f68f2)) 60 | * missing pipeline ID due to referenceing env variable not action input ([e311d7e](https://github.com/matmar10/pr-heroku-review-app/commit/e311d7e102d6faca92b777b666857fe25e77dde0)) 61 | * only check for label if PR event is labeled ([6c6b601](https://github.com/matmar10/pr-heroku-review-app/commit/6c6b601ac530ec3bed2939e16fedb78b906ad746)) 62 | * use find app to check build status ([25a1133](https://github.com/matmar10/pr-heroku-review-app/commit/25a1133c094cc6c975ea0c7e2069f5050fc03c6c)) 63 | * use find app to check build status ([882e58e](https://github.com/matmar10/pr-heroku-review-app/commit/882e58ef0c9187fee187a19031f6d36d40c11437)) 64 | * wait for seconds not milliseconds ([e30ac39](https://github.com/matmar10/pr-heroku-review-app/commit/e30ac398d1ddc6744c5b6ee1bdd0b4334f8dc94a)) 65 | 66 | 67 | ### Tests 68 | 69 | * allow writing test files using dotenv configuration ([0248d83](https://github.com/matmar10/pr-heroku-review-app/commit/0248d8346eda87b21701859410f7df1431acf9e0)) 70 | 71 | 72 | ### Documentation 73 | 74 | * update config section to use inputs not env variables ([a0ac403](https://github.com/matmar10/pr-heroku-review-app/commit/a0ac403b165ab629ee34eb16c340c14292ecd0e4)) 75 | * update readme example to self-reference; add roadmap ([adf7e7d](https://github.com/matmar10/pr-heroku-review-app/commit/adf7e7da8757278f1a30bed4b0424706d07bcd28))### [1.0.1](https://github.com/matmar10/pr-heroku-review-app/compare/v1.0.0...v1.0.1) (2022-04-04) 76 | 77 | 78 | ### Code Refactoring 79 | 80 | * use latest octokit ([9c9aee8](https://github.com/matmar10/pr-heroku-review-app/commit/9c9aee8d23bbd1e491af58be9d7e0d603232939e)) 81 | 82 | 83 | ### Continuous Integration 84 | 85 | * scope publicly to avoid paid since this is public ([262c955](https://github.com/matmar10/pr-heroku-review-app/commit/262c955d73aec24ea01288e84698434986750222)) 86 | 87 | ## [1.0.0](https://github.com/matmar10/pr-heroku-review-app/compare/v1.0.0...v1.0.1) (2019-09-07) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Heroku = require('heroku-client'); 2 | const core = require('@actions/core'); 3 | const github = require('@actions/github'); 4 | 5 | const VALID_EVENT = 'pull_request'; 6 | 7 | async function run() { 8 | try { 9 | const githubToken = core.getInput('github_token', { required: true }); 10 | const prLabel = core.getInput('github_label', { 11 | required: false, 12 | default: 'Review App', 13 | }); 14 | const shouldCommentPR = core.getInput('should_comment_pull_request', { 15 | required: false, 16 | default: false, 17 | }); 18 | const herokuApiToken = core.getInput('heroku_api_token', { 19 | required: true, 20 | }); 21 | const herokuPipelineId = core.getInput('heroku_pipeline_id', { 22 | required: true, 23 | }); 24 | 25 | const octokit = new github.getOctokit(githubToken); 26 | const heroku = new Heroku({ token: herokuApiToken }); 27 | 28 | const { 29 | action, 30 | eventName, 31 | payload: { 32 | pull_request: { 33 | head: { 34 | ref: branch, 35 | sha: version, 36 | repo: { 37 | id: repoId, 38 | fork: forkRepo, 39 | html_url: repoHtmlUrl, 40 | }, 41 | }, 42 | number: prNumber, 43 | // updated_at: prUpdatedAtRaw, 44 | }, 45 | }, 46 | issue: { 47 | number: issueNumber, 48 | }, 49 | repo, 50 | } = github.context; 51 | 52 | const { 53 | owner: repoOwner, 54 | } = repo; 55 | 56 | if (eventName !== VALID_EVENT) { 57 | throw new Error(`Unexpected github event trigger: ${eventName}`); 58 | } 59 | 60 | // const prUpdatedAt = DateTime.fromISO(prUpdatedAtRaw); 61 | const sourceUrl = `${repoHtmlUrl}/tarball/${version}`; 62 | const forkRepoId = forkRepo ? repoId : undefined; 63 | 64 | const getAppDetails = async (id) => { 65 | const url = `/apps/${id}`; 66 | core.debug(`Getting app details for app ID ${id} (${url})`); 67 | const appDetails = await heroku.get(url); 68 | core.info(`Got app details for app ID ${id} OK: ${JSON.stringify(appDetails)}`); 69 | return appDetails; 70 | }; 71 | 72 | const outputAppDetails = (app) => { 73 | core.startGroup('Output app details'); 74 | const { 75 | id: appId, 76 | web_url: webUrl, 77 | } = app; 78 | core.info(`Review app ID: "${appId}"`); 79 | core.setOutput('app_id', appId); 80 | core.info(`Review app Web URL: "${webUrl}"`); 81 | core.setOutput('app_web_url', webUrl); 82 | core.endGroup(); 83 | }; 84 | 85 | const findReviewApp = async () => { 86 | const apiUrl = `/pipelines/${herokuPipelineId}/review-apps`; 87 | core.debug(`Listing review apps: "${apiUrl}"`); 88 | const reviewApps = await heroku.get(apiUrl); 89 | core.info(`Listed ${reviewApps.length} review apps OK: ${reviewApps.length} apps found.`); 90 | 91 | core.debug(`Finding review app for PR #${prNumber}...`); 92 | const app = reviewApps.find(app => app.pr_number === prNumber); 93 | if (app) { 94 | const { status } = app; 95 | if ('errored' === status) { 96 | core.notice(`Found review app for PR #${prNumber} OK, but status is "${status}"`); 97 | return null; 98 | } 99 | core.info(`Found review app for PR #${prNumber} OK: ${JSON.stringify(app)}`); 100 | } else { 101 | core.info(`No review app found for PR #${prNumber}`); 102 | } 103 | return app; 104 | }; 105 | 106 | const waitReviewAppUpdated = async () => { 107 | core.startGroup('Ensure review app is up to date'); 108 | 109 | const waitSeconds = secs => new Promise(resolve => setTimeout(resolve, secs * 1000)); 110 | 111 | const checkBuildStatusForReviewApp = async (app) => { 112 | core.debug(`Checking build status for app: ${JSON.stringify(app)}`); 113 | if ('pending' === app.status || 'creating' === app.status) { 114 | return false; 115 | } 116 | if ('deleting' === app.status) { 117 | throw new Error(`Unexpected app status: "${app.status}" - ${app.message} (error status: ${app.error_status})`); 118 | } 119 | if (!app.app) { 120 | throw new Error(`Unexpected app status: "${app.status}"`); 121 | } 122 | const { 123 | app: { 124 | id: appId, 125 | }, 126 | status, 127 | error_status: errorStatus, 128 | } = app; 129 | 130 | core.debug(`Fetching latest builds for app ${appId}...`); 131 | const latestBuilds = await heroku.get(`/apps/${appId}/builds`); 132 | core.debug(`Fetched latest builds for pipeline ${appId} OK: ${latestBuilds.length} builds found.`); 133 | 134 | core.debug(`Finding build matching version ${version}...`); 135 | const build = await latestBuilds.find(build => version === build.source_blob.version); 136 | if (!build) { 137 | core.error(`Could not find build matching version ${version}.`); 138 | core.setFailed(`No existing build for app ID ${appId} matches version ${version}`); 139 | throw new Error(`Unexpected build status: "${status}" yet no matching build found`); 140 | } 141 | core.info(`Found build matching version ${version} OK: ${JSON.stringify(build)}`); 142 | 143 | switch (build.status) { 144 | case 'succeeded': 145 | return true; 146 | case 'pending': 147 | return false; 148 | default: 149 | throw new Error(`Unexpected build status: "${status}": ${errorStatus || 'no error provided'}`); 150 | } 151 | }; 152 | 153 | let reviewApp; 154 | let isFinished; 155 | do { 156 | reviewApp = await findReviewApp(); 157 | isFinished = await checkBuildStatusForReviewApp(reviewApp); 158 | await waitSeconds(5); 159 | } while (!isFinished); 160 | core.endGroup(); 161 | 162 | return getAppDetails(reviewApp.app.id); 163 | }; 164 | 165 | const createReviewApp = async () => { 166 | try { 167 | core.startGroup('Create review app'); 168 | 169 | const archiveBody = { 170 | owner: repoOwner, 171 | repo: repo.repo, 172 | ref: version, 173 | }; 174 | core.debug(`Fetching archive: ${JSON.stringify(archiveBody)}`); 175 | const { url: archiveUrl } = await octokit.rest.repos.downloadTarballArchive(archiveBody); 176 | core.info(`Fetched archive OK: ${JSON.stringify(archiveUrl)}`); 177 | 178 | const body = { 179 | branch, 180 | pipeline: herokuPipelineId, 181 | source_blob: { 182 | url: archiveUrl, 183 | version, 184 | }, 185 | fork_repo_id: forkRepoId, 186 | pr_number: prNumber, 187 | environment: { 188 | GIT_REPO_URL: repoHtmlUrl, 189 | }, 190 | }; 191 | core.debug(`Creating heroku review app: ${JSON.stringify(body)}`); 192 | const app = await heroku.post('/review-apps', { body }); 193 | core.info('Created review app OK:', app); 194 | core.endGroup(); 195 | 196 | return app; 197 | } catch (err) { 198 | // 409 indicates duplicate; anything else is unexpected 199 | if (err.statusCode !== 409) { 200 | throw err; 201 | } 202 | // possibly build kicked off after this PR action began running 203 | core.warning('Review app now seems to exist after previously not...'); 204 | core.endGroup(); 205 | 206 | // just some sanity checking 207 | const app = await findReviewApp(); 208 | if (!app) { 209 | throw new Error('Previously got status 409 but no app found'); 210 | } 211 | return app; 212 | } 213 | }; 214 | 215 | core.debug(`Deploy info: ${JSON.stringify({ 216 | branch, 217 | version, 218 | repoId, 219 | forkRepo, 220 | forkRepoId, 221 | repoHtmlUrl, 222 | prNumber, 223 | issueNumber, 224 | repoOwner, 225 | sourceUrl, 226 | })}`); 227 | 228 | if (forkRepo) { 229 | core.notice('No secrets are available for PRs in forked repos.'); 230 | return; 231 | } 232 | 233 | if ('labeled' === action) { 234 | core.startGroup('PR labelled'); 235 | core.debug('Checking PR label...'); 236 | const { 237 | payload: { 238 | label: { 239 | name: newLabelAddedName, 240 | }, 241 | }, 242 | } = github.context; 243 | if (newLabelAddedName === prLabel) { 244 | core.info(`Checked PR label: "${newLabelAddedName}", so need to create review app...`); 245 | await createReviewApp(); 246 | const app = await waitReviewAppUpdated(); 247 | outputAppDetails(app); 248 | } else { 249 | core.info('Checked PR label OK: "${newLabelAddedName}", no action required.'); 250 | } 251 | core.endGroup(); 252 | return; 253 | } 254 | 255 | // Only people that can close PRs are maintainers or the author 256 | // hence can safely delete review app without being collaborator 257 | if ('closed' === action) { 258 | core.debug('PR closed, deleting review app...'); 259 | const app = await findReviewApp(); 260 | if (app) { 261 | await heroku.delete(`/review-apps/${app.id}`); 262 | core.info('PR closed, deleted review app OK'); 263 | core.endGroup(); 264 | } else { 265 | core.error(`Could not find review app for PR #${prNumber}`); 266 | core.setFailed(`Action "closed", yet no existing review app for PR #${prNumber}`); 267 | } 268 | return; 269 | } 270 | 271 | // TODO: ensure we have permission 272 | // const perms = await tools.github.repos.getCollaboratorPermissionLevel({ 273 | // ...tools.context.repo, 274 | // username: tools.context.actor, 275 | // }); 276 | 277 | const app = await findReviewApp(); 278 | if (!app) { 279 | await createReviewApp(); 280 | } 281 | const updatedApp = await waitReviewAppUpdated(); 282 | outputAppDetails(updatedApp); 283 | 284 | if (prLabel) { 285 | core.startGroup('Label PR'); 286 | core.debug(`Adding label "${prLabel}" to PR...`); 287 | await octokit.rest.issues.addLabels({ 288 | ...repo, 289 | labels: [prLabel], 290 | issue_number: prNumber, 291 | }); 292 | core.info(`Added label "${prLabel}" to PR... OK`); 293 | core.endGroup(); 294 | } else { 295 | core.debug('No label specified; will not label PR'); 296 | } 297 | 298 | if (shouldCommentPR) { 299 | core.startGroup('Comment on PR'); 300 | core.debug('Adding comment to PR...'); 301 | await octokit.rest.issues.createComment({ 302 | ...repo, 303 | issue_number: prNumber, 304 | body: `Review app deployed to ${updatedApp.web_url}`, 305 | }); 306 | core.info('Added comment to PR... OK'); 307 | core.endGroup(); 308 | } else { 309 | core.debug('should_comment_pull_request is not set; will not comment on PR'); 310 | } 311 | } catch (err) { 312 | core.error(err); 313 | core.setFailed(err.message); 314 | } 315 | } 316 | 317 | run(); 318 | --------------------------------------------------------------------------------