Skip to content

From commit to release

  • Lesson 1
  • 40 min
  • Module 1: Understand the delivery path
Lesson 1 of 15 · CI/CD, Testing and Safe ReleasesFrom commit to release

What you will be able to do

A change is not finished when it works on your computer. It is finished when it reaches users, through steps that a machine repeats the same way every time. Learn those steps on one real change, and see what stops a bad change at each one.

Before you startWhat to know and to have readyAbout 40 minutes

What you need to know

What you need on your computer

  • A web browser. Everything in this lesson is on public pages of the reference repository Nextia-Intelligence/ticket-api-ci. You do not need to sign in.
  • For the optional commands: a terminal with curl and Python 3 (from Build and Deploy Your First API). They read GitHub’s public API, which needs no account.
  • Your ticket API from the Docker course, in ~/projects/ticket-api (Windows: $HOME\projects\ticket-api). This lesson does not change it. If it is missing or broken, copy the Docker course’s snapshot end-of-m05, as in Use a snapshot.
  • You do not need a GitHub account or Azure yet. Module 2 makes your own repository, with a free GitHub account. Azure is optional in the whole course.

Run with GitHub Actions (ubuntu-24.04 runners) in the public repository Nextia-Intelligence/ticket-api-ci, and Azure CLI 2.88.0 in canadacentral. The commands of this lesson were run with curl and Python 3.14 on macOS. The Windows steps are not tested.

Amira owns ticket-api, the support-ticket API from the API course and the Docker course. On 2026-10-07 at 18:20, she pushed one commit: a better classifier, keywords-1.1, switched off by default. At 18:26, production ran it. Between those two times, no person built anything, copied a file or typed a deployment command. Amira did two things: she merged a pull request, and she approved one deployment.

A machine did the rest, in a fixed order:

  1. It checked the change on the branch.
  2. After the merge, it checked the change again on main, and built one image.
  3. It deployed that image to a test copy of the service, and checked it there.
  4. After Amira’s approval, it deployed the same image to production, and checked it again.

Each step can stop the change. If a check fails, the next step does not start.

CI, continuous integration, is the first half: every change is merged into the main branch often, and a machine checks it on every push. CD is the second half: a machine takes the checked result to the environments where it runs. A pipeline is the whole automated path, from the commit to production. This course builds a pipeline for the ticket API, and every lesson shows the real one in the reference repository.

CD has two meanings, and teams mix them up:

Continuous delivery Continuous deployment
After the checks pass Every change can go to production. Every change goes to production.
Who decides about production A person approves. The pipeline. Nobody approves.
In this course Yes: production waits for a reviewer. No.

The ticket API uses continuous delivery. The help desk depends on its priorities, and a new classifier changes them. A person reads the staging result and decides. The steps are the same automatic steps in both cases. Only the last gate differs.

The reference repository uses GitHub Actions, the CI/CD service of GitHub. Other services, such as Azure DevOps Pipelines and GitLab CI, use the same ideas with other names. Module 2 maps them to Azure DevOps.

Two workflows. Trigger: push to main, the merge of PR #3. It starts workflow CI, file .github/workflows/ci.yml, with two jobs. Job Lint, test, audit, evaluate runs on runner ubuntu-24.04, a new VM, with the steps install the exact versions, lint, unit and contract tests, security, AI evaluation gate, keep the reports. It makes the artifact reports. The second job, Build, smoke-test and push, needs the first. It runs on another new VM with the steps build the image, smoke-test it, push the same build, write build-info.json, keep the build evidence. It makes the artifact build-info and the image sha-7324d7b in ghcr.io. Trigger: workflow_run, CI completed on main. It starts workflow Deploy, file .github/workflows/deploy.yml, with three jobs: Find the image, then Deploy to staging in environment staging, then Deploy to production in environment production, which waits for a required reviewer.
The two workflows of the reference repository after PR #3, with each word on the part that it names. Every job starts on a new, clean virtual machine.
Text description of the diagram

The push to main is the trigger of workflow CI. CI has two jobs. The first job runs the checks as steps on a new virtual machine and keeps the test results as an artifact. The second job needs the first one: it builds the image, tests it, pushes it to the registry, and keeps a small file with the image’s digest as a second artifact. When CI completes, that event triggers workflow Deploy. Its first job reads the digest from the artifact. The second job deploys that image to the staging environment. The third job deploys it to the production environment, after a reviewer approves.

Each word names one part:

