Did your midnight schedule skip every job again?
I still pin a lint badge on merge requests.
That badge does not prove the graph you wanted.
GitLab evaluates those rules at pipeline creation time.
A chat window only evaluates tokens, not runners.
Those two clocks almost never match in practice.
This FAQ offers a corrected mental model.
It is not a product tour at all.
It is a contract checklist I reuse on reviews.
What I draft off GitLab first
I sometimes draft a job script off GitLab.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode offers free model access and a free server option.
I use that pair only to sketch script blocks.
I do not treat that sketch as a runner.
Then I paste the YAML back into GitLab.
Then I lint and I read CI_PIPELINE_SOURCE.
Then I check cache against artifacts with care.
Why do I split the work like that?
Because models copy tidy keywords with confidence.
They do not inherit your private runner tags.
Myth 1: CI Lint is the pipeline contract
You ran CI Lint and the page said valid.
Did GitLab expand every include against your files?
Did it attach your protected variables as well?
Lint is a grammar check for GitLab CI YAML.
It catches unknown keys and broken YAML.
It does not pick a runner for you.
It does not simulate rules on a schedule.
It does not prove your fleet has those tags.
It does not fetch every private remote include.
Corrected model
Treat lint as a compiler front end only.
Treat pipeline creation as the real compiler.
Treat the first job log as the linker.
This proposed check is not a benchmark.
Label it unexecuted until you run it.
# Proposed: syntax only. This is not a runner test.
glab ci lint .gitlab-ci.yml
If you lack glab, use the project lint API.
# Proposed curl. Replace URL, token, and project id.
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--data-urlencode "content@$(cat .gitlab-ci.yml)" \
"https://gitlab.example.com/api/v4/projects/$ID/ci/lint"
A valid lint payload is not a merge receipt.
It is only a spelling report for YAML.
Read valid as "parses", never as "will run".
Myth 2: cache is a quieter artifacts keyword
Does the next job need a file you built?
Then you want artifacts, not a cache key.
cache is a speed hint for later jobs.
GitLab does not guarantee a cache hit.
Runners can miss, rotate, or isolate caches.
artifacts upload into GitLab storage on purpose.
needs and dependencies can fetch those files.
That path is a data contract, not a hint.
# Example only. Not a production pipeline.
build:
stage: build
script:
- mkdir -p dist && echo ok > dist/app.txt
artifacts:
paths:
- dist/
expire_in: 1 day
test:
stage: test
needs: ["build"]
script:
- test -f dist/app.txt
Compare a cache that can vanish without failing GitLab:
# Example: cache is optional. Do not require it.
build:
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
script:
- npm ci
Corrected model
Cache misses are normal in real fleets.
Missing artifacts are bugs in the graph.
If a job must consume a file, declare artifacts.
If a job might go faster, declare cache.
Do not mix those claims in a review.
Ask one question on every file-passing job.
Would this job fail if the cache were cold?
If yes, you still owe an artifacts contract.
Myth 3: rules:changes will skip scheduled jobs
You added changes for your Go package paths.
You expected nights to stay quiet anyway.
Did you also pin CI_PIPELINE_SOURCE in rules?
Path diffs belong to push and merge requests.
Schedules, API triggers, and web runs differ.
They often lack the git push you imagined.
Do not assume changes hides the scheduled job.
Do not assume it always includes that job.
Read the rules as a source-specific filter.
# Example: pair source and path. Review before use.
test:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- src/**/*
- go.mod
- if: $CI_PIPELINE_SOURCE == "push"
changes:
- src/**/*
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
script:
- go test ./...
Corrected model
The changes keyword is not a cron guard.
CI_PIPELINE_SOURCE is the actual cron guard.
Combine them, or scheduled graphs will surprise you.
Ask this question during every pipeline review.
Which pipeline sources should create this job?
Write those sources as if clauses first.
Then attach changes only to sources with diffs.
Push and merge request pipelines usually have diffs.
Schedules and many API pipelines do not.
Myth 4: the chat log documents the YAML
Clean keys are not the same as clear intent.
Did the reviewer open the chat transcript?
Or did they only open .gitlab-ci.yml?
I want the why beside the rules block.
I want the cache key explained in YAML.
I do not want a missing comment there.
# Why: schedule must never publish.
# Why: only protected tags hold the deploy token.
deploy:
stage: deploy
rules:
- if: $CI_COMMIT_TAG && $CI_COMMIT_REF_PROTECTED == "true"
script:
- ./scripts/publish.sh
environment:
name: production
A model can emit a short comment block.
Keep that block in the file, not chat.
The chat thread will rot after merge.
The YAML file remains for the next incident.
Corrected model
The pipeline file is the operational runbook.
The chat window is only a scratch pad.
Reviewers should never merge a scratch pad.
If a rules line needs a spoken footnote, write it.
If a cache:key looks clever, explain the collision.
Future you will not recover the chat search.
Myth 5: a free server run proves image and tags
I can run a script on a free server.
That proves the shell fragment actually parses.
It does not prove the GitLab executor.
GitLab still needs a matching runner tag.
It still needs the container image name.
It still needs services and pull policy.
# Example: these keys never run off GitLab.
qa:
image: python:3.12-slim
tags:
- gpu
- shielded
services:
- name: postgres:16
alias: db
script:
- pytest -q
Your laptop has no gpu runner tag.
A generic free server has no gpu tag.
A green local pytest is still not qa.
Corrected model
Off-platform runs only test the script body.
GitLab runs also test the job envelope.
Envelope means image, tags, services, and rules.
A pending job is an envelope failure.
A timeout after pull is an envelope failure.
A skipped graph is a rules failure, not pytest.
Artifact: a four-row contract table
Use this table inside the merge request.
Tick evidence, not vibes from a chat summary.
| Claim you heard | What GitLab actually checks | What you still must prove | Fail if you skip |
|---|---|---|---|
| Lint said valid | YAML grammar, some includes |
rules versus pipeline source |
Jobs vanish on schedule |
| Cache warmed | Best-effort local or remote cache | File exists without a cache hit | Test job has empty inputs |
changes listed paths |
Diff for some pipeline sources | Source is push or merge request | Nightly graph explodes or dies |
| Script ran elsewhere | Shell exit code on that host |
image, tags, services
|
Job pending, then timeout |
Print the table and tick the last columns.
Do not tick them from a chat summary.
One empty cell means the myth is still live.
Artifact: a proposed pattern grep
This script does not run GitLab itself.
It fails a review when myths leak in.
Treat it as a linter, not a pipeline.
#!/usr/bin/env bash
# Proposed review helper. Label: unexecuted example.
set -euo pipefail
file="${1:-.gitlab-ci.yml}"
fail() { echo "contract: $1"; exit 1; }
test -f "$file" || fail "missing $file"
grep -q 'rules:' "$file" || fail "no rules: blocks found"
if grep -n 'changes:' "$file" >/dev/null; then
grep -q 'CI_PIPELINE_SOURCE' "$file" \
|| fail "changes: without CI_PIPELINE_SOURCE"
fi
if grep -n 'cache:' "$file" >/dev/null; then
echo "note: cache is optional; require artifacts for files"
fi
if grep -n 'tags:' "$file" >/dev/null; then
echo "note: prove those tags exist on a runner"
fi
echo "contract: static checks passed for $file"
Run that helper against the YAML file.
chmod +x scripts/ci-contract-check.sh
./scripts/ci-contract-check.sh .gitlab-ci.yml
I still open the GitLab lint page after.
Static grep cannot expand nested include files.
Static grep cannot see protected variables either.
A short workflow I actually follow
- Draft the
scriptbody off GitLab first. - Paste the YAML into the project file.
- Run the pattern grep on that file.
- Run CI Lint against the real project.
- Open a merge request pipeline before tags.
- Trigger a schedule inside a dry project.
- Confirm
artifacts, notcache, feed tests.
Step six matters more than step one.
Schedules lie differently than merge request graphs.
I want both graphs before a protected tag.
Step seven is the file contract, not speed.
If tests read dist/, that path is an artifact.
A warm cache is a nice extra, nothing more.
Limitations
This FAQ does not replace official GitLab docs.
Keywords move across GitLab releases over time.
Read the current rules page before you copy.
The grep script stays shallow on purpose.
It will miss hidden nested include files.
It will miss spec:inputs on CI components.
I did not publish runner timings in this FAQ.
I did not claim a free server equals shared runners.
Those two claims would be false here.
Protected variables stay invisible on open branches.
A green MR pipeline can still fail on tags.
Test that protected path as a separate step.
include expansion depends on project permissions.
A lint on your fork can miss parent files.
Lint in the destination project, not only locally.
Who should not use this approach
Do not use a free model as the merge gate.
Do not skip lint because a chat said valid.
Do not skip schedules because an MR went green.
Do not follow this if you lack GitLab.
Local YAML theater will not create runners.
You need a real project and real tags.
If you ship regulated artifacts, stop even earlier.
Use your existing CI policy, not a sketch host.
This workflow teaches contracts, not production release.
If your fleet uses custom executors, verify tags first.
Envelope bugs will dominate script bugs there.
This FAQ will not inventory those machines for you.
What I want you to remember
Lint only checks grammar for the YAML.
Cache remains a hint, never a file contract.
Artifacts are the files the next job needs.
The changes keyword is still not cron.
A free server is not a tagged runner.
The chat log is not your runbook.
Which myth did your last incident match?
Fix that row in the table first.
Keep the GitLab lint step on every merge request.
That habit catches broken rules before protected tags.
Top comments (0)