├── .nvmrc ├── .eslintignore ├── .github ├── actions │ ├── thanks-action │ │ ├── action.yml │ │ ├── __tests__ │ │ │ └── thanks.test.ts │ │ └── thanks.ts │ └── debug-action │ │ ├── action.yml │ │ ├── debug.ts │ │ └── __tests__ │ │ └── debug.test.ts └── workflows │ ├── thanks-workflow.yml │ └── debug-workflow.yml ├── tsconfig.json ├── jest.config.js ├── README.md ├── package.json ├── .eslintrc.json └── .gitignore /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.7.0 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !/.github -------------------------------------------------------------------------------- /.github/actions/thanks-action/action.yml: -------------------------------------------------------------------------------- 1 | name: "thanks-action" 2 | description: "Says thanks when a contributor opens an issue" 3 | author: "jeffrafter" 4 | runs: 5 | using: "node12" 6 | main: "./thanks.js" 7 | inputs: 8 | thanks-message: 9 | description: Say thanks 10 | default: Thanks for opening an issue ❤! 11 | -------------------------------------------------------------------------------- /.github/actions/debug-action/action.yml: -------------------------------------------------------------------------------- 1 | name: "debug-action" 2 | description: "Outputs debug information" 3 | author: "jeffrafter" 4 | inputs: 5 | amazing-creature: 6 | description: What kind of amazing creature are you? 7 | default: person 8 | outputs: 9 | amazing-message: 10 | description: We said something nice, this was what we said. 11 | runs: 12 | using: "node12" 13 | main: "./debug.js" 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "esnext", 5 | "lib": ["es2015", "es2017"], 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitAny": true, 12 | "removeComments": false, 13 | "preserveConstEnums": true 14 | }, 15 | "include": [".github/actions/**/*.ts", "**/*.ts"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/thanks-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Thanks workflow 2 | on: [issues] 3 | 4 | jobs: 5 | build: 6 | name: Thanks 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | with: 11 | fetch-depth: 1 12 | - run: npm install 13 | - run: npm run build 14 | - uses: ./.github/actions/thanks-action 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | THANKS_USER_TOKEN: ${{ secrets.THANKS_USER_TOKEN }} 18 | id: thanks 19 | -------------------------------------------------------------------------------- /.github/workflows/debug-workflow.yml: -------------------------------------------------------------------------------- 1 | name: "Debug workflow" 2 | on: [push] 3 | 4 | jobs: 5 | build: 6 | name: Debug 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | with: 11 | fetch-depth: 1 12 | - run: npm install 13 | - run: npm run build 14 | - uses: ./.github/actions/debug-action 15 | with: 16 | amazing-creature: Octocat 17 | id: debug 18 | - run: echo There was an amazing message - ${{ steps.debug.outputs.amazing-message }} 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nock = require('nock') 2 | nock.disableNetConnect() 3 | 4 | const processStdoutWrite = process.stdout.write.bind(process.stdout) 5 | process.stdout.write = (str, encoding, cb) => { 6 | if (!str.match(/^##/)) { 7 | return processStdoutWrite(str, encoding, cb) 8 | } 9 | return false 10 | } 11 | 12 | module.exports = { 13 | clearMocks: true, 14 | moduleFileExtensions: ['js', 'ts'], 15 | testEnvironment: 'node', 16 | testMatch: ['**/*.test.ts'], 17 | transform: { 18 | '^.+\\.ts$': 'ts-jest', 19 | }, 20 | transformIgnorePatterns: ['^.+\\.js$'], 21 | verbose: true, 22 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example GitHub Actions in TypeScript 2 | 3 | This repository contains two example GitHub Actions and associated workflows. The code is written in TypeScript and the tests are written using Jest. 4 | 5 | You can either clone this repository or use it as a template. 6 | 7 | ## Dependencies 8 | 9 | Once you've got the code, install the dependencies: 10 | 11 | ```bash 12 | npm install 13 | ``` 14 | 15 | ## Testing 16 | 17 | Run the tests: 18 | 19 | ```bash 20 | npm test 21 | ``` 22 | 23 | And lint: 24 | 25 | ```bash 26 | npm run lint 27 | ``` 28 | 29 | # More information 30 | 31 | For more information read the associated blog post: https://jeffrafter.com/working-with-github-actions/ -------------------------------------------------------------------------------- /.github/actions/debug-action/debug.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | 4 | const run = async (): Promise => { 5 | try { 6 | const creature = core.getInput('amazing-creature') 7 | if (creature === 'mosquito') { 8 | core.setFailed('Sorry, mosquitos are not amazing 🚫🦟') 9 | return 10 | } 11 | const pusherName = github.context.payload.pusher.name 12 | const message = `👋 Hello ${pusherName}! You are an amazing ${creature}! 🙌` 13 | core.debug(message) 14 | core.setOutput('amazing-message', message) 15 | } catch (error) { 16 | core.setFailed(`Debug-action failure: ${error}`) 17 | } 18 | } 19 | 20 | run() 21 | 22 | export default run 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "tsc", 5 | "test": "tsc --noEmit && jest", 6 | "lint": "eslint . --ext .ts" 7 | }, 8 | "license": "ISC", 9 | "devDependencies": { 10 | "@actions/core": "^1.1.0", 11 | "@actions/github": "^1.1.0", 12 | "@types/jest": "^24.0.18", 13 | "@types/js-yaml": "^3.12.1", 14 | "@types/nock": "^11.1.0", 15 | "@types/node": "^12.7.5", 16 | "@typescript-eslint/eslint-plugin": "^2.2.0", 17 | "@typescript-eslint/parser": "^2.2.0", 18 | "eslint": "^6.3.0", 19 | "eslint-config-prettier": "^6.3.0", 20 | "eslint-plugin-prettier": "^3.1.0", 21 | "jest": "^24.9.0", 22 | "js-yaml": "^3.13.1", 23 | "nock": "^11.3.4", 24 | "prettier": "^1.18.2", 25 | "ts-jest": "^24.0.2", 26 | "typescript": "^3.6.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint", "prettier"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier/@typescript-eslint", 8 | "plugin:prettier/recommended" 9 | ], 10 | "rules": { 11 | "prettier/prettier": [ 12 | "error", 13 | { 14 | "singleQuote": true, 15 | "trailingComma": "all", 16 | "bracketSpacing": false, 17 | "printWidth": 120, 18 | "tabWidth": 2, 19 | "semi": false 20 | } 21 | ], 22 | // octokit/rest requires parameters that are not in camelcase 23 | "camelcase": "off", 24 | "@typescript-eslint/camelcase": ["error", { "properties": "never" }] 25 | }, 26 | "env": { 27 | "node": true, 28 | "jest": true 29 | }, 30 | "parserOptions": { 31 | "ecmaVersion": 2018, 32 | "sourceType": "module" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/actions/thanks-action/__tests__/thanks.test.ts: -------------------------------------------------------------------------------- 1 | import * as github from '@actions/github' 2 | import {WebhookPayload} from '@actions/github/lib/interfaces' 3 | import nock from 'nock' 4 | import run from '../thanks' 5 | 6 | beforeEach(() => { 7 | jest.resetModules() 8 | 9 | github.context.payload = { 10 | action: 'opened', 11 | issue: { 12 | number: 1, 13 | }, 14 | } as WebhookPayload 15 | }) 16 | 17 | describe('thanks action', () => { 18 | it('adds a thanks comment and heart reaction', async () => { 19 | process.env['INPUT_THANKS-MESSAGE'] = 'Thanks for opening an issue ❤!' 20 | process.env['GITHUB_REPOSITORY'] = 'example/repository' 21 | process.env['GITHUB_TOKEN'] = '12345' 22 | 23 | nock('https://api.github.com') 24 | .post('/repos/example/repository/issues/1/comments', body => body.body === 'Thanks for opening an issue ❤!') 25 | .reply(200, {url: 'https://github.com/example/repository/issues/1#comment'}) 26 | 27 | nock('https://api.github.com') 28 | .post('/repos/example/repository/issues/1/reactions', body => body.content === 'heart') 29 | .reply(200, {content: 'heart'}) 30 | 31 | await run() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore any generated TypeScript -> JavaScript files 2 | .github/actions/**/*.js 3 | 4 | # Ignore test runner output 5 | __tests__/runner/* 6 | 7 | # ========================= 8 | # Node.js-Specific Ignores 9 | # ========================= 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # node-waf configuration 19 | .lock-wscript 20 | 21 | # Dependency directories 22 | node_modules/ 23 | jspm_packages/ 24 | 25 | # Typescript v1 declaration files 26 | typings/ 27 | 28 | # Optional npm cache directory 29 | .npm 30 | 31 | # Optional eslint cache 32 | .eslintcache 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # Output of 'npm pack' 38 | *.tgz 39 | 40 | # Yarn Integrity file 41 | .yarn-integrity 42 | 43 | # dotenv environment variables file 44 | .env 45 | 46 | # ========================= 47 | # Operating System Files 48 | # ========================= 49 | 50 | # OSX 51 | # ========================= 52 | 53 | .DS_Store 54 | .AppleDouble 55 | .LSOverride 56 | 57 | # Thumbnails 58 | ._* 59 | 60 | # Files that might appear on external disk 61 | .Spotlight-V100 62 | .Trashes 63 | 64 | # Directories potentially created on remote AFP share 65 | .AppleDB 66 | .AppleDesktop 67 | Network Trash Folder 68 | Temporary Items 69 | .apdisk 70 | 71 | # Windows 72 | # ========================= 73 | 74 | # Windows image file caches 75 | Thumbs.db 76 | ehthumbs.db 77 | 78 | # Folder config file 79 | Desktop.ini 80 | 81 | # Recycle Bin used on file shares 82 | $RECYCLE.BIN/ 83 | 84 | # Windows Installer files 85 | *.cab 86 | *.msi 87 | *.msm 88 | *.msp 89 | 90 | # Windows shortcuts 91 | *.lnk -------------------------------------------------------------------------------- /.github/actions/thanks-action/thanks.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | 4 | const run = async (): Promise => { 5 | try { 6 | // Limit only to when issues are opened (not edited, closed, etc.) 7 | if (github.context.payload.action !== 'opened') return 8 | 9 | // Check the payload 10 | const issue = github.context.payload.issue 11 | if (!issue) return 12 | 13 | const token = process.env['THANKS_USER_TOKEN'] || process.env['GITHUB_TOKEN'] 14 | if (!token) return 15 | 16 | // Create the octokit client 17 | const octokit: github.GitHub = new github.GitHub(token) 18 | const nwo = process.env['GITHUB_REPOSITORY'] || '/' 19 | const [owner, repo] = nwo.split('/') 20 | 21 | // Reply with the thanks message 22 | // https://octokit.github.io/rest.js/#octokit-routes-issues-create-comment 23 | const thanksMessage = core.getInput('thanks-message') 24 | const issueCommentResponse = await octokit.issues.createComment({ 25 | owner, 26 | repo, 27 | issue_number: issue.number, 28 | body: thanksMessage, 29 | }) 30 | console.log(`Replied with thanks message: ${issueCommentResponse.data.url}`) 31 | 32 | // Add a reaction 33 | // https://octokit.github.io/rest.js/#octokit-routes-reactions-create-for-issue 34 | const issueReactionResponse = await octokit.reactions.createForIssue({ 35 | owner, 36 | repo, 37 | issue_number: issue.number, 38 | content: 'heart', 39 | }) 40 | console.log(`Reacted: ${issueReactionResponse.data.content}`) 41 | } catch (error) { 42 | console.error(error.message) 43 | core.setFailed(`Thanks-action failure: ${error}`) 44 | } 45 | } 46 | 47 | run() 48 | 49 | export default run 50 | -------------------------------------------------------------------------------- /.github/actions/debug-action/__tests__/debug.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import run from '../debug' 4 | import fs from 'fs' 5 | import yaml from 'js-yaml' 6 | import {WebhookPayload} from '@actions/github/lib/interfaces' 7 | 8 | beforeEach(() => { 9 | jest.resetModules() 10 | const doc = yaml.safeLoad(fs.readFileSync(__dirname + '/../action.yml', 'utf8')) 11 | Object.keys(doc.inputs).forEach(name => { 12 | const envVar = `INPUT_${name.replace(/ /g, '_').toUpperCase()}` 13 | process.env[envVar] = doc.inputs[name]['default'] 14 | }) 15 | github.context.payload = { 16 | pusher: { 17 | name: 'mona', 18 | }, 19 | } as WebhookPayload 20 | }) 21 | 22 | afterEach(() => { 23 | const doc = yaml.safeLoad(fs.readFileSync(__dirname + '/../action.yml', 'utf8')) 24 | Object.keys(doc.inputs).forEach(name => { 25 | const envVar = `INPUT_${name.replace(/ /g, '_').toUpperCase()}` 26 | delete process.env[envVar] 27 | }) 28 | }) 29 | 30 | describe('debug action debug messages', () => { 31 | it('outputs a debug message', async () => { 32 | const debugMock = jest.spyOn(core, 'debug') 33 | await run() 34 | expect(debugMock).toHaveBeenCalledWith('👋 Hello mona! You are an amazing person! 🙌') 35 | }) 36 | 37 | it('does not output debug messages for non-amazing creatures', async () => { 38 | process.env['INPUT_AMAZING-CREATURE'] = 'mosquito' 39 | const debugMock = jest.spyOn(core, 'debug') 40 | const setFailedMock = jest.spyOn(core, 'setFailed') 41 | await run() 42 | expect(debugMock).toHaveBeenCalledTimes(0) 43 | expect(setFailedMock).toHaveBeenCalledWith('Sorry, mosquitos are not amazing 🚫🦟') 44 | }) 45 | }) 46 | 47 | describe('debug action output', () => { 48 | it('sets the action output', async () => { 49 | const setOutputMock = jest.spyOn(core, 'setOutput') 50 | await run() 51 | expect(setOutputMock).toHaveBeenCalledWith('amazing-message', '👋 Hello mona! You are an amazing person! 🙌') 52 | }) 53 | }) 54 | --------------------------------------------------------------------------------