<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jordan Huang</title>
    <description>The latest articles on DEV Community by Jordan Huang (@gitlab_3188).</description>
    <link>https://dev.to/gitlab_3188</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4066558%2F3018daf5-9f41-49c5-89c9-a0dadd1551c2.png</url>
      <title>DEV Community: Jordan Huang</title>
      <link>https://dev.to/gitlab_3188</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gitlab_3188"/>
    <language>en</language>
    <item>
      <title>FAQ: A Free Model Lease Is Not a Server Lease</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:03:27 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/faq-a-free-model-lease-is-not-a-server-lease-4lhc</link>
      <guid>https://dev.to/gitlab_3188/faq-a-free-model-lease-is-not-a-server-lease-4lhc</guid>
      <description>&lt;p&gt;Your free model just said the patch landed cleanly. Did the free server actually accept that same patch? I keep seeing this leap in review threads.&lt;/p&gt;

&lt;p&gt;A cheap answer turns into a deploy story fast. Then that box still serves yesterday's old binary. Who actually owns that gap in your story?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this myth keep winning?
&lt;/h2&gt;

&lt;p&gt;People still love one receipt for two separate gifts. A free model session feels like real compute. A free server option feels like real hosting.&lt;/p&gt;

&lt;p&gt;I keep hearing them merged into one stack. That quiet merger is the actual bug here. Those two leases can fail on their own.&lt;/p&gt;

&lt;p&gt;MonkeyCode offers free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. I treat those lines as availability claims only.&lt;/p&gt;

&lt;p&gt;I do not treat them as a quota sheet. I will not invent model names in this note. I will not invent hardware or duration either.&lt;/p&gt;

&lt;p&gt;If a console shows numbers, those numbers win. A screenshot from last month is not a contract. Write the date next to any number you paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which claims keep showing up?
&lt;/h2&gt;

&lt;p&gt;These lines show up after a green chat. Each one sounds practical at first quick glance. Each one quietly skips a real host receipt.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"The model wrote the file, so the server has it."&lt;/li&gt;
&lt;li&gt;"The free box is up, so the model saw my repo."&lt;/li&gt;
&lt;li&gt;"One retry covers both the chat and the host."&lt;/li&gt;
&lt;li&gt;"Free access means this path is a release path."&lt;/li&gt;
&lt;li&gt;"A health URL alone proves the patch bytes."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do you want a cleaner mental model now? I split this world into two separate ledgers. A model ledger records only the model calls.&lt;/p&gt;

&lt;p&gt;A server ledger records files and live processes. Neither ledger inherits the other side's lease. You store both lines before you claim a win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 1: Is the reply the diff?
&lt;/h2&gt;

&lt;p&gt;The model can describe a file almost perfectly. That text still lives only in the transcript. It is still not an inode on the host.&lt;/p&gt;

&lt;p&gt;I ask one blunt question every single time. Where is the content hash from that host? If you cannot paste it, you do not have a diff.&lt;/p&gt;

&lt;p&gt;Corrected model: generation is not the same as placement. Placement still needs a write on that host. Then you hash the exact path you named.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 2: Did a live box see the repo?
&lt;/h2&gt;

&lt;p&gt;A free server can boot with an empty home. Your repo may sit only on your laptop. Uptime alone does not mount your git remote.&lt;/p&gt;

&lt;p&gt;I check three facts before I trust a read. Which user account is actually running that process? Which directory is the real current working directory?&lt;/p&gt;

&lt;p&gt;Which git remote URL is actually configured there? I write those three answers in the note. I do not accept a shrug as an answer.&lt;/p&gt;

&lt;p&gt;Corrected model: reachability is not the same as context. Real context means a cwd plus working credentials. Without those facts, the model is only guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 3: Can one retry heal both sides?
&lt;/h2&gt;

&lt;p&gt;A model retry only spends another model call. A server retry restarts a process or job. Those clocks are not the same clock.&lt;/p&gt;

&lt;p&gt;People mash one shared button for both failures. The chat pane retries all on its own. The host still keeps the old process running.&lt;/p&gt;

&lt;p&gt;Which failure did you actually mean to hit? Name it in the note before you click again. A second click without a name is noise.&lt;/p&gt;

&lt;p&gt;Corrected model: name the failing ledger before you retry. You then retry only that one failing ledger. Record the new id beside the old id.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 4: Does free mean release-ready?
&lt;/h2&gt;

&lt;p&gt;Free access only answers a narrow cost question. It does not answer a real promotion question. A real release needs owners, logs, and rollback.&lt;/p&gt;

&lt;p&gt;I would not ship customer traffic this way. A small demo can still live here safely. A production pager should not depend on this.&lt;/p&gt;

&lt;p&gt;Corrected model: price and promotion are different gates. You should pass the cheap gate first here. Then you pass the release gate on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 5: Does health prove the bytes?
&lt;/h2&gt;

&lt;p&gt;A 200 from /health means the process answered. It does not mean your patch is on disk. It does not mean the model read that disk.&lt;/p&gt;

&lt;p&gt;I want a content hash, not a smile code. You compare the content hash you actually expected. Then compare the hash the host actually prints.&lt;/p&gt;

&lt;p&gt;Corrected model: liveness is not the same as integrity. Integrity is a digest over the named path. Liveness is only a short timeout story here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the corrected picture?
&lt;/h2&gt;

&lt;p&gt;Hold two leases in your head at once. A model lease buys only fresh text generation. A server lease buys a place to run.&lt;/p&gt;

&lt;p&gt;A lease does not copy files by sympathy. A lease does not share retries with the other. You reconcile them with hashes and call ids.&lt;/p&gt;

&lt;h2&gt;
  
  
  A two-ledger decision table
&lt;/h2&gt;

&lt;p&gt;Use this table before you start the argument. Pick the row that actually matches the evidence. Do not pick the row that matches the vibe.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evidence you hold&lt;/th&gt;
&lt;th&gt;Model ledger&lt;/th&gt;
&lt;th&gt;Server ledger&lt;/th&gt;
&lt;th&gt;What you may claim&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transcript only&lt;/td&gt;
&lt;td&gt;Call happened&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;The model spoke&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host hash only&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;File exists now&lt;/td&gt;
&lt;td&gt;The box has bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both ids and same hash&lt;/td&gt;
&lt;td&gt;Call happened&lt;/td&gt;
&lt;td&gt;Write matches&lt;/td&gt;
&lt;td&gt;Placement matched this call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health 200 only&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Process answered&lt;/td&gt;
&lt;td&gt;The process is up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry of chat only&lt;/td&gt;
&lt;td&gt;New call id&lt;/td&gt;
&lt;td&gt;Old state likely&lt;/td&gt;
&lt;td&gt;You did not retry the host&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read that last column out loud before you post. If it feels smaller than your Slack message, shrink the message. I do this small check before I reply.&lt;/p&gt;

&lt;h2&gt;
  
  
  An unexecuted check you can copy
&lt;/h2&gt;

&lt;p&gt;This script is a proposal, not a lab report. I have not executed it against your host. Please label it that way in your notes.&lt;/p&gt;

&lt;p&gt;It assumes a local shell on the server. It assumes curl and sha256sum both exist there. It does not assume any vendor product name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="c"&gt;# UNEXECUTED EXAMPLE. Fill paths from your own session.&lt;/span&gt;
&lt;span class="nv"&gt;MODEL_ID_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MODEL_ID_FILE&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;./model_call_id.txt&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;EXPECT_HASH_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;EXPECT_HASH_FILE&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;./expected.sha256&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;./app.py&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;HEALTH_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HEALTH_URL&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;http&lt;/span&gt;://127.0.0.1:8080/health&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ledger,key,value"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MODEL_ID_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;model_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[:space:]'&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MODEL_ID_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"model,call_id,&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;model_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"model,call_id,MISSING"&lt;/span&gt;
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;host_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"server,path,&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"server,sha256,&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host_hash&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"server,path,MISSING"&lt;/span&gt;
  &lt;span class="nv"&gt;host_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXPECT_HASH_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;expect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXPECT_HASH_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"compare,expected,&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;expect&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host_hash&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host_hash&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$expect&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"compare,match,yes"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"compare,match,no"&lt;/span&gt;
  &lt;span class="k"&gt;fi
else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"compare,expected,MISSING"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;health_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}'&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HEALTH_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;curl_fail&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"server,health_http,&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;health_code&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"server,user,&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-un&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"server,cwd,&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep paths free of commas or the row splits. sha256sum is a coreutils command, not a shell builtin. If it is missing, use shasum -a 256 instead.&lt;/p&gt;

&lt;p&gt;Run it on the server, not on your laptop. Save that stdout right next to the transcript. That saved pair is your only real artifact.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash two_ledger_check.sh | &lt;span class="nb"&gt;tee &lt;/span&gt;receipts.tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I ask these three plain questions next. Does that saved model call id look real? Does the compare match field actually say yes?&lt;/p&gt;

&lt;p&gt;Does the server health field say 200 now? A missing call id means you never left the story. A match of no means the bytes differ.&lt;/p&gt;

&lt;p&gt;A curl_fail means you checked the wrong port. A timeout is not a hash match in disguise. I record the raw line before I interpret it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commands I paste beside the chat
&lt;/h2&gt;

&lt;p&gt;I do not trust a paragraph without a command. I paste these commands right after the script. Each one answers one broken table cell only.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-un&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git rev-parse &lt;span class="nt"&gt;--show-toplevel&lt;/span&gt;
git status &lt;span class="nt"&gt;--short&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git rev-parse HEAD
&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'%U %Y %n'&lt;/span&gt; ./app.py
&lt;span class="nb"&gt;sha256sum&lt;/span&gt; ./app.py
ss &lt;span class="nt"&gt;-ltnp&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR==1 || /:8080/'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The id line tells me the user and cwd. The git line tells me whether a repo exists. The stat line tells me owner and mtime.&lt;/p&gt;

&lt;p&gt;The hash line is the server ledger entry. The socket line shows who holds the port. If git rev-parse fails, stop claiming a repo read.&lt;/p&gt;

&lt;p&gt;That failure is evidence, not a tool bug. A dirty work tree is not a successful write. An empty git status is not a model confession.&lt;/p&gt;

&lt;p&gt;The stat flag in that snippet is GNU specific. On macOS, switch to stat -f before you run it. ss may hide process names without extra rights.&lt;/p&gt;

&lt;p&gt;Treat a blank process field as unknown, not proof. A hidden process name is not a clean bill. I would rather see unknown than invent an owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I read a mismatch
&lt;/h2&gt;

&lt;p&gt;Start with the smallest broken cell you see. Please do not restart the whole world yet. I want one cause, not a new myth.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing model id: save the call id before you edit.&lt;/li&gt;
&lt;li&gt;Missing path: you targeted the wrong cwd or user.&lt;/li&gt;
&lt;li&gt;Hash mismatch: the write went elsewhere or never ran.&lt;/li&gt;
&lt;li&gt;Health fail: the process is down or bound elsewhere.&lt;/li&gt;
&lt;li&gt;Health pass plus hash miss: liveness lied about bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each bullet is a next command, not a vibe. I paste the command output under the bullet. Then the whole review thread gets much quieter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this check does not prove
&lt;/h2&gt;

&lt;p&gt;A matching hash is still a narrow win. It only says this path matched this digest. It does not say your tests already passed.&lt;/p&gt;

&lt;p&gt;It does not say the free option lasts. Availability can still change without a blog post. Check the console on the day you care.&lt;/p&gt;

&lt;p&gt;It does not name a model for you. It does not set a quota for you. Those fields belong to the vendor page you can see.&lt;/p&gt;

&lt;p&gt;I am not claiming any benchmark in this note. I am not claiming any customer result here. If I lack a number, I leave the cell blank.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should skip this check?
&lt;/h2&gt;

&lt;p&gt;Skip this flow if you need a signed release. Skip it if your policy forbids free hosts. Skip it if secrets would land in a transcript.&lt;/p&gt;

&lt;p&gt;Skip it if you cannot open a shell on the box. The check script needs that shell to be open. A screenshot of chat cannot replace that shell.&lt;/p&gt;

&lt;p&gt;Skip it if two teams share one free box. You will not know who wrote the file. Get a private path on that box first.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short close
&lt;/h2&gt;

&lt;p&gt;I still like free model access for drafts. I still like a free server for a sandbox read. I do not let either receipt sign the other.&lt;/p&gt;

&lt;p&gt;If you want that pair, try MonkeyCode and keep both receipts. The current docs should beat any sentence I wrote. Your own console should beat both of them.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Myth FAQ: Three Papers a Green Badge Cannot Sign</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Tue, 13 Oct 2026 10:38:28 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/myth-faq-three-papers-a-green-badge-cannot-sign-h9f</link>
      <guid>https://dev.to/gitlab_3188/myth-faq-three-papers-a-green-badge-cannot-sign-h9f</guid>
      <description>&lt;p&gt;Did your free server paint the job green?&lt;br&gt;
People still call that badge a signed contract.&lt;br&gt;
I do not buy that story at all.&lt;/p&gt;

&lt;p&gt;A green badge answers only one small question.&lt;br&gt;
Something ran once, on some machine, under rules you may not share.&lt;br&gt;
Which repeated claims still hide inside that badge?&lt;/p&gt;

&lt;p&gt;I keep four of them in this FAQ.&lt;br&gt;
Each one gets a claim, a check, and a cleaner model.&lt;br&gt;
None of those claims gets a free pass from me.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I mean by a receipt
&lt;/h2&gt;

&lt;p&gt;I want three papers before I trust a change.&lt;br&gt;
The chat note only counts as paper one.&lt;br&gt;
The job log only counts as paper two.&lt;/p&gt;

&lt;p&gt;The runner contract only counts as paper three.&lt;br&gt;
Paper one can be fluent and still wrong.&lt;br&gt;
Paper two can be green and still narrow.&lt;/p&gt;

&lt;p&gt;Paper three holds registration, tags, and protection flags.&lt;br&gt;
Can one free minute really replace all three?&lt;br&gt;
No, one minute cannot replace the other papers.&lt;/p&gt;

&lt;p&gt;It can only fill paper two, and only for that run.&lt;br&gt;
Ask yourself which paper you are actually holding.&lt;br&gt;
If you cannot name it, you are still guessing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 1: A free job shares production tags
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The claim
&lt;/h3&gt;

&lt;p&gt;Here is the claim I want to retire first.&lt;br&gt;
The project is the same, so the runner class matches.&lt;br&gt;
That leap quietly skips the runner registration step.&lt;/p&gt;
&lt;h3&gt;
  
  
  The check
&lt;/h3&gt;

&lt;p&gt;Shared capacity and your group runner still differ.&lt;br&gt;
They can share a YAML file and still diverge.&lt;br&gt;
A script match is not a machine match.&lt;/p&gt;

&lt;p&gt;What can you actually read after the run?&lt;br&gt;
Look for CI_RUNNER_ID inside the saved job log.&lt;br&gt;
Look for CI_RUNNER_TAGS in that same job log.&lt;br&gt;
Look for CI_RUNNER_DESCRIPTION when the instance prints it.&lt;/p&gt;

&lt;p&gt;Empty tags are not some hidden allow-list.&lt;br&gt;
Missing tags are not your real production tags.&lt;br&gt;
A different id means a different contract today.&lt;/p&gt;
&lt;h3&gt;
  
  
  The corrected model
&lt;/h3&gt;

&lt;p&gt;So what is the corrected mental model here?&lt;br&gt;
Tags are an allow-list, not a friendly vibe.&lt;br&gt;
Name the tag when a private registry is required.&lt;/p&gt;

&lt;p&gt;If the free server lacks that tag, stop.&lt;br&gt;
That green run never walked the private path.&lt;br&gt;
Do not promote the result into a release note.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 2: Protected variables ride every free job
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The claim
&lt;/h3&gt;

&lt;p&gt;Here is another claim I want retired next.&lt;br&gt;
The variable lives on the project, so every job gets it.&lt;br&gt;
Storage on the settings page is not runtime exposure.&lt;/p&gt;
&lt;h3&gt;
  
  
  The check
&lt;/h3&gt;

&lt;p&gt;GitLab has long limited protected variables by ref.&lt;br&gt;
They show up on protected branches and protected tags.&lt;br&gt;
They stay hidden on ordinary feature-branch pipelines.&lt;/p&gt;

&lt;p&gt;Confirm that exposure rule on the docs you trust.&lt;br&gt;
The flag CI_COMMIT_REF_PROTECTED is what I read first.&lt;br&gt;
The string true and the string false differ.&lt;/p&gt;

&lt;p&gt;Those two strings mark two different security worlds.&lt;br&gt;
A feature-branch job often prints false right there.&lt;br&gt;
That job cannot prove your deploy token works.&lt;/p&gt;

&lt;p&gt;A missing value is the expected result, not a puzzle.&lt;br&gt;
Did someone paste the raw value into chat?&lt;br&gt;
Stop before the model even answers that prompt.&lt;/p&gt;
&lt;h3&gt;
  
  
  The corrected model
&lt;/h3&gt;

&lt;p&gt;A useful receipt never needs the secret itself.&lt;br&gt;
Keep the variable name and the protection flag.&lt;br&gt;
Record whether this job could see the name.&lt;/p&gt;

&lt;p&gt;What model should replace that casual claim now?&lt;br&gt;
Storage, exposure, and proof are three separate steps.&lt;br&gt;
Free compute does not promote your feature branch.&lt;/p&gt;

&lt;p&gt;Free model access does not widen variable scope.&lt;br&gt;
A chat replay of a secret is not a test.&lt;br&gt;
It is a leak with extra steps attached.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 3: Fluent prose validated your YAML
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The claim
&lt;/h3&gt;

&lt;p&gt;A softer claim shows up after a late edit.&lt;br&gt;
The model rewrote the job, so the rules are safe.&lt;br&gt;
Reading text is not the same as scheduling work.&lt;/p&gt;
&lt;h3&gt;
  
  
  The check
&lt;/h3&gt;

&lt;p&gt;I already file a chat draft as an unread diff.&lt;br&gt;
This myth still sneaks past that careful habit.&lt;br&gt;
Local consistency can still fail on your runner.&lt;/p&gt;

&lt;p&gt;An image name can exist in prose and fail.&lt;br&gt;
A service alias can look tidy and stay unreachable.&lt;br&gt;
A needs edge can be valid text and still wait.&lt;/p&gt;

&lt;p&gt;What do I ask the model to produce instead?&lt;br&gt;
I ask for an assumption list I can check.&lt;br&gt;
Which image tag did you assume is present?&lt;/p&gt;

&lt;p&gt;Which runner tag did you assume is registered?&lt;br&gt;
Which ref protection state did you assume holds?&lt;br&gt;
Which files does this job actually execute today?&lt;/p&gt;
&lt;h3&gt;
  
  
  The corrected model
&lt;/h3&gt;

&lt;p&gt;Then I check those lines against the job page.&lt;br&gt;
I do not ask the model to bless the pipeline.&lt;br&gt;
I ask it to expose every quiet guess.&lt;/p&gt;

&lt;p&gt;What is the cleaner model I keep nearby?&lt;br&gt;
A suggestion is a diff that carries footnotes.&lt;br&gt;
The footnotes are the useful part of the reply.&lt;/p&gt;

&lt;p&gt;The confident closing sentence is just optional noise.&lt;br&gt;
If a footnote has no log line, park it.&lt;br&gt;
Come back only when paper two already exists.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 4: One green run freezes tomorrow
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The claim
&lt;/h3&gt;

&lt;p&gt;This claim shows up when a demo felt smooth.&lt;br&gt;
It passed on the free server, so tomorrow will match.&lt;br&gt;
Tags float, caches drift, and mirrors move overnight.&lt;/p&gt;
&lt;h3&gt;
  
  
  The check
&lt;/h3&gt;

&lt;p&gt;The tag alpine:3.20 is a label, not a digest.&lt;br&gt;
A retag can change bytes without touching your YAML.&lt;br&gt;
A cleared cache can turn one job into another.&lt;/p&gt;

&lt;p&gt;I pin by digest when the job gates a release.&lt;br&gt;
I treat cache keys as speed, not as correctness.&lt;br&gt;
I store the job URL beside the commit SHA.&lt;/p&gt;
&lt;h3&gt;
  
  
  The corrected model
&lt;/h3&gt;

&lt;p&gt;Does a free server make pinning less urgent?&lt;br&gt;
It makes image pinning more urgent for me.&lt;br&gt;
You control that host less than a private runner.&lt;/p&gt;

&lt;p&gt;What should you remember after the sample passes?&lt;br&gt;
A pass is one sample, not a freeze-frame.&lt;br&gt;
Store the sample id where you can reopen it.&lt;/p&gt;

&lt;p&gt;Do not store a vibe about the environment.&lt;br&gt;
Tomorrow's queue can land on another host.&lt;br&gt;
Your YAML did not freeze that host for you.&lt;/p&gt;
&lt;h2&gt;
  
  
  A receipt script you can review first
&lt;/h2&gt;

&lt;p&gt;I have not executed this script on your runner.&lt;br&gt;
Treat the file as a proposal, then review it.&lt;br&gt;
It prints facts a normal job already holds.&lt;/p&gt;

&lt;p&gt;It does not print secret values on purpose.&lt;br&gt;
Why do I use unset instead of a filler word?&lt;br&gt;
The dash form keeps an empty value empty.&lt;/p&gt;

&lt;p&gt;The colon form would hide that difference.&lt;br&gt;
Empty tags and unset tags are different facts.&lt;br&gt;
You can rehearse those branches on a laptop.&lt;/p&gt;

