DEV Community

Jordan Huang
Jordan Huang

Posted on

Myth FAQ: What My Clean Draft Run Still Hides

I kept a green pipeline, and I still blocked the merge.
The draft YAML looked tidy, and the job status looked calm.
Why did that pair still fail my review?

I was not hunting for style nits in the script block.
I was hunting for controls I could explain a month later.
A calm log does not answer that kind of question.

The gap I keep tripping over

A control record names the revision that actually ran.
It names the secret boundary, and it names the token scope.
A status badge names none of those three things.

Have you ever confused a successful sketch with a kept decision?
I have, especially when the YAML arrived already formatted.
Speed feels like certainty, and it is not certainty.

This piece is a myth FAQ for that exact mix-up.
Each myth is a claim I still hear in review.
Each correction is a habit you can run on one file.

Where a free draft loop actually helps

I sketch risky YAML with MonkeyCode's free model access.
I sometimes run that sketch through the free server option.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.

The model is useful when I want a first draft quickly.
The free server is useful when I want one execution to fail visibly.
Neither one writes my pin, my scope, or my retention choice.

I was not given quotas, hardware, duration, or model names.
I will not invent those facts to make the draft loop sound stronger.
Missing numbers stay missing until a primary source supplies them.

Remove every product mention and the checks still stand.
That is the bar I want for a technical article.
A vendor sketch is not the control I am trying to build.

Myth 1: a branch name is basically a pin

Have you accepted ref: main because the group felt trusted?
I have seen that excuse survive a green pipeline more than once.
Trust in the authors is not immutability of the included file.

A branch moves the moment someone merges.
A tag can move too, unless tag protection actually blocks the rewrite.
A full commit SHA does not slide forward under your next run.

Corrected model: a pin is a revision your policy treats as fixed.
Write that SHA into the include, and record why you chose it.
Do not let a draft call main reproducible.

include:
  - project: "platform/ci-templates"
    ref: "9f2c1ab0e4d84a6c91b0a1e7c0d4f8a2b6e1d903"
    file: "/templates/scan.yml"
Enter fullscreen mode Exit fullscreen mode

That snippet is an example, not a file I verified in production.
Replace the project path and the SHA with values you control.
If you cannot name the SHA, you do not have a pin yet.

Component versions deserve the same suspicion.
A version tag is only as still as the project that owns it.
Confirm tag protection there before you treat @1.4.2 as frozen.

Myth 2: a masked variable makes the log shareable

Someone told me the variable was masked, so the log was fine.
That sentence is a myth I want out of review culture.
Masking hides an exact string in a job log.

It is not encryption at rest on the runner.
It is not a promise about split, transformed, or encoded values.
Reread the current GitLab masking docs before you paste any trace.

Corrected model: a shareable log is a log you redacted on purpose.
Prefer protected variables for refs you actually protect.
Never ask a draft to echo a secret just to prove the wiring.

job_with_a_secret:
  stage: test
  script:
    - echo "Token is present"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Enter fullscreen mode Exit fullscreen mode

I deliberately avoided printing the variable in that example.
A present-or-absent line still leaks less than a raw echo.
If your draft contains echo "$DEPLOY_TOKEN", stop and delete that line.

Is a protected variable automatically available on every branch?
No. GitLab documents exposure on protected branches and protected tags.
A free server run on an unprotected ref can miss the variable entirely.

Confirm that rule against current docs if your instance is customized.
I am not quoting a menu label from memory as your admin's label.
The setting you see today beats a sentence I remember from last year.

Myth 3: one successful token call means a chosen scope

The job cloned another project with CI_JOB_TOKEN, and the clone worked.
Does that success mean someone designed the allowlist?
No. A default that still permits access is not your written decision.

Job token permissions have changed across GitLab releases.
I will not quote a version cutoff from memory in this article.
Open the project token setting, and match it to current docs.

Corrected model: scope lives in project settings, not in a YAML comment.
A model can insert a token into a git URL.
It cannot see your allowlist unless you bring that evidence yourself.

# Local text check only. This does not query GitLab settings.
grep -n "CI_JOB_TOKEN" .gitlab-ci.yml || echo "no job token string"
Enter fullscreen mode Exit fullscreen mode

I would still open the job token permissions screen after that grep.
The menu wording can change, so I follow the current documentation.
A forbidden settings page is a finding, not a reason to skip the myth.

What would I store beside the merge request?
I store a dated note of who can inbound-call this project with a job token.
I do not store the token value, and I do not paste the clone URL with it.

Myth 4: an artifact URL is retained evidence

The job uploaded an artifact, and the log printed a URL.
Can I cite that URL in an incident review next month?
I cannot know that from the green status alone.

GitLab expires artifacts from expire_in or from the instance default.
Your admin can change that default, so a blog memory is not your setting.
I also do not treat the free server option as a retention guarantee.

Corrected model: evidence I need later gets copied to a store I control.
The pipeline log is a clue, not the archive.
Write expire_in when the artifact is part of the review story.

review_bundle:
  stage: test
  script:
    - python3 check_ci_draft.py .gitlab-ci.yml > draft-report.txt
  artifacts:
    paths:
      - draft-report.txt
    expire_in: 7 days
