Find the Best Cosmetic Hospitals

Explore trusted cosmetic hospitals and make a confident choice for your transformation.

“Invest in yourself — your confidence is always worth it.”

Explore Cosmetic Hospitals

Start your journey today — compare options in one place.

GitHub Actions Essentials — Learn CI/CD in 2 Hours

Audience: Beginners, developers, junior DevOps engineers, QA engineers, and students

Level: Beginner to early-intermediate

Duration: About 120 minutes

Format: Learn, modify, run, break, and fix

Goal: By the end of this tutorial, you should be able to read an unfamiliar GitHub Actions workflow, create a basic CI workflow, use common Actions features safely, and troubleshoot common failures.


Table of Contents

  1. How to Use This Tutorial
  2. GitHub Actions in 10 Minutes
  3. Understanding Workflow YAML
  4. Events and Triggers
  5. Jobs, Steps, and Runners
  6. Using Actions
  7. Variables, Contexts, and Expressions
  8. Secrets and GITHUB_TOKEN
  9. Hands-On Lab: Build Your First CI Pipeline
  10. Matrix Builds
  11. Caching
  12. Artifacts
  13. From CI to CD and Environments
  14. GitHub Actions Security: 5 Rules
  15. Troubleshooting: The 7-Step Method
  16. Final Hands-On Challenge
  17. GitHub Actions Essential Cheat Sheet
  18. 15-Question Knowledge Check
  19. What to Learn Next

1. How to Use This Tutorial

This tutorial is intentionally small enough to complete in about two hours.

The objective is not to memorize every GitHub Actions feature. The objective is to build a strong mental model and become productive with the features that appear in most day-to-day CI workflows.

You will repeatedly modify one workflow instead of studying many unrelated examples.

The learning sequence is:

Understand
   |
   v
Write
   |
   v
Run
   |
   v
Inspect
   |
   v
Break
   |
   v
Fix
Code language: PHP (php)

1.1 Suggested timing

TimeModuleMain outcome
0-10 minGitHub Actions mental modelUnderstand the platform
10-25 minWorkflow YAMLRead and write workflow structure
25-38 minTriggers, jobs, and stepsControl when and how work runs
38-48 minRunners and ActionsUnderstand where work runs and how Actions are reused
48-60 minVariables, contexts, expressions, secretsUse runtime data safely
60-78 minMain CI labBuild a real CI pipeline
78-90 minMatrix, cache, artifactsAdd important CI capabilities
90-101 minCI to CDUnderstand deployment flow
101-110 minSecurityLearn the five rules that matter most
110-117 minTroubleshootingDebug failures systematically
117-120 minChallengeApply what you learned

1.2 Prerequisites

You should have:

  • a GitHub account;
  • access to a repository where you can create branches and commits;
  • basic Git knowledge;
  • basic command-line knowledge;
  • optional Node.js knowledge for the hands-on lab.

You do not need prior GitHub Actions experience.

1.3 What this tutorial intentionally does not cover

The following topics are important, but they belong in a longer course:

  • custom JavaScript actions;
  • custom Docker actions;
  • advanced reusable workflow architecture;
  • OpenID Connect cloud federation implementation;
  • Kubernetes deployment;
  • Terraform and infrastructure-as-code workflows;
  • Actions Runner Controller;
  • enterprise governance;
  • monorepo optimization;
  • artifact attestations;
  • advanced release engineering;
  • organization-wide workflow policy.

2. GitHub Actions in 10 Minutes

2.1 What is GitHub Actions?

GitHub Actions is GitHub’s automation platform.

It listens for events such as:

  • code being pushed;
  • a pull request being opened;
  • a pull request being updated;
  • a release being created;
  • a user manually starting a workflow;
  • a schedule being reached.

When an event matches a workflow definition, GitHub starts a workflow run.

The most common use is CI/CD.

Typical CI tasks include:

  • checkout source code;
  • install dependencies;
  • lint code;
  • run unit tests;
  • run integration tests;
  • compile or build software;
  • create packages;
  • upload build artifacts.

Typical CD tasks include:

  • publish a package;
  • push a container image;
  • deploy to staging;
  • deploy to production;
  • run smoke tests;
  • roll back a deployment.

2.2 The core mental model

Remember this sequence:

Event
  |
  v
Workflow
  |
  v
Job
  |
  v
Runner
  |
  v
Steps
  |
  v
Actions / Commands
  |
  v
Result

That single flow explains most of GitHub Actions.

2.3 Core terms

TermSimple meaning
EventSomething happened in or around GitHub
WorkflowAutomation defined in YAML
Workflow runOne execution of a workflow
JobA unit of work scheduled to a runner
RunnerThe machine that executes a job
StepA task inside a job
ActionReusable automation executed by a step
CommandA shell command executed by a step
ArtifactA file kept from a workflow run
CacheReusable data intended to make future runs faster
SecretProtected sensitive configuration
VariableNon-sensitive configuration
ContextStructured runtime information from GitHub
ExpressionGitHub’s ${{ }} evaluation syntax
EnvironmentA named deployment target such as staging or production

2.4 CI, continuous delivery, and continuous deployment

These terms are related but different.

PracticeQuestion it answersExample
Continuous IntegrationIs this code safe to merge?Build, lint, test
Continuous DeliveryIs this release ready to deploy?Build, package, stage, wait for approval
Continuous DeploymentCan validated changes deploy automatically?CI plus automatic production deployment

A beginner should first learn CI. Deployment should come after you understand workflow execution and security.

2.5 Your first workflow

Create this file:

.github/workflows/hello.yml

Add:

name: Hello GitHub Actions

on:
  push:

