in ,

Working with GitHub Actions, Hacker News

Working with GitHub Actions, Hacker News


          Hubot from the Octodex    
Image credit: GitHub’s Hubotfrom the Octodex

GitHub Actions are still in beta and are changing quickly. But if you are looking to get started the possibilities are endless. This guide is mostly about pointing to documentation and exploring some fun ways to use GitHub Actions.

In this post we’ll create a repository which contains a GitHub Action – built in TypeScript – and an associated workflow. In the action we’ll respond to push events and output some logging information. Technically, you don’t need a custom script to accomplish this; you could instead build a very simple workflow which runsechocommands. Using a full script will allow us to explore more capabilities of GitHub Actions. We’ll also create an action that automatically responds to, and reacts to, issue comments.

Before you read this it is important to note: starting with a (template) will save you a lot of time and setup. In this post, however, I am going to work through and explain all of the steps. Included in this post are some of the reasons I’ve chosen one particular setup and skipped another. When getting started with GitHub Actions it is difficult to understand how all of the pieces fit together, or why you might want to create and action for a particular task. Hopefully this post provides some helpful examples. That said, there are probably steps here that you’ve seen before, don’t care about, or just want to skip and that’s okay.

In order to follow this, you’ll need a GitHub account. Additionally, you’ll need tosign up for the GitHub Actions beta. The examples will be in TypeScript.

All of the code (and commits) are availble on GitHub:https://github.com/jeffrafter/example-github-action-typescript

The documentation for GitHub Actions is really good (far more complete than this post) and is good to have on hand. You can learn how to build Actions, Workflows and core concepts; as well as dive deeply on using the toolkit, octokit and handling payloads.

First you want to create a folder for your project:

mkdirexample-github-action-typescriptcdexample-github-action-typescript

We’ll be using TypeScript to build our action, which requires Node. Out of the box GitHub supports a few environments for your actions to runThere are several differentvirtual operating systemsyou can use for your actions which come preloaded withuseful software. Additionally, you can utilize Docker containers running in one of these virtual environments to pre-configure your hosts.. There is built-in support for running actions built in JavaScript (using Node). So why did I choose to use TypeScript? It makes development a lot easier by providing compile-time checks and hints in my editor about methods and parameters (especially if you are using an editor like VSCode that has support for it). As part of our action we’ll export the TypeScript to JavaScript.

Let’s setup our example to use Node. If you have multiple local projects you might run into a conflict about which Node version should be used. Node Version Manager solves this problem. To control which version of Node should be used in your project, add an.nvmrcNotice that the.nvmrcfile starts with a “.“” (period). By default on most systems this creates a hidden file. Oftentimes general project config is hidden away. On MacOS you can show hidden files in Finder by runningdefaults write com.apple.finder AppleShowAllFiles -bool trueand restarting Finder. If you want to list hidden files in your console use the- aparameter:LS-A.file:

The file is pretty simple; just the version. I chose 12 .7.0 because it matches the version that is used to run our action ( (node) ). Node 10. 16 .3 is installed in the defaultGitHub Action software environmentand can be used as well but the will not match the running action environment. At the time you read this there may be a newer version of Node or you may chose to use an older version because your code requirements. You can checkhttps://nodejs.org.