Word What it is In the reference repository
Trigger The event that starts a workflow. A push to main; a pull request; “workflow CI completed”; a tag push; a person who selects Run workflow.
Workflow One automated process, in a YAML file in .github/workflows/. ci.yml (CI), deploy.yml (Deploy), release.yml (Release).
Job A group of steps that runs on one machine. Jobs can run one after another (needs:) or at the same time. “Lint, test, audit, evaluate”; “Build, smoke-test and push the image”.
Step One command or one ready-made action in a job. Steps run in order. If one fails, the job stops. ruff check .; pytest; “Push the same build”.
Runner The machine that runs a job. A hosted runner is a new virtual machine (VM) for every job, deleted after it. ubuntu-24.04, from GitHub.
Artifact A file that a run keeps after its machine is deleted. reports (test results); build-info (the image’s digest, the commit and the run).
Environment A place where the application runs, with its own settings, secrets and rules. staging and production.

The container image is an artifact too, in a wider sense: the build output that you test and then deploy. In this course, “the artifact” means the image, and the pipeline names it by its digest. A tag can move to other content. A digest cannot.

Two more environment words. Staging is a copy of production where you test a change in the same conditions, before users see it. The image in staging is a release candidate: it becomes the release if it passes there. A release is a version that you give to users with a name, such as the Git tag v1.2.0, and notes about what changed.

Worked example: follow PR #3 to production

Section titled “Worked example: follow PR #3 to production”

PR #3 has the source feature/classifier-1.1 and the target main. Amira opens PR #3 in the browser:

Pull request 3, Classifier keywords-1.1 behind a flag, and the AI evaluation gate. Merged: usmanahmed99 merged 1 commit into main from feature/classifier-1.1. The tabs show 1 commit and 2 checks. The description says keywords-1.1 is off by default, CLASSIFIER_VERSION=1.0, and that CI now runs the evaluation gate on the 33 holdout tickets. The commit 19d172b has a green check mark. Below: usmanahmed99 merged commit 7324d7b into main, 2 checks passed.
PR #3, logged out. The header names the target (main) and the source (feature/classifier-1.1). The commit 19d172b passed both checks; the merge made commit 7324d7b on main.

The page gives two commit IDs: 19d172b, the commit on the branch, and 7324d7b, the merge commit on main. Every run in the pipeline belongs to one of them. GitHub’s public API lists the runs of a commit. The output is short if you print only the fields that you need:

Terminal
curl -s "https://api.github.com/repos/Nextia-Intelligence/ticket-api-ci/actions/runs?head_sha=7324d7b3ade618916d89d452bbe040c3fa630590" \
| python3 -c "import json,sys; [print(r['id'], r['name'], r['event'], r['conclusion']) for r in json.load(sys.stdin)['workflow_runs']]"

The output:

37666627403 Deploy workflow_run success
37666387332 CI push success
37666141730 CI pull_request success

Three runs, newest first. Read them from the bottom up, with the times from the run pages:

Time (UTC) What happened Trigger Evidence
18:20:56 Amira pushes 19d172b to feature/classifier-1.1 and opens the PR into main. PR #3
18:21–18:22 CI checks the branch: both jobs pass (25 s each). The image is built and tested, not pushed. pull_request run 37666141730
18:22:55 Amira merges. main gets the merge commit 7324d7b. PR #3
18:23–18:24 CI checks main again (40 s), then builds, smoke-tests and pushes the image sha-7324d7b (47 s). push run 37666387332
18:24:58 Deploy sends the image to staging and smoke-checks it (55 s). workflow_run run 37666627403
18:25:53 Production waits for a reviewer. Amira approves. the same run
18:26:54 Production runs the same image, and its smoke check passes (53 s). the same run

Why does the pipeline check the code twice? The pull request run checks the branch. The run on main checks what main has after the merge, and only this run makes the image that can go to production. A pull request run never pushes an image.

The Deploy run shows the rest of the path on one page:

Deploy run number 3, Success, triggered via workflow run, commit 7324d7b, total duration 2 minutes 7 seconds. The graph of deploy.yml, on workflow_run: Find the image, 4 seconds, then staging / Deploy to staging, 55 seconds, with the staging address, then production / Deploy to production, 53 seconds, with the production address. Below, Deployment protection rules: usmanahmed99 approved production, with the comment: keywords-1.1 is off by default (CLASSIFIER_VERSION=1.0). Staging passed. Approved.
Deploy run 37666627403, logged out. The three jobs run one after another. The approval and its comment are part of the run's record.

