fix(ci): dry_run never skipped the real publish; make the gate work - #1477
Conversation
`dry_run` is declared `type: boolean`, so `inputs.dry_run` is a BOOLEAN, but
both guards compared it to the STRING 'true'. GitHub coerces mismatched types to
numbers (true -> 1, 'true' -> NaN), so `inputs.dry_run != 'true'` is always true
and `== 'true'` is always false: every "dry run" ran the REAL publish step and
skipped the report.
This is not theoretical — a run dispatched with dry_run=true published
blueprint-manager-bridge 0.2.0-alpha.11 to crates.io before failing. crates.io
versions are immutable, so a bad batch cannot be undone.
- Guards now compare the boolean: ${{ !inputs.dry_run }} / ${{ inputs.dry_run }}.
- The dry-run report now actually checks something: it resolves each crate's
workspace version and flags any already published on crates.io, failing the
job with the list. That is the collision that strands batch-publish.sh
half-way through its topological, rate-limited run. A per-crate
'cargo publish --dry-run' is deliberately NOT used — dry-run still resolves
dependencies from the registry, so every crate depending on a sibling in the
same batch would fail spuriously.
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 088db74a
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-25T01:35:11Z
PR Quality Gate Summary
|
tangletools
left a comment
There was a problem hiding this comment.
🟡 Value Audit — sound-with-nits
| Verdict | sound-with-nits |
| Concerns | 2 (2 weak-concern) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 108.6s (2 bridge agents) |
| Total | 108.6s |
💰 Value — sound-with-nits
Fixes a real, inert safety gate (boolean-vs-string comparison made dry_run always publish for real) and adds a fail-closed version-collision pre-check; ships correctly with one minor reuse opportunity.
- What it does: Two changes in .github/workflows/publish-crates.yml. (1) Flips the two job-step guards from broken string comparisons (
inputs.dry_run != 'true'/== 'true') to idiomatic boolean expressions (!inputs.dry_run/inputs.dry_run). The input is declaredtype: boolean(line 10), soinputs.dry_runis a JSON boolean; GitHub's expression evaluator does not coerce boolean-to-string, so the old `! - Goals it achieves: Restore the only safety gate before an irreversible ~55-crate publish: make dry_run actually skip publishing and instead run a meaningful pre-check that catches version collisions before they strand a batch half-published. The collision check is the specific failure that batch-publish.sh cannot cleanly recover from mid-flight (a late-list collision aborts the run with earlier crates already live o
- Assessment: Good change, correct on the merits. The bug is real and serious — I confirmed the old guards via git show on the parent commit and the type mismatch via the input declaration at line 10; a boolean-vs-string comparison in Actions expressions does not coerce, so the publish step was unconditionally active and the dry-run step was dead code. The fail-closed collision check is a coherent, well-scoped
- Better / existing approach: Searched .github/scripts/, .github/actions/, and all workflows for existing version/collision/publish checks. Found .github/scripts/batch-publish.sh:168
version_is_live()— already answers 'is version V of crate N on crates.io?' against the sparse index (index.crates.io), which is the SAME index cargo resolves dependencies against and is used at lines 209/269 to skip already-published crates. Th - Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 2
- Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content
🎯 Usefulness — sound-with-nits
Correctly fixes an inert safety gate (boolean dry_run compared to string 'true' always mismatched, so every dry run actually published) and upgrades the dry-run report into a real pre-flight collision check against crates.io.
- Integration: Reachable and correctly wired.
publish-crates.ymlisworkflow_dispatch-only — the manual maintainer publish/validation path, distinct from the automatedrelease-plz.yml(push-to-main) path which has no dry-run gate at all (release-plz.yml:80-85). Both consume the same.github/scripts/batch-publish.sh. The fixed guards (publish-crates.yml:59${{ !inputs.dry_run }},:66`${{ inputs.dry - Fit with existing patterns: Fits the established grain precisely. The collision check reuses
cargo metadata --format-version 1 --no-deps— the identical invocation in the release-JSON step (:41) and batch-publish.sh (:61,:196). Its comment explaining why per-cratecargo publish --dry-runis unusable (sibling-dep registry resolution) mirrors the same propagation-aware reasoning already in batch-publish.sh (`:152-16 - Real-world viability: Holds up on the real path with one caveat. crates.io web-API lookup is authoritative for 'is this version already published' — a version visible there is definitely live, so no false negatives from the source. Fail-closed on real collisions (
sys.exit(1)at:111) matches the stated fail-closed contract. However, transient network/HTTP errors are fail-open (:94catches all exceptions, treats t - Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
💰 Value Audit
🟡 Collision check duplicates batch-publish.sh's version_is_live against a different registry endpoint [duplication] ``
batch-publish.sh:168-172 already implements 'is version V of crate N published?' via version_is_live() / sparse_index_path() against index.crates.io (the sparse index cargo resolves against), used at lines 209 and 269. The new dry-run step re-implements the same question in Python against the crates.io web API (crates.io/api/v1/crates/{n}, lines 89-93). Two implementations of one fact that can drift; the web API also lags the sparse index that the real publish path keys off. Prefer adding a --ch
🎯 Usefulness Audit
🟡 Transient crates.io API errors fail-open, masking the exact collision the dry run exists to catch [robustness] ``
At publish-crates.yml:94 the broad
except Exceptionswallows any HTTP/timeout error, prints a note, and treats the crate as non-colliding. crates.io rate-limits and 503s do happen, so under realistic use a blip during the dry run silently disables the pre-flight for that crate. Consider distinguishing HTTP errors (fail-closed or surface prominently in the summary) from the genuine 'crate not yet on registry' case (404 → ok). Low impact because collisions are deterministic and the operator can
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
✅ No Blockers —
|
tangletools
left a comment
There was a problem hiding this comment.
✅ Approved — 3 non-blocking findings — 088db74a
Full multi-shot audit completed 1/1 planned shots over 1 changed files. Global verifier still owns final merge decision.
Full immutable report for this review: trace
Summary comment for this run: full summary
tangletools · 2026-07-25T01:41:54Z · immutable trace
Summary
dry_runinpublish-crates.ymlnever skipped the real publish. The input is declaredtype: boolean, soinputs.dry_runis a boolean, but both guards compared it to the string'true'. GitHub coerces mismatched types to numbers (true→ 1,'true'→ NaN), soinputs.dry_run != 'true'is always true and== 'true'is always false — every "dry run" executed the real publish step and skipped the report.This is not theoretical. A run dispatched with
dry_run=truepublishedblueprint-manager-bridge 0.2.0-alpha.11to crates.io before failing. crates.io versions are immutable — a bad batch cannot be undone.Change Class
Class A(docs/tooling only)Class B(single-crate behavior)Class C(cross-crate/runtime behavior)Class D(protocol/security/metadata semantics)Behavior Contract
dry_run=truerunsbatch-publish.shagainst crates.io and skips the report — the inverse of the intent.dry_run=trueskips publishing and runs a report that fails on a real problem;dry_run=falsepublishes.fail-closedorfail-open): fail-closed — the dry run exits non-zero and lists the offending crates when any target version already exists.Risk and Scope
Verification
Harness Evidence
${{ !inputs.dry_run }}, so a booleantrueskips it; the report step is guarded by${{ inputs.dry_run }}.Deliberately not using a per-crate
cargo publish --dry-run: dry-run still resolves dependencies from the registry, so every crate depending on a sibling being published in the same batch would fail spuriously. The version-collision check is the meaningful, cheap gate — it is the failure that actually strandsbatch-publish.shpartway through its topological, rate-limited run.Checklist
docs/engineering/HARNESS_ENGINEERING_PLAYBOOK.md.docs/engineering/HARNESS_ENGINEERING_SPEC.md.docs/engineering/HARNESS_REVIEW_CHECKLIST.mdwhile implementing/reviewing.