&lt;p&gt;This stand-in does not contact GitLab at all.&lt;br&gt;
No token belongs in this local practice command.&lt;br&gt;
The tag line should be empty, not the word missing.&lt;/p&gt;

&lt;p&gt;Print the file if your grep behaves differently.&lt;br&gt;
Do not fix the script until you can explain it.&lt;br&gt;
A short job can publish that text file.&lt;/p&gt;

&lt;p&gt;Use artifact when always if failed jobs still matter.&lt;br&gt;
Never echo variables that hold tokens or keys.&lt;br&gt;
RECEIPT_IMAGE_HINT is only a label you chose.&lt;/p&gt;

&lt;p&gt;GitLab will not invent an image digest for you.&lt;br&gt;
Resolve the digest in a step you already trust.&lt;br&gt;
Then write that digest into the receipt yourself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# Proposal only. Review before you add it.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;
&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;-receipt.txt&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"job_id=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_JOB_ID&lt;/span&gt;&lt;span class="p"&gt;-unset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pipeline_id=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_PIPELINE_ID&lt;/span&gt;&lt;span class="p"&gt;-unset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"job_url=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_JOB_URL&lt;/span&gt;&lt;span class="p"&gt;-unset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sha=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_COMMIT_SHA&lt;/span&gt;&lt;span class="p"&gt;-unset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ref=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_COMMIT_REF_NAME&lt;/span&gt;&lt;span class="p"&gt;-unset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ref_protected=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_COMMIT_REF_PROTECTED&lt;/span&gt;&lt;span class="p"&gt;-unset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"runner_id=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_RUNNER_ID&lt;/span&gt;&lt;span class="p"&gt;-unset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"runner_desc=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_RUNNER_DESCRIPTION&lt;/span&gt;&lt;span class="p"&gt;-unset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"runner_tags=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_RUNNER_TAGS&lt;/span&gt;&lt;span class="p"&gt;-unset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"image_hint=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RECEIPT_IMAGE_HINT&lt;/span&gt;&lt;span class="p"&gt;-unset&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Local stand-in. This does not contact GitLab.&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CI_JOB_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CI_PIPELINE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;55
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CI_JOB_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://gitlab.example/jobs/1001
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CI_COMMIT_SHA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;abc123def456
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CI_COMMIT_REF_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;feature/demo
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CI_COMMIT_REF_PROTECTED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
export &lt;/span&gt;&lt;span class="nv"&gt;CI_RUNNER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CI_RUNNER_DESCRIPTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shared
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CI_RUNNER_TAGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;RECEIPT_IMAGE_HINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alpine:3.20
sh ci-receipt.sh /tmp/ci-receipt.txt
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'ref_protected=false'&lt;/span&gt; /tmp/ci-receipt.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;receipt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alpine:3.20&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;RECEIPT_IMAGE_HINT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alpine:3.20&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sh ci-receipt.sh ci-receipt.txt&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ci-receipt.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A digest check can live beside the receipt file.&lt;br&gt;
I have not run this against your registry.&lt;br&gt;
Point it at the image you actually ship.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposal. Point this at the image you ship.&lt;/span&gt;
docker buildx imagetools inspect alpine:3.20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The decision table I keep open
&lt;/h2&gt;

&lt;p&gt;I keep a small table beside the log.&lt;br&gt;
Each row splits real proof from wishful reading.&lt;br&gt;
I read each row from left to right.&lt;/p&gt;

&lt;p&gt;If the right cell blocks my claim, I stop.&lt;br&gt;
I go and collect the missing paper instead.&lt;br&gt;
Wishing harder does not fill that empty cell.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;th&gt;What it never proves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CI_JOB_URL&lt;/td&gt;
&lt;td&gt;Where to reopen that run&lt;/td&gt;
&lt;td&gt;That a later run matches it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI_COMMIT_SHA&lt;/td&gt;
&lt;td&gt;Which commit was checked out&lt;/td&gt;
&lt;td&gt;That default-branch YAML matches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI_COMMIT_REF_PROTECTED&lt;/td&gt;
&lt;td&gt;Whether this ref is protected&lt;/td&gt;
&lt;td&gt;That every variable was exposed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI_RUNNER_ID&lt;/td&gt;
&lt;td&gt;Which runner accepted the job&lt;/td&gt;
&lt;td&gt;That registration survives tomorrow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI_RUNNER_TAGS&lt;/td&gt;
&lt;td&gt;Tags reported for this job&lt;/td&gt;
&lt;td&gt;That production tags were implied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image hint&lt;/td&gt;
&lt;td&gt;The label you claimed to run&lt;/td&gt;
&lt;td&gt;The bytes, unless you pinned a digest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where a free model and a free server fit
&lt;/h2&gt;

&lt;p&gt;I still want a first pass on the assumption list.&lt;br&gt;
I still want one real job log I can archive.&lt;br&gt;
MonkeyCode offers free model access and a free server option for that narrow drill.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;Those two options are availability claims I was given.&lt;br&gt;
I am not naming a model or quoting a quota.&lt;br&gt;
I am not describing hardware, duration, or a benchmark.&lt;/p&gt;

&lt;p&gt;I am not claiming the offer stays open forever.&lt;br&gt;
How do I keep that help inside a narrow lane?&lt;br&gt;
I paste redacted logs, never raw secret values.&lt;/p&gt;

&lt;p&gt;I ask for assumption lists, never a pipeline blessing.&lt;br&gt;
I run the receipt script on the free server once.&lt;br&gt;
I compare runner id and tags with the contract.&lt;/p&gt;

&lt;p&gt;I refuse to treat a chat summary as paper three.&lt;br&gt;
Why is the free server still worth one trial?&lt;br&gt;
It can fail in a log you can archive.&lt;/p&gt;

&lt;p&gt;A missing tag shows up as a skip or an error.&lt;br&gt;
A protected ref flag shows up as plain text.&lt;br&gt;
Why is a free model still worth that first pass?&lt;/p&gt;

&lt;p&gt;It stays patient with those boring rules diffs.&lt;br&gt;
It will list branches I skip when I am tired.&lt;br&gt;
It cannot see runner registration unless you provide it.&lt;/p&gt;

&lt;p&gt;Want a low-stakes place to practice this split?&lt;br&gt;
Run the receipt once on that free server option.&lt;br&gt;
Then keep only the job URL you can reopen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits I will not talk past
&lt;/h2&gt;

&lt;p&gt;This script trusts predefined variables in the job.&lt;br&gt;
A compromised runner can still lie inside a log.&lt;br&gt;
A green file from a hostile host is not notarized.&lt;/p&gt;

&lt;p&gt;I am not offering a security certification here.&lt;br&gt;
I am offering a habit that catches common mix-ups.&lt;br&gt;
Your compliance team may still require a private runner.&lt;/p&gt;

&lt;p&gt;Image digests need a resolver you already trust.&lt;br&gt;
The hint variable does not compute those digests.&lt;br&gt;
Copying a tag into the file is a label, not a pin.&lt;/p&gt;

&lt;p&gt;Local stand-ins use fake ids on purpose.&lt;br&gt;
They prove how the script branches on empty values.&lt;br&gt;
They prove nothing about your current GitLab version.&lt;/p&gt;

&lt;p&gt;Check the variable list for the instance you run.&lt;br&gt;
Shared capacity can be queued or simply unavailable.&lt;br&gt;
A failed start is not proof your YAML is invalid.&lt;/p&gt;

&lt;p&gt;Read the job page before you rewrite the config.&lt;br&gt;
A queue delay is not a logic bug in your rules.&lt;br&gt;
Separate capacity pain from a real contract miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should skip this approach
&lt;/h2&gt;

&lt;p&gt;Skip this when the job handles production secrets.&lt;br&gt;
A free server is the wrong rehearsal room for that.&lt;br&gt;
Use a protected runner and a protected ref instead.&lt;/p&gt;

&lt;p&gt;Skip this when you need a stable performance number.&lt;br&gt;
One free job is a single sample, not a benchmark.&lt;br&gt;
I will not pretend that sample is a percentile.&lt;/p&gt;

&lt;p&gt;Skip this when your policy forbids external runners.&lt;br&gt;
Availability on a page is not permission to use it.&lt;br&gt;
Your security review outranks a convenient job log.&lt;/p&gt;

&lt;p&gt;Skip this when a model asks you to paste a token.&lt;br&gt;
No check in this FAQ needs a live secret.&lt;br&gt;
Redact the log, then ask the question again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do on the next red job
&lt;/h2&gt;

&lt;p&gt;I open the job URL before I read the chat.&lt;br&gt;
I copy the runner id, the tags, and the ref flag.&lt;br&gt;
I ignore the model's first paragraph until those exist.&lt;/p&gt;

&lt;p&gt;Then I ask one narrow question of the draft.&lt;br&gt;
Which assumption here contradicts these three logged lines?&lt;br&gt;
That question keeps the chat inside its lane.&lt;/p&gt;

&lt;p&gt;Does the green badge still matter after all that?&lt;br&gt;
Yes, it still matters as paper two only.&lt;br&gt;
It never gets to sign paper three for me.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>cicd</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>FAQ: Which Myths Die When the Job Id Changes?</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Tue, 13 Oct 2026 05:00:26 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/faq-which-myths-die-when-the-job-id-changes-5h4c</link>
      <guid>https://dev.to/gitlab_3188/faq-which-myths-die-when-the-job-id-changes-5h4c</guid>
      <description>&lt;p&gt;Did your last retry orphan the log you just pasted?&lt;br&gt;
I treat that move as a review risk now.&lt;/p&gt;

&lt;p&gt;Someone fails a job, hits retry, then asks a model.&lt;br&gt;
The written answer sounds sharp while the cited job sits stale.&lt;/p&gt;

&lt;p&gt;A fluent paragraph is not a fresh job trace.&lt;br&gt;
I want a mental model I can check before I reply.&lt;br&gt;
Have you closed a thread on a job you already left?&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this keeps wasting review time
&lt;/h2&gt;

&lt;p&gt;I look at pipeline pages more often than chat panes.&lt;br&gt;
A later green attempt can sit beside an older red one.&lt;br&gt;
People still quote the older red one in the thread.&lt;/p&gt;

&lt;p&gt;So I collect the claims that survive a retry click.&lt;br&gt;
They sound practical until you compare two job ids.&lt;br&gt;
Have you trusted a note after that id already changed?&lt;/p&gt;
&lt;h2&gt;
  
  
  Five claims I still hear
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;People claim a retry keeps the same job evidence.&lt;/li&gt;
&lt;li&gt;People claim a free server reran the project runner.&lt;/li&gt;
&lt;li&gt;People claim the model already saw the newer trace.&lt;/li&gt;
&lt;li&gt;People claim quick agreement means the cause is found.&lt;/li&gt;
&lt;li&gt;People claim free model access replaces runner minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each claim fails a different field check on the job.&lt;br&gt;
I walk them in that order on purpose.&lt;br&gt;
The id check comes before any story about the cause.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 1: A retry keeps the same evidence
&lt;/h2&gt;

&lt;p&gt;People say a retry is the same job, only cleaned.&lt;br&gt;
I do not treat that sentence as API truth.&lt;br&gt;
A job retry creates a new job record in GitLab.&lt;/p&gt;

&lt;p&gt;The older job remains visible in the pipeline history.&lt;br&gt;
Its trace stays attached to the older job id.&lt;br&gt;
Your new buttons point at a different job id.&lt;/p&gt;

&lt;p&gt;Corrected model: a retry is a new attempt.&lt;br&gt;
The same stage name does not mean the same evidence.&lt;br&gt;
I match ids before I trust any explanatory sentence.&lt;/p&gt;

&lt;p&gt;Confirm this on your own instance docs before you script it.&lt;br&gt;
The Jobs API is the source I trust for those fields.&lt;br&gt;
A blog memory is not a substitute for that page.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 2: A free server reran your runner
&lt;/h2&gt;

&lt;p&gt;This myth appears when a laptop feels too slow.&lt;br&gt;
Someone starts a free server and pastes the failing script.&lt;br&gt;
The script exits zero, so they call the pipeline fixed.&lt;/p&gt;

&lt;p&gt;MonkeyCode is an open source project I use for that desk.&lt;br&gt;
Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;I use its free model access to draft one hypothesis.&lt;br&gt;
I use its free server option only as a scratch desk.&lt;br&gt;
I do not treat that desk as the project runner.&lt;/p&gt;

&lt;p&gt;A free server has your shell, not the runner tags.&lt;br&gt;
It does not hold the project's protected variables.&lt;br&gt;
It does not mint the job token your script expects.&lt;/p&gt;

&lt;p&gt;Corrected model: that box can rehearse a snippet you own.&lt;br&gt;
The runner is still the only place the job ran.&lt;br&gt;
If the desk and the runner disagree, I believe the runner.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 3: The model saw the newer trace
&lt;/h2&gt;

&lt;p&gt;A model answers from the text you chose to paste.&lt;br&gt;
If you pasted the first failure, that text is the world.&lt;br&gt;
A later retry does not reach the model by magic.&lt;/p&gt;

&lt;p&gt;I watch for line numbers that exist only in the old log.&lt;br&gt;
The newer job log often does not contain those lines.&lt;br&gt;
That mismatch is a citation bug, not a deep insight.&lt;/p&gt;

&lt;p&gt;Did you upload the newer trace, or the comforting one?&lt;br&gt;
Corrected model: context is a snapshot you selected.&lt;br&gt;
No new snapshot means no claim about the latest attempt.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 4: Quick agreement means the cause is found
&lt;/h2&gt;

&lt;p&gt;A confident paragraph can feel like a closed root cause.&lt;br&gt;
I treat that tone as a guess with clean grammar.&lt;br&gt;
A model that agrees quickly is not a witness.&lt;/p&gt;

&lt;p&gt;Two causes can share one error string in the log.&lt;br&gt;
A cache miss and a bad lockfile can look alike.&lt;br&gt;
Only the newer job can split those two stories.&lt;/p&gt;

&lt;p&gt;Corrected model: verbal agreement is not real attribution.&lt;br&gt;
I want a prediction I can falsify on the next run.&lt;br&gt;
If that prediction stays vague, I do not merge it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 5: Free model access replaces runner minutes
&lt;/h2&gt;

&lt;p&gt;People talk as if a token pool were a CI budget.&lt;br&gt;
I have not verified any cap, duration, or hardware size.&lt;br&gt;
I will not quote a number I cannot source today.&lt;/p&gt;

&lt;p&gt;Read the current product page before you plan a week.&lt;br&gt;
Caps and clocks move, and docs should beat memory.&lt;br&gt;
My working rule is smaller than any hallway rumor.&lt;/p&gt;

&lt;p&gt;Corrected model: model access buys drafting time only.&lt;br&gt;
Runner minutes buy the evidence a reviewer can open.&lt;br&gt;
Those meters are different, so I do not swap them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Faster notes, same-age evidence
&lt;/h2&gt;

&lt;p&gt;The model can answer faster than I can scroll a log.&lt;br&gt;
The job evidence does not get younger while I read.&lt;br&gt;
Speed in the chat does not refresh a finished_at stamp.&lt;/p&gt;

&lt;p&gt;That is the gap I care about on a review day.&lt;br&gt;
A smoother paragraph can still cite a dead attempt.&lt;br&gt;
I would rather be slow and attached to the current id.&lt;/p&gt;
&lt;h2&gt;
  
  
  The check I keep beside the thread
&lt;/h2&gt;

&lt;p&gt;I do not need a new platform to spot a stale note.&lt;br&gt;
I download two job JSON payloads from the Jobs API.&lt;br&gt;
Then I compare the fields a retry is supposed to change.&lt;/p&gt;

&lt;p&gt;Keep the token outside the chat window and the prompt.&lt;br&gt;
I use a personal access token limited to read_api.&lt;br&gt;
I never paste that token into a model conversation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/v4/projects/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/jobs/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;OLD_JOB_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; old_job.json

curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/v4/projects/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/jobs/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_JOB_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; new_job.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fetch the trace only after the id check passes.&lt;br&gt;
The trace endpoint is a separate download from the job JSON.&lt;br&gt;
I still refuse to drop either file into a shared prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/v4/projects/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/jobs/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NEW_JOB_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/trace"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; new_trace.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script is a proposal, not a benchmark I already ran.&lt;br&gt;
Run it only on JSON you downloaded from your instance.&lt;br&gt;
Do not send private traces where your policy forbids that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Flag a model note that still cites a pre-retry job.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;cited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;stale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ids match; this pair is not a retry split&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cited_sha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;commit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;current_sha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;commit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cited_sha&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;current_sha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;commit sha differs; do not share one story&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cited_pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;current_pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cited_pipe&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;current_pipe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pipeline id differs; widen the search&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;finished&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;canceled&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;finished&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;current job is not finished&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cited_done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;finished_at&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;current_done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;finished_at&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cited_done&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;current_done&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;cited_done&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;current_done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cited finish is not older than current finish&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stale_citation=%s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cited_job=%s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;cited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;current_job=%s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;note=%s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stale&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 check_retry_citation.py old_job.json new_job.json
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want a true stale flag before I discard the older note.&lt;br&gt;
If that flag is false, I still read the trace myself.&lt;br&gt;
Matching ids means I have not moved attempts yet.&lt;/p&gt;

&lt;p&gt;Those sample ids are placeholders, not jobs from a real project.&lt;br&gt;
I include them only so the print shape is obvious.&lt;br&gt;
Do not treat the numbers as evidence from any pipeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stale_citation=true
cited_job=111
current_job=222
note=cited finish is not older than current finish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The clicks I make after the flag
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the cited job JSON with a read-only token.&lt;/li&gt;
&lt;li&gt;Fetch the current job JSON from the same project.&lt;/li&gt;
&lt;li&gt;Run the citation check before you answer the thread.&lt;/li&gt;
&lt;li&gt;Paste a newer trace only after the flag says you left.&lt;/li&gt;
&lt;li&gt;Write one falsifiable prediction, then retry a single job.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  When the citation flag is true
&lt;/h3&gt;

&lt;p&gt;The model note cites a job you have already left.&lt;br&gt;
I keep the note as a hypothesis, not as a verdict.&lt;br&gt;
I open the newer job URL before I answer the thread.&lt;/p&gt;

&lt;h3&gt;
  
  
  When the commit sha also changed
&lt;/h3&gt;

&lt;p&gt;Now I have two builds, not one confusing failure.&lt;br&gt;
A retry on a new commit is a different tree.&lt;br&gt;
I stop mixing those logs inside one review sentence.&lt;/p&gt;

&lt;h3&gt;
  
  
  When the newer job is still running
&lt;/h3&gt;

&lt;p&gt;There is no finished trace worth a confident explanation.&lt;br&gt;
I wait, or I read the live log with my own eyes.&lt;br&gt;
A model cannot finish a job that is still queued.&lt;/p&gt;

&lt;h2&gt;
  
  
  A table I use before I comment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim I hear&lt;/th&gt;
&lt;th&gt;Field I check&lt;/th&gt;
&lt;th&gt;What I do next&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retry kept the same log&lt;/td&gt;
&lt;td&gt;job &lt;code&gt;id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fetch the newer job before commenting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free server proved the fix&lt;/td&gt;
&lt;td&gt;job &lt;code&gt;runner&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Re-run on the real runner tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model saw the latest failure&lt;/td&gt;
&lt;td&gt;trace you pasted&lt;/td&gt;
&lt;td&gt;Paste the newer trace or stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confident cause is enough&lt;/td&gt;
&lt;td&gt;one testable prediction&lt;/td&gt;
&lt;td&gt;Write it, then retry once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token pool covers CI&lt;/td&gt;
&lt;td&gt;nothing in the job JSON&lt;/td&gt;
&lt;td&gt;Keep runner minutes as a separate budget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I use that table in review, not in a slide deck.&lt;br&gt;
One matching row should change my next click.&lt;br&gt;
If no row changes a click, I am collecting trivia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits I refuse to sand away
&lt;/h2&gt;

&lt;p&gt;This script only sees JSON you have already fetched.&lt;br&gt;
It does not open or diff the trace body itself.&lt;br&gt;
Two jobs can share a sha and still fail differently.&lt;/p&gt;

&lt;p&gt;The finished_at field can be null while a job is running.&lt;br&gt;
I treat a null finish as not evidence yet.&lt;br&gt;
Laptop clocks can also drift away from GitLab time.&lt;/p&gt;

&lt;p&gt;Protected logs may arrive redacted on your instance.&lt;br&gt;
A redacted trace can hide the line that matters.&lt;br&gt;
I do not ask a model to invent those missing bytes.&lt;/p&gt;

&lt;p&gt;Self-managed GitLab can lag the docs I happen to remember.&lt;br&gt;
Confirm the Jobs API on your instance before you automate it.&lt;br&gt;
I am describing a local check, not certifying your server.&lt;/p&gt;

&lt;p&gt;Free model access can be limited or withdrawn later.&lt;br&gt;
The free server option is not an uptime promise.&lt;br&gt;
I did not benchmark either option for this draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should skip this habit
&lt;/h2&gt;

&lt;p&gt;Skip this if policy forbids external model tools entirely.&lt;br&gt;
A security review is not a scratch-desk exercise.&lt;br&gt;
Do not upload secrets, token-bearing traces, or customer data.&lt;/p&gt;

&lt;p&gt;Skip this if you need a signed release attestation.&lt;br&gt;
A chat note will not satisfy a careful auditor.&lt;br&gt;
Your release job still has to run inside the real project.&lt;/p&gt;

