TLDR; They are simply repository scoped event listeners.
Ever been bored having to go through others code when working in open source projects, or having to always manually test people's code before you merge their pull requests or even having to manually update issues? Well the simple solution is GitHub actions. It is basically an automation tool for tests, builds, deployments and anything else you might want to do to your repo.
Creating Your First action
With GitHub actions you set up jobs to be run when certain events are triggered. Well let's dive in. This write up presumes you have a solid knowledge of git, GitHub, issues, and pull requests.
Step One: Create a GitHub repo
For this, it is better to create a new repo where you can try things out without having to worry about breaking anything.
- Go to GitHub. If you don't have an account then this is not for you. Leave this page immediately
- Create a new repository
- For consistency you can name it
mygithubactions
which is the name I used for mine and will be referencing throughout this article.
Step Two: Create a workflow
A GitHub Actions workflow is defined in a YAML file (usually with a .yml
or .yaml
extension) and placed in the .github/workflows
directory of your repository. Here's a breakdown of how to define one:
-
Create the workflow file
- Navigate to your repository root directory (either on your device or on github)
- Create a directory name
.github
- Inside
.github
create another directory nameworkflows
- Inside the workflows folder we'll create the workflow file. Create a file name
github_actions_demo.yml
or Simply navigate to the actions tab and click new action
Define the workflow
Inside the YML file, you'll define the workflow. Copy and paste the code below into the github_actions_demo.yml
# Demo workflow
name: GitHub Actions Demo
run-name: ${{github.actor}} is testing out GitHub Actions 🎉🎆🎇
on: push
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "1️⃣ This job was automatically triggered by a ${{github.event_name}} event"
- run: echo "2️⃣ This job is now running on a ${{runner.os}} server hosted by GitHub!"
- run: echo "3️⃣ The name of your branch is ${{github.ref}} and your repository is ${{github.repository}}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "4️⃣ The ${{github.repository}} repository has been cloned to the runner."
- run: echo "5️⃣ The wrokflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{github.workspace}}
- run: echo "6️⃣ This job's status is ${{job.status}}."
Step Three: Commit and push your workflow
Commit the changes you've made and push it to the main branch. The action will also run from this point since it was set to run on push. Click on the actions tab
You should see your newly created action either running or already completed running.
There you can check out the steps the job took to complete the action. If your action ever fails this is also a good place to debug your action.
Congratulations 🎉. You just created your first github workflow.
How Workflows Work
A workflow is a bunch of tasks you want to be executed on some event. It is specified in a YAML file i.e a file with .yml
extension. The workflows must be placed in .github/workflows/
directory within your GitHub repository.
Key Arguments
Let's take a look at the key arguments in a workflow file
name
The name argument specifies the name of the workflow. It is optional and when not specified, the name of the file will be used as the name of the workflow.
run-name (optional)
The name for workflow runs generated from the workflow. It is shown in the list of workflow runs in your repo's actions tab.
on (required)
This specifies the events that trigger the workflow. You can use various events like push, pull_request, schedule (for scheduled workflows), issues, issue_comment, and many more. You can also use filters to target specific branches, tags, or file paths.
# Run on push or pull request event for all branches
on: [push, pull_request]
# Run on push or pull requests made only in the main branch
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs (required)
This specifies the jobs to be run. Each job by default is run independently on its own virtual machine and concurrently (they are all run at the same time). However you can also make a job dependent on another.
jobs:
Test-Code:
...
Notify-admin:
...
runs-on (required)
This is where the runner is specified. The runner simply means which device you want your job to be run on. You can choose between ubuntu, mac and windows.
runs-on: ubuntu-latest
steps (required)
This contains the individual actions that will be performed by the job. Each item nested here is a separate action or shell script.
steps:
...
...
Actions/Scripts (required)
Nested within the steps are the individual scripts or actions that should be performed by the job.
- name (optional): name of the action
- run: run a script on the runner terminal
- uses: run an action
- with: assigns values to action parameters
steps:
- name: Print Hello
run: echo "Hello and Welcome to ${{github.actor}}'s repository"
- name: Print Machine name
run: echo "This job is running on a ${{runner.os}}"
- name: Clone the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: List the files in the repository
run: ls ${{github.workspace}}
- name: Display job status
run: echo "The job status is ${{job.status}}"
In this example we print hello and the machine name, the checkout action clones our repo unto the virtual machine, we list the files in the repo and then print the status of the job. There are many more actions you can use in your workflows. For a full overview of them all check out GitHub Marketplace.
Contexts such as github.workspace
or runner.os
are provided by default in our workspace. Some keywords available are
- github
- runner
- env
- job
- secrets
For a comprehensive list and explanation of the contexts, take a look at Accessing contextual information about workflow runs.
It is also possible to create you own actions to do your bidding but that will not be covered in this article. See Creating Actions to learn how to creating your very own actions.
Examples
The easiest way to learn is by doing
Let's go ahead and create two simple workflows that we can use in our projects. If anything not pertaining to the workflow is confusing please do your personal research.
Sample 1: Code Linter
We will create a workflow which will test our code and ensure it is properly formatted. This workflow will be triggered on a push to our dev branch. For this example I will be using our existing mygithubactions
repo.
Objectives
- Run when a push is made to the dev branch
- Ensure the code is properly formatted
- Run the test on node v20.
- If the everything passes create a pull request to the main branch
- If it fails simply echo "Action Failed"
Steps
1.
Setup the repo
For this example we'll create a simple example repo.
- Open up your terminal
- Navigate to the root directory of your repository
- Run npm init -y
- Run npm i --D is-odd
- Create an index.js file and write some basic javascript
js console.log("Hello World");
This should be enough for this example
2.
Create the code_linter.yml
file.
3.
Add the name for our workflow
name: Code Linter
run-name: Check code for proper formatting
4.
Set the events that trigger our workflow
on:
push:
branches: dev
5.
Create the job and runner
jobs:
Lint-The-Code:
runs-on: ubuntu-latest
6.
Define the steps
steps:
- name: Checkout the repository code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use node js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Lint the code
uses: super-linter/super-linter@v7.2.1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
VALIDATE_JAVASCRIPT_PRETTIER: true
- name: Test the code
run: npm test
- name: Create Pull Request if Lint Passes
if: success()
run: gh pr create -B main-branch -H dev --title "Merge dev into main-branch" --body "Created by Github Action"
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Comment when Lint fails
if: failure()
run: echo "Lint failed"
In this action we set it to run on a push event to a branch named dev.
- The action will first clone the repository onto the virtual server using the checkout action.
- Next we setup the node environment and specify the version using node-setup.
- Next we install dependencies which is the
is-odd
package. - Next it uses super-linter to scan all JavaScript files in the entire codebase to ensure it is properly formatted. Super-linter can do much more but for this scenario this is all we're using it for. It required our GitHub token to access our codebase.
- Next if the previous run was successful create a pull request. By default GitHub hosted runners have GitHub cli installed so we will make use of it rather than an external action to create the pull request.
- Should a failure occur at any point we simply echo "Lint failed". Take it as a challenge to make something more useful here for when the lint fails.
NOTE: The workflow will fail if there is already an existent pull request. So ensure there are no current pull requests or edit the workflow to first check for pull requests.
NOTE: You need to enable actions to create pull requests in settings.
- Click settings
- Click actions is the sidebar
- Scroll down and toggle "Allow github Actions to create and approve pull requests"
- Click the save button
7.
Commit your changes and push it to the repo. If you're creating it on the github page, ensure you rename the file to code_linter.yml
. Then click the commit changes at the top right. You can commit it to the main-branch branch.
If you're using your editor, simply open git bash, commit your change and push it to the dev branch.
NOTE: Pushing it to the dev branch will also trigger it as that was the event set.
8.
Now lets test out our workflow. Make some changes to your readme or any other file in your repository, commit it and push it to the dev branch.
- Click the actions tab button. There you will see the running action. Once it completes a pull request should be created. If a pull request was not created, try debugging it or going through the steps that the action took to see what is causing the error.
Sample 2: Auto Assign
In projects where communication does not happen so frequently, it is possible to have two people unknowingly working on the same issue only to later find out that they both implemented the same thing. This is why issues need to be assigned to a person before they begin work on it.
In this workflow we'll automatically assign issues to contributors who comment the trigger word "!take" or "!assign" in a new comment. This way everyone will be aware that someone has started work on that issue.
Objectives
- When a user comments !take, the action will assign that issue to them if they are the first to request it.
- If already assigned to someone else the action will comment that its already assigned and tell them to use !support.
- When a user comments !support, the action will add them to the assignees for that issue to support in fixing the issue.
- If no user is already assigned, the action will tell them to use !take
- When a user comments !drop, the user will be removed from the issue.
To get started we'll be using the same mygithubactions repo we've been using.
Steps
1.
Create an issue with the label bug report.
2.
Create the action
name: Auto Assign Issue
run-name: Auto assign issues to members who want to handle it
on:
issue_comment:
types: [created]
jobs:
Assign:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const commentBody = context.payload.comment.body;
const issue = context.payload.issue;
const commenter = context.payload.comment.user.login;
if (commentBody.includes('!take')) {
if (issue.assignees.length === 0) {
await github.rest.issues.addAssignees({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
assignees: [commenter]
});
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Issue assigned to @${commenter}!`
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Issue already taken by @${issue.assignees[0].login}. Use !support to assist.`
});
}
} else if (commentBody.includes('!support')) {
if (issue.assignees.length > 0 && !issue.assignees.some(assignee => assignee.login === commenter)) {
await github.rest.issues.addAssignees({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
assignees: [commenter] // Add the supporter as an assignee
});
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `@${commenter} is now supporting this issue.`
});
} else if (issue.assignees.some(assignee => assignee.login === commenter)){
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `@${commenter} is already assigned to this issue.`
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `@${commenter} No one has taken this issue yet. Use !take to claim it.`
});
}
} else if (commentBody.includes('!drop')) {
if (issue.assignees.some(assignee => assignee.login === commenter)) {
await github.rest.issues.removeAssignees({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
assignees: [commenter]
});
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `@${commenter} has dropped the issue.`
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `@${commenter} is not assigned to this issue.`
});
}
}
This code uses the GitHub-Script action which allows us to run JavaScript code on the runner.
NOTE: In order for this code to run, you have to enable read and write access for your actions.
Go to settings on your repo
Click on actions in the sidebar
Scroll down and enable the "Read and Write Permissions"
Click save button
3.
Test the action
Now go to the issue page and comment !take. What happens ?
If everything went well you should receive a response with "Issue assigned to ".
Test out the !support and !drop commands too to ensure they work properly.
To learn more about workflows and actions check out the GitHub Documentations.
To learn more about writing your own custom actions check out GitHub's Creating Actions Documentation
To support me and the work I do, you can Become a Sponsor or