Diagnosing GitLab CI/CD Failures
Read the first error; it names the class
A pipeline turns red the moment one command misbehaves, and the fix always starts the same way: read the job log top-down to the first error, then name the failure class it belongs to. A GitLab CI job succeeds only when every command in its script: runs to the end and returns an exit status of 0; the operating system reports 0 for success and any non-zero number for failure, so pytest, pyats, and ansible-lint are ordinary shell commands that fail a job the same way false does, by exiting non-zero when a check does not pass. The runner records that as the line you will learn to look for, ERROR: Job failed: exit code 1. (GitLab CI/CD YAML: script[1])
Fail-fast: the shell stops at the first non-zero exit
The commands under script: run in order, and the shell stops at the first one that returns non-zero, so everything after that line never executes. That is why a half-finished log is not a mystery: if you listed three test commands and see the output of only the first two, the second failed and short-circuited the rest, and the missing third line is the evidence rather than lost information. before_script: runs before script: and fails by the same rule, so a job that dies before its real work began is pointing at setup, not at your tests. after_script: is the exception; it always runs, on success or failure, and its own exit code never changes the verdict, so it is for teardown and extra diagnostics, never the place a test's pass or fail is decided. (before_script and after_script[1])
The first error sorts the failure into one of three classes
Because the verdict is a single exit code, diagnosis is finding the command that produced it and reading what it said, top-down and not bottom-up. The last line is almost always the runner's generic ERROR: Job failed; the cause is the first command whose output shows an error, together with the traceback printed just above that final line. (GitLab: view job logs[2]) The error text on that first failing line puts the failure into exactly one of three families, and the rest of this page takes one family per section:
- Missing dependency (
command not found,ModuleNotFoundError): a tool or file the job needed was never delivered into the container. - Version incompatibility (a
SyntaxErroron valid code, anImportErrororAttributeErrorafter no code change): the code ran, but on the wrong runtime or library version. - Failed test (
AssertionError, a pyATSResult: FAILED, a lint rule id): the code ran and a check disagreed with it.
Name the class first. It tells you which lever to pull, and it stops you editing automation code that was never the problem.
Missing dependency: the four supply channels
A job that ran a Python script fine on your workstation can fail the moment it runs in the pipeline, reporting python: command not found or ModuleNotFoundError: No module named 'nornir'. The code did not change; the environment did. Every job runs inside a brand-new container that GitLab builds from the job's image:[1] keyword, on a runner that shares nothing with your workstation or with the other jobs in the pipeline, so whatever the job needs at runtime has to be put into that container deliberately.
Two error strings account for almost every missing-dependency failure, and each fingerprints a different gap. command not found (for example ansible-playbook: command not found) is the shell reporting that no executable by that name is on the job's PATH: the tool is absent from the base image and was never installed. Do not misread it as a YAML error; invalid YAML fails CI Lint[1] and the pipeline never starts, so if you are reading a job log at all, the YAML already parsed and the tool, not the syntax, is missing. ModuleNotFoundError: No module named 'nornir' is different: the Python interpreter is present and running, it just cannot import the library. ModuleNotFoundError is a subclass of ImportError, raised when a module cannot be located[3]; the interpreter arrived through the image, while the library was meant to arrive through an install step that was never written or never listed that package.
Four supply channels, one of them empty
A dependency reaches a job through exactly one of four channels, and a missing-dependency failure is deciding which one was supposed to deliver and came up empty:
- Base image sets the interpreters and command-line tools that exist the instant the job starts (
python,git,ansiblewhen the image ships them). - Install step is a
before_script:[1] block that installs the packages the image lacks; it runs beforescript:on every job. - Artifacts are files produced by an earlier job and handed to a later one, but only when the producer declares them (the next section covers this channel).
- Cache reuses download directories to speed repeated jobs, but caching is an optimization and is not guaranteed to always work[4], so it is never a channel you may depend on.
The first three are channels you may rely on; cache is not. The figure below shows the isolated job drawing from all four, with cache drawn as best-effort.
Minimal images omit tools
The base image sets what already exists in the container, so the smaller the image, the more you install yourself. Minimal images such as python:3.12-slim and alpine ship almost nothing beyond the interpreter, which is exactly why a job on one reports git: command not found or cannot compile a C-extension: git, gcc, and make were never included. You have two levers, mapping to the first two channels: pick a fuller image: (swap python:3.12-slim for python:3.12, the Debian-based default, which already ships git and a build toolchain), or install the one tool you need in before_script:.
run-automation:
image: python:3.12 # Debian-based: ships git + a build toolchain
before_script:
- pip install -r requirements.txt # installs nornir, ncclient, etc.
script:
- ansible-playbook -i inventory.yml deploy.yml
Match the install command to the image's package manager. On a Debian or Ubuntu image you install with apt-get update && apt-get install -y git; on an Alpine image you install with apk add --no-cache git, because Alpine uses apk and the musl C library, not apt-get and glibc. A Debian snippet copied onto Alpine fails with apt-get: command not found, a missing-dependency failure of its own. When the tooling gets heavy, a purpose-built image is cheaper to maintain than a long before_script:.
Passing files between jobs: artifacts vs cache
Files a job creates do not survive into the next job unless the producing job declares them as artifacts. Each job starts from the clean container of the previous section, so a render-configs job that writes configs/router1.cfg leaves nothing behind for a later deploy job, which then fails with a no-such-file error while the automation code is blameless. The supported hand-off has three moving parts: the producer lists the files under artifacts:paths:[5], GitLab uploads them when the job succeeds, and by default later jobs fetch a copy of all artifacts from jobs in earlier stages[5] automatically.
render-configs:
stage: build
script:
- python render.py # writes configs/*.cfg
artifacts:
paths:
- configs/ # without this line, nothing transfers
deploy:
stage: deploy
script:
- ansible-playbook push_configs.yml # reads configs/*.cfg
Because deploy is in a later stage, it downloads configs/ with no extra keyword. When the consumer cannot see the file, check the producer first: a missing artifacts:paths: is the usual cause, and a stray dependencies: [][1] on the consumer, which downloads nothing, is the next.
Cache is an optimization, not a dependency channel
Cache makes repeated jobs faster, and you must never depend on it for correctness. A cache: reuses a directory of downloaded packages (a pip or npm cache) so the next run skips re-downloading, but GitLab is explicit that caching is an optimization and is not guaranteed to always work[4]: the cache can be absent on a fresh runner, evicted, or simply not populated yet. A job that assumes its dependency is already unpacked in the cache therefore fails intermittently, passing on runners that happen to have it and failing on those that do not. Keep the real supply in a channel you control: runtime tools install in before_script:, files that move between jobs travel as artifacts, and cache sits on top of those two, never replacing them.
| Cache | Artifacts | |
|---|---|---|
| Purpose | Speed up repeated jobs | Pass files between stages |
| Guaranteed present | No, best-effort | Yes, when declared |
| Right use | pip / npm download dirs | build outputs a later job reads |
| Safe to depend on | Never | Yes |
If a design or troubleshooting question offers passing the built files to the next stage via cache, it is wrong by construction: use artifacts to pass intermediate build results between stages, and cache for dependencies you download[4].
Version incompatibility: retag or pin
A job that installed and ran cleanly for months can start failing overnight with SyntaxError: invalid syntax on a line nobody touched, or with an AttributeError on a library call that worked yesterday. The code is fine; the environment no longer matches what the code needs. Every version-incompatibility failure has one shape, the code expects one version and the environment supplies another, and the mismatch lives on one of two surfaces. The runtime is the language interpreter the job executes on, fixed by the job's image: tag[1]; a dependency is a library the job installs into that runtime, fixed by a version specifier in requirements.txt or a lockfile. A transitive dependency is one you never named yourself, pulled in because a package you did name depends on it. The error class names the surface: a SyntaxError on plainly valid code means the interpreter is too old to parse the syntax (runtime), while an ImportError, AttributeError, or TypeError on a call that worked before means an installed package resolved to a different version (dependency).
Runtime mismatch: align the image tag
The runner image sets the language runtime, so a job can only run code the image's interpreter understands. Structural pattern matching, the match/case statement, was added in Python 3.10[6]; point a job at the python:3.8 image and it fails at parse time with SyntaxError: invalid syntax on the match line, before a single statement runs. Confirm which image actually ran by reading the CI_JOB_IMAGE predefined variable[7] rather than assuming the pipeline default, then point image: at a tag whose runtime is new enough:
test:
image: python:3.12 # was python:3.8 - now match/case parses
script:
- python -m pytest
Prefer an explicit version tag over a floating one. A bare python resolves to latest and python:3 tracks the newest 3.x, so either can move under you on the next pull and turn the runtime itself into drift, the same class of bug the dependency surface has.
Dependency drift: pin and lock
An unpinned dependency is a build that reinstalls itself differently over time. Write ansible with no version in requirements.txt and each run resolves it to that day's latest release; the moment a new major ships a breaking change, a pipeline that changed nothing goes red. Pin the exact version with pip's == operator[8] so every run reinstalls the same release:
# requirements.txt - drift-prone
ansible
# requirements.txt - reproducible
ansible==9.2.0
A single == pin fixes the package you named, but its own transitive dependencies can still drift. A committed lockfile closes that gap: requirements.txt with hashes, poetry.lock, or Pipfile.lock records the exact version of every package in the resolved graph, so CI installs precisely what was tested. When a green pipeline turns red with no relevant code change, resist editing the code: reinstall from the committed lockfile and re-run, and if the failure disappears an unpinned transitive update caused it and the fix is a pin, not a rewrite. Then read the offending tool's changelog to confirm the breaking change and pick the version to hold.
Failed test: the code ran, a check disagreed
A failed test is the one class where the code actually ran, and a check judged it wrong: pytest prints an AssertionError, pyATS reports Result: FAILED, and ansible-lint names a rule id. That shared trait, the code ran and something disagreed with it, is exactly what separates this class from a missing dependency or a version mismatch, where the code could not run at all. The fix lives in the code or the test's expected value, read straight from the log.
Three tools show up constantly on 350-901 automation pipelines, each with its own failure fingerprint:
| Tool | What a failure prints | Where the fix lives |
|---|---|---|
| pytest | AssertionError[3], FAILED test_x::test_y | The code under test, or the expected value in the assertion |
| pyATS / aetest | Result: FAILED, a section Reason: and Genie diff | The device-facing code, or the expected state in the test |
| ansible-lint | A rule id with file:line (e.g. yaml[indentation]) | The playbook, or an explicit rule waiver |
pytest raises Python's built-in AssertionError when an assert fails, and its assertion rewriting prints the compared values, so you see assert 3 == 4 with both sides expanded rather than a bare failure; the short test summary info block lists each FAILED test_file.py::test_name. pyATS and its aetest engine report per-section results instead of raising: a section that calls self.failed(reason=...), or whose Genie diff finds a mismatch, is marked Result: FAILED, and the reason plus the expected-versus-actual device-state diff prints with that section. (Cisco pyATS documentation[9]) ansible-lint prints each violation as a rule id with the file and line it fired on, for example yaml[indentation], and exits non-zero when any rule at or above its failure threshold matches. (Ansible documentation[10])
allow_failure downgrades a gate to an advisory
allow_failure: true lets a job exit non-zero without failing the pipeline; the pipeline shows an orange warning and keeps going. When it is absent or false, which is the default, a failed test blocks every later stage. That is how you separate a blocking gate, such as unit tests that must pass, from an advisory check you are still cleaning up. It downgrades the failure to a warning; it does not hide the job, and it is never a fix for a real failing test. (GitLab CI/CD YAML: allow_failure[1])
Exam triage: match the symptom to the class
On the 350-901 exam a failure item usually hands you a short job-log excerpt and four candidate fixes, and the excerpt is the whole question: the error text on the first failing line decides the answer, and the trap is a plausible option that ignores it. Read the symptom, name the class from the first three sections, then pick the option that fills that specific class.
| Stem shows | Failure class | Right fix |
|---|---|---|
command not found | Missing dependency | Fuller image:, or install in before_script: |
ModuleNotFoundError | Missing dependency | pip install -r requirements.txt in before_script: |
| Later job: file not found | Missing dependency (artifacts) | artifacts:paths: on the producing job |
SyntaxError on valid match/case | Version incompatibility | Set image: to python:3.10 or newer |
| Passed for months, now red, no code change | Version incompatibility | Pin the version and install from the lockfile |
AssertionError / Result: FAILED / lint id | Failed test | Fix the code or the expected value |
Learn the distractors by name, because the same few recur:
- Fixing the YAML indentation is a distractor whenever a job log is shown at all: invalid YAML fails CI Lint and no job runs, so a shell error like
command not foundproves the YAML already parsed. - Adding the built files to
cache:so the next stage can read them inverts the cache-versus-artifacts rule; files that must reach a later job go inartifacts:paths:, because cache is best-effort and cannot be depended on. - Rewriting the playbook or the script targets code that was never the problem when the log shows
ModuleNotFoundErroror aSyntaxError: the code is fine and its dependency or runtime is wrong. - Retrying the job, or clearing the cache, does not fix drift. A retry reinstalls the same unpinned version, and clearing the cache can discard the last-good resolved set, so a green-to-red run with no code change is answered by a pin or a lockfile, never a retry.
allow_failure: trueis never a fix for a failing test; it downgrades a real regression to a warning so the pipeline reports green.
One more reflex worth rehearsing: a job that fails, then passes on a plain re-run of the same commit with nothing changed, is flaky infrastructure, not any of the three classes. Re-run it and, separately, make the test deterministic; the wrong move is to paper over a reproducible failure by retrying until it happens to pass.
Sorting a red pipeline into one of three failure classes
| Signal in the first error | Missing dependency | Version incompatibility | Failed test |
|---|---|---|---|
| Typical error text | `command not found`, `ModuleNotFoundError` | `SyntaxError` on valid code, `ImportError` after no change | `AssertionError`, `Result: FAILED`, a lint rule id |
| Did the code run? | No, the tool or package was never there | Partly, on a mismatched runtime or library version | Yes, and a check judged it wrong |
| Where the fix lives | `image:` or a `before_script:` install, or `artifacts:paths:` | The `image:` tag, or a version pin plus a lockfile | The code or the test's expected value |
| How to confirm | The tool is absent in a fresh image shell | Rebuild from the committed lockfile and the error clears | Re-run the one test locally and the assertion repeats |
Decision tree
Sharp facts the exam loves — give these one last read before exam day.
Cheat sheet
Sharp facts the exam loves — scan these before test day.
- command not found / ModuleNotFoundError signals a missing dependency
A job that fails with "command not found" or Python's "ModuleNotFoundError: No module named ..." indicates a missing dependency: the runner image or
before_scriptnever installed the required package (e.g.pip install -r requirements.txt).Trap Reading 'command not found' as a YAML syntax error rather than a missing tool.
8 questions test this
- A GitLab CE 'compile-deps' job uses image python:3.12-alpine and runs 'pip install -r requirements.txt', which includes a package with a C extension. The job fails during the build of that package wit
- A GitLab CE 'deploy' job originally installed Ansible collections with 'ansible-galaxy collection install -r requirements.yml' in before_script and cached ~/.ansible/collections through cache:paths. T
- A GitLab CE 'test' job installs pytest with 'pip install -r requirements.txt' in before_script, then relies on 'cache:paths: [.cache/pip]' to reuse wheels. Intermittently the job fails with 'pytest: c
- A GitLab CE 'fetch-plugins' job on image python:3.12-slim downloads a Terraform provider bundle with curl and then runs 'unzip plugins.zip -d ./plugins'. The job fails with '/bin/sh: 1: unzip: not fou
- A GitLab CE 'inventory-sync' job on image python:3.12-slim uses before_script 'pip install pyyaml jinja2' and then runs 'python run_nornir.py', which imports the nornir framework to gather facts from
- An engineer's GitLab CE pipeline runs a validation job on image python:3.12-slim with script 'ansible-playbook site.yml --syntax-check'. The job fails immediately with 'ModuleNotFoundError: No module
- A GitLab CE 'build-frontend' job on image node:20 runs 'npm ci' in before_script and relies on 'cache:paths: [node_modules/]' to make dependencies available. A separate 'unit-test' job in a later stag
- A GitLab CE job named 'push-config' runs on image python:3.12-slim with before_script installing black and yamllint only, then executes a script that imports the netmiko library to connect to switches
- Downstream jobs need artifacts declared upstream
If a later job depends on files produced by an earlier job, the producing job must declare them under
artifacts:paths:; otherwise the consuming job fails because the artifact was never uploaded and transferred between stages.Trap Assuming files produced in one job persist to the next without an artifacts declaration.
9 questions test this
- A GitLab CE 'package' job produces dist/app.whl and a 'deploy' job in a later stage installs it. The engineer added 'cache:key: build-cache' with 'cache:paths: [dist/]' to the package job, expecting d
- A GitLab CE 'deploy' job originally installed Ansible collections with 'ansible-galaxy collection install -r requirements.yml' in before_script and cached ~/.ansible/collections through cache:paths. T
- A GitLab CE 'test' job installs pytest with 'pip install -r requirements.txt' in before_script, then relies on 'cache:paths: [.cache/pip]' to reuse wheels. Intermittently the job fails with 'pytest: c
- In a GitLab CE pipeline, a 'terraform-plan' job in the plan stage produces tfplan.binary, and a later 'terraform-apply' job in the apply stage runs 'terraform apply tfplan.binary'. The apply job fails
- A GitLab CE pipeline runs 'terraform init' in an 'init' job (build stage) and expects a later 'plan' job (prevalidation stage) to reuse the downloaded provider plugins. The engineer added cache:key: t
- A GitLab CE pipeline has a 'render' job in the build stage that normalizes source-of-truth data into normalized/intent.json, and a 'validate' job in a later prevalidation stage whose script reads norm
- A GitLab CE pipeline has a 'build' job that renders Jinja2 templates into a compiled/ directory and a later 'deploy' job in a subsequent stage that reads compiled/config.cfg. The deploy job fails with
- A GitLab CE pipeline has a 'collect' job in the build stage that runs a script writing device backups into a backups/ directory, and a 'report' job in a later stage that runs 'python summarize.py back
- A GitLab CE 'build-frontend' job on image node:20 runs 'npm ci' in before_script and relies on 'cache:paths: [node_modules/]' to make dependencies available. A separate 'unit-test' job in a later stag
- cache is best-effort, not a dependency guarantee
cachespeeds up repeated jobs by reusing files (pip/npm dirs) but is best-effort and not guaranteed to be present between jobs; runtime dependencies must be installed or passed as artifacts, never relied on from cache.Trap Treating cache as a reliable way to pass dependencies between stages.
12 questions test this
- A GitLab CE 'package' job produces dist/app.whl and a 'deploy' job in a later stage installs it. The engineer added 'cache:key: build-cache' with 'cache:paths: [dist/]' to the package job, expecting d
- A GitLab CE 'deploy' job originally installed Ansible collections with 'ansible-galaxy collection install -r requirements.yml' in before_script and cached ~/.ansible/collections through cache:paths. T
- A GitLab CE 'test' job installs pytest with 'pip install -r requirements.txt' in before_script, then relies on 'cache:paths: [.cache/pip]' to reuse wheels. Intermittently the job fails with 'pytest: c
- In a GitLab CE pipeline, a 'terraform-plan' job in the plan stage produces tfplan.binary, and a later 'terraform-apply' job in the apply stage runs 'terraform apply tfplan.binary'. The apply job fails
- A GitLab CE pipeline runs 'terraform init' in an 'init' job (build stage) and expects a later 'plan' job (prevalidation stage) to reuse the downloaded provider plugins. The engineer added cache:key: t
- A GitLab CE pipeline has a 'render' job in the build stage that normalizes source-of-truth data into normalized/intent.json, and a 'validate' job in a later prevalidation stage whose script reads norm
- A GitLab CE pipeline has a 'build' job that renders Jinja2 templates into a compiled/ directory and a later 'deploy' job in a subsequent stage that reads compiled/config.cfg. The deploy job fails with
- A GitLab CE 'fetch-plugins' job on image python:3.12-slim downloads a Terraform provider bundle with curl and then runs 'unzip plugins.zip -d ./plugins'. The job fails with '/bin/sh: 1: unzip: not fou
- A GitLab CE 'inventory-sync' job on image python:3.12-slim uses before_script 'pip install pyyaml jinja2' and then runs 'python run_nornir.py', which imports the nornir framework to gather facts from
- A GitLab CE pipeline has a 'collect' job in the build stage that runs a script writing device backups into a backups/ directory, and a 'report' job in a later stage that runs 'python summarize.py back
- A GitLab CE 'build-frontend' job on image node:20 runs 'npm ci' in before_script and relies on 'cache:paths: [node_modules/]' to make dependencies available. A separate 'unit-test' job in a later stag
- A GitLab CE job named 'push-config' runs on image python:3.12-slim with before_script installing black and yamllint only, then executes a script that imports the netmiko library to connect to switches
- A minimal base image can omit required tools
Choosing a minimal base
image:(e.g.python:3.12-slim,alpine) that lacks tools likegit,gcc, oropensshcauses "not found" failures; fix by selecting a fuller image or installing the tool inbefore_script.4 questions test this
- A GitLab CE 'compile-deps' job uses image python:3.12-alpine and runs 'pip install -r requirements.txt', which includes a package with a C extension. The job fails during the build of that package wit
- A GitLab CE 'fetch-plugins' job on image python:3.12-slim downloads a Terraform provider bundle with curl and then runs 'unzip plugins.zip -d ./plugins'. The job fails with '/bin/sh: 1: unzip: not fou
- A GitLab CE 'inventory-sync' job on image python:3.12-slim uses before_script 'pip install pyyaml jinja2' and then runs 'python run_nornir.py', which imports the nornir framework to gather facts from
- An engineer's GitLab CE pipeline runs a validation job on image python:3.12-slim with script 'ansible-playbook site.yml --syntax-check'. The job fails immediately with 'ModuleNotFoundError: No module
- Unpinned dependencies break reproducibility
An unpinned dependency (e.g.
ansiblewith no version) lets a job pull a newer, incompatible release that breaks the pipeline; pinning exact versions inrequirements.txt(ansible==9.2.0) makes builds reproducible.Trap Assuming an unpinned dependency always resolves to the last-known-good version.
8 questions test this
- A GitLab CE build job's script line runs pip install napalm netmiko with no version constraints instead of installing from a committed, pinned requirements.txt. The pipeline succeeded last month, but
- A GitLab CE post-validation stage that ran pyATS tests successfully last week now fails, reporting an unexpected keyword argument in a library call. Git shows the automation repository has no new comm
- A GitLab CE build stage sets image: python:3.12 and installs fully version-pinned packages from a committed requirements.txt. The job ran green for months on the team's prior configuration, but after
- A team wants their GitLab CE pipeline to build byte-for-byte reproducibly so an identical commit always yields identical dependency resolution, regardless of when the job runs. Their current build sta
- A GitLab CE prevalidation stage that passed yesterday fails today with a new library error, although git log shows no commits since the last green run and the .gitlab-ci.yml is unchanged. The build in
- A GitLab CE pipeline for an Ansible network deployment passed for months, then began failing overnight in the build stage with a module import error, though no commit touched the code or the .gitlab-c
- A GitLab CE prevalidation stage stayed green for weeks, then turned red overnight with a Jinja2 templating TypeError, although git log shows no new commits. The build stage installs from requirements.
- A GitLab CE build stage uses image: python:3 in .gitlab-ci.yml and installs fully version-locked packages from requirements.txt. The job passed for months, then began failing this morning during depen
- Runner runtime must match code requirements
A job failing on syntax or library APIs can stem from the runner image's language runtime differing from what the code targets (e.g.
matchstatements requiring 3.10+ run on apython:3.8image); align theimage:tag to the required runtime.8 questions test this
- An engineer's GitLab CE job runs a Python automation script that uses match/case structural pattern matching. The pipeline fails immediately in the build stage with a SyntaxError on the match block, e
- Two GitLab CE runners are tagged for the same Terraform deploy job. Runs that land on runner A succeed, while runs on runner B fail during terraform init with a message that the state was written by a
- A GitLab CE build stage sets image: python:3.12 and installs fully version-pinned packages from a committed requirements.txt. The job ran green for months on the team's prior configuration, but after
- A GitLab CE build stage sets image: python:3.9 in .gitlab-ci.yml and installs a fully version-pinned requirements.txt. A newly merged automation module declares runtime-evaluated function annotations
- A GitLab CE build job runs a Python automation script that uses asyncio.TaskGroup to gather facts from multiple devices concurrently. The .gitlab-ci.yml sets image: python:3.9 and requirements.txt is
- A GitLab CE pipeline declares image: cisco-tools:stable in .gitlab-ci.yml and installs a fully version-pinned requirements.txt. The pipeline passed all week, but today an identical rerun of a previous
- A GitLab CE build job runs a Python automation module that groups concurrent device-connection failures using except* exception-group handling. The .gitlab-ci.yml sets image: python:3.10 and requireme
- A GitLab CE build stage uses image: python:3 in .gitlab-ci.yml and installs fully version-locked packages from requirements.txt. The job passed for months, then began failing this morning during depen
- A green pipeline going red without code changes
A pipeline that passed before and now fails with new errors despite no code change usually means a transitively-updated tool shipped a breaking change; reproduce with the committed lockfile and read the tool's changelog to confirm.
8 questions test this
- A GitLab CE build job's script line runs pip install napalm netmiko with no version constraints instead of installing from a committed, pinned requirements.txt. The pipeline succeeded last month, but
- A GitLab CE post-validation stage that ran pyATS tests successfully last week now fails, reporting an unexpected keyword argument in a library call. Git shows the automation repository has no new comm
- Two GitLab CE runners are tagged for the same Terraform deploy job. Runs that land on runner A succeed, while runs on runner B fail during terraform init with a message that the state was written by a
- A GitLab CE prevalidation stage that passed yesterday fails today with a new library error, although git log shows no commits since the last green run and the .gitlab-ci.yml is unchanged. The build in
- A GitLab CE pipeline for an Ansible network deployment passed for months, then began failing overnight in the build stage with a module import error, though no commit touched the code or the .gitlab-c
- A GitLab CE prevalidation stage stayed green for weeks, then turned red overnight with a Jinja2 templating TypeError, although git log shows no new commits. The build stage installs from requirements.
- A GitLab CE pipeline declares image: cisco-tools:stable in .gitlab-ci.yml and installs a fully version-pinned requirements.txt. The pipeline passed all week, but today an identical rerun of a previous
- A GitLab CE build stage uses image: python:3 in .gitlab-ci.yml and installs fully version-locked packages from requirements.txt. The job passed for months, then began failing this morning during depen
- Lockfiles pin the full transitive graph
A committed lockfile (
requirements.txtwith hashes,poetry.lock,Pipfile.lock) pins the entire transitive dependency graph so CI installs exactly the versions that were tested, eliminating drift between runs.- A non-zero exit code fails the job
A GitLab CI job is marked failed when a command in its
script:returns a non-zero exit code; a failingpytest,pyats, oransible-lintstep exits non-zero, halting that stage and blocking every later stage.Trap Believing a job only fails if the runner itself crashes, not when a test exits non-zero.
14 questions test this
- A GitLab CE pipeline reports an overall status of 'passed', but the engineer expected a failing 'terraform validate' job to stop the run. The job's log ends with 'ERROR: Job failed: exit code 1' and i
- A GitLab CE job in the test stage defines a single script line: 'pytest tests/unit && ansible-playbook -i inv deploy.yml'. During a run, several unit tests fail and the log ends with 'ERROR: Job faile
- A pyATS validation job in GitLab CE fails. The job log shows, near the top, 'common_setup: connect_devices' raising 'ConnectionError: failed to connect to sw1', then a long list of testcases each repo
- A GitLab CE job named lint has this script written as two separate lines: line 1 'ansible-lint site.yml', line 2 'echo lint passed'. In a run, ansible-lint exits with code 2 after reporting rule viola
- A GitLab CE unit job runs one script line: 'pytest tests/ | tee pytest-report.txt'. The log clearly shows three failed tests, yet the job is reported as passed with a green icon and the deploy stage p
- A team's .gitlab-ci.yml has a `style` job that runs `ansible-lint` in the test stage. Today any lint finding returns non-zero and blocks the deploy stage. The team wants lint results to be information
- An engineer reviews a failed GitLab CE pytest job. The bottom of the log reads 'ERROR: Job failed: exit code 1' just under a summary line '13 errors in 0.42s'. Scrolling up, the first entry is 'ERROR
- An engineer diagnoses a failed GitLab CE job whose single script line runs 'bash run_all.sh', a wrapper that sequentially calls three validation scripts and does not stop when one errors. The job log
- A GitLab CE test job's `script:` contains one line: `pytest tests/unit; echo 'tests complete'`. Multiple unit tests fail, yet the pipeline marks the job as passed and proceeds to the deploy stage. Ass
- A GitLab CE pipeline has a test stage containing a pytest job and a later deploy stage containing an ansible-playbook job. In one run the pytest job ends with 'ERROR: Job failed: exit code 1', and the
- A pyATS validation job in GitLab CE fails. The engineer scrolls straight to the bottom of the job log and sees only 'Cleaning up project directory' followed by 'ERROR: Job failed: exit code 1'. To loc
- In a GitLab CE pipeline, the 'security-scan' job shows an orange warning icon and its log ends with 'ERROR: Job failed: exit code 1', yet the overall pipeline status is 'passed' and the deploy stage r
- A team runs a 'bandit' Python security scan in the test stage of a GitLab CE pipeline. Today any finding returns a non-zero exit and blocks the deploy stage, but the team wants scan findings to appear
- An engineer diagnoses a failed GitLab CE test job. At the very bottom of the log, under an 'after_script' section, are several 'command not found' messages, followed by 'ERROR: Job failed: exit code 1
- Read the job log top-down to the first error
The job log echoes each command and its output in order; the first non-zero exit and the traceback just above "ERROR: Job failed" localize the fault. Read top-down to the first error, not just the last line.
Trap Assuming the final log line is always the root cause of the failure.
6 questions test this
- A pyATS validation job in GitLab CE fails. The job log shows, near the top, 'common_setup: connect_devices' raising 'ConnectionError: failed to connect to sw1', then a long list of testcases each repo
- An engineer reviews a failed GitLab CE pytest job. The bottom of the log reads 'ERROR: Job failed: exit code 1' just under a summary line '13 errors in 0.42s'. Scrolling up, the first entry is 'ERROR
- An engineer diagnoses a failed GitLab CE job whose single script line runs 'bash run_all.sh', a wrapper that sequentially calls three validation scripts and does not stop when one errors. The job log
- A GitLab CE test job's `script:` contains one line: `pytest tests/unit; echo 'tests complete'`. Multiple unit tests fail, yet the pipeline marks the job as passed and proceeds to the deploy stage. Ass
- A pyATS validation job in GitLab CE fails. The engineer scrolls straight to the bottom of the job log and sees only 'Cleaning up project directory' followed by 'ERROR: Job failed: exit code 1'. To loc
- An engineer diagnoses a failed GitLab CE test job. At the very bottom of the log, under an 'after_script' section, are several 'command not found' messages, followed by 'ERROR: Job failed: exit code 1
- allow_failure separates advisory from blocking jobs
allow_failure: truelets a job fail without failing the pipeline (it shows as a warning); when it is absent or false, a failed test blocks the pipeline. Use it to distinguish advisory checks from blocking gates.Trap Thinking allow_failure hides the job from the pipeline rather than merely down-grading its failure to a warning.
6 questions test this
- A GitLab CE pipeline reports an overall status of 'passed', but the engineer expected a failing 'terraform validate' job to stop the run. The job's log ends with 'ERROR: Job failed: exit code 1' and i
- A GitLab CE job named lint has this script written as two separate lines: line 1 'ansible-lint site.yml', line 2 'echo lint passed'. In a run, ansible-lint exits with code 2 after reporting rule viola
- A team's .gitlab-ci.yml has a `style` job that runs `ansible-lint` in the test stage. Today any lint finding returns non-zero and blocks the deploy stage. The team wants lint results to be information
- A GitLab CE pipeline has a test stage containing a pytest job and a later deploy stage containing an ansible-playbook job. In one run the pytest job ends with 'ERROR: Job failed: exit code 1', and the
- In a GitLab CE pipeline, the 'security-scan' job shows an orange warning icon and its log ends with 'ERROR: Job failed: exit code 1', yet the overall pipeline status is 'passed' and the deploy stage r
- A team runs a 'bandit' Python security scan in the test stage of a GitLab CE pipeline. Today any finding returns a non-zero exit and blocks the deploy stage, but the team wants scan findings to appear
- script stops at the first failing command
Each line in
script:runs so that the job stops at the first command returning non-zero; a failure early inscript:means the later lines never executed, which the log makes visible by their absence.
References
- https://docs.gitlab.com/ci/yaml/
- https://docs.gitlab.com/ci/jobs/
- https://docs.python.org/3/library/exceptions.html
- https://docs.gitlab.com/ci/caching/
- https://docs.gitlab.com/ci/jobs/job_artifacts/
- https://docs.python.org/3/whatsnew/3.10.html
- https://docs.gitlab.com/ci/variables/predefined_variables/
- https://docs.python.org/3/installing/index.html
- https://developer.cisco.com/docs/pyats/
- https://docs.ansible.com/ansible/latest/