&lt;p&gt;Skip this if you cannot name the job id yet.&lt;br&gt;
Without an id, you are debating a mood.&lt;br&gt;
I would rather wait for the pipeline page to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do on the next red job
&lt;/h2&gt;

&lt;p&gt;I copy the newer job id into the issue first.&lt;br&gt;
I write one prediction in a single plain sentence.&lt;br&gt;
Then I retry, and I let the newer trace judge it.&lt;/p&gt;

&lt;p&gt;If I want help wording that prediction, I use a free model session.&lt;br&gt;
I run the id check on the free server, away from the runner.&lt;br&gt;
Then I return to GitLab and read the job that finished.&lt;/p&gt;

&lt;p&gt;When I need that scratch desk, I open MonkeyCode first.&lt;br&gt;
Then I leave it and read the job that actually finished.&lt;br&gt;
That sequence is the whole habit I trust.&lt;/p&gt;

&lt;p&gt;Older logs can teach you, but they cannot close a retry.&lt;br&gt;
Which job id is your comment still citing right now?&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>cicd</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>Myth FAQ: A Clone Does Not Register the Runner</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Tue, 13 Oct 2026 03:59:51 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/myth-faq-a-clone-does-not-register-the-runner-26nm</link>
      <guid>https://dev.to/gitlab_3188/myth-faq-a-clone-does-not-register-the-runner-26nm</guid>
      <description>&lt;p&gt;Did your open shell just register a runner for you? I keep seeing that leap in pipeline review notes. The files look familiar, so the leap feels easy.&lt;/p&gt;

&lt;p&gt;A copied tree is not a runner process on GitLab. A prompt that can edit YAML is not registration. I want both distinctions taped above the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this myth keeps returning
&lt;/h2&gt;

&lt;p&gt;You open a remote shell and clone the project. A model then drafts a register sketch beside you. Someone still asks why the jobs sit queued.&lt;/p&gt;

&lt;p&gt;I ask this whenever the only evidence is a transcript. Have you watched that same gap in your own reviews? The missing step is almost never more YAML prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I will not pretend to know
&lt;/h2&gt;

&lt;p&gt;I do not have a lab log for your project. I will not invent quotas, hardware, or uptime numbers. I will not name models I cannot verify here.&lt;/p&gt;

&lt;p&gt;The operator notes I was given say two things only. MonkeyCode has free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;Those notes do not turn a shell into CI. They only name a place where a draft can live. The proof still has to come from GitLab itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claim one: a clone is not registration
&lt;/h2&gt;

&lt;p&gt;Developers repeat this right after a clone succeeds. The shell has the tree, so they expect jobs next. GitLab does not schedule work from a clone alone.&lt;/p&gt;

&lt;p&gt;A runner must be registered before it can poll. That step writes local config after an admin action. Your clone uses your credentials, not that config path.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I accept as evidence
&lt;/h3&gt;

&lt;p&gt;GitLab documents registration as its own runner step. I link public docs later so you can recheck them. I am not quoting a private admin screen here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claim two: YAML tags do not grab a desk
&lt;/h2&gt;

&lt;p&gt;People paste docker tags into the CI file and relax. They think the word docker binds their open shell. Tags match registered runners, not the mood of a prompt.&lt;/p&gt;

&lt;p&gt;Your job waits until some runner advertises that tag. An interactive box does not advertise tags by existing. Have you checked the runner list in the project UI?&lt;/p&gt;

&lt;h2&gt;
  
  
  Claim three: a drafted command has not run
&lt;/h2&gt;

&lt;p&gt;Free model access can draft a command that looks finished. Drafting is not execution on the host you mean. Execution on that host is still not successful registration.&lt;/p&gt;

&lt;p&gt;I want three artifacts before I believe a runner exists. I want a transcript that hides every secret value. I want verify output plus a job log with a runner id.&lt;/p&gt;

&lt;p&gt;If any artifact is missing, I still have only a story. Stories do not pick up jobs that remain queued. Do you still approve the merge on the story alone?&lt;/p&gt;

&lt;h2&gt;
  
  
  Claim four: a free desk is not shared capacity
&lt;/h2&gt;

&lt;p&gt;A free server option can give you an interactive host. That host is a desk, not a fleet membership card. I will not claim how long that desk stays available.&lt;/p&gt;

&lt;p&gt;Shared runners are configured by an admin on the instance. Your open shell stays absent until someone registers it. Should a coding desk inherit production job traffic at all?&lt;/p&gt;

&lt;p&gt;I would not send protected jobs to an unowned desk. Protected variables follow separate rules from a plain shell. Read the current variable docs before you assume any exposure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claim five: docker on the host is not an executor
&lt;/h2&gt;

&lt;p&gt;This claim shows up when the desk can run containers. People see a docker binary and assume the executor type. Executor type is config, not a package that happens to exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the config decides
&lt;/h3&gt;

&lt;p&gt;A shell executor can live on a machine that also has docker. The image key applies only to executors that honor images. Does your config name shell, docker, or something you never checked?&lt;/p&gt;

&lt;h2&gt;
  
  
  Claim six: job tokens do not follow the repo
&lt;/h2&gt;

&lt;p&gt;A job token exists inside a running job only. It does not follow you into a random SSH session. If the probe prints absent, you are outside that job.&lt;/p&gt;

&lt;p&gt;People paste tokens into chat so a model can finish. Please do not hand registration secrets to any transcript. A registration secret stays secret, even inside a helpful draft.&lt;/p&gt;

&lt;p&gt;Token field names also change across GitLab versions. I will not freeze one register flag set in this FAQ. Check the registration page that matches your instance version.&lt;/p&gt;

&lt;h2&gt;
  
  
  A corrected mental model
&lt;/h2&gt;

&lt;p&gt;Hold three objects apart when a pipeline claim arrives. The editor host is the place where you type commands. The runner process is what polls GitLab for new work.&lt;/p&gt;

&lt;p&gt;The job process is what exports the predefined CI variables. Free model access sits with the editor, not with the runner. It can propose YAML, a probe, or a register sketch.&lt;/p&gt;

&lt;p&gt;Prose cannot mint a runner id on the GitLab side. A free server option remains that editor host until registration. Remove the product name and this same split still holds.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You still have a host, a runner, and a job process.&lt;/li&gt;
&lt;li&gt;Those three doors still need three different keys today.&lt;/li&gt;
&lt;li&gt;Mixing those keys is how this myth survives a review.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Probe you can copy
&lt;/h2&gt;

&lt;p&gt;The next script is a proposal, not a result I collected. Run it yourself on the host you actually control. Do not print secret values into shared output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Proposed identity probe. Not an executed lab result.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"section=identity"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_JOB_ID&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ci_job_id=present"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ci_job_id=absent"&lt;/span&gt;
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_RUNNER_ID&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ci_runner_id=present"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ci_runner_id=absent"&lt;/span&gt;
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_PIPELINE_ID&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ci_pipeline_id=present"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ci_pipeline_id=absent"&lt;/span&gt;
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_JOB_TOKEN&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ci_job_token=present"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ci_job_token=absent"&lt;/span&gt;
&lt;span class="k"&gt;fi

if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; gitlab-runner &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"gitlab_runner_binary=present"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"gitlab_runner_binary=absent"&lt;/span&gt;
&lt;span class="k"&gt;fi

if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; docker &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"docker_binary=present"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"docker_binary=absent"&lt;/span&gt;
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; /etc/gitlab-runner/config.toml &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"runner_config_readable=yes"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"runner_config_readable=no"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How I read the bundle
&lt;/h3&gt;

&lt;p&gt;Read those printed lines as one bundle, not isolated wins. Absent job variables mean you are standing outside a job. A missing binary means that host is not polling yet.&lt;/p&gt;

&lt;p&gt;A missing config means registration has not landed there. It can also mean you cannot read the config path. Either reading blocks the claim that this runner is live.&lt;/p&gt;

&lt;p&gt;Docker present beside runner absent is a useful split. The package can exist while no executor is configured. Do not let the container tool close that gap for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table before I trust a desk
&lt;/h2&gt;

&lt;p&gt;I use this table before I trust an open coding desk. The right column is the gate I refuse to skip. The left column is how the myth usually enters chat.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim in chat&lt;/th&gt;
&lt;th&gt;Smallest honest check&lt;/th&gt;
&lt;th&gt;Still not enough&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The shell cloned the repo&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git rev-parse --short HEAD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No runner id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags appear in the YAML&lt;/td&gt;
&lt;td&gt;Runner UI shows those tags&lt;/td&gt;
&lt;td&gt;No online match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A model wrote a register sketch&lt;/td&gt;
&lt;td&gt;Diff of that sketch only&lt;/td&gt;
&lt;td&gt;No verify output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A runner binary exists&lt;/td&gt;
&lt;td&gt;&lt;code&gt;command -v gitlab-runner&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No readable config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker exists on the host&lt;/td&gt;
&lt;td&gt;&lt;code&gt;command -v docker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Executor still unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify names a configured runner&lt;/td&gt;
&lt;td&gt;Redacted verify output&lt;/td&gt;
&lt;td&gt;No picked-up job log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A job log shows a runner id&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CI_RUNNER_ID&lt;/code&gt; present in that log&lt;/td&gt;
&lt;td&gt;Not proof for other tags&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The middle column is only the smallest honest check. Pass that check, then still demand the right-hand column. A partial row is not a registered runner yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commands that separate a story from a runner
&lt;/h2&gt;

&lt;p&gt;Ask the host what it is before you grant it trust. These commands are examples for a shell you control. Skip any path your security policy forbids outright.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; gitlab-runner &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"binary_absent"&lt;/span&gt;
gitlab-runner &lt;span class="nt"&gt;--version&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;gitlab-runner verify &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;gitlab-runner list &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List and verify only read local runner configuration. They do not create a runner by themselves. If those commands fail, the myth remains only a myth.&lt;/p&gt;

&lt;p&gt;Inside a real job, print presence instead of token text. The snippet below is an example, not a pipeline I ran. It proves nothing until some registered runner picks it up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;probe_runner_identity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test -n "$CI_JOB_ID" &amp;amp;&amp;amp; echo "ci_job_id=present"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test -n "$CI_RUNNER_ID" &amp;amp;&amp;amp; echo "ci_runner_id=present"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test -n "$CI_RUNNER_TAGS" &amp;amp;&amp;amp; echo "ci_runner_tags=present"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test -n "$CI_RUNNER_EXECUTABLE_ARCH" &amp;amp;&amp;amp; echo "runner_arch=present"&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_PIPELINE_SOURCE == "web"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A queued pipeline is a waiting state, not a passing job. Did your last success come from this job or a typed shell? I want that answer written beside the two transcripts.&lt;/p&gt;

&lt;p&gt;The web source rule keeps this sample off ordinary pushes. Older instances may omit some of those runner variables. Compare the predefined variable list for the version you run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What those free options can honestly do
&lt;/h2&gt;

&lt;p&gt;Use free model access to draft the probe and the snippet. Use a free server option as the interactive host only. Stop once the draft exists and identity is still unproven.&lt;/p&gt;

&lt;p&gt;The model should not receive registration secrets or job tokens. Do not describe that server as shared runner capacity. I have no basis for capacity, region, lifetime, or speed claims.&lt;/p&gt;

&lt;p&gt;If a generated command includes a raw secret, delete that draft. Rotate the secret if it already left your password manager. A helpful completion can still be a credential leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should not use this desk pattern
&lt;/h2&gt;

&lt;p&gt;Skip this pattern when you need audited production runners. Skip it on protected release branches and compliance gates. Skip it whenever policy forbids hosts you do not administer.&lt;/p&gt;

&lt;p&gt;Also skip it if you came looking for a speed benchmark. This FAQ covers runner identity, not model latency numbers. I am not publishing timings, hardware notes, or quota math.&lt;/p&gt;

&lt;p&gt;Admins who already operate a fleet can ignore this myth list. They need their current config review, not another coding desk. I am writing for people who confuse a desk with that fleet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of this probe
&lt;/h2&gt;

&lt;p&gt;The probe cannot read group runner settings it cannot access. It cannot prove tag policy stored only on the GitLab side. It cannot prove that a paused runner will accept work later.&lt;/p&gt;

&lt;p&gt;Your config file may live on a different install path. A readable file is not the same thing as a healthy runner. Verify can fail because the stored credential already expired.&lt;/p&gt;

&lt;p&gt;Documentation pages move, so recheck the runner guide first. I am describing stable roles, not a dated changelog entry. If your instance is old, compare that version's admin docs too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions I ask before I close the review
&lt;/h2&gt;

&lt;p&gt;I ask these questions before I close this kind of review. A missing answer means the desk is still only a desk. Can you answer each one without pasting a secret?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Where is that runner registered: project, group, or instance?&lt;/li&gt;
&lt;li&gt;Which tags does that runner advertise in the UI today?&lt;/li&gt;
&lt;li&gt;Does the job log show a runner id, or only a prompt?&lt;/li&gt;
&lt;li&gt;Who can read the runner config file on that host?&lt;/li&gt;
&lt;li&gt;Did any secret enter a model transcript during this change?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you cannot answer those five, do not call it registered. Ask for the missing artifact instead of another YAML rewrite. A short refusal is cheaper than a release left queued.&lt;/p&gt;

&lt;h2&gt;
  
  
  One comparison worth keeping
&lt;/h2&gt;

&lt;p&gt;If you already have MonkeyCode's free server option, start on that host. Run the shell probe there before you trust the generated sketch. Then place that output next to a log from a runner you registered.&lt;/p&gt;

&lt;p&gt;Keep both transcripts inside the same review comment. That comparison is the only next step I want here. I am not offering uptime numbers, queue times, or extra certainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources to recheck before a ticket
&lt;/h2&gt;

&lt;p&gt;I am linking public GitLab pages for the roles named above. Open each page before you paste a conclusion into a ticket. If a page has moved, follow the current registration guide.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ci/runners/" rel="noopener noreferrer"&gt;GitLab CI runners&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/runner/register/" rel="noopener noreferrer"&gt;Registering runners&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ci/variables/predefined_variables/" rel="noopener noreferrer"&gt;Predefined CI/CD variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ci/variables/" rel="noopener noreferrer"&gt;CI/CD variables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These links point at public documentation, not at your instance version. Self-managed menus can lag or rename a token field. Treat the live admin docs as the final word on flags.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>devops</category>
      <category>cicd</category>
      <category>testing</category>
    </item>
    <item>
      <title>FAQ: A Free Shell Cannot Borrow Your Runner Tag</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Mon, 12 Oct 2026 19:46:27 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/faq-a-free-shell-cannot-borrow-your-runner-tag-1d2c</link>
      <guid>https://dev.to/gitlab_3188/faq-a-free-shell-cannot-borrow-your-runner-tag-1d2c</guid>
      <description>&lt;p&gt;I keep hearing one claim in GitLab review threads.&lt;br&gt;
A free server cloned the repo, so it counts as a runner.&lt;br&gt;
Is a shell with git really a runner?&lt;br&gt;
I do not think that claim holds up.&lt;/p&gt;

&lt;p&gt;A runner is a process GitLab already knows.&lt;br&gt;
It has an id, tags, and a job history.&lt;br&gt;
A side shell has a checkout and a prompt.&lt;br&gt;
Those two objects are not the same thing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this FAQ exists
&lt;/h2&gt;

&lt;p&gt;I am not posting a speed benchmark here.&lt;br&gt;
I am not naming a chip, a quota, or a timer.&lt;br&gt;
I want a corrected mental model you can test.&lt;/p&gt;

&lt;p&gt;You run the checks on your own project.&lt;br&gt;
Then you keep the job trace, not my opinion.&lt;br&gt;
Five claims show up when someone demos a side server.&lt;/p&gt;

&lt;p&gt;Each claim sounds practical until you ask for ids.&lt;br&gt;
I answer each one with a check, not a vibe.&lt;br&gt;
Confirm the linked docs on the day you try them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 1: A clone is registration
&lt;/h2&gt;

&lt;p&gt;People say the repo is on the box, so it is a runner.&lt;br&gt;
GitLab lists runners only after you create one.&lt;br&gt;
A git clone never adds a row to that list.&lt;/p&gt;

&lt;p&gt;The runner process must register and stay reachable.&lt;br&gt;
The checkout is just files on a disk.&lt;br&gt;
Open Settings, then CI/CD, then the Runners page.&lt;/p&gt;

&lt;p&gt;If that host is missing, it is not your runner.&lt;br&gt;
Docker on the box does not close this gap.&lt;br&gt;
An executor binary is not a registration record.&lt;/p&gt;

&lt;p&gt;Read the runner guide before you trust a demo.&lt;br&gt;
The current page is &lt;a href="https://docs.gitlab.com/ci/runners/" rel="noopener noreferrer"&gt;GitLab runners&lt;/a&gt;.&lt;br&gt;
Use your version's create-runner flow, not an old snippet.&lt;/p&gt;

&lt;p&gt;You can also ask the API with your own read token.&lt;br&gt;
Use it only on a project you maintain.&lt;br&gt;
Do not paste that token into a chat log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposal: read-only. Run on your machine. I did not run this for you.&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/v4/projects/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/runners"&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; gitlab-runner &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'gitlab-runner binary absent\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty array means no project runner is registered.&lt;br&gt;
A free shell hostname appears only after registration.&lt;br&gt;
That missing row is the myth in one check.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 2: YAML tags grab any host
&lt;/h2&gt;

&lt;p&gt;People say YAML tags bind whatever host they SSH into.&lt;br&gt;
Tags match a job to runners you already registered.&lt;br&gt;
An unknown host is not in that match set.&lt;/p&gt;

&lt;p&gt;GitLab leaves the job pending when nothing fits.&lt;br&gt;
Job tags select registered runners, not random servers.&lt;br&gt;
A shared shell prompt is not a tag match.&lt;/p&gt;

&lt;p&gt;Here is a probe job I would put on a branch.&lt;br&gt;
It is a proposal, not a pipeline result.&lt;br&gt;
I have not executed it for this article.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Proposal only. Commit it on a throwaway branch if you choose.&lt;/span&gt;
&lt;span class="na"&gt;runner_identity_probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;probe-only&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test -n "${CI_RUNNER_ID}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test -n "${CI_JOB_ID}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test -n "${CI_JOB_URL}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;printf 'runner=%s job=%s\n' "${CI_RUNNER_ID}" "${CI_JOB_ID}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;printf 'trace=%s\n' "${CI_JOB_URL}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no runner has the tag, the job stays pending.&lt;br&gt;
A free server can still run those lines by hand.&lt;br&gt;
That manual run never creates a job id.&lt;/p&gt;

&lt;p&gt;A pending job in GitLab is the honest signal.&lt;br&gt;
Read the &lt;a href="https://docs.gitlab.com/ci/yaml/#tags" rel="noopener noreferrer"&gt;tags reference&lt;/a&gt; before you loosen rules.&lt;br&gt;
Untagged runners are a project choice, not a default gift.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 3: Exported names mint a job
&lt;/h2&gt;

&lt;p&gt;People export &lt;code&gt;CI=true&lt;/code&gt; and expect a job token.&lt;br&gt;
The job token is injected into a running job.&lt;br&gt;
The job id and runner id arrive the same way.&lt;/p&gt;

&lt;p&gt;A hand export can print names without issuing secrets.&lt;br&gt;
Job identity is issued by GitLab for that run.&lt;br&gt;
You cannot type a trace into the environment.&lt;/p&gt;

&lt;p&gt;Run the loop on the side shell before you trust it.&lt;br&gt;
Expect those job fields to be absent there.&lt;br&gt;
Do not print secret values into tickets or chat.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposal: side-shell identity probe. Absent means this is not a job.&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;name &lt;span class="k"&gt;in &lt;/span&gt;CI_JOB_ID CI_JOB_URL CI_JOB_TOKEN CI_RUNNER_ID CI_RUNNER_TAGS CI_PIPELINE_ID&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if &lt;/span&gt;&lt;span class="nb"&gt;printenv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s=present\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s=absent\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A present name without a job URL is not enough.&lt;br&gt;
Read the &lt;a href="https://docs.gitlab.com/ci/jobs/ci_job_token/" rel="noopener noreferrer"&gt;job token page&lt;/a&gt; before you reuse one.&lt;br&gt;
Scope on that page is not your laptop environment.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 4: A maintainer shell sees protected values
&lt;/h2&gt;

&lt;p&gt;People say maintainers may read protected variables anywhere.&lt;br&gt;
Protected variables go to pipelines on protected refs.&lt;br&gt;
GitLab injects them into that job's environment.&lt;/p&gt;

&lt;p&gt;Your SSH session is not that protected pipeline.&lt;br&gt;
Your role in the UI is not an injection path.&lt;br&gt;
Protection follows the pipeline, not your other login.&lt;/p&gt;

&lt;p&gt;Confirm the current page before you depend on a toggle.&lt;br&gt;
I will not guess extra switches from memory.&lt;br&gt;
Open &lt;a href="https://docs.gitlab.com/ci/variables/#protect-a-cicd-variable" rel="noopener noreferrer"&gt;protect a CI/CD variable&lt;/a&gt; on test day.&lt;/p&gt;

&lt;p&gt;If that page adds a runner condition, follow the page.&lt;br&gt;
Do not haul protected values out to a scratch host.&lt;br&gt;
A demo that needs them is already on the wrong machine.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 5: The side box can mint the job JWT
&lt;/h2&gt;

&lt;p&gt;People say cloud login on the free server matches the job.&lt;br&gt;
ID tokens are minted for a job by GitLab.&lt;br&gt;
The cloud trusts GitLab's issuer and those claims.&lt;/p&gt;

