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:latesttag for deploys; it's ambiguous.)- The whole job runs on
10.100.100.11. The.indocker buildis the build context — the directory containing yourDockerfile.
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 chaintest -> 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
- GitHub Actions — Workflow syntax reference
- GitHub Actions — Events that trigger workflows
- GitHub Actions — Using secrets in a workflow
- Gitea — Act Runner documentation
- actions/checkout on GitHub
Search terms
github actions workflow jobs steps explainedgithub actions on push pull_request triggergithub actions build and push docker imagegitea act runner setup tutorialgithub actions secrets best practicesgithub actions needs depend on another job
Check yourself
- Put these in order from largest to smallest: step, workflow, job. What does a runner do?
- Which key decides when a workflow runs, and how would you make it run only on pushes to
main? - What is the difference between a
run:step and auses:step? - Why tag the image with
${{ github.sha }}instead oflatest? - How should you provide the registry password to the workflow, and why must it not appear in the YAML?
No comments to display
No comments to display