jobs:
  hello:
    runs-on: ubuntu-latest

    steps:
      - name: Say hello
        run: echo "Hello from GitHub Actions"
Code language: PHP (php)

Commit and push it.

Then open:

Repository -> Actions -> Hello GitHub Actions

You should see a workflow run.

2.6 What happened?

You pushed a commit
      |
      v
GitHub detected push
      |
      v
hello.yml matched the event
      |
      v
Workflow run was created
      |
      v
Job "hello" was scheduled
      |
      v
Ubuntu runner started
      |
      v
Step executed echo command
      |
      v
Workflow completed
Code language: PHP (php)

That is GitHub Actions in its simplest useful form.


3. Understanding Workflow YAML

GitHub Actions workflows are YAML files stored under:

.github/workflows/

Common filenames:

ci.yml
pull-request-ci.yml
release.yml
deploy-production.yml
Code language: CSS (css)

Both .yml and .yaml work.

3.1 YAML indentation matters

Correct:

jobs:
  test:
    runs-on: ubuntu-latest

Incorrect:

jobs:
test:
  runs-on: ubuntu-latest

YAML uses indentation to describe structure.

3.2 The basic workflow structure

Study this example carefully:

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Run a command
        run: echo "CI is running"
Code language: PHP (php)

The important keywords are:

name
on
permissions
jobs
runs-on
steps
uses
run
with
Code language: JavaScript (javascript)

3.3 Workflow hierarchy

Workflow
|
+-- name
+-- on
+-- permissions
+-- jobs
    |
    +-- test
        |
        +-- runs-on
        +-- steps
            |
            +-- checkout
            +-- command

3.4 name

The workflow display name:

name: Pull Request CI
Code language: HTTP (http)

This appears in the Actions UI.

3.5 on

The trigger:

on:
  push:

Or multiple triggers:

on:
  push:
  pull_request:
  workflow_dispatch:

3.6 jobs

A workflow contains one or more jobs:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

Here test is the job ID.

3.7 runs-on

This selects the runner:

runs-on: ubuntu-latest
Code language: HTTP (http)

3.8 steps

Steps run in order inside one job:

steps:
  - run: echo "Step 1"
  - run: echo "Step 2"
  - run: echo "Step 3"
Code language: PHP (php)

3.9 uses

Use a reusable Action:

- uses: actions/checkout@v6

3.10 run

Run a shell command:

- run: npm test

Multiline commands:

- name: Build
  run: |
    npm ci
    npm test
    npm run build

3.11 with

Pass inputs to an Action:

- uses: actions/setup-node@v7
  with:
    node-version: "24"
Code language: JavaScript (javascript)

3.12 Micro-practice

Modify your first workflow:

name: My First CI Workflow

on:
  push:

jobs:
  hello:
    runs-on: ubuntu-latest

    steps:
      - name: Print repository information
        run: |
          echo "Repository: $GITHUB_REPOSITORY"
          echo "Commit: $GITHUB_SHA"
Code language: PHP (php)

Push the change and inspect the log.


4. Events and Triggers

A workflow does nothing until an event matches its on configuration.

For a two-hour course, focus on three triggers:

push
pull_request
workflow_dispatch

4.1 Push

Run whenever code is pushed:

on:
  push:

Run only when main changes:

on:
  push:
    branches:
      - main

Run for multiple branches:

on:
  push:
    branches:
      - main
      - develop

4.2 Pull request

Run when a pull request is opened or updated:

on:
  pull_request:

This is one of the most important CI triggers.

Typical PR workflow:

Developer creates PR
        |
        v
GitHub starts CI
        |
        v
Lint
        |
        v
Test
        |
        v
Build
        |
        v
Status check passes or fails

4.3 Manual workflow

Use workflow_dispatch when a person should start the workflow manually:

on:
  workflow_dispatch:

After pushing the workflow to the default branch, GitHub can show a “Run workflow” button in the Actions UI.

A simple manual input:

on:
  workflow_dispatch:
    inputs:
      environment:
        description: Deployment environment
        required: true
        type: choice
        options:
          - staging
          - production
Code language: JavaScript (javascript)

Read the selected value with:

${{ inputs.environment }}

4.4 Multiple triggers

A common CI workflow:

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:
Code language: CSS (css)

Meaning:

Push to main --------+
                     |
Pull request --------+--> Run workflow
                     |
Manual execution ----+

4.5 Path filters

If you only want CI when application code changes:

on:
  pull_request:
    paths:
      - "src/**"
      - "package.json"
      - "package-lock.json"
Code language: JavaScript (javascript)

Use path filters carefully. A workflow skipped because of a path filter can interact with required checks in ways that need deliberate repository design.

4.6 Trigger practice

Change your workflow so that it runs:

  • on pull requests;
  • on pushes to main;
  • manually.

Solution:

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:
Code language: CSS (css)

5. Jobs, Steps, and Runners

This is the most important execution concept after triggers.

5.1 Jobs

A job is an independently scheduled unit of work.

Example:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Linting"

  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing"
Code language: PHP (php)

By default, these jobs can run in parallel.

             +--> Job: lint --> Runner A
Workflow ----+
             +--> Job: test --> Runner B

5.2 Steps

Steps inside a job run sequentially.

steps:
  - run: echo "1"
  - run: echo "2"
  - run: echo "3"
Code language: PHP (php)

Execution:

Step 1
  |
  v
Step 2
  |
  v
Step 3

5.3 Jobs do not automatically share files

Each job normally has its own runner environment.

This is important:

Job A filesystem != Job B filesystem

If one job creates a file and another job needs it, use an artifact or another explicit data-transfer mechanism.

5.4 Sequential jobs with needs

