MLOps, Session 2: from a push to a published image
A workflow is a list of shell commands that GitHub runs for you, on a clean machine, every time something happens in your repository.
Business corner
GitHub’s Octoverse 2025 counts 180 million developers, 43.2 million pull requests merged per month and almost 1 billion commits in a year. Each of them can start a workflow.
Every row is one run. Green check: passed. Red cross: failed. Orange dot: waiting for a machine. Example: the public repository of uv itself.
This is the workflow you write in the lab, read from the file itself.
name: docker
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Build
run: docker build -t fraud-scorer:ci .
- name: Smoke test
run: docker run --rm -v "$PWD:/data" fraud-scorer:ci ...mainsample.csv; if it crashes, the job turns red and stops thereA step fails when its command exits with an error. That is the only rule.
Same keywords as your lab. On line 30 the action is pinned to a commit, with its version in a comment.
actions/checkout is the step every workflow starts with. The page tells you the latest version (v7) and what changed in it.
push: every commit on a branch. Build and publish the image from mainpull_request: before merging. Lint, tests, data validationschedule: a cron time. Nightly drift check on fresh dataworkflow_dispatch: a button, or an API call. Retrain on demand; Airflow can call itrelease: a tag. Promote an image to productionBusiness corner
Since March 2026, a scheduled workflow can run in your own timezone, so “08:00 in Paris” no longer drifts with daylight saving.
The summary shows who triggered it, the commit, the status and the duration. The left column has one line per job; click one to read its steps and logs.
GitHub starts three jobs in parallel, one per value. For ML: the same tests on several Python versions, or the same model check on several data slices.
actions/download-artifactIn session 2 you saw CACHED when you rebuilt an image. On a runner it will not appear: the machine is new every time.
The cache lives in GitHub, outside the runner, and survives it.
secrets.NAME: stored encrypted in the repo settings, injected into one step, masked in logsGITHUB_TOKEN: a free token that exists for one run; this is what pushed your image to GHCRpermissions:: shrink that token to what the job needs; the lab asks for contents: read and packages: writeecho a secret, never put one in the repo, never bake one into an imageMasking is best effort: a secret that is transformed (base64, split) is no longer recognised in the log.
The workflow needs id-token: write. The cloud trusts a repository and a branch, not a password.
An action is someone else’s code running with your secrets.
tj-actions/changed-files to a malicious commit; tens of thousands of workflows pulled it@v4 can be moved; a full commit SHA cannotcheckout v7 refuses to check out fork pull requests under pull_request_target; workflow lockfiles are in previewCode is tested on pull requests, models are trained and gated, images are built and approved, retraining is triggered by drift.
Rules live in Settings, Environments: required reviewers, a wait timer, allowed branches. The same image goes to staging and production: only the approval differs.
docker build could notuv sync --locked fails when pyproject.toml and uv.lock disagreelatest: an image maps back to the code and data that made itA workflow that nobody has to respect protects nothing.
mainruff, test, build)Result: no green check, no merge. The workflow of the lab becomes a rule of the team.
The red cross sits on the test job; the jobs above it are green. The run page tells you exactly where to look first.
A team pushes 20 times a day and a run takes 3 minutes:
3 min x 20 pushes x 22 working days = 1,320 minutes a month
1,320 min x 0.006 dollars per Linux minute = 7.92 dollars
Three lines keep the bill small:
Business corner
Self-hosted runners are the way to run CI on your own GPU machines. A fee on them would change the build-or-buy maths, so watch this one.
The Packages page of a repository lists the image tags and the pull command. In your lab, the tag is the first 7 characters of the commit hash, so each image points back to its code.
docker.yml, push, open the Actions tab and read each step’s logpermissions and the GHCR step; the Packages sidebar shows an image tagged with the commitDockerfile and watch the build fail; make predict.py crash and watch the smoke test failDeliverable: a green run, a package in GHCR, and the sentence “the build passed but the smoke test failed” explained in your own words.
Find at least six problems.
on: push: every branch, every commit; no paths, no concurrency@main and @v1: moving references, one of them from an unknown author; pin a SHApip install without versions: use uv sync --lockedecho of a secret leaks the key into the logslatest: tag with the commit; and no test, no timeout-minutes, no permissionsbuild:
image: docker:27
services: [docker:27-dind]
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER"
--password-stdin "$CI_REGISTRY"
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
rules:
- if: $CI_COMMIT_BRANCH == "main".gitlab-ci.yml; jobs are grouped in stagesbitbucket-pipelines.yml; steps run in orderWhat you learn here transfers: a trigger, a clean machine, commands in order, a verdict.
Albert School, GitHub Actions