Skip to main content

Lesson: Pipelines with GitHub Actions

What you'll learn

  • The building blocks of GitHub Actions: workflows, jobs, steps, and runners — and how they nest.
  • How triggers (the on: key) decide when a workflow runs.
  • How to read and write a real workflow YAML file.
  • How to build a container image and push it to the lab's private registry using the Gitea Actions runner.

By the end you can write a working build-and-push-image pipeline and run it on the lab.

The lesson

GitHub Actions is a CI/CD system built into GitHub. You describe your pipeline in a YAML file inside your repo, and GitHub runs it for you on triggers like "someone pushed code." The big reason we teach it: its workflow syntax is widely copied. The lab's Gitea server has an Actions runner that understands the same syntax, so everything you learn here runs for real on the lab — no GitHub account needed.

1. The four building blocks

GitHub Actions has a clear hierarchy. Define these terms once and the rest is easy:

  • Workflow — the whole automated process, defined in one YAML file in .github/workflows/ (Gitea uses .gitea/workflows/). A repo can have many.
  • Job — a named unit of work inside a workflow. Jobs run on a fresh machine and, by default, run in parallel with each other.
  • Step — a single command or action inside a job. Steps run in order on the same machine and share its filesystem.
  • Runner — the machine that actually executes a job. In the lab, the runner lives on the build VM (10.100.100.11).
 Workflow (build-and-push.yml)
 |
 +-- Job: build
 |    |
 |    +-- Step 1: checkout code
 |    +-- Step 2: log in to registry
 |    +-- Step 3: build image
 |    +-- Step 4: push image
 |
 +-- Job: notify   (could run in parallel)

A step is either a shell command (run:) or a reusable pre-built action (uses:). An action is a packaged piece of automation someone else wrote — for example actions/checkout clones your repo.

2. Triggers — the on: key

The on: key says when the workflow runs. Common triggers:

on:
  push:
    branches: [ main ]        # run when code is pushed to main
  pull_request:               # run on every PR (great for CI tests)
  workflow_dispatch:          # add a manual "Run" button
  schedule:
    - cron: '0 2 * * *'       # run nightly at 02:00

You can combine them. A common pattern is: run tests on every pull_request, but only build-and-push an image on push to main.

3. A minimal CI workflow

Here is a complete, readable workflow that runs tests on every push. Read it top to bottom:

name: CI                      # shown in the UI

on:
  push:
  pull_request:

jobs:
  test:                       # job id
    runs-on: ubuntu-latest    # which runner to use
    steps:
      - name: Check out the code
        uses: actions/checkout@v4

      - name: Run the tests
        run: |
          echo "Running tests..."
          ./run-tests.sh

runs-on: picks the runner. On GitHub this would be a GitHub-hosted machine; on the lab it selects the Gitea runner on 10.100.100.11. The | after run: lets you write a multi-line script.

4. Secrets — never hardcode credentials

To push to a registry you need a password, but you must never write passwords in YAML. Instead store them as secrets (in repo settings) and reference them with ${{ secrets.NAME }}. They are encrypted and masked in logs:

      - name: Log in to the registry
        run: echo "${{ secrets.REGISTRY_PASSWORD }}" | \
             docker login 10.100.100.6 \
             -u "${{ secrets.REGISTRY_USER }}" --password-stdin

If you ever see a real password in a YAML file, that is a bug — flag it.

5. Build-and-push image workflow on the lab

This is the workflow you will actually build in Assignment 1. It builds a container image and pushes it to the lab's private registry at 10.100.100.6. The runner on 10.100.100.11 uses Docker-out-of-Docker (DooD) — meaning the job talks to the host's Docker daemon directly, so plain docker build works.

name: build-and-push

on:
  push:
    branches: [ main ]

jobs:
  image:
    runs-on: ubuntu-latest
    steps:
      - name: Check out the code
        uses: actions/checkout@v4

      - name: Log in to the private registry
        run: echo "${{ secrets.REGISTRY_PASSWORD }}" | \
             docker login 10.100.100.6 \
             -u "${{ secrets.REGISTRY_USER }}" --password-stdin

      - name: Build the image
        run: docker build -t 10.100.100.6/myapp:${{ github.sha }} .

      - name: Push the image
        run: docker push 10.100.100.6/myapp:${{ github.sha }}

Two things to notice:

  • ${{ github.sha }} is the commit's unique ID. Tagging the image with the commit SHA means every build produces a uniquely named image — you always know exactly which commit an image came from. This matches the lab convention of sha-based tags. (Avoid the :latest tag for deploys; it's ambiguous.)
  • The whole job runs on 10.100.100.11. The . in docker build is the build context — the directory containing your Dockerfile.

6. Expressions, variables, and the job lifecycle

A few more pieces you'll see:

  • ${{ ... }} expressions read context data — github.sha, github.ref (which branch/tag), secrets.*, and outputs from earlier steps.
  • env: sets environment variables at workflow, job, or step level.
  • needs: makes one job wait for another, so you can chain test -> build -> deploy:
jobs:
  test:
    runs-on: ubuntu-latest
    steps: [ { run: ./run-tests.sh } ]
  build:
    needs: test          # only runs if 'test' succeeded
    runs-on: ubuntu-latest
    steps: [ { run: docker build -t myapp . } ]

Each job starts on a clean machine, so anything one job produces and another needs (like a built file) must be passed explicitly — using artifacts (actions/upload-artifact / download-artifact) or, for images, by pushing to the registry. That registry image is the handoff point to the deploy step you'll build with Argo CD in the next chapters.

Dig deeper

Search terms

  • github actions workflow jobs steps explained
  • github actions on push pull_request trigger
  • github actions build and push docker image
  • gitea act runner setup tutorial
  • github actions secrets best practices
  • github actions needs depend on another job

Check yourself

  1. Put these in order from largest to smallest: step, workflow, job. What does a runner do?
  2. Which key decides when a workflow runs, and how would you make it run only on pushes to main?
  3. What is the difference between a run: step and a uses: step?
  4. Why tag the image with ${{ github.sha }} instead of latest?
  5. How should you provide the registry password to the workflow, and why must it not appear in the YAML?