├── .eslintignore ├── .prettierignore ├── dist ├── elf_cam_bg.wasm └── thread.js ├── doc_assets ├── github-deployment.png └── deploy-url-comment.png ├── src ├── index.d.ts ├── wait.ts ├── inputs.ts └── main.ts ├── .prettierrc.json ├── jest.config.js ├── functions-test └── hello.js ├── .github ├── dependabot.yml └── workflows │ ├── comment-run.yml │ └── test.yml ├── tsconfig.json ├── LICENSE ├── package.json ├── action.yml ├── .eslintrc.json ├── .gitignore ├── __tests__ ├── production-deploy.test.ts └── main.test.ts ├── README.md └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /dist/elf_cam_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwtgck/actions-netlify/HEAD/dist/elf_cam_bg.wasm -------------------------------------------------------------------------------- /doc_assets/github-deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwtgck/actions-netlify/HEAD/doc_assets/github-deployment.png -------------------------------------------------------------------------------- /doc_assets/deploy-url-comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwtgck/actions-netlify/HEAD/doc_assets/deploy-url-comment.png -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Can be removed if https://github.com/netlify/js-client/issues/89 is resolved 2 | declare module 'netlify' 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } -------------------------------------------------------------------------------- /src/wait.ts: -------------------------------------------------------------------------------- 1 | export async function wait(milliseconds: number): Promise { 2 | return new Promise(resolve => { 3 | if (isNaN(milliseconds)) { 4 | throw new Error('milliseconds not a number') 5 | } 6 | 7 | setTimeout(() => resolve('done!'), milliseconds) 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /functions-test/hello.js: -------------------------------------------------------------------------------- 1 | // NOTE: This is for operational test 2 | 3 | // Go to https://hogehoge--nwtgck-actions-netlify.netlify.app/.netlify/functions/hello?name=John 4 | // (from: https://kentcdodds.com/blog/super-simple-start-to-serverless) 5 | exports.handler = async event => { 6 | const subject = event.queryStringParameters.name || 'World' 7 | return { 8 | statusCode: 200, 9 | body: `Hello ${subject}!`, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | timezone: Asia/Tokyo 8 | open-pull-requests-limit: 99 9 | reviewers: 10 | - nwtgck 11 | assignees: 12 | - nwtgck 13 | - package-ecosystem: github-actions 14 | directory: "/" 15 | schedule: 16 | interval: daily 17 | timezone: Asia/Tokyo 18 | open-pull-requests-limit: 99 19 | reviewers: [ nwtgck ] 20 | assignees: [ nwtgck ] 21 | -------------------------------------------------------------------------------- /.github/workflows/comment-run.yml: -------------------------------------------------------------------------------- 1 | name: "Comment run" 2 | on: 3 | issue_comment: 4 | types: [created, edited] 5 | 6 | jobs: 7 | comment-run: 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | # 0 indicates all history 13 | fetch-depth: 0 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 16 17 | - uses: nwtgck/actions-comment-run@v3.0 18 | with: 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | allowed-associations: '["OWNER"]' 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 5 | "outDir": "./lib", /* Redirect output structure to the directory. */ 6 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 7 | "strict": true, /* Enable all strict type-checking options. */ 8 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 9 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 10 | }, 11 | "exclude": ["node_modules", "**/*.test.ts", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 Ryo Ota, GitHub, Inc. and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "build-test" 2 | on: 3 | push: 4 | pull_request: 5 | 6 | permissions: 7 | statuses: write 8 | deployments: write 9 | pull-requests: write 10 | 11 | jobs: 12 | build: # make sure build/ci work properly 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | - uses: actions/checkout@v4 19 | - run: npm ci 20 | - run: npm run all 21 | test: # make sure the action works on a clean machine without building 22 | runs-on: ubuntu-20.04 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Create a simple Web site 26 | run: mkdir -p deploy_dist && echo -e "
$(date -u)\n$GITHUB_SHA\n$GITHUB_REF
" > deploy_dist/index.html 27 | - uses: ./ 28 | id: deploy-to-netlify 29 | with: 30 | publish-dir: './deploy_dist' 31 | production-branch: master 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | deploy-message: "Deploy from GitHub Actions" 34 | functions-dir: './functions-test' 35 | env: 36 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 37 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 38 | - run: "echo 'outputs.deploy-url: ${{ steps.deploy-to-netlify.outputs.deploy-url }}'" 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actions-netlify", 3 | "version": "3.0.0", 4 | "private": true, 5 | "description": "GitHub Actions for Netlify", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "build": "tsc", 9 | "format": "prettier --write **/*.ts", 10 | "format-check": "prettier --check **/*.ts", 11 | "lint": "eslint src/**/*.ts", 12 | "pack": "ncc build", 13 | "test": "jest", 14 | "all": "npm run build && npm run format && npm run lint && npm run pack && npm test" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/nwtgck/actions-netlify.git" 19 | }, 20 | "keywords": [ 21 | "actions", 22 | "Netlify" 23 | ], 24 | "author": "Ryo Ota (https://github.com/nwtgck)", 25 | "license": "MIT", 26 | "dependencies": { 27 | "@actions/core": "^1.10.0", 28 | "@actions/github": "^6.0.0", 29 | "netlify": "^6.1.29" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "^29.5.3", 33 | "@types/node": "^20.11.25", 34 | "@typescript-eslint/eslint-plugin": "^5.59.2", 35 | "@typescript-eslint/parser": "^5.62.0", 36 | "@vercel/ncc": "^0.38.1", 37 | "eslint": "^8.47.0", 38 | "jest": "^29.6.2", 39 | "jest-circus": "^29.5.0", 40 | "jest-mock": "^29.5.0", 41 | "js-yaml": "^4.1.0", 42 | "prettier": "^3.0.2", 43 | "ts-jest": "^29.1.1", 44 | "typescript": "^5.1.6" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /dist/thread.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const crypto = require('crypto'); 4 | const {parentPort} = require('worker_threads'); 5 | 6 | const handlers = { 7 | hashFile: (algorithm, filePath) => new Promise((resolve, reject) => { 8 | const hasher = crypto.createHash(algorithm); 9 | fs.createReadStream(filePath) 10 | // TODO: Use `Stream.pipeline` when targeting Node.js 12. 11 | .on('error', reject) 12 | .pipe(hasher) 13 | .on('error', reject) 14 | .on('finish', () => { 15 | const {buffer} = new Uint8Array(hasher.read()); 16 | resolve({value: buffer, transferList: [buffer]}); 17 | }); 18 | }), 19 | hash: async (algorithm, input) => { 20 | const hasher = crypto.createHash(algorithm); 21 | 22 | if (Array.isArray(input)) { 23 | for (const part of input) { 24 | hasher.update(part); 25 | } 26 | } else { 27 | hasher.update(input); 28 | } 29 | 30 | const {buffer} = new Uint8Array(hasher.digest()); 31 | return {value: buffer, transferList: [buffer]}; 32 | } 33 | }; 34 | 35 | parentPort.on('message', async message => { 36 | try { 37 | const {method, args} = message; 38 | const handler = handlers[method]; 39 | 40 | if (handler === undefined) { 41 | throw new Error(`Unknown method '${method}'`); 42 | } 43 | 44 | const {value, transferList} = await handler(...args); 45 | parentPort.postMessage({id: message.id, value}, transferList); 46 | } catch (error) { 47 | const newError = {message: error.message, stack: error.stack}; 48 | 49 | for (const [key, value] of Object.entries(error)) { 50 | if (typeof value !== 'object') { 51 | newError[key] = value; 52 | } 53 | } 54 | 55 | parentPort.postMessage({id: message.id, error: newError}); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Netlify Actions' 2 | description: 'Netlify deploy from GitHub Actions' 3 | author: 'Ryo Ota' 4 | inputs: 5 | publish-dir: 6 | description: Publish directory 7 | required: true 8 | functions-dir: 9 | description: Functions directory 10 | required: false 11 | deploy-message: 12 | description: Custom deploy message for Netlify 13 | required: false 14 | github-token: 15 | description: GitHub token 16 | required: false 17 | production-branch: 18 | description: Production branch 19 | required: false 20 | production-deploy: 21 | description: Indicate whether to deploy production build 22 | required: false 23 | enable-pull-request-comment: 24 | description: Enable pull request comment 25 | required: false 26 | enable-commit-comment: 27 | description: Enable commit comment 28 | required: false 29 | enable-commit-status: 30 | description: Enable GitHub commit status 31 | required: false 32 | overwrites-pull-request-comment: 33 | description: Overwrites pull request comment 34 | required: false 35 | netlify-config-path: 36 | description: Path to netlify.toml 37 | required: false 38 | alias: 39 | description: Specifies the prefix for the deployment URL 40 | required: false 41 | enable-github-deployment: 42 | description: Whether or not to deploy to GitHub 43 | required: false 44 | github-deployment-environment: 45 | description: Environment name of GitHub Deployments 46 | required: false 47 | github-deployment-description: 48 | description: Description of the GitHub Deployment 49 | required: false 50 | fails-without-credentials: 51 | description: Fails if no credentials provided 52 | required: false 53 | outputs: 54 | deploy-url: 55 | description: Deploy URL 56 | runs: 57 | using: 'node20' 58 | main: 'dist/index.js' 59 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@typescript-eslint"], 3 | "extends": [], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "eslint-comments/no-use": "off", 12 | "import/no-namespace": "off", 13 | "no-unused-vars": "off", 14 | "@typescript-eslint/no-unused-vars": "error", 15 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], 16 | "@typescript-eslint/no-require-imports": "error", 17 | "@typescript-eslint/array-type": "error", 18 | "@typescript-eslint/await-thenable": "error", 19 | "camelcase": "off", 20 | "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], 21 | "@typescript-eslint/func-call-spacing": ["error", "never"], 22 | "@typescript-eslint/no-array-constructor": "error", 23 | "@typescript-eslint/no-empty-interface": "error", 24 | "@typescript-eslint/no-explicit-any": "error", 25 | "@typescript-eslint/no-extraneous-class": "error", 26 | "@typescript-eslint/no-for-in-array": "error", 27 | "@typescript-eslint/no-inferrable-types": "error", 28 | "@typescript-eslint/no-misused-new": "error", 29 | "@typescript-eslint/no-namespace": "error", 30 | "@typescript-eslint/no-non-null-assertion": "warn", 31 | "@typescript-eslint/no-unnecessary-qualifier": "error", 32 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 33 | "@typescript-eslint/no-useless-constructor": "error", 34 | "@typescript-eslint/no-var-requires": "error", 35 | "@typescript-eslint/prefer-for-of": "warn", 36 | "@typescript-eslint/prefer-function-type": "warn", 37 | "@typescript-eslint/prefer-includes": "error", 38 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 39 | "@typescript-eslint/promise-function-async": "error", 40 | "@typescript-eslint/require-array-sort-compare": "error", 41 | "@typescript-eslint/restrict-plus-operands": "error", 42 | "semi": "off", 43 | "@typescript-eslint/semi": ["error", "never"], 44 | "@typescript-eslint/type-annotation-spacing": "error", 45 | "@typescript-eslint/unbound-method": "error" 46 | }, 47 | "env": { 48 | "node": true, 49 | "es6": true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # TODO: Remove because /dist/typescript should not be generated automatically 2 | /dist/typescript 3 | /dist/typescript1 4 | /dist/typescript2 5 | /dist/typescript3 6 | /dist/typescript4 7 | /dist/typescript5 8 | /dist/typescript6 9 | /dist/typescript7 10 | /dist/typescript8 11 | /dist/typescript9 12 | /dist/esbuild 13 | 14 | # Dependency directory 15 | node_modules 16 | 17 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 18 | # Logs 19 | logs 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | lerna-debug.log* 25 | 26 | # Diagnostic reports (https://nodejs.org/api/report.html) 27 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 28 | 29 | # Runtime data 30 | pids 31 | *.pid 32 | *.seed 33 | *.pid.lock 34 | 35 | # Directory for instrumented libs generated by jscoverage/JSCover 36 | lib-cov 37 | 38 | # Coverage directory used by tools like istanbul 39 | coverage 40 | *.lcov 41 | 42 | # nyc test coverage 43 | .nyc_output 44 | 45 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 46 | .grunt 47 | 48 | # Bower dependency directory (https://bower.io/) 49 | bower_components 50 | 51 | # node-waf configuration 52 | .lock-wscript 53 | 54 | # Compiled binary addons (https://nodejs.org/api/addons.html) 55 | build/Release 56 | 57 | # Dependency directories 58 | jspm_packages/ 59 | 60 | # TypeScript v1 declaration files 61 | typings/ 62 | 63 | # TypeScript cache 64 | *.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | .npm 68 | 69 | # Optional eslint cache 70 | .eslintcache 71 | 72 | # Optional REPL history 73 | .node_repl_history 74 | 75 | # Output of 'npm pack' 76 | *.tgz 77 | 78 | # Yarn Integrity file 79 | .yarn-integrity 80 | 81 | # dotenv environment variables file 82 | .env 83 | .env.test 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | 88 | # next.js build output 89 | .next 90 | 91 | # nuxt.js build output 92 | .nuxt 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # OS metadata 107 | .DS_Store 108 | Thumbs.db 109 | 110 | # Ignore built ts files 111 | __tests__/runner/* 112 | lib/**/* 113 | 114 | # Intellij 115 | 116 | .idea/ -------------------------------------------------------------------------------- /src/inputs.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | 3 | // Why use function rather than raw string? => Inputs should be lazy evaluated. 4 | export interface Inputs { 5 | publishDir(): string 6 | functionsDir(): string | undefined 7 | deployMessage(): string | undefined 8 | productionBranch(): string | undefined 9 | productionDeploy(): boolean 10 | enablePullRequestComment(): boolean 11 | enableCommitComment(): boolean 12 | enableCommitStatus(): boolean 13 | githubToken(): string 14 | overwritesPullRequestComment(): boolean 15 | netlifyConfigPath(): string | undefined 16 | alias(): string | undefined 17 | enableGithubDeployment(): boolean 18 | githubDeploymentEnvironment(): string | undefined 19 | githubDeploymentDescription(): string | undefined 20 | failsWithoutCredentials(): boolean 21 | } 22 | 23 | export const defaultInputs: Inputs = { 24 | publishDir() { 25 | return core.getInput('publish-dir', {required: true}) 26 | }, 27 | functionsDir() { 28 | return core.getInput('functions-dir') || undefined 29 | }, 30 | deployMessage() { 31 | return core.getInput('deploy-message') || undefined 32 | }, 33 | productionBranch() { 34 | return core.getInput('production-branch') || undefined 35 | }, 36 | productionDeploy(): boolean { 37 | // Default: false 38 | return core.getInput('production-deploy') === 'true' 39 | }, 40 | enablePullRequestComment() { 41 | // Default: true 42 | return (core.getInput('enable-pull-request-comment') || 'true') === 'true' 43 | }, 44 | enableCommitComment() { 45 | // Default: true 46 | return (core.getInput('enable-commit-comment') || 'true') === 'true' 47 | }, 48 | enableCommitStatus() { 49 | // Default: true 50 | return (core.getInput('enable-commit-status') || 'true') === 'true' 51 | }, 52 | githubToken() { 53 | return core.getInput('github-token') 54 | }, 55 | overwritesPullRequestComment() { 56 | // Default: true 57 | return ( 58 | (core.getInput('overwrites-pull-request-comment') || 'true') === 'true' 59 | ) 60 | }, 61 | netlifyConfigPath() { 62 | return core.getInput('netlify-config-path') || undefined 63 | }, 64 | alias() { 65 | return core.getInput('alias') || undefined 66 | }, 67 | enableGithubDeployment() { 68 | // Default: true 69 | return (core.getInput('enable-github-deployment') || 'true') === 'true' 70 | }, 71 | githubDeploymentEnvironment(): string | undefined { 72 | return core.getInput('github-deployment-environment') || undefined 73 | }, 74 | githubDeploymentDescription(): string | undefined { 75 | return core.getInput('github-deployment-description') || undefined 76 | }, 77 | failsWithoutCredentials(): boolean { 78 | // Default: false 79 | return core.getInput('fails-without-credentials') === 'true' 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /__tests__/production-deploy.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import {mocked} from 'jest-mock' 3 | import {defaultInputs} from '../src/inputs' 4 | import {context} from '@actions/github' 5 | import {run} from '../src/main' 6 | 7 | const mockDeploy = jest.fn() 8 | 9 | jest.mock('netlify', () => { 10 | return jest.fn().mockImplementation(() => { 11 | return {deploy: mockDeploy} 12 | }) 13 | }) 14 | jest.mock('../src/inputs') 15 | jest.mock('@actions/github') 16 | 17 | mockDeploy.mockResolvedValue({deploy: {}}) 18 | mocked(defaultInputs.githubToken).mockReturnValue('') // NOTE: empty string means the input is not specified 19 | mocked(defaultInputs.publishDir).mockReturnValue('my-publish-dir') 20 | 21 | process.env.NETLIFY_AUTH_TOKEN = 'dummy-netlify-auth-token' 22 | process.env.NETLIFY_SITE_ID = 'dummy-netlify-site-id' 23 | 24 | describe('Draft deploy', () => { 25 | const expectedSiteId = 'dummy-netlify-site-id' 26 | const expectedDeployFolder = path.resolve(process.cwd(), 'my-publish-dir') 27 | 28 | test('deploy should have draft true when production-deploy input is false', async () => { 29 | mocked(defaultInputs.productionDeploy).mockReturnValue(false) 30 | 31 | await run(defaultInputs) 32 | 33 | expect(mockDeploy).toHaveBeenCalledWith( 34 | expectedSiteId, 35 | expectedDeployFolder, 36 | { 37 | draft: true 38 | } 39 | ) 40 | }) 41 | 42 | test('deploy should have draft false when production-deploy input is true', async () => { 43 | mocked(defaultInputs.productionDeploy).mockReturnValue(true) 44 | 45 | await run(defaultInputs) 46 | 47 | expect(mockDeploy).toHaveBeenCalledWith( 48 | expectedSiteId, 49 | expectedDeployFolder, 50 | { 51 | draft: false 52 | } 53 | ) 54 | }) 55 | 56 | test('deploy should have draft false when production-branch matches context ref', async () => { 57 | mocked(defaultInputs.productionDeploy).mockReturnValue(false) 58 | mocked(defaultInputs.productionBranch).mockReturnValue('master') 59 | 60 | context.ref = 'refs/heads/master' 61 | 62 | await run(defaultInputs) 63 | 64 | expect(mockDeploy).toHaveBeenCalledWith( 65 | expectedSiteId, 66 | expectedDeployFolder, 67 | { 68 | draft: false 69 | } 70 | ) 71 | }) 72 | 73 | test('deploy should have draft true when production-branch does not match context ref', async () => { 74 | mocked(defaultInputs.productionDeploy).mockReturnValue(false) 75 | mocked(defaultInputs.productionBranch).mockReturnValue('master') 76 | 77 | context.ref = 'refs/heads/not-master' 78 | 79 | await run(defaultInputs) 80 | 81 | expect(mockDeploy).toHaveBeenCalledWith( 82 | expectedSiteId, 83 | expectedDeployFolder, 84 | { 85 | draft: true 86 | } 87 | ) 88 | }) 89 | 90 | test('deploy should have draft true when production-branch is not defined', async () => { 91 | mocked(defaultInputs.productionDeploy).mockReturnValue(false) 92 | mocked(defaultInputs.productionBranch).mockReturnValue(undefined) 93 | 94 | context.ref = 'refs/heads/master' 95 | 96 | await run(defaultInputs) 97 | 98 | expect(mockDeploy).toHaveBeenCalledWith( 99 | expectedSiteId, 100 | expectedDeployFolder, 101 | { 102 | draft: true 103 | } 104 | ) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # actions-netlify 2 | ![build-test](https://github.com/nwtgck/actions-netlify/workflows/build-test/badge.svg) 3 | 4 | GitHub Actions for deploying to Netlify 5 | 6 | 7 | 8 | Deploy URLs are commented on your pull requests and commit comments! 9 | 10 | 11 | 12 | GitHub Deployments are also supported! 13 | 14 | ## Usage 15 | 16 | ```yaml 17 | # .github/workflows/netlify.yml 18 | name: Build and Deploy to Netlify 19 | on: 20 | push: 21 | pull_request: 22 | jobs: 23 | build: 24 | runs-on: ubuntu-22.04 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | # ( Build to ./dist or other directory... ) 29 | 30 | - name: Deploy to Netlify 31 | uses: nwtgck/actions-netlify@v3.0 32 | with: 33 | publish-dir: './dist' 34 | production-branch: master 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | deploy-message: "Deploy from GitHub Actions" 37 | enable-pull-request-comment: false 38 | enable-commit-comment: true 39 | overwrites-pull-request-comment: true 40 | env: 41 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 42 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 43 | timeout-minutes: 1 44 | ``` 45 | 46 | 47 | ### Required inputs and env 48 | - `publish-dir` (e.g. "dist", "_site") 49 | - `NETLIFY_AUTH_TOKEN`: [Personal access tokens](https://app.netlify.com/user/applications#personal-access-tokens) > New access token 50 | - `NETLIFY_SITE_ID`: team page > your site > Settings > Site details > Site information > API ID 51 | - NOTE: API ID is `NETLIFY_SITE_ID`. 52 | 53 | ### Optional inputs 54 | - `production-branch` (e.g. "master") 55 | - `production-deploy`: Deploy as Netlify production deploy (default: false) 56 | - `github-token: ${{ secrets.GITHUB_TOKEN }}` 57 | - `deploy-message` A custom deploy message to see on Netlify deployment (e.g. `${{ github.event.pull_request.title }}`) 58 | - `enable-pull-request-comment: true` Comment on pull request (default: true) 59 | - `enable-commit-comment: true` Comment on GitHub commit (default: true) 60 | - `enable-commit-status: true` GitHub commit status (default: true) 61 | - `overwrites-pull-request-comment: true` Overwrites comment on pull request (default: true) 62 | - `netlify-config-path: ./netlify.toml` Path to `netlify.toml` (default: undefined) 63 | - `functions-dir` Netlify functions output directory (default: undefined) 64 | - `alias` Specifies the prefix for the deployment URL, must not have uppercase or special characters (default: Netlify build ID) 65 | - `alias: ${{ github.head_ref }}` replicates the [branch deploy prefix](https://docs.netlify.com/site-deploys/overview/#definitions) 66 | - `alias: deploy-preview-${{ github.event.number }}` replicates the [deploy preview prefix](https://docs.netlify.com/site-deploys/overview/#definitions) 67 | - `enable-github-deployment` Whether or not to deploy to GitHub (default: true) 68 | - `github-deployment-environment` Environment name of GitHub Deployments 69 | - `github-deployment-description` Description of the GitHub Deployment 70 | - `fails-without-credentials` Fails if no credentials provided (default: false) 71 | 72 | ### Paths are relative to the project's root 73 | All paths (eg, `publish-dir`, `netlify-config-path`, `functions-dir`) are relative to the project's root or absolute paths. 74 | 75 | ### Outputs 76 | - `deploy-url` A deployment URL generated by Netlify 77 | 78 | ## Build on local 79 | 80 | ```bash 81 | npm ci 82 | npm run all 83 | ``` 84 | -------------------------------------------------------------------------------- /__tests__/main.test.ts: -------------------------------------------------------------------------------- 1 | import {wait} from '../src/wait' 2 | import * as process from 'process' 3 | import * as cp from 'child_process' 4 | import * as path from 'path' 5 | import {defaultInputs} from '../src/inputs' 6 | 7 | /** 8 | * With input 9 | * @param inputName 10 | * @param value 11 | * @param f 12 | */ 13 | export async function withInput( 14 | inputName: string, 15 | value: string, 16 | f: () => void | Promise 17 | ): Promise { 18 | // (from: https://github.com/actions/toolkit/blob/83dd3ef0f1e5bc93c5ab7072e1edf1715a01ba9d/packages/core/src/core.ts#L71) 19 | const envName = `INPUT_${inputName.replace(/ /g, '_').toUpperCase()}` 20 | process.env[envName] = value 21 | await f() 22 | // NOTE: Not sure this is correct deletion 23 | delete process.env[envName] 24 | } 25 | 26 | describe('defaultInputs', () => { 27 | test('publishDir', () => { 28 | withInput('publish-dir', './my_publish_dir', () => { 29 | const publishDir: string = defaultInputs.publishDir() 30 | expect(publishDir).toBe('./my_publish_dir') 31 | }) 32 | }) 33 | 34 | describe('functionsDir', () => { 35 | test('it should be a string when specified', () => { 36 | withInput('functions-dir', './my_functions_dir', () => { 37 | const functionsDir: string | undefined = defaultInputs.functionsDir() 38 | expect(functionsDir).toBe('./my_functions_dir') 39 | }) 40 | }) 41 | 42 | test('it should be undefined when not specified', () => { 43 | const functionsDir: string | undefined = defaultInputs.functionsDir() 44 | expect(functionsDir).toBe(undefined) 45 | }) 46 | }) 47 | 48 | describe('deployMessage', () => { 49 | test('it should be a string when specified', () => { 50 | withInput('deploy-message', 'Deploy with GitHub Actions', () => { 51 | const deployMessage: string | undefined = defaultInputs.deployMessage() 52 | expect(deployMessage).toBe('Deploy with GitHub Actions') 53 | }) 54 | }) 55 | 56 | test('it should be undefined when not specified', () => { 57 | const deployMessage: string | undefined = defaultInputs.deployMessage() 58 | expect(deployMessage).toBe(undefined) 59 | }) 60 | }) 61 | 62 | describe('productionBranch', () => { 63 | test('it should be a string when specified', () => { 64 | withInput('production-branch', 'master', () => { 65 | const productionBranch: string | undefined = 66 | defaultInputs.productionBranch() 67 | expect(productionBranch).toBe('master') 68 | }) 69 | }) 70 | 71 | test('it should be undefined when not specified', () => { 72 | const deployMessage: string | undefined = defaultInputs.productionBranch() 73 | expect(deployMessage).toBe(undefined) 74 | }) 75 | }) 76 | 77 | describe('enablePullRequestComment', () => { 78 | test('it should be default value (true) when not specified', () => { 79 | const b: boolean = defaultInputs.enablePullRequestComment() 80 | expect(b).toBe(true) 81 | }) 82 | 83 | test('it should be true when "true" specified', () => { 84 | withInput('enable-pull-request-comment', 'true', () => { 85 | const b: boolean = defaultInputs.enablePullRequestComment() 86 | expect(b).toBe(true) 87 | }) 88 | }) 89 | 90 | test('it should be true when "false" specified', () => { 91 | withInput('enable-pull-request-comment', 'false', () => { 92 | const b: boolean = defaultInputs.enablePullRequestComment() 93 | expect(b).toBe(false) 94 | }) 95 | }) 96 | }) 97 | 98 | describe('enableCommitComment', () => { 99 | test('it should be default value (true) when not specified', () => { 100 | const b: boolean = defaultInputs.enableCommitComment() 101 | expect(b).toBe(true) 102 | }) 103 | 104 | test('it should be true when "true" specified', () => { 105 | withInput('enable-commit-comment', 'true', () => { 106 | const b: boolean = defaultInputs.enableCommitComment() 107 | expect(b).toBe(true) 108 | }) 109 | }) 110 | 111 | test('it should be true when "false" specified', () => { 112 | withInput('enable-commit-comment', 'false', () => { 113 | const b: boolean = defaultInputs.enableCommitComment() 114 | expect(b).toBe(false) 115 | }) 116 | }) 117 | }) 118 | 119 | describe('enableCommitComment', () => { 120 | test('it should be empty string when not specified', () => { 121 | const t: string = defaultInputs.githubToken() 122 | expect(t).toBe('') 123 | }) 124 | 125 | test('it should be a string when specified', () => { 126 | withInput('github-token', 'DUMMY_GITHUB_TOKEN', () => { 127 | const t: string = defaultInputs.githubToken() 128 | expect(t).toBe('DUMMY_GITHUB_TOKEN') 129 | }) 130 | }) 131 | }) 132 | 133 | describe('overwritesPullRequestComment', () => { 134 | test('it should be default value (true) when not specified', () => { 135 | const b: boolean = defaultInputs.overwritesPullRequestComment() 136 | expect(b).toBe(true) 137 | }) 138 | 139 | test('it should be true when "true" specified', () => { 140 | withInput('overwrites-pull-request-comment', 'true', () => { 141 | const b: boolean = defaultInputs.overwritesPullRequestComment() 142 | expect(b).toBe(true) 143 | }) 144 | }) 145 | 146 | test('it should be false when "false" specified', () => { 147 | withInput('overwrites-pull-request-comment', 'false', () => { 148 | const b: boolean = defaultInputs.overwritesPullRequestComment() 149 | expect(b).toBe(false) 150 | }) 151 | }) 152 | }) 153 | 154 | describe('alias', () => { 155 | test('it should be a string when specified', () => { 156 | withInput('alias', 'foo', () => { 157 | const alias: string | undefined = defaultInputs.alias() 158 | expect(alias).toBe('foo') 159 | }) 160 | }) 161 | }) 162 | 163 | describe('production deploy', () => { 164 | test('it should be default value (false) when not specified', () => { 165 | const b: boolean = defaultInputs.productionDeploy() 166 | expect(b).toBe(false) 167 | }) 168 | 169 | test('it should be true when "true" specified', () => { 170 | withInput('production-deploy', 'true', () => { 171 | const b: boolean = defaultInputs.productionDeploy() 172 | expect(b).toBe(true) 173 | }) 174 | }) 175 | 176 | test('it should be false when "false" specified', () => { 177 | withInput('production-deploy', 'false', () => { 178 | const b: boolean = defaultInputs.productionDeploy() 179 | expect(b).toBe(false) 180 | }) 181 | }) 182 | }) 183 | 184 | describe('enableGithubDeployment', () => { 185 | test('it should be default value (true) when not specified', () => { 186 | const b: boolean = defaultInputs.enableGithubDeployment() 187 | expect(b).toBe(true) 188 | }) 189 | 190 | test('it should be true when "true" specified', () => { 191 | withInput('enable-github-deployment', 'true', () => { 192 | const b: boolean = defaultInputs.enableGithubDeployment() 193 | expect(b).toBe(true) 194 | }) 195 | }) 196 | 197 | test('it should be true when "false" specified', () => { 198 | withInput('enable-github-deployment', 'false', () => { 199 | const b: boolean = defaultInputs.enableGithubDeployment() 200 | expect(b).toBe(false) 201 | }) 202 | }) 203 | }) 204 | }) 205 | 206 | // Old tests below 207 | 208 | test('throws invalid number', async () => { 209 | const input = parseInt('foo', 10) 210 | await expect(wait(input)).rejects.toThrow('milliseconds not a number') 211 | }) 212 | 213 | test('wait 500 ms', async () => { 214 | const start = new Date() 215 | await wait(500) 216 | const end = new Date() 217 | var delta = Math.abs(end.getTime() - start.getTime()) 218 | expect(delta).toBeGreaterThan(450) 219 | }) 220 | 221 | // shows how the runner will run a javascript action with env / stdout protocol 222 | test('test runs', () => { 223 | if (false) { 224 | process.env['INPUT_MILLISECONDS'] = '500' 225 | const ip = path.join(__dirname, '..', 'lib', 'main.js') 226 | const options: cp.ExecSyncOptions = { 227 | env: process.env 228 | } 229 | console.log(cp.execSync(`node ${ip}`, options).toString()) 230 | } 231 | }) 232 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 5 | 6 | ## [Unreleased] 7 | 8 | ## [3.0.0] - 2024-03-10 9 | ### Changed 10 | * Update dependencies 11 | * Updates the default runtime to node20 12 | 13 | ## [2.1.0] - 2023-08-18 14 | ### Changed 15 | * Update dependencies 16 | 17 | ### Added 18 | * Add "enable-github-deployment" input [#901](https://github.com/nwtgck/actions-netlify/pull/901) by [@a-tokyo](https://github.com/a-tokyo) 19 | 20 | ## [2.0.0] - 2022-12-08 21 | ### Changed 22 | * Update dependencies 23 | * Updates the default runtime to node16 24 | 25 | ## [1.2.4] - 2022-10-14 26 | ### Changed 27 | * Update dependencies 28 | 29 | ## [1.2.3] - 2021-12-20 30 | ### Changed 31 | * Update dependencies 32 | 33 | ## [1.2.2] - 2021-05-08 34 | ### Fixed 35 | * Fix GitHub deployment description 36 | 37 | ### Changed 38 | * Update dependencies 39 | 40 | ## [1.2.1] - 2021-05-05 41 | ### Added 42 | * Add "fails-without-credentials" input to fail if the credentials not provided [#532](https://github.com/nwtgck/actions-netlify/pull/532) 43 | 44 | ### Changed 45 | * Update dependencies 46 | 47 | ## [1.2.0] - 2021-04-29 48 | ### Changed 49 | * Update dependencies 50 | * (breaking change for `overwrites-pull-request-comment: true`): Support multiple app deploys in a single PR [#484](https://github.com/nwtgck/actions-netlify/pull/484) by [@kaisermann](https://github.com/kaisermann) 51 | 52 | ### Added 53 | * Add "github-deployment-description" input [#513](https://github.com/nwtgck/actions-netlify/pull/513) by [@scopsy](https://github.com/scopsy) 54 | 55 | ### Fixed 56 | * Make deployment link on pull request if CI is running on pull request [#516](https://github.com/nwtgck/actions-netlify/pull/516) by [@nojvek](https://github.com/nojvek) 57 | 58 | ## [1.1.13] - 2021-01-28 59 | ### Changed 60 | * Update dependencies 61 | 62 | ## [1.1.12] - 2021-01-16 63 | ### Changed 64 | * Update dependencies 65 | 66 | ## [1.1.11] - 2020-09-22 67 | ### Changed 68 | * Update dependencies 69 | 70 | ## [1.1.10] - 2020-08-22 71 | ### Fixed 72 | * Support Github commit status triggered by "pull_request" event 73 | 74 | ## [1.1.9] - 2020-08-21 75 | ### Added 76 | * Add "enable-commit-status" input 77 | 78 | ## [1.1.8] - 2020-08-20 79 | ### Added 80 | * Add GitHub commit status 81 | 82 | ## [1.1.7] - 2020-08-20 83 | ### Added 84 | * Add "'github-deployment-environment" input to specify environment name of GitHub Deployments 85 | 86 | ## [1.1.6] - 2020-08-17 87 | ### Changed 88 | * Update dependencies 89 | 90 | ## Fixed 91 | * Ignore alias deployment when production deployment 92 | 93 | ## [1.1.5] - 2020-06-27 94 | ### Changed 95 | * Update dependencies 96 | 97 | ## [1.1.4] - 2020-06-18 98 | ### Added 99 | * Add "functions-dir" input to deploy Netlify Functions [#191](https://github.com/nwtgck/actions-netlify/pull/191) by [@fmr](https://github.com/fmr) 100 | 101 | ## [1.1.3] - 2020-06-13 102 | ### Added 103 | * Add "production-deploy" input to deploy as Netlify production deploy [#188](https://github.com/nwtgck/actions-netlify/pull/188) by [@gvdp](https://github.com/gvdp) 104 | 105 | ## [1.1.2] - 2020-06-13 106 | ### Added 107 | * Add "alias" input to deploy with alias [#178](https://github.com/nwtgck/actions-netlify/pull/178) by [@rajington](https://github.com/rajington) 108 | 109 | ## [1.1.1] - 2020-05-30 110 | ### Added 111 | * Add "netlify-config-path" input to specify path to `netlify.toml` 112 | * Support GitHub Deployments 113 | 114 | ### Changed 115 | * Update dependencies 116 | 117 | ## [1.1.0] - 2020-05-10 118 | ### Added 119 | * Add "overwrites-pull-request-comment" input 120 | 121 | ### Changed 122 | * Overwrite comment on pull request by default 123 | - You can use `overwrites-pull-request-comment: false` not to overwrite 124 | * Update dependencies 125 | 126 | ## [1.0.13] - 2020-05-09 127 | ### Changed 128 | * Update dependencies 129 | 130 | ### Added 131 | * Add "enable-pull-request-comment" input 132 | * Add "enable-commit-comment" input 133 | 134 | ## [1.0.12] - 2020-04-07 135 | ### Changed 136 | * Update dependencies 137 | 138 | ## [1.0.11] - 2020-04-06 139 | ### Changed 140 | * Update dependencies 141 | 142 | ## [1.0.10] - 2020-04-03 143 | ### Changed 144 | * Update dependencies 145 | 146 | ## [1.0.9] - 2020-04-01 147 | ### Changed 148 | * Update dependencies 149 | 150 | ## [1.0.8] - 2020-03-30 151 | ### Changed 152 | * Update dependencies 153 | 154 | ## [1.0.7] - 2020-03-29 155 | ### Changed 156 | * Update dependencies 157 | 158 | ## [1.0.6] - 2020-03-17 159 | ### Changed 160 | * Update dependencies 161 | 162 | ### Added 163 | * Add `deploy-url` output [#48](https://github.com/nwtgck/actions-netlify/pull/48) by [@kentaro-m](https://github.com/kentaro-m) 164 | 165 | ## [1.0.5] - 2020-03-03 166 | ### Added 167 | * Add `deploy-message` input [#40](https://github.com/nwtgck/actions-netlify/pull/40) by [@South-Paw](https://github.com/South-Paw) 168 | 169 | ## [1.0.4] - 2020-03-02 170 | ### Changed 171 | * Update dependencies 172 | 173 | ## [1.0.3] - 2020-02-28 174 | ### Changed 175 | * Do not error out when no credentials are provided [#33](https://github.com/nwtgck/actions-netlify/pull/33) by [@tiangolo](https://github.com/tiangolo) 176 | * Comment Netlify deploy URL on commit 177 | 178 | ## [1.0.2] - 2020-02-26 179 | ### Changed 180 | * Update dependencies 181 | 182 | ## [1.0.1] - 2020-02-22 183 | ### Changed 184 | * Update dependencies 185 | * Improve files by dependency updates 186 | 187 | ## [1.0.0] - 2020-02-08 188 | ### Changed 189 | * Update dependencies 190 | 191 | ## [0.2.0] - 2020-02-05 192 | ### Added 193 | * Add `production-branch` input 194 | 195 | ### Changed 196 | * Make `github-token` input optional 197 | 198 | ## [0.1.1] - 2020-02-04 199 | ### Changed 200 | * Print deploy URL 201 | 202 | ## 0.1.0 - 2020-02-04 203 | ### Added 204 | * Deploy to Netlify 205 | * Comment on GitHub PR 206 | 207 | [Unreleased]: https://github.com/nwtgck/actions-netlify/compare/v3.0.0...HEAD 208 | [3.0.0]: https://github.com/nwtgck/actions-netlify/compare/v2.1.0...v3.0.0 209 | [2.1.0]: https://github.com/nwtgck/actions-netlify/compare/v2.0.0...v2.1.0 210 | [2.0.0]: https://github.com/nwtgck/actions-netlify/compare/v1.2.4...v2.0.0 211 | [1.2.4]: https://github.com/nwtgck/actions-netlify/compare/v1.2.3...v1.2.4 212 | [1.2.3]: https://github.com/nwtgck/actions-netlify/compare/v1.2.2...v1.2.3 213 | [1.2.2]: https://github.com/nwtgck/actions-netlify/compare/v1.2.1...v1.2.2 214 | [1.2.1]: https://github.com/nwtgck/actions-netlify/compare/v1.2.0...v1.2.1 215 | [1.2.0]: https://github.com/nwtgck/actions-netlify/compare/v1.1.13...v1.2.0 216 | [1.1.13]: https://github.com/nwtgck/actions-netlify/compare/v1.1.12...v1.1.13 217 | [1.1.12]: https://github.com/nwtgck/actions-netlify/compare/v1.1.11...v1.1.12 218 | [1.1.11]: https://github.com/nwtgck/actions-netlify/compare/v1.1.10...v1.1.11 219 | [1.1.10]: https://github.com/nwtgck/actions-netlify/compare/v1.1.9...v1.1.10 220 | [1.1.9]: https://github.com/nwtgck/actions-netlify/compare/v1.1.8...v1.1.9 221 | [1.1.8]: https://github.com/nwtgck/actions-netlify/compare/v1.1.7...v1.1.8 222 | [1.1.7]: https://github.com/nwtgck/actions-netlify/compare/v1.1.6...v1.1.7 223 | [1.1.6]: https://github.com/nwtgck/actions-netlify/compare/v1.1.5...v1.1.6 224 | [1.1.5]: https://github.com/nwtgck/actions-netlify/compare/v1.1.4...v1.1.5 225 | [1.1.4]: https://github.com/nwtgck/actions-netlify/compare/v1.1.3...v1.1.4 226 | [1.1.3]: https://github.com/nwtgck/actions-netlify/compare/v1.1.2...v1.1.3 227 | [1.1.2]: https://github.com/nwtgck/actions-netlify/compare/v1.1.1...v1.1.2 228 | [1.1.1]: https://github.com/nwtgck/actions-netlify/compare/v1.1.0...v1.1.1 229 | [1.1.0]: https://github.com/nwtgck/actions-netlify/compare/v1.0.13...v1.1.0 230 | [1.0.13]: https://github.com/nwtgck/actions-netlify/compare/v1.0.12...v1.0.13 231 | [1.0.12]: https://github.com/nwtgck/actions-netlify/compare/v1.0.11...v1.0.12 232 | [1.0.11]: https://github.com/nwtgck/actions-netlify/compare/v1.0.10...v1.0.11 233 | [1.0.10]: https://github.com/nwtgck/actions-netlify/compare/v1.0.9...v1.0.10 234 | [1.0.9]: https://github.com/nwtgck/actions-netlify/compare/v1.0.8...v1.0.9 235 | [1.0.8]: https://github.com/nwtgck/actions-netlify/compare/v1.0.7...v1.0.8 236 | [1.0.7]: https://github.com/nwtgck/actions-netlify/compare/v1.0.6...v1.0.7 237 | [1.0.6]: https://github.com/nwtgck/actions-netlify/compare/v1.0.5...v1.0.6 238 | [1.0.5]: https://github.com/nwtgck/actions-netlify/compare/v1.0.4...v1.0.5 239 | [1.0.4]: https://github.com/nwtgck/actions-netlify/compare/v1.0.3...v1.0.4 240 | [1.0.3]: https://github.com/nwtgck/actions-netlify/compare/v1.0.2...v1.0.3 241 | [1.0.2]: https://github.com/nwtgck/actions-netlify/compare/v1.0.1...v1.0.2 242 | [1.0.1]: https://github.com/nwtgck/actions-netlify/compare/v1.0.0...v1.0.1 243 | [1.0.0]: https://github.com/nwtgck/actions-netlify/compare/v0.2.0...v1.0.0 244 | [0.2.0]: https://github.com/nwtgck/actions-netlify/compare/v0.1.1...v0.2.0 245 | [0.1.1]: https://github.com/nwtgck/actions-netlify/compare/v0.1.0...v0.1.1 246 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {context, getOctokit} from '@actions/github' 3 | import type {GitHub} from '@actions/github/lib/utils' 4 | import NetlifyAPI from 'netlify' 5 | import * as path from 'path' 6 | import {defaultInputs, Inputs} from './inputs' 7 | import * as crypto from 'crypto' 8 | 9 | function getCommentIdentifier(siteId: string): string { 10 | const sha256SiteId: string = crypto 11 | .createHash('sha256') 12 | .update(siteId) 13 | .digest('hex') 14 | return `` 15 | } 16 | 17 | async function findIssueComment( 18 | githubClient: InstanceType, 19 | siteId: string 20 | ): Promise { 21 | const listCommentsRes = await githubClient.rest.issues.listComments({ 22 | owner: context.issue.owner, 23 | repo: context.issue.repo, 24 | issue_number: context.issue.number 25 | }) 26 | 27 | const comments = listCommentsRes.data 28 | const commentIdentifier = getCommentIdentifier(siteId) 29 | 30 | for (const comment of comments) { 31 | // If comment contains the comment identifier 32 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 33 | if (comment.body!.includes(commentIdentifier)) { 34 | return comment.id 35 | } 36 | } 37 | return undefined 38 | } 39 | 40 | async function createGitHubDeployment( 41 | githubClient: InstanceType, 42 | environmentUrl: string, 43 | environment: string, 44 | description: string | undefined 45 | ): Promise { 46 | const deployRef = context.payload.pull_request?.head.sha ?? context.sha 47 | const deployment = await githubClient.rest.repos.createDeployment({ 48 | auto_merge: false, 49 | owner: context.repo.owner, 50 | repo: context.repo.repo, 51 | ref: deployRef, 52 | environment, 53 | description, 54 | required_contexts: [] 55 | }) 56 | await githubClient.rest.repos.createDeploymentStatus({ 57 | state: 'success', 58 | environment_url: environmentUrl, 59 | owner: context.repo.owner, 60 | repo: context.repo.repo, 61 | deployment_id: (deployment.data as {id: number}).id 62 | }) 63 | } 64 | 65 | export async function run(inputs: Inputs): Promise { 66 | try { 67 | const netlifyAuthToken = process.env.NETLIFY_AUTH_TOKEN 68 | const siteId = process.env.NETLIFY_SITE_ID 69 | // NOTE: Non-collaborators PRs don't pass GitHub secrets to GitHub Actions. 70 | if (!(netlifyAuthToken && siteId)) { 71 | const errorMessage = 'Netlify credentials not provided, not deployable' 72 | if (inputs.failsWithoutCredentials()) { 73 | throw new Error(errorMessage) 74 | } 75 | process.stderr.write(errorMessage) 76 | return 77 | } 78 | const dir = inputs.publishDir() 79 | const functionsDir: string | undefined = inputs.functionsDir() 80 | const deployMessage: string | undefined = inputs.deployMessage() 81 | const productionBranch: string | undefined = inputs.productionBranch() 82 | const enablePullRequestComment: boolean = inputs.enablePullRequestComment() 83 | const enableCommitComment: boolean = inputs.enableCommitComment() 84 | const overwritesPullRequestComment: boolean = 85 | inputs.overwritesPullRequestComment() 86 | const netlifyConfigPath: string | undefined = inputs.netlifyConfigPath() 87 | const alias: string | undefined = inputs.alias() 88 | 89 | const branchMatchesProduction: boolean = 90 | !!productionBranch && context.ref === `refs/heads/${productionBranch}` 91 | const productionDeploy: boolean = 92 | branchMatchesProduction || inputs.productionDeploy() 93 | // Create Netlify API client 94 | const netlifyClient = new NetlifyAPI(netlifyAuthToken) 95 | // Resolve publish directory 96 | const deployFolder = path.resolve(process.cwd(), dir) 97 | // Resolve functions directory 98 | const functionsFolder = 99 | functionsDir && path.resolve(process.cwd(), functionsDir) 100 | // Deploy to Netlify 101 | const deploy = await netlifyClient.deploy(siteId, deployFolder, { 102 | draft: !productionDeploy, 103 | message: deployMessage, 104 | configPath: netlifyConfigPath, 105 | ...(productionDeploy ? {} : {branch: alias}), 106 | fnDir: functionsFolder 107 | }) 108 | if (productionDeploy && alias !== undefined) { 109 | // eslint-disable-next-line no-console 110 | console.warn( 111 | `Only production deployment was conducted. The alias ${alias} was ignored.` 112 | ) 113 | } 114 | // Create a message 115 | const message = productionDeploy 116 | ? `🎉 Published on ${deploy.deploy.ssl_url} as production\n🚀 Deployed on ${deploy.deploy.deploy_ssl_url}` 117 | : `🚀 Deployed on ${deploy.deploy.deploy_ssl_url}` 118 | // Print the URL 119 | process.stdout.write(`${message}\n`) 120 | 121 | // Set the deploy URL to outputs for GitHub Actions 122 | const deployUrl = productionDeploy 123 | ? deploy.deploy.ssl_url 124 | : deploy.deploy.deploy_ssl_url 125 | core.setOutput('deploy-url', deployUrl) 126 | 127 | // Get GitHub token 128 | const githubToken = inputs.githubToken() 129 | if (githubToken === '') { 130 | return 131 | } 132 | const markdownComment = `${getCommentIdentifier(siteId)}\n${message}` 133 | 134 | // Create GitHub client 135 | const githubClient = getOctokit(githubToken) 136 | 137 | if (enableCommitComment) { 138 | const commitCommentParams = { 139 | owner: context.repo.owner, 140 | repo: context.repo.repo, 141 | commit_sha: context.sha, 142 | body: markdownComment 143 | } 144 | // TODO: Remove try 145 | // NOTE: try-catch is experimentally used because commit message may not be done in some conditions. 146 | try { 147 | // Comment to the commit 148 | await githubClient.rest.repos.createCommitComment(commitCommentParams) 149 | } catch (err) { 150 | // eslint-disable-next-line no-console 151 | console.error(err, JSON.stringify(commitCommentParams, null, 2)) 152 | } 153 | } 154 | 155 | // If it is a pull request and enable comment on pull request 156 | if (context.issue.number !== undefined) { 157 | if (enablePullRequestComment) { 158 | let commentId: number | undefined = undefined 159 | if (overwritesPullRequestComment) { 160 | // Find issue comment 161 | commentId = await findIssueComment(githubClient, siteId) 162 | } 163 | 164 | // NOTE: if not overwrite, commentId is always undefined 165 | if (commentId !== undefined) { 166 | // Update comment of the deploy URL 167 | await githubClient.rest.issues.updateComment({ 168 | owner: context.issue.owner, 169 | repo: context.issue.repo, 170 | comment_id: commentId, 171 | body: markdownComment 172 | }) 173 | } else { 174 | // Comment the deploy URL 175 | await githubClient.rest.issues.createComment({ 176 | issue_number: context.issue.number, 177 | owner: context.repo.owner, 178 | repo: context.repo.repo, 179 | body: markdownComment 180 | }) 181 | } 182 | } 183 | } 184 | 185 | if (inputs.enableGithubDeployment()) { 186 | try { 187 | const environment = 188 | inputs.githubDeploymentEnvironment() ?? 189 | (productionDeploy 190 | ? 'production' 191 | : context.issue.number !== undefined 192 | ? 'pull request' 193 | : 'commit') 194 | 195 | const description = inputs.githubDeploymentDescription() 196 | // Create GitHub Deployment 197 | await createGitHubDeployment( 198 | githubClient, 199 | deployUrl, 200 | environment, 201 | description 202 | ) 203 | } catch (err) { 204 | // eslint-disable-next-line no-console 205 | console.error(err) 206 | } 207 | } 208 | 209 | if (inputs.enableCommitStatus()) { 210 | try { 211 | // When "pull_request", context.payload.pull_request?.head.sha is expected SHA. 212 | // (base: https://github.community/t/github-sha-isnt-the-value-expected/17903/2) 213 | const sha = context.payload.pull_request?.head.sha ?? context.sha 214 | await githubClient.rest.repos.createCommitStatus({ 215 | owner: context.repo.owner, 216 | repo: context.repo.repo, 217 | context: 'Netlify', 218 | description: 'Netlify deployment', 219 | state: 'success', 220 | sha, 221 | target_url: deployUrl 222 | }) 223 | } catch (err) { 224 | // eslint-disable-next-line no-console 225 | console.error(err) 226 | } 227 | } 228 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 229 | } catch (error: any) { 230 | core.setFailed(error.message) 231 | } 232 | } 233 | 234 | run(defaultInputs) 235 | --------------------------------------------------------------------------------