CI/CD Pipelines with Jenkins or GitHub Actions

Chapter 6
GitHub Actions Fundamentals

Diploma in DevOps
CI/CD Pipelines with Jenkins or GitHub Actions   •   Diploma

GitHub Actions Fundamentals

Introduction

Continuous integration and continuous delivery (CI/CD) automate the path from a change in source code to a tested, approved, and deployable software release. GitHub Actions is GitHub’s event-driven automation platform. It enables teams to define workflows in YAML files stored in a repository and execute them on hosted or self-hosted runners.

A GitHub Actions workflow can compile software, run tests, build container images, publish packages, deploy infrastructure, generate documentation, and notify stakeholders. Its value does not come merely from replacing manual commands. Properly designed workflows make software delivery repeatable, observable, auditable, and easier to govern.

This chapter develops the foundational concepts needed to create professional workflows: events, YAML syntax, jobs, steps, actions, runners, contexts, expressions, matrices, artifacts, environments, secrets, permissions, and reusable workflows.

Learning Objectives

By the end of this chapter, a learner should be able to:

  • explain the architecture and execution model of GitHub Actions;
  • create event-driven workflows using valid YAML;
  • distinguish workflows, jobs, steps, actions, and runners;
  • use contexts and expressions safely;
  • apply matrices for multi-version or multi-platform testing;
  • preserve build outputs with artifacts;
  • protect deployments with environments and approvals;
  • manage secrets and the principle of least privilege;
  • design reusable workflows for consistent organizational automation; and
  • evaluate security, maintainability, and operational limitations.

1. The GitHub Actions Execution Model

A workflow is an automated process defined in a YAML file under .github/workflows/. A workflow starts when one or more specified events occur. It contains one or more jobs. Each job runs on a runner and contains ordered steps. A step may execute a shell command or invoke a reusable action.

The relationship can be expressed as:

event → workflow → job → runner → steps → commands or actions

A job is normally isolated from other jobs. If two jobs must exchange files, the workflow should explicitly use artifacts, caches, or another storage mechanism. Jobs can run in parallel unless dependencies are declared with needs.

A minimal workflow is:

name: Continuous Integration

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

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Check out source
        uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run tests
        run: pytest

The name field gives the workflow a human-readable label. The on field defines triggers. permissions limits the workflow token. The jobs mapping defines executable units. runs-on selects the runner, while uses invokes an action and run executes a command.

2. Events and Event Filters

An event is a change or request that can start a workflow. Common events include:

EventTypical use
pushTest changes committed to a branch
pull_requestValidate proposed changes before merging
workflow_dispatchStart a workflow manually
scheduleRun periodic maintenance or scans
releasePublish or deploy a release
workflow_callInvoke a reusable workflow
workflow_runStart work after another workflow completes

Filters reduce unnecessary executions. For example:

on:
  push:
    branches: [main, develop]
    paths:
      - 'src/**'
      - 'tests/**'
      - 'requirements.txt'

A manually started workflow may define typed inputs:

on:
  workflow_dispatch:
    inputs:
      environment:
        description: Deployment target
        required: true
        type: choice
        options: [staging, production]

Event choice is an architectural decision. Pull-request workflows should provide rapid feedback and should not normally receive write permissions or production secrets. Production deployment is better associated with a protected branch, release, tag, or explicitly approved manual operation.

3. YAML Structure, Jobs, and Steps

YAML uses indentation to represent structure. Spaces, not tabs, should be used. A malformed indentation level can prevent a workflow from loading, so validation should be part of development practice.

Jobs can be sequential:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./build.sh

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh

Here, deploy starts only after build succeeds. A job may depend on several jobs:

needs: [build, security_scan]

By default, a failed dependency prevents dependent jobs from running. Conditional expressions such as if: ${{ always() }} can be used for cleanup or reporting, but broad use can accidentally deploy after a failed build.

Steps execute in order within a job. A step can have an identifier, environment variables, a condition, a timeout, and a working directory:

- name: Run integration tests
  working-directory: service
  env:
    TEST_MODE: integration
  timeout-minutes: 15
  run: ./run-integration-tests.sh

A step’s shell command executes on the selected operating system. Shell syntax therefore matters: Bash commands are not automatically portable to Windows PowerShell.

4. Actions and Runners