Example:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build"

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploy"
Code language: PHP (php)

Flow:

Build
  |
  v
Deploy

Without needs, jobs may run in parallel.

5.5 Fan-out pattern

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Build"

  unit:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Unit tests"

  integration:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Integration tests"
Code language: PHP (php)

Flow:

            +--> Unit
            |
Build ------+
            |
            +--> Integration

5.6 Runners

A runner is the compute environment that executes a job.

Common options:

RunnerMeaning
ubuntu-latestGitHub-hosted Linux runner
windows-latestGitHub-hosted Windows runner
macos-latestGitHub-hosted macOS runner
self-hostedRunner managed by you

For this course, use:

runs-on: ubuntu-latest
Code language: HTTP (http)

5.7 GitHub-hosted runners

Advantages:

  • no server maintenance;
  • fresh environment per job;
  • common tools preinstalled;
  • easy scaling;
  • simple for most CI use cases.

Important consequence:

The environment is temporary. Do not expect files to remain after the job ends unless you upload them somewhere.

5.8 Self-hosted runners

A self-hosted runner is a machine you operate.

Examples:

  • EC2 instance;
  • VM;
  • bare-metal server;
  • on-premises server;
  • Kubernetes runner pod.

Use self-hosted runners when you need special networking, hardware, tooling, or internal access.

For beginners, the most important security lesson is:

Do not run untrusted code on a powerful persistent self-hosted runner that can access production systems.

5.9 Practice

Create two jobs:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Lint"

  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Test"
Code language: PHP (php)

Run the workflow and inspect whether the jobs overlap in time.

Then add:

needs: lint
Code language: HTTP (http)

to the test job and run it again.

Observe the difference.


6. Using Actions

An Action is reusable automation used inside a step.

6.1 Action vs command

Action:

- uses: actions/checkout@v6

Command:

- run: npm test

The difference is simple:

uses = reuse an Action
run  = execute a shell command

6.2 Checkout Action

Most workflows need the repository contents:

- name: Checkout
  uses: actions/checkout@v6

Without checkout, your runner does not automatically have the repository files available for your commands.

6.3 Setup Actions

Example for Node.js:

- name: Setup Node
  uses: actions/setup-node@v7
  with:
    node-version: "24"
Code language: JavaScript (javascript)

Example for Python:

- name: Setup Python
  uses: actions/setup-python@v5
  with:
    python-version: "3.13"
Code language: JavaScript (javascript)

6.4 Action inputs

Actions can accept inputs through with:

- uses: actions/setup-node@v7
  with:
    node-version: "24"
    cache: npm
Code language: JavaScript (javascript)

6.5 Action versions

Examples commonly use a major release tag:

uses: actions/checkout@v6
Code language: HTTP (http)

Production security-sensitive third-party Actions should be reviewed carefully and often pinned to a full commit SHA according to your organization’s supply-chain policy.

The beginner rule is:

  • prefer official or well-reviewed Actions;
  • understand what permissions they receive;
  • do not blindly copy random Marketplace Actions into privileged workflows.

7. Variables, Contexts, and Expressions

These concepts often confuse beginners because they look similar.

Use this table first:

TypeExamplePurpose
Environment variable$NODE_ENVRuntime process configuration
Configuration variable${{ vars.API_URL }}Non-secret GitHub configuration
Context${{ github.ref }}Runtime information from GitHub
Expression${{ ... }}GitHub-side evaluation and logic
Secret${{ secrets.API_TOKEN }}Sensitive configuration

7.1 Environment variables

Workflow-level:

env:
  NODE_ENV: test

Job-level:

jobs:
  test:
    env:
      MODE: ci

Step-level:

- name: Run test
  env:
    API_URL: https://example.test
  run: npm test
Code language: JavaScript (javascript)

A more specific scope overrides a broader scope.

7.2 Default GitHub environment variables

GitHub automatically provides many useful values.

Examples:

GITHUB_REPOSITORY
GITHUB_SHA
GITHUB_REF
GITHUB_REF_NAME
GITHUB_RUN_ID
GITHUB_RUN_NUMBER
GITHUB_WORKFLOW
GITHUB_WORKSPACE
RUNNER_OS
RUNNER_ARCH

Use them in shell commands:

- name: Print metadata
  run: |
    echo "Repository: $GITHUB_REPOSITORY"
    echo "Commit: $GITHUB_SHA"
    echo "Ref: $GITHUB_REF"
    echo "Runner: $RUNNER_OS/$RUNNER_ARCH"
Code language: PHP (php)

7.3 Contexts

Contexts are structured objects evaluated by GitHub.

Important contexts:

ContextExample data
githubrepository, ref, SHA, actor, event
envworkflow/job/step environment values
varsconfiguration variables
secretsavailable secrets
stepsstep outputs and outcomes
needsdependency job results and outputs
matrixcurrent matrix values
runnerrunner details
inputsmanual or reusable workflow inputs

Example:

- run: |
    echo "Repository: ${{ github.repository }}"
    echo "Branch: ${{ github.ref_name }}"
    echo "Commit: ${{ github.sha }}"
Code language: PHP (php)

7.4 Expressions

GitHub expression syntax:

${{ expression }}

Example:

if: ${{ github.ref == 'refs/heads/main' }}
Code language: JavaScript (javascript)

In many if: conditions, the outer ${{ }} is optional:

if: github.ref == 'refs/heads/main'
Code language: JavaScript (javascript)

7.5 Useful operators

==
!=
&&
||
!

Example:

if: github.ref == 'refs/heads/main' && github.event_name == 'push'
Code language: JavaScript (javascript)

7.6 Useful functions

Examples:

