CI/CD & Software Supply Chain›01 · The execution model
Learning Hub / Delivery & Infrastructure as Code / CI/CD & Software Supply Chain

Lesson 01 of 8 · Modules

The execution model

How GitHub Actions actually runs your code: events trigger workflows, jobs run on runners (in parallel unless linked), steps share a workspace, contexts and expressions carry data, and caches, artifacts, permissions and concurrency control speed and safety.

Beginner → Practitioner
Key wordsGitHub ActionsworkflowseventsjobsstepsrunnerscontextsexpressionsGITHUB_TOKEN permissionscacheartifactsconcurrency
Event push · PR · tag Workflow job: test runner A checkout → test job: build runner B image → registry job: deploy environment: prod approval, OIDC needs Registry image@digest GitOps repo new tag → Argo CD
An event starts a workflow; jobs run on runners; the result lands in the registry and GitOps repo.

The moving parts

  • A workflow is triggered by events (push, pull_request, workflow_dispatch for manual runs, schedule with cron in UTC, and many more).
  • A workflow has jobs. Each job runs on a runner chosen by runs-on labels: GitHub-hosted (ubuntu-latest) or self-hosted (your machines or Kubernetes, lesson 03).
  • Jobs run in parallel unless linked with needs. Each gets a fresh workspace.
  • A job's steps run in order on the same runner and share files. A step either runs a shell command (run:) or uses an action (uses:).

A workflow is a recipe card, and the event is someone saying "start cooking!". Each job is a separate cook at their own kitchen counter (runner); they can work at the same time, but they don't share bowls. If the baker needs the frosting from the other cook, it has to be handed over (an artifact or output).

A first workflow

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

permissions:
  contents: read

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

  build:
    needs: test
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - id: meta
        run: echo "version=$(git describe --tags --always)" >> "$GITHUB_OUTPUT"
      - run: tar czf app.tgz src/
      - uses: actions/upload-artifact@v4
        with:
          name: app
          path: app.tgz

(Major-version tags like @v4 keep examples readable. Lesson 06 explains why production workflows pin actions to a full commit SHA.)

Contexts and expressions

${{ … }} expressions read contexts: github (event, ref, sha, actor), env, vars (configuration variables), secrets, matrix, needs, steps, runner, inputs. They also drive conditions:

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

Never paste untrusted input into run: with an expression. Pass it via env: instead:

      - env:
          TITLE: ${{ github.event.pull_request.title }}
        run: echo "PR title is $TITLE"

Speed and safety controls

Control Use
Cache (actions/cache, or cache: in setup actions) Dependencies keyed by lockfile hash
Artifacts Pass files between jobs; keep reports and binaries
permissions Least-privilege GITHUB_TOKEN per workflow or job
concurrency One deploy at a time; cancel superseded PR runs
timeout-minutes Stop hung jobs (the default is very long)
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true      # for PR builds; use false for deployments

Try it: your first pipeline (a GitHub repo you own)

  1. Add the workflow above to a small Python (or any language) repo; push and watch both jobs.
  2. Break a test; confirm build is skipped because it needs: test.
  3. Add a workflow_dispatch trigger with an input, and print it safely via env:.
  4. Push twice quickly on a PR branch with the concurrency block, and see the first run cancelled.
  5. Compare run times with and without cache: pip.

Going deeper: execution details that bite

  • pull_request from forks runs without secrets and with a read-only token. pull_request_target runs in the base repo's context with secrets, so never check out and run the PR's code in it.
  • Scheduled workflows run on the default branch only, and can be delayed at busy times.
  • Set default workflow permissions to read-only in the organisation settings, then grant more per workflow.
  • Use environments for deployments (lesson 02): protection rules, required reviewers and environment-scoped secrets.

Recap

  • Events → workflows → jobs (on runners) → steps; jobs run in parallel unless linked with needs.
  • Contexts and expressions carry data; pass untrusted values through env:, never straight into run:.
  • Cache for speed, artifacts/outputs between jobs, permissions for least privilege, concurrency for safe deploys.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.