&lt;p&gt;An outside process does not hold the signing key.&lt;br&gt;
Federation trusts the job token, not a lookalike.&lt;br&gt;
If the token variable is missing, you are outside the job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Proposal only. GitLab mints this inside a real job. Never echo the token.&lt;/span&gt;
&lt;span class="na"&gt;deploy_oidc_probe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;id_tokens&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;GITLAB_OIDC_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;aud&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://example.invalid&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test -n "${GITLAB_OIDC_TOKEN}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;printf 'oidc_token=present\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never print that token into logs or chat.&lt;br&gt;
Read &lt;a href="https://docs.gitlab.com/ci/secrets/id_token_authentication/" rel="noopener noreferrer"&gt;ID token authentication&lt;/a&gt; before you copy a sample.&lt;br&gt;
A fake JWT from a script will fail issuer checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 6: A zero exit is a green pipeline
&lt;/h2&gt;

&lt;p&gt;People say a zero exit means the pipeline is green.&lt;br&gt;
A job trace carries a job URL and a runner id.&lt;br&gt;
It also carries section timing from the runner.&lt;/p&gt;

&lt;p&gt;Your shell history has an exit code and a clock.&lt;br&gt;
Those two records do not point at each other.&lt;br&gt;
Local success is a rehearsal, not pipeline status.&lt;/p&gt;

&lt;p&gt;Compare fields, not feelings, before you approve.&lt;br&gt;
Names below are the usual predefined set.&lt;br&gt;
Confirm them on the &lt;a href="https://docs.gitlab.com/ci/variables/predefined_variables/" rel="noopener noreferrer"&gt;predefined variables page&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Inside a real job&lt;/th&gt;
&lt;th&gt;On a free shell&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CI_JOB_ID&lt;/td&gt;
&lt;td&gt;Numeric id from GitLab&lt;/td&gt;
&lt;td&gt;Absent unless you fake it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI_JOB_URL&lt;/td&gt;
&lt;td&gt;Link that opens the trace&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI_RUNNER_ID&lt;/td&gt;
&lt;td&gt;Id of the runner that picked the job&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI_RUNNER_TAGS&lt;/td&gt;
&lt;td&gt;Tags of that runner&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI_PIPELINE_ID&lt;/td&gt;
&lt;td&gt;Id of that pipeline&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exit code&lt;/td&gt;
&lt;td&gt;Result of the job script&lt;/td&gt;
&lt;td&gt;Your local process only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A faked export can fill names from the left column.&lt;br&gt;
It still will not create a job URL that opens.&lt;br&gt;
Open that URL, or you do not have a job trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a draft shell still helps
&lt;/h2&gt;

&lt;p&gt;I still want a scratch place to edit the probe.&lt;br&gt;
I do not want that draft to pretend it is CI.&lt;br&gt;
MonkeyCode offers free model access and a free server option.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;br&gt;
That pair is enough to edit YAML and run the loop.&lt;br&gt;
It is not enough to wear your runner tag.&lt;/p&gt;

&lt;p&gt;Draft there if you want, then prove the runner id.&lt;br&gt;
Stop when the check needs secrets, deploys, or a release tag.&lt;br&gt;
A useful draft still ends at the runner list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table I keep beside the config
&lt;/h2&gt;

&lt;p&gt;I keep this table beside the pipeline config.&lt;br&gt;
Use it when a demo skips the runner list.&lt;br&gt;
Each row is a claim, a check, and a model.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim I hear&lt;/th&gt;
&lt;th&gt;Check I run&lt;/th&gt;
&lt;th&gt;Corrected model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clone means runner&lt;/td&gt;
&lt;td&gt;Project runner list or runners API&lt;/td&gt;
&lt;td&gt;Registration creates the runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tags bind my SSH host&lt;/td&gt;
&lt;td&gt;Job pending without a tag match&lt;/td&gt;
&lt;td&gt;Tags select registered runners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;export CI mints a token&lt;/td&gt;
&lt;td&gt;printenv on job fields&lt;/td&gt;
&lt;td&gt;GitLab issues job identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintainer may read protected vars&lt;/td&gt;
&lt;td&gt;Ask where injection happened&lt;/td&gt;
&lt;td&gt;Protection follows the pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Side box signs the cloud JWT&lt;/td&gt;
&lt;td&gt;Look for a real id token in the job&lt;/td&gt;
&lt;td&gt;The issuer is GitLab, not the shell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero exit means green pipeline&lt;/td&gt;
&lt;td&gt;Open CI_JOB_URL&lt;/td&gt;
&lt;td&gt;Status lives in GitLab&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Who should not use this split
&lt;/h2&gt;

&lt;p&gt;Skip this split when the risk sits in the draft.&lt;br&gt;
These cases should stay on a trusted runner.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your policy forbids third-party compute for repository files.&lt;/li&gt;
&lt;li&gt;The branch holds production secrets or customer data.&lt;/li&gt;
&lt;li&gt;You need an audit trail for the draft step itself.&lt;/li&gt;
&lt;li&gt;You are about to deploy, tag, or rotate keys.&lt;/li&gt;
&lt;li&gt;You cannot separate a rehearsal log from a job trace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep those secrets inside GitLab's protected variable path.&lt;br&gt;
Do not paste them into a side prompt to save time.&lt;br&gt;
If the draft host is untrusted, do not clone the repo there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits I will not smooth over
&lt;/h2&gt;

&lt;p&gt;This article is a method, not a lab report.&lt;br&gt;
I did not execute the probe on your project.&lt;br&gt;
I did not measure speed, cost, or uptime.&lt;/p&gt;

&lt;p&gt;Free model access and the free server can change.&lt;br&gt;
I am not claiming they are permanent or unlimited.&lt;br&gt;
A model can misread rules, needs, or tags.&lt;/p&gt;

&lt;p&gt;The runner creation steps differ by GitLab version.&lt;br&gt;
Use the current UI, not an old token snippet.&lt;br&gt;
Docs pages move, so open the links on test day.&lt;/p&gt;

&lt;p&gt;A passed probe does not grant registry or deploy rights.&lt;br&gt;
Use GitLab's predefined variables page as the name list.&lt;br&gt;
Cache, images, and service accounts are separate checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model I want in review
&lt;/h2&gt;

&lt;p&gt;Hold three layers apart when someone says CI passed.&lt;br&gt;
Ask which layer wrote the log in front of you.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The draft shell can edit files and run local checks.&lt;/li&gt;
&lt;li&gt;The registered runner can pick up a tagged job.&lt;/li&gt;
&lt;li&gt;The pipeline record can show the job and runner ids.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Which layer produced the log you are reading?&lt;br&gt;
If the answer is the draft shell, you have a rehearsal.&lt;br&gt;
If the answer is a job URL, you have a trace.&lt;/p&gt;

&lt;p&gt;I would keep this checklist in the repo as a proposal.&lt;br&gt;
I would update it when the runner docs change.&lt;br&gt;
I would not let a green prompt skip the runner list.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>cicd</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>FAQ: A Green Free Shell Is Not a Retry Rule</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:00:17 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/faq-a-green-free-shell-is-not-a-retry-rule-4ac7</link>
      <guid>https://dev.to/gitlab_3188/faq-a-green-free-shell-is-not-a-retry-rule-4ac7</guid>
      <description>&lt;p&gt;Did one free shell pass, so you widened &lt;code&gt;retry&lt;/code&gt;?&lt;br&gt;
I keep that question on a worksheet before any edit.&lt;br&gt;
A green shell is a clue, not a finished rule.&lt;/p&gt;
&lt;h2&gt;
  
  
  The debate I am trying to slow down
&lt;/h2&gt;

&lt;p&gt;Retry talk gets sloppy after a cheap reproduction.&lt;br&gt;
Someone pastes a later pass and calls the flake gone.&lt;br&gt;
I want a class for the failure before YAML changes.&lt;/p&gt;

&lt;p&gt;I am not publishing a fleet study or a private metric.&lt;br&gt;
I am handing you a worksheet you can run on one log.&lt;br&gt;
Treat every result as local notes, not as merge approval.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth one: a later pass means it was a blip
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The thin story
&lt;/h3&gt;

&lt;p&gt;People treat one later pass as proof of a blip.&lt;br&gt;
That story skips the failure class, by rush or by habit.&lt;br&gt;
A blip, a dirty cache, and a bad script can all recover.&lt;/p&gt;

&lt;p&gt;What would actually support a transient platform class?&lt;br&gt;
You need the first failure text, not only the second pass.&lt;br&gt;
You also need the same image, script, and inputs.&lt;/p&gt;
&lt;h3&gt;
  
  
  What I require instead
&lt;/h3&gt;

&lt;p&gt;Would you widen &lt;code&gt;retry&lt;/code&gt; from a story that thin?&lt;br&gt;
I would not, and the worksheet exists to block me.&lt;br&gt;
Corrected model: classify the miss, then pick a &lt;code&gt;when&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;A pass without a class is only a second sample.&lt;br&gt;
Two samples still do not author a policy commit.&lt;br&gt;
If the class is unknown, the edit waits for quotes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth two: the free shell is the runner
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Questions the shell cannot answer
&lt;/h3&gt;

&lt;p&gt;A free interactive box can feel like the job.&lt;br&gt;
You see the same repo, commands, and green text.&lt;br&gt;
The runner still owns image, tags, cache, and secrets.&lt;/p&gt;

&lt;p&gt;Did you pin the digest the failed job actually pulled?&lt;br&gt;
Did your shell add tools the CI image never had?&lt;br&gt;
Did the cache key differ between those two attempts?&lt;/p&gt;

&lt;p&gt;I do not assume hardware, region, duration, or permanence.&lt;br&gt;
Those facts move, and this FAQ will not freeze them.&lt;br&gt;
Parity is a checklist you can fail, not a mood.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth three: the model should choose &lt;code&gt;retry.when&lt;/code&gt;
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Draft label, not a commit
&lt;/h3&gt;

&lt;p&gt;A model can read a log and sound completely sure.&lt;br&gt;
Sure text in a chat is not a policy commit.&lt;br&gt;
A wide retry can hide a script bug for days.&lt;/p&gt;

&lt;p&gt;Have you watched a summary agree with your hunch?&lt;br&gt;
I treat that agreement as a draft, then I discard it.&lt;br&gt;
Agreement is cheap when a bad &lt;code&gt;when&lt;/code&gt; list is not.&lt;/p&gt;

&lt;p&gt;I use model help only to draft a bucket label.&lt;br&gt;
I still map that draft onto documented &lt;code&gt;when&lt;/code&gt; values.&lt;br&gt;
If your version docs disagree, the docs win the edit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth four: cheap exploration means cheap retries
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Two different bills
&lt;/h3&gt;

&lt;p&gt;A free shell makes a second run feel almost free.&lt;br&gt;
That feeling mixes exploration cost with production damage.&lt;br&gt;
They are different bills, paid by different people.&lt;/p&gt;

&lt;p&gt;A wrong retry spends shared runner time and reviewer trust.&lt;br&gt;
It can also mask a step that is not idempotent.&lt;br&gt;
Cheap exploring does not make a guessed rule safe.&lt;/p&gt;

&lt;p&gt;Would you retry a migrate because the shell felt cheap?&lt;br&gt;
I hope the answer is no, and the table agrees.&lt;br&gt;
Cost of curiosity is not cost of a bad merge.&lt;/p&gt;
&lt;h2&gt;
  
  
  Four buckets before anyone touches YAML
&lt;/h2&gt;

&lt;p&gt;I split one failed job into four buckets first.&lt;br&gt;
This table is a proposal, not a measured study.&lt;br&gt;
Fill one row from a real log, then stop writing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;th&gt;Evidence required&lt;/th&gt;
&lt;th&gt;Retry stance&lt;/th&gt;
&lt;th&gt;Do not claim&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transient platform&lt;/td&gt;
&lt;td&gt;Runner system text, timeout, or API failure lines&lt;/td&gt;
&lt;td&gt;Narrow &lt;code&gt;when&lt;/code&gt; list only&lt;/td&gt;
&lt;td&gt;That every timeout is safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dirty cache&lt;/td&gt;
&lt;td&gt;Outcome changes when the cache key is cleaned&lt;/td&gt;
&lt;td&gt;Fix the key first&lt;/td&gt;
&lt;td&gt;That retry heals a bad key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script defect&lt;/td&gt;
&lt;td&gt;Same miss on a clean shell and in CI&lt;/td&gt;
&lt;td&gt;Do not add retry&lt;/td&gt;
&lt;td&gt;That a later pass deletes it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Missing job id, image line, or command exit&lt;/td&gt;
&lt;td&gt;No policy change&lt;/td&gt;
&lt;td&gt;That a summary closed the gap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Commands that collect facts, not vibes
&lt;/h2&gt;

&lt;p&gt;Run these in a clone, not inside a generated summary.&lt;br&gt;
They print local git facts and a retry search.&lt;br&gt;
They do not call an API, and they need no token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git rev-parse &lt;span class="nt"&gt;--short&lt;/span&gt; HEAD
git status &lt;span class="nt"&gt;--porcelain&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; .gitlab-ci.yml &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ci file present"&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"retry:"&lt;/span&gt; .gitlab-ci.yml &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no retry key found"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the failed job log outside the chat window.&lt;br&gt;
Then point a small search at that file only.&lt;br&gt;
If the search is empty, you are stuck in unknown.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;JOB_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?pass&lt;span class="p"&gt; a saved job log&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s2"&gt;"ERROR:|exit code|timed out|system failure"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JOB_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 40
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s2"&gt;"Using docker image|Pulling docker image"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JOB_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Silence from either search is not a clean bill.&lt;br&gt;
Silence means the bucket stays unknown for this change.&lt;br&gt;
Unknown means you do not edit retry in that request.&lt;/p&gt;
&lt;h2&gt;
  
  
  A cache check that retry loves to skip
&lt;/h2&gt;

&lt;p&gt;People blame the runner when the cache key is the bug.&lt;br&gt;
I want that possibility visible before I blame the platform.&lt;br&gt;
A retry will happily replay a poisoned cache for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s2"&gt;"cache:|key:|paths:"&lt;/span&gt; .gitlab-ci.yml
git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; .gitlab-ci.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask whether the key includes the lockfile that changed.&lt;br&gt;
Ask whether two jobs share a key but not paths.&lt;br&gt;
If you cannot answer, mark dirty cache or unknown.&lt;/p&gt;
&lt;h2&gt;
  
  
  A heuristic script you must not trust blindly
&lt;/h2&gt;

&lt;p&gt;I also keep a tiny classifier next to the worksheet.&lt;br&gt;
It is an unexecuted proposal with obvious over-match risk.&lt;br&gt;
It must never open a merge request by itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Proposal only. Heuristic. Do not pipe this into git commit.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;
&lt;span class="nv"&gt;log&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?pass&lt;span class="p"&gt; a saved job log&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"system failure|stuck_or_timeout|API failure|execution took longer"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"bucket=transient_platform_candidate"&lt;/span&gt;
&lt;span class="k"&gt;elif &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"script failure|exit code 1"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"bucket=script_defect_candidate"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"bucket=unknown"&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"next=quote lines and confirm against current GitLab docs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why does the script print candidate instead of verdict?&lt;br&gt;
Because a keyword can appear in a red herring line.&lt;br&gt;
You still owe a human quote before any YAML edit.&lt;/p&gt;

&lt;p&gt;The first matching branch wins, even when both patterns hit.&lt;br&gt;
That is why the label says candidate, not verdict.&lt;br&gt;
A timeout line plus a script error still needs your eyes.&lt;/p&gt;

&lt;p&gt;Feed it a synthetic fixture before you trust the shape.&lt;br&gt;
This fixture is fake text I wrote for the demo.&lt;br&gt;
It is not a log from a real pipeline run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: Job failed (system failure): execution took longer than expected
Using docker image sha256:example-not-a-real-digest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output is a candidate line, plus the next hint.&lt;br&gt;
If your real log prints unknown, believe that unknown.&lt;br&gt;
Do not massage the regex until it flatters your plan.&lt;/p&gt;
&lt;h2&gt;
  
  
  A YAML shape you still have to verify
&lt;/h2&gt;

&lt;p&gt;This fragment is an unexecuted example for discussion.&lt;br&gt;
Copy the &lt;code&gt;when&lt;/code&gt; names only after your docs confirm them.&lt;br&gt;
I am not claiming this list stays complete forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;lint_job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sh ci/lint.sh&lt;/span&gt;
  &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;runner_system_failure&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;stuck_or_timeout_failure&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api_failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why is &lt;code&gt;script_failure&lt;/code&gt; missing from that &lt;code&gt;when&lt;/code&gt; list?&lt;br&gt;
A script failure is often your bug, not the platform.&lt;br&gt;
Retrying it can turn a fast red into a slow lie.&lt;/p&gt;

&lt;p&gt;Confirm the allowed max for your version before you copy two.&lt;br&gt;
Some versions cap retries below what a chat will suggest.&lt;br&gt;
A copied max that the linter rejects is not a clever rule.&lt;/p&gt;

&lt;p&gt;Set the image to the digest from the failed job.&lt;br&gt;
A floating tag can change between the fail and the pass.&lt;br&gt;
If you cannot pin it, stay in the unknown bucket.&lt;/p&gt;

&lt;p&gt;Read the current retry keyword before you copy names.&lt;br&gt;
Start from the GitLab CI YAML reference, then search retry.&lt;br&gt;
Your version page beats this snippet if they disagree.&lt;/p&gt;

&lt;p&gt;Check the primary YAML reference before you trust my names.&lt;br&gt;
I linked the docs root so a moved anchor cannot trick you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.gitlab.com/ci/yaml/" rel="noopener noreferrer"&gt;https://docs.gitlab.com/ci/yaml/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Where free model access and a free server fit
&lt;/h2&gt;

&lt;p&gt;Here is the only role I give MonkeyCode in this worksheet.&lt;br&gt;
Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;br&gt;
Use its free server to rerun the failing command cleanly.&lt;/p&gt;

&lt;p&gt;Use free model access to draft a bucket from the saved log.&lt;br&gt;
I am not naming models, quotas, hardware, or time limits.&lt;br&gt;
If your account screen disagrees, trust that screen over me.&lt;/p&gt;

&lt;p&gt;The model may misread a wrapped line as a platform fault.&lt;br&gt;
The free server may differ in tools, architecture, and clock.&lt;br&gt;
Neither gap is fixed by a confident paragraph in chat.&lt;/p&gt;

&lt;p&gt;So I require three saved facts before any YAML edit.&lt;br&gt;
Keep the job id, the image line, and the exit.&lt;br&gt;
If one of those three is missing, stay unknown.&lt;/p&gt;

&lt;p&gt;Use this request beside the log, and strip secrets first.&lt;br&gt;
A token in the prompt is a leak, not context.&lt;br&gt;
I stop the draft if redaction is still incomplete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Classify this saved job log into one bucket only.
Choose transient platform, dirty cache, script defect, or unknown.
Quote the lines you used for that bucket label.
Do not suggest a retry max or a when list.
Do not invent a missing image, job id, or exit code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That request refuses a policy recommendation on purpose.&lt;br&gt;
I want a clerk for quotes, not a release manager.&lt;br&gt;
If the draft bucket says unknown, I stop the edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this worksheet cannot prove
&lt;/h2&gt;

&lt;p&gt;It cannot prove parity across runner tags or executors.&lt;br&gt;
It cannot prove a job stays safe under concurrency.&lt;br&gt;
It cannot prove a free offer remains available later.&lt;/p&gt;

&lt;p&gt;It also cannot replace a flaky test quarantine process.&lt;br&gt;
Quarantine needs a named owner and a review date.&lt;br&gt;
A tidy table is not that owner, even on paper.&lt;/p&gt;

&lt;p&gt;Do not use this flow on deploys that mutate shared state.&lt;br&gt;
A retried deploy can double-write or stop halfway through.&lt;br&gt;
Those jobs need idempotency tests, not a wider &lt;code&gt;when&lt;/code&gt; list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should skip it
&lt;/h2&gt;

&lt;p&gt;Skip this FAQ if you cannot download the job log.&lt;br&gt;
Skip it if the image floats and you refuse to pin it.&lt;br&gt;
Skip it if you wanted a model to approve the merge.&lt;/p&gt;

&lt;p&gt;Also skip it when the log holds secrets or customer data.&lt;br&gt;
Do not paste tokens into a model just to gain speed.&lt;br&gt;
Redaction comes before any classification prompt, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pass I can defend in review
&lt;/h2&gt;

&lt;p&gt;I timebox the pass so the debate cannot sprawl.&lt;br&gt;
Seven checks are enough for one failed job.&lt;br&gt;
More checks usually mean I am avoiding the unknown bucket.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save the failed job log outside the chat pane.&lt;/li&gt;
&lt;li&gt;Run the git commands and keep their raw output.&lt;/li&gt;
&lt;li&gt;Run the heuristic and treat the bucket as a candidate.&lt;/li&gt;
&lt;li&gt;Fill one table row with quoted lines from that log.&lt;/li&gt;
&lt;li&gt;Ask for a label only, and ban retry advice.&lt;/li&gt;
&lt;li&gt;Edit YAML only for a confirmed transient platform case.&lt;/li&gt;
&lt;li&gt;Link the current GitLab retry docs in the merge request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If step four has an empty evidence cell, stop there.&lt;br&gt;
Empty cells are the useful part of this worksheet.&lt;br&gt;
They block a confident story you cannot quote yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Close the myth without a new one
&lt;/h2&gt;

