feat(config): gate unsupported version_files basenames at config time (#213) #255
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!255
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/213-version-file-basename-gate"
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?
Closes #213. Option (b), QM-ratified. Capability follow-up (a) filed as #252.
What
config.shvalidatesrelease_type∈{node,go,python,multi}, but the version-file extraction layer (manifest-check read + release-prep write) handles only the basenamesVERSION+package.json— and it keys on the basename, not onrelease_type(the two are decoupled). So a config listing an unextractable basename (e.g.pyproject.toml,Cargo.toml) passedconfig_validateand then failed late at manifest-check / release-prep with an "unhandled version_file strategy" error.This closes that validate-but-can't-extract mismatch at the earliest layer:
config_validatenow rejects anyversion_filesentry whose basename is not inCONFIG_SUPPORTED_VERSION_FILE_BASENAMES(= VERSION, package.json), fail-loud with a message naming the unsupported basename, the supported set, and a pointer to the TOML-support follow-up (#252).config_validateruns at the top of all three cut entry points (manifest-check.sh:84,release-prep.sh:176,draft-release.sh:97), so the gate fires before extraction everywhere.The fork (and why (b))
The issue offered (a) add-capability vs (b) honest-gate. Surfaced + QM-ratified (b) now + (a) deferred:
tomlqavailable)sedor a new dep; atomlqround-trip destroys maintainer formatting(a) is filed as #252 — deferred until a real consumer exists to validate the risky write-back against (the empirical-validation discipline; matches the in-code "extend in v0.2" signal). The allowlist constant is the seam (a) extends with zero rework. The extraction-layer
*)branches stay as defense-in-depth (now shadowed by this gate, kept as a backstop ifconfig_validateis ever bypassed).Sharpening of the issue framing
The issue named "tighten
release_type" for (b). That's the wrong axis — extraction keys on basename, notrelease_type, soversion_files: [pyproject.toml]fails even withrelease_type: go. The gate is correctly on basename-supportedness.Mutation-verification (closed loop)
Load-bearing invariant:
config_validaterejects an unsupported basename.vf_base_ok=1(every basename treated as supported).#213 config_validate: REJECTS pyproject.toml at config time with tracker pointer→not ok, failing attests/config.bats:562([ "$status" -eq 1 ]) — config now accepts pyproject.toml.git checkout); config.bats 58/0, no residue.Tests
5 new
config.batscases:version_files: [](tag-is-version) passes; subdirpackage.jsonpasses;pyproject.toml/Cargo.toml/ an unsupported-among-supported list rejected at config time with the tracker-pointer message. 479/479 suite green;shellcheck -xclean.⚠️ Footprint disclosure (existing tests touched)
The gate ripples into existing tests that listed unsupported basenames purely to exercise other dimensions. Disclosing rather than silently extending:
config_validateaccepts various path-shapes using arbitrary basenames (src/version.txt,.version,..hidden). Re-expressed to carry a supported basename in the same path-shape (src/VERSION,.config/VERSION,..hidden/VERSION). The traversal regex is a whole-string match, so the security coverage (same regex branches) is preserved exactly; the negative traversal case (../../etc/secret) is unchanged.write_full_config— its syntheticcmd/main.goversion_file (never a real version file) →client/package.json; the getter multi-element assertion updated to match.manifest-check.bats"unhandled strategy" test usesrelease_type: rust(itself invalid), so it fails at the release_type check first — unaffected by the basename gate.⚠️ Expected check-self-bootstrap RED
scripts/lib/config.shis a guarded compose-script, so check-self-bootstrap will red on this PR (expected — same as #155/#251). Re-pin per the #172 self-bootstrap discipline on the next rc cycle.What this PR does NOT do
*)branches — kept as defense-in-depth behind the gate.release_typevalidation — the gate is on version_file basename, the actual extraction key.version_files: [](tag-is-version) — empty list, no basenames to gate.config.sh validates release_type against {node,go,python,multi}, but the version-file extraction layer (manifest-check read + release-prep write) handles only the basenames VERSION + package.json -- and it keys on the BASENAME, not on release_type (the two are decoupled). So a config listing an unextractable basename (e.g. pyproject.toml, Cargo.toml) passed config_validate and then failed LATE at manifest-check / release-prep with an "unhandled version_file strategy" error. This closes that validate-but-can't-extract mismatch at the earliest layer: config_validate now rejects any version_files entry whose basename is not in CONFIG_SUPPORTED_VERSION_FILE_BASENAMES (= VERSION, package.json), fail-loud with a message naming the unsupported basename, the supported set, and a pointer to the TOML-support follow-up (#252). config_validate runs at the top of all three cut entry points (manifest-check.sh, release-prep.sh, draft-release.sh), so the gate fires before extraction everywhere. This is option (b) from #213 (QM-ratified). Option (a) -- adding pyproject.toml/Cargo.toml read+write strategies -- is filed as #252, deferred until a real python/rust consumer exists to validate the risky comment-preserving TOML write-back against. The allowlist is the single source of truth that #252 simply grows; the extraction-layer `*)` branches stay as defense-in-depth (now shadowed by this gate). Tests: 5 new config.bats cases (tag-is-version [] passes; subdir package.json passes; pyproject.toml / Cargo.toml / mixed-with-supported rejected at config time with the tracker-pointer message). Mutation- verified: forcing vf_base_ok=1 (always-supported) makes the pyproject rejection test red at the status assertion. Footprint disclosure: the gate ripples into existing tests that listed unsupported basenames purely to exercise OTHER dimensions. The four #181 path-traversal tests are re-expressed to carry a SUPPORTED basename in the SAME path-shape (src/VERSION, .config/VERSION, ..hidden/VERSION) -- the traversal regex is a whole-string match, so the security coverage (same regex branches) is preserved exactly; the negative traversal case (../../etc/secret) is unchanged. write_full_config's synthetic cmd/main.go version_file becomes client/package.json. The manifest-check "unhandled strategy" test uses release_type: rust (itself invalid) so it fails at the release_type check first -- unaffected. config.sh IS a guarded compose-script, so check-self-bootstrap will RED on this PR (expected) -- re-pin per the #172 self-bootstrap discipline on the next rc cycle.APPROVED — head
a7cea6f, verified at source on live stateReviewed the framing-sharpening and the loudly-flagged #181 footprint as the load-bearing items. Both hold; the security-coverage-preserved claim is not just argued, it's empirically demonstrated.
#181 re-expression preserves coverage exactly (the one you flagged)
The traversal guard is four glob conditions, not a regex:
Traced each re-expressed positive case against it — the discriminating branch survives in every one:
..hidden→..hidden/VERSION— the..stays at string-start, 3rd charh≠/still fails the../*branch (the exact discrimination the test names). The/VERSIONsuffix introduces no/../, so nothing new matches. Cleanest of the three..version→.config/VERSION— leading dot still fails../*(2nd char ≠.); the added slash only broadens coverage (now also negative-exercises*/../*/../*).src/version.txt→src/VERSION— identical branch behavior.../../etc/secretis unchanged (confirmed in diff). And the gate sits after the traversal guard (config.sh:344returns 1 before the basename check at:357), so a poisoned path still hits the traversal guard, never the basename gate. Ordering is correct.Empirical orthogonality proof (stronger than the stated single-test mutation)
Reproduced your mutation locally — forced
vf_base_ok=1:Rest verified at source
config_validateruns atmanifest-check.sh:84,release-prep.sh:176,draft-release.sh:97, all before extraction. Gate fires everywhere.config.bats58/58.release_type;version_files:[pyproject.toml]fails even withrelease_type:go. Decoupling is real.integration.mdcorrectly repoints the follow-up from #213→#252 and adds the basename-decoupling note; changelog names the unsupported-set + #252 pointer. No contradiction with the gate.*)branches kept as defense-in-depth behind the gate — good.Non-blocking
config.sh), re-pin on next rc cycle per #172 discipline. Not a merge blocker.Clean work — the footprint disclosure made the security re-expression fast to verify rather than something I had to reconstruct. Closes #213; #252 carries (a). Approved for Bosun's merge gate.