Why did job B miss a file GitLab already built?
Job A is green on the pipeline graph today.
The pipeline graph still looks fully connected though.
Did cache secretly ship that wheel to the next job?
I keep splitting this fight into five myths.
Each myth dies against one GitLab keyword, not vibes.
What this FAQ is not
This FAQ is not another lint API walkthrough.
This is not a pytest-versus-merge-request widget recap.
I am talking artifacts, cache, dotenv, and fake runners.
Myth 1: Cache is artifacts with a friendlier name
Does cache upload your build into GitLab storage?
No, cache lives beside the runner, keyed by you.
GitLab documents this split in very plain language.
Read the GitLab caching guide first, then stop guessing.
Then read the job artifacts page after that.
- Caching: Caching in GitLab CI/CD
- Uploads: Job artifacts
Cache restores files for later jobs on matching keys.
It can miss, go stale, or vanish overnight.
Artifacts upload to GitLab for download in later jobs.
Ask one question before you debug job B.
Did job A declare artifacts paths, or only cache paths?
Tiny pipeline that lies to you
# labeled example: not executed for this article
stages: [build, test]
build_wheel:
stage: build
script:
- mkdir -p dist
- echo "wheel-bytes" > dist/app.whl
cache:
key: "$CI_COMMIT_REF_SLUG"
paths: ["dist/"]
test_wheel:
stage: test
script:
- test -f dist/app.whl
What happens when the cache key misses on test?
The file test command fails on that runner.
The pipeline graph can still look completely fine.
Nobody uploaded that wheel file as an artifact.
Myth 2: If the next job started, it got the files
Did GitLab start test_wheel, so files must exist?
Starting a job only proves the DAG allowed it.
The needs keyword can start jobs early.
It still does not mint the missing files.
An empty dependencies list downloads no artifacts at all.
See current needs and dependencies docs.
Skipped optional needs are another quiet hole.
needs:optional still lets job B run.
Job B then starts without that missing archive.
A green predecessor is not a file transfer receipt.
Decision table I actually use
Signal in .gitlab-ci.yml
|
What GitLab stored | What job B can assume |
|---|---|---|
only cache:paths
|
runner cache, best effort | nothing |
artifacts:paths |
job artifact archive | files if download succeeded |
needs: [job] default |
DAG edge plus artifacts | files from that job |
needs: [{job, artifacts: false}] |
DAG edge only | no files from that job |
needs:optional and the job skipped |
no producer archive | job B still runs |
dependencies: [] |
job still runs | no artifacts downloaded |
artifacts:reports:dotenv |
report parsed by GitLab | variables, not a file tree |
artifacts:when: on_success (default) |
archive only after success | failure means no archive |
Print that table next to the failed job.
Then stop arguing from that pipeline emoji.
Myth 3: dotenv reports unzip like normal artifacts
Did you add a dotenv report for version strings?
GitLab parses that file into variables for later jobs.
The dotenv report docs spell that out.
The workspace does not magically grow that file.
Later jobs see APP_VERSION, not build.env on disk.
Unless you also listed it under artifacts:paths.
# labeled example: not executed for this article
build_meta:
script:
- echo "APP_VERSION=1.4.2" > build.env
artifacts:
reports:
dotenv: build.env
use_meta:
script:
- echo "$APP_VERSION"
- test ! -f build.env
Would cat build.env pass inside use_meta?
Usually no, the file is gone from the workspace.
The variable is the product, not the file.
Myth 4: The failed producer still published the archive
Did job A fail after writing dist/?
Default artifacts:when is on_success, not always.
Confirm it on artifacts:when before you argue.
If the producer failed, later jobs may see nothing.
on_failure and always are explicit choices, not defaults.
Cache when follows a similar success-oriented default.
# labeled example: make the upload policy visible
build_wheel:
stage: build
script:
- mkdir -p dist
- echo "wheel-bytes" > dist/app.whl
- test -f dist/app.whl
artifacts:
paths: ["dist/"]
expire_in: 1 day
when: on_success
cache:
key: "$CI_COMMIT_REF_SLUG"
paths: ["dist/"]
policy: pull-push
Did you open the job's Artifacts browser tab?
An empty tab beats a confident chat summary.
Expiry is instance-specific, so I refuse a canned number.
Check expire_in on the job, not in memory.
Myth 5: A free server replayed the GitLab job
Can I paste the failing script into a chat agent?
Sure, that still is not a GitLab runner.
I use MonkeyCode here for one narrow step.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Free model access and a free server option exist.
I still do not treat that box as a GitLab runner.
Why would I treat a chat box as GitLab?
GitLab injects the usual CI environment variables there.
It also injects a short lived CI_JOB_TOKEN.
It can mount services defined in the YAML.
It applies cache, artifacts, and needs for you.
A free server can run the same shell lines.
It cannot prove GitLab would fetch the same files.
It cannot prove rules would even create the job.
Reproduction workflow I trust
- Export the job log from GitLab, not from chat.
- Copy the job script into a local file.
- Strip every CI variable name you do not understand.
- Run the script inside a throwaway directory.
- Mark the result local-only in the merge note.
# labeled example: local replay, not a GitLab runner
mkdir -p /tmp/local-replay && cd /tmp/local-replay
printf '%s\n' 'test -f dist/app.whl' > job.sh
sh job.sh; echo "exit:$?"
Did that fail on your laptop right away?
Good, you just learned the script's hidden assumption.
Did that pass on a clean directory?
You still learned almost nothing about GitLab itself.
Local green never proves the artifact download happened.
Do not paste CI_JOB_TOKEN into the agent
Tempting, right, because the token already downloads artifacts.
GitLab documents CI_JOB_TOKEN as short-lived and scoped.
Do not paste it into a model prompt.
Do not paste it into a shared free server notebook.
The job token is a credential, not a log snippet.
If you need the archive, use your own machine.
Use a personal access token you can revoke.
Or download the artifact from the GitLab UI.
# labeled example: run locally, never in a prompt
# Replace PROJECT, JOB_ID, and host on your laptop only
curl --fail --header "JOB-TOKEN: $CI_JOB_TOKEN" \
"https://gitlab.example.com/api/v4/projects/$PROJECT/jobs/$JOB_ID/artifacts" \
--output artifacts.zip
unzip -l artifacts.zip | head
That command belongs in a runner or your laptop.
It does not belong in a chat transcript.
An agent cannot cite a zip it never opened.
Commands that read GitLab without feeding a model
I want evidence from GitLab, not from a paraphrase.
These commands stay on a machine I already trust.
Treat every line below as unlabeled production advice? No.
Treat it as a proposal you still have to adapt.
# labeled example: inspect a saved log, no token in chat
grep -n -E 'Downloading artifacts|Checking cache for|Successfully extracted cache|No such file or directory|Could not download artifacts' job.log
Need the archive itself from your laptop session?
Use the UI, or a CLI you already authenticated.
Keep that token out of any model context window.
# labeled example: GitLab CLI on your laptop
# glab must already be logged in for your project
glab ci trace
# Then download artifacts with an API call you control
Did the log mention cache extraction but no artifact download?
That is myth 1 firing in real time.
Did it skip artifact download after needs:artifacts: false?
That is myth 2, not a mysterious missing wheel.
A classifier you can run without GitLab credentials
I want a check that never needs a job token.
This script only reads a saved job log file.
Treat it as a proposal, not a shipped product.
# labeled example: classify a downloaded GitLab job log
from pathlib import Path
MARKERS = {
"missing_file": ("No such file or directory", "test: dist/"),
"cache_hint": ("Checking cache for", "Successfully extracted cache"),
"artifact_hint": ("Downloading artifacts", "Downloading artifacts from"),
"needs_skip": ("failed to pull artifacts", "could not download artifacts"),
}
def classify(log_text: str) -> list[str]:
hits = []
lowered = log_text.lower()
for name, needles in MARKERS.items():
if any(n.lower() in lowered for n in needles):
hits.append(name)
return hits or ["unknown"]
if __name__ == "__main__":
text = Path("job.log").read_text(encoding="utf-8", errors="replace")
print(",".join(classify(text)))
Save the real job log as job.log.
Run python classify_log.py on your own machine.
Then match the label to the decision table above.
I will rewrite the marker list in a free model.
I still run the classifier on a file I downloaded.
The model never holds CI_JOB_TOKEN in that loop.
Corrected mental model
GitLab keeps four different file stories in CI.
- Logs are text GitLab captured from stdout.
- Cache is only a best-effort runner-side shortcut.
- Artifacts are uploaded archives later jobs may fetch.
- Dotenv reports become variables, not a workspace tree.
An agent on a free server adds a fifth story.
It only rewrites commands for you to re-run.
It does not become GitLab in that process.
Who should not use this approach
Do not use local replay as merge-gate evidence.
Do not feed job tokens into any model prompt.
Do not skip artifacts:paths because cache usually works.
If you ship regulated software, keep credentials offline.
If you never read .gitlab-ci.yml, this FAQ will bounce.
If you need GitLab to be the runner, use GitLab.
Limitations
I did not benchmark cache hit rates here.
I did not claim free servers match runner images.
GitLab defaults can change; check current docs before merge.
Log markers can drift across different runner versions.
My classifier will miss silent test -f failures.
Always open the job's YAML and the artifact tab.
Instance admins can change artifact expiry defaults.
I will not quote a number that your server overrode.
Read the job page if the archive already expired.
What I do on the next red job
I ask where the file was supposed to live.
Cache, artifacts, dotenv, or only the chat log?
If the answer is cache, I add artifacts:paths.
If the YAML wording is messy, I draft it with a free model, then I paste GitLab docs links under the change.
The pipeline stays on GitLab, not in the chat.
The job token stays off that chat.
Top comments (0)