The logs need a sign-in, but the facts are in the run. Staging and production both received ghcr.io/usmanahmed99/ticket-api-ci@sha256:e5157802…, the digest that CI pushed for 7324d7b. Both smoke checks printed the same four lines:

ok health (200)
ok ready (200)
ok classify with the key (200)
ok classify without a key (401)

The approval has a comment that says why: the new classifier is off by default, and staging passed. A reviewer who approves without reading is not a gate.

The change is in production, but it is not yet a release. A release has a version name and notes. Later that day, v1.2.0 included this change, with notes and the digest of its image: see the v1.2.0 release. Lesson 2 explains tags and versions.

A gate is a point that a change passes only if a condition is true. Every step of PR #3’s path has one, and every gate has an exit for a change that fails:

Workflow CI. 1, commit on a branch: push to feature/classifier-1.1 and open a pull request into main. If someone pushes straight to main, the ruleset refuses it. 2, pull request checks: lint, test, audit, evaluate, then build and smoke-test the image, no push. If a check is red, the PR cannot merge; fix it on the branch. 3, merge into main: the ruleset Protect main needs both required checks green and the branch up to date. If BLOCKED or BEHIND, no merge; update the branch and check again. 4, build once on main: the checks run again, then build, smoke-test and push the image sha-7324d7b, and record its digest. If a job fails, no image is pushed and there is nothing to deploy. When CI passed on main, workflow Deploy starts and reads the digest. 5, staging: deploy the digest, wait for the new revision, run the smoke check. If staging fails, the run stops before production. 6, approval: environment production waits for a required reviewer. If rejected or not answered, production keeps the image that it has. 7, production: deploy the same digest and run the smoke check again. If it fails, deploy the last good digest. One artifact: ghcr.io/usmanahmed99/ticket-api-ci@sha256:e5157802..., built once in step 4 and deployed by its digest in steps 5 and 7. A release tag such as v1.2.0 names the same digest; nothing is built again.
The commit-to-production pipeline of the reference repository, with the exit at each gate. The image is built once, in step 4, and every later step uses its digest.
Text description of the diagram

Seven steps, in two workflows. In workflow CI: a commit on a branch, the pull request checks, the merge, and the build on main. A push straight to main is refused. A red check stops the merge. A branch that is blocked or behind main cannot merge until it is updated and checked again. A failed job on main pushes no image. When CI passes on main, workflow Deploy starts with the image’s digest. It deploys to staging and runs the smoke check; a failure stops the run before production. Production waits for a reviewer; without an approval, production keeps its current image. Production gets the same digest and its own smoke check; if it fails, the team deploys the last good digest. One artifact goes through every step after the build.

Look at what is not in the picture. No step builds a second image for production. No step copies files to a server. No person runs a command that the pipeline did not record. If staging and production ran two different builds, a test in staging would prove nothing about production. Module 2 builds the image once; Module 3 promotes it by its digest.

Not every gate is a check that you write. Steps 1 and 3 are rules of the repository (a ruleset, which Module 2 makes). Step 6 is a rule of the environment (Module 3). Lesson 3 of this module decides which checks steps 2 and 4 run.

Trace PR #3 with your own browser and terminal. The goal is to find, for one change, every run and the image that reached production.

  1. Open PR #3. Write down the source branch, the target branch and the merge commit.

  2. Select the Checks tab. Write down the names of the two checks.

  3. Run the curl command of the worked example. Check that you get three runs.

  4. Open the CI run with the event push: https://github.com/Nextia-Intelligence/ticket-api-ci/actions/runs/37666387332. Look at the job graph. Which job ran first?

  5. List the jobs of the Deploy run with their times:

    Terminal
    curl -s https://api.github.com/repos/Nextia-Intelligence/ticket-api-ci/actions/runs/37666627403/jobs \
    | python3 -c "import json,sys; [print(j['name'], '|', j['conclusion'], '|', j['started_at'][11:19], '-', j['completed_at'][11:19]) for j in json.load(sys.stdin)['jobs']]"

    The output:

    Find the image | success | 18:24:50 - 18:24:54
    staging / Deploy to staging | success | 18:24:58 - 18:25:53
    production / Deploy to production | success | 18:26:01 - 18:26:54
  6. Look at the gap between staging and production. What happened in those 8 seconds?

  7. Open the Deploy run. Find who approved production, and why.

If curl prints API rate limit exceeded, wait one hour, or use the browser only. GitHub allows 60 requests per hour without a sign-in.

