Automate Anything with GitHub Actions
11 August 2020
You can take a look at the Action we build in this post here on GitHub. I've also included an action that's responsible for building and updating the action
dist
which may also be of interest. It's all in this Repository
A few weeks ago I was playing around with GitHub actions and the recently introduced GitHub Account README functionality and wanted a way to make this "static" file a bit more dynamic
Enter GitHub Actions. GitHub actions are a way for you to define and run programmatic tasks. If you can put it in code, then you can run it as an action
Usually, you'll be using actions that have been defined by GitHub or another developer for common tasks, such as running your application CI or deployment processes. However, since actions are extremely generic, we're going to do something a little different - update our Twitter bio?
In this post, we're going create a GitHub action that uses the Twitter API to update our bio, as well as make the action we created run automatically on GitHub
- Create a GitHub Repository for us to work in
- Set up Twitter Developer Credentials
- Write a Node.js script that uses the Twitter API
- Configure our GitHub Action Metadata
- Add our Twitter Secrets in GitHub
- Configure a GitHub Action that will run our script (an action, within an action)
Prerequisites
- Git
- Node.js
- Visual Studio Code or any other Code Editor
Create a GitHub Repo
For us to run our action we'll need a GitHub Repository to use, to create a Repo go to GitHub and sign in, thereafter go the 'Create a new repository page' and fill in the details, be sure to select Initialize this repository with a README
, pick Node
as the .gitignore
file, and select a license if you'd like to
Once you've done that, click Create repository
and you should see the initial files we added to the Repo. Next, click on the Code
button and copy the URL in the text box
Now, from a terminal, you will need to clone the repository. Run the following command and make sure to paste the link you just copied in place of <YOUR URL>
below:
git clone <YOUR URL>
Next, open the folder you just cloned in your code editor and create a new file called .env
in the folder root directory. We'll keep our Twitter credentials in this file for testing. For now, just add the following to the file:
.env
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_KEY=
TWITTER_ACCESS_SECRET=
In the next step, we're going to get the credentials from Twitter, we'll add them into the file above when we're done
Get Some Twitter Cred.
Twitter exposes the Twitter Developer API that allows us to do all kinds of useful and pointless things by interacting with Twitter's data
For us to consume the Twitter API we require credentials for the API. To set these up we'll need to do a few things
First, open your browser on the Twitter Developer Portal and click the sign-in button. Then, once you've signed in, you should see your name on the top right of the page, click on the dropdown arrow and select Apps
to go to the App Dashboard
On the App Dashboard, click Create an app
and fill in the required information. For the Website URL
you can put the URL of your GitHub repository that you copied previously into this field. Once you've filled in all the details click Create
at the bottom of the page
You should now see the Details Page for the app we just created. Next, click on the Keys and Tokens
tab and click Generate
then copy and paste each token after the =
in the .env
file we created without any spaces before or after the key
When you've pasted your tokens into their places, your .env
file should look something like this:
Note that using the template for
.gitignore
that we chose, the.env
file will be automatically ignored. Don't publically upload this data just anywhere as it can potentially give someone access to your Twitter account
.env
TWITTER_CONSUMER_KEY=xxxxxxxxxxxxxxxxxxxxxx
TWITTER_CONSUMER_SECRET=xxxxxxxxxxxxxxxxxxx
TWITTER_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
TWITTER_ACCESS_SECRET=xxxxxxxxxxxxxxxxxxxxx
(With the actual keys, and not just xxxx
)
Now that we've got our credentials set up, we can start to work on the application
Using the Twitter API
We're going to be writing a script that runs on Node.js (JavaScript) and makes use of the Twitter API using the twit
library for Node.js and the GitHub libraries for working with GitHub actions
To get started, run the following command from a terminal within your repository's directory to initialize a new Node.js project and select the defaults for all the questions:
npm init -y
Next, we'll add the dependencies that our application is going to need to run:
@actions/core
allows us to interact with the data provided to our action by GitHubtwit
is used to interact with the Twitter APIdotenv
enables us to load in our.env
file so our application can use it
npm install @actions/core twit dotenv
Once the application is done installing we'll create an index.js
file inside of our repo folder and we can get started on our code
Inside of the index.js
the first thing we'll want to do is import our environment variables from the .env
file we configured, we'll do that using the dotenv
NPM Package we installed previously:
index.js
require('dotenv').config()
Next, we'll configure a new Twitter
API Client using our environment variables. The twitter
Package exports a Twitter
class that we can create an instance of, do to this we will use the Twitter
constructor and get our environment variables that are all stored in the process.env
variable:
index.js
const Twitter = require('twitter')
const client = new Twitter({
consumer_key: process.env.TWITTER_CONSUMER_KEY,
consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
access_token_key: process.env.TWITTER_ACCESS_KEY,
access_token_secret: process.env.TWITTER_ACCESS_SECRET,
})
Now that we've got an instance of a Twitter
client that we can use to interact with the API we'll want to use it
The Twitter API Client runs asynchronously, for us to work with it correctly we have two options:
- Using the
callback
method, which means that we give theclient
a function to run when it's done sending the data to Twitter. While this is easier in our specific circumstance it can become difficult to keep track of when we have many different callback functions - The second option is to use
async
andawait
syntax, which allows us to write our code more sequentially and makes it easier for us to control our sequence and flow
To use the async/await
functionality, we need the code that we need to await
to be inside of an async
function. In this one function, we can await
as many tasks as we want sequentially without worrying about callbacks
We'll create a main
function that is async
in which we will do any asynchronous interactions, in our case - interact with Twitter
After the code we've already got in our index.js
file, define the main
function as follows:
index.js
const main = async () => {
// this is where we'll work with the twitter client
}
Next, we'll add the code to interact with the Twitter API inside of this function. To update our Profile Bio (or description
as it's called in the Twitter API) we'll make use of the account/update_profile
endpoint
The data we send the Twitter API will need to be an object with a description
property for what we want to set our Twitter Bio as. We'll await
this function call by adding it within our main
function:
index.js
const main = async () => {
const response = await client.post('account/update_profile', {
description: 'Hello, World!',
})
console.log(response)
}
The above code will set our Twitter description
to Hello, World!
and then print our the response
we get back from Twitter
The way we've written the above code is a little bit dangerous as we aren't handling any potential errors/failures that may happen when interacting with the Twitter API
When working with an API via an HTTP Request there's always the chance that there could be an error. Errors can be caused by anything ranging from poor network connections, incorrect credentials, or a system outage on the API host itself
For our application to give us a bit more information about what happened, we may want to handle the error before passing it on to the process that kicked off our script. We'll make use of a try/catch
to handle this error:
index.js
const main = async () => {
try {
const response = await client.post('account/update_profile', {
description: 'Hello, World!',
})
console.log(response)
} catch (error) {
console.error(error)
throw new Error(
`The Twitter API Responded with an Error: ${error[0].code}, ${error[0].message}`
)
}
}
You can see above that we're just doing some cleanup of the error message before throwing the exception to the process
Now that we've fully defined our client
and main
functions we can call the function as the last line of the script:
index.js
main()
This should make a request to the Twitter API and print our the response
or error
if there is one
So, the above function will always set our Twitter Bio to the same value, this isn't interesting. Next, we'd like to get some data from the workflow that's going to be running our action. The way we would do this is with an input
Our action is going to take an input
called bio
. We can use the @actions/core
library to get the value of the input
. We can do this with the following:
index.js
const core = require('@actions/core')
const bio = core.getInput('bio') || 'Hello, World!'
The above code also sets a default value of Hello, World!
to the description, this will allow us to also run the action without throwing an exception on our local machine as well as if the bio
is not provided to the action
Furthermore, instead of just throwing errors, we can rather set the error-status using the core.setFailed
function. Updating our main
function to do this we now have:
index.js
const main = async () => {
try {
const response = await client.post('account/update_profile', {
description: bio,
})
console.log(response)
} catch (error) {
console.error(error)
core.setFailed(
`The Twitter API Responded with an Error: ${error[0].code}, ${error[0].message}`
)
}
}
Finally, our full index.js
file should look like this:
index.js
re('dotenv').config()
const Twitter = require('twitter')
const core = require('@actions/core')
const bio = core.getInput('bio') || 'Hello, World!'
const client = new Twitter({
consumer_key: process.env.TWITTER_CONSUMER_KEY,
consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
access_token_key: process.env.TWITTER_ACCESS_KEY,
access_token_secret: process.env.TWITTER_ACCESS_SECRET,
})
const main = async () => {
try {
const response = await client.post('account/update_profile', {
description: bio,
})
console.log(response)
} catch (error) {
console.error(error)
core.setFailed(
`The Twitter API Responded with an Error: ${error[0].code}, ${error[0].message}`
)
}
}
main()
Now that we've written the functionality for our action we'll want to turn it into an action
Configure the Action Metadata
For GitHub to recognise our code as an action, we need to create an action.yml
file that contains a description of our action. Our action.yml
file needs to have the following information:
name
for our actiondescription
of the action itself- an
input
parameter ofbio
that will be used by our application runs
which states the script to run
The action.yml
file for our action looks like the following:
action.yml
name: 'Twitter Bio Update'
description: 'Update your Twitter Account Bio'
inputs:
bio:
description: 'Text that you would like to set as your Twitter Bio'
required: true
default: 'Hello, World!'
runs:
using: 'node12'
main: 'index.js'
The fields we've got above are all pretty much required. The above fields are the simplest configuration for a Node.js action. The GitHub Docs have a lot more information on more complex configurations and actions
Now that we've defined our action, we will want to configure it to run. But before we can do that, we'll want to set up our secrets
Setting Up Secrets in GitHub
Now that we've got our action defined, we're almost ready to write a Workflow that will use this action. However, our action requires environment variables (that we've got saved in our .env
file) but we don't want these to be pushed to GitHub as part of our source code. The way to set up these environment variables in GitHub is called a secret
To add our environment variables in GitHub you'll need to open your Repo on GitHub and navigate to Settings > Secrets
then click New secret
and add your first secret. If we use our .env
file as a reference we'll want to create a secret for each line in the file. To do this look at the name of the environment variable (everything before the =
) and set this as the Name
for the secret, then look at the value (everything after the =
) and set this as the Value
for the secret then click Add secret
. Do this for every line in your .env
file (every environment variable`
Create a Workflow
Workflows are GitHub's way of tying together a bunch of actions to run. Often, we will want to run multiple actions. We place these into what's called a step
in a job
. Each Workflow can have multiple steps
and jobs
and a repository can have multiple workflows
The structure of a workflow is something like this:
workflow
|-- Job 1
| |-- Step 1
| |-- Step 2
|-- Job 2
|-- Step 1
A Workflow can have any number of jobs and steps
We're going to create a Workflow with a single job with the goal of running our script. For us to do this, the job will need to do the following:
- Checkout our code
- Configure Node.js and NPM
- Install Dependencies
- Run our Action
To do Steps 1 and 2 we're going to use actions that are defined by GitHub. We're going to go through our Workflow file working from the top-down as this is the order in which everything will be run
Firstly, we'll need to create a new directory in our Repository named .github
and inside of this another called workflows
. Inside of the .github/workflows
directory create a file named main.yml
(this can be any name so long as it's a yml
file)
Next, on the first line of this file we will define a name for our workflow like so:
main.yml
name: Run Twitter Bio Action
This is the name that will be displayed when the workflow runs. Actions are run when a specific GitHub event happens (more about that in the Docs). We're going to configure our action to run manually only, so we will use the workflow_dispatch
event. After the name
in the main.yml
file, add the following:
main.yml
on:
workflow_dispatch:
inputs:
bio:
description: 'Twitter Bio'
required: true
In the above, we set an on
event for workflow_dispatch
with an input
for bio
that we're going to pass on to our action
Now that we've got the workflow metadata defined, we're going to add a job
. To do this, we'll add a jobs
object with a name of update-bio
, a specification on what OS it needs to run on, and the steps
that will be a part of it:
main.yml
jobs:
update-bio:
runs-on: ubuntu-latest
name: Update Twitter Bio
steps:
# we'll add the steps here in a moment
We can see above that we're running on the ubuntu-latest
OS. Next, we'll add the first step for checking out our code in the action. This will use actions/checkout
:
main.yml
steps:
- name: Checkout
uses: actions/checkout@v2
In the above, we give our step a name
which is for display purposes, and a uses
which says what action this step should use. In our case, we're checking out our code using the actions/checkout@v2
action
Next, we'll want to configure Node.js and NPM because our action needs these to run. We can use the actions/setup-node@v1
for this:
main.yml
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
In this action, we use the with
to state an input
that the action needs. In this case, we specify that we want to use a node-version
of 12.x
Now we've got Node.js and NPM, we need to install the dependencies for our action to run. We do this with the npm install
command like so:
main.yml
- name: Install Dependencies
run: npm install
And lastly, we will configure our action to run with the following:
main.yml
- name: Run Action
uses: ./
env:
TWITTER_CONSUMER_KEY: $
TWITTER_CONSUMER_SECRET: $
TWITTER_ACCESS_KEY: $
TWITTER_ACCESS_SECRET: $
with:
bio: $
In this last step we're doing quite a few things:
- We're setting a
name
for our action - We set a
uses
to be./
which means we want to run the action at the root of our Repo (ouraction.yml
) - We set the
env
, these are the different environment variables we want to pass to the application. We use${\{ ... }\}
to mean that it's a variable, and we usesecrets.variable
to access the variable from the secrets we configured in the repository - We use the
with
to set thebio
from the input we give to the workflow on ourworkflow_dispatch
event. GitHub exposes this in thegithub.event.inputs.bio
variable
With the Run Action
step added, we've got a full workflow. The overall main.yml
file should have the following:
main.yml
name: Run Twitter Bio Action
on:
workflow_dispatch:
inputs:
bio:
description: 'Twitter Bio'
required: true
jobs:
update-bio:
runs-on: ubuntu-latest
name: Update Twitter Bio
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install Dependencies
run: npm install
- name: Run Action
uses: ./
env:
TWITTER_CONSUMER_KEY: $
TWITTER_CONSUMER_SECRET: $
TWITTER_ACCESS_KEY: $
TWITTER_ACCESS_SECRET: $
with:
bio: $
Run the Action
To run the action, we first need to get everything to GitHub. From Repo directory, in the terminal, run the following commands:
git add .
git commit -m "I made a GitHub Action!"
git push
Then, go to your repository on GitHub and click on the Actions
tab. You should then see your Workflow listed. Click on your workflow name, and then the Run workflow
dropdown. Fill in your Twitter Bio
, wait for the workflow to complete and look at your Twitter profile!
From GitHub, you are also able to inspect and view any logs or errors from a Workflow run. If the workflow fails you can also take a look at the output that was thrown by the step that resulted in the failure
Summary
And that's about it. We've taken a look at how you can use GitHub actions to automate a pretty silly task, but there's a lot more to using GitHub Actions, and the sky's the limit in terms of what you can use them for
Overall, GitHub actions aren't too different from similar options like Azure Pipelines, Jenkins, or any other CI service. What makes them so useful is how easily they hook into the source control system and how well they work and are supported within the GitHub ecosystem
That all being said, they're not the easiest things to configure and it can take a while to get them right if you're doing something especially complex. Overall they're pretty cool and are a pretty good place to get started with automating tasks and working with things like continuous integration and deployments and I'd definitely recommend giving them a shot