GitLab CI/CD Foundations: .gitlab-ci.yml, Pipelines, Jobs, Stages, and Execution Model: Guided Hands-On Workflow and Core Operations
Validate and run a tiny Free-compatible pipeline, prove its source and commit identity, inspect the assigned runner and logs, preserve a small artifact, and observe CI_JOB_TOKEN safely without revealing it.
Learning objectives
- Create a disposable branch/project path and inspect runner availability before adding CI configuration.
- Validate a minimal .gitlab-ci.yml in Pipeline Editor/CI Lint before committing it.
- Run one harmless job and correlate pipeline source, ref, SHA, job ID, runner ID, and local Git state.
- Add a tiny artifact and verify it as hosted evidence without confusing it with cache or workspace state.
- Prove CI_JOB_TOKEN presence without printing its value and avoid creating any external side effect.
.gitlab-ci.yml, Pipeline Editor/CI Lint,
jobs, stages, ordinary branch and merge-request pipelines, predefined
variables, artifacts, runner scheduling, and
CI_JOB_TOKEN are available on GitLab Free across
GitLab.com, Self-Managed, and Dedicated. GitLab-hosted runners are
provided for GitLab.com and GitLab Dedicated; Self-Managed
installations provide and operate their own runner capacity. Hosted
compute quotas, credits, machine types, and billing can change, so
this chapter never hard-codes them and provides a validation-only path
whenever a runner is unavailable. Current glab uses the
glab ci command family; older pipe/pipeline
aliases are deprecated.
1. Disposable scenario and no-runner fallback
Use a disposable project such as
GROUP/ch10-ci-foundations or a dedicated lab branch in
a disposable project. The mandatory configuration prints only
synthetic execution metadata and writes a tiny text artifact. It
does not deploy, call cloud services, mutate registries, or create
credentials.
- Minimum role: enough project access to push a lab branch and create a pipeline. Maintainer/Owner is not required for the basic branch lab.
- Offering/tier: GitLab Free on GitLab.com, Self-Managed, or Dedicated.
- Runner path: if an eligible runner exists, run the job. If not, complete CI Lint/pipeline simulation and compare against the included expected-state fixture. Do not register a production runner merely to finish this lesson.
2. Preflight: observe before changing
Start by recording the repository and CI state. The commands below reveal metadata, not credentials.
REPO="GROUP/ch10-ci-foundations"
LAB_BRANCH="ch10/minimal-pipeline"
mkdir -p ch10-evidence
glab auth status
git status --short --branch
git remote -v
git rev-parse HEAD | tee ch10-evidence/local-head-before.txt
glab ci list -R "$REPO" --output json --per-page 10 --jq '.[] | {id,status,source,ref,sha}' | tee ch10-evidence/pipelines-before.json
glab runner list -R "$REPO" --output json | tee ch10-evidence/runners-before.json
3. Write the first one-job configuration
The first configuration deliberately uses only ubiquitous shell
behavior. It prints safe predefined metadata, verifies that the
checked-out commit equals CI_COMMIT_SHA, verifies that
the job token exists without printing it, and writes a tiny evidence
file.
stages:
- inspect
identity_probe:
stage: inspect
script:
- printf 'pipeline_source=%s\n' "$CI_PIPELINE_SOURCE"
- printf 'pipeline_id=%s\n' "$CI_PIPELINE_ID"
- printf 'job_id=%s\n' "$CI_JOB_ID"
- printf 'job_stage=%s\n' "$CI_JOB_STAGE"
- printf 'commit_sha=%s\n' "$CI_COMMIT_SHA"
- printf 'ref_name=%s\n' "$CI_COMMIT_REF_NAME"
- printf 'runner_id=%s\n' "${CI_RUNNER_ID:-unknown}"
- printf 'runner_description=%s\n' "${CI_RUNNER_DESCRIPTION:-unknown}"
- test "$CI_COMMIT_SHA" = "$(git rev-parse HEAD)"
- test -n "${CI_JOB_TOKEN:-}"
- printf 'token_check=present-not-printed\n'
- printf 'sha=%s\nsource=%s\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_SOURCE" > ch10-evidence.txt
artifacts:
paths:
- ch10-evidence.txt
expire_in: 1 day
4. Validate before commit
Use Build → Pipeline editor → Validate or CI Lint before changing the repository. Syntax validation catches YAML/schema errors. Pipeline simulation goes further by trying to create the graph in a project/ref context and can catch logical problems that plain YAML parsers cannot.
If your current glab supports it,
glab ci lint is another documented path. The CI Lint
REST endpoint is also available, but a beginner should not create a
new token only for this lab when the UI/CLI already provides safe
validation.
| Check | What it proves | What it does not prove |
|---|---|---|
| Generic YAML parser | The document is syntactically YAML. | That GitLab accepts keys/stages/rules. |
| CI Lint syntax | GitLab accepts the CI configuration structure. | That a particular source/ref will create the expected graph unless simulated. |
| Pipeline simulation | GitLab can construct a graph in the chosen context. | That a runner exists or job commands will succeed. |
| Successful job | A runner executed the commands for one pipeline/SHA. | That future commits, runners, or external systems behave identically. |
5. Commit on a disposable branch and observe pipeline creation
git switch -c "$LAB_BRANCH"
# Save the YAML above as .gitlab-ci.yml, then:
git add -- .gitlab-ci.yml
git diff --cached --check
git diff --cached
git commit -m "ch10: add minimal observable pipeline"
LAB_SHA="$(git rev-parse HEAD)"
printf '%s\n' "$LAB_SHA" | tee ch10-evidence/lab-sha.txt
git push -u origin "$LAB_BRANCH"
# A push pipeline should normally have source=push unless workflow/rules say otherwise.
glab ci list -R "$REPO" --ref "$LAB_BRANCH" --output json --per-page 5 --jq '.[] | {id,status,source,ref,sha,web_url}'
6. Correlate GitLab and runner evidence
Once the job is created, inspect it in
Build → Pipelines and
Build → Jobs. Confirm the pipeline SHA equals the
recorded LAB_SHA. In the job page, record the runner
description/ID and executor-related information GitLab exposes. Then
compare the job log to your prediction.
# Current glab command family.
glab ci status -R "$REPO" --branch "$LAB_BRANCH" --compact
glab ci get -R "$REPO" --branch "$LAB_BRANCH" --output json --with-job-details --jq '{id,status,source,ref,sha,jobs}'
# Optional live trace after choosing a job interactively:
glab ci trace -R "$REPO" --branch "$LAB_BRANCH"
present-not-printed. If a token
value appears anywhere, stop the lab, treat it as exposed, and
follow the credential-revocation guidance from Chapter 02.
7. Verify the artifact as a separate hosted object
After a successful job, GitLab stores
ch10-evidence.txt as a job artifact because the job
explicitly requested it. The artifact is not the runner workspace:
the workspace can disappear while the uploaded artifact remains
according to retention policy. Download or browse it in the job UI
and verify it contains only the expected SHA/source lines.
# Expected artifact content (values differ per run):
sha=0123456789abcdef0123456789abcdef01234567
source=push
8. Determine hosted versus self-managed without guessing
On GitLab.com, GitLab-hosted runners are available by default for projects subject to current compute policy. A project/group can also have self-managed runners. On Self-Managed GitLab, the organization supplies runner infrastructure. On Dedicated, GitLab-hosted runner capability is available but tenant configuration matters.
Use the job’s runner identity plus
Settings → CI/CD → Runners or
glab runner list. Do not infer “GitLab-hosted” just
because the runner has a generic name, and do not inspect
config.toml on infrastructure you do not administer.
9. If no runner is available, finish without purchasing compute
A validated pipeline graph is still useful evidence. Preserve the CI Lint result, the predicted pipeline source/SHA, and this expected job-state fixture:
{
"pipeline": {"source": "push", "ref": "ch10/minimal-pipeline", "sha": "<LAB_SHA>"},
"job": {"name": "identity_probe", "stage": "inspect", "expected_status": "pending_without_runner_or_success_with_runner"},
"secret_output": "none",
"external_side_effects": "none"
}
10. Challenge: choose the correct surface
The job is present but pending. Which surface do you
inspect first: Pipeline Editor, repository diff, or runner
eligibility? Choose runner eligibility because
pipeline creation already succeeded. Now suppose no pipeline exists
after the push: move your investigation back to
configuration/ref/source and workflow logic. The
resource state tells you which subsystem owns the next diagnostic
question.
11. Cleanup and verification
Keep the pipeline evidence long enough to complete the lesson, then
remove only the synthetic CI change. If the whole project is
disposable, you may archive/delete it only after confirming it
contains nothing valuable. Otherwise revert
.gitlab-ci.yml on the lab branch or delete that branch
after preserving the commit SHA and pipeline URL.
git fetch origin --prune
git show --stat "$LAB_SHA"
# Verify the branch points to the expected lab commit before deleting it.
git ls-remote --heads origin "$LAB_BRANCH"
# Then remove the disposable branch through GitLab UI or:
git push origin --delete "$LAB_BRANCH"
git fetch origin --prune
Knowledge check
Why lint before pushing the first .gitlab-ci.yml?
It separates configuration/schema problems from runner/execution problems and can simulate pipeline creation before compute is involved.
The pipeline SHA differs from your current local HEAD after you made another local commit. Is the pipeline wrong?
Not necessarily. The pipeline is tied to its recorded commit; your local branch may have moved. Compare against the recorded LAB_SHA and remote ref.
How did the lab prove CI_JOB_TOKEN existed without leaking it?
The job used test -n on the variable and printed a fixed presence message, never the token value.
What proves which runner executed the job?
The GitLab job metadata/log context and runner ID/description, corroborated by the project runner listing when permitted.
Why is ch10-evidence.txt an artifact rather than just a file?
Because artifacts configuration explicitly uploads it to GitLab after the job; it becomes hosted job output with its own retention lifecycle.
Summary
You validated configuration before mutation, ran or simulated one deterministic job, correlated source/ref/SHA with local Git, inspected runner assignment, verified temporary job identity without printing it, and separated uploaded artifact evidence from runner workspace state. The workflow remains useful even when hosted compute is unavailable.
Official references
- GitLab Docs — CI/CD
- GitLab Docs — Get started with GitLab CI/CD
- GitLab Docs — CI/CD YAML syntax reference
- GitLab Docs — Validate CI/CD configuration
- GitLab Docs — Pipeline editor
- GitLab Docs — Pipelines
- GitLab Docs — CI/CD jobs
- GitLab Docs — Job execution flow
- GitLab Docs — Predefined CI/CD variables
- GitLab Docs — CI/CD variables
- GitLab Docs — CI/CD job token
- GitLab Docs — Runners
- GitLab Docs — Configure runners
- GitLab Docs — GitLab-hosted runners
- GitLab Docs — Pipelines API
- GitLab Docs — Jobs API
- GitLab Docs — CI Lint API
- GitLab Docs — Job rules and CI_PIPELINE_SOURCE
- GitLab CLI — ci
- GitLab CLI — ci list
- GitLab CLI — ci get
- GitLab CLI — ci status
- GitLab CLI — runner list
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.