fix: runs_on parameterization + defensive newline normalization (v0.2.0 slices 1+2) #5
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!5
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/3-runs-on-parameterization"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
v0.2.0 first + second slices per release-toolkit#3. SemVer-precedent settled at α per operator: required-input addition is breaking → minor bump (v0.2.0) rather than patch (v0.1.1).
What's in the PR (TWO SLICES bundled)
Slice 1 (commit
64adc7b— Surveyor APPROVED at ee98)runs_onas required workflow_call input (no default); reference${{ inputs.runs_on }}in jobstests/workflows.bats: schema-regression test verifyingruns_onis present + required + referenceddocs/integration.md: all 4 consumer-wiring examples +runs_on: go; "About the runs_on parameter" subsectionchangelog.d/3.fixed.md: fragmentSlice 2 (commit
f490312— Surveyor RE-STAMP APPROVED at f71f)scripts/lib/fragments.sh: new_normalize_paragraph_continuationsawk state machine;categorize_fragmentspipes each fragment body through ittests/fragments.bats: 7 new tests (40/40 fragments tests green)docs/conventions.md: convention flagged as "defensive from v0.2.0"changelog.d/3.fixed-2.md: second fragmentVersion-label propagation (commit
8f14569— mechanical s/v0.1.1/v0.2.0/g)v0.1.1references across integration.md / conventions.md / fragmentsv0.2 → v0.3(since v0.2 is now used by this fix)dockerlabel" requirement (artifact of the v0.1.0 bug this fix closes)Per Surveyor's f71f scope framing: the version-label propagation changes CUT-VERSION LABELS, not code substance. Code-stamp at
f490312holds.BREAKING (for v0.1.0 consumers)
The
runs_onrequired input addition is technically breaking. No consumers existed at v0.1.0 release time, so practical blast radius is zero.Acceptance criteria
runs_ondeclared as required workflow_call input${{ inputs.runs_on }}docs/integration.mdupdated with new examples + explanation + prerequisite correctionchangelog.d/3.fixed.mdfragment added_normalize_paragraph_continuationsawk state machine handles all relevant cases (40/40 fragments tests green)categorize_fragmentspipes each fragment through normalizerdocs/conventions.mdrewritten to flag convention as defensive (v0.2.0)changelog.d/3.fixed-2.mdfragment addedWhat this PR does NOT do
.forgejo/workflows/release.yml+release-draft.yml+manifest-check.ymlfor v0.2.1+ dogfood) — separate small follow-up PRCross-tracker
— QM, 2026-06-24, per operator α ratification.
Closes release-toolkit#3 first half. The 4 reusable workflows (_release-prep / _release-draft / _release-publish / _manifest-check) v0.1.0 hard-coded `runs-on: docker`, which isn't a valid label in the alcatraz Forgejo runner config (available labels: dotnet / godot / python / go / playwright / squadron-test / docker-build / squadron-deploy). The reusables would never schedule. Surfaced when starting cellblock greenfield adoption (cellblock#162). My oversight at v0.1.0 authoring-time; should have grepped the runner config before specifying. ## Change Each reusable now declares `runs_on` as a required workflow_call input (no default). Workflows reference `runs-on: ${{ inputs.runs_on }}`. Consumers MUST specify `runs_on: <label>` matching a label in their Forgejo runner config. ## BREAKING (for any v0.1.0 consumer) No consumers existed at v0.1.0 release time, so practical blast radius is zero. v0.1.1 documents the required input as part of the fix's CHANGELOG entry; integration.md consumer-wiring examples updated to include `runs_on: go` in every `with:` block. ## Tests tests/workflows.bats — new test "every reusable has runs_on as a required workflow_call input" verifies: - runs_on input exists in workflow_call.inputs - runs_on declared as required: true - every non-uses job references `${{ inputs.runs_on }}` in runs-on 7/7 workflow schema tests green. ## Documentation docs/integration.md: - All 4 consumer-wiring example blocks updated with `runs_on: go` - Workflow @ref bumped from @v0.1.0 to @v0.1.1 in the examples - New section "About the runs_on parameter" explaining why no default + how to pick a label ## Changelog changelog.d/3.fixed.md — fragment documenting the fix for the v0.1.1 release-prep cut. Tracker: frankenbit/release-toolkit#3 (v0.1.1 scope)✅ APPROVED — runs_on parameterization (v0.1.1 first slice)
Correct fix for a genuinely broken v0.1.0 feature, well-tested and honestly documented. Verified independently.
The fix — correct across all 4 reusables
Each of
_release-prep/_release-draft/_release-publish/_manifest-checkaddsruns_on(required, string, with a clear description) and swaps the hard-codedruns-on: docker→runs-on: ${{ inputs.runs_on }}. Confirmed all four in the diff. Thedocker-label-never-schedules diagnosis is right — a job pinned to a non-existent runner label sits unscheduled forever.Verified — ran the suite myself
Cloned the PR head and ran
bats tests/workflows.bats→ 7/7 green, including the new test 6 ("every reusable has runs_on as a required workflow_call input"). The test is comprehensive — it checks input-present and required:true and each non-usesjob'sruns-onreferencesinputs.runs_on, so it guards both halves (declaration + wiring). Nice touch handling the YAMLon:→Trueboolean gotcha (doc.get("on", doc.get(True, {}))) — that bites a lot of GHA/Forgejo YAML parsers.Docs — self-sufficient (and the genericizing is right)
The integration.md "About the
runs_onparameter" section explains the no-default decision + the rationale (runner labels vary per deployment; a wrong default = unscheduled-forever) + practical guidance ("any runner with bash+git+jq+curl; your other workflows'runs-onis usually the answer"). And the changelog fragment correctly genericizes to "most Forgejo runner configurations — including the one this toolkit was developed against" rather than naming alcatraz. (The lone "alcatraz" mention is in a test comment — internal-dev-facing, not the external-adopter surface — so that's fine.)One non-blocking observation: the patch-vs-minor SemVer call (first dogfood of your own policy)
The fragment honestly labels this "BREAKING for v0.1.0 consumers (blast radius zero)" — exactly the right honesty. The question it raises: a self-labeled BREAKING change shipping as a patch (v0.1.1) is counter to most SemVer intuition (breaking → minor/major). Your own SemVer policy threads it though — "patch bumps avoid removing or renaming public surfaces", and this adds a required input (doesn't remove/rename), so it's policy-letter-consistent — and with zero consumers + a non-functional v0.1.0 feature, the practical case for patch is airtight (you're fixing a broken feature, not breaking a working one). So I'm not asking you to change the bump. But since this is the toolkit's first dogfood of its own SemVer policy, it's worth a deliberate confirm: is "a new required input is patch-eligible because it's an addition" the intended reading? In a future world with consumers, someone pinned
@v0.1.0who bumps to@v0.1.1expecting patch-safety would break — so if that reading holds, a one-line policy clarification ("required-input additions are breaking-but-patch-eligible pre-1.0") would make the precedent explicit rather than implicit. Moot for blast radius today; worth nailing down while you're the only consumer.Clean slice. Closes the runs_on gap that blocks cellblock's consumer-workflows. Merge-ready → Bosun.
Closes release-toolkit#3 second slice. Author convention from v0.1.0's docs/conventions.md was source-side advisory ("write single-line bullets, don't hard-wrap"); v0.1.1 makes it defensive — authors who slip get correct output anyway. ## Change scripts/lib/fragments.sh: - New `_normalize_paragraph_continuations` helper — awk state machine that collapses indented continuation lines into single-line paragraphs while preserving blank-line separators, code fences (including indented fences inside bullets), and column-0 unit boundaries - `categorize_fragments` now pipes each fragment body through the normalizer instead of `cat`-ing verbatim ## State-machine rules - Lines at column 0 start a new "unit" (bullet, heading, paragraph) - Indented lines (whitespace + content) are continuation of the current unit; joined to previous content with a single space - Blank lines flush current unit + are emitted as paragraph separators - Fenced code blocks (``` ... ```) pass through verbatim; closing fence must match opening indent ## Tests tests/fragments.bats — 7 new tests covering: - Hard-wrapped single-paragraph bullet → joins into one line - Already-clean single-line bullet → unchanged - Multiple bullets without internal wraps → stay separate - Multi-paragraph bullet → first paragraph joined, blank-line preserved, second paragraph joined with indent preserved - Indented code fence inside bullet → fence + content preserved verbatim - Top-level code fence at column 0 → preserved verbatim - Single fragment kind heading → preserved as `### Kind\n\n- bullet` 40/40 fragments tests green. Shellcheck clean. ## Empirical motivation Operator's hard-wrap framing during v0.1.0 cut: bullets hard-wrapped at ~70 chars rendered with literal line breaks through some pipelines (GFM-soft-break-as-`<br>` renderers, RSS/email/notifications, plain-text copy-paste). v0.1.0 shipped with source-side convention documented in docs/conventions.md; v0.1.1 adds the defensive tool- side normalization so consumers who hard-wrap by accident still ship clean release notes. ## Convention update docs/conventions.md `### Single-line bullets — the no-hard-wrap convention` subsection rewritten to flag the convention as "defensive from v0.1.1" rather than purely advisory. Documents what the normalizer preserves verbatim + what it joins. ## Fragment changelog.d/3.fixed-2.md documents the fix for the v0.1.1 release- prep cut (second fragment alongside 3.fixed.md from the runs-on parameterization slice). Tracker: frankenbit/release-toolkit#3 (v0.1.1 second slice) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyHfix(workflows): runs_on required input — drop hard-coded docker labelto fix: runs_on parameterization + defensive newline normalization (v0.1.1 slices 1+2)✅ RE-STAMP APPROVED (bundled slice-1 + slice-2) at
f490312Re-reviewed after the slice-2 bundle. Both slices verified; the bundling was handled exactly right.
On the bundling decision (advisory) — clean, because you handled it right
The substrate-honest disclosure IS the discipline: you deviated from the stated "separate PR," recognized it invalidated my
64adc7bapproval, and re-requested rather than letting the stamp silently cover unreviewed content. That's the no-silent-scope-expansion rule, honored. And the bundle is provably clean to re-review because the two slices are file-disjoint — I diffed 64adc7b→f490312 and confirmed slice-2 touches onlyfragments.sh/fragments.bats/conventions.md/ the changelog fragment; nothing in slice-1's surface (.forgejo/workflows/,integration.md,workflows.bats) moved. So slice-1 is byte-identical and my ee98 approval of the runs_on work stands unchanged; slice-2 is purely additive on top. Bundling two related, clean v0.1.1 slices is a fine call given that disclosure — a separate PR would only have saved this re-stamp, which is cheap. No concern.Slice-2 (newline normalization) — verified
_normalize_paragraph_continuationsawk state machine is well-built. I exercised it directly: a hard-wrapped bullet collapses to one line (- A bullet that is hard-wrapped across lines), fenced code passes verbatim (internal spacing + indent preserved), and the subtle case works — an indented fence inside a bullet is matched by its opening indent and passed through verbatim rather than mangled as continuations. The indent-aware fence-close (only matches at the opening indent; mismatched```inside is content) is the right call for the bullets-with-code case.fragments.bats(incl. the 7 new normalization cases), 7/7workflows.bats(slice-1 still green on the bundled head),shellcheck scripts/lib/fragments.shexit 0.conventions.md"defensive from v0.1.1 (was advisory)" framing is accurate — the normalization makes the single-line-bullet convention forgiving of accidental hard-wraps, which is the right posture for a shared toolkit (authors shouldn't ship broken release notes over a wrap they didn't notice).Note
This re-stamp is on the code (both slices) and is independent of the still-pending α/β SemVer-precedent call (Bosun + operator) — that decision changes the bump number + the
@vXstrings in integration.md/fragment/commits, not the substance I verified here. Whichever way it lands, the slices are sound.Merge-ready → Bosun (after the SemVer-precedent settles, since it touches the version strings).
Per operator α ratification: SemVer-precedent settles toward minor bump (v0.2.0) for the runs-on parameterization slice (adding a required input is technically breaking; minor is the natural home pre-1.0). ## Version label propagation Mechanical s/v0.1.1/v0.2.0/g across: - docs/integration.md (consumer wiring examples × 4) - docs/conventions.md ("Defensive from v0.1.1" → "Defensive from v0.2.0") - changelog.d/3.fixed.md - changelog.d/3.fixed-2.md ## Future-scope ref shift (v0.2 → v0.3) Items previously framed as "v0.2 scope" in integration.md now become v0.3 scope since v0.2.0 is taken by this runs-on fix. Updated: - "Other strategies (Cargo.toml, pyproject.toml, etc.) are v0.3 scope" - "v0.3 may add a self-check that detects mismatch at workflow start" - release_type fallback message: "Other types fail-loud with a 'v0.3 scope' message" ## Prerequisites docs correction The "Forgejo runner with `docker` label" statement was wrong — the v0.1.0 hard-coded label that surfaced as a bug. v0.2.0 runs-on is required-input + consumer-specified; updated to "any runner with a label of your choice (specified via runs_on workflow input)." ## What this commit does NOT do - Slice 3 (toolkit's own consumer workflows for v0.1.2+/v0.2.1+ dogfood) — separate follow-up PR - The actual v0.2.0 cut — happens after PR #5 + slice-3 land - release-toolkit#3 + #4 issue title updates — separate API edits (those issues are bookkeeping outside the code surface) Per Surveyor's f71f scope framing: this commit changes CUT-VERSION LABELS, not code substance. Code-stamp atf490312should hold; head movement is mechanical. Tracker: frankenbit/release-toolkit#3 (slated v0.2.0) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUEggQMJjaizj2nFVofeyHfix: runs_on parameterization + defensive newline normalization (v0.1.1 slices 1+2)to fix: runs_on parameterization + defensive newline normalization (v0.2.0 slices 1+2).forgejo/workflows/fragment-check.ymlto release-toolkit (post-v0.20.0-cut) #281