if: startsWith(github.ref, 'refs/tags/v')
Code language: JavaScript (javascript)
if: contains(github.event.pull_request.labels.*.name, 'run-full-ci')
Code language: JavaScript (javascript)
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
Code language: JavaScript (javascript)

You do not need to memorize all expression functions. Learn how to recognize and look them up.

7.7 Conditions

A common conditional step:

- name: Main branch only
  if: github.ref == 'refs/heads/main'
  run: echo "Running on main"
Code language: PHP (php)

Common status functions:

if: success()
if: failure()
if: cancelled()
if: always()
Code language: HTTP (http)

Example cleanup:

- name: Collect diagnostics
  if: failure()
  run: echo "Tests failed - collecting diagnostics"
Code language: PHP (php)

8. Secrets and GITHUB_TOKEN

Secrets and permissions are where beginner workflows can accidentally become unsafe.

8.1 What is a secret?

A secret is sensitive information stored in GitHub instead of being written directly into the workflow file.

Examples:

  • API tokens;
  • passwords;
  • package registry tokens;
  • signing credentials;
  • webhook tokens.

Do not do this:

env:
  API_TOKEN: abc123-real-secret

Instead, create a GitHub secret and reference it:

env:
  API_TOKEN: ${{ secrets.API_TOKEN }}

8.2 Secret scopes

Secrets can be configured at different scopes, including:

  • repository;
  • organization;
  • environment.

For beginners, repository secrets are the easiest to understand.

For deployments, environment secrets are often better because production and staging can use different credentials.

8.3 Using a secret safely

Prefer passing a secret as environment data:

- name: Call API
  env:
    API_TOKEN: ${{ secrets.API_TOKEN }}
  run: ./call-api.sh

Avoid printing secrets:

echo "$API_TOKEN"
Code language: PHP (php)

GitHub masks many known secret values in logs, but masking should not be treated as a complete data-loss prevention system.

8.4 Fork pull requests

Untrusted pull requests do not automatically receive powerful repository secrets.

That is intentional.

The safe mental model is:

Untrusted pull request
        |
        v
Low-privilege CI
        |
        v
Test / validate
        |
        v
Trusted workflow handles privileged work

Do not design ordinary PR testing so that arbitrary code requires production credentials.

8.5 What is GITHUB_TOKEN?

GitHub automatically creates a short-lived token for workflow jobs.

You usually do not need to manually create or rotate it.

A workflow can access it as:

${{ secrets.GITHUB_TOKEN }}

or:

${{ github.token }}

8.6 Permissions

Always start with the minimum permissions required.

A safe CI baseline:

permissions:
  contents: read

A job that needs to create or modify repository content may need more:

permissions:
  contents: write

Do not grant write permission to every job just because one job needs it.

8.7 Workflow-level vs job-level permissions

Example:

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Read-only CI"

  publish:
    permissions:
      contents: write
    runs-on: ubuntu-latest
    steps:
      - run: echo "Publishing needs additional permission"
Code language: PHP (php)

This keeps the test job less privileged.

8.8 Beginner security rule

Remember:

Secrets protect sensitive values.
Permissions control what the workflow identity can do.
Code language: JavaScript (javascript)

They solve different problems.


9. Hands-On Lab: Build Your First CI Pipeline

This is the main practical exercise.

You will build one CI workflow step by step.

The example assumes a Node.js project with commands similar to:

npm ci
npm run lint
npm test
npm run build

If your project uses different commands, adapt them.

9.1 Goal

By the end, the workflow will look like this:

Push / Pull Request
        |
        v
Checkout
        |
        v
Setup Node.js
        |
        v
Install dependencies
        |
        v
Lint
        |
        v
Test
        |
        v
Build
        |
        v
Upload build artifact

9.2 Step 1 – Create the workflow

Create:

.github/workflows/ci.yml

Start with:

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

jobs:
  ci:
    runs-on: ubuntu-latest

    steps:
      - name: Say hello
        run: echo "Starting CI"
Code language: PHP (php)

Commit and push.

Confirm that the workflow starts.

9.3 Step 2 – Checkout the repository

Replace the hello-only workflow with:

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

jobs:
  ci:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: List repository files
        run: ls -la
Code language: HTTP (http)

Run it.

The ls -la command should now show repository files.

9.4 Step 3 – Setup Node.js

Add:

- name: Setup Node
  uses: actions/setup-node@v7
  with:
    node-version: "24"
Code language: JavaScript (javascript)

Then verify:

- name: Check Node version
  run: node --version

Your steps now include:

steps:
  - name: Checkout
    uses: actions/checkout@v6

  - name: Setup Node
    uses: actions/setup-node@v7
    with:
      node-version: "24"

  - name: Check Node version
    run: node --version
Code language: JavaScript (javascript)

9.5 Step 4 – Install dependencies

Add:

- name: Install dependencies
  run: npm ci

Why npm ci instead of npm install in CI?

npm ci is designed for clean, repeatable installation based on the lock file.

9.6 Step 5 – Lint

Add:

- name: Lint
  run: npm run lint

If linting fails, the job normally stops and the workflow fails.

That is desirable in CI when lint is a required quality gate.

9.7 Step 6 – Test

Add:

- name: Test
  run: npm test

Now the pipeline validates behavior, not only syntax/style.

9.8 Step 7 – Build

Add:

- name: Build
  run: npm run build

A successful run now proves:

Repository can be checked out
Dependencies can be installed
Code passes lint
Tests pass
Application can build

9.9 Step 8 – Add a timeout

Jobs should not run forever.

Add:

timeout-minutes: 15
Code language: HTTP (http)

Example:

jobs:
  ci:
    runs-on: ubuntu-latest
    timeout-minutes: 15

9.10 Step 9 – Final baseline CI workflow

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

jobs:
  ci:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup Node
        uses: actions/setup-node@v7
        with:
          node-version: "24"

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Test
        run: npm test

      - name: Build
        run: npm run build
Code language: HTTP (http)

9.11 What happens when npm test fails?

Execution normally looks like:

Checkout ........ PASS
Setup Node ...... PASS
Install ......... PASS
Lint ............ PASS
Test ............ FAIL
Build ........... NOT RUN
Workflow ........ FAIL

That is standard fail-fast behavior for sequential steps.

9.12 Add diagnostics on failure

Add:

- name: Failure diagnostics
  if: failure()
  run: |
    echo "A previous step failed"
    echo "Commit: $GITHUB_SHA"
    echo "Runner: $RUNNER_OS"
Code language: PHP (php)

This introduces conditional steps.

9.13 Add concurrency for pull request CI

When developers push several commits quickly, old CI runs can become wasteful.

Add at workflow level:

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
Code language: JavaScript (javascript)

Complete baseline:

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

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

jobs:
  ci:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup Node
        uses: actions/setup-node@v7
        with:
          node-version: "24"

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Test
        run: npm test

      - name: Build
        run: npm run build

      - name: Failure diagnostics
        if: failure()
        run: |
          echo "CI failed"
          echo "Commit: $GITHUB_SHA"
Code language: PHP (php)

You now have a useful real-world CI workflow.


10. Matrix Builds

A matrix allows one job definition to run multiple combinations.

10.1 Why use a matrix?

Suppose your application supports Node.js 22 and 24.

Without a matrix, you could create two nearly identical jobs.

That duplicates YAML.

With a matrix:

strategy:
  matrix:
    node: [22, 24]
Code language: CSS (css)

GitHub expands the job.

Test job
   |
   +--> Node 22
   |
   +--> Node 24

10.2 Basic matrix workflow

name: Matrix CI

on:
  pull_request:

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node: [22, 24]

    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-node@v7
        with:
          node-version: ${{ matrix.node }}

      - run: npm ci
      - run: npm test
Code language: HTTP (http)

GitHub creates separate jobs for the matrix values.

10.3 Multi-dimensional matrices

Example:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [22, 24]

runs-on: ${{ matrix.os }}

This expands to four combinations:

OSNode
Ubuntu22
Ubuntu24
Windows22
Windows24

Do not create large matrices just because you can. Every combination costs time and compute.

10.4 fail-fast

If one matrix job fails, GitHub can cancel other in-progress matrix jobs according to strategy behavior.

If you want all compatibility results:

strategy:
  fail-fast: false
  matrix:
    node: [22, 24]
Code language: JavaScript (javascript)

10.5 Beginner rule

Use a matrix when you are testing the same logic against multiple supported environments.

Do not use a matrix merely to make YAML look advanced.


11. Caching

Caching speeds up workflows by reusing data that is expensive to download or compute.

11.1 Mental model

Without cache:
Download dependencies -> run build

With cache hit:
Restore dependency cache -> run build

A cache is a performance optimization.

Your build should still be correct if the cache disappears.

11.2 Node.js dependency caching

The simplest approach uses setup-node:

- name: Setup Node
  uses: actions/setup-node@v7
  with:
    node-version: "24"
    cache: npm
Code language: JavaScript (javascript)

Then:

- run: npm ci

The cache reduces repeated dependency download work.

11.3 Cache keys

For manual caching, keys usually include information that determines whether cached content is valid.

Example concept:

OS + runtime + dependency lock hash

Example:

key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
Code language: JavaScript (javascript)

11.4 Cache vs artifact

Do not confuse them.

CacheArtifact
Speeds up future workPreserves workflow output
Usually dependencies/build cacheUsually packages/reports/build output
Correctness should not depend on itCan be part of the delivery pipeline
Looked up by cache keyUploaded/downloaded by artifact name or ID

11.5 Cache security

Cached content can become a supply-chain risk if untrusted workflows can poison caches later consumed by privileged jobs.

Beginner rule:

Treat cache as untrusted performance data, not as proof that software is safe.


12. Artifacts

Artifacts are files produced by a workflow and stored by GitHub.

Examples:

  • compiled application;
  • coverage report;
  • test report;
  • generated documentation;
  • packaged release candidate;
  • diagnostic logs.

12.1 Upload an artifact

If your build creates dist/:

- name: Upload build artifact
  uses: actions/upload-artifact@v4
  with:
    name: application
    path: dist/

12.2 Add artifact upload to the lab

Update your CI workflow:

- name: Build
  run: npm run build

- name: Upload build artifact
  uses: actions/upload-artifact@v4
  with:
    name: application-${{ github.sha }}
    path: dist/
    retention-days: 7

Now the flow becomes:

Checkout
   |
Setup
   |
Install
   |
Lint
   |
Test
   |
Build
   |
Upload artifact

12.3 Download in another job

A separate job can download the artifact:

deploy:
  needs: ci
  runs-on: ubuntu-latest

  steps:
    - name: Download build
      uses: actions/download-artifact@v5
      with:
        name: application-${{ github.sha }}
        path: dist/

    - name: Inspect
      run: ls -la dist/

12.4 Build once, deploy many

A mature CI/CD design often creates one immutable artifact and promotes that exact artifact.

Commit
  |
  v
Build once
  |
  v
Artifact
  |
  +--> Staging
  |
  +--> UAT
  |
  +--> Production

This is better than rebuilding different binaries independently for each environment when consistency matters.


13. From CI to CD and Environments

Once CI proves the code is acceptable, CD moves a release toward an environment.

13.1 Basic lifecycle

