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

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.

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
- 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.
- 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.
- 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.
- 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.