Intermediate · CI/CD pipelines · topic 26 of 53 · 35 min
GitHub Actions
Explanation
The words
- Workflow: one automated process, defined in a YAML file in .github/workflows/ (for example ci.yml).
- Event (trigger): what starts the workflow, such as push or pull_request.
- Job: a set of steps that runs on one runner. Jobs run in parallel unless one needs another.
- Step: one command (run) or one action (uses), run in order inside a job.
- Action: a reusable step published by someone, such as actions/checkout, which fetches your code.
- Runner: the machine that runs a job. GitHub-hosted runners such as ubuntu-latest are fresh VMs for every job; self-hosted runners are your own machines.
Triggers
on:
push:
branches: [main] # only pushes to main
pull_request: # every pull request
workflow_dispatch: # a "Run workflow" button in the Actions tab
schedule:
- cron: "30 21 * * *" # every day at 21:30 UTC, which is 03:00 ISTA complete workflow for a Node.js app
name: ci
on:
push:
branches: [main]
pull_request:
permissions:
contents: read # least privilege for GITHUB_TOKEN by default
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22, 24] # two jobs, one per Node.js version
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: npm # caches the npm download folder
- run: npm ci
- run: npm run lint
- run: npm test
build:
needs: test # waits for every test job to pass
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v7
with:
name: web-dist
path: dist/
deploy:
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production # required reviewers must approve first
permissions:
contents: read
id-token: write # allows an OIDC token for the cloud login
steps:
- uses: actions/download-artifact@v8
with:
name: web-dist
path: dist/
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: ap-south-1
- run: aws s3 sync dist/ s3://shop-web-prod --deleteuses versus run
run executes shell commands on the runner, exactly as you would type them. uses runs an action, a packaged step with inputs given under with. Actions save you from writing the same setup again, but they are code written by someone else, so choose them carefully.
Expressions, env and secrets
Text inside ${{ }} is an expression that GitHub fills in, such as ${{ github.sha }} (the commit) or ${{ matrix.node-version }}. Secrets are stored in the repository or organisation settings and read with ${{ secrets.NAME }}. GitHub masks them in logs, but a careless script can still leak them, for example by printing them in a changed form.
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}Matrix builds, caching and artefacts
A matrix runs the same job for every combination of values: os [ubuntu-latest, windows-latest] times node-version [22, 24] gives 4 jobs. exclude removes combinations you do not need, and include adds extra ones. A matrix can create at most 256 jobs per workflow run.
Caching keeps downloaded dependencies between runs, keyed on a hash of the lock file, so the next run skips the download. setup-node can do it for you with cache: npm, or you can use actions/cache directly. Artefacts are files a job saves for later jobs or for people to download, such as a build folder or test reports: upload-artifact saves them and download-artifact fetches them.
Security basics
- Least privilege: set permissions at the top (contents: read) and give a job more only when it needs it. GITHUB_TOKEN is the automatic token each run gets.
- OIDC instead of long-lived keys: with id-token: write, a job can prove who it is to AWS, Azure or Google Cloud and get short-lived credentials. No cloud keys are stored in secrets at all.
- Pin third-party actions to a full commit SHA, like uses: some-org/some-action@<40-character-sha>, with the version in a comment. A tag like v2 can be moved to malicious code; a SHA cannot.
- Environments with required reviewers: a job that targets the production environment waits until an approved person clicks Approve.
- Pull requests from forks do not receive your secrets. Be very careful with the pull_request_target event, which runs with more power.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
GitHub Actions · 1/8
In this lesson you will write CI/CD workflows with GitHub Actions.
Prefer a video by a teacher? Find free videos on “github actions tutorial ci cd workflow matrix secrets” on YouTube ↗
In real life: A startup's open-source SDK tested on every OS
A Bengaluru payments startup publishes a small Node.js SDK that merchants use on Linux and Windows servers. Its workflow runs a matrix of 2 operating systems and 2 Node.js versions on every pull request, excluding Windows on the older Node version to save minutes. Merges to main build the package once and upload it as an artefact. Publishing to npm happens only from a version tag, in a job tied to a "release" environment that needs approval from one of two maintainers. Deploys of the docs site to AWS use OIDC, so no AWS keys exist in GitHub at all.
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [22, 24]
exclude:
- os: windows-latest
node-version: 22
runs-on: ${{ matrix.os }}Practice (0/3 done)
Where must GitHub Actions workflow files be stored?
Write expand_matrix(matrix, exclude=None) that turns a GitHub Actions matrix into the list of job combinations. matrix is a dict of lists; the first key varies slowest, like itertools.product. Each combination is a dict. exclude is a list of dicts: drop a combination if ALL the keys in any one exclude entry match it (an entry may list only some keys).
Write a GitHub Actions workflow for a Python Flask app: run on pull requests and on pushes to main; test on Python 3.12 and 3.13 with pip caching; then, only on pushes to main, build a container image and push it to GitHub Container Registry tagged with the commit SHA.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Jenkins pipelinesCI/CD pipelines · same module
- Testing, quality gates and artefactsCI/CD pipelines · same module
- CI/CD: shipping every change safelyFull-Stack Development · Deploying and running the app
- Environment variables and secretsFull-Stack Development · Deploying and running the app
- Secrets managementDevSecOps
- Environment variables and configBackend Development · Node.js and Express
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What is the difference between uses and run in a step?
Answer: uses runs a reusable action; run executes shell commands
uses: actions/checkout@v7 calls an action; run: npm test runs a command.
A team wants a workflow at 06:00 IST every day. Which cron line is right?
Answer: "30 0 * * *"
Schedules are in UTC. 06:00 IST minus 5 hours 30 minutes is 00:30 UTC.
Why use OIDC to deploy to a cloud from GitHub Actions?
Answer: The job gets short-lived cloud credentials, so no long-lived keys are stored as secrets
OIDC trades a signed token from GitHub for temporary credentials limited to the role you configure.
Why pin a third-party action to a full commit SHA instead of a tag like v2?
Answer: A tag can be moved to different code later; a commit SHA always means the same code
Pinning to a SHA protects you if the action's repository is compromised and a tag is moved.
How do you make a deploy job wait until the test job has passed?
Answer: Add needs: test to the deploy job
Jobs run in parallel by default. needs creates the dependency and skips the job if the needed job fails.
What does CI do on every push?Related
Answer: Installs, lints, tests and builds the code on a fresh machine
Continuous integration checks each change automatically, so problems show up within minutes.
A session secret was pushed to a public GitHub repo. What fixes the problem?Related
Answer: Rotate: make a new secret, deploy it, and stop using the old one
The old value stays in git history and may already be copied. Only replacing it makes it useless.
A developer deletes .env in a new commit after pushing it. Is the secret gone?Related
Answer: No, it is still in the earlier commit in history
Git keeps every earlier commit. Revoke the secret; cleaning history is a later step.
From Secrets management
How can modern Node.js load a .env file without any package?Related
Answer: node --env-file=.env server.js
Node.js 20.6 and later understand the --env-file flag. Older projects use the dotenv package.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “CI/CD pipelines” before its exam: Jenkins pipelines, Testing, quality gates and artefacts, Deployment strategies, feature flags and GitOps.
- Module 6 exam: CI/CD pipelines · 12 questions · 20 min, without help. 5 topics of this module still to finish first.
- Then the next module, Infrastructure as code with Terraform. 53 topics left in the course before the final exam.