Developer
   |
   v
Push / Pull Request
   |
   v
CI
   |
   +--> Lint
   +--> Test
   +--> Build
   |
   v
Artifact
   |
   v
Staging
   |
   v
Approval / Protection
   |
   v
Production

13.2 GitHub environments

A GitHub environment can represent a deployment target such as:

development
staging
uat
production

Environments can be used with:

  • environment secrets;
  • environment variables;
  • deployment approvals;
  • branch restrictions;
  • deployment protection rules;
  • deployment history.

13.3 Reference an environment

deploy:
  needs: build
  runs-on: ubuntu-latest
  environment: production

  steps:
    - run: ./deploy.sh

13.4 A safer deployment shape

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: ./build.sh
      - uses: actions/upload-artifact@v4
        with:
          name: release
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/download-artifact@v5
        with:
          name: release
          path: dist/

      - run: ./deploy.sh dist/

The key design idea is:

Build first
Verify first
Then deploy

13.5 Approvals

A production environment can require reviewers before a deployment job receives protected environment access.

Conceptually:

Deployment requested
       |
       v
Environment gate
       |
       +--> Rejected --> Stop
       |
       +--> Approved --> Deploy

For a two-hour course, understand the concept. Detailed environment governance belongs in the advanced course.


14. GitHub Actions Security: 5 Rules

If students remember only five security rules, use these.

Rule 1 – Use least privilege

Start with:

permissions:
  contents: read

Add write permissions only when a specific job needs them.

Bad beginner habit:

permissions: write-all
Code language: HTTP (http)

Avoid broad permissions unless there is a strong, reviewed reason.

Rule 2 – Never hard-code secrets

Bad:

env:
  PASSWORD: my-password-123

Good:

env:
  PASSWORD: ${{ secrets.PASSWORD }}

Rule 3 – Do not print secrets

Bad:

echo "$PASSWORD"
Code language: PHP (php)

Also avoid writing secrets into artifacts or caches.

Rule 4 – Treat Actions as code dependencies

An Action can execute code in your job.

That means an Action may be able to access:

  • repository files;
  • available secrets;
  • job token permissions;
  • the network;
  • workflow outputs.

Prefer trusted Actions and review third-party Actions before allowing them into privileged workflows.

Rule 5 – Treat pull request input as untrusted

This can be dangerous:

- run: echo "${{ github.event.pull_request.title }}"
Code language: PHP (php)

Why?

A pull request title is user-controlled data. Interpolating untrusted values directly into shell syntax can create command-injection risks.

Safer:

- name: Print title safely
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: printf '%s\n' "$PR_TITLE"
Code language: PHP (php)

The general rule is:

Untrusted input should be treated as data,
not as executable shell syntax.
Code language: JavaScript (javascript)

14.1 One more critical PR rule

pull_request and pull_request_target are not interchangeable.

For beginner CI that executes pull request code, prefer a low-privilege pull_request workflow.

Do not combine a privileged base-repository context with execution of untrusted pull request code unless you fully understand the security model.


15. Troubleshooting: The 7-Step Method

Do not debug GitHub Actions by randomly editing YAML.

Use layers.

15.1 Debugging flow

Problem
  |
  v
1. Did workflow start?
  |
  +-- No --> Trigger / branch / path / YAML
  |
  +-- Yes
        |
        v
2. Did job start?
        |
        +-- No --> needs / if / runner / environment
        |
        +-- Yes
              |
              v
3. Which step failed?
              |
              v
4. Read first useful error
              |
              v
5. Classify error
              |
      +-------+-------+-------+-------+
      |       |       |       |       |
    YAML   Command  Auth   Network  Runtime

15.2 Step 1 – Did the workflow start?

If no workflow run appears, check:

  • Is the file under .github/workflows/?
  • Is the YAML valid?
  • Did the expected event happen?
  • Does on match the event?
  • Does the branch filter match?
  • Does the path filter match?
  • Is the workflow enabled?

15.3 Step 2 – Did the job start?

If the workflow exists but a job is skipped or queued, check:

  • if condition;
  • needs dependency;
  • runner label;
  • environment approval;
  • runner availability.

15.4 Step 3 – Which step failed?

Open the failing job and locate the first failed step.

Avoid focusing only on the last red line. The useful error may appear earlier.

15.5 Step 4 – Is it YAML or application logic?

Example workflow error:

Invalid workflow file

Usually means syntax, indentation, expression, or unsupported configuration.

Example application error:

npm test exited with code 1
Code language: JavaScript (javascript)

That may mean the workflow works correctly but the test failed.

This distinction matters.

15.6 Step 5 – Check authentication and permissions

Useful mental model:

401 = authentication problem is likely
403 = identity exists but permission/policy may deny
404 = resource may not exist or may be hidden by authorization

Check:

  • permissions;
  • secret availability;
  • token scope;
  • environment approval;
  • fork restrictions.

15.7 Step 6 – Check the runner environment

Print selected values:

- name: Runner diagnostics
  run: |
    echo "OS=$RUNNER_OS"
    echo "ARCH=$RUNNER_ARCH"
    echo "WORKSPACE=$GITHUB_WORKSPACE"
    pwd
    df -h
Code language: PHP (php)

For self-hosted runners also check:

  • runner online status;
  • labels;
  • disk space;
  • network access;
  • service health;
  • file permissions.

15.8 Step 7 – Reproduce the failing command

If this fails in Actions:

- run: npm test

try to reproduce the same command with the same runtime and dependencies locally or in a controlled container.

The workflow may simply be exposing a real application issue.

15.9 Common mistakes

