Skip to content

The release publishes on a fixed sleep, and v0.15.0 half-published when crates.io was slow #420

Description

@vladimirrott

The v0.15.0 release failed halfway on the first attempt, published nothing wrong,
and needed a manual re-run. The cause is a race with the crates.io index, and it
will recur.

What happened

sysknife-daemon v0.15.0 uploaded successfully. Cargo then waited for it to
appear in the index and gave up:

15:27:29  Uploaded sysknife-daemon v0.15.0 to registry `crates-io`
15:27:29  note: waiting for sysknife-daemon v0.15.0 to be available at registry `crates-io`
15:28:29  warning: timed out waiting for sysknife-daemon v0.15.0 to be available in registry `crates-io`
          note: the registry may have a backlog that is delaying making the crate available.

Thirty seconds later the next crate in the loop tried to package against an
index that still did not have it:

15:29:00  error: failed to prepare local package for uploading
Caused by:
  failed to select a version for the requirement `sysknife-daemon = "^0.15.0"`
  candidate versions found which didn't match: 0.14.0, 0.13.1, 0.13.0, ...
  required by package `sysknife-cli v0.15.0`

Five of six crates were on crates.io, sysknife-cli was not, and
sysknife-setup was already on npm. So the ecosystem showed a half-published
release: npm i sysknife-setup would fetch 0.15.0 and cargo install sysknife-cli would fetch 0.14.0.

Re-running the same job on the same commit fixed it, because the step is
idempotent and skips what already exists. That part of the design worked.

Why the current pacing cannot be right

.github/workflows/release.yml publishes in dependency order and sleeps a fixed
30 seconds between crates. Cargo adds its own 60-second wait. Ninety seconds was
not enough here, and no fixed number can be: the delay is crates.io's backlog,
which is not ours to predict. The failure lands on whichever crate happens to be
slow that day, which is why this has not bitten before.

The blast radius is the part that matters. A partial publish is visible to
everyone immediately, and the two registries disagree until a human notices and
re-runs the job by hand.

Scope

  • Replace the fixed sleep 30 with a poll: after each upload, query
    https://crates.io/api/v1/crates/<crate>/<version> until it returns 200, with
    a timeout and a clear failure message naming the crate that never appeared.
    The step already makes exactly that call for its skip check, so the code is
    there.
  • Consider cargo publish --no-verify for the dependents, or publishing with a
    workspace-aware tool that does not re-resolve each crate against the public
    index. The re-resolution is what actually fails; the archive was already fine.
  • Say in docs/release.md what a partial publish looks like and that the fix is
    re-running the job rather than moving the tag. That is written down in the
    general case; the specific symptom is not, and the error message points at a
    version mismatch that does not exist.

Difficulty

Easy, and the polling loop is the whole job. Worth doing before the next release
rather than after, because the next one has the same coin flip.

Tests first

A release workflow is awkward to test, which is the reason this shape survived.
Make the polling a shell function in a script the suite can call, rather than
inline run: steps, and then both directions are cheap.

Break what the guard protects. Point the poll at a stub that answers 404
three times and then 200, and require it to return success without a fixed
sleep. Then point it at a stub that answers 404 forever and require a non-zero
exit whose message names the crate and the version that never appeared. That
second case is the one that turns the v0.15.0 failure from a cryptic
failed to select a version into a sentence an operator can act on.

Break the guard's own input. Assert the loop actually polled. A poll that
returns success because its URL was malformed, or because the stub was never
reached, passes every test above while doing nothing: count the requests and
require at least one. Pin the timeout too, so a future edit cannot turn a bounded
wait into an unbounded one without a test going red.

Then assert the fixed sleep is gone, so it cannot come back:

$ grep -n 'sleep' .github/workflows/release.yml
210:        # sleep 30 lets crates.io index each crate before its dependents publish.
225:              sleep 30

Getting started

CONTRIBUTING.md
has the build and test commands. Everything here is one workflow file plus a
shell script and its test; no Rust, no VM, no daemon, and no crates.io
credentials. You never need to publish anything to work on this: the skip check
at .github/workflows/release.yml:221 already makes the exact crates.io API
call the poll needs, so the request shape is in the file.

Do not test this by cutting a release. No CLA and no copyright waiver. The
project is MIT.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingclaimedSomeone has said in the thread that they are working on thiseasyDifficulty: self-contained, no deep context neededhelp wantedExtra attention is needed

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions