Skip to content

ci: release workflow can silently downgrade Homebrew or silently skip crates.io #89

Description

@Bccorb

Problem

Two latent footguns in the release pipeline, surfaced while getting the 1.2.0 release working end to end. Neither bit this time, but both can cause a silent regression on a future release.

1. No downgrade guard on the Homebrew formula

scripts/release-helper.sh regenerates the formula for whatever version it is handed, with no "only if newer" check. Because release.yml accepts a workflow_dispatch version input and also fires on any pushed v* tag, re-running the release for an older tag (e.g. dispatching v1.1.0) would rewrite the tap formula backwards, downgrading everyone's brew upgrade to the older version. There is nothing stopping an accidental re-dispatch of an old tag from doing this.

Related: gh release create in the same job auto-marks the most recently created release as "Latest", so creating an older release can also steal the Latest pointer from the current version, which install.sh resolves via releases/latest.

2. crates.io publish failures are silent

The crates job is continue-on-error: true (.github/workflows/release.yml). That was the right call when crates.io was best-effort and unproven, but now that the crate is live and expected to track every release, a failed publish (expired token, transient registry error, a version that fails cargo publish --locked) goes green and crates.io silently falls behind the tags and the Homebrew tap, with no signal.

Evidence

  • scripts/release-helper.sh — no version comparison anywhere; writes asset_url for the passed $VERSION unconditionally
  • .github/workflows/release.ymlcrates job continue-on-error: true; release job creates the GitHub Release without forcing --latest=false
  • History confirming the shape: crates.io currently has 1.0.0 and 1.2.0 but not 1.1.0 (the 1.1.0 release was cancelled mid-matrix and its publish was skipped, unnoticed)

Proposed fix

  • Guard the Homebrew formula update so it only advances the version (compare against the formula's current version, or against the tag being the highest), and skip with a clear log line otherwise.
  • Decide the Latest-pointer policy explicitly (e.g. pass --latest only when the tag is the newest semver).
  • Keep crates non-fatal to the binary release, but surface a failure instead of swallowing it — a step that checks the publish result and emits a ::warning::/::error:: annotation, or a notification, so a silent crates.io drift cannot happen again.

Acceptance

  • Re-dispatching an older tag cannot downgrade the Homebrew formula
  • A crates.io publish failure is visible (annotation/notification), even though it does not fail the binary release

Context

Companion to the hardening in #87 (retire macos-13, decouple crates from the binary matrix, 6-binary check, timeouts). Those changes made the pipeline complete a run; these are the remaining reliability edges.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Low priority / backlogtech-debtRefactor / maintainability

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions