Skip to content

fix(release): retry the Modrinth read-back instead of reding a released tag - #164

Merged
minekube-ai-engineer[bot] merged 1 commit into
mainfrom
fix/modrinth-readback-retry
Sep 14, 2026
Merged

minekube-ai-engineer[bot] merged 1 commit into
mainfrom
fix/modrinth-readback-retry

Conversation

@minekube-ai-engineer

Copy link
Copy Markdown

Problem

release.yml's "Publish to Modrinth" step is fail-closed by design: after creating each platform version it reads that version back and asserts sha1+sha512 against the jar this run built. On the 0.15.12 release the read-back raced the create and returned HTTP 404 for a version Modrinth had in fact stored, so the step red-ed a tag that had published correctly:

attempt evidence
1 (job 103814141173) OK: 0.15.12+velocity published as Jp5zP2v3; connect-velocity.jar matches on sha1 and sha512. then ::error::Could not read version 0.15.12+spigot back from Modrinth (HTTP 404) - bungee never attempted
2 (job 103814731948) Modrinth already carries 0.15.12+velocity / ...+spigot (so spigot was stored), then ::error::Could not read version 0.15.12+bungee back from Modrinth (HTTP 404)
3 (job 103815789359) all three already carries -> step success

Only the read-back raced; the bytes were stored every time. Two manual job reruns were needed to get a green release run. 0.15.9/0.15.10/0.15.11 had this step green, so it is a latency flake, not a permission or contract problem.

What changed

.github/workflows/release.yml, "Publish to Modrinth" step only:

  • Bounded retry around the read-back only: local read_attempts=4, local read_retry_seconds=8. Only HTTP 404 ("not visible yet") is retryable; after the attempts are spent the step fails exactly as before.
  • The fail-closed contract is unchanged: a version is still only reported published after a real read-back whose sha1 and sha512 match the built jar. A digest mismatch is final on the first read - never retried, never downgraded, never reported as success. A 401/403 refusal is final on the first read too (waiting cannot grant a scope).
  • create is not retried. A duplicate version number (400/409 with an already exists/duplicate error) is now treated as inventory: resolve the existing version id from the version list and let the same read-back confirm the bytes. Anything else still fails immediately.
  • A version id missing from a 200 create response fails immediately rather than looping four times before dying.

TDD - RED then GREEN

core/.../release/ReleaseModrinthPublishTest now executes the step's own publish_platform shell against a stubbed Modrinth API (api() and sleep() overridden) instead of only grepping the YAML, so the retry decision is run, not described. Counts of create/read-back/inventory calls are asserted, which is what pins "only the read-back is retried".

RED - test file only, workflow untouched, at a55e14b2:

> Task :core:test FAILED
ReleaseModrinthPublishTest > modrinthReadBackRetriesUntilTheVersionIsVisible() FAILED
  a read-back that is not visible yet must not red the step:
  ::error::Could not read version 0.15.12+velocity back from Modrinth (HTTP 404);
  PUBLISH_EXIT=1  READBACK_CALLS=1  CREATE_CALLS=1
ReleaseModrinthPublishTest > modrinthDuplicateCreateIsConfirmedByReadBack() FAILED
  ::error::Modrinth rejected version 0.15.12+velocity (HTTP 400).
  {"error":"A version with this version number already exists"} PUBLISH_EXIT=1
ReleaseModrinthPublishTest > modrinthReadBackRetryIsBoundedAndFailsClosed() FAILED
  "Publish to Modrinth" declares no `local read_attempts=...`
ReleaseModrinthPublishTest > modrinthReadBackRetryIsBoundedWithABackoff() FAILED
13 tests completed, 4 failed
BUILD FAILED

GREEN - ./gradlew build (the CI command), JDK 21 on the Hermes pod:

BUILD SUCCESSFUL in 1m 6s
> Task :core:test  > Task :spigot:test  > Task :velocity:test  > Task :bungee:test
core 356 / spigot 26 / velocity 32 / bungee 4 = 418 tests, failures=0 errors=0
ReleaseModrinthPublishTest: tests=14 failures=0 errors=0
  - modrinthReadBackRetriesUntilTheVersionIsVisible()      (404, 404, then stored version)
  - modrinthReadBackRetryIsBoundedAndFailsClosed()         (404 forever -> fails after exactly the declared bound)
  - modrinthReadBackNeverRetriesADigestMismatch()          (mismatch -> 1 read, hard failure)
  - modrinthReadBackNeverRetriesARefusedRead()             (403 -> 1 read, hard failure)
  - modrinthDuplicateCreateIsConfirmedByReadBack()         (400 duplicate -> inventory -> read-back)
  - modrinthReadBackRetryIsBoundedWithABackoff()           (bound + backoff + "only 404")
  - modrinthPublishStepIsValidShell()                      (bash -n on the step)
  + the 7 pre-existing step-contract tests, unchanged and still passing

