Chapter 01Lesson 02~120 minutes

Continuous Integration and Delivery Foundations, GitLab Pipeline Architecture, and Delivery Flow: Guided Hands-On Workflow and Core Operations

Turn the Chapter 01 state model into a controlled experiment. You will validate a minimal configuration, create a push pipeline, create a manual pipeline for the same disposable branch, compare pipeline source and SHA evidence, inspect job and runner state without exposing credentials, preserve one synthetic artifact, and clean up only the resources created by the lab.

Hands-onPipeline sourcesCI LintArtifactsSafe lab

Learning objectives

  • Create and validate a minimal disposable GitLab CI/CD configuration before using runner compute.
  • Trigger a branch push pipeline and a manual web pipeline, then compare CI_PIPELINE_SOURCE, ref, SHA, pipeline ID, and job ID.
  • Inspect runner assignment and executor-facing evidence without logging tokens or broad variable dumps.
  • Persist a small synthetic evidence file as an artifact and distinguish it from incidental runner workspace state.
  • Verify cleanup using exact branch/project identity and complete the lab without requiring paid or production infrastructure.
Lab safety boundary. Use a throwaway GitLab project or a clearly disposable lab branch in a project you are authorized to modify. The configuration below prints only non-secret metadata, writes a tiny text artifact, and performs no deployment, registry publication, package release, cloud call, or infrastructure change. If your project has no eligible disposable runner, use the validation and simulation path instead of registering a production or employer runner.

1. Scenario: one project, one lab branch, two pipeline sources

The purpose of this lab is to change exactly one variable at a time. The Git commit will stay fixed while the pipeline source changes. First, a push to the lab branch should create a push pipeline. After that pipeline is observed, use GitLab's Build → Pipelines → New pipeline interface to create a second pipeline for the same branch. That pipeline should report source web. If the branch has not moved, both pipelines should refer to the same commit SHA but have different pipeline IDs and sources.

That comparison demonstrates why “what commit?” and “why did this pipeline exist?” are separate questions. Later chapters will use that distinction in workflow:rules and job rules. Here we only observe it.

Controlled input

One disposable branch and one known commit SHA.

Changed dimension

Pipeline source: push versus web.

Expected evidence

Two pipeline IDs; same SHA if the branch did not move.

External side effects

None. No deployment, registry, cloud, or API mutation.

2. Preflight: record repository state before changing it

Start from a local clone of the disposable project. Record enough Git state to prove exactly what you are about to push. Do not create a new personal access token merely for this lesson; normal Git authentication already configured for the disposable project is sufficient.

LAB_BRANCH="glci/ch01-observe"

git status --short --branch
git remote -v
git rev-parse HEAD
git log -1 --oneline --decorate

In GitLab, also inspect Settings → CI/CD → Runners if your role exposes it. The question is read-only: is there an eligible runner that is appropriate for disposable course code? Do not copy runner authentication tokens and do not modify runner protection, tags, or privileges just to make the lab green.

Do not use a privileged shared production runner for this exercise. If the available runner has broad internal-network, Docker socket, host filesystem, cloud, or production access, use the no-runner simulation path instead.

3. Write the smallest observable pipeline

Save the following file as .gitlab-ci.yml at the project root. The job uses ordinary POSIX-style shell commands and assumes the selected runner's execution environment provides Git and a compatible shell. It deliberately declares no secret variables and performs no authenticated API calls.

stages:
  - observe

observe_identity:
  stage: observe
  script:
    - mkdir -p evidence
    - printf 'pipeline_source=%s\n' "$CI_PIPELINE_SOURCE"
    - printf 'ref=%s\n' "$CI_COMMIT_REF_NAME"
    - printf 'sha=%s\n' "$CI_COMMIT_SHA"
    - printf 'pipeline_id=%s\n' "$CI_PIPELINE_ID"
    - printf 'job_id=%s\n' "$CI_JOB_ID"
    - printf 'runner_id=%s\n' "${CI_RUNNER_ID:-unknown}"
    - printf 'runner_version=%s\n' "${CI_RUNNER_VERSION:-unknown}"
    - test "$CI_COMMIT_SHA" = "$(git rev-parse HEAD)"
    - printf 'source=%s\nsha=%s\npipeline_id=%s\njob_id=%s\n' "$CI_PIPELINE_SOURCE" "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" "$CI_JOB_ID" > evidence/identity.txt
  artifacts:
    paths:
      - evidence/identity.txt
    expire_in: 1 day

Notice what is absent: no CI_JOB_TOKEN output, no printenv, no cloud credential, no registry login, and no mutable external container image selected by this lesson. GitLab Runner's executor decides the execution environment. If your runner does not provide the expected shell/Git behavior, treat that as an explicit runner compatibility observation rather than weakening security controls.

4. Validate before you commit

Open Build → Pipeline editor → Validate and validate the configuration. Current GitLab documentation describes CI Lint as a Free-tier capability across GitLab offerings and supports pipeline-creation simulation for more complicated logic. The pipeline editor also validates syntax automatically.

If you already use the GitLab CLI, the current command is glab ci lint; the --dry-run mode can simulate pipeline creation and --ref can select the branch/tag context. CLI use is optional. Do not install tools or create credentials solely because a copied tutorial expects them.

# Optional, only if glab is already authenticated to the disposable project.
glab ci lint
# After the lab branch exists remotely, a simulation can be scoped to it:
glab ci lint --dry-run --include-jobs --ref "$LAB_BRANCH"
Observation What it proves What remains unproved
Generic YAML parses The document is syntactically YAML. That GitLab CI/CD accepts its semantics.
CI Lint valid GitLab accepts the configuration in the lint context. Runner availability and command behavior.
Simulation creates observe_identity The simulated pipeline graph includes the expected job. That a runner can execute it.
Live job succeeds One concrete runner execution completed successfully. External target health—there is no external target in this lab.

5. Create the push pipeline from an exact commit

Create a dedicated branch, review the staged diff, commit the CI file, and record the commit SHA before pushing. The recorded SHA becomes the reference point for all later comparisons.

git switch -c "$LAB_BRANCH"
git add -- .gitlab-ci.yml
git diff --cached --check
git diff --cached
git commit -m "glci ch01: add observable pipeline"

LAB_SHA="$(git rev-parse HEAD)"
printf 'lab_sha=%s\n' "$LAB_SHA"

git push -u origin "$LAB_BRANCH"

Open Build → Pipelines. Locate the pipeline for the recorded branch/SHA rather than selecting an ambiguous “latest” pipeline. Confirm its source is push. Open the pipeline, then open observe_identity. Record the pipeline ID and job ID before retrying or changing anything.

6. Follow the evidence into the job

A running or completed job gives you execution-plane evidence. Compare the safe log lines to the control-plane metadata shown in the pipeline/job UI. The SHA in the job must match the pipeline SHA and the recorded LAB_SHA. The checked-out git rev-parse HEAD test should also match for this simple branch pipeline.

Evidence Expected value Why it matters
Pipeline source push Proves why this first pipeline was created.
Pipeline SHA LAB_SHA Ties GitLab state to immutable source.
Job SHA / checkout HEAD Same SHA in this scenario Proves which source the script actually sees.
Pipeline ID A concrete integer Correlation key for pipeline evidence.
Job ID A concrete integer Correlation key for trace/artifact evidence.
Runner ID/version Assigned runner metadata Identifies execution infrastructure when a runner exists.

If the job is pending and has no runner assignment, preserve the pipeline/job IDs and stop at that boundary. The correct conclusion is “configuration and pipeline creation succeeded; execution has not started.” Do not infer a script failure from a script that never ran.

7. Verify the artifact as durable GitLab-managed evidence

When the job succeeds, GitLab Runner uploads evidence/identity.txt because the job explicitly declared it under artifacts:paths. Download or browse the artifact from the job/pipeline interface and compare it with the log and pipeline metadata. It should contain only four non-secret fields.

source=push
sha=3f4c...actual-commit-sha...
pipeline_id=12345
job_id=67890

The file's existence on GitLab after the job is different from its existence in the runner workspace. The artifact has a producer job, pipeline/SHA context, and retention lifecycle. This distinction becomes essential when later jobs execute on different runners.

8. Trigger the same branch manually and compare source identity

Without changing the branch, open Build → Pipelines → New pipeline, select glci/ch01-observe, and create the pipeline. Do not add arbitrary variables; this comparison is about pipeline source only. Current GitLab pipeline-source semantics identify a pipeline created from the web interface as source web.

Open the new observe_identity job and compare the evidence packet with the push pipeline. If no commit was added between the two runs, you should see:

  • different pipeline IDs;
  • different job IDs;
  • push versus web as pipeline source;
  • the same branch/ref;
  • the same commit SHA;
  • possibly a different runner ID if scheduling selected another runner.
Interpretation: source and revision are independent dimensions. Two pipelines can evaluate the same revision for different reasons, and later rules can intentionally make those pipelines contain different jobs.

9. No eligible runner? Complete the learning without weakening controls

If CI Lint and pipeline simulation succeed but a live job cannot be executed safely, preserve the lint result and use the fixture below to state your expected live evidence. Mark it as a simulation, not as observed runtime evidence.

{
  "project": "disposable GitLab lab project",
  "branch": "glci/ch01-observe",
  "sha": "captured-lab-sha",
  "push_pipeline": {
    "source": "push",
    "expected_job": "observe_identity"
  },
  "manual_pipeline": {
    "source": "web",
    "expected_job": "observe_identity"
  },
  "external_side_effects": "none",
  "runtime_evidence": "not observed because no eligible disposable runner was available"
}

This is not equivalent to a live run, and the lesson should say so. It is still better than granting an unsafe runner more privilege or attaching course code to infrastructure that was never intended for it.

10. Map the lab to CI, delivery, and deployment

The lab is CI observation. It associates an exact source revision with pipeline/job execution evidence and one stored artifact. It is not continuous delivery because it does not create or promote a deployable application artifact. It is not deployment because it changes no environment. This negative boundary is useful: a pipeline can be valuable even when it never deploys.

In later chapters, a build job may produce an immutable package or image, a security job may generate reports, an environment job may require approval, and a deployment job may authenticate through OIDC. Those additional states extend this evidence chain; they do not replace it.

11. Challenge: choose the correct diagnostic surface

For each symptom, identify the first useful layer before reading the answer:

  • No pipeline appears after push. Start with ref/source/configuration and pipeline-creation logic.
  • Pipeline exists but job is pending. Start with job eligibility and runner matching/capacity.
  • Job runs and exits 1. Start with the first failing command and execution environment.
  • Job succeeds but artifact is absent. Inspect artifact path/upload configuration and runner artifact-upload evidence.
  • Manual pipeline uses a new SHA. Verify whether the branch moved between the push and manual runs before assuming GitLab selected the wrong commit.

12. Cleanup: delete only the resource you can identify exactly

Preserve the two pipeline IDs, two job IDs, sources, SHA, runner metadata (if any), and artifact evidence long enough to finish the comparison. Then clean up only the disposable branch or project you created. Before deleting the branch, verify that the remote ref still points to the expected lab history.

git fetch origin --prune
git log -1 --oneline "$LAB_BRANCH"
git ls-remote --heads origin "$LAB_BRANCH"

# Only after verifying this is the disposable lab branch:
git push origin --delete "$LAB_BRANCH"
git switch -
git branch -D "$LAB_BRANCH"
git fetch origin --prune
If you used a whole disposable project, delete it only after confirming its namespace/path and that it contains no unrelated work. This chapter never requires deletion of pipelines, artifacts, runners, or project settings.

Summary

You separated validation from execution, tied two pipelines to source/ref/SHA evidence, changed the pipeline source without intentionally changing the commit, inspected runner identity only after assignment, persisted a synthetic artifact instead of relying on workspace state, and kept the entire exercise free of production credentials and deployment side effects.

Next lesson

Configuration, Design Choices, and Tradeoffs

Use the same state model to choose between stage flow and DAG flow, GitLab-native automation and external orchestration, advisory and merge-blocking checks, and different identity/trust boundaries.

Knowledge check

Why validate with CI Lint before spending runner time?

Two pipelines have the same SHA but sources push and web. Is that contradictory?

A job is pending with no runner ID. Should you debug the first shell command?

Why is evidence/identity.txt more durable than a file left only in the runner workspace?

What should you do if the only available runner is privileged production infrastructure?

Official references and version notes

Version and compatibility note

UI wording, GitLab CLI flags, runner offerings, and hosted-compute quotas can change. The steps and command family in this lesson were checked against primary GitLab documentation on 2026-09-11. The mandatory learning objective remains source/SHA/pipeline/job/runner evidence and can be completed with validation/simulation if safe runner compute is unavailable.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.