&lt;p&gt;A free shell can make a second run cheaper to try.&lt;br&gt;
It still cannot sign a retry rule for an unmatched runner.&lt;br&gt;
I would rather ship no retry than ship a guessed &lt;code&gt;when&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you already have MonkeyCode's free server, use it here.&lt;br&gt;
Run this worksheet on one saved log before you edit YAML.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>cicd</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>FAQ: Which Group Facts Never Ride Along</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Mon, 12 Oct 2026 09:21:18 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/faq-which-group-facts-never-ride-along-59ee</link>
      <guid>https://dev.to/gitlab_3188/faq-which-group-facts-never-ride-along-59ee</guid>
      <description>&lt;h1&gt;
  
  
  FAQ: Which Group Facts Never Ride Along
&lt;/h1&gt;

&lt;p&gt;I stared at a scratch shell that printed my repo name.&lt;br&gt;
It felt like the group had followed me in.&lt;br&gt;
Had it, or did I just paste a familiar path?&lt;/p&gt;

&lt;p&gt;I treat these drafts as unproven until context shows up.&lt;br&gt;
A common ask is to bless a job nobody has run.&lt;br&gt;
I start with context, not with pretty YAML syntax.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the shortcut feels earned
&lt;/h2&gt;

&lt;p&gt;A copied job file is full of familiar names.&lt;br&gt;
A shell prompt looks like work already began.&lt;br&gt;
Those two sights fool tired reviewers every week.&lt;/p&gt;

&lt;p&gt;A free model can echo names from the paste.&lt;br&gt;
A free server can give you a shell fast.&lt;br&gt;
Neither sight is a grant from your GitLab group.&lt;/p&gt;

&lt;p&gt;I draft this presence check with MonkeyCode free model access.&lt;br&gt;
Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;br&gt;
I then run it on the free server option and stop.&lt;/p&gt;

&lt;p&gt;A scratch host is not my group, and it never was.&lt;br&gt;
Would you merge a job that cannot name its own job id?&lt;/p&gt;
&lt;h2&gt;
  
  
  Claims I refuse on sight
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The shell inherited CI variables
&lt;/h3&gt;

&lt;p&gt;People say the shell already holds the CI variables.&lt;br&gt;
I ask which job injected them, and then I wait.&lt;br&gt;
That silence is the evidence I actually needed.&lt;/p&gt;

&lt;p&gt;GitLab injects variables into a specific job process.&lt;br&gt;
A chat window is not that job process.&lt;br&gt;
A spare shell is not that job process either.&lt;/p&gt;

&lt;p&gt;Protected variables stay on protected refs and pipelines.&lt;br&gt;
Masked does not mean present in your spare shell.&lt;br&gt;
File-type variables arrive as paths inside the job.&lt;/p&gt;

&lt;p&gt;Have you opened the job log and searched for the injection?&lt;br&gt;
No log means you are guessing about scope.&lt;br&gt;
I will not guess about scope in a review comment.&lt;/p&gt;
&lt;h3&gt;
  
  
  Registry auth came along
&lt;/h3&gt;

&lt;p&gt;A drafted docker push is just a line of text.&lt;br&gt;
Text does not create a Docker config for you.&lt;br&gt;
A model can spell the command and still be logged out.&lt;/p&gt;

&lt;p&gt;Did this workspace run a real docker login?&lt;br&gt;
Did a job expose CI_JOB_TOKEN in this process?&lt;br&gt;
If both answers are no, you have no registry grant.&lt;/p&gt;

&lt;p&gt;I refuse to treat a suggested push as a login.&lt;br&gt;
Would you ship an image with a fictional auth file?&lt;br&gt;
That mistake often shows up dressed as confidence.&lt;/p&gt;
&lt;h3&gt;
  
  
  Same cache key, same bytes
&lt;/h3&gt;

&lt;p&gt;Cache is a key, a project, and a runner store.&lt;br&gt;
Your laptop store is not the shared runner store.&lt;br&gt;
A free server store is a third bucket again.&lt;/p&gt;

&lt;p&gt;Matching cache key strings can still miss badly.&lt;br&gt;
The bytes live where the runner actually saved them.&lt;br&gt;
Have you blamed a flake when the store was simply elsewhere?&lt;/p&gt;

&lt;p&gt;I want the runner id beside the key before I argue.&lt;br&gt;
No runner id means I only have a string.&lt;br&gt;
A bare string is not a cache hit.&lt;/p&gt;
&lt;h3&gt;
  
  
  The model remembered the environment
&lt;/h3&gt;

&lt;p&gt;Models keep prompt text, not environment protection rules.&lt;br&gt;
A protected environment is a GitLab project setting.&lt;br&gt;
That setting does not live in shell history.&lt;/p&gt;

&lt;p&gt;If a draft claims a production deploy, I want the job URL.&lt;br&gt;
No URL means no deploy record I can trust.&lt;br&gt;
A confident sentence is not an environment audit log.&lt;/p&gt;

&lt;p&gt;Could the model have copied an old success story from the prompt?&lt;br&gt;
Yes, and that story is still not this run.&lt;br&gt;
I separate memory of text from memory of a deploy.&lt;/p&gt;
&lt;h3&gt;
  
  
  Agreement means the docs were checked
&lt;/h3&gt;

&lt;p&gt;A free model may agree that your variable scope is fine.&lt;br&gt;
Agreement is not a fetch of the current docs.&lt;br&gt;
Did anyone open the primary docs page in this session?&lt;/p&gt;

&lt;p&gt;I keep the links in the review note, not in my head.&lt;br&gt;
GitLab's variable page is the source, not the chat.&lt;br&gt;
If the page and the draft disagree, the page wins.&lt;/p&gt;

&lt;p&gt;I do not trust a rule I cannot reopen today.&lt;br&gt;
Read these before you cite a rule, and check the page date:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI/CD variables: &lt;a href="https://docs.gitlab.com/ci/variables/" rel="noopener noreferrer"&gt;https://docs.gitlab.com/ci/variables/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Predefined variables: &lt;a href="https://docs.gitlab.com/ci/variables/predefined_variables/" rel="noopener noreferrer"&gt;https://docs.gitlab.com/ci/variables/predefined_variables/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Protected environments: &lt;a href="https://docs.gitlab.com/ci/environments/protected_environments/" rel="noopener noreferrer"&gt;https://docs.gitlab.com/ci/environments/protected_environments/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Caching: &lt;a href="https://docs.gitlab.com/ci/caching/" rel="noopener noreferrer"&gt;https://docs.gitlab.com/ci/caching/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am not quoting a version number from memory.&lt;br&gt;
Pages move, and my memory is a bad pin.&lt;br&gt;
The receipt below does not replace those pages.&lt;/p&gt;
&lt;h2&gt;
  
  
  The artifact: a presence receipt
&lt;/h2&gt;

&lt;p&gt;I want a receipt that lists names and states only.&lt;br&gt;
Values stay out of the file, the log, and the chat.&lt;br&gt;
You can run this on a free server without importing secrets.&lt;/p&gt;

&lt;p&gt;Treat the script as an example until your own run exists.&lt;br&gt;
I have not run this against your group.&lt;br&gt;
I will not pretend that a sample run exists.&lt;/p&gt;

&lt;p&gt;This example targets Python 3.9 or newer on purpose.&lt;br&gt;
Save the script as &lt;code&gt;context_gap.py&lt;/code&gt; beside the pipeline.&lt;br&gt;
Commit only a name list beside that script.&lt;/p&gt;

&lt;p&gt;Do not commit values, tokens, or a live env file.&lt;br&gt;
A leaked env file would undo the whole point.&lt;br&gt;
Keep secrets in the job, not in the repo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Presence receipt. Record names and states, never values.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_names&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isalnum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refusing odd name: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;state_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;present&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;absent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CI context gap receipt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manifest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context-gap.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_names&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;docker_cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.docker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checked_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%dT%H:%M:%SZ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hostname&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gethostname&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ci_job_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;state_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CI_JOB_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;state_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CI_JOB_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;state_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CI_REGISTRY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docker_config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file_present&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;docker_cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_file&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file_absent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;state_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrote &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Name list
&lt;/h3&gt;

&lt;p&gt;Here is the name list I would commit beside the script.&lt;br&gt;
Names are the only claim I put under test.&lt;br&gt;
Values are never part of the claim file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# ci-names.txt — names only, never values
CI_PROJECT_ID
CI_PROJECT_PATH
CI_COMMIT_SHA
CI_COMMIT_REF_PROTECTED
CI_REGISTRY
CI_REGISTRY_IMAGE
CI_JOB_ID
CI_JOB_TOKEN
CI_ENVIRONMENT_NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Commands
&lt;/h3&gt;

&lt;p&gt;Run the script, then read states, not stories.&lt;br&gt;
Keep the output next to the draft, not inside it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 context_gap.py ci-names.txt &lt;span class="nt"&gt;--out&lt;/span&gt; context-gap.json
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool context-gap.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a scratch shell I expect almost every state to read absent.&lt;br&gt;
That absence is a pass for this myth, not a GitLab outage.&lt;br&gt;
If you export real tokens to flip red into green, you failed.&lt;/p&gt;

&lt;p&gt;The JSON below is a shaped example, not a captured log.&lt;br&gt;
Use it to learn the shape, not to cite a measurement.&lt;br&gt;
Your hostname and timestamp will differ, and they should.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checked_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-12T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hostname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scratch-example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ci_job_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"absent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"job_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"absent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"registry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"absent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"docker_config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file_absent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"names"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CI_PROJECT_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"absent"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CI_JOB_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"absent"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A stricter pass
&lt;/h3&gt;

&lt;p&gt;Want a stricter pass than a single shell?&lt;br&gt;
Add a second run inside a real job and diff the two receipts.&lt;br&gt;
Same script, two hosts, no values in either file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diff &lt;span class="nt"&gt;-u&lt;/span&gt; scratch-context-gap.json job-context-gap.json &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I label the scratch file as untrusted context.&lt;br&gt;
I label the job file as job context only.&lt;br&gt;
I still do not call either file a release receipt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four steps, no folklore
&lt;/h3&gt;

&lt;p&gt;Four steps keep the check boring, which is the point.&lt;br&gt;
Skip a step and you are back to folklore.&lt;br&gt;
Run them in order on hosts you actually control.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the script and the name list into the scratch shell.&lt;/li&gt;
&lt;li&gt;Run the script and keep the JSON receipt local.&lt;/li&gt;
&lt;li&gt;Repeat inside one real job that already has context.&lt;/li&gt;
&lt;li&gt;Diff the receipts and write the conclusion in the review.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not paste the receipt into chat if a token state says present.&lt;br&gt;
Say that the job token is present, then stop.&lt;br&gt;
The file path is enough for the reviewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I decide from the receipt
&lt;/h2&gt;

&lt;p&gt;I read the receipt before I read the drafted job.&lt;br&gt;
The table is the whole decision rule I use.&lt;br&gt;
I do not add trust because the prose sounded sure.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Conclusion&lt;/th&gt;
&lt;th&gt;Next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CI_JOB_ID&lt;/code&gt; absent&lt;/td&gt;
&lt;td&gt;This process is not a GitLab job&lt;/td&gt;
&lt;td&gt;Do not claim job scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Names present, job id absent&lt;/td&gt;
&lt;td&gt;Values were exported by hand&lt;/td&gt;
&lt;td&gt;Stop and purge that shell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker_config&lt;/code&gt; file present&lt;/td&gt;
&lt;td&gt;Some auth file exists&lt;/td&gt;
&lt;td&gt;Do not print it; ask who made it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CI_JOB_TOKEN&lt;/code&gt; present&lt;/td&gt;
&lt;td&gt;A job context is actually here&lt;/td&gt;
&lt;td&gt;Keep the token out of chat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All listed names absent&lt;/td&gt;
&lt;td&gt;Scratch shell, as expected&lt;/td&gt;
&lt;td&gt;Draft stays unproven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job receipt differs from scratch&lt;/td&gt;
&lt;td&gt;Context did not ride along&lt;/td&gt;
&lt;td&gt;Cite the diff, not the vibe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three outcomes cover the draft reviews in front of me.&lt;br&gt;
Scratch-absent means the draft has no group facts yet.&lt;br&gt;
Hand-exported means someone leaked scope into the wrong host.&lt;/p&gt;

&lt;p&gt;Job-present means you can discuss that job, and only that job.&lt;br&gt;
A config file can exist and still lack your registry.&lt;br&gt;
Ask who created the file before you call it a login.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model I want in the room
&lt;/h2&gt;

&lt;p&gt;Context is a grant, and grants are scoped.&lt;br&gt;
A grant names the project, the ref, the environment, and the job.&lt;br&gt;
A hostname names a machine, which is a different fact.&lt;/p&gt;

&lt;p&gt;A pasted YAML file is not variable injection.&lt;br&gt;
A model memory is not a protected environment.&lt;br&gt;
A cache key string is not a cache hit.&lt;/p&gt;

&lt;p&gt;I separate job process, registry auth, cache store, and environment rule.&lt;br&gt;
Which of those four buckets did this draft actually touch?&lt;br&gt;
If you cannot point at one, you are still guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this check cannot prove
&lt;/h2&gt;

&lt;p&gt;Presence is not correctness, and absence is not an incident.&lt;br&gt;
A present name can still hold the wrong scope or a stale value.&lt;br&gt;
I never print the value to settle that argument.&lt;/p&gt;

&lt;p&gt;An empty string counts as absent in this script.&lt;br&gt;
That choice avoids printing blanks that look like secrets.&lt;br&gt;
If you need empty versus unset, extend the check locally.&lt;/p&gt;

&lt;p&gt;This script does not call the GitLab API.&lt;br&gt;
It cannot see protected-branch rules from outside a job.&lt;br&gt;
It cannot prove a cache hit on a shared runner.&lt;/p&gt;

&lt;p&gt;Free model access can draft the script, then drift from the docs.&lt;br&gt;
The free server option is a workspace claim, not a duration promise.&lt;br&gt;
I do not know your quota, hardware, or retention window.&lt;/p&gt;

&lt;p&gt;Neither should you invent those numbers in a review.&lt;br&gt;
Do not paste production tokens into a scratch shell to be thorough.&lt;br&gt;
Would you rather stay incomplete than expose a token?&lt;/p&gt;

&lt;p&gt;The name check rejects odd strings, and that is only a guard.&lt;br&gt;
It is not a secret scanner and not a policy engine.&lt;br&gt;
Pair it with your real secret controls if you have them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should skip this
&lt;/h2&gt;

&lt;p&gt;Skip this if you need a signed release receipt.&lt;br&gt;
Skip this if your team forbids any off-runner shell.&lt;br&gt;
Skip this if the next step is a production deploy approval.&lt;/p&gt;

&lt;p&gt;This workflow is a myth check for drafters and reviewers.&lt;br&gt;
It is not a control for auditors or release managers.&lt;br&gt;
Use the real job log when the decision can spend money or ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you bless the draft
&lt;/h2&gt;

&lt;p&gt;I run the receipt, read the absent states, and leave the YAML unblessed.&lt;br&gt;
That small habit cuts fake certainty out of the thread.&lt;/p&gt;

&lt;p&gt;Already drafting on a free server with a free model?&lt;br&gt;
Start with this receipt, not with a merge comment.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>cicd</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Where Is the Job Behind That Green Prompt?</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:52:59 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/where-is-the-job-behind-that-green-prompt-3lfk</link>
      <guid>https://dev.to/gitlab_3188/where-is-the-job-behind-that-green-prompt-3lfk</guid>
      <description>&lt;p&gt;I keep seeing a green prompt offered as pipeline proof.&lt;br&gt;
Someone ran a script on a box they do not administer.&lt;br&gt;
Then they closed the tab and called the merge safe.&lt;/p&gt;

&lt;p&gt;Have you accepted that story during a review?&lt;br&gt;
I want a harder question before anyone clicks approve.&lt;br&gt;
Where is the job, and who stored the trace?&lt;/p&gt;

&lt;p&gt;This piece is a myth list with a local checker.&lt;br&gt;
It is not an audit, and it is not a runner guide.&lt;br&gt;
You can drop the helper tools and keep the rule.&lt;/p&gt;
&lt;h2&gt;
  
  
  The claim that keeps sneaking in
&lt;/h2&gt;

&lt;p&gt;Developers repeat a comfortable sentence after a quick demo.&lt;br&gt;
They say it ran, so the pipeline must be fine.&lt;br&gt;
I break that sentence into five smaller myths below.&lt;/p&gt;

&lt;p&gt;Each myth fails when you ask for a GitLab receipt.&lt;br&gt;
A receipt is a pipeline record you can open later.&lt;br&gt;
If you cannot open it, you do not have proof yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth one: the prompt is the job trace
&lt;/h2&gt;

&lt;p&gt;You run the tests, the prompt returns, and you stop.&lt;br&gt;
That idle prompt feels like a finished job log.&lt;br&gt;
It is not, because GitLab stores the trace with the job.&lt;/p&gt;

&lt;p&gt;A foreign shell never writes that trace for you.&lt;br&gt;
Would a screenshot survive a compliance question next month?&lt;br&gt;
I would not bet a release on a cropped image.&lt;/p&gt;

&lt;p&gt;Open the job log page, or admit you only have a prompt.&lt;br&gt;
GitLab documents job logs as belonging to the job.&lt;br&gt;
Read the &lt;a href="https://docs.gitlab.com/ci/jobs/job_logs/" rel="noopener noreferrer"&gt;job logs page&lt;/a&gt; before you script around traces.&lt;/p&gt;
&lt;h3&gt;
  
  
  The corrected picture
&lt;/h3&gt;

&lt;p&gt;A prompt is evidence that a shell returned, nothing more.&lt;br&gt;
The trace you can defend lives on the job page.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth two: leftover files are artifacts
&lt;/h2&gt;

&lt;p&gt;The scratch directory still holds a tarball after the script.&lt;br&gt;
People call that tarball an artifact and move on.&lt;br&gt;
Job artifacts are files the runner uploads back to GitLab.&lt;/p&gt;

&lt;p&gt;A path on a free-floating host is not that upload.&lt;br&gt;
Can you download it from the job page tomorrow?&lt;br&gt;
If the answer is no, you do not have an artifact.&lt;/p&gt;

&lt;p&gt;I will not guess retention on a server I do not operate.&lt;br&gt;
The &lt;a href="https://docs.gitlab.com/ci/jobs/job_artifacts/" rel="noopener noreferrer"&gt;artifacts page&lt;/a&gt; describes upload, expiry, and download.&lt;br&gt;
Confirm &lt;code&gt;expire_in&lt;/code&gt; in your project instead of borrowing a default.&lt;/p&gt;
&lt;h3&gt;
  
  
  The corrected picture
&lt;/h3&gt;

&lt;p&gt;An artifact is a file GitLab can hand back to you.&lt;br&gt;
A leftover path is a clue, not a downloadable receipt.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth three: drafted rules were already evaluated
&lt;/h2&gt;

&lt;p&gt;A model can write a &lt;code&gt;rules&lt;/code&gt; block that looks confident.&lt;br&gt;
Confidence is not the same thing as pipeline creation.&lt;br&gt;
GitLab evaluates rules when it creates the pipeline.&lt;/p&gt;

&lt;p&gt;Your bash process does not perform that evaluation.&lt;br&gt;
Did the pipeline even exist for that commit SHA?&lt;br&gt;
If the YAML never landed in the CI config path, no.&lt;/p&gt;

&lt;p&gt;Most projects use &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; at the repository root.&lt;br&gt;
A custom CI config path is also a real project setting.&lt;br&gt;
Check that setting before you blame the file name.&lt;/p&gt;

&lt;p&gt;Start at the &lt;a href="https://docs.gitlab.com/ci/yaml/" rel="noopener noreferrer"&gt;CI YAML docs&lt;/a&gt; and read the rules section.&lt;br&gt;
A chat file is not the file GitLab compiled.&lt;br&gt;
I link the docs so you can re-check evaluation notes.&lt;/p&gt;
&lt;h3&gt;
  
  
  The corrected picture
&lt;/h3&gt;

&lt;p&gt;Rules fire only when GitLab creates the pipeline.&lt;br&gt;
Until that record exists, the block is still a draft.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth four: a scratch export is a CI variable
&lt;/h2&gt;

&lt;p&gt;You export a token, the command passes, and you cheer.&lt;br&gt;
That export lives in that shell and its child processes.&lt;br&gt;
Project CI variables are injected by GitLab into the job.&lt;/p&gt;

&lt;p&gt;Protected variables are withheld from unprotected pipelines and runners.&lt;br&gt;
Confirm the current protection rules before you rely on them.&lt;br&gt;
Should you paste a masked variable into a third-party shell?&lt;/p&gt;

&lt;p&gt;No, you should not, because that is a leak.&lt;br&gt;
A leak is not a test, even when the script goes green.&lt;br&gt;
Start at the &lt;a href="https://docs.gitlab.com/ci/variables/" rel="noopener noreferrer"&gt;variables page&lt;/a&gt; and the &lt;a href="https://docs.gitlab.com/ci/variables/predefined_variables/" rel="noopener noreferrer"&gt;predefined names page&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  The corrected picture
&lt;/h3&gt;

&lt;p&gt;CI variables show up inside the job environment GitLab built.&lt;br&gt;
A manual export is a different environment with a different risk.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth five: a printed job id means a job ran
&lt;/h2&gt;

