├── .github ├── workflows │ ├── check.yml │ └── package.yml └── config.yml ├── tsconfig.json ├── action.yml ├── action-sms.test.js ├── LICENSE ├── src └── main.ts ├── dist └── main.js ├── package.json ├── .gitignore ├── README.md └── jest.config.js /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | [pull_request] 4 | jobs: 5 | check: 6 | name: Run Tests 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout Repository 10 | uses: actions/checkout@v4 11 | - name: Setup Node.js 12 | uses: actions/setup-node@v4 13 | - name: npm 14 | run: | 15 | npm run dev 16 | npm run clean 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "./dist", 5 | "target": "es2018", 6 | "strict": true, 7 | "strictNullChecks": true, 8 | "alwaysStrict": true, 9 | "moduleResolution": "node", 10 | "skipLibCheck": true 11 | }, 12 | "types": ["node"], 13 | "typeRoots": ["./node_modules/@types"], 14 | "include": [ 15 | "./src/**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules", 19 | "./node_modules", 20 | "./node_modules/*", 21 | "./node_modules/@types/node/index.d.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | name: Package 7 | 8 | jobs: 9 | check: 10 | name: Package distribution file 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | ref: master 17 | - name: Package 18 | run: | 19 | npm ci 20 | npm run test 21 | - name: Commit 22 | run: | 23 | git config --global user.name "GitHub Actions" 24 | git add dist/ 25 | git add node_modules/ 26 | git commit -m "chore: Update dist" || echo "No changes to commit" 27 | git push origin HEAD:master 28 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Twilio SMS' 2 | author: 'Twilio Labs' 3 | description: 'Send an SMS from GitHub Actions using Twilio Programmable SMS' 4 | inputs: 5 | FROM_PHONE_NUMBER: 6 | description: 'Phone number in your Twilio account to send the SMS from' 7 | required: true 8 | TO_PHONE_NUMBER: 9 | description: 'Phone number to send the SMS to' 10 | required: true 11 | message: 12 | description: 'The message you want to send' 13 | required: true 14 | TWILIO_ACCOUNT_SID: 15 | description: 'A Twilio Account SID.' 16 | required: true 17 | TWILIO_AUTH_TOKEN: 18 | description: 'A Twilio Auth Token.' 19 | required: true 20 | outputs: 21 | messageSid: 22 | description: 'The Twilio Message SID' 23 | runs: 24 | using: 'node16' 25 | main: 'dist/main.js' 26 | branding: 27 | color: 'red' 28 | icon: 'message-circle' 29 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | newIssueWelcomeComment: | 2 | Thank you so much for opening your first issue for this project! We'll try to get back to it as quickly as possible. While you are waiting...here's a random picture of a corgi (powered by [dog.ceo](https://dog.ceo)) 3 | 4 | ![picture of dog](https://images.dog.ceo/breeds/corgi-cardigan/n02113186_1447.jpg) 5 | 6 | firstPRMergeComment: > 7 | Congratulations to your first contribution to Twilio Labs! 8 | 9 | We'd love to say thank you and send you some swag. If you are interested please fill out this form at https://twil.io/hacktoberfest-swag 10 | 11 | If you are on the look out for more ways to contribute to open-source, 12 | check out a list of some of our repositories at https://github.com/twilio/opensource. 13 | 14 | And if you love Twilio as much as we do, make sure to check out our 15 | Twilio Champions program! https://www.twilio.com/champions -------------------------------------------------------------------------------- /action-sms.test.js: -------------------------------------------------------------------------------- 1 | const core = require("@actions/core"); 2 | const twilio = require("twilio"); 3 | const run = require("./dist/main.js"); 4 | 5 | jest.mock("@actions/core"); 6 | jest.mock("twilio"); 7 | 8 | test("Log failures", async () => { 9 | const errorMessage = "Error from twilio"; 10 | 11 | twilio.mockImplementation(() => { 12 | throw new Error(errorMessage); 13 | }); 14 | 15 | await run(); 16 | 17 | expect(core.error.mock.calls).toEqual( 18 | expect.arrayContaining([["Failed to send message", errorMessage]]) 19 | ); 20 | 21 | expect(core.setFailed.mock.calls).toEqual( 22 | expect.arrayContaining([[errorMessage]]) 23 | ); 24 | }); 25 | 26 | test("Returns message sid", async () => { 27 | const sid = "ID123"; 28 | 29 | twilio.mockReturnValue({ 30 | messages: { 31 | create: () => ({ sid }) 32 | } 33 | }); 34 | 35 | const { sid: resultSid } = await run(); 36 | expect(resultSid).toEqual(sid); 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Twilio Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const twilio = require('twilio'); 3 | 4 | async function run() { 5 | const from = core.getInput('fromPhoneNumber') || process.env.FROM_PHONE_NUMBER; 6 | const to = core.getInput('toPhoneNumber') || process.env.TO_PHONE_NUMBER; 7 | const message = core.getInput('messageToSend') || process.env.MESSAGE_TO_SEND; 8 | 9 | 10 | const accountSid = 11 | core.getInput('TWILIO_ACCOUNT_SID') || process.env.TWILIO_ACCOUNT_SID; 12 | const authToken = 13 | core.getInput('TWILIO_AUTH_TOKEN') || process.env.TWILIO_AUTH_TOKEN; 14 | 15 | core.debug('Authenticating with Twilio'); 16 | const client = twilio(accountSid, authToken); 17 | core.debug('Attempting to send SMS!'); 18 | const resultMessage = await client.messages.create({ 19 | from, 20 | to, 21 | body: message, 22 | }); 23 | core.debug('SMS sent!'); 24 | 25 | core.setOutput('messageSid', resultMessage.sid); 26 | 27 | return resultMessage; 28 | } 29 | 30 | async function execute() { 31 | try { 32 | return await run(); //@ts-ignore 33 | } catch({ message }) { 34 | core.error('Failed to send message', message); 35 | core.setFailed(message); 36 | } 37 | } 38 | 39 | module.exports = execute; 40 | 41 | execute(); 42 | -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const core = require('@actions/core'); 3 | const twilio = require('twilio'); 4 | async function run() { 5 | const from = core.getInput('fromPhoneNumber') || process.env.FROM_PHONE_NUMBER; 6 | const to = core.getInput('toPhoneNumber') || process.env.TO_PHONE_NUMBER; 7 | const message = core.getInput('messageToSend') || process.env.MESSAGE_TO_SEND; 8 | const accountSid = core.getInput('TWILIO_ACCOUNT_SID') || process.env.TWILIO_ACCOUNT_SID; 9 | const authToken = core.getInput('TWILIO_AUTH_TOKEN') || process.env.TWILIO_AUTH_TOKEN; 10 | core.debug('Authenticating with Twilio'); 11 | const client = twilio(accountSid, authToken); 12 | core.debug('Attempting to send SMS!'); 13 | const resultMessage = await client.messages.create({ 14 | from, 15 | to, 16 | body: message, 17 | }); 18 | core.debug('SMS sent!'); 19 | core.setOutput('messageSid', resultMessage.sid); 20 | return resultMessage; 21 | } 22 | async function execute() { 23 | try { 24 | return await run(); //@ts-ignore 25 | } 26 | catch ({ message }) { 27 | core.error('Failed to send message', message); 28 | core.setFailed(message); 29 | } 30 | } 31 | module.exports = execute; 32 | execute(); 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@twilio-labs/actions-sms", 3 | "version": "1.0.0", 4 | "description": "Send an SMS through GitHub Actions", 5 | "main": "dist/main.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "test": "npm run build && jest", 9 | "install:dev": "rm -rf node_modules && npm install", 10 | "install:prod": "rm -rf node_modules && npm install --omit=dev", 11 | "dev": "npm run install:dev && npm run test", 12 | "clean": "npm run install:prod", 13 | "dependencyUpdate": "npx npm-check-updates -u && npm install" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/twilio-labs/actions-sms.git" 18 | }, 19 | "keywords": [ 20 | "github", 21 | "actions", 22 | "twilio", 23 | "sms" 24 | ], 25 | "author": "Dominik Kundel (https://twilio.com/labs)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/twilio-labs/actions-sms/issues" 29 | }, 30 | "homepage": "https://github.com/twilio-labs/actions-sms#readme", 31 | "dependencies": { 32 | "@actions/core": "^1.11.1", 33 | "twilio": "^5.3.7" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^22.10.1", 37 | "jest": "^29.7.0", 38 | "typescript": "^5.7.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | # node_modules/ this is a GitHub Action, we need to not remove node_modules 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | # dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and not Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Stores VSCode versions used for testing VSCode extensions 107 | .vscode-test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twilio SMS GitHub Action 2 | 3 | Send an SMS from GitHub Actions. 4 | 5 | ## Prerequisites 6 | 7 | - A Twilio Account. [Sign up for free](https://www.twilio.com/try-twilio) 8 | - A [Twilio Auth Token](https://www.twilio.com/docs/iam/api/authtoken) 9 | - A [Registered Phone Number](https://www.twilio.com/docs/phone-numbers/regulatory/faq) 10 | 11 | ## Usage 12 | 13 | 1. Set up your credentials as secrets in your repository settings using `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TO_PHONE_NUMBER`, `FROM_PHONE_NUMBER` 14 | 15 | 2. Add the following to your workflow 16 | 17 | ```yml 18 | name: Twilio Send 19 | on: 20 | workflow_dispatch: # allows you to manually trigger the workflow 21 | schedule: # runs on a cron, nightly 22 | - cron: 0 0 * * * 23 | env: 24 | TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }} 25 | TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }} 26 | TWILIO_API_SECRET: ${{ secrets.TWILIO_API_SECRET }} 27 | permissions: 28 | contents: read 29 | jobs: 30 | send: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: 'Sending SMS Notification' 34 | uses: twilio-labs/actions-sms@v1 35 | with: 36 | FROM_PHONE_NUMBER: ${{ secrets.FROM_PHONE_NUMBER }} 37 | TO_PHONE_NUMBER: ${{ secrets.TO_PHONE_NUMBER }} 38 | message: 'Hello from Twilio' 39 | ``` 40 | 41 | ## Inputs 42 | 43 | ### `FROM_PHONE_NUMBER` 44 | 45 | **Required** Phone number in your Twilio account to send the SMS from 46 | 47 | ### `TO_PHONE_NUMBER` 48 | 49 | **Required** Phone number to send the SMS to 50 | 51 | ### `message` 52 | 53 | **Required** The message you want to send 54 | 55 | ### `TWILIO_ACCOUNT_SID` 56 | 57 | A Twilio Account SID. Can alternatively be stored in environment 58 | 59 | ### `TWILIO_AUTH_TOKEN` 60 | 61 | A Twilio Auth Token. Can alternatively be stored in environment 62 | 63 | ## Outputs 64 | 65 | ### `messageSid` 66 | 67 | The SID of the [message resource](https://www.twilio.com/docs/sms/api/message-resource#message-properties) associated with the SMS sent. 68 | 69 | ## Contributing 70 | 71 | ## Third Party Licenses 72 | 73 | This GitHub Action uses a couple of Node.js modules to work. 74 | 75 | License and other copyright information for each module are included in the release branch of each action version under `node_modules/{module}`. 76 | 77 | More information for each package can be found at `https://www.npmjs.com/package/{package}` 78 | 79 | ## License 80 | 81 | [![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/) 82 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/lb/7ldkbf557vn8zp42341vhd0m0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: null, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | // modulePathIgnorePatterns: [], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | // preset: null, 95 | 96 | // Run tests from one or more projects 97 | // projects: null, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: null, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: null, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | // roots: [ 119 | // "" 120 | // ], 121 | 122 | // Allows you to use a custom runner instead of Jest's default test runner 123 | // runner: "jest-runner", 124 | 125 | // The paths to modules that run some code to configure or set up the testing environment before each test 126 | // setupFiles: [], 127 | 128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 129 | // setupFilesAfterEnv: [], 130 | 131 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 132 | // snapshotSerializers: [], 133 | 134 | // The test environment that will be used for testing 135 | testEnvironment: "node", 136 | 137 | // Options that will be passed to the testEnvironment 138 | // testEnvironmentOptions: {}, 139 | 140 | // Adds a location field to test results 141 | // testLocationInResults: false, 142 | 143 | // The glob patterns Jest uses to detect test files 144 | // testMatch: [ 145 | // "**/__tests__/**/*.[jt]s?(x)", 146 | // "**/?(*.)+(spec|test).[tj]s?(x)" 147 | // ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | // testPathIgnorePatterns: [ 151 | // "/node_modules/" 152 | // ], 153 | 154 | // The regexp pattern or array of patterns that Jest uses to detect test files 155 | // testRegex: [], 156 | 157 | // This option allows the use of a custom results processor 158 | // testResultsProcessor: null, 159 | 160 | // This option allows use of a custom test runner 161 | // testRunner: "jasmine2", 162 | 163 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 164 | // testURL: "http://localhost", 165 | 166 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 167 | // timers: "real", 168 | 169 | // A map from regular expressions to paths to transformers 170 | // transform: null, 171 | 172 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 173 | // transformIgnorePatterns: [ 174 | // "/node_modules/" 175 | // ], 176 | 177 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 178 | // unmockedModulePathPatterns: undefined, 179 | 180 | // Indicates whether each individual test should be reported during the run 181 | // verbose: null, 182 | 183 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 184 | // watchPathIgnorePatterns: [], 185 | 186 | // Whether to use watchman for file crawling 187 | // watchman: true, 188 | }; 189 | --------------------------------------------------------------------------------