Chapter 11Lesson 02~255 minutes

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.

Disposable labPipeline EditorImage identityService aliasLogsExit codes

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.
Availability baseline (verified 2026-08-21). The CI/CD YAML keywords taught here—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.

Important artifact point: an artifact is hosted job output, not a live shared filesystem. GitLab can make previous-stage artifacts available to later jobs depending on artifact/dependency configuration and defaults; Chapter 16 covers that data-flow behavior in depth. Here, the file exists primarily as safe evidence.

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: []
Fallback: if the runner is Shell-based or services are unsupported, do not “fix” the job by changing a shared production runner. Keep the YAML as a validated fixture and reproduce the network model locally with disposable containers if desired. Executor capability is the lesson, not an obstacle to bypass.

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
Do not use this pattern for real validation. If cleanup/report generation must determine job success, perform the correctness check in 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_probe before 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?

Why is web the correct hostname in the service example?

What does a failed after_script prove about final job status when script succeeded?

If the runner is Shell executor, should you force the service example by changing the runner?

Why record the commit SHA alongside image evidence?

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

Next lesson

Design defaults and runtime dependencies deliberately

Lesson 3 compares global defaults with explicit job configuration, trusted minimal images with convenience images, service sidecars with external dependencies, and portable shell code with executor-specific optimization.

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.