&lt;p&gt;Models are happy to invent a plausible job number.&lt;br&gt;
They will also invent a tidy job URL around it.&lt;br&gt;
Predefined names like &lt;code&gt;CI_JOB_ID&lt;/code&gt; exist inside a real job.&lt;/p&gt;

&lt;p&gt;Outside that process they are ordinary strings you typed.&lt;br&gt;
Would you approve a deploy from a made-up URL?&lt;br&gt;
I hope the answer is a fast no.&lt;/p&gt;

&lt;p&gt;Match the URL to a job you can open while logged in.&lt;br&gt;
Then compare the trace line to the note in your ticket.&lt;br&gt;
A matching pair is still a human check, not a script miracle.&lt;/p&gt;
&lt;h3&gt;
  
  
  The corrected picture
&lt;/h3&gt;

&lt;p&gt;A real job id is printed by the job, then stored by GitLab.&lt;br&gt;
Anything else is a story with numbers in it.&lt;/p&gt;
&lt;h2&gt;
  
  
  A free draft can still be useful
&lt;/h2&gt;

&lt;p&gt;I still like a cheap place to shape a command.&lt;br&gt;
MonkeyCode offers free model access and a free server option.&lt;br&gt;
Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;I ask the model for a draft, then I distrust the draft.&lt;br&gt;
I may retry the command shape on the free server option.&lt;br&gt;
That retry is scratch execution, not a GitLab receipt.&lt;/p&gt;

&lt;p&gt;Take MonkeyCode out of the story and the myths remain.&lt;br&gt;
The product is a place to practice, not the proof.&lt;br&gt;
If those options change, the receipt rule does not move.&lt;/p&gt;

&lt;p&gt;I am not stating model names, quotas, hardware, or duration.&lt;br&gt;
Those facts were not supplied, so I will not invent them.&lt;br&gt;
Check the product yourself if you need current limits.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three layers, not one blur
&lt;/h2&gt;

&lt;p&gt;I keep three layers separate when I review a claim.&lt;br&gt;
Mixing them is how a green prompt becomes a false pass.&lt;br&gt;
Ask which layer you hold before you type approve.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Draft text is untrusted wording, even when it parses.&lt;/li&gt;
&lt;li&gt;Scratch execution means a command ran on some host.&lt;/li&gt;
&lt;li&gt;A receipt means GitLab created a pipeline and a job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which layer is in your hand when you type LGTM?&lt;br&gt;
If you cannot answer, you are not ready to merge.&lt;br&gt;
Only the third layer closes a CI proof comment.&lt;/p&gt;
&lt;h2&gt;
  
  
  The checker I want beside the review
&lt;/h2&gt;

&lt;p&gt;I am offering a small classifier you run locally.&lt;br&gt;
It reads a folder of notes and prints one verdict.&lt;br&gt;
It never logs in, and it never calls the network.&lt;/p&gt;

&lt;p&gt;Shape checks are not authentication, and I say so in the output.&lt;br&gt;
Treat the script as a proposal until you run it.&lt;br&gt;
I have not published timings, pass rates, or server specs.&lt;/p&gt;

&lt;p&gt;Save it as &lt;code&gt;proof-gap.sh&lt;/code&gt; and call it with bash.&lt;br&gt;
Make it executable only on a machine you trust.&lt;/p&gt;
&lt;h3&gt;
  
  
  Files it expects
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;job_url.txt&lt;/code&gt; holds one URL shaped like a GitLab job link.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trace_note.txt&lt;/code&gt; holds a line you personally saw in the log.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;variable_source.txt&lt;/code&gt; is &lt;code&gt;gitlab_job_env&lt;/code&gt; or some other source token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rules_source.txt&lt;/code&gt; is &lt;code&gt;pipeline_created&lt;/code&gt; only after GitLab compiled CI.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;image_ref.txt&lt;/code&gt; is optional, and a digest uses the sha256 form.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  The script
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# proof-gap.sh&lt;/span&gt;
&lt;span class="c"&gt;# Local evidence classifier. No network. Not authentication.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;./evidence&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

need&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

say&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

say &lt;span class="s2"&gt;"evidence_root=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; need job_url.txt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;say &lt;span class="s2"&gt;"verdict=SCRATCH_ONLY"&lt;/span&gt;
  say &lt;span class="s2"&gt;"missing=job_url.txt"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[:space:]'&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/job_url.txt"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;https://&lt;span class="k"&gt;*&lt;/span&gt;/-/jobs/[0-9]&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    say &lt;span class="s2"&gt;"job_url_shape=plausible"&lt;/span&gt;
    &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    say &lt;span class="s2"&gt;"verdict=NOT_A_JOB_URL"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
    &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; need trace_note.txt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;say &lt;span class="s2"&gt;"verdict=LINK_WITHOUT_TRACE"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; need variable_source.txt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;say &lt;span class="s2"&gt;"verdict=ENV_UNPROVEN"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[:space:]'&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/variable_source.txt"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
say &lt;span class="s2"&gt;"variable_source=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;src&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;src&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"gitlab_job_env"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;say &lt;span class="s2"&gt;"verdict=ENV_NOT_CI"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; need rules_source.txt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;say &lt;span class="s2"&gt;"verdict=RULES_UNPROVEN"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;rules&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[:space:]'&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/rules_source.txt"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
say &lt;span class="s2"&gt;"rules_source=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;rules&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;rules&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"pipeline_created"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;say &lt;span class="s2"&gt;"verdict=RULES_NOT_EVALUATED"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

if &lt;/span&gt;need image_ref.txt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'[:space:]'&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/image_ref.txt"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;@sha256:&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      say &lt;span class="s2"&gt;"image_pin=digest"&lt;/span&gt;
      &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      say &lt;span class="s2"&gt;"verdict=IMAGE_NOT_A_DIGEST"&lt;/span&gt;
      &lt;span class="nb"&gt;exit &lt;/span&gt;0
      &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;say &lt;span class="s2"&gt;"verdict=RECEIPT_SHAPE_ONLY"&lt;/span&gt;
say &lt;span class="s2"&gt;"note=open_the_job_and_compare_the_trace"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The shape check only looks for a jobs path plus digits.&lt;br&gt;
A plausible shape still is not proof you can log in.&lt;br&gt;
Open the page yourself before you trust the folder.&lt;/p&gt;
&lt;h2&gt;
  
  
  How the verdicts map to the myths
&lt;/h2&gt;

&lt;p&gt;The table is a legend, not a policy engine.&lt;br&gt;
Read the verdict, then do the human step beside it.&lt;br&gt;
A tidy folder can still be a lie you told yourself.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Myth it catches&lt;/th&gt;
&lt;th&gt;Human step that remains&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SCRATCH_ONLY&lt;/td&gt;
&lt;td&gt;Prompt treated as a trace&lt;/td&gt;
&lt;td&gt;Commit CI config and wait for a job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NOT_A_JOB_URL&lt;/td&gt;
&lt;td&gt;Invented or sloppy link&lt;/td&gt;
&lt;td&gt;Paste a real jobs URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LINK_WITHOUT_TRACE&lt;/td&gt;
&lt;td&gt;URL with no log note&lt;/td&gt;
&lt;td&gt;Open the log and save the line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ENV_NOT_CI&lt;/td&gt;
&lt;td&gt;Scratch export called CI&lt;/td&gt;
&lt;td&gt;Re-run inside a job or drop the claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ENV_UNPROVEN&lt;/td&gt;
&lt;td&gt;No variable source note&lt;/td&gt;
&lt;td&gt;Record where the values came from&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RULES_NOT_EVALUATED&lt;/td&gt;
&lt;td&gt;Draft rules called done&lt;/td&gt;
&lt;td&gt;Wait until GitLab creates the pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RULES_UNPROVEN&lt;/td&gt;
&lt;td&gt;No rules source note&lt;/td&gt;
&lt;td&gt;Record whether a pipeline was created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IMAGE_NOT_A_DIGEST&lt;/td&gt;
&lt;td&gt;Loose tag treated as a pin&lt;/td&gt;
&lt;td&gt;Record a digest or drop the pin claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RECEIPT_SHAPE_ONLY&lt;/td&gt;
&lt;td&gt;Folder looks complete&lt;/td&gt;
&lt;td&gt;Log in and compare the trace&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Run it against two fixtures
&lt;/h2&gt;

&lt;p&gt;Create a folder that mimics a cheerful scratch demo.&lt;br&gt;
Point the classifier at it and read the verdict.&lt;br&gt;
You should see &lt;code&gt;ENV_NOT_CI&lt;/code&gt; if you follow this fixture.&lt;/p&gt;

&lt;p&gt;The example host is a fixture, not a live project.&lt;br&gt;
Do not expect that URL to open in your browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; evidence/scratch-demo
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'https://gitlab.example.com/group/app/-/jobs/1042'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; evidence/scratch-demo/job_url.txt
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'I opened the log and copied the failing line.'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; evidence/scratch-demo/trace_note.txt
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'scratch_export'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; evidence/scratch-demo/variable_source.txt
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'chat_draft'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; evidence/scratch-demo/rules_source.txt
bash proof-gap.sh evidence/scratch-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URL shape is plausible, and the trace note exists.&lt;br&gt;
The variable source is still a scratch export.&lt;br&gt;
That is myth four, caught before anyone approves.&lt;/p&gt;

&lt;p&gt;Now flip only the variable source and run again.&lt;br&gt;
The next failure should be the rules source.&lt;br&gt;
A green chain of files can still hide an unevaluated rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; evidence/rules-gap
&lt;span class="nb"&gt;cp &lt;/span&gt;evidence/scratch-demo/job_url.txt evidence/rules-gap/
&lt;span class="nb"&gt;cp &lt;/span&gt;evidence/scratch-demo/trace_note.txt evidence/rules-gap/
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'gitlab_job_env'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; evidence/rules-gap/variable_source.txt
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'chat_draft'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; evidence/rules-gap/rules_source.txt
bash proof-gap.sh evidence/rules-gap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a tiny assertion so the fixture fails loudly.&lt;br&gt;
This is a test plan, not a recorded lab result.&lt;br&gt;
Run it on your machine before you trust the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;bash proof-gap.sh evidence/rules-gap&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'verdict=RULES_NOT_EVALUATED'&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'fixture_ok'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What a real job would print
&lt;/h2&gt;

&lt;p&gt;Here is an unexecuted job that prints receipt names.&lt;br&gt;
Do not commit it only to decorate a README.&lt;br&gt;
Commit it when you actually want a pipeline record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Unexecuted example. GitLab compiles this only after you commit it&lt;/span&gt;
&lt;span class="c1"&gt;# on the CI config path your project actually uses.&lt;/span&gt;
&lt;span class="na"&gt;receipt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "CI_PIPELINE_ID=${CI_PIPELINE_ID}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "CI_JOB_ID=${CI_JOB_ID}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "CI_JOB_URL=${CI_JOB_URL}"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "CI_COMMIT_SHA=${CI_COMMIT_SHA}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those names are predefined inside jobs GitLab starts.&lt;br&gt;
They are not magic strings a model may invent.&lt;br&gt;
Confirm the &lt;a href="https://docs.gitlab.com/ci/variables/predefined_variables/" rel="noopener noreferrer"&gt;predefined variables page&lt;/a&gt; if you encode policy.&lt;/p&gt;

&lt;p&gt;After the job exists, save the URL into &lt;code&gt;job_url.txt&lt;/code&gt;.&lt;br&gt;
Write &lt;code&gt;gitlab_job_env&lt;/code&gt; only if the values came from that job.&lt;br&gt;
Write &lt;code&gt;pipeline_created&lt;/code&gt; only if GitLab showed the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits you should say out loud
&lt;/h2&gt;

&lt;p&gt;The classifier trusts your files, and your files can lie.&lt;br&gt;
A forged URL with a forged note still looks tidy.&lt;br&gt;
You still have to open GitLab and read the trace.&lt;/p&gt;

&lt;h3&gt;
  
  
  What can vanish or drift
&lt;/h3&gt;

&lt;p&gt;Free model text can omit image, interruptible, or timeout.&lt;br&gt;
A free server can disappear with your scratch files.&lt;br&gt;
I do not know those limits, so plan for loss.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you must not paste
&lt;/h3&gt;

&lt;p&gt;Do not put secrets, production tokens, or customer data there.&lt;br&gt;
A practice shell is a bad vault and a bad production runner.&lt;br&gt;
If you need a registered runner, use your own runner fleet.&lt;/p&gt;

&lt;p&gt;This method does not prove coverage, flaky retries, or review quality.&lt;br&gt;
Those are different problems and they need their own receipts.&lt;br&gt;
It also does not replace CODEOWNERS, approval rules, or signed commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should not use this approach
&lt;/h2&gt;

&lt;p&gt;Skip this if you need an audit-grade attestation.&lt;br&gt;
Skip it if the next step handles production secrets.&lt;br&gt;
Skip it if a regulator wants a retained, access-controlled log.&lt;/p&gt;

&lt;p&gt;Students and reviewers can use the fixture as a teaching drill.&lt;br&gt;
People who already confuse prompts with traces need it more.&lt;br&gt;
People who already have a strict release ledger do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you type approve
&lt;/h2&gt;

&lt;p&gt;If a free server is already open, run this classifier there first.&lt;br&gt;
Read the verdict out loud before you type approve.&lt;br&gt;
Then open the real job, because the script cannot log in.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>devops</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Myth FAQ: What My Clean Draft Run Still Hides</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Mon, 12 Oct 2026 02:19:43 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/myth-faq-what-my-clean-draft-run-still-hides-29dd</link>
      <guid>https://dev.to/gitlab_3188/myth-faq-what-my-clean-draft-run-still-hides-29dd</guid>
      <description>&lt;p&gt;I kept a green pipeline, and I still blocked the merge.&lt;br&gt;
The draft YAML looked tidy, and the job status looked calm.&lt;br&gt;
Why did that pair still fail my review?&lt;/p&gt;

&lt;p&gt;I was not hunting for style nits in the script block.&lt;br&gt;
I was hunting for controls I could explain a month later.&lt;br&gt;
A calm log does not answer that kind of question.&lt;/p&gt;
&lt;h2&gt;
  
  
  The gap I keep tripping over
&lt;/h2&gt;

&lt;p&gt;A control record names the revision that actually ran.&lt;br&gt;
It names the secret boundary, and it names the token scope.&lt;br&gt;
A status badge names none of those three things.&lt;/p&gt;

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

&lt;p&gt;This piece is a myth FAQ for that exact mix-up.&lt;br&gt;
Each myth is a claim I still hear in review.&lt;br&gt;
Each correction is a habit you can run on one file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where a free draft loop actually helps
&lt;/h2&gt;

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

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

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

&lt;p&gt;Remove every product mention and the checks still stand.&lt;br&gt;
That is the bar I want for a technical article.&lt;br&gt;
A vendor sketch is not the control I am trying to build.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 1: a branch name is basically a pin
&lt;/h2&gt;

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

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform/ci-templates"&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9f2c1ab0e4d84a6c91b0a1e7c0d4f8a2b6e1d903"&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/templates/scan.yml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Component versions deserve the same suspicion.&lt;br&gt;
A version tag is only as still as the project that owns it.&lt;br&gt;
Confirm tag protection there before you treat &lt;code&gt;@1.4.2&lt;/code&gt; as frozen.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 2: a masked variable makes the log shareable
&lt;/h2&gt;

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

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;job_with_a_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Token is present"&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;Confirm that rule against current docs if your instance is customized.&lt;br&gt;
I am not quoting a menu label from memory as your admin's label.&lt;br&gt;
The setting you see today beats a sentence I remember from last year.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 3: one successful token call means a chosen scope
&lt;/h2&gt;

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

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Local text check only. This does not query GitLab settings.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"CI_JOB_TOKEN"&lt;/span&gt; .gitlab-ci.yml &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no job token string"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;What would I store beside the merge request?&lt;br&gt;
I store a dated note of who can inbound-call this project with a job token.&lt;br&gt;
I do not store the token value, and I do not paste the clone URL with it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 4: an artifact URL is retained evidence
&lt;/h2&gt;

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

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

&lt;p&gt;Corrected model: evidence I need later gets copied to a store I control.&lt;br&gt;
The pipeline log is a clue, not the archive.&lt;br&gt;
Write &lt;code&gt;expire_in&lt;/code&gt; when the artifact is part of the review story.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;review_bundle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python3 check_ci_draft.py .gitlab-ci.yml &amp;gt; draft-report.txt&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;draft-report.txt&lt;/span&gt;
    &lt;span class="na"&gt;expire_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7 days&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven days is an example choice, not a compliance window I recommend.&lt;br&gt;
Pick a window your team can actually defend in review.&lt;br&gt;
If you need years of retention, this pattern is the wrong tool.&lt;/p&gt;
&lt;h2&gt;
  
  
  A local checker, labeled unexecuted
&lt;/h2&gt;

&lt;p&gt;This script is a proposal for a single draft file.&lt;br&gt;
I have not executed it for this article, and I am not calling it a pass engine.&lt;br&gt;
It will miss anchors, &lt;code&gt;extends&lt;/code&gt;, and &lt;code&gt;!reference&lt;/code&gt; indirection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Local draft check. Not a policy engine. Unexecuted here.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;MOVING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;master&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;develop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;head&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;SECRET_HINTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.gitlab-ci.yml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;findings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;stripped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stripped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ref:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stripped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;short&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MOVING&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;short&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: moving or short ref &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stripped&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stripped&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SECRET_HINTS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: script may print a secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artifacts:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expire_in:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artifacts block has no expire_in&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ci_job_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CI_JOB_TOKEN present; confirm scope outside this file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no local findings; this is not a pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;findings:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ref heuristic is intentionally blunt.&lt;br&gt;
A short tag can be a fair pin if tag protection holds.&lt;br&gt;
Treat every finding as a question, not as a guilty verdict.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 check_ci_draft.py .gitlab-ci.yml
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would run that before any free server execution of the same draft.&lt;br&gt;
A zero exit only means this file tripped no local rule.&lt;br&gt;
It never means the pipeline is pinned, secret-safe, or approved.&lt;/p&gt;
&lt;h2&gt;
  
  
  Walk a bad draft before you trust the script
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform/ci-templates"&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/templates/scan.yml"&lt;/span&gt;

&lt;span class="na"&gt;leak_check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "debug token is $DEPLOY_TOKEN"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git clone "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.example/app/ops.git"&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;scan.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My manual findings are boring, and that is the point.&lt;/p&gt;

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

&lt;p&gt;A model can repair those lines after you name the findings.&lt;br&gt;
Do not ask it to repair them before you name the findings.&lt;br&gt;
Otherwise you get a cleaner myth, not a clearer control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiny test plan for the checker
&lt;/h2&gt;

&lt;p&gt;I would keep four fixtures beside the script, and I would not skip the last one.&lt;br&gt;
This plan is proposed, not a run I am reporting as complete.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A file with &lt;code&gt;ref: main&lt;/code&gt; must exit nonzero and mention that ref.&lt;/li&gt;
&lt;li&gt;A forty-character SHA ref must not be flagged as a moving branch.&lt;/li&gt;
&lt;li&gt;A line that echoes a token must exit nonzero.&lt;/li&gt;
&lt;li&gt;An artifacts block without &lt;code&gt;expire_in&lt;/code&gt; must be listed.&lt;/li&gt;
&lt;li&gt;A quiet file must print that no finding is not a pass.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h2&gt;
  
  
  The decision table I keep beside the draft
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim I heard&lt;/th&gt;
&lt;th&gt;What I check&lt;/th&gt;
&lt;th&gt;Corrected model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The include came from our group&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ref&lt;/code&gt; is a SHA, not a branch&lt;/td&gt;
&lt;td&gt;Group trust is not immutability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The variable is masked&lt;/td&gt;
&lt;td&gt;No echo and no encoded reprint&lt;/td&gt;
&lt;td&gt;Masking is a log filter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The token clone worked&lt;/td&gt;
&lt;td&gt;Job token permissions in settings&lt;/td&gt;
&lt;td&gt;Success is not a written scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The artifact URL exists&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;expire_in&lt;/code&gt; plus an external copy&lt;/td&gt;
&lt;td&gt;A URL is not retention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;h2&gt;
  
  
  Who should skip this habit
&lt;/h2&gt;

&lt;p&gt;Do not use this habit as a compliance attestation.&lt;br&gt;
Do not use it when your reviewer wants signed provenance.&lt;br&gt;
Do not use a free server run as proof of isolation, hardware, or uptime.&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  How I close the review
&lt;/h2&gt;

&lt;p&gt;I ask four questions, and I wait for boring answers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which SHA did we include, and who can move a tag if we used one?&lt;/li&gt;
&lt;li&gt;Which project setting scopes the job token, and where is that note stored?&lt;/li&gt;
&lt;li&gt;Which log lines are safe to paste into chat after this run?&lt;/li&gt;
&lt;li&gt;Where did the artifact get copied, and when does the copy expire?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I cannot answer those, the green status can wait.&lt;br&gt;
The free model can redraft the YAML after the checker complains.&lt;br&gt;
The free server can rerun the sketch after the pin is real.&lt;/p&gt;

&lt;p&gt;If a free model session is already open, feed it these findings before another hopeful include.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>cicd</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>FAQ: A Side Session Is Not a Pipeline Receipt</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Sun, 11 Oct 2026 19:47:41 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/faq-a-side-session-is-not-a-pipeline-receipt-332b</link>
      <guid>https://dev.to/gitlab_3188/faq-a-side-session-is-not-a-pipeline-receipt-332b</guid>
      <description>&lt;p&gt;I keep stopping on the same merge request comment. The author says the tests already passed somewhere else. I ask which pipeline recorded that exact result.&lt;/p&gt;

