Chapter 03Lesson 01~110 minutes

Jobs, Stages, Scripts, Images, Services, before_script, after_script, and Exit Behavior: Concepts, Architecture, and Mental Model

A compiled GitLab job is still only a plan until a runner accepts it and creates an execution environment. This lesson follows the job from stage/DAG eligibility through runner assignment, executor preparation, workspace and service creation, before_script, script, after_script, exit status, and retained evidence so learners can identify exactly which boundary produced a result.

Job lifecycleStages & DAGRunner/executorScripts & hooksExit status

Learning objectives

  • Trace a compiled job from stage/DAG eligibility through runner assignment, executor preparation, workspace setup, main script execution, after_script, and evidence upload.
  • Explain default stage ordering and why jobs in one stage can execute in parallel while later stages normally wait for prior-stage success.
  • Distinguish runner identity, executor type, job image, service containers, workspace state, and GitLab-hosted job metadata.
  • Explain the execution relationship among before_script, script, and after_script, including the separate-shell boundary of after_script.
  • Interpret non-zero exit codes, allow_failure, cancellation, timeouts, and artifacts without treating one status as proof of unrelated external health.
Execution boundary rule. A job that exists in the pipeline graph has not necessarily been assigned to a runner; a runner that starts a job has not necessarily produced artifacts; and a green job does not establish the health of any external service. Keep these states separate throughout the chapter.

1. Why “the job ran” is too vague

Chapter 02 ended at compiled configuration and materialized jobs. Chapter 03 begins where execution starts. Operationally, the phrase “the job ran” hides several independent transitions: the job became eligible, waited in a queue, matched a runner, received an executor environment, obtained source and prior artifacts, ran setup and main commands, ran cleanup/post-processing, uploaded evidence, and reported a final status.

When those transitions are collapsed, teams troubleshoot the wrong layer. A pending job is usually not fixed by editing shell syntax. A job that reaches running and exits 127 is not primarily a pipeline-creation problem. A service that exposes a TCP port but is not application-ready requires different evidence from a missing runner.

2. The job execution model

Causal chain — follow the first state that differs from expectation
            flowchart TD
              A[Compiled job in pipeline graph] --> B[Stage or needs eligibility]
              B --> C[Queue and runner selection]
              C --> D[Executor prepares workspace / container / pod]
              D --> E[Source + cache + prior artifacts]
              E --> F[Main execution: before_script then script]
              F --> G[after_script in separate shell]
              G --> H[Artifacts / cache upload + cleanup]
              H --> I[Final job status + retained evidence]
          

The model deliberately includes Runner work that is not written in .gitlab-ci.yml. Source preparation, cache/artifact transfer, executor setup, and upload/cleanup are Runner phases around your commands. The main execution step contains before_script and script; after_script is a distinct later step and uses a new shell context.

3. Stage ordering is a scheduling rule, not a machine boundary

By default, GitLab groups jobs into stages. Jobs in the same stage are eligible to run in parallel when runner capacity permits. Jobs in a later stage normally wait until jobs in the previous stage complete successfully. A job without an explicit stage uses test.

stages:
  - build
  - test

build_linux:
  stage: build
  script: echo "build linux"

build_docs:
  stage: build
  script: echo "build docs"

unit_test:
  stage: test
  script: echo "test after build stage"

build_linux and build_docs belong to one scheduling stage; they do not share a filesystem just because they share a stage. Chapter 09 explores DAG design deeply. For now, know that needs can let a job start as soon as its declared dependencies finish rather than waiting for the whole previous stage.

4. Job, runner, executor, image, service, and workspace are different objects

Object What it controls Evidence to inspect
Job One executable unit in a pipeline, with its own status/log. Job ID/name, stage, dependencies, variables, trace.
Runner Agent that requests and executes eligible jobs. Runner ID/description/version/tags and job assignment.
Executor How the runner creates the execution environment. Docker, Kubernetes, shell, etc.; runner/job metadata.
Image Container filesystem/tooling for a Docker/Kubernetes-style job. Image name/tag/digest where exposed; tool versions inside job.
Service Additional job-local network container such as a database or HTTP server. Service image/alias, Runner service logs/warnings, network checks.
Workspace Checked-out project directory and job-local files. CI_PROJECT_DIR, checkout SHA, generated files.

The shell executor executes directly on the runner host and has limited isolation; container-oriented executors create a stronger boundary but do not make privileged configuration safe by default. Treat executor choice as security state, not cosmetic syntax.

5. before_script, script, and after_script do not have identical semantics

before_script prepares the main job work, and script performs the primary task. GitLab Runner executes the main phase before moving to after_script. The important boundary is that after_script runs in a new shell. Shell variables exported during the main phase, aliases, and changes outside the working tree are not automatically available there.

job:
  before_script:
    - export MAIN_PHASE_ONLY="visible-here"
    - printf 'prepared
' > prepared.txt
  script:
    - test "$MAIN_PHASE_ONLY" = "visible-here"
    - printf 'main-complete
' > result.txt
  after_script:
    - test -f result.txt
    - printf 'after-script-ran
' > after.txt

Files inside the project working tree are a useful handoff between phases. Environment mutations are not. after_script also has its own timeout, and a failing after_script does not change the job exit code when the main script succeeded. That makes it useful for post-processing and evidence, not for “repairing” a failed build result.

6. Exit status is control flow

For normal script commands, a non-zero exit causes the job to fail and later script commands do not execute. That behavior is valuable because it keeps failures visible. Suppressing a non-zero exit should therefore be explicit and justified.

strict_check:
  script:
    - printf 'about to test
'
    - test -f required.txt
    - printf 'only runs when test succeeded
' 

If a non-zero code is expected and genuinely non-fatal, capture it and decide:

controlled_check:
  script:
    - rc=0
    - optional_tool --probe || rc=$?
    - printf 'probe_exit=%s
' "$rc"
    - if [ "$rc" -ne 0 ]; then printf 'probe unavailable; continuing by policy
'; fi
Avoid || true as a generic repair. It destroys the signal that a command failed unless you also capture, classify, and report the error deliberately.

7. Images and services define runtime dependencies

With the Docker executor, the job image is the container in which job commands run. services are separate containers reachable over the job network. The job image must provide a supported shell in PATH; GitLab also documents grep as a requirement for Docker executor job images. An image with an incompatible entrypoint can prevent Runner from delivering the generated script unless the entrypoint is designed for shell commands or explicitly overridden.

service_probe:
  image: alpine:3.22.1
  services:
    - name: nginx:1.28.0-alpine
      alias: web
  script:
    - wget -qO- http://web/ | head -n 1

Services are job-local dependencies, not production deployments. Runner performs a port-based service health check, but an open port is not equivalent to application readiness. Robust jobs still perform the application-specific readiness check they actually require.

8. Read-only inspection before rerun

Source

CI_PIPELINE_SOURCE, ref, exact CI_COMMIT_SHA.

Pipeline/job

Pipeline ID, job ID/name, stage, status and timestamps.

Runner

Runner ID/description/version, executor and tags where visible.

Runtime

Image identity, service aliases, working directory, safe tool versions.

Failure

First non-zero command or Runner/service diagnostic before retry.

Outputs

Artifacts/reports uploaded after the execution phases.

Preserve the first failing trace before retrying because a rerun can land on a different runner, pull a different mutable image, observe a recovered network dependency, or repeat an external side effect.

9. Foundation mistakes to eliminate now

  • “Same stage means same machine.” Stage is scheduling metadata; each job has its own execution environment.
  • “The image defines the whole pipeline.” It defines a job runtime only for executors that use job images.
  • “Service health warning means the job must fail.” Runner can warn while the job later succeeds; inspect what the application actually required.
  • “after_script can change a failed job to success.” It is post-processing in a separate shell, not a status override mechanism.
  • “A cleanup command should always ignore errors.” Decide whether cleanup failure is informational or operationally important and preserve the evidence.
  • “Green job means deployment is healthy.” It proves only the configured job execution result.

10. Micro-lab: predict the job before running it

stages: [build, test]

default:
  before_script:
    - printf 'job=%s stage=%s sha=%s
' "$CI_JOB_NAME" "$CI_JOB_STAGE" "$CI_COMMIT_SHA"

build_marker:
  stage: build
  script:
    - printf 'built
' > marker.txt
  artifacts:
    paths: [marker.txt]

verify_marker:
  stage: test
  script:
    - test -s marker.txt
  after_script:
    - printf 'after=%s
' "$CI_JOB_STATUS" > after.txt
  artifacts:
    when: always
    paths: [after.txt]

Predict: verify_marker cannot become stage-eligible until the build stage succeeds; its job environment is separate; the earlier-stage artifact is the deliberate cross-job file transfer; after.txt can be captured because after_script runs before artifact upload; and the cleanup file does not redefine the main result.

Next lesson

Guided Hands-On Workflow and Core Operations

Run the model in a disposable pipeline, inspect stage/job/runner evidence, use a containerized HTTP service when available, and deliberately observe exit and cleanup behavior.

Knowledge check

A job is pending. Should you debug its shell command first?

Do two jobs in the same stage share their workspace?

Why can a variable exported in script disappear in after_script?

What does a Runner service health check prove?

If the main script succeeds but after_script fails, what is the documented default effect on the job exit result?

Official references and version notes

  • CI/CD YAML syntax reference — current stages, stage, needs, image, services, before_script, after_script, allow_failure, and timeout semantics.
  • Scripts and job logs — non-zero exit behavior, multiline-command caveats, default hooks, and cancellation behavior.
  • Job execution flow — Runner source preparation, cache/artifact transfer, main execution, after_script, upload, and cleanup phases.
  • Docker executor — job images, service containers, runner workflow, shell requirements, entrypoints, and privilege risks.
  • Run jobs in Docker containers — image/service syntax, entrypoint handling, and where scripts execute.
  • Services — service aliases, networking, health checks, startup warnings, and service lifecycle.
  • Runner executors and supported shells — executor isolation and shell portability boundaries.
Version and compatibility note

Version-sensitive statements were rechecked against current primary GitLab documentation on 2026-09-11. Core jobs, stages, scripts, hooks, images, services, needs, and allow_failure are documented across GitLab Free, Premium, and Ultimate. Docker image/service examples require a Docker-capable runner or the documented local Docker simulation; the mandatory learning path does not require registering or weakening a production runner.

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.