An action is a packaged unit of automation. It may be maintained by GitHub, a vendor, a community, or the organization itself. Examples include actions/checkout, language setup actions, cloud-provider authentication actions, and artifact actions.

Action references should be governed carefully. A major version such as actions/checkout@v4 receives compatible updates within that major line, while a full commit SHA gives stronger immutability and provenance. Organizations should review third-party actions, restrict allowed actions where appropriate, and monitor changes.

A runner is the machine or execution environment that performs a job. GitHub-hosted runners are ephemeral virtual machines. Self-hosted runners are managed by the organization and may provide specialized hardware or private network access, but they introduce patching, isolation, credential, and lifecycle responsibilities.

A runner does not necessarily preserve files between jobs. If a later job requires a file created by an earlier job, use an artifact or regenerate the file. Caches are intended primarily to accelerate dependency retrieval; they should not be treated as the authoritative storage of build outputs.

5. Contexts and Expressions

Contexts provide event and execution data. Important contexts include:

ContextExamples of information
githubRepository, branch, event name, commit SHA
envWorkflow, job, or step environment variables
varsOrganization, repository, or environment variables
secretsEncrypted secret values
stepsOutputs from earlier steps
needsOutputs and results of dependent jobs
matrixCurrent matrix combination
runnerOperating system and runner metadata
inputsManual or reusable-workflow inputs

Expressions are enclosed in ${{ }}. For example:

- name: Identify event
  run: echo "Event is ${{ github.event_name }}"

- name: Deploy only from main
  if: ${{ github.ref == 'refs/heads/main' }}
  run: ./deploy.sh

Some fields automatically evaluate expressions, while explicit expression syntax improves clarity. Boolean, comparison, logical, and function operations are available. A common condition is:

if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}

Expressions should not be confused with shell interpolation. ${{ github.sha }} is evaluated by GitHub before the command runs; $HOME is interpreted by the shell on the runner.

A step can publish an output through the environment file:

- id: version
  run: echo "value=1.4.0" >> "$GITHUB_OUTPUT"

- run: echo "Version is ${{ steps.version.outputs.value }}"

Do not place untrusted issue titles, pull-request text, or commit messages directly into shell code. Treat event data as input and quote or validate it to reduce injection risk.

6. Matrix Strategies

A matrix runs a job for each combination of declared values. It is useful for testing supported language versions, operating systems, or database versions.

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: ['20', '22']
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test

This configuration creates four job instances: two operating systems × two Node.js versions. In general, if a matrix has dimensions with cardinalities n₁, n₂, …, nₖ, the number of combinations is:

N = ∏ᵢ₌₁ᵏ nᵢ

Here, N is the number of generated job instances and nᵢ is the number of values in dimension i. The formula assumes no exclusions or additional combinations. Matrices improve coverage but increase cost and queue time. Use include to add special combinations and exclude to remove unsupported ones.

7. Artifacts, Caches, and Job Outputs

An artifact is a named file or directory retained from a workflow run. Typical artifacts include test reports, compiled packages, coverage reports, and deployment manifests.

- name: Build package
  run: npm run build

- name: Upload package
  uses: actions/upload-artifact@v4
  with:
    name: web-package
    path: dist/
    retention-days: 14

A later job can download it:

- uses: actions/download-artifact@v4
  with:
    name: web-package
    path: dist/

Artifacts are suitable for transferring explicit outputs between jobs. Caches, commonly implemented with dependency-cache actions, are optimization mechanisms. A cache miss should not break a correct build. Job outputs are better for small values such as a version string, not large files.

8. Environments, Secrets, and Permissions

An environment represents a deployment target such as staging or production. Environment rules can require reviewers, restrict branches, and expose environment-specific secrets only after protection requirements are satisfied.

deploy:
  environment:
    name: production
    url: https://example.org
  runs-on: ubuntu-latest
  steps:
    - run: ./deploy.sh

Secrets are encrypted values supplied through repository, organization, or environment configuration:

env:
  CLOUD_TOKEN: ${{ secrets.CLOUD_TOKEN }}

Secrets should not be printed, placed in source code, embedded in artifacts, or passed to untrusted pull-request code. Secret masking reduces accidental disclosure but is not a substitute for careful design. Forked pull requests generally do not receive ordinary repository secrets, which protects the repository but may require a separate trusted validation design.

The automatically provided GITHUB_TOKEN should have the smallest necessary permission set:

permissions:
  contents: read
  pull-requests: write

Permissions can be set at workflow or job level. A read-only default is a sound baseline. Grant write access only to the job that requires it. Production authentication should preferably use short-lived, federated credentials such as OpenID Connect rather than long-lived cloud keys.

9. Reusable Workflows

Reusable workflows reduce duplication and standardize organizational practices. They are invoked with workflow_call:

on:
  workflow_call:
    inputs:
      language:
        required: true
        type: string
    secrets:
      registry_token:
        required: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "Building ${{ inputs.language }}"

A caller uses:

jobs:
  standard-build:
    uses: company/platform/.github/workflows/build.yml@v1
    with:
      language: Python
    secrets: inherit

Reusable workflows should have clear contracts: typed inputs, declared secrets, documented outputs, stable version references, and explicit permissions. secrets: inherit is convenient but should be used only when the called workflow is trusted and its secret requirements are understood.

10. A Complete Applied Scenario

Illustrative case: Northstar Inventory Service

Background. Northstar maintains a Python inventory API. Developers submit pull requests, and the operations team deploys approved changes to staging and production.

Problem. Tests were run inconsistently on personal machines. Deployment was manual, credentials were shared, and a failed release was difficult to trace.

Analysis. The team identified four control points: dependency installation and unit tests, security scanning, creation of an immutable package, and protected deployment. It also determined that pull requests must not receive production credentials.

Approach. Northstar implemented a workflow with separate validation and deployment jobs. Validation runs on pull requests and pushes to main. The build job uploads a package artifact. Deployment requires the build and scan jobs, uses the production environment, and obtains short-lived cloud credentials through an approved identity mechanism. Workflow-level permissions are read-only; deployment receives only the additional permission it needs.

A simplified dependency structure is:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install -r requirements.txt
      - run: pytest

  package:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./package.sh
      - uses: actions/upload-artifact@v4
        with:
          name: inventory-package
          path: output/

  deploy:
    if: ${{ github.ref == 'refs/heads/main' }}
    needs: package
    environment: production
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: inventory-package
          path: output/
      - run: ./deploy.sh output/

Outcome. Every main-branch deployment is traceable to a commit and artifact. Pull requests receive automated feedback without production access. Approval rules provide separation of duties, while job boundaries make failures easier to diagnose.

Learning points. Automation is also governance. Correct sequencing, explicit permissions, protected environments, and artifact traceability are as important as the test command itself.

11. Limitations, Governance, and Best Practices

GitHub Actions introduces practical constraints. Hosted runners are temporary, network access can vary, concurrency can delay feedback, and third-party actions create supply-chain risk. YAML is expressive but can become difficult to maintain when workflows contain duplicated, implicit, or deeply nested logic.

Recommended practices include:

  1. Keep workflows small and purpose-oriented.
  2. Pin or centrally govern action versions.
  3. Use least-privilege permissions.
  4. Separate pull-request validation from privileged deployment.
  5. Protect production with environments and required reviewers.
  6. Use artifacts for explicit outputs and caches only for acceleration.
  7. Test workflows with linting, local simulation where appropriate, and controlled branches.
  8. Set timeouts and concurrency policies to prevent stale deployments.
  9. Record provenance: commit, workflow run, artifact, approver, and deployment target.
  10. Review logs for accidental disclosure and avoid printing environment values.
  11. Use self-hosted runners only with strong isolation, patching, and workload controls.
  12. Reuse stable workflow components, but version their interfaces deliberately.

Artificial intelligence can assist with generating YAML, explaining failures, or proposing test cases. However, generated workflows must be reviewed for excessive permissions, unsafe shell interpolation, leaked secrets, incorrect event assumptions, and unverified third-party actions. Human review remains essential because workflow code controls access to source code, credentials, and production systems.

Chapter Summary

GitHub Actions is an event-driven CI/CD platform in which events start workflows, workflows contain jobs, jobs execute on runners, and jobs contain ordered steps. Actions package reusable automation, while shell steps execute project-specific commands. Contexts and expressions provide controlled access to event, matrix, job, and secret data. Matrices expand testing across supported combinations, artifacts transfer durable outputs, and caches accelerate repeatable dependency retrieval. Environments add deployment protection and approval, secrets protect sensitive values, and permissions enforce least privilege. Reusable workflows establish organizational standards. Professional implementations combine technical correctness with security, traceability, separation of duties, and careful treatment of untrusted input.

Key Terms

Workflow: A YAML-defined automated process.

Event: A condition or activity that triggers a workflow.

Job: An isolated unit of work within a workflow.

Step: An ordered operation within a job.

Action: A reusable package of automation.

Runner: The machine or environment executing a job.

Context: Structured GitHub Actions data available to a workflow.

Expression: An evaluated condition or value written using ${{ }} syntax.

Matrix: A strategy that creates job variations from combinations of values.

Artifact: A retained file or directory produced by a workflow.

Cache: Stored data used to accelerate later executions.

Environment: A deployment target with rules, variables, and secrets.

Secret: An encrypted sensitive configuration value.

Permission: An allowed capability granted to the workflow token.

Reusable workflow: A workflow callable by another workflow.

needs: A dependency declaration between jobs.

OpenID Connect: A mechanism for obtaining short-lived federated credentials.

Least privilege: Granting only the access required for a task.

Review Questions

Short Answer Questions

  1. What is the relationship between a workflow, job, step, and runner?
  2. Why should caches not be treated as authoritative build storage?
  3. What problem does a matrix strategy solve?
  4. How do environments improve production deployment governance?
  5. Why is least-privilege permission design important in GitHub Actions?

Long Answer Questions

  1. Explain how events, filters, jobs, dependencies, and runners combine to execute a CI workflow.
  2. Compare artifacts, caches, and job outputs, including an appropriate use for each.
  3. Describe how contexts and expressions can control conditional execution, and identify risks when handling untrusted event data.
  4. Explain the design of a secure pull-request validation and production-deployment workflow.
  5. Discuss the advantages and limitations of reusable workflows in a large organization.

Analytical / Application Questions

  1. Design a matrix strategy for testing a service on three operating systems and two supported language versions. State the number of generated combinations and discuss the cost implications.
  2. A deployment job currently has repository write permissions and receives production secrets on every pull request. Identify the risks and propose a safer event, permission, environment, and secret design.
  3. A build job creates a package needed by a later deployment job. Explain why relying on the runner’s filesystem is unreliable and design an artifact-based solution.

Multiple Choice Questions with Answers

Question 1

Which GitHub Actions component defines when automation starts?

A. A runner

B. An event

C. An artifact

D. A matrix

Correct Answer: B. An event

Question 2

What does the needs keyword primarily define?

A. A job dependency

B. A secret value

C. A runner operating system

D. An artifact retention period

Correct Answer: A. A job dependency

Question 3

Which mechanism is most appropriate for transferring a package between separate jobs?

A. A cache

B. A shell alias

C. An artifact

D. A repository label

Correct Answer: C. An artifact

Question 4

What is the principal purpose of a matrix strategy?

A. To encrypt secrets

B. To create job variations from value combinations

C. To approve production deployments

D. To change YAML into JSON

Correct Answer: B. To create job variations from value combinations

Question 5

Which permission design follows least privilege?

A. Grant write access to every job

B. Use read-only permissions by default and add narrowly required access

C. Disable all workflow logs

D. Store credentials in source code

Correct Answer: B. Use read-only permissions by default and add narrowly required access

Question 6

Which feature can require reviewer approval before production deployment?

A. An environment

B. A cache key

C. A step identifier

D. A runner label

Correct Answer: A. An environment

Question 7

Where are reusable workflows designed to be invoked from?

A. An on.workflow_call trigger

B. A matrix.include list

C. An artifact manifest

D. A shell comment

Correct Answer: A. An on.workflow_call trigger

Question 8

What is a major security risk of inserting untrusted pull-request text directly into a shell command?

A. Matrix expansion

B. Command injection

C. Artifact compression

D. Runner autoscaling

Correct Answer: B. Command injection

Question 9

A matrix has 2 operating systems and 3 language versions, with no exclusions. How many job instances are generated?

A. 2

B. 3

C. 5

D. 6

Correct Answer: D. 6

Question 10

Which credential approach generally reduces the need for long-lived cloud keys?

A. OpenID Connect with short-lived credentials

B. Printing a token in the log

C. Committing a password to YAML

D. Sharing one permanent administrator key

Correct Answer: A. OpenID Connect with short-lived credentials

CI/CD Pipelines with Jenkins or GitHub Actions
Chapter 6 of 14