&lt;p&gt;The thread often goes quiet, and nobody pastes a link. Have you watched a review stall on that gap? A busy side session can feel like proof.&lt;/p&gt;

&lt;p&gt;It is still not a receipt GitLab stored. I want the pipeline row GitLab actually kept.&lt;/p&gt;

&lt;h2&gt;
  
  
  The claims I keep hearing
&lt;/h2&gt;

&lt;p&gt;These lines show up in review threads often. They sound helpful, and they skip the record. I want these claims retired from review notes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A zero exit on my server means the pipeline is green.&lt;/li&gt;
&lt;li&gt;Cloning the default branch means I tested the merge head.&lt;/li&gt;
&lt;li&gt;A pasted log means the failure cause is already known.&lt;/li&gt;
&lt;li&gt;A temporary free server is safe enough for job tokens.&lt;/li&gt;
&lt;li&gt;A commit hash in chat means that job already finished.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which of these have you accepted too quickly?&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I started asking for ids
&lt;/h3&gt;

&lt;p&gt;Speed is not the same thing as evidence. I can still respect a fast careful rehearsal. I still need a stored row before I approve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 1: A side pass is a pipeline
&lt;/h2&gt;

&lt;p&gt;You ran a suite, and the shell returned zero. That event happened outside GitLab's own pipeline table. A pipeline row has an id, a status, and a sha.&lt;/p&gt;

&lt;p&gt;Your scrollback has none of those stored fields. I call the side run a rehearsal, not evidence. I will not cite it as a green check.&lt;/p&gt;

&lt;p&gt;Where is the job URL for this head? I will not invent one from terminal memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I accept instead
&lt;/h3&gt;

&lt;p&gt;I accept a pipeline id that I can open. I accept a job status from that same pipeline. I accept a web_url pasted into the thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 2: Default clone means this head
&lt;/h2&gt;

&lt;p&gt;The server cloned whatever branch the remote offered. Often that branch is the default, not your head. A merge request head can differ after a rebase.&lt;/p&gt;

&lt;p&gt;Did your rehearsal fetch the exact sha under review? A branch name match is not a commit match. I compare the checked out commit with the merge head.&lt;/p&gt;

&lt;p&gt;If those hashes differ, stop the claim there. You tested another snapshot, not this review. Why argue about logs from the wrong tree?&lt;/p&gt;

&lt;h3&gt;
  
  
  A local equality check
&lt;/h3&gt;

&lt;p&gt;Run this before you describe the rehearsal. It fails closed when the local checkout drifted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MR_HEAD_SHA&lt;/span&gt;:?sha&lt;span class="p"&gt; from the merge request&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse HEAD&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MR_HEAD_SHA&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"checkout matches &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MR_HEAD_SHA&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That test proves your worktree, not a pipeline. Keep that limit in the sentence you post. A matching checkout is still only a rehearsal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 3: A pasted log is the trace
&lt;/h2&gt;

&lt;p&gt;A colleague pastes forty lines into the model chat. The reply then names a confident root cause. I still cannot see a job id in that paste.&lt;/p&gt;

&lt;p&gt;A trace belongs to one job in one project. A paste can be trimmed, stale, or mislabeled. Which job URL produced those exact log lines?&lt;/p&gt;

&lt;p&gt;Does that job's sha match the merge request head? If you cannot answer, you do not have the trace. Guessing a cause from a snippet wastes the next hour.&lt;/p&gt;

&lt;h3&gt;
  
  
  Questions I ask before debugging
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Which project hosted the job that failed?&lt;/li&gt;
&lt;li&gt;Which job id emitted the lines you pasted?&lt;/li&gt;
&lt;li&gt;Which sha did that job actually build?&lt;/li&gt;
&lt;li&gt;Is that sha still the merge request head?&lt;/li&gt;
&lt;li&gt;Did anyone trim the trace before the paste?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If one answer is missing, I stop the diagnosis. I go fetch the job instead of theorizing. The trace page beats another confident chat paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 4: Temporary means safe for tokens
&lt;/h2&gt;

&lt;p&gt;A free hosted shell will still execute what you type. Free access does not create a trust boundary. A short lifetime is not an access policy.&lt;/p&gt;

&lt;p&gt;Would you type a job token on a laptop you do not administer? Protected variables belong on protected runners and refs. A side shell sits outside that fence entirely.&lt;/p&gt;

&lt;p&gt;Keep production secrets on machines you already control. Masked variables are not safe just because chat feels private. A pasted token can outlive the session that received it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 5: A typed SHA means the job finished
&lt;/h2&gt;

&lt;p&gt;The chat can mention a full commit hash. Typing a hash only proves someone typed it. It does not prove a pipeline reached success.&lt;/p&gt;

&lt;p&gt;I ask the API for pipelines with that sha. I want the status, ref, source, and web_url. If the list is empty, the claim is unfinished.&lt;/p&gt;

&lt;p&gt;The same hash on another ref is still different. A branch pipeline is not a tag pipeline. A merge-result pipeline is a third distinct case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fields I refuse to infer
&lt;/h3&gt;

&lt;p&gt;I do not infer status from a cheerful summary. I do not infer ref from a branch nickname. I do not infer source from the words about pushing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model I keep instead
&lt;/h2&gt;

&lt;p&gt;I keep three layers apart when I review. Mixing them is how weak green claims survive.&lt;/p&gt;

&lt;p&gt;The claim is whatever the author or model said. The rehearsal is whatever a side shell ran. The receipt is whatever GitLab stored for that sha.&lt;/p&gt;

&lt;p&gt;Only the receipt belongs in the approval note. The other layers can shape your next question. They cannot close the review on their own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claim map
&lt;/h3&gt;

&lt;p&gt;Use this map before you trust a comment. Weak answers stay in the rehearsal column only.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Weak answer&lt;/th&gt;
&lt;th&gt;Receipt field&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Was it green?&lt;/td&gt;
&lt;td&gt;"I ran it"&lt;/td&gt;
&lt;td&gt;pipeline &lt;code&gt;status&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which commit?&lt;/td&gt;
&lt;td&gt;a hash in chat&lt;/td&gt;
&lt;td&gt;pipeline &lt;code&gt;sha&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which branch context?&lt;/td&gt;
&lt;td&gt;a nickname&lt;/td&gt;
&lt;td&gt;pipeline &lt;code&gt;ref&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which job?&lt;/td&gt;
&lt;td&gt;a YAML snippet&lt;/td&gt;
&lt;td&gt;job &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;status&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where is the log?&lt;/td&gt;
&lt;td&gt;pasted lines&lt;/td&gt;
&lt;td&gt;job &lt;code&gt;web_url&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did secrets work?&lt;/td&gt;
&lt;td&gt;a token in shell&lt;/td&gt;
&lt;td&gt;do not export it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A receipt check you can copy
&lt;/h2&gt;

&lt;p&gt;The script below is a proposal, not a benchmark. I am not reporting timings or sample counts. Run the authenticated calls where your token already lives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_HOST&lt;/span&gt;:?set&lt;span class="p"&gt; your GitLab host&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_TOKEN&lt;/span&gt;:?read_api&lt;span class="p"&gt; token&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;:?numeric&lt;span class="p"&gt; project id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SHA&lt;/span&gt;:?merge&lt;span class="p"&gt; request head sha&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

api&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"PRIVATE-TOKEN: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITLAB_HOST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/v4/projects/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"commit"&lt;/span&gt;
api &lt;span class="s2"&gt;"/repository/commits/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SHA&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="s1"&gt;'{id, short_id, title}'&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pipelines"&lt;/span&gt;
api &lt;span class="s2"&gt;"/pipelines?sha=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SHA&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;per_page=20"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.[] | {id, status, ref, source, sha, web_url}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick a pipeline id from that first list. Then fetch the pipeline row and its jobs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PIPELINE_ID&lt;/span&gt;:?id&lt;span class="p"&gt; from the list above&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

api &lt;span class="s2"&gt;"/pipelines/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PIPELINE_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'{id, status, sha, ref, source, web_url}'&lt;/span&gt;

api &lt;span class="s2"&gt;"/pipelines/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PIPELINE_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/jobs?per_page=100"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.[] | {id, name, status, stage, allow_failure, web_url}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare these points before you write the review.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the sha equals the merge request head.&lt;/li&gt;
&lt;li&gt;Confirm status is success, not a chat summary.&lt;/li&gt;
&lt;li&gt;Confirm the job name matches the YAML you read.&lt;/li&gt;
&lt;li&gt;Paste web_url into the thread, not shell history.&lt;/li&gt;
&lt;li&gt;Treat a missing job as an absent receipt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No pipeline for that sha means you only rehearsed. Say that plainly when you reply in review. Do not promote a rehearsal into a receipt.&lt;/p&gt;

&lt;p&gt;Those paths follow the Pipelines API and the Jobs API. Confirm them in the current &lt;a href="https://docs.gitlab.com/" rel="noopener noreferrer"&gt;GitLab docs&lt;/a&gt; for your host.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read the flags, not just the color
&lt;/h3&gt;

&lt;p&gt;A green badge can still be a soft pass. Look at &lt;code&gt;allow_failure&lt;/code&gt; on the job you care about. Look at &lt;code&gt;source&lt;/code&gt; so you know why the pipeline exists.&lt;/p&gt;

&lt;p&gt;A scheduled pipeline is not your merge pipeline. A trigger pipeline may carry different variables entirely. Name the source in your note when it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where drafting help fits
&lt;/h2&gt;

&lt;p&gt;MonkeyCode provides free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;I treat both as drafting aids, not as runners. Neither one writes a row in your pipeline table. Removing them would leave this receipt habit intact.&lt;/p&gt;

&lt;p&gt;Ask the model to list the receipt fields you need. Ask it to flag comments that lack a job URL. Do not ask it to approve the merge request.&lt;/p&gt;

&lt;p&gt;Use the free server to syntax-check a local fixture. Point that session only at fake sample JSON. Run real curl calls on your own credentialed machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; fixtures
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; fixtures/pipelines.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[
  {
    "id": 101,
    "status": "success",
    "ref": "feature/receipt",
    "source": "push",
    "sha": "abc123deadbeef",
    "web_url": "https://example.invalid/pipelines/101"
  }
]
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;jq &lt;span class="s1"&gt;'.[] | {id, status, sha, ref, web_url}'&lt;/span&gt; fixtures/pipelines.json
bash &lt;span class="nt"&gt;-n&lt;/span&gt; check-receipt.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model may draft that fixture for you. The server may run jq against the file. Neither step created a pipeline in your project.&lt;/p&gt;

&lt;p&gt;Do you see the gap between rehearsal and receipt? If the model invents a job name, discard it. Your YAML and the jobs API remain the source.&lt;/p&gt;

&lt;p&gt;A fluent paragraph is not an API field. I throw those inventions out of the note.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I will not claim about those options
&lt;/h3&gt;

&lt;p&gt;I am not stating quotas, hardware, or duration. I am not naming models or ranking them. Check the current product docs before you rely on availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits I will not smooth over
&lt;/h2&gt;

&lt;p&gt;This check stays read-only on purpose for reviewers. It does not retry jobs or explain flakes. It does not prove coverage or deploy health.&lt;/p&gt;

&lt;p&gt;A success status can still hide allow_failure jobs. Read that flag before you relax your review. A green pipeline can also skip your target job.&lt;/p&gt;

&lt;p&gt;Use a read_api token for this glance, not a maintainer token. Rotate any secret you pasted into a side session. A vanished server does not unpaste a leaked token.&lt;/p&gt;

&lt;p&gt;Free model text can be stale or simply wrong. Free server files can disappear when the session ends.&lt;/p&gt;

&lt;p&gt;Plan on rerunning the authenticated check after every push. Treat yesterday's pasted log as expired context now.&lt;/p&gt;

&lt;p&gt;GitLab.com and self-managed hosts are not the same. Set GITLAB_HOST to the site you actually use. I am not choosing a default for your company.&lt;/p&gt;

&lt;p&gt;Parent-child pipelines need a second API request too. Bridge jobs do not inline every downstream job. Follow the bridges endpoint when you see a trigger.&lt;/p&gt;

&lt;p&gt;Confirm these paths against current GitLab API docs. I am describing a habit, not pinning an API version. Your admin may also expose a different API prefix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should skip this approach
&lt;/h2&gt;

&lt;p&gt;Skip this if you already open the job log yourself. You do not need a script just to click web_url.&lt;/p&gt;

&lt;p&gt;Skip this when the decision is a release approval. A pipeline id is not your release process. Keep that checklist with the release owners.&lt;/p&gt;

&lt;p&gt;Skip this if personal tokens are forbidden on the project. Use the access path your maintainers already approved. Do not bypass policy through a side server.&lt;/p&gt;

&lt;p&gt;Skip this after the merge request head moves. Recheck pipelines for the new head sha only. An older green row describes a different commit.&lt;/p&gt;

&lt;p&gt;Skip this if you cannot see pipelines in the project. A hidden pipeline is not something to reconstruct from chat. Ask a maintainer for the recorded status instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The note I actually leave
&lt;/h2&gt;

&lt;p&gt;I post one question, and then I stop pushing. A short question beats a lecture every time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which pipeline id matches this current head sha? Paste the job URL, not the shell history.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Need a drafter for that checklist, not a verdict? Use the free model to phrase the questions only. Keep the token on the machine you control.&lt;/p&gt;

&lt;p&gt;Claim, rehearsal, receipt: I merge on the last one. I let the first two stay as questions only.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>cicd</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Did the Agent Compile Your Pipeline, or Just Read It?</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Sun, 11 Oct 2026 16:23:19 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/did-the-agent-compile-your-pipeline-or-just-read-it-1l8o</link>
      <guid>https://dev.to/gitlab_3188/did-the-agent-compile-your-pipeline-or-just-read-it-1l8o</guid>
      <description>&lt;h1&gt;
  
  
  Did the Agent Compile Your Pipeline, or Just Read It?
&lt;/h1&gt;

&lt;p&gt;Did a free shell clone just become your pipeline proof?&lt;br&gt;
The model read the YAML and called that pipeline valid.&lt;br&gt;
I do not accept that leap without a compile receipt.&lt;/p&gt;

&lt;p&gt;I keep hearing four claims after a quick agent pass.&lt;br&gt;
Each claim treats a local read as GitLab's compiler.&lt;br&gt;
I want a guard that separates those two steps.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why this bites on review day
&lt;/h2&gt;

&lt;p&gt;A merge request can look calm while the graph is wrong.&lt;br&gt;
Includes fail quietly when the ref or the file moved.&lt;br&gt;
Rules then hide the job you thought you fixed.&lt;/p&gt;

&lt;p&gt;Have you merged a green chat and met a red pipeline?&lt;br&gt;
That gap can waste a whole review cycle.&lt;br&gt;
The file on the branch was fine as plain text.&lt;/p&gt;

&lt;p&gt;The compiled job graph was not fine at all.&lt;br&gt;
Which layer failed is the only useful question?&lt;br&gt;
A calm diff can still hide a broken graph.&lt;/p&gt;
&lt;h2&gt;
  
  
  What compiled means in this checklist
&lt;/h2&gt;

&lt;p&gt;GitLab does not execute your YAML as a raw script.&lt;br&gt;
It merges includes, applies inputs, and evaluates rules.&lt;br&gt;
Then it builds jobs for one pipeline source.&lt;/p&gt;

&lt;p&gt;A text view shows the file you happened to open.&lt;br&gt;
A remote shell shows the tree you happened to clone.&lt;br&gt;
Neither view is the compile that creates jobs.&lt;/p&gt;
&lt;h3&gt;
  
  
  Four receipts
&lt;/h3&gt;

&lt;p&gt;I ask for four receipts before I trust a draft.&lt;br&gt;
Those receipts are parse, includes, source, and runner identity.&lt;br&gt;
Miss one, and the green chat is still a guess.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 1: a clean parse means a valid pipeline
&lt;/h2&gt;

&lt;p&gt;Someone pastes the config into a chat window.&lt;br&gt;
The model says the indentation looks structurally fine.&lt;br&gt;
Why do we treat that note as CI lint?&lt;/p&gt;

&lt;p&gt;A YAML loader checks mapping shape, not job meaning.&lt;br&gt;
It does not understand &lt;code&gt;rules:&lt;/code&gt;, &lt;code&gt;needs:&lt;/code&gt;, or &lt;code&gt;trigger:&lt;/code&gt;.&lt;br&gt;
Your file can parse and still create zero jobs.&lt;/p&gt;
&lt;h3&gt;
  
  
  Corrected model
&lt;/h3&gt;

&lt;p&gt;Here is the corrected picture I use in review.&lt;br&gt;
Parse is only the first layer of evidence.&lt;br&gt;
It proves the text is YAML, not that jobs exist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposed local guard. Unexecuted example, not a project result.&lt;/span&gt;
python3 - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import sys
from pathlib import Path
try:
    import yaml
except ImportError:
    sys.exit('install pyyaml before you trust this parse')
path = Path('.gitlab-ci.yml')
if not path.exists():
    sys.exit('missing .gitlab-ci.yml')
data = yaml.safe_load(path.read_text())
if not isinstance(data, dict):
    sys.exit('top level is not a mapping')
print('parse_ok keys', ','.join(sorted(data)[:12]))
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command answers only one narrow question for you.&lt;br&gt;
Did a parser accept the top-level mapping in this tree?&lt;br&gt;
Ask project CI lint before you call the result a pipeline.&lt;/p&gt;

&lt;p&gt;Project CI lint is the compile receipt I accept for job creation.&lt;br&gt;
It is still not a full rules narrative on its own.&lt;br&gt;
I am not re-arguing that separate point in this note.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 2: seeing include paths means they expanded
&lt;/h2&gt;

&lt;p&gt;I open the file and spot several &lt;code&gt;include:&lt;/code&gt; entries.&lt;br&gt;
The model repeats those paths in a tidy list.&lt;br&gt;
Did it fetch the targets, or only echo strings?&lt;/p&gt;

&lt;p&gt;Includes may be local, project, remote, template, or component.&lt;br&gt;
A component also carries inputs and a version ref.&lt;br&gt;
Reading the key never expands the merged graph.&lt;/p&gt;

&lt;p&gt;An include path is a promise, not a fetched file.&lt;br&gt;
Expansion is a separate fetch plus a merge step.&lt;br&gt;
If that fetch fails, your local text is not the pipeline.&lt;/p&gt;
&lt;h3&gt;
  
  
  Illustration, not a catalog
&lt;/h3&gt;

&lt;p&gt;The next snippet is an illustration, not a live catalog.&lt;br&gt;
Do not fetch these hosts as if they were your project.&lt;br&gt;
Use them only to see three include shapes side by side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci/test.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acme/ci-templates&lt;/span&gt;
    &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/templates/go.yml&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v2.4.0&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.gitlab.com/acme/go-test@1.2.3&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;go_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.22'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Count those entries before you celebrate a draft.&lt;br&gt;
If the count is not zero, demand the merged YAML.&lt;br&gt;
Do not let a prose summary replace that artifact.&lt;/p&gt;
&lt;h2&gt;
  
  
  Myth 3: one shell run covers every source
&lt;/h2&gt;

&lt;p&gt;You ran &lt;code&gt;go test&lt;/code&gt; on the branch and it passed.&lt;br&gt;
Will the merge request pipeline select the same jobs?&lt;br&gt;
Will a nightly schedule select those same jobs again?&lt;/p&gt;

&lt;p&gt;Job &lt;code&gt;rules:&lt;/code&gt; can depend on the pipeline source.&lt;br&gt;
A push, a merge request, and a schedule are different events.&lt;br&gt;
The variable &lt;code&gt;CI_PIPELINE_SOURCE&lt;/code&gt; is how GitLab names that event.&lt;/p&gt;

&lt;p&gt;One green shell is one context, not every context.&lt;br&gt;
Name the source you actually rehearsed in the note.&lt;br&gt;
Lint again for the source you intend to merge.&lt;/p&gt;
&lt;h3&gt;
  
  
  Decision matrix
&lt;/h3&gt;

&lt;p&gt;I keep this decision matrix on the review comment.&lt;br&gt;
It separates a local rehearsal from a compiled pipeline.&lt;br&gt;
Read each row before you accept a green chat.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What a local shell can show&lt;/th&gt;
&lt;th&gt;What only GitLab compile can show&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Did the YAML parse?&lt;/td&gt;
&lt;td&gt;Yes, if you ran a parser here&lt;/td&gt;
&lt;td&gt;Re-check in project CI lint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did includes expand?&lt;/td&gt;
&lt;td&gt;Only if you fetched each target&lt;/td&gt;
&lt;td&gt;Merged YAML for this project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which source was simulated?&lt;/td&gt;
&lt;td&gt;The story you typed into rules&lt;/td&gt;
&lt;td&gt;The ref and source you lint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Will runner tags match?&lt;/td&gt;
&lt;td&gt;No, a shell is not registered&lt;/td&gt;
&lt;td&gt;The scheduler's runner match&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Myth 4: a free shell means my tags match
&lt;/h2&gt;

&lt;p&gt;This claim sounds practical, and it still fails.&lt;br&gt;
When a free server hands you a shell, commands can run.&lt;br&gt;
Your job may still declare tags and a resource group.&lt;/p&gt;

&lt;p&gt;A handed shell is not a registered GitLab runner.&lt;br&gt;
It has no tag set and no protected flag.&lt;br&gt;
The scheduler never consulted that remote shell session.&lt;/p&gt;

