{ Simple Frontend }

Newcomer guide to Github Actions

Master key concepts and best practices for your Github Actions workflows.

Jeremy Colin Jeremy Colin
May 14, 2025 - 7 min read

Github Actions make it easy to automate checks and process for your Continuous Integration and Continuous Delivery. If you are completely new to Github Actions, I would recommend you start with the Github Actions documentation first.

This guide will focus on a few important concepts not so well covered and that I wish I’d knew when setting up more advanced workflows.

Jobs are what matters

While the starting point to write Github Actions are workflows represented by yml files, the core unit of Github Actions is a job.

A workflow will be made up of one or more jobs and when it comes to Continuous Integration such as a pull request validation, a job will correspond to a status check. That’s why they are so fundamental to Github Actions since those jobs success is what you will require in your Github repository rulesets as well as third party integrations.

Github UI for a pull request showing successful status checks

Debugging

Github Actions workflow context revolve around the github object context. This object is important as it defines the context for your workflows, for example the SHA to checkout when you use actions/checkout.

A very convenient way to debug your workflows is to log this event using a step like this:

steps:
- name: Log Github Event
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"

From there you can read properties such as your workflow ref, sha and event_name to understand what triggered your workflow and validate its context.

Concurrency made easy

Concurrency is an important concept for any CI/CD system and Github Actions gives you a lot of flexibility to control it for your workflows and I’ll help you cut through the noise.

Pull request workflows concurrency

Pull request worflows are usually integration checks such as type checking, linting, testing and for those you only to run your workflow on the latest commit of the pull request. You can configure a workflow concurrency for this using the following format:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

The value of github.workflow is the name of your workflow so make sure it is unique across your repository.

The value of github.ref is the a unique identifier for the pull request, for example refs/pull/123/merge.

Adding cancel-in-progress: true makes it so new commits pushed to the pull request will cancel the running workflow and start a new one.

Main branch workflows concurrency

Main branch workflows are usually used for tasks such as deployments and it is typically not a good idea to cancel them while in progress but rather wait for them to finish in order to start a new one, effectively queueing them.

This is how you can configure such behavior, typically on a main branch push:

concurrency:
group: ${{ github.workflow }}

If you omit the concurrency group altogether, multiple workflows will be allowed to run in parallel which is probably not what you want for deployments.

Workflows dependencies gotchas

There are many ways to express workflow dependencies and being able to trigger workflows from other workflows is a powerful feature of Github Actions. You can also trigger workflows manually from the Github UI for example to rollback a deployment to a previous commit.

It’s extremely important to understand that workflows are triggered asynchronously so the newly trigger workflow will not have access to the github context of the original workflow! So what I found useful for workflow dependencies is to use inputs and outputs to keep track of the context you need, for example the sha of the commit to checkout in case of a deployment after successful end-to-end tests declared in another workflow file:

on:
workflow_dispatch:
inputs:
sha:
description: sha of the commit to checkout
required: true
type: string

In this case, you can also trigger the workflow manually from the Github UI and pass the sha of the commit to checkout(for example if you need to bypass your end-to-end tests).

A few tips

  1. Keep it Simple! I have seen many workflows with complex triggers and conditions which only made it hard to understand when they should run and harder to maintain.
  2. Avoid over-splitting your workflows and jobs, especially if they contain steps that need to checkout your repository and install dependencies. It’s often better to have jobs with multiple steps instead to keep things simple and save overall time.
  3. Parallelize your jobs as much as possible, for example by running your tests in parallel to your type checking, linting and build, especially if they take a long time to run.
  4. You probably do not need a composite action to make your workflow reusable, repeating a few lines in different workflow files is perfectly fine and easier to maintain.

I found what worked well is the right balance between giving quick feedback to developer’s pull requests versus over-complicating your workflows.

Bonus: Interacting with deployments

Github deployments API makes it easy to interact with your application deployments for any provider you are using. If they have a native integration like Vercel does, then it’s even easier and you can directly rely on Github Actions deployment hooks.

Otherwise, you can use your deployment provider deploy hooks to create Github deployments and update their statuses. I have written a example guide on how to integrate Github Actions with Netlify deployments here.

Pull request deployments

I recommend using the deployment_status event to trigger your workflows which depend on a deployment. This event is triggered for every deployment status update and is available in the github.event context. You can then filter using the deployment_status.state and deployment.environment properties:

on:
deployment_status:
jobs:
e2e:
if: github.event.deployment_status.state == 'success' && github.event.deployment.environment == 'Preview'
runs-on: ubuntu-latest
steps: ...

You can then use the github.event.deployment_status.environment_url to interact with the deployment and run automated end-to-end tests.

What is important to note with the deployment_status event (at least with Vercel deployments) is that the associated github context will be referencing the commit which triggered the deployment so using for example the Github Actions checkout action will automically checkout the right pull request commit and ref.

Jobs triggered through the deployment_status event will be reported as status checks for pull requests in the Github UI.

Main branch deployments

You can also use the deployment_status event to trigger workflows for main branch deployments, however Vercel has been recommending to use the repository_dispatch event instead but you can’t go wrong with either.

Why not using the repository_dispatch event for pull request deployments as well? This event will only trigger a workflow if the file exists on your default branch and because its github context will be referencing the latest commit on the default branch, it will not be reflected in pull request status checks, making it less useful for pull request deployments.

For your main branch deployments, the repository_dispatch event presents the advantage of being able to filter its dispatch type such as vercel.deployment.success to only trigger a workflow for successful deployments:

on:
repository_dispatch:
types:
- vercel.deployment.success

Then there are a couple of gotchas to be aware of:

  • The github event context will be the latest main branch commit and not the commit which triggered the deployment so we will need to use the
    github.event.client_payload.git.sha to checkout the right commit.
  • The environment will now be available in the github.event.client_payload.environment property.
  • The deployment URL will now be available in the github.event.client_payload.url property.

So our workflow will now look like this:

on:
repository_dispatch:
types:
- "vercel.deployment.success"
jobs:
deploy-production-vercel:
if: github.event.client_payload.environment == 'production'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.client_payload.git.sha }}
...
# Deployment URL is available under ${{ github.event.client_payload.url }}

I hope you found this guide useful and if you have any questions or suggestions, feel free to reach out.