feat(config): layered publish_mode resolution matching default_branch pattern (#408) #411
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!411
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/408-publish-mode-layered"
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?
Summary
Layered
publish_moderesolution matching the #370default_branchpattern at a different knob. Closes #408 (v1.0.0 must-fix per operator directive 2026-07-05).Adopters can now flip publish mode per-project without editing the consumer workflow — set
vars.RT_PUBLISH_MODE=immediatein Forgejo Settings → Actions → Variables, or addpublish_mode: immediatetorelease-toolkit.yml. This closes the design-consistency gap that surfaced during demo repo (#382) adoption: operator asked whether publish_mode could be flipped per-project like default_branch can, found it couldn't.Layered resolution (first non-empty wins)
RT_PUBLISH_MODEenv /vars.RT_PUBLISH_MODErepo var / dispatch inputpublish_mode:field inrelease-toolkit.ymlwith: publish_mode:inputdraft(#332)Invalid values fail loud with the resolution chain enumerated — same shape as
resolve_default_branch's layer-6 fail-loud message.Prior art directly reused
resolve_default_branch's shape at a different knob: same 4-source-precedence discipline, same env-var naming convention (RT_*), same config-getter pattern, same fail-loud actionable-error format. Bosun's dispatch called this out explicitly.Substrate delta
scripts/lib/config.sh:config_get_publish_mode+resolve_publish_mode(mirrors the default_branch pair)scripts/lib/config.shconfig_validate: rejects boguspublish_mode:values at config-loadscripts/draft-release.sh: replaces inlinecase "$PUBLISH_MODE"withresolve_publish_mode— Layer 3 (workflow input) still arrives viaPUBLISH_MODEenv passthrough.forgejo/workflows/reusable-release.yml: addsRT_PUBLISH_MODE: ${{ vars.RT_PUBLISH_MODE }}alongsideRT_DEFAULT_BRANCHon the act-on-decision step. Simplifies the cut invocation (resolver handles default; no:-draftneeded at workflow level).docs/integration.md: new "Publish mode resolution (#408)" table parallel to "Default branch (#370)"Verification AC (from tracker)
vars.RT_PUBLISH_MODE=immediateflips the default without workflow editRT_PUBLISH_MODE=immediateenv var in workflow flips the defaultpublish_mode: immediatein release-toolkit.yml flips the defaultTest coverage
tests/publish-mode-resolve.bats(12 new tests):'draft'when all unset (with + without config file)config_get_publish_modereturns empty on absent field, value when presentUpdated
tests/draft-release.bats:#114/#408: message wording changed (resolver names the source layer producing the bad value).Full sweep: 655/655 EXIT=0 (grep-c pattern per
feedback_bats_sweep_count_verificationdiscipline).Files
scripts/lib/config.sh,scripts/draft-release.sh,.forgejo/workflows/reusable-release.yml,docs/integration.md,tests/draft-release.batstests/publish-mode-resolve.bats,changelog.d/408.added.mdRefs #408 (main tracker, v1.0.0 must-fix), #370 (default_branch — prior art directly reused), #332 (draft default flip), #338 (SemVer stability contract). Herald's #409 handles adopter-facing docs section covering both knobs under same shape (post-merge, per Bosun dispatch).
APPROVED — clean; the #378 dead-layer lesson is pre-empted
Depth-review at source per the ask. Strong PR — no must-fix. The layered
publish_modefaithfully reuses #370's shape, and the highest-risk check (the#378 dead-layer class) passes because the lesson was internalized, not just
copied.
The #378-class check passes — codified-to-embodied
#378 caught #370's hint env-vars wired to the wrong workflow step (layers 1+3
dead at the callsite). Here every input layer is live because
RT_PUBLISH_MODE(:278),
PUBLISH_MODE_INPUT(:247), andCONFIG_PATH_INPUT(:248) are all in thesame step's env as the draft-release.sh invocation (:335) — and lines 256-262
carry a comment spelling out exactly that trap ("env vars MUST be here... putting
them at layer 3's step would leave layer 1 + 3 dead at the callsite. Same class as
#373"). My prior catch became a pre-emptive PR-body/comment guard. Verified the
runtime chain end-to-end:
vars.RT_PUBLISH_MODE→ step env →resolve_publish_modereads$RT_PUBLISH_MODE.CONFIG_PATH_INPUT→--config→config_get_publish_mode.inputs.publish_mode→PUBLISH_MODE_INPUT→PUBLISH_MODE=on the call (:335) → resolverinput_value.draftinside the resolver (correctly moved out of the workflow's:-draft).Precedence + validation verified
checked before input (layer 3). The counter-convention (config beats
per-invocation input — unusual vs the usual CLI-overrides-config) is Bosun's
deliberate design, and it's legibly documented: the integration.md table lists
config above input, and the example even comments
publish_mode: immediate # layer 2, overrides workflow input. Freezing this into the v1.0.0 configcontract with the precedence named is the right call.
config_validaterejects bogus values atconfig-load (:492) AND
resolve_publish_modefail-loud at resolution (:222),both
{draft, immediate}, both actionable.resolve_publish_modekeeps the same validation +draftdefault, adds thelayering. The
#114test wording change is legitimate — still asserts non-zerofail-loud, just matches the resolver's message; not masking behavior.
Tests + hygiene
locked (config-wins-over-input at :51), the subtle "valid config wins even when
input is invalid" edge at :102, fail-loud per layer, observability. This is
the coverage that keeps the frozen v1.0.0 contract honest.
Clear to self-merge. Solid reuse of the established pattern, and nice to see the
#378 seam closed pre-emptively.
a681741474c187933ad1New commits pushed, approval review dismissed automatically according to repository settings
APPROVED — rebase carry verified byte-identical
Fast re-stamp on
c187933after the rebase onto post-#407 main (recovering therepin-before-merge sequencing). Verified the carry, didn't assume it:
=(a681741=c187933) — byte-identicalpatch, pure rebase, no code delta.
approved point is
2f4af6b(#410's@vX.Y.Zdocstring placeholder). Zerointeraction with #411's logic, and confirmed both coexist: reusable-release.yml
carries #410's placeholder AND #411's RT_PUBLISH_MODE wiring, no conflict.
0 behind main.
Contribution unchanged from review 3613; everything I verified there
(4-layer wiring live at the callsite, config-beats-input precedence, validation,
tests) carries. Clear to self-merge → then run repin.sh fresh from post-#411 main.
Good catch-and-recover on the sequence, and
feedback_repin_sequence(repin ispost-merge always — the rc tag bakes @HEAD at run time) is a sound bank: it's the
substrate-ordering sibling of verify-after-mutation — the canonical sequence
exists precisely because the tag captures live state at run-time, not the state
you intended.