SymptomLikely cause
Workflow does not runTrigger/filter/path/YAML issue
Job is skippedif or failed needs dependency
Job stays queuedNo matching runner or capacity
command not foundTool missing or PATH issue
File missing in second jobJobs do not share filesystem
Secret emptySecret unavailable in that context/scope
API returns 403Token lacks permission
Cache always missesBad key or changing inputs
Artifact missingWrong path or build did not create file
Works locally, fails in CIEnvironment/runtime/dependency difference

16. Final Hands-On Challenge

Start with this incomplete workflow:

name: CI

on:
  push:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6

      - run: echo "TODO"
Code language: PHP (php)

Complete as many tasks as possible without copying the final solution.

Challenge tasks

  1. Add the pull_request trigger.
  2. Make push run only for main.
  3. Add workflow_dispatch.
  4. Add permissions: contents: read.
  5. Add a 15-minute job timeout.
  6. Setup Node.js 24.
  7. Enable npm caching.
  8. Run npm ci.
  9. Run npm run lint.
  10. Run npm test.
  11. Run npm run build.
  12. Upload dist/ as an artifact.
  13. Name the artifact using github.sha.
  14. Add a failure-diagnostics step.
  15. Convert Node.js versions 22 and 24 into a matrix.

One possible solution

name: CI

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

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

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    strategy:
      fail-fast: false
      matrix:
        node: [22, 24]

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup Node
        uses: actions/setup-node@v7
        with:
          node-version: ${{ matrix.node }}
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Test
        run: npm test

      - name: Build
        run: npm run build

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: application-node-${{ matrix.node }}-${{ github.sha }}
          path: dist/
          retention-days: 7

      - name: Failure diagnostics
        if: failure()
        run: |
          echo "Workflow: $GITHUB_WORKFLOW"
          echo "Commit: $GITHUB_SHA"
          echo "Runner: $RUNNER_OS/$RUNNER_ARCH"
Code language: PHP (php)

If you can explain every line in this workflow, you have learned the essential GitHub Actions model.


17. GitHub Actions Essential Cheat Sheet

17.1 Workflow location