.agents/skills/release/SKILL.md documents the bounded read-back retry, the retryable answer and the duplicate-create handling.

Acceptance

The fix is on a fresh tag via the normal release chain (fix: prefix -> release-please -> release.yml dispatched on the tag), where the read-back races are the thing being tested. Evidence (run URL + the step's log lines) will be posted on this PR once the release run finishes.

@minekube-ai-engineer
minekube-ai-engineer Bot merged commit cb9cb85 into main Sep 14, 2026
2 checks passed
@minekube-ai-engineer

Copy link
Copy Markdown
Author

Acceptance evidence - fresh tag, first attempt, no reruns

The merge cut 0.15.13 through the normal chain (fix: -> release-please -> release.yml dispatched on the tag). The read-back race is exactly what a real release exercises, and the step went green on attempt 1 with no manual reruns:

  • Run: 34792148689 - Release @ tag 0.15.13, run_attempt=1, conclusion success
  • Job: build (103818242788)
  • Release: 0.15.13 - not draft, not prerelease, 4 assets (connect-spigot.jar, connect-velocity.jar, connect-bungee.jar, LICENSE)

Step table of the tag run (Verify published release assets, Publish to Hangar, Publish to Modrinth all success; pre-release steps skipped, as expected for a tag dispatch):

Upload Release Artifacts:      completed/success
Update Latest Release:         completed/success
Prepare Pre-Release Artifacts: completed/skipped
Update Pre-Release:            completed/skipped
Verify published release assets: completed/success
Publish to Hangar:             completed/success
Publish to Modrinth:           completed/success

Publish to Modrinth log lines - all three platform versions created and confirmed against the digests Modrinth computed from the stored bytes, in a single pass:

2026-09-14T00:19:04.8634738Z OK: 0.15.13+velocity published as IK0bvuXQ; connect-velocity.jar matches on sha1 and sha512.
2026-09-14T00:19:09.6302122Z OK: 0.15.13+spigot published as 5ecqm9r5; connect-spigot.jar matches on sha1 and sha512.
2026-09-14T00:19:14.5306331Z OK: 0.15.13+bungee published as fKbtdHvH; connect-bungee.jar matches on sha1 and sha512.

No Could not read version ... back from Modrinth (HTTP 404) line and no already carries line: this particular release did not hit the lagging read, so no retry was needed - the step went green on its first read-back. (An earlier repair rerun of the 0.15.12 run is what the removed failure looked like, for contrast: ::error::Could not read version 0.15.12+spigot back from Modrinth (HTTP 404) followed by Modrinth already carries 0.15.12+spigot; leaving it untouched. on the rerun - i.e. the bytes were stored.)

The retry path itself, replayed against the released workflow

Since this release did not race, the retry path is shown by executing the publish_platform function as released in tag 0.15.13 against the exact 404-then-visible sequence that red-ed 0.15.12 (first two read-backs answer 404, third returns the stored version):

# extracted publish_platform from .github/workflows/release.yml @ 0.15.13: 156 lines
# released retry bound: 4 attempts, backoff 8s
Modrinth is not serving 0.15.13+velocity yet (HTTP 404, attempt 1 of 4); retrying in 8s. The upload is only reported as published once the read-back matches.
Modrinth is not serving 0.15.13+velocity yet (HTTP 404, attempt 2 of 4); retrying in 8s. The upload is only reported as published once the read-back matches.
OK: 0.15.13+velocity published as vid-1; connect-velocity.jar matches on sha1 and sha512.
PUBLISH_EXIT=0 READBACK_CALLS=3 CREATE_CALLS=1

Three read-backs (two retried, the third confirmed), one create, exit 0. The inverse cases are pinned by the new tests: 404 forever fails after exactly the declared bound, a 403 fails on the first read, and a digest mismatch fails on the first read without ever being retried or reported as published.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants