CI/CD YAML, Scripts, Images, Services, before_script, after_script, and Defaults: Guided Hands-On Workflow and Core Operations
Build and observe a small Free-compatible pipeline that proves default inheritance, before_script/script/after_script ordering, image identity, working-directory behavior, service networking, readiness, and exit-code semantics.
Learning objectives
- Inspect the current CI configuration and runner/executor capability before adding image or service assumptions.
- Validate a small pipeline that demonstrates default inheritance across two jobs.
- Prove before_script/script/after_script ordering and separate-shell behavior from logs and files.
- Run a harmless service sidecar only when the executor supports it and verify readiness through an alias.
- Capture safe image/job metadata without exposing credentials or depending on paid compute.
default,
image, services, before_script,
script, and after_script—are part of core
GitLab CI/CD and are available on Free, Premium, and Ultimate across
GitLab.com, Self-Managed, and Dedicated. Runtime behavior still
depends on the runner executor. In particular, container
image/services semantics require a
compatible container-capable executor; a Shell executor runs commands
directly on the runner host and has materially different isolation and
dependency assumptions. Hosted-compute quotas and registry
availability can change, so all mandatory learning also has a CI
Lint/log-fixture or local-container fallback.
1. Disposable scenario and preflight
Use a disposable project or branch such as
ch11/yaml-runtime. The live path needs an eligible
container-capable runner for the image/service portions. If your
available runner uses Shell or hosted compute is unavailable,
complete CI Lint plus the supplied expected-state reasoning and
optionally reproduce the service locally with Docker/Podman. Do not
register a privileged production runner for this lesson.
| Preflight item | What to record |
|---|---|
| Repository | Project path, lab branch, current HEAD SHA. |
| CI configuration |
Whether default, include, or an
existing image/services exists.
|
| Runner | Runner ID/description and executor capability if visible; never expose runner auth tokens. |
| Compute | Whether a tiny job can run without buying capacity. |
| Data | Synthetic text only; no credentials, customer data, or production endpoints. |
2. Start with a two-job lifecycle probe
The first pipeline uses an explicit version tag and no external service. It demonstrates that two jobs inherit the same defaults, while one job deliberately replaces a default lifecycle hook.
default:
image: alpine:3.22
before_script:
- echo "01 default before_script"
- export FROM_DEFAULT="visible-in-main-shell"
after_script:
- echo "90 default after_script"
- printf 'after_pwd=%s
' "$PWD"
- test -z "${FROM_DEFAULT:-}"
stages:
- inspect
- verify
inspect_runtime:
stage: inspect
script:
- echo "10 inspect script"
- test "$FROM_DEFAULT" = "visible-in-main-shell"
- printf 'sha=%s
' "$CI_COMMIT_SHA"
- printf 'image=%s
' "${CI_JOB_IMAGE:-not-reported}"
- printf 'workspace=%s
' "$PWD"
- printf 'persisted-file
' > runtime-evidence.txt
artifacts:
paths: [runtime-evidence.txt]
expire_in: 1 day
verify_override:
stage: verify
before_script:
- echo "02 job-specific before_script replaces default before_script"
script:
- echo "20 verify script"
- test -z "${FROM_DEFAULT:-}"
after_script: []
3. Validate before committing
Open Build → Pipeline editor (or the current CI
Lint surface), paste/inspect the configuration, and validate it. A
generic YAML parser cannot prove GitLab job-key semantics. If the
editor can display merged configuration, confirm that
inspect_runtime receives both default lifecycle hooks
while verify_override replaces
before_script and disables after_script.
4. Commit on the disposable branch and bind evidence to a SHA
Use the same Git discipline as Chapter 10: inspect the diff, commit only the lab YAML, record the exact SHA, then push. The commands are examples; substitute your project path.
LAB_BRANCH="ch11/yaml-runtime"
mkdir -p ch11-evidence
git switch -c "$LAB_BRANCH"
git add -- .gitlab-ci.yml
git diff --cached --check
git diff --cached
git commit -m "ch11: observe CI runtime lifecycle"
EXPECTED_SHA="$(git rev-parse HEAD)"
printf '%s
' "$EXPECTED_SHA" | tee ch11-evidence/expected-sha.txt
git push -u origin "$LAB_BRANCH"
5. Read the log as an execution trace
For inspect_runtime, the expected order is default
before_script → job script → default
after_script. The main shell should see
FROM_DEFAULT; after_script should not. The
file created in the workspace remains available to
after_script if you choose to read it there.
For verify_override, the job-specific
before_script replaces the default one. Therefore
FROM_DEFAULT should be absent. Because
after_script: [] is explicit, the default after hook is
disabled for this job.
| Observation | Interpretation |
|---|---|
01 default before_script before script output
|
Default hook copied into a job that has no own
before_script.
|
FROM_DEFAULT visible in script |
before_script and script share the main shell context. |
FROM_DEFAULT absent in after_script |
after_script runs in a new shell. |
| Override job lacks default before marker |
Job-level before_script replaced the default;
it was not appended.
|
| Override job lacks after marker |
after_script: [] disabled the inherited hook.
|
6. Add one harmless service with an explicit alias and readiness loop
If and only if your runner supports container services, add this
independent job. It uses an NGINX sidecar as a synthetic network
dependency. The job contacts the alias web, not
localhost. The loop handles the difference between
container start and application readiness.
service_probe:
stage: verify
image: alpine:3.22
services:
- name: nginx:1.28-alpine
alias: web
before_script: []
script:
- |
attempt=1
while [ "$attempt" -le 10 ]; do
if wget -qO- http://web/ >/dev/null; then
echo "service_ready_attempt=$attempt"
exit 0
fi
attempt=$((attempt + 1))
sleep 1
done
echo "service did not become ready" >&2
exit 1
after_script: []
7. Record image provenance without pretending a tag is immutable
The YAML records alpine:3.22 and
nginx:1.28-alpine. These are explicit version tags, but
tags can move. Preserve the runner pull log or registry digest if
available. For production, promote reviewed images by digest or
through a controlled internal registry.
Do not fabricate a digest. A digest must come from the registry or
runtime that actually resolved the image. A fake
sha256: string is suitable only as a formatting
fixture, never as production evidence.
8. Prove after_script failure semantics safely
Create a temporary lab-only job whose main script succeeds
and whose after_script returns non-zero. Preserve the
log and observe the final job state. Current GitLab Runner semantics
do not let a failing after_script overturn a successful
main script.
after_failure_probe:
stage: verify
before_script: []
script:
- echo "main script succeeds"
after_script:
- echo "intentional after_script failure"
- false
script and reserve
after_script for post-processing/cleanup whose failure
policy you understand.
9. Challenge: choose the owning control
You need a job that uses the default image but must skip the default setup because it runs a prebuilt static checker. Which change is narrowest?
Answer target: define
before_script: [] on that job. Do not duplicate the
entire default block, do not remove setup from every
job, and do not create a new runner merely to change one job
lifecycle.
10. Verification and cleanup
- ☐ Record pipeline ID, job IDs, commit SHA, and runner/executor evidence.
- ☐ Confirm no log prints token values or production environment variables.
- ☐ Confirm service endpoint is synthetic and limited to the ephemeral job network.
- ☐ Record image references and, when available, the actually resolved digest.
-
☐ Remove the lab-only
after_failure_probebefore retaining configuration. - ☐ Revert/delete the disposable branch only after evidence capture; do not change shared runner settings as cleanup.
Knowledge check
Why did verify_override not see FROM_DEFAULT?
Its job-level before_script replaced the default before_script, so the export never occurred in that job.
Why is web the correct hostname in the service example?
It is the configured service alias on the job network; the service is a separate container, not localhost inside the job container.
What does a failed after_script prove about final job status when script succeeded?
By current semantics, after_script failure alone does not change the successful job exit code, which is why essential validation belongs in script.
If the runner is Shell executor, should you force the service example by changing the runner?
No. Use the static/local fallback or a disposable compatible runner; do not mutate shared runner infrastructure just to satisfy the lab.
Why record the commit SHA alongside image evidence?
The SHA binds the exact CI configuration to the run; image evidence binds the runtime dependency. Both are needed for reproducibility.
Summary
The guided workflow turned YAML semantics into evidence: defaults were inspected before execution, overrides were proven from logs, shell-state boundaries were demonstrated, service networking used an explicit alias and readiness loop, and image identity was recorded without pretending mutable tags are immutable. The next lesson turns those observations into design policy.
Official references
- GitLab Docs — CI/CD YAML syntax reference
- GitLab Docs — Scripts and job logs
- GitLab Docs — Deprecated CI/CD keywords
- GitLab Docs — Use CI/CD configuration from other files
- GitLab Docs — Run CI/CD jobs in Docker containers
- GitLab Docs — Services
- GitLab Docs — Docker executor
- GitLab Docs — Shell executor
- GitLab Docs — Runner executors
- GitLab Docs — Pipeline editor
- GitLab Docs — CI Lint
- GitLab Docs — Predefined CI/CD variables
- GitLab Docs — Runner security
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.