.github/workflows/*.yml

17.2 Minimal workflow

name: CI

on:
  push:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Hello"
Code language: PHP (php)

17.3 Common triggers

on:
  push:
  pull_request:
  workflow_dispatch:

17.4 Push only to main

on:
  push:
    branches: [main]
Code language: CSS (css)

17.5 Checkout

- uses: actions/checkout@v6

17.6 Setup Node

- uses: actions/setup-node@v7
  with:
    node-version: "24"
    cache: npm
Code language: JavaScript (javascript)

17.7 Run a command

- run: npm test

17.8 Multiline shell

- run: |
    npm ci
    npm test
    npm run build

17.9 Job dependency

needs: build
Code language: HTTP (http)

17.10 Condition

if: github.ref == 'refs/heads/main'
Code language: JavaScript (javascript)

17.11 Secret

env:
  API_TOKEN: ${{ secrets.API_TOKEN }}

17.12 Read-only permissions

permissions:
  contents: read

17.13 Context values

${{ github.repository }}
${{ github.sha }}
${{ github.ref_name }}

17.14 Matrix

strategy:
  matrix:
    node: [22, 24]
Code language: CSS (css)

Use:

${{ matrix.node }}

17.15 Upload artifact

- uses: actions/upload-artifact@v4
  with:
    name: application
    path: dist/

17.16 Download artifact

- uses: actions/download-artifact@v5
  with:
    name: application
    path: dist/

17.17 Cancel stale CI

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
Code language: JavaScript (javascript)

17.18 Failure-only step

- if: failure()
  run: echo "Something failed"
Code language: PHP (php)

17.19 Environment

environment: production
Code language: HTTP (http)

17.20 The five security rules

1. Least privilege.
2. Never hard-code secrets.
3. Never print secrets.
4. Treat Actions as executable dependencies.
5. Treat pull request input as untrusted.
Code language: PHP (php)

18. 15-Question Knowledge Check

Try to answer before reading the answer key.

Questions

1. Where must GitHub Actions workflow files normally be stored?

A. .github/actions/ B. .github/workflows/ C. .git/workflows/ D. workflows/

2. What does on define?

A. The runner B. Workflow permissions C. Workflow triggers D. Workflow artifacts

3. What is the difference between a job and a step?

4. Do two jobs automatically share the same filesystem?

5. What does runs-on select?

6. What is the difference between uses and run?

7. What does needs: build do?

8. Which should hold an API password?

A. vars B. secrets C. github D. runner

9. What does ${{ github.sha }} represent?

10. What problem does a matrix solve?

11. What is the main purpose of a cache?

12. What is the main purpose of an artifact?

13. Why should workflow permissions be minimal?

14. Why is direct interpolation of pull request titles into shell commands risky?

15. If a workflow does not start at all, what should you check first?


Answer key

1. B – .github/workflows/

That is the standard workflow directory.

2. C – Workflow triggers

on tells GitHub when the workflow is eligible to run.

3. Job vs step

A job is independently scheduled to a runner. A step is one ordered task inside a job.

4. No

Jobs normally run on separate runner environments. Use artifacts, outputs, or another explicit mechanism to transfer data.

5. The runner

Example:

runs-on: ubuntu-latest
Code language: HTTP (http)

6. uses vs run

uses invokes an Action. run executes a shell command.

7. It creates a dependency

The dependent job waits for build and normally only runs if the dependency succeeds.

8. B – secrets

Sensitive values should not be ordinary configuration variables.

9. The commit SHA

It identifies the commit associated with the workflow run.

10. Repeated compatibility combinations

A matrix expands one job definition across multiple values such as runtime versions or operating systems.

11. Performance

A cache reduces repeated download or computation work.

12. Retaining workflow output

Artifacts preserve files such as build output, reports, or packages.

13. Reduce blast radius

If a job or dependency is compromised, least privilege limits what the workflow identity can do.

14. Command injection risk

Pull request metadata is user-controlled. It should be passed as data rather than inserted directly into executable shell syntax.

15. Trigger and workflow definition

Check the workflow location, YAML validity, event, branch/path filters, and whether the workflow is enabled.


19. What to Learn Next

After completing this tutorial, you should be comfortable with:

  • workflows;
  • triggers;
  • jobs;
  • steps;
  • runners;
  • Actions;
  • variables;
  • contexts;
  • expressions;
  • secrets;
  • GITHUB_TOKEN;
  • permissions;
  • matrix builds;
  • caching;
  • artifacts;
  • basic CI/CD flow;
  • environments;
  • security basics;
  • troubleshooting basics.

The next learning sequence should be:

GitHub Actions Essentials
        |
        v
Reusable Workflows
        |
        v
Advanced Security
        |
        v
OIDC / Cloud Authentication
        |
        v
Docker CI/CD
        |
        v
Cloud / Kubernetes Deployment
        |
        v
Infrastructure as Code
        |
        v
Self-hosted Runners / ARC
        |
        v
Organization and Enterprise Governance
Code language: PHP (php)

19.1 Recommended next topics

Reusable automation

Learn:

  • reusable workflows;
  • composite actions;
  • workflow inputs;
  • workflow outputs;
  • centralized CI standards.

Advanced security

Learn:

  • pull_request vs pull_request_target;
  • third-party Action pinning;
  • cache poisoning;
  • supply-chain trust;
  • branch protection;
  • rulesets;
  • environment protection.

Cloud authentication

Learn OpenID Connect so workflows can obtain short-lived cloud credentials instead of storing long-lived cloud access keys.

Containers

Learn:

  • Docker build and push;
  • container registries;
  • image tags and digests;
  • image scanning;
  • SBOMs;
  • provenance.

Kubernetes

Learn:

  • secure cluster authentication;
  • Helm or Kustomize;
  • rollout verification;
  • namespace and identity isolation;
  • GitOps patterns.

Infrastructure as Code

Learn a controlled pipeline such as:

fmt
 |
 v
validate
 |
 v
security checks
 |
 v
plan
 |
 v
review / approval
 |
 v
apply

Runner architecture

Learn:

  • self-hosted runners;
  • ephemeral runners;
  • runner groups;
  • private networking;
  • Actions Runner Controller;
  • trust-zone separation.

Final Summary

If you remember only one model, remember this:

Event
  |
  v
Workflow
  |
  v
Jobs
  |
  v
Runners
  |
  v
Steps
  |
  v
Actions / Commands

And if you remember only one production pipeline shape, remember this:

Change
  |
  v
CI Trigger
  |
  v
Checkout
  |
  v
Install
  |
  v
Lint
  |
  v
Test
  |
  v
Build
  |
  v
Artifact
  |
  v
Protected Deployment
Code language: PHP (php)

And if you remember only one security principle:

Give every workflow, job, runner, Action, and credential only the access it actually needs.

That foundation is enough to start building useful GitHub Actions workflows safely and confidently.

Find Trusted Cardiac Hospitals

Compare heart hospitals by city and services — all in one place.

Explore Hospitals
I'm Rajesh Kumar, a DevOps, SRE, DevSecOps, Cloud, and Platform Engineering expert passionate about sharing practical knowledge, real-world experiences, and industry best practices. I have worked at Cotocus and regularly write about technology, travel, investing, health, product reviews, and digital marketing through my various platforms. I publish technical articles at DevOps School, travel stories at Holiday Landmark, stock market insights at Stocks Mantra, health and fitness guidance at My Medic Plus, product reviews at TrueReviewNow, and SEO and digital marketing strategies at Wizbrand.

Related Posts

GitHub Organization Administration Essentials — 2-Hour Tutorial

Last Verified: September 2026Based on: GitHub Organization Administration — Complete Reference Guide and TutorialDuration: 2 hoursAudience: GitHub organization owners, DevOps/platform engineers, developers with delegated administration duties, security administrators, and technical trainers….

Read More

GitHub Packages Essentials — 2-Hour Hands-On Tutorial

Last Verified: September 2026Scope: GitHub.com / GitHub Enterprise Cloud unless explicitly stated otherwiseDuration: ~2 hours including hands-on practiceAudience: Developers, DevOps engineers, platform engineers, administrators, trainers, and studentsPrimary hands-on registry: GitHub Container Registry…

Read More

GitHub Projects Essentials — 2-Hour Tutorial & Hands-On Guide

Scope: Current GitHub Projects / Projects v2, not Projects (classic).Audience: Developers, DevOps engineers, engineering managers, product managers, project administrators, and GitHub organization members.Training duration: Approximately 2 hours, including guided practice.Prerequisite: Basic familiarity with…

Read More

GitHub Actions — CI/CD Automation & DevOps Engineering

Course Duration 2 Days | 16 Hours Format: Instructor-Led Training + Hands-on Labs + Real-World CI/CD Project Offered By DevOpsSchool Website: DevOpsSchool GitHub Actions — CI/CD Automation…

Read More

Top 10 AI UI-to-Code Generators: Features, Pros, Cons & Comparison

Introduction AI UI-to-Code Generators help designers, developers, product teams, and startups convert visual designs, screenshots, wireframes, Figma files, sketches, or natural language prompts into usable frontend code….

Read More
Subscribe
Notify of
guest
0 Comments
Newest
Oldest Most Voted
0
Would love your thoughts, please comment.x
()
x