Show the expected answers
  1. Source feature/classifier-1.1, target main, merge commit 7324d7b.
  2. “Lint, test, audit, evaluate” and “Build, smoke-test and push the image”. These are the two job names of workflow CI.
  3. 37666627403 Deploy, 37666387332 CI push, 37666141730 CI pull_request, all success.
  4. “Lint, test, audit, evaluate” ran first. The image job starts only when the checks job passed (needs: checks).
  5. As shown.
  6. Production waited for the required reviewer, and Amira approved. The wait was short because she was ready. A real wait can take hours, and the run waits with it.
  7. usmanahmed99 (Amira’s account in the test) approved, with the comment “keywords-1.1 is off by default (CLASSIFIER_VERSION=1.0). Staging passed. Approved.”

PR #5 has the source feature/score-field and the target main. It added the field score and became version 1.2.0. Its merge commit is c3c7135ec035fc0f7990a23a111ca4d460830d01. Find its runs with the same curl command and this commit. Then answer: which workflows ran, and which one did not run for PR #3? Which digest went to production? Where do you find that digest without a sign-in?

Show the answer

The output (2026-10-07):

37667587273 Release push success
37667574234 Deploy workflow_run success
37667326873 CI push success

A third workflow ran: Release, triggered by the push of the tag v1.2.0. A tag push is a trigger too. The v1.2.0 release page ends with the image: ghcr.io/usmanahmed99/ticket-api-ci@sha256:bd2316b56067c738199d6203a23845c97fb7b85b550379411eddd7cc6a2ddab8 (tag 1.2.0). The Deploy run sent the same digest to staging and production, and the approval comment says “Release 1.2.0: staging smoke checks passed. keywords-1.1 stays off in production for now.” The Release workflow built nothing: it gave the image that CI had built a version name. Module 3 shows how.

This run list has no pull_request run, because the PR’s runs belong to the branch commit, not to the merge commit. GitHub listed it for PR #3; do not depend on it.

Common mistake

The pull request is green, but production did not change

Symptom: Sam sees the green check marks on a pull request, and tells the help desk that the new classifier is live. The help desk sends a ticket, and the answer still says keywords-1.0.

Cause: a green pull request means that the branch passed the checks. The pull request run builds and tests an image, but it does not push it, so there is nothing to deploy. Look at the steps of PR #3’s run:

Smoke-test the image before it leaves this runner success
Sign in to GitHub Container Registry skipped
Push the same build skipped

Production changes only after the merge, the CI run on main, a passed staging, and the approval. Here there is a second reason: keywords-1.1 is off by default. Even in production, the answers say keywords-1.0 until someone sets CLASSIFIER_VERSION=1.1 (Module 4).

Fix: to know what runs in production, read the Deploy run, not the pull request. Check three things: the run’s commit, its conclusion, and the digest that the production job deployed. In the reference repository, the Deploy run of 7324d7b answers all three.

For a real AI service: “the model is deployed” can mean “the pull request passed”, “the image exists”, “staging runs it” or “users get it”. The help desk acts on what users get. Say which one you mean, with the run as evidence.

Check your understanding

The ticket API's production waits for a reviewer. Is that continuous delivery or continuous deployment, and why did the team choose it?

Show the answer

Continuous delivery. Every change that passes the checks can go to production, and the steps are automatic, but a person approves the production step. The team chose it because a classifier change changes the priorities that the help desk works with. A person reads the staging result before users see it. With continuous deployment, the pipeline would send every passed change to production with no approval.

Check your understanding

Predict: the staging job of a Deploy run fails at its first step. What happens to production, and what image does it run after that?

Show the answer

The production job does not start, because it runs only after staging passed. Production keeps the image that it already had. This happened in the reference repository. The first attempt of Deploy run 37664760027 failed at the Azure sign-in in staging. The production job was skipped. Module 3 explains that error and its fix.

Recap

  • CI checks every change on every push. CD takes the checked result to the environments. A pipeline is the whole path.
  • Continuous delivery: every passed change can go to production, after an approval. Continuous deployment: it goes with no approval. This course uses delivery.
  • A trigger starts a workflow. A workflow has jobs, a job has steps, and each job runs on a new runner. An artifact is a file that a run keeps. An environment is a place where the app runs, such as staging or production.
  • PR #3 went from a commit to production in about six minutes, and passed a gate at every step. The image was built once, and staging and production ran the same digest.
  • A green pull request is not a deployment. Read the Deploy run to know what production runs.

Next: Choose a branch workflow