.github/workflows/first-turn.yml (additions)
A loop that cannot be stopped is not an engine โ it is a runaway cart. Before your serpent grows teeth (golems, fix lanes, self-merges), it must learn the Wardens' first law: everything autonomous is OFF until a named switch turns it on, and it stops the instant that switch is unset. Today you build the gate itself โ and then you meet the failure that killed the real engine three mornings straight without producing a single log line.
The real-world skills: opt-in automation via repository variables, go/no-go gate jobs, least-privilege permissions, and the GitHub Actions rule that a caller job's permissions cap every job in a workflow it calls โ validated at trigger time, where no linter can save you.
๐งญ Campaign note: Level
0101is the Factory โ CI/CD & DevOps โ exactly where a gate belongs. The campaign's chapters climb its difficulty ladder, not the level order.
๐ The Legend Behind This Quest¶
When the quest-perfection engine first shipped, its conductor never played a note: three scheduled mornings in a row ended in startup_failure โ zero jobs, zero logs, only a single sentence on the run's page. The fix was two lines. The lesson was permanent: some laws are enforced by the realm itself, at the gate, before any of your code runs โ and your loop must be designed so that "off" is a clean, silent, zero-cost state, not an error.
๐ฏ Quest Objectives¶
By the end of this quest you will:
- Add a kill switch โ a
LOOP_ENABLEDrepository variable your workflow checks before doing anything - Build a gate job โ one cheap job that outputs
go=true/false; every other job needs it - Apply least privilege โ workflow-level
permissions: contents: read, elevated only where written proof requires it - Trigger and read a
startup_failureโ and learn whygh run view --log-failedreturns nothing for it - Grant caller permissions to a called workflow โ the two-line fix for the whole class
๐บ๏ธ Quest Prerequisites¶
- ๐ Chapter I's
potion-bookrepo with a green First Turn run - ๐ 15 minutes of patience for GitHub's scheduler (or use
workflow_dispatchlevers)
๐งโโ๏ธ Chapter 1: The Kill Switch and the Gate Job¶
The gate pattern is one cheap job that decides go or no-go โ and a workflow whose every other job refuses to start without it. Add this to your First Turn workflow:
{% raw %}
# .github/workflows/first-turn.yml (additions)
permissions:
contents: read # the workflow-wide floor: read-only
jobs:
gate:
runs-on: ubuntu-latest
outputs:
go: ${{ steps.check.outputs.go }}
steps:
- id: check
env:
ENABLED: ${{ vars.LOOP_ENABLED }}
run: |
set -euo pipefail
go=false
if [ "$ENABLED" = "true" ]; then
go=true
else
echo "loop idle (LOOP_ENABLED=$ENABLED) โ flip the variable to arm."
fi
echo "go=$go" >> "$GITHUB_OUTPUT"
turn:
needs: gate
if: ${{ needs.gate.outputs.go == 'true' }}
permissions:
contents: write # elevated ONLY here โ this job commits the ledger
runs-on: ubuntu-latest
steps:
# โฆ your Chapter I steps unchanged โฆ
Arm and disarm it like a warden, and watch both behaviors:
gh variable set LOOP_ENABLED --body true # armed
gh workflow run first-turn.yml && gh run watch
gh variable delete LOOP_ENABLED # disarmed
gh workflow run first-turn.yml && gh run watch # gate: success, turn: skipped
The disarmed run is the important one: green, silent, zero side effects. An automation that errors when disabled trains humans to ignore red โ the Sentinel's Horn fatigue rule applies to CI too.
โ๏ธ Skills You'll Forge¶
- The
vars.*context (repository variables) vssecrets.*โ switches are variables, credentials are secrets - Job outputs โ
needs.<job>.outputs.<name>โ how gate verdicts travel - Per-job
permissionselevation over a read-only floor
๐ Knowledge Check¶
- Why check the switch in a job instead of skipping the whole workflow with a repository setting?
- When you later add auth (Chapter III), the gate should require the switch and the secret. Why both?
๐งโโ๏ธ Chapter 2: The Error With No Logs โ startup_failure¶
Some failures happen before any job exists. GitHub validates the whole workflow graph at trigger time; if the graph itself is illegal, the run is stamped startup_failure โ no jobs, no logs, and gh run view --log-failed shrugs. The real engine hit exactly this, three days straight, with this shape:
{% raw %}
# The trap: a workflow-level read-only floorโฆ
permissions:
contents: read
jobs:
fix:
uses: ./.github/workflows/quest-fix.yml # โฆcalling a workflow whose jobs
secrets: inherit # request contents:write โ DENIED
The realm's law: a caller job's permissions cap every job in the workflow it calls. The called workflow requested contents: write, pull-requests: write; the caller granted contents: read. Verdict, rendered at the gate, before the gate job itself could even run:
Invalid workflow file โฆ
The nested job 'fix' is requesting 'contents: write, pull-requests: write',
but is only allowed 'contents: read, pull-requests: none'.
The two-line cure โ grant on the caller job exactly what the called jobs request:
{% raw %}
fix:
permissions:
contents: write
pull-requests: write
uses: ./.github/workflows/quest-fix.yml
secrets: inherit
Two survival notes from the same battle:
- actionlint cannot catch this. The rule is validated server-side. Lint locally, but smoke-test the graph by dispatching with inputs that make the expensive jobs no-op (the real engine uses a
max_slices: 0dispatch input for exactly this). - The error lives only on the run's web page. When a scheduled run shows
startup_failurewith zero jobs, open it in the browser โ the one-sentence verdict is printed there and nowhere else.
๐ Knowledge Check¶
- Why does GitHub validate called-workflow permissions at trigger time instead of when the job starts?
- Your gate job never ran during the startup failures. What does that tell you about where in the lifecycle graph validation sits?
๐ Reproduce It¶
Study the real two-line fix and its battle report:
- PR #422 โ
bamr87/it-journey@cfeb5e3aa(+24/โ2): the caller-permissions grant, the artifact-name fix, and themax_slices=0smoke-test pattern, written up after three silentstartup_failuremornings - The gate pattern at full scale:
gate:jobs in.github/workflows/quest-perfection.yml,quest-fix.yml, andquest-walkthrough.ymlโ every lane carries its own*_ENABLEDswitch (QUEST_PERFECTION_ENABLED,QUEST_FIX_ENABLED, โฆ), so a human can kill one arm without silencing the others
๐ฎ Mastery Challenge¶
Objective: stage-gate your potion book like a real fleet.
Success Criteria:
- [ ] Split your loop into two lanes โ first-turn.yml (check + ledger) and a stub potion-fix.yml reusable workflow with its own FIX_ENABLED gate โ and call the stub from the first lane
- [ ] Deliberately reproduce the startup_failure (call the stub while its jobs request contents: write under a read-only caller), read the error on the run page, then fix it with the caller grant
- [ ] Prove both kill switches work independently: walk-only mode (LOOP_ENABLED=true, FIX_ENABLED unset) runs the check and skips the fix lane cleanly
๐ Rewards & Progression¶
- ๐ฆ Warden of the Gate โ earned when your disarmed loop runs green, silent, and free
- โก Skills unlocked: gate jobs ยท kill switches ยท least privilege ยท startup-failure forensics
- ๐ +75 XP
๐บ๏ธ Quest Network¶
graph LR
Prev[๐ I ยท The First Turn] --> Cur[๐ฆ II ยท The Warden's Gate]
Cur --> Next[๐ค III ยท Summon the Golem]
click Prev "/quests/0000/ouroboros-loop-01-the-first-turn/"
click Next "/quests/0011/ouroboros-loop-03-summon-the-golem/"
๐ฎ Next Adventures¶
- ๐ค Chapter III โ Summon the Golem: a Claude Code agent joins the loop, under oath
- ๐ Campaign hub: Epic Quest: The Ouroboros Loop
๐ Resource Codex¶
- GitHub Actions: workflow permissions
- GitHub Actions: reusing workflows โ the caller/called permission rules
- actionlint โ catches much, but not server-side graph law
๐ธ๏ธ Knowledge Graph¶
Structured wiki-links connect this quest to the IT-Journey knowledge graph.
Campaign hub: [[Epic Quest: The Ouroboros Loop]] Previous: [[The First Turn]] ยท Next: [[Summon the Golem]] Level home: [[Level 0101 - CI/CD & DevOps]]