&lt;p&gt;Script execution and job scheduling are different problems.&lt;br&gt;
The shell can rehearse the script text you wrote.&lt;br&gt;
Only a matching runner can pick up the real job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;go-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;linux&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;resource_group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging-db&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;go test ./...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Can that job start on the free server session?&lt;br&gt;
No, that session is not your runner fleet.&lt;br&gt;
Use it to rehearse commands, not to prove a match.&lt;/p&gt;

&lt;p&gt;This is not a toolchain pin argument either.&lt;br&gt;
I am not asking which Go version sat on the shell.&lt;br&gt;
I am asking whether any runner would even take the job.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the free options fit
&lt;/h2&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;
&lt;h3&gt;
  
  
  Draft partner, not a compiler
&lt;/h3&gt;

&lt;p&gt;I use the free model access as a draft partner only.&lt;br&gt;
I ask it to list include kinds and tag tokens.&lt;br&gt;
I do not ask it to declare the pipeline valid.&lt;/p&gt;

&lt;p&gt;I use the free server option as a scratch shell.&lt;br&gt;
I clone the branch, parse the file, and print include kinds.&lt;br&gt;
I still mark that printout as local evidence, not a job log.&lt;/p&gt;

&lt;p&gt;This is the workflow I recommend, not a reported run.&lt;br&gt;
I am not stating a quota, a machine size, or a time limit.&lt;br&gt;
Those terms live in the current product notes, not here.&lt;/p&gt;

&lt;p&gt;If the shell vanishes, run the same census on your laptop.&lt;br&gt;
The checklist should still make sense without that host.&lt;br&gt;
Copy the script into the repo if you want it versioned.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposed include census. Unexecuted example, not a measured run.&lt;/span&gt;
python3 - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import sys
from pathlib import Path
try:
    import yaml
except ImportError:
    sys.exit('install pyyaml first')
data = yaml.safe_load(Path('.gitlab-ci.yml').read_text())
if not isinstance(data, dict):
    sys.exit('top level is not a mapping')
includes = data.get('include', [])
if includes is None:
    includes = []
if isinstance(includes, dict):
    includes = [includes]
if isinstance(includes, str):
    includes = [includes]
print('include_count', len(includes))
kinds = ('local', 'project', 'remote', 'template', 'component')
for item in includes:
    if isinstance(item, str):
        print('include_kind', 'local_shorthand')
    elif isinstance(item, dict):
        kind = next((k for k in kinds if k in item), 'unknown')
        print('include_kind', kind)
    else:
        print('include_kind', 'unexpected')
jobs = [
    k for k, v in data.items()
    if isinstance(v, dict) and 'script' in v
]
print('script_jobs_seen', len(jobs))
print('tagged_jobs_seen', sum(1 for k in jobs if data[k].get('tags')))
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposed receipt log. Unexecuted example, not a measured run.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eu&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; .gitlab-ci.yml
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOURCE_TO_SIMULATE&lt;/span&gt;:&lt;span class="p"&gt;=merge_request_event&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'source_to_simulate=%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_TO_SIMULATE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'runner_check=compare tags and resource_group with registered runners\n'&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'lint_status=pending until project CI lint is attached\n'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; | &lt;span class="nb"&gt;tee &lt;/span&gt;compile-receipts.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair the census with GitLab's own CI lint for the project.&lt;br&gt;
Use the lint flow documented for your current GitLab version.&lt;br&gt;
I will not freeze a CLI flag that your version may not ship.&lt;/p&gt;

&lt;h3&gt;
  
  
  Questions the lint must answer
&lt;/h3&gt;

&lt;p&gt;What should that lint answer before you merge?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the merged config valid inside this project?&lt;/li&gt;
&lt;li&gt;Which jobs exist for this ref and this source?&lt;/li&gt;
&lt;li&gt;Which warnings remain after the includes actually expand?&lt;/li&gt;
&lt;li&gt;Did you simulate the merge request, not only the branch?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you cannot answer those four questions, stop the merge.&lt;br&gt;
A confident paragraph is not a substitute receipt.&lt;br&gt;
Would you ship a binary you never linked?&lt;/p&gt;

&lt;h2&gt;
  
  
  The note I paste on the merge request
&lt;/h2&gt;

&lt;p&gt;I want the review comment to stay boring and specific.&lt;br&gt;
No pep talk, and no model transcript pasted in.&lt;br&gt;
Just list the receipts that are still missing today.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checklist text
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pipeline compile checklist
- [ ] YAML parse ran in this repo, not only in chat
- [ ] Include count is recorded and merged YAML is attached
- [ ] CI lint simulated the pipeline source we will use
- [ ] Job tags and resource_group were checked against real runners
- [ ] Free shell output is marked as rehearsal, not a job log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Would I block a merge when this list is empty?&lt;br&gt;
Yes, when the change touches pipeline config or an include.&lt;br&gt;
A dropped include should not wait for a later patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this guard refuses to prove
&lt;/h2&gt;

&lt;p&gt;The census script does not expand remote includes for you.&lt;br&gt;
It does not evaluate rules or variable precedence either.&lt;br&gt;
It does not query your runner fleet or resource groups.&lt;/p&gt;

&lt;p&gt;Default PyYAML may reject GitLab tags such as reference.&lt;br&gt;
A parse error there can be the tool, not your pipeline.&lt;br&gt;
That is another reason the project lint still wins.&lt;/p&gt;

&lt;p&gt;The census counts jobs that contain a script key only.&lt;br&gt;
Jobs that only use extends or trigger can be missed.&lt;br&gt;
Treat the numbers as a census, not a job inventory.&lt;/p&gt;

&lt;p&gt;Protected variables and file variables stay inside GitLab.&lt;br&gt;
A free shell should not receive those values for a demo.&lt;br&gt;
If a draft asks you to paste a token, stop and refuse.&lt;/p&gt;

&lt;p&gt;Component catalogs move when you track a floating ref.&lt;br&gt;
Pin a version and re-lint after you bump that pin.&lt;br&gt;
This script does not resolve &lt;code&gt;spec:inputs&lt;/code&gt; on its own.&lt;/p&gt;

&lt;p&gt;GitLab's compiler remains the authority for job creation.&lt;br&gt;
Read the current CI lint and include docs before you automate a gate.&lt;br&gt;
I am not pinning a click path that the UI may rename.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pages I expect you to confirm
&lt;/h2&gt;

&lt;p&gt;GitLab documents includes, CI lint, and predefined variables in its docs.&lt;br&gt;
I am linking the public paths I expect you to confirm.&lt;br&gt;
Start with CI lint and the include reference before you automate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ci/lint/" rel="noopener noreferrer"&gt;CI lint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ci/yaml/includes/" rel="noopener noreferrer"&gt;Include merging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ci/variables/predefined_variables/" rel="noopener noreferrer"&gt;Predefined variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/ci/yaml/#tags" rel="noopener noreferrer"&gt;&lt;code&gt;tags&lt;/code&gt; keyword&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who should not use this approach
&lt;/h2&gt;

&lt;p&gt;Skip this if the repository has no &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; at all.&lt;br&gt;
Skip it when a platform gate already stores merged YAML.&lt;br&gt;
Skip it when you need a signed release attestation instead.&lt;/p&gt;

&lt;p&gt;Also skip it for shared runner capacity planning.&lt;br&gt;
A free shell says nothing about minutes or queue time.&lt;br&gt;
That question belongs on a different sheet entirely.&lt;/p&gt;

&lt;p&gt;Do not register the free server as a production runner.&lt;br&gt;
Do not store secrets in the chat that drafted the job.&lt;br&gt;
Do not merge because the model sounded sure today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Close the loop on the next review
&lt;/h2&gt;

&lt;p&gt;Next time a draft says the pipeline looks valid, ask for receipts.&lt;br&gt;
Run the parse and the include census on the free server, or locally.&lt;br&gt;
Then let GitLab's CI lint close the argument for that source.&lt;/p&gt;

&lt;p&gt;Want a second pass on the checklist wording before review?&lt;br&gt;
Free model access is enough to start that pass.&lt;br&gt;
Keep the verdict in the lint result, not in the chat log.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>devops</category>
      <category>cicd</category>
      <category>testing</category>
    </item>
    <item>
      <title>FAQ: A Model Patch Is Not a GitLab Review</title>
      <dc:creator>Jordan Huang</dc:creator>
      <pubDate>Sun, 11 Oct 2026 09:20:47 +0000</pubDate>
      <link>https://dev.to/gitlab_3188/faq-a-model-patch-is-not-a-gitlab-review-2daf</link>
      <guid>https://dev.to/gitlab_3188/faq-a-model-patch-is-not-a-gitlab-review-2daf</guid>
      <description>&lt;p&gt;I keep reading merge requests that tell a bigger story than the diff. Someone asked a model to repair a red job. The patch looks tidy, so the review stops there.&lt;/p&gt;

&lt;p&gt;Would you approve a comment that never touched the repo? I would not treat chat prose as a merge. GitLab will not treat it that way either.&lt;/p&gt;

&lt;p&gt;This FAQ is the mental model I want before anyone hits merge. Each myth is a claim developers still repeat in reviews. Each fix points at a file or a project setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this FAQ refuses to pretend
&lt;/h2&gt;

&lt;p&gt;I will not invent a model name, a quota, or a hardware bill. I will not invent a session length or a permanence promise. I was not given those facts, so I will not fake them.&lt;/p&gt;

&lt;p&gt;The operator supplied two availability facts about MonkeyCode, and only those. Free model access is available, and a free server option is available. Those facts do not make a reviewed pipeline by themselves.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. I would use that access to draft checks, not to approve merges. I would use the free server option as scratch space only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 1: A tidy YAML patch is a reviewed pipeline
&lt;/h2&gt;

&lt;p&gt;People say the model read the log, so the YAML is reviewed. A fluent diff is still just text sitting in a chat pane. Did anyone open the pipeline editor and compare the stages?&lt;/p&gt;

&lt;p&gt;GitLab runs the file in the repository, not the paragraph above it. If the patch never lands on a branch, the project did not change. I want the branch commit, not a screenshot of the suggestion.&lt;/p&gt;

&lt;p&gt;Corrected model: a review starts when the YAML is committed and linted. Chat is a draft tray, not the project contract you merge. The committed file is the contract your runners will execute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lint the file, not the vibe
&lt;/h3&gt;

&lt;p&gt;I treat schema lint as the first gate, not the model tone. GitLab documents CI Lint for pipeline files in the project. You can also call the lint API with a real project token.&lt;/p&gt;

&lt;p&gt;I keep the payload in a file so secrets stay out of history. The host and project id should come from the environment. If lint fails, the model draft is still only a draft.&lt;/p&gt;

&lt;p&gt;Read the current CI Lint docs before you trust this curl shape. Endpoints move, and I will not freeze a stale path as law. The snippet below is a proposal, not a measured run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposal only. Do not paste a real token into any chat.&lt;/span&gt;
&lt;span class="c"&gt;# Confirm the path in GitLab's current CI Lint API docs.&lt;/span&gt;
curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; @ci-lint-payload.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GITLAB_HOST&lt;/span&gt;&lt;span class="s2"&gt;/api/v4/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;/ci/lint"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I inspect the cached diff before I push the scratch branch. A chat paste can hide a line the diff will show. If the diff echoes a token, I stop and rotate it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; ci-evidence-scratch
git add .gitlab-ci.yml
git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; .gitlab-ci.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Myth 2: "Tests passed" in prose is a test report
&lt;/h2&gt;

&lt;p&gt;I still see job logs pasted under a heading called evidence. A log line can be edited, truncated, or never uploaded. Would you bet a release decision on a single sentence?&lt;/p&gt;

&lt;p&gt;GitLab's YAML reference separates job logs from report artifacts. The junit report key is how test summaries reach the merge request. A green echo does not create that report file.&lt;/p&gt;

&lt;p&gt;Corrected model: evidence is a file path GitLab can ingest. Prose is narration, and narration can sound finished. The XML can still be missing while the story sounds done.&lt;/p&gt;

&lt;p&gt;I set when always so a failed job still uploads the report. I set expire_in myself instead of trusting an unspoken default. I do not cite a default duration, because defaults can move.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Unexecuted example. Confirm keys in the current CI YAML reference:&lt;/span&gt;
&lt;span class="c1"&gt;# https://docs.gitlab.com/ci/yaml/&lt;/span&gt;
&lt;span class="na"&gt;unit_tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mkdir -p reports&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python -m pytest --junitxml=reports/junit.xml&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;expire_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7 days&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;reports/junit.xml&lt;/span&gt;
    &lt;span class="na"&gt;reports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;junit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reports/junit.xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I read the &lt;a href="https://docs.gitlab.com/ci/jobs/job_artifacts/" rel="noopener noreferrer"&gt;job artifacts docs&lt;/a&gt; before I trust an upload story. I read the &lt;a href="https://docs.gitlab.com/ci/yaml/" rel="noopener noreferrer"&gt;YAML reference&lt;/a&gt; before I copy keys from chat. I treat pytest junitxml as a runner detail, not a GitLab feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 3: Free model access is a safe vault for CI variables
&lt;/h2&gt;

&lt;p&gt;Someone says the prompt is private, so the token is fine. Is a convenience box really a vault for credentials? I do not grant a chat box that title.&lt;/p&gt;

&lt;p&gt;GitLab documents protected variables for protected branches and tags. Confirm the current &lt;a href="https://docs.gitlab.com/ci/variables/" rel="noopener noreferrer"&gt;variables page&lt;/a&gt; before you rely on that limit. Masking is a log filter, not a license to print values.&lt;/p&gt;

&lt;p&gt;Corrected model: secrets live in GitLab or a real secret store. A prompt is a copy, and copies have a habit of multiplying. I will not trade a red job for a leaked token.&lt;/p&gt;

&lt;p&gt;I do not paste CI_JOB_TOKEN into a free model prompt. I do not paste masked variables just to help a debug session. I do not paste customer data so a sample patch looks real.&lt;/p&gt;

&lt;p&gt;The free server option does not weaken that copying rule. A scratch shell is still a machine you do not administer. If I cannot name its retention, I will not feed it secrets.&lt;/p&gt;

&lt;h3&gt;
  
  
  A dotenv file can smuggle what the prompt should not
&lt;/h3&gt;

&lt;p&gt;Models love to emit KEY equals VALUE and call it config. GitLab can import dotenv reports into later jobs. Would you let a later job inherit a guessed secret?&lt;/p&gt;

&lt;p&gt;I want dotenv keys to be build metadata, never credentials. A bad dotenv line is not a convenience. It is a second copy with a job scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 4: A free server transcript is a merge request artifact
&lt;/h2&gt;

&lt;p&gt;The transcript says the script exited zero on a free server. People attach that transcript and call the pipeline finished. Where is the artifact entry in the job sidebar?&lt;/p&gt;

&lt;p&gt;GitLab artifacts are files uploaded by a job the project ran. A foreign shell transcript is not an artifact record in GitLab. Cache is not an artifact, and a cache hit can miss later.&lt;/p&gt;

&lt;p&gt;Corrected model: no widget, no report, no argument from me. A transcript is a story about a run you did elsewhere. Stories are not downloads your reviewer can open later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Myth 5: Model confidence replaces rules, needs, and approvals
&lt;/h2&gt;

&lt;p&gt;I hear that the model checked every branch, so rules are noise. Confidence is not a rules block in your pipeline file. Confidence is not needs, and it is not an approval rule.&lt;/p&gt;

&lt;p&gt;allow_failure true can keep a pipeline green while a job failed. needs can start a job without waiting for every earlier stage. Approval rules live in project settings, not in a chat score.&lt;/p&gt;

&lt;p&gt;Corrected model: control flow is YAML plus project settings. Tone is not control flow, no matter how certain it sounds. I read the policy files before I trust the adjective.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order I trust more than a fluent patch
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Commit the YAML on a branch you can throw away.&lt;/li&gt;
&lt;li&gt;Run CI Lint against that committed file, not the chat.&lt;/li&gt;
&lt;li&gt;Produce a junit file from the test command you really run.&lt;/li&gt;
&lt;li&gt;Run the local evidence check against those files on disk.&lt;/li&gt;
&lt;li&gt;Push and confirm the merge request shows the test report.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A local check on files, not on vibes
&lt;/h2&gt;

&lt;p&gt;This script is a proposal you can run on a local folder. I have not published timings, pass rates, or hardware notes. It checks files on disk, not a live shared runner.&lt;/p&gt;

&lt;p&gt;A pass means the files exist and look roughly shaped right. A pass does not mean GitLab accepted the report upload. I still want CI Lint, then a pipeline on a branch I control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# ci-evidence-check.sh - proposal, not a published benchmark.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$root&lt;/span&gt;&lt;span class="s2"&gt;/.gitlab-ci.yml"&lt;/span&gt;
&lt;span class="nv"&gt;junit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$root&lt;/span&gt;&lt;span class="s2"&gt;/reports/junit.xml"&lt;/span&gt;
&lt;span class="nv"&gt;dotenv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$root&lt;/span&gt;&lt;span class="s2"&gt;/reports/build.env"&lt;/span&gt;

fail&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'FAIL: %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$yaml&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;"missing .gitlab-ci.yml"&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$junit&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;"missing reports/junit.xml"&lt;/span&gt;

&lt;span class="c"&gt;# -q so a match does not print a secret into the terminal.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'CI_JOB_TOKEN|echo .*(SECRET|TOKEN|PASSWORD)'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$yaml&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;fail &lt;span class="s2"&gt;"yaml looks ready to print a secret; open the file locally"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'reports:'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$yaml&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;"yaml has no artifacts reports block"&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'junit:'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$yaml&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;"yaml does not point at a junit report"&lt;/span&gt;

&lt;span class="c"&gt;# Minimal XML sniff. This is not a full JUnit schema validation.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;testsuite'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$junit&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;"junit xml has no testsuite element"&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/testsuites&amp;gt;|&amp;lt;/testsuite&amp;gt;'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$junit&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;"junit xml looks truncated"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dotenv&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; line&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'#'&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^[A-Z_][A-Z0-9_]&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;"dotenv line is not KEY=VALUE form"&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;TOKEN&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;SECRET&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;PASSWORD&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;PRIVATE&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; fail &lt;span class="s2"&gt;"dotenv looks like a secret"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dotenv&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'PASS: files look like evidence, not like prose\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That invocation is a usage sketch, not a recorded lab result. If the script fails, fix the files before you debate tone. If it passes, you still owe GitLab a real pipeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; reports
bash ci-evidence-check.sh &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The table I keep beside the editor
&lt;/h2&gt;

&lt;p&gt;I keep this table beside the editor when a patch arrives as prose. Each row names the claim, the demand, and the refusal. If a row fails, I do not argue from the model's tone.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim I hear&lt;/th&gt;
&lt;th&gt;What I demand instead&lt;/th&gt;
&lt;th&gt;Why I refuse the shortcut&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The model rewrote the job&lt;/td&gt;
&lt;td&gt;Committed YAML plus CI Lint&lt;/td&gt;
&lt;td&gt;Chat text is not the repo file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;It said tests passed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reports/junit.xml&lt;/code&gt; on the job&lt;/td&gt;
&lt;td&gt;Logs are not report artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The prompt can hold the token&lt;/td&gt;
&lt;td&gt;Variables in project settings&lt;/td&gt;
&lt;td&gt;A prompt is a copy, not a vault&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The free server exited zero&lt;/td&gt;
&lt;td&gt;Artifact uploaded by the project job&lt;/td&gt;
&lt;td&gt;Foreign transcripts are not downloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The model felt sure about rules&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rules&lt;/code&gt;, &lt;code&gt;needs&lt;/code&gt;, approval settings&lt;/td&gt;
&lt;td&gt;Tone is not control flow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A generated dotenv is harmless&lt;/td&gt;
&lt;td&gt;Metadata keys only, no credentials&lt;/td&gt;
&lt;td&gt;Later jobs can inherit the file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Who should skip this, and what it cannot prove
&lt;/h2&gt;

&lt;p&gt;Skip this approach if your pipeline has no tests to report. Skip it if you cannot keep secrets out of prompts and shells. Skip it if you need a signed release attestation, not a FAQ.&lt;/p&gt;

&lt;p&gt;This check does not validate a full JUnit schema. This check does not prove runner tags, images, or digest pins. Those are separate gates, and I am not repeating them here.&lt;/p&gt;

&lt;p&gt;The greps are heuristics, and comments can fool them. Treat a pass as a smell test, not as a security audit. A green local pass is still not a merge approval.&lt;/p&gt;

&lt;p&gt;Who should use it: reviewers staring at a fluent CI patch. Use it when the author cites a model patch as the proof. Do not use it as a compliance program or a quota claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would ask for, and nothing more
&lt;/h2&gt;

&lt;p&gt;I would ask a free model for a checklist and a hostile reading. I would ask it to flag allow_failure and secret echoes. I would not ask it to bless a merge request.&lt;/p&gt;

&lt;p&gt;A free server can hold the script while I poke sample files. I would not point that shell at production runners or secrets. I would not store release artifacts on a scratch box.&lt;/p&gt;

&lt;p&gt;If scratch space would help, MonkeyCode is the option I would open. Then close that tab and prove the files inside GitLab. I would not let the draft close the review by itself.&lt;/p&gt;

&lt;p&gt;So ask the rude question before you click merge. Which file will GitLab ingest when the chat tab is closed? If you cannot name that file, you do not have evidence.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>cicd</category>
      <category>testing</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