Ignore some things (****************************************** (We plan to use) ******************************************** (git) to keep track of our changes. As we work on our project locally, there will be a lot of files we won’t want to keep track of; we’ll want to ignore them. To do this we’ll create a new file called.Gitignore.Gitignorealso starts with a “.“and you can start to see a pattern emerge.. These files can be very short and specific, or they can be very long and general. We’ll use a more generic one that will work on different kinds of computers. If you are looking for an example.Gitignoreyou can check outhttps://github.com/github/gitignore. For now, just copy the following (this.Gitignoreincludes only what is important for this post.For a more complete version see (here):

# Ignore any generated TypeScript ->JavaScript files .github / actions / ** / *. js  # Logs logs * .log npm-debug.log *  # Dependency directories / node_modules  # Typescript v1 declaration files typings /  # Optional npm cache directory .npm  # Optional eslint cache .eslintcache

With this setup we’ll ignorenode_modulesand JavaScript files in our action folders (if there was any generated locally). This is a non-standard choice but makes developing our action a little easier. By default, GitHub recommends you include thenode_modulesfolder as installing them per-action execution is slow (25 – (seconds). Including all of thenode_modulesin your repository can lead to a lot of files and large commits which can be confusing. Additionally, if yournode_modulesinclude platform specific dependencies which must be compiled (such asHunspell) you will need to recompile them for the target action container anyway.

Ignoring generated JavaScript in our action folders means that we have to build our TypeScript as part of our workflow. Again, this is slower and can lead to compile time errors on the server, but it saves us a few steps when developing actions.

Save your progress using version control

At this point we really haven’t made anythi ng (except a lot of configuration). Even though our website isn’t even a website yet – it still makes sense to save our work. If we make a mistake having our code saved will help us. To do this we’ll usegit– a version control software that lets us create commits or (versions) as we go. To initialize agitrepository run:

By default this creates an empty git repository (none of our files have been added to it). Generally, I useGitHub Desktop; however, I’ll use the command line here. You can check the status of your changes and repository:

You should see:

On branch master  No commits yet  Untracked files:   (Use"git add... "to includeinwhat will be committed).gitignore .nvmrc  nothing added to commit but untracked files present(use"git add"to track)

Let’s get ready to create a commit by adding all of the files:

Here the.means: “everything in the current folder”. But what are we adding it to? We are adding it to the commitstage. Let’s check the status again:

You should see:

On branch master  No commits yet  Changes to be committed:   (Use"git rm --cached... " (to unstage) )new file: .gitignore new file: .nvmrc

We’re getting ready to add two new files to our repository. Let’s commit:

(git) ************************************ (commit -m)  "Initial commit with configuration"

This creates a commit with the message we specified. The commit acts like a save point. If we add or delete files or change something and make a mistake, we can always revert back to this point. We’ll continue to commit as we make changes.

Packages & Dependencies

For almost any Node project you’ll find that you use a lot of packages – you’ll have far more code in packages in yournode_modulesfolder (where package code is stored) than your main project.

Initialize your packages:

Now you have apackage.json:

{  "name":"example-github-action-typescript",  "version":"1.0.0",  "description":"",  "main":"index.js",  "scripts":{    "test":"echo " Error: no test specified  "&& exit 1"  },  "keywords":[],  "author":"",  "license":"ISC"}

Let’s simplify it a bit ( you can fill out or keep fields you like here, but this is the minimum):

{  "private":true,  "scripts":{    "build":"TSC",    "test":"tsc --noEmit && jest"  },  "license":"ISC"}

The only scripts we need at the moment arebuildwhich will convert our TypeScript to JavaScript and (testwhich will run our tests. While we’re working on our action we’ll need access to all of our project’s dependencies; the difference betweendependenciesanddevDependencieswon’t matter very much. For that reason we’ll install everything as adevDependency:

NPMinstall- save-dev  @ actions / core  @ actions / github

(The @ actions / coreand@ actions / githubare the baseline for interacting with GitHub and the incoming events. When you publish an action which is meant to be used by multiple repositories and workflows, you’ll release the action with dependencies included (so they run more quickly). In that case you would use- saveinstead of--save-dev. In most other cases the code for your actions should only be used as part of your testing or development environment (not part of your production environment).

We’ll want to add testing support to test our action:

NPMinstall--save- dev  Jest  JS-Yaml

And TypeScript support:

NPMinstall- save-dev  Typescript  TS-JEST

Finally, because we’re using TypeScript, we’ll want to add type support for development:

npminstall- save-dev  @ types / jest  @ types / node

This is a good opportunity to create another commit; check the status:

You should see:

On branch master Untracked files:   (Use"git add... "to includeinwhat will be committed)package-lock.json package.json  nothing added to commit but untracked files present(use"git add"to track)

We’ve added a lot of files to our folder but many of them are ignored. For example, thenode_modulesfolder contains tons of files (as mentioned before). We want everyone working on our project to install the same dependencies. When installing, they were automatically added to thepackage.jsonfile. Thepackage-lock .jsonensures that the dependencies of our packages are locked to specific versions. Because of this, we’ll add both of these files togit:

And then commit :

(git) ************************************ (commit -m)  "Setting up our packages"

(TypeScript)

We’ll also need to configure TypeScript before we can build our action. Createtsconfig.json:

{  "compilerOptions":{    "module":"commonjs",    "target":"esnext",    "lib":["es2015","es2017"],    "Strict":true,    "esModuleInterop":true,    "skipLibCheck":true,    "noUnusedLocals":true,    "noUnusedParameters":true,    "noImplicitAny":true,    "removeComments":false,    "preserveConstEnums":true  },  "include":[".github/actions/**/*.ts","**/*.ts"],  "exclude":["node_modules"]}

By default this allows us to build all of the actions contained in our repository, adds some strict compile-time checks, and skips type checking for our dependencies.

Let’s commit this file:

You should see:

On branch master Untracked files:   (Use"git add... "to includeinwhat will be committed)tsconfig.json  nothing added to commit but untracked files present(use"git add"to track)

Add it:

And commit :

(git) ************************************ (commit -m)  "TypeScript configuration"

Note: this section is not required to complete this tutorial; if you want to skip it feel free.

Everyone has different preferences when they edit code. Some prefer tabs over spaces. Some want two spaces instead of four. Some prefer semicolons and some don’t. It shouldn’t matter right? But it does. If editors are autoformatting code based on user preferences it is important to make sure everyone has chosen the same set of defaults for that autoformatting. This makes it easy to tell what changed between versions – even when different developers (with different preferences) have made changes.

For this reason we’ll setup a linter and code formatter for our code. Install (eslint) andPrettier:

NPM (install) ************************************ (- save-dev)    eslint  @ typescript-eslint / eslint-plugin  @ typescript-eslint / parser  eslint-config-prettier  eslint-plugin-prettier  Prettier

Now that we have the packages we’ll need to configure them in. eslintrc.json:

{  "Parser":"@ typescript-eslint / parser",  "plugins":["@typescript-eslint","prettier"],  "extends":[ "eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier/@typescript-eslint", "plugin:prettier/recommended"],  "rules":{    "prettier / prettier":[   "error",   {     "singleQuote":true,     "trailingComma":"all",     "bracketSpacing":false,     "printWidth":120,     "tabWidth":2,     "semi":false   } ],         "Camelcase":"off",    "@ typescript-eslint / camelcase":["error",{"properties":"never"}]  },  "env":{    "node":true,    "JEST":true  },  "parserOptions":{    "ecmaVersion":2018,    "sourceType":"module"  }}

I won’t go into too much detail here; there arebetter explanationsto be found. This configuration does a few things:

  • Relies on the typescript eslint parser with the prettier plugin – I’ve found this works very well in @Code. If you were previously using (ts-lint) with prettier this setup should replace your old configuration.
  • This eslint config doesn’t work perfectly for projects that contain both JavaScript and TypeScript – because of that we won’t attempt to lint JavaScript files in our project
  • I’ve turned off the camelcase rules for properties – when writing GitHub Actions you will likely use properties from@ actions / githuband from the API and these will not be camelcase.There are lots of ways to name variables includingcamelCase,snake_case,ProperCase, andCONSTANT_CASE. JavaScript and TypeScript tend to usecamelCasefor variable names, but Ruby (which is what much of GitHub and the GitHub API is written in) generally uses (snake_case) . This is one of those places where the idioms of different languages ​​collide and you have to make the best of it.
  • The expected environment should includenodeandjest– this will helpeslintignore missing declarations for things likedescribe,process,Module, etc.

If you need to ignore specific files when linting you can add them to. eslintignore. Because our setup doesn’t work well for JavaScript we’ll ignore all JavaScript files in. eslintignore:

Notice that we are explicitly unignoring the.githubfolder. This is where our source code will be kept (see next section). We have to unignore this folder explicitly because it starts with a.and is normally ignored by eslint.

Add alintaction to thescripts (node ​​in) ******************************************** (package.json) *********************************************:

"scripts" : {     "build": "tsc",     "test": "tsc --noEmit && jest",     "lint": "eslint. --ext .ts"   },

With this in place we can run:

Wait, there’s an error:

Oops! Something went wrong! :(  ESLint: 6.4.0.  No files matching the pattern "." were found. Please check for typing mistakes in the pattern.

We haven’t written any TypeScript to lint yet. Time to stop configuring and start writing code.

Checking ourgit statusshould show our changes:

On branch master Changes not stagedforcommit:   (Use"git add... "to update what will be committed)  (Use"git checkout -... "to discard changesinworking directory)modified: package-lock.json modified: package.json  Untracked files:   (Use"git add... "to includeinwhat will be committed).eslintignore .eslintrc.json  no changes added to commit(use"git add"and / or"git commit -a ")

This makes sense; we’ve added two files and installed some newdevDependenciesinto our packages. Let’s add everything and to the commit stage:

If we checkgit statusagain:

On branch master Changes to be committed:   (Use"git reset HEAD... "to unstage)new file: .eslintignore new file: .eslintrc.json modified: package-lock.json modified: package.json

Let’s commit:

(git)  commmit -m"Linting configuration"

Project Layout

The code for GitHub Actions are generally kept in theactionsfolder in the.githubfolder. By default, the.githubfolder contains metadata for the repository that can be used for automated tasks. The steps for running an action are defined in a Workflow which is usually stored in theworkflowsfolder in the.githubfolder:

.github /   actions /     debug-action /       __tests __ /         debug.test.ts       action.yml       debug.js       debug.ts   workflows /     debug-workflow.yml

Repositories can contain multiple actions (or none at all); we’ll define our debug action inside a folder calleddebug-action. This will contain our TypeScript, generated JavaScript, tests, and theaction.ymlwhere all of the settings for the action are kept.

A repository may also have multiple workflows (or none at all); we’ll setup a workflow that runs our debug action insidedebug-workflow.yml.

mkdir-p .github / actionsmkdir-p .github / actions / debug-actionmkdir-p .github / actions / debug-action / __ tests__mkdir-p .github / workflows

Building the debug action

Enough setup; let’s get building. Create a new file called.github / actions / debug-action / debug.ts:

import*(as) ************************************ (core)  from'@ actions / core'construn=Async():Promisevoid>=>{  }run()export(default) ************************************ (run)

The code inside your action should be auto-executing. In this case we define arunWe’ve named our functionrunbut you could name the function anything you wanted.runis a convention used in the basejavascript-templatemethod and then immediately call it right after it has been defined. In fact, you don’t even need to define a method, you could include the code for your action directly. In some cases that might be okay, but as the complexity of the action increases it would become confusing. We’ve also made our function the default export. This isn’t required but will make things easier as we move forward and test our code.

There are lots of helpers built into theCorepackage we imported. This is the simplest. Because we’re using TypeScript you may see autocomplete information in your editor:I’m using VSCode here with theYoncé theme. It is super rad.

                 A popup showing the available methods for the core object           

At this point the action does nothing. Lets add some debugging:

(const)  run=Async():Promisevoid>=>{  core.debug('👋 Hello! You are an amazing person! 🙌')}

Even though this action isn ‘ t accomplishing much, let’s write a test for it.

Testing th e debug action

Create a new file called.github / actions / debug-action / __ tests __ / debug.test.ts:

import*ascorefrom'@ actions / core'importrunfrom'../ debug'describe('debug action debug messages',()=>{  it('outputs a debug message',async()=>{    constdebugMock=jest.spyOn( (core) ,'debug')    awaitrun()    expect( (debugMock) ).toHaveBeenCalledWith('👋 Hello! You are an amazing person! 🙌')  })})

We import the actions core library and the run method we just created in our debug action. In our test we create a Jest spy which allows us to verify that thecore.debugmethod is getting called with the correct parameters. Normally, I wouldn’t test the debug output (if it fails I don’t care too much) but this is a good foundation.

In order to run this we’ll need to configure Jest. Create a new file calledjest.config.jsin the root of your project:

module.exports={  clearMocks:true,  moduleFileExtensions:['js','ts'],  testEnvironment:'node',  testMatch:['**/*.test.ts'],  transform:{    '^.   \. ts $':'ts-jest',  },  Verbose:true,}

At this point you can run the tests. From your terminal run:

You should see:

>@ test / Users / njero / Code / Examples / example-github-action-typescript>tsc --noEmit && jest   PASS .github / actions / debug-action / __ tests __ / debug.test.ts   debug action debug messages     ✓ outputs a debug message (5ms)  :: debug :: 👋 Hello! You are an amazing person! 🙌 :: debug :: 👋 Hello! You are an amazing person! 🙌 Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1. 423 s Ran all test suites.

It should pass.The debug output here is a little verbose. There’s anopen issueto be able to silence this while running tests.But let’s remove the debug information from the test output. Changejest.config .jsso that it doesn’t output the debug lines:

(const)  processStdoutWrite=process.stdout.write.bind(process stdout)process.stdout.write=(str,encoding,cb)=>{  if(!  (str) . match(/ ^ :: debug :: /)){    returnprocessStdoutWrite( (str) ,encoding,  (cb) )  }  returnfalse}module.exports={  clearMocks:true,  mo duleFileExtensions:['js','ts'],  testEnvironment:'node',  testMatch:['**/*.test.ts'],  transform:{    '^.   \. ts $':'ts-jest',  },  Verbose:true,}

With that change the debug output should no longer appear when we run the tests. We can run ourlinttask to verify that our code is clean:

It should succeed this time with no errors and no warnings. Let’s commit what we have. Rungit status:

On branch master Untracked files:   (Use"git add... "to includeinwhat will be committed).github / jest.config.js  nothing added to commit but untracked files present(use"git add"to track)

Let’s add those files:

Note, one of the items listed was the.githubfolder. When we added it all of the newly added files inside of that folder were also added. Let’s check the status again:

Notice that we are about to commit three files:

On branch master Changes to be committed:   (Use"git reset HEAD... "to unstage)new file: .github / actions / debug-action / __ tests __ / debug.test.ts new file: .github / actions / debug-action / debug.ts new file: jest.config.js

Commit:

****************************** (git)  commit -m"Debug action"

Create the action.yml for the debug action

We’ve written the code for our action and a test that tells us it is working. Unfortunately we haven’t defined how our action should be used. To do that we have to configure the action in aYMLfile. Create. actions / debug-action / action.yml:

(name):'debug-action'description:'Outputs debug information'author:'jeffrafter'runs:  using:'node 12 '  main:'./ debug.js'

There aremore configuration options availablefor actions but this represents the minimum amount needed to run. Specifically, it gives the action a name (which does not need to match the name of the folder) and points to the code./ debug.js. Unfortunately, we don’t have a file calleddebug.js, we have a file calleddebug.ts.

GitHub Actions have built-in support for JavaScript and cannot run TypeScript directly. Because of this we will need totranspileour TypeScript to JavaScript before it can be run. This is done withTSC(the TypeScript compiler). We’ve already included a task:

This will generate JavaScript filesdebug. JS(anddebug.test. JS):

'Use strict'var__importStar=  (this&&this.__ importStar)||  function(mod){    if( (mod)  &&  (mod) .  __ esModule) )returnmod     varresult={}    if( (mod) !=null)for((var) ************************************ (k)  in  (mod) ) (if)   (Object.hasOwnProperty.call (mod,  (k) ))result[k]=(mod)  [k]    result['default']=mod     returnresult   }Object.defineProperty(exports,'__ esModule',{value:true})const  (core)=__ importStar(require('@ actions / core'))construn=Async()=>{  core.debug('👋 Hello! You are an amazing person! 🙌')}run()exports.  (default)=run

In our setup we’ve ignored these files – they will not be included when we push our code to GitHub. This isn’t the recommended setup; GitHub suggests you include the built files for your actions to save time when running your action (and to reduce the dependencies needed by your action on the server). When developing actions it is easy to forget to build your code with each change. Because of this I’ve chosen to automatically build on execution (even though it is slower)Taking a few extra seconds to build your action on execution may not seem like a big deal; but GitHub Actions have a limited about offreecompute time. While developing an action this might not matter; but long-term those seconds can add up.. When releasing the action it is best to include the built JavaScript.

Workflows

In order to execute thedebug-actionwe’ve created we need to create a workflow. Theaction.ymlin our action definesthe codeto execute and the workflow defineswhento execute it. Workflows should be kept in the.Github / Workflowsfolder in your repository. Your repository may contain multiple workflows. Create. workflows / debug-workflow.yml:

name:'Debug workflow'on:[push]jobs:  build:    name:Debug     runs-on:ubuntu-latest     steps:      -uses:actions / checkout @ v1         with:          fetch-depth:1      -run:npm install       -run:npm run build       -uses:./.github/actions/debug-action

We’ve created a workflow that should be executed on[push]. There are many differentevents that trigger worklows. By specifyingwe’re saying that every time new code is pushed to our GitHub repository our workflow should be executed. In this case we’ve chosen to execute our workflow using theubuntu-latestEnvironment. The steps for a workflow can point to an action that should be used or a command that should be run. Here we use both.

The first step checks out our code using theactions / checkout @ v1 (action:

-uses:actions / checkout @ v1   with:    fetch-depth:1

The (actions / checkout) actionThe code foractions / checkoutliveson github. Where is the copy of your code located on the server? This path is exported to theGITHUB_WORKSPACEdefault environment variable.checks out a copy of your code on the server where the workflow is running. We’ve set the fetch-depth to (1) indicating we only want a shallow-clone.A shallow clone of the code ignores all of the history. Since our action doesn’t use any of the history this is a good speedup.When your action code is included in your.githubfolder (as ourdebug-actionis), you must use theactions / checkoutaction to checkout a copy of the code so that it can run.

The next two steps install our action dependencies and build it:

-run:npm install-run:npm run build

Again, this is generally discouraged because it is slower and takes more resources. While developing, however it is much more simple.

Finally we use our debug action:

-uses:./.github/actions/debug-action

This should be enough to run ourdebug-action. Let’s commit:

You should see:

On branch master Untracked files:   (Use"git add... "to includeinwhat will be committed)  .github / actions / debug-action / action.yml .github / workflows /  nothing added to commit but untracked files present(use"git add"to track)

Let’s add the workflow:

And commit:

(git) ************************************ (commit -m)  "Debug action and wo rkflow configuration "

Pushing to GitHub

Our action and workflow are ready. All that’s left is to push to our repository on GitHub. Create anew repository. I called mineexample -github-action-typescriptand made it public.

                 Creating a new repository           

Once you’ve created the repository you’ll need to click theSSHbutton make sure to setup the remote for your repository to usesshinstead of HTTPs:

                 Choose ssh           

Unfortunately you can’t push workflow changes via HTTPs as it is considered an integration. If you try you’ll see something like the following when you try to push:

Enumerating objects:31, done. Counting objects:100%(31/ 31), done. Delta compression using up to12threads Compressing objects:100%(25/ 25), done. Writing objects:100%(31/ 31),55. 08  (KiB)  |(7.) MiB / s, done. Total31(delta  (6)), reused  (0)(DELTA  (0))remote: Resolving deltas:100%((6)  / 6), done. To https://github.com/jeffrafter/example-github-action-typescript.git  ![remote rejected]master ->master(refusing to allow an integration to create or update. github / workflows / debug-workflow.yml)error: failed to push some refs to'https: // github. com / jeffrafter / example-github-action-typescript.git '

Setup your remote by copying the instructions on the page foran existing repository from the command line:

gitremoteaddorigin [email protected]: jeff rafter / example-github-action-typescript.git

Then push:

gitpush -u origin master

On GitHub, click on the (Actions) tab of your repository and click on the running build. When complete you should see something like:

                 Completed action           

The action ran and the build was marked as complete. But we can’t see the debug information we added. By default, GitHub willnot output debug information in the action logs. To see debug output in the logs you need to add a new secret to your repository. Go to theSettingstab of your repository and clickon the sidebar . Then clickAdd a new secret. Set the name toACTIONS_STEP_DEBUGwith the valuetrueand click (Add) . Currently, there is no way to re-run an action. We’ll see the debug information when we push a new commit.

Using action input

By default GitHu b injectsdefault environment variablesthat can be used by your action including:

  • HOME
  • GITHUB_WORKFLOW
  • GITHUB_ACTION
  • GITHUB_REPOSITORY
  • GITHUB_EVENT_NAME
  • GITHUB_EVENT_PATH
  • GITHUB_WORKSPACE
  • GITHUB_SHA
  • GITHUB_REF
  • GITHUB_HEAD_REF(only in forks)
  • GITHUB_BASE_REF(only in forks)

These are commonly used, but there are many instances where you want to change how an action runs based on configuration in each workflow that uses that action. Let’s add an input to our debug action that changes what the debug message says.

First, define the input and default in.github / debug-action / action.yml:

name:'debug-action'description:'Outputs debug information'author:'jeffrafter'inputs:  amazing-creature:    description:What kind of amazing creature are you?    default:personruns:  using:'node 12 '  main:'./ debug.js'

ve defined a new input calledamazing-creature. When the action is executed the name will be will be converted toINPUT_AMAZING-CREATUREand the value will be passed in via the process environment. Environment variable names normally wouldn’t have-in them as we see inINPUT_AMAZING-CREATURE. Because of the-we need to access the values ​​as strings. The key names inYAML Syntaxcan vary wildly but only spaces are replaced with underscores. You could access the values ​​directlyprocess.env ['INPUT_AMAZING-CREATURE']but usinggetInputas we have done is more future-proof. Let’s use it. Change.github / actions / debug-action / debug.ts:

import*asCorefrom'@ actions / core 'construn=Async():Promisevoid>=>{  constcreature=core.getInput('amazing-creature')  core.debug(``👋 Hello! You are an amazing$ {creature}! 🙌``)}run()export(default) ************************************ (run)

If we save that file and re-run our tests we’ll see a new failure:

FAIL .github / actions / debug-action / __ tests __ / debug.test.ts   debug action debug messages     ✕ outputs a debug message(12 ms)  ● debug action debug messages ›outputs a debug message      expect(jest.fn()). toHaveBeenCalledWith(...Expected)    Expected:"👋 Hello! You are an amazing person! 🙌"    Received:"👋 Hello! You are an amazing undefined! 🙌"    Number of calls:1       6|const debugMock=jest.spyOn( (core,'debug')       7|await run()    >(8)|expect(debugMock). toHaveBeenCalledWith('👋 Hello! You are an amazing person! 🙌')         |^        9|})      10|})      11|      at Object.it(. github / actions / debug-action / __ tests __ / debug.test.ts: 8: 23)Test Suites:1failed, [name] ************* (1)  total Tests:1failed,  (1)  total Snapshots:0total Time:1. 413 s Ran alltestsuites.

Being an amazingundefinedis not very gratifying. The problem is that our test doesn’t know about the environment variableINPUT_AMAZING-CREATURE. Let’s set it in the test in.github / actions / debug-action / __ tests __ / debug.test.ts:

import*(as) ************************************ (core)  from'@ actions / core'importrunfrom'../ debug'describe('debug action debug messages',()=>{  it('outputs a debug message',async()=>{    process.  (ENV)  ['INPUT_AMAZING-CREATURE']='person'    constdebugMock=jest.spyOn( (core) ,'debug')    awaitrun()    expect( (debugMock) ).toHaveBeenCalledWith('👋 Hello! You are an amazing person! 🙌')    deleteprocess.env['INPUT_AMAZING-CREATURE']  })})

Setting the environment variable directly will make our test pass. After our test is complete we remove the variable to reset our state. We could do this [setup and teardown] withbeforeEachandafterEachcallbacks:

import*(as) ************************************ (core)  from'@ actions / core'importrunfrom'../ debug'beforeEach(()=>{  Jest.resetModules()  process.  (ENV)  ['INPUT_AMAZING-CREATURE']='person'})afterEach(()=>{  deleteprocess.env['INPUT_AMAZING-CREATURE']})describe('debug action debug messages',()=>{  it('outputs a debug message',async()=>{    constdebugMock=jest.spyOn( (core) ,'debug')    awaitrun()    expect( (debugMock) ).toHaveBeenCalledWith('👋 Hello! You are an amazing person! 🙌')  })})

We’ve also calledjest.resetModules ()which will prevent other imported modules from using a cached value. The test should still pass.

What if you have a lot of inputs? It would be nice to automatically import all of the defaults. To do this we’ll need to read theaction.ymland assign all of the defaults to the environment. Install the type definitions for (js-yaml) :

NPMinstall--save-dev @ types / js-yaml

And then lets expand ourbeforeEachandafterEachcallbacks:

import*asCorefrom'@ actions / core'importrunfrom'../ debug'import  (FS)  from'FS'importyamlfrom'js-yaml'beforeEach(()=>{  Jest.resetModules()  const  (doc)=yaml.safeLoad( (FS) . readFileSync(__ dirname'/../ action.yml','utf8'))  Object.keys( (DOC) .  (inputs) ).forEach(name=>{    constenvVar=``INPUT _$ {name.replace(/ / g,'_').toUpperCase(}``    process.  (ENV)  [envVar]=doc.  (inputs)  [name]['default']  })})afterEach(()=>{  const  (doc)=yaml.safeLoad( (FS) . readFileSync(__ dirname'/../ action.yml','utf8'))  Object.keys( (DOC) .  (inputs) ).forEach(name=>{    constenvVar=``INPUT _$ {name.replace(/ / g,'_').toUpperCase(}``    deleteprocess.env[envVar]  })})describe('debug action debug messages',()=>{  it('outputs a debug message',async()=>{    constdebugMock=jest.spyOn( (core) ,'debug')    awaitrun()    expect( (debugMock) ).toHaveBeenCalledWith('👋 Hello! You are an amazing person! 🙌')  })})

While this is cool, it probably adds complexity rather than reducing it. Still, it is helpful to get an idea of ​​how the action is being executed.

Our workflow can take advantage of the newly created property:

name:'Debug workflow'on:[push]jobs:  build:    name:Debug     runs-on:ubuntu-latest     steps:      -uses:actions / checkout @ v1         with:          fetch-depth:1      -run:npm install       -run:npm run build       -uses:./.github/actions/debug-action         with:          amazing-creature:Octocat

We’ve added awithnode to our action definition and specified the value (Octocat) ********************************************* (as our) amazing-creature. Let’s commit and push these changes to GitHub. We should see the output this time because we turned debug output on. Checkgit status:

On branch master Your branch is up todatewith'origin / master'.Changes not stagedforcommit:   (Use"git add... "to update what will be committed)  (Use"git checkout -... "to discard changesinworking directory)modified: .github / actions / debug-action / __ tests __ / debug.test.ts modified: .github / actions / debug-action / action.yml modified: .github / actions / debug-action / debug.ts modified: .github / workflows / debug-workflow.yml modified: package-lock.json modified: package.json  no changes added to commit(use"git add"and / or"git commit -a ")

That’s a lot of files. Add all of them:

And commit:

(git)  commit -m"Add amazing-creature input"

Finally let’s push to GitHub:

                 Debug output of the action shows amazing Octocat           

Action outputs

Debugging output is useful but actions are much more powerful when cha ined together. Each action can define a set ofoutputsthat can be used by subsequent actions.TheREADMEfor the GitHub Actions toolkit (core) package has some great examples of inputs, outputs, exporting variables and more.Additionally an action ca n set it’sstatus. This is especially useful for pull request workflows as the statuses can be used for automated-approval (or rejection).

Suppose we want to set an output containing our message so that other steps in our workflow can use it. We can define the output in ouraction.yml:

name:'debug-action'description:'Outputs debug information'author:'jeffrafter'inputs:  amazing-creature:    description:What kind of amazing creature are you?    default:personoutputs:  amazing-message:    description:We said something nice,this was what we said.runs:  using:'node 12 '  main:'./ debug.js'

Here we ‘ve called itamazing-messageand set thedescription. Indebug.tslet’s use it :

import*as  (core)  from'@ actions / core'construn=Async():Promisevoid>=>{  constcreature=core.getInput('amazing-creature')  constmessage=``👋 Hello! You are an amazing$ {creature}! ***``  core.debug(message)  core.setOutput('amazing-message',message)}run()export(default) ************************************ (run)

We can add a test for this:

describe('debug action output',()=>{  it('sets the action output',async()=>{    constsetOutputMock=jest.spyOn( (core) ,'setOutput')    awaitrun()    expect( (setOutputMock) ).toHaveBeenCalledWith(      'amazing-message',      '👋 Hello! You are an amazing person! 🙌 '',    )  })})

We can use the output in our workflow:

jobs:  build:    name:Debug     runs-on:ubuntu-latest     steps:      -uses:actions / checkout @ v1         with:          fetch-depth:1      -run:npm install       -run:npm run build       -uses:./.github/actions/debug-action         with:          amazing-creature:Octocat         ID:debug       -run:echo There was an amazing message-${{steps.debug.outputs.amazing-message}}

We’ve given our action anIDnode, then we refer to thatIDin our echo command. Let’s commit this. Rungit status:

On branch master Your branch is up todatewith'origin / master'.Changes not stagedforcommit:   (Use"git add... "to update what will be committed)  (Use"git checkout -... "to discard changesinworking directory)modified: .github / actions / debug-action / __ tests __ / debug.test.ts modified: .github / actions / debug-action / action.yml modified: .github / actions / debug-action / debug.ts modified: .github / workflows / debug-workflow.yml  no changes added to commit(use"git add"and / or"git commit -a ")

Add the changes:

And commit:

git  (commit -m)  "Set the debug action output"

And push it to GitHub:

Once we’ve push this to GitHub we’ll see:

                 The output message printed using echo in the action run           

Each action can have multiple outputs.

Setting status of the action

By default, if our action crashes it will fail. We can make this explicit:

construn=async():Promisevoid>=>{  try{    constcreature=core.getInput('amazing-creature')    constmessage=``👋 Hello! You are an amazing$ {creature}! ***``    core.debug(message)    core.setOutput('amazing-message',message)  }catch(error){    core.setFailed(``Debug-action failure:$ {error}``)  }}

If the exception is handled and the program can continue we can make use of the logging functions instead:

  • core.error
  • core.warning
  • core.infoActually,core.infois not exposed by default. Instead, you can just useconsole.logwhich outputs directly to the log as well. In many ways usingconsole.logis easier; but there is less built in formatting.

Catching exceptions is great, but failures can happen for other reasons. For example, suppose someone chosemosquitoas theamazing- creature. That’s not okay:

import*asCorefrom'@ actions / core'construn=Async():Promisevoid>=>{  try{    constcreature=core.getInput('amazing-creature')    if( (creature)==='Mosquito'){      core.setFailed('Sorry, mosquitos are not amazing 🚫🦟')      return    }    constmessage=``👋 Hello! You are an amazing$ {creature}! ***``    core.debug(message)    core.setOutput('amazing-message',message)  }catch(error){    core.setFailed(``Debug-action failure:$ {error}``)  }}run()export(default) ************************************ (run)

We can test this indebug.test.ts:

it('does not output debug messages for non-amazin g creatures',Async()=>{  process.  (ENV)  ['INPUT_AMAZING-CREATURE']='Mosquito'  constdebugMock=jest.spyOn( (core) ,'debug')  constsetFailedMock=jest.spyOn( (core) ,'setFailed')  awaitrun()  expect( (debugMock) ).toHaveBeenCalledTimes(***************************** (0))  expect( (setFailedMock) ).toHaveBeenCalledWith('Sorry, mosquitos are not amazing 🚫🦟')})

Get thegit status:

On branch master Your branch is up todatewith'origin / master'.Changes not stagedforcommit:   (Use"git add... "to update what will be committed)  (Use"git checkout -... "to discard changesinworking directory)modified: .github / actions / debug-action / __ tests __ / debug.test.ts modified: .github / actions / debug-action / debug.ts  no changes added to commit(use"git add"and / or"git commit -a")

Add and commit in one step:

****************************** (git)  commit -am"Set failed status for debug action when necessary"

Payloads

Actions are intended to r espond to events: when code is pushed, when a pull request is opened or updated, when someone leaves a comment, scheduled events, etc. Every action is passed a payload.

The easiest way to work with a payload is to try it out and log the payload to the console:

(import)*(as)  corefrom'@ actions / core'import*asgithubfrom'@ actions / github'construn=Async():Promisevoid>=>{  try{    constcreature=core.getInput('amazing-creature')    if( (creature)==='Mosquito'){      core.setFailed('Sorry, mosquitos are not amazing 🚫🦟')      return    }    constmessage=``👋 Hello! You are an amazing$ {creature}! ***``    core.debug(message)    core.setOutput('amazing-message',message)    console.log{payload:github.context.payload})  }catch(error){    core.setFailed(``Debug-action failure:$ {error}``)  }}run()export(default) ************************************ (run)

Notice we added an import for the@ actions / githubtoolkit:

import*asgithubfrom'@ actions / github'

Then we logged thegithub. context.payload. If you push this to GitHub to run you might see:

{  payload:{    After:'152612B7cabe  55f184249 e 24efbefb90035  (d) ********************************* (4) ************************************ (b) ********************************* (3)  d ',    base_ref:null,    Before:'e  (3) ************************************ (f)   (E3)  ADC(e)   (f)  00FDEC84******************************* (1) ************************************ (DA)  741  (D)  53 E4',    Commits:[[Object],[Object]],    compare:'https:    created:(false) ************************************ () ,    deleted:(false) ************************************ () ,    forced:(false) ************************************ () ,    head_commit:{      added:[],      author:[Object],      Committer:[Object],      distinct:true,      ID:'152612B7cabe  55f(e)efbefb90035d4  (b)  3)  d ',      message:'Use the action payload',      modified:[Array],      removed:[],      Timestamp:'2019- 09- 13T12:27:07- 07:00',      tree_id:' (5)  fa0185 e 3911be4586a(e)6e5  (e) ********************************* (2) ************************************ (c)   (e)',      url:'https:    },    Pusher:{email:'[email protected]',name:'jeffrafter'},    Ref:'refs / heads / master'        }

Thepush event documentationc an be really helpful. With this information we can make our message more personal. We’ll include the name of the person pushing the code. Utilizing the information in the (pushernode is useful but that’s only available forpushactions. If you want to know who triggered the workflow for other kinds of actions you can use theGITHUB_ACTORdefault environment variable. In this case we’ll use the value from the payload. Changedebug.ts:

import*as  (core)  from'@ actions / core'import*asgithubfrom'@ actions / github'construn=Async():Promisevoid>=>{  try{    constcreature=core.getInput('amazing-creature')    if( (creature)==='Mosquito'){      core.setFailed('Sorry, mosquitos are not amazing 🚫🦟')      return    }    constpusherName=github.context.payload.Pusher.name     constmessage=``👋 Hello$ {pusherName}! You are an amazing$ {creature}! ***``    core.debug(message)    core.setOutput('amazing-message',message)  }catch(error){    core.setFailed(``Debug-action failure:$ {error}``)  }}run()export(default) ************************************ (run)

We’ll need to change our tests as well. We’ll directly set the payload in thebeforeEach:

beforeEach(()=>{  Jest.resetModules()  const  (doc)=yaml.safeLoad( (FS) . readFileSync(__ dirname'/../ action.yml','utf8'))  Object.keys( (DOC) .  (inputs) ).forEach(name=>{    constenvVar=``INPUT _$ {name.replace(/ / g,'_').toUpperCase(}``    process.  (ENV)  [envVar]=doc.  (inputs)  [name]['default']  })  github.context.payload={    Pusher:{      name:'mona',    },  }asWebhookPayload})

We couldstore payloads as files and use thoseas well, but this approach is more readable.

(Run) git status:

On branch master Your branch is up todatewith'origin / master'.Changes not stagedforcommit:   (Use"git add... "to update what will be committed)  (Use"git checkout -... "to discard changesinworking directory)modified: .github / actions / debug-action / __ tests __ / debug.test.ts modified: .github / actions / debug-action / debug.ts  no changes added to commit(use"git add"and / or"git commit -a ")

And commit:

gitcommit -am"Use the action payload"

And push to GitHub:

It is so encouraging!

There was an amazing message - 👋 Hello jeffrafter! You are an amazing Octocat! ***

Writing output to the logs is fine. Setting the completion and failed status of the action is also cool. Automating your workflow using the API is the best. Actions can automatically create issues, pull request reviews, commits and more. To demonstrate, let’s create a new action. When a friendly contributor opens an issue in our repository our GitHub Action will thank them and add a reaction to their issue.

We’ll add the following:

.github /   actions /     thanks-action /       __tests __ /         thanks.test.ts       action.yml       thanks.js       thanks.ts   workflows /     thanks-workflow.yml

Using the API

Every action that runs has access to aGITHUB_TOKENenvironment variablethat can be used to interact with the API. The token has read and write (but not admin)repository app permissionsby default.Note, forks do not normally have access to secrets in the actions environment. Forks do have access to aGITHUB_TOKENbut again, the permissions are limitted.

To use theGITHUB_TOKENyou must configure the environment of your action when it is referenced in the workflow. Remember actions can be used by many workflows in many repositories and granting access should be protected. The workflow for our thanks action will be triggered when an issue is opened. Create. workflows / thanks-workflow.yml:

(name):Thanks workflowon:[issues]jobs:  build:    name:Thanks     runs-on:ubuntu-latest     steps:      -uses:actions / checkout @ v1         with:          fetch-depth:1      -run:npm install       -run:npm run build       -uses:./.github/actions/thanks-action         ENV:          GITHUB_TOKEN:($)  {{secrets.GITHUB_TOKEN}}        ID:thanks

Each step that makes use of theGITHUB_TOKENmust include:

env:  GITHUB_TOKEN:($)  {{secrets.GITHUB_TOKEN}}

We’ll also need a newaction.ymlfor our thanks action. Create. actions / thanks-action / action.yml:

name:'thanks-action'description:'Says thanks when a contributor opens an issue'author:'jeffrafter'runs:  using:'node 12 '  main:'./ thanks.js'inputs:  thanks-message:    description:Say thanks     default:Thanks for opening an issue ❤!

Notice that we’ve specified an input with a default message. If we wanted we could specify different messages in our workflows that use this action.

With the environment set we’re ready to create.github / actions / thanks-action / thanks.ts:

import*as  (core) ********************************* (from)'@ actions / core'import*asgithubfrom'@ actions / github'construn=Async():Promisevoid>=>{  try{         if( (github) .context.  (payload) .action (*********************************!=='opened')return         constissue=github.context.payload.issue     if(!issue)return    constToken=process.  (ENV)  ['GITHUB_TOKEN']    if(!Token)return         constoctokit:github.GitHub=newgithub.GitHub(token)    constnwo=process.  (ENV)  ['GITHUB_REPOSITORY']||'/'    const[owner,repo]=nwo.split('/')              constthanksMessage=core.getInput('thanks-message')    constissueCommentResponse=awaitoctokit.  (issues) .createComment({      owner,      repo,      issue_number:issue.number,      body:thanksMessage,    })    console.log``Replied with thanks message:$ {issueCommentResponse.data.url}``)              constissueReactionResponse=awaitoctokit.  (reactions) .createForIssue({      owner,      repo,      issue_number:issue.number,      content:'heart',    })    console.log``Reacted:$ {issueReactionResponse.  (data) *********************************content}``)  }catch(error){    console.error(error.message)    core.setFailed(``Thanks-action failure:$ {error}``)  }}run()export(default) ************************************ (run)

Let’s break this down.

Usingissuesevents to trigger our workflow allows us to respond to newly opened issues. However, every change to an issue will trigger our workflow: when an issue is opened, closed, edited, assigned, etc. Because of this we want to make sure our action is only making changes when the issue is opened:

if( (github) .context.  (payload) .action (*********************************!=='opened')return

We’ll need to access Theissuein the payload:

constissue=github.context.payload.issueif(!issue)return

At this point we grab the token that was injected into the environement from our workflow:

(const)  Token=process.ENV['GITHUB_TOKEN']if(!Token)return

We use the token to create a new GitHub client:

constoctokit:github.GitHub=newgithub.GitHub(token)

(The client that is created is actually an) octokit / rest.jsAPI client. Theoctokitclient has full access to TheRest API V3.It is possible to create anoctokit /graphql.jsinstance to access the V4 GraphQL API as well. See the documentation for working with custom requests.There is great documentation available:

  • (octokit / rest.js)
  • @ actions / github
  • (Rest API V3)
  • Once you have aoctokitclient you’ll usually want to work with the current repository. There are a set of automatically included environment variables to make this easier. For example, theGITHUB_REPOSITORYenvironment variable contains the repository name with owner (NWO) likejeffrafter / example-github-action-typescript:

    constnwo=process.env['GITHUB_REPOSITORY']||'/'const[owner,repo]=nwo.split('/')

    At this point we’re ready to create a comment replying to the opened issue. We grab thethanks -messagefrom the action input. Then we create the comment via theoctokit / rest.jsclient:

    constthanksMessage=core.getInput('thanks-message')constissueCommentResponse=awaitoctokit.  (issues) .createComment({  owner,  repo,  issue_number:issue.******************************** (number),  body:thanksMessage,})console.log``Replied with thanks message:$ {issueCommentResponse.data.url}``)

    Calling the API requires HTTP interactions which are not instant. Because working with the API involves asynchronous callbacks, most API calls will return aPromisecontaining a response object (withdata,status, andheaders). if If you don’t care about the result, you can ignore the response and continue on. If you need to use the response, however, you’ll need to useawaitto let theAsyncrequest complete. Here we are logging out the comment URL from the response so we need to useawaitto make sure the response is complete.

    We also want to add a reaction to the issue:

    constissueReactionResponse=awaitoctokit.  (reactions) .createForIssue({  owner,  repo,  issue_number:issue.number,  content:'heart',})console.log``Reacted:$ {issueReactionResponse.  (data) *********************************content}``)

    Again we useawaitto wait for the response from the API call.

    Testing API interactions

    Whem working with the API it is important to configure your tests so they don’t actually interact with GitHub’s API. In general, you don’t want your tests to call the API directly; they might start creating real issues in your repositories or use up your rate limits. Instead you should be mocking all of the external calls from your test suite. This will also make your tests run faster.

    It is common to usenockto mock external requests and responses. Install it along with the supporting types:

    NPMinstall- save-dev nock @ types / nock

    There are great examples available in the@ actions / toolkitrepository onmocking the octokit clientand in the (nock)README.

    By default, we want to disable all external calls from our test suite. To do this add the following to the top ofjest.config.js:

    (const) ************************************ (nock)=(require)(' nock ')nock.disableNetConnect()

    Now if one of our tests attempts use the API nock will prevent it and fail the test with an error like:

    request to https://api.github.com/repos/sample/repository/issues/1/reactions failed, reason: Nock: No matchf orrequest

    Let’s create a new test. Create. actions / thanks-action / __ tests __ / thanks.test.ts:

    import*(as) ************************************ (github)  from'@ actions / github'import{ (WebhookPayload) }from'@ actions / github / lib / interfaces'import  (nock)  from'nock'importrunfrom'../ thanks'beforeEach(()=>{  Jest.resetModules()  github.context.payload={    Action:'opened',    issue:{      number:(1),    },  }asWebhookPayload})describe('thanks action',()=>{  it('adds a thanks comment and heart reaction',Async()=>{    process.  (ENV)  ['INPUT_THANKS-MESSAGE']='Thanks for opening an issue ❤!'    process.  (ENV)  ['GITHUB_REPOSITORY']='example / repository'    process.  (ENV)  ['GITHUB_TOKEN']='12345 '    nock('https://api.github.com')      .post(        '/ repos / example / repository / issues / 1 / comments',        body=>body.  (body)==='Thanks for opening an issue ❤!',      )      .reply(200,{ (url) :'https://github.com/example/repository/issues/1#comment'})    nock('https://api.github.com')      .post('/ repos / example / repository / issues / 1 / reactions',body=>body.content==='heart')      .reply(200,{content:'heart'})    awaitrun()  })})

    We start off by setting up a fake payload. The real payload (when GitHub runs our action) will be much bigger and contain more informationWhen you are first working with a GitHub Action it is sometimes helpful to start with aconsole.logfor the whole payload. You can copy the output from the logs and use it as your default payload while testing. Additionally, theEvent Types & Payloadsdocumentation contains sample payloads if you can’t use theconsole.logtrick.; however we’ve made our example payload in the test as small as possible to keep things focused.

    The test that we’ve created does nothing more than attempt to run our action. We’re not verifying any output or debug information (though we could). Instead we are validating that the API endpoints are hit with specific parameters. If these mocked API requests don’t occur (as we have specified them), the test will fail:

    it('adds a thanks comment and heart reaction',(Async)()=>{  process.  (ENV)  ['INPUT_THANKS-MESSAGE']='Thanks for opening an issue ❤!'  process.  (ENV)  ['GITHUB_REPOSITORY']='example / repository'  process.  (ENV)  ['GITHUB_TOKEN']='12345 '  nock('https://api.github.com')    .post(      '/ repos / example / repository / issues / 1 / comments',      body=>body.  (body)==='Thanks for opening an issue ❤!',    )    .reply(200,{ (url) :'https://github.com/example/repository/issues/1#comment'})  nock('https://api.github.com')    .post('/ repos / example / repository / issues / 1 / reactions',body=>body.content==='heart')    .reply(200,{content:'heart'})  awaitrun()})

    Notice that we are also specifying the response body. This allows our action code to utilize the response exactly as it would from a real API interaction. Again, when developing your action you might useconsole.logto see what the actual output looks like before setting up your tests.

    Some might argue that this level of mocking for your tests is too much. We’re faking the input, faking the API endpint and faking the responses. So what is this test even doing? The approach here is one of efficiency. I’m trusting that the GitHub API works and that the way I’ve set it up won’t change. With those assumptions set in my tests, I’m free to change the code that leads up to those interactions in any way I see fit. I’ve mocked the edges, but my actions code still must do the right thing. It’s a trade-off but once you’ve established how the edges of your code work it allows much faster iteration.

    If we run the tests with (npm test) we see:

    PASS .github / actions / debug-action / __ tests __ / debug.test.ts   debug action debug messages     ✓ outputs a debug message((ms) )    ✓ does not output debug messagesfornon-amazing creatures(2ms)  debug action output     ✓ sets the action output( (1ms) ) PASS .github / actions / thanks-action / __ tests __ / thanks.test.ts   thanks action     ✓ adds a thanks comment and heart reaction((ms) ) Test Suites:2passed,  (2)  total Tests:4passed,  (4)  total Snapshots:0total Time:2. 194 s Ran alltestsuites.   console.log .github / actions / thanks-action / thanks.ts: 30     Replied with thanks message: https://github.com/example/repository/issues/1    console.log .github / actions / thanks-action / thanks.ts: 40     Reacted: heart

    At this point the action works. Let’s checkgit status:

    On branch master Your branch is up todatewith'origin / master'.Changes not stagedforcommit:   (Use"git add... "to update what will be committed)  (Use"git checkout -... "to discard changesinworking directory)modified: jest.config.js modified: package-lock.json modified: package.json  Untracked files:   (Use"git add... "to includeinwhat will be committed).github / actions / thanks-action / .github / workflows / thanks-workflow.yml  no changes added to commit(use"git add"and / or"git commit -a ")

    Let’s add all of those:

    And commit:

    gitcom mit -m"Thanks action"

    And push to GitHub:

    When we pushed, our (debug-action) still executed, but not our newthanks- action. In order to trigger that we have to open a new issue. Open a new issue with any message and then watch the action execute. You should see something like:

                     A bot replying with a thanks message           

    It works! But… it doesn’t feel very personal to have a bot replying to collaborators. It would feel much better if a human were replying. Unfortunately all of the interactions with the repository are on behalf of the GitHub Actions bot because we are using the (GITHUB_TOKEN) . In order to act on behalf of another user we’ll need to use a different token. o do this, we’ll generate apersonal access token.Note: personal access tokens are powerful things and should be kept secret. They allow applications (and GitHub actions) to impersonate you and act on your behalf. You should never check a personal access token into version control or share it on the Internet. If you’ve accidently done that, go to your settings and delete the token to revoke access.

    To create a token, go to yourtoken settingsin GitHub (click on (Settings) in the user drop-down menu, thenDeveloper settingsin the sidebar, then click onPersonal access tokens). Then click theGenerate new tokenbutton .

                     Creating a thanks-action personal access token           

    Make sure you’ve checked theREPObox to grant repository access permissions to the token. Copy the token (note, this is just an example and this token has been revoked so you can’t use it):

                     Copy the personal access token           

    Next, we’ll need to add a new secret to our repository. Open the setings for your repository and clickSecretsin the sidebar. ClickAdd a new secretand set the name toTHANKS_USER_TOKENand paste the copied personal access token into theValue. ClickAdd secret.Repository secrets are extremely powerful. We can use them to configure keys to external services, setup CI and control our environment. For more information seeCreating and using secrets.

                     Action secrets           

    Now that we’ve created a new secret containing our token we need to use it. To use it, we’ll need to modify our workflow. Right now theenvnode in our workflow specifies theGITHUB_TOKEN.

    ENV:  GITHUB_TOKEN:($)  {{secrets.GITHUB_TOKEN}}

    Let’s add an entry for theTHANKS_USER_TOKEN:

    env:  GITHUB_TOKEN:($)  {{secrets.GITHUB_TOKEN}}  THANKS_USER_TOKEN:($)  {{secrets.THANKS_USER_TOKEN}}

    This will inject the secret into our environment. We’ll need to modifythanks.tsto use it. Currently we have:

    constToken=process.ENV['GITHUB_TOKEN']

    Let’s change that to:

    consttoken=process.['THANKS_USER_TOKEN']||process.env['GITHUB_TOKEN']

    That’s it. At this pointnpm testshould still pass andnpm run lintshould have no warnings or errors. Let’s check thegit status:

    On branch master Your branch is up todatewith'origin / master'.Changes not stagedforcommit:   (Use"git add... "to update what will be committed)  (Use"git checkout -... "to discard changesinworking directory)modified: .github / actions / thanks-action / thanks.ts modified: .github / workflows / thanks-workflow.yml  no changes added to commit(use"git add"and / or"git commit -a ")

    And commit:

    gitcommit -am"Make it more personal"

    Push it to GitHub:

    Open a new example issue and you should see your user account reply:

                     A more personal response from a GitHub Action           

    Lots of folks @GitHub helped review and solve some of the issues I came across while writing this post. Special shout-outs go to@ jasonetco,@ mscoutermarsh, and@ mikekavouras. Also, special thanks to the docs team and the octokit / rest.js team who make great things.

    Brave Browser
    Read MorePayeer

    What do you think?

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    GIPHY App Key not set. Please check settings

    Elon Musk Unfazed by Daimler's EV Push, but Should He Be? – CCN.com, Crypto Coins News

    Elon Musk Unfazed by Daimler's EV Push, but Should He Be? – CCN.com, Crypto Coins News

    Startups still love Slack but big companies prefer Microsoft Teams, Recode

    Startups still love Slack but big companies prefer Microsoft Teams, Recode