What is GitHub Actions?
Have you heard of GitHub Actions but assumed it was too complicated to learn? I had the same thought — until I used it on a real project (Build and deploy Hugo site with GitHub Actions, Schedule future-dated posts with Github Actions).
Think of GitHub Actions as a robot living on GitHub: you call it, and it does exactly what you've told it to do. More precisely, it's GitHub's built-in automation. You write a workflow, and GitHub runs it for you whenever one of the events you specify happens.
With GitHub Actions, I no longer build my Hugo site by hand: I push a new post, and the robot builds and deploys it.
In this post I'll show you the two sections every workflow file needs, then walk you through a six-line workflow that you can paste into your own repository today.
A real example: my Hugo site
Let me make this concrete with my own case of static site generation. With Hugo, the classic approach was to build the site locally and manually, then push the generated public/ folder to GitHub for deployment. With GitHub Actions, GitHub's servers take over the build. Now I just need to push a new post, and GitHub will build and deploy the site for me.
Even better, GitHub Actions can run on a schedule, so it runs at the time and frequency you choose.
The magic is in the workflow file
So how does it actually work?
The key is to define the workflow file: a .yml or .yaml file under .github/workflows/. It's the set of instructions we hand the "robot" in advance, so it knows what to do when it's triggered.
Note: a
workflows/folder can hold several workflow files.
Two sections are required: on and jobs.
The on section: when it runs
The on section specifies which events trigger the workflow. For example, a push to a branch, or a pull_request.
Let's use the Hugo site as our example. Its workflow lists three triggers: push, schedule, and workflow_dispatch.
on:
push:
branches:
- main
schedule:
- cron: '26 1 * * *'
workflow_dispatch:
The
pushtrigger means the workflow runs whenever someone pushes to themainbranch.The
scheduletrigger runs the workflow on a cron schedule — here, at 01:26 UTC every day. GitHub's cron uses UTC, so convert your local time. I walk through every cron field in this post).The
workflow_dispatchtrigger lets you run the workflow manually. Normally a workflow only starts in response to an event, so testing it means making a throwaway commit.workflow_dispatchexists precisely so you don't have to.
Adding workflow_dispatch puts a Run workflow button at the top of the Actions tab. Here's how to use it:
(1) In your repository, click the Actions tab;
(2) In the left sidebar, click the workflow name;
(3) On the right side, click the Run workflow button. The button only exists when the file contains workflow_dispatch. You can pick the branch here.
(4) The new run appears at the top of the list.
Note: pay attention to the indentation before each trigger. I once made the mistake of putting
scheduleon the same indentation level as thebranchesunderpush, that cost me a lot of time debugging.
The jobs section: what it does
The jobs section specifies what the workflow does when it runs. In the Hugo example, the jobs build and deploy the site.
Each job needs two keys:
-
runs-on: the machine the job runs on (ubuntu-latest,windows-latest,macos-latest, …); -
steps: the list of individual tasks to run, in order.
The code below shows the build job. The deploy job that follows it is omitted. You can read the full workflow here .
jobs:
build:
runs-on: ubuntu-latest
env:
HUGO_VERSION: 0.166.0
steps:
- name: Install Hugo CLI
run: wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb && sudo dpkg -i ${{ runner.temp }}/hugo.deb
- name: Install Dart Sass
run: sudo snap install dart-sass
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Pages
id: pages
uses: actions/configure-pages@v4
- name: Install Node.js dependencies
run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
- name: Build with Hugo
env:
HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache
HUGO_ENV: production
run: hugo --gc --minify
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./public
Read the job top to bottom and you can follow the flow: install Hugo and Dart Sass; check out the repository onto the runner; build the site with Hugo; and pack the public/ folder as a Pages artifact.
Try it yourself
In fact, you've been using this kind of thing for years — an auto-reply rule in your email app runs the moment a message arrives. GitHub Actions is the same idea applied to your code.
GitHub is full of examples, too. Open your first pull request on a project, for instance, and you may receive a welcome message — that's GitHub Actions at work.
How do you write your first GitHub Action?
You don't need a site on GitHub Pages yet. Start small: set up a push-triggered workflow in any repository. Here's a complete, minimal one:
on: push
jobs:
hello:
runs-on: ubuntu-latest
steps:
- run: echo "hello"
Save it as .github/workflows/hello.yml in your repository. Push, and it runs.
Keep in mind that GitHub Actions runs on a GitHub-hosted runner, so echo "hello" won't display on your local terminal. Where does it go?
The output is captured in the run's log. To view it, open your repository on GitHub and click the Actions tab. Then click the run and then the job (hello in this example).
Expand the step, and you will see hello in the log.
That's it. Now you have a workflow that runs on every push. And that's the whole trick: you write a workflow file, GitHub does the work.
If you want to go further:
- Add a schedule: reuse the
scheduletrigger from theonsection so your site rebuilds itself every night, and please remember the cron runs in UTC. - Add a manual trigger: add
workflow_dispatchand you get a Run workflow button, so you can test a workflow change without pushing a throwaway commit. - Read a real one: here's the full workflow file for my Hugo site. Now you should be able to follow it.
Have you set up GitHub Actions in your own repository yet? I'd love to hear what you're automating with it.