Enter fullscreen mode Exit fullscreen mode

Seven days is an example choice, not a compliance window I recommend.
Pick a window your team can actually defend in review.
If you need years of retention, this pattern is the wrong tool.

A local checker, labeled unexecuted

This script is a proposal for a single draft file.
I have not executed it for this article, and I am not calling it a pass engine.
It will miss anchors, extends, and !reference indirection.

#!/usr/bin/env python3
"""Local draft check. Not a policy engine. Unexecuted here."""
import sys
from pathlib import Path

MOVING = {"main", "master", "develop", "dev", "head"}
SECRET_HINTS = ("token", "secret", "password", "key")

def main() -> int:
    path = Path(sys.argv[1] if len(sys.argv) > 1 else ".gitlab-ci.yml")
    text = path.read_text(encoding="utf-8")
    findings = []
    for i, line in enumerate(text.splitlines(), start=1):
        stripped = line.strip().lower()
        if stripped.startswith("ref:"):
            value = stripped.split(":", 1)[1].strip().strip("'\"")
            short = len(value) < 40
            if value in MOVING or (value and not value[0].isdigit() and short):
                findings.append(f"{i}: moving or short ref '{value}'")
        if "echo" in stripped and any(k in stripped for k in SECRET_HINTS):
            findings.append(f"{i}: script may print a secret")
    if "artifacts:" in text and "expire_in:" not in text:
        findings.append("artifacts block has no expire_in")
    if "ci_job_token" in text.lower():
        findings.append("CI_JOB_TOKEN present; confirm scope outside this file")
    if not findings:
        print("no local findings; this is not a pass")
        return 0
    print("findings:")
    for item in findings:
        print(f"- {item}")
    return 1

if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

The ref heuristic is intentionally blunt.
A short tag can be a fair pin if tag protection holds.
Treat every finding as a question, not as a guilty verdict.

python3 check_ci_draft.py .gitlab-ci.yml
echo "exit=$?"
Enter fullscreen mode Exit fullscreen mode

I would run that before any free server execution of the same draft.
A zero exit only means this file tripped no local rule.
It never means the pipeline is pinned, secret-safe, or approved.

Walk a bad draft before you trust the script

Read this fixture out loud before you automate anything.
I expect three questions, even if the script prints fewer lines.
Would you merge it only because a free server once went green?

include:
  - project: "platform/ci-templates"
    ref: main
    file: "/templates/scan.yml"

leak_check:
  script:
    - echo "debug token is $DEPLOY_TOKEN"
    - git clone "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.example/app/ops.git"
  artifacts:
    paths:
      - scan.json
Enter fullscreen mode Exit fullscreen mode

My manual findings are boring, and that is the point.

  • The include ref is a moving branch, so the next run can differ.
  • The script prints a deployment token into a log that people share.
  • The clone uses a job token without a settings note beside the change.
  • The artifact block never says when scan.json should disappear.

A model can repair those lines after you name the findings.
Do not ask it to repair them before you name the findings.
Otherwise you get a cleaner myth, not a clearer control.

Tiny test plan for the checker

I would keep four fixtures beside the script, and I would not skip the last one.
This plan is proposed, not a run I am reporting as complete.

  1. A file with ref: main must exit nonzero and mention that ref.
  2. A forty-character SHA ref must not be flagged as a moving branch.
  3. A line that echoes a token must exit nonzero.
  4. An artifacts block without expire_in must be listed.
  5. A quiet file must print that no finding is not a pass.

If fixture two flags every SHA that starts with a letter, fix the heuristic.
Do not weaken fixture one to hide that bug.
A blunt tool is allowed, and a lying tool is not.

The decision table I keep beside the draft

Claim I heard What I check Corrected model
The include came from our group ref is a SHA, not a branch Group trust is not immutability
The variable is masked No echo and no encoded reprint Masking is a log filter
The token clone worked Job token permissions in settings Success is not a written scope
The artifact URL exists expire_in plus an external copy A URL is not retention

I read one row out loud before I approve a drafted pipeline.
If I cannot point at the middle column, I do not merge.
The right column is the sentence I want in the review note.

Who should skip this habit

Do not use this habit as a compliance attestation.
Do not use it when your reviewer wants signed provenance.
Do not use a free server run as proof of isolation, hardware, or uptime.

Skip it if you need a locked archive measured in years.
Skip it if CODEOWNERS and approval rules are the control you actually lack.
This checker never sees those systems, so it cannot replace them.

Also skip it when the YAML is mostly generated at runtime.
A text scan cannot follow includes that appear only after a script runs.
In that case, capture the expanded config from GitLab and review that file.

How I close the review

I ask four questions, and I wait for boring answers.

  1. Which SHA did we include, and who can move a tag if we used one?
  2. Which project setting scopes the job token, and where is that note stored?
  3. Which log lines are safe to paste into chat after this run?
  4. Where did the artifact get copied, and when does the copy expire?

If I cannot answer those, the green status can wait.
The free model can redraft the YAML after the checker complains.
The free server can rerun the sketch after the pin is real.

If a free model session is already open, feed it these findings before another hopeful include.

Top comments (0)