Skip to content

Intermediate · CI/CD pipelines · topic 26 of 53 · 35 min

GitHub Actions

Explanation

GitHub Actions runs CI/CD workflows defined in YAML files under .github/workflows/. A workflow starts on an event such as a push, a pull request, a manual button or a schedule, and runs jobs on runners; each job is a list of steps that either run shell commands or use ready-made actions. Matrix builds, caching, artefacts, environments with approvals, least-privilege tokens and OIDC make it fast and safe.

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

yaml
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 IST
Schedule times are in UTC. India is UTC+5:30, so convert carefully. Scheduled runs use the default branch and can start a few minutes late when GitHub is busy.

A complete workflow for a Node.js app

yaml
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 --delete
Test on two Node versions, build once, upload the artefact, then deploy that same artefact after approval. Check each action's page for its current major version.

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

yaml
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Pass a secret to one step as an environment variable, not to the whole workflow.

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.
Careful: Common mistakes. (1) Bad YAML indentation: steps end up in the wrong job. Use an editor with YAML support. (2) Forgetting needs, so deploy runs in parallel with the tests. (3) Writing cron times in IST instead of UTC. (4) Giving every workflow write-all permissions. (5) Storing a long-lived AWS access key as a secret when OIDC would avoid it. (6) Deploying from every branch because the deploy job has no if condition.

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

GitHub Actions

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.

yaml
strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node-version: [22, 24]
    exclude:
      - os: windows-latest
        node-version: 22
runs-on: ${{ matrix.os }}
Four combinations minus one excluded gives three jobs.

Practice (0/3 done)

Task 1 · Question

Where must GitHub Actions workflow files be stored?

Task 2 · Code · Python

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

Task 3 · Practice

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

Question 1

1.What is the difference between uses and run in a step?

Question 2

2.A team wants a workflow at 06:00 IST every day. Which cron line is right?

Question 3

3.Why use OIDC to deploy to a cloud from GitHub Actions?

Question 4

4.Why pin a third-party action to a full commit SHA instead of a tag like v2?

Question 5

5.How do you make a deploy job wait until the test job has passed?

Read more (free)

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.

From CI/CD: shipping every change safely

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.

From Environment variables and secrets

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.

From Environment variables and config

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

  1. 3 more topics in “CI/CD pipelines” before its exam: Jenkins pipelines, Testing, quality gates and artefacts, Deployment strategies, feature flags and GitOps.
  2. Module 6 exam: CI/CD pipelines · 12 questions · 20 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Infrastructure as code with Terraform. 53 topics left in the course before the final exam.

← Previous: Continuous integration, delivery and deployment