Top comments (13)
I was about to write an article on this but you beat me to it haha.
Love the article. Well written and straight to the point! I am trying to see if I can use GitHub Actions creatively as my CI/CD workflow. It's going well so far and a very convenient tool that everyone should use.
Well done Julie!
Thank you!😄 I'm glad you like it. Having tried to learn it twice, I might have a better sense of where beginners feel lost. 😄
I'd love to hear how you use GitHub Actions in your workflow.
I'd make missing lockfiles optional without making a failed npm ci optional. The final || true makes both cases look successful to the runner, so a broken dependency install can reach the Hugo build. An explicit if around lockfile presence keeps the beginner-friendly skip, while allowing a real install error to stop the job. I'd test it once with a deliberately invalid lockfile to check that distinction in the Actions log.
Thank you for pointing this out! 😄
To be honest, the AI agent wrote the
.ymlworkflow file for me when I was setting up my Hugo site. To ensure the run stops whennpm cifails, the AI agent suggested the following change:But it seems that the starter template on GitHub provides the same code as in the article, so I'm wondering whether the change is necessary.
Yes, I'd keep your explicit
if. You're right that GitHub's Hugo starter currently uses the same|| trueform, so this isn't something your AI invented.The difference is failure policy: that one-liner turns both "no lockfile" and "npm ci failed" into success. Your version skips only the first case. With Actions' standard Ubuntu shell behavior, a failed
npm ciin thatthenbranch stops the step; addingshell: bashmakes the shell choice explicit.The right policy depends on the site: if the theme needs Node dependencies, hiding an install error can produce an incomplete build. If the site needs no Node dependencies, skipping is fine. If they are required, even a missing lockfile should fail rather than skip. I'd check three cases: no lockfile, a valid install, and a deliberately broken install. Only the last should fail under your optional-install policy.
Thank you for the clear explanation!
The six-line hello workflow is a good starting point because you can see the trigger and the job without deployment details getting in the way. For the Hugo example, what would you add next to handle a scheduled build overlapping with a push-triggered deployment?
Thank you! As for adding scheduling, I wrote a post about it. Feel free to take a look.😄
The
scheduleandpushevents trigger the workflow independently: one at the scheduled time, the other on every push.Thanks, Julie. That clears up the distinction: the schedule doesn't replace the push trigger. Showing both runs side by side would make a useful follow-up exercise for beginners. Appreciate the explanation and the link!
You're welcome! 😄 Yes, the
schedule,push, andworkflow_dispatchtriggers don't replace one other. Each servers a different purpose.Clear explanations! I am fairly recently using GitHub actions in my portfolio projects as well and I'm kinda liking it because of it's straightforward approach.
Thank you! 😄Yes, once you start using GitHub Actions, you'll find it convenient because it can automate many tasks.
What do you use GitHub Actions for in your portfolio projects?😄
Mainly for linting, building prod, and running tests for CI.