fix(release): the console job could never have claimed the name it exists to claim - #45
Merged
Merged
Conversation
…y inventory Two ASVS threads, both about a claim nothing was checking. ## ASVS 15.2.4 — the console has no publish path, so its PyPI name stays free release.yml had zero mentions of the web console: jobs for the engine and the harness, none for messagefoundry-webconsole. So the name could never be claimed by CI, and PyPI confirms it is UNCLAIMED today while messagefoundry and messagefoundry-harness are both taken. A pending Trusted Publisher is now registered against this workflow, which grants permission to publish but -- PyPI's own words -- "does not reserve that name". release-webconsole is a close sibling of release-harness with one deliberate difference: it is NOT lockstep. The console is separately versioned (0.2.15 vs the engine's 0.3.2, its own __version__ root, changelog and cadence per docs/WEBCONSOLE-PACKAGE.md), so it fires on its own `webconsole-v*` tag and carries no `needs: release`. An engine release must not drag it along and a console release must not wait on one. It lives in release.yml rather than its own file because the Trusted Publisher is registered against `release.yml`; a separate workflow would not match it. The engine job is now guarded with `!startsWith(github.ref_name, 'webconsole-')`. Without that a console tag would have run the engine release AT THE CONSOLE'S VERSION -- and neither job's version check could catch it, since each compares its own wheel against the same tag. Publishing stays gated on PUBLISH_WEBCONSOLE, like the harness: the build and version-check run on every console tag so the path is exercised before it is armed. Flipping the variable is what actually creates the project and claims the name. ## Two existing release guards were brittle, and this proved it Adding the job reds two tests that had nothing to do with it. Both were pinning an incidental shape rather than the property, so both are de-hardcoded rather than renumbered: - test_release_pypi_publish_is_last_step: isolated "the release job" by splitting on the literal string "\n release-harness:". Inserting a job between the two silently widened that slice, so the assertion measured the wrong job's last step. The boundary is now derived from the next top-level job. - test_both_wheel_smokes_compare_versions: asserted exactly 2 PEP 440 comparisons. That number needs bumping every time a wheel job is added, which is how it gets bumped without thought. It is now derived from the count of jobs that actually build a wheel, so a NEW wheel job carrying a string compare fails there instead of lowering the bar. ## ASVS 11.1.2 — the usage-scope clauses were scored Pass and nothing pinned them `grep -rn 'Usage scope' tests/` returned nothing. The cell was scored Pass on the **Usage scope:** clauses in ASVS-L2-PHASE0-CHANGES.md section 4, and a new key-material row with no clause -- or a deleted clause -- would have shipped green. Writing the guard found a live gap: the Audit chain row keys its HMAC on an HKDF-derived subkey of the store DEK (and a named Transit audit key under vault_transit) and documented no usage scope, despite being exactly 11.1.2's case -- a key derived from a confidentiality key but used for a different property, where the separation is the thing worth recording. Clause added. Classification is explicit rather than a heuristic. A "does this look like key material" regex mis-sorted 2 of 20 rows on first contact, and a guard with a non-zero false-positive rate gets suppressed. So every row is classified and an UNCLASSIFIED row fails -- a new row cannot be added without someone deciding which side it is on. Same shape as CRITICAL_SECRETS in the rotation inventory. It also caught two of my own exclusion reasons being too thin to be reasons. Three mutations red: delete the audit-chain clause, delete the store-DEK clause, add an unclassified row. Unlike the threat-model guard this document is TRACKED, so this one runs where CI runs.
…ists to claim
release-webconsole used a bare `gh release upload`, copied from release-harness.
The harness can do that ONLY because `needs: release` guarantees the engine
already created the GitHub release. The console deliberately has no `needs` --
it fires on its own tag namespace -- so on a `webconsole-v*` tag NO release
exists, the upload fails with "release not found", and the job dies BEFORE its
publish step. The one job whose purpose is to create the PyPI project and claim
messagefoundry-webconsole could never have claimed it. The first real tag would
have burned on it.
Now create-or-update, mirroring the engine. The update arm is load-bearing for a
second reason the workflow already documents: on v0.3.1 a bare `gh release
create` turned a single PyPI failure into a permanent retry deadlock -- every
re-run died there and SKIPPED the publish, so a re-run could not test the fix it
existed to verify. Notes come from the CONSOLE's own changelog, not the
engine's; step order (reversible GitHub release before irreversible PyPI upload)
matches the engine deliberately.
Two guards, and the first one did NOT work when written:
- test_a_job_without_needs_release_must_create_its_own_github_release --
DERIVED, not console-specific: any release job that attaches assets without
depending on the engine release must create its own.
- test_every_release_creating_job_is_rerunnable -- every `gh release create`
needs a matching `gh release view` guard, or the v0.3.1 deadlock returns.
Mutation-testing caught that the first guard stayed GREEN when the console was
reverted to a bare upload -- i.e. it did not catch the exact defect it was
written for. Two causes, both now fixed: job chunks were sliced with
`body.index(f"{name}:")`, which matches the earliest substring ANYWHERE
including header prose, so every chunk began at the top of the file; and the
chunk was matched against raw text, where the job's own comment explaining the
v0.3.1 deadlock contains the literal `gh release create`. Chunks are now
line-anchored on `^ <name>:` and both guards read through a shared
_executed_shell() that strips comments -- count executed shell, never narration.
That helper exists because the same comment-counting bug was fixed in one guard
and not the other.
All four mutations now red: bare upload; drop the re-run arm; let the engine
fire on a console tag; couple the cadences with `needs: release`.
Nothing here changes what a tag does until PUBLISH_WEBCONSOLE=true.
PR #44 was SQUASH-merged, so its content reached main as a new sha (aa837f8) while c27d4c4 stayed unmerged on this branch. git therefore saw the same change twice and PR #45 came up CONFLICTING. Resolution: both conflicting files on main are BYTE-IDENTICAL to this branch's pre-fix commit (verified by comparison, not assumed), so this branch is a clean superset and taking its side loses nothing from main. Worth recording because it caught me twice today: `git merge-base --is-ancestor` reports NOT-MERGED after a squash, because the sha differs. Content comparison is authoritative; ancestry is not.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Blocks the first
webconsole-v*tag. Asmainstands,release-webconsolewould fail before itspublish step, so the tag would claim nothing and burn the release.
The defect
release-webconsoleused a baregh release upload, copied fromrelease-harness. The harness can dothat only because
needs: releaseguarantees the engine already created the GitHub release. Theconsole deliberately has no
needs— it fires on its own tag namespace — so on awebconsole-v*tagno release exists, the upload fails with "release not found", and the job dies before the
publish step. The one job whose purpose is to create the PyPI project and claim
messagefoundry-webconsolecould never have claimed it.Now create-or-update, mirroring the engine. The update arm is load-bearing for a second reason this
workflow already documents: on v0.3.1 a bare
gh release createturned a single PyPI failure into apermanent retry deadlock — every re-run died there and skipped the publish, so a re-run could not
test the fix it existed to verify. Notes come from the console's own changelog; the step order
(reversible GitHub release before irreversible PyPI upload) matches the engine deliberately.
The guard did not work when written
Two derived guards were added with it:
test_a_job_without_needs_release_must_create_its_own_github_release— any release job that attachesassets without depending on the engine release must create its own.
test_every_release_creating_job_is_rerunnable— everygh release createneeds a matchinggh release viewguard, or the v0.3.1 deadlock returns.Mutation-testing caught that the first stayed GREEN when the console was reverted to a bare upload
— it did not catch the exact defect it was written for. Two causes, both fixed:
body.index(f"{name}:"), which matches the earliest substringanywhere including header prose, so every chunk began at the top of the file and contained every
job. Chunks are now line-anchored on
^ <name>:.contains the literal
gh release create, so it read as "creates a release" with the command deleted.Both guards now read through a shared
_executed_shell()that strips comment lines: count executedshell, never narration. That helper exists precisely because the same comment-counting bug was fixed
in one guard and not the other.
All four mutations now red: bare upload · drop the re-run arm · let the engine fire on a console tag ·
couple the cadences with
needs: release.Verification
9442 passed. The single failure (
test_installed_metadata_matches_dunder_version) is a local-venvartifact — this worktree has no
.venv, so the run used a stale0.3.0install against0.3.2source; it passes on a fresh CI install.
Nothing here changes what a tag does until
PUBLISH_WEBCONSOLE=true.🤖 Generated with Claude Code