DEV Community

Jordan Huang
Jordan Huang

Posted on

FAQ: Five Myths About a Linted GitLab Pipeline

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ./...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Run that helper against the YAML file.

chmod +x scripts/ci-contract-check.sh
./scripts/ci-contract-check.sh .gitlab-ci.yml
Enter fullscreen mode Exit fullscreen mode

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

  1. Draft the script body off GitLab first.
  2. Paste the YAML into the project file.
  3. Run the pattern grep on that file.
  4. Run CI Lint against the real project.
  5. Open a merge request pipeline before tags.
  6. Trigger a schedule inside a dry project.
  7. Confirm artifacts, not cache, 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)