fix: fail releases without published build artifacts - #69
Merged
Conversation
A release that publishes no downloadable asset is indistinguishable from a healthy one: the run is green, the release page exists, and the hole only surfaces later when a server owner's download link 404s. release.yml ended at "upload the jars and hope" - nothing ever re-read the release that actually landed. Add a "Verify published release assets" step that re-reads each PUBLISHED release via `gh api /repos/.../releases/tags/<tag>` rather than trusting the upload steps it just ran. Trusting those steps would rebuild the same "green run, empty artifact" defect one layer up: the upload action can skip files, partially fail, or be silently gated off, and only the landed release tells the truth. A positive asset count is not proof of a usable release either - connect-java uploads LICENSE alongside the jars, so a release carrying only LICENSE would pass a count-based check while offering nothing anyone can run. The guard classifies assets by name/type and requires a real downloadable build jar, then range-requests its first byte to prove it is actually served. This is the same fail-loud guard proven written in geyserlite (GeyserMC#136), transplanted and adapted to connect-java's artifacts. The checksums.txt manifest sub-checks are omitted because connect-java publishes no manifest and has no auto-download path that resolves one. The guard runs unconditionally and covers every target a run publishes: the version tag and the stable `latest` release whose download URLs the release body advertises, or `latest-prerelease` on the push path. ReleaseAssetVerificationTest pins the guard against removal, reordering, being made conditional, and having its classifier weakened; all five tests fail on the pre-fix workflow. The release workflow is declared a test input so a workflow edit re-runs them instead of being served from the build cache.
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.
Intent
Transplant the proven R2 fail-loud release-asset guard from geyserlite (minekube/geyserlite#136) into connect-java's release workflow, adapted to connect-java's build-artifact names. This is deliberately the SAME change that landed in geyserlite, in one more place - sameness is the justification for it shipping under standing authority, so faithfulness to the proven original is a hard requirement, not an accident. Do not redesign it, do not generalize it, do not force-fit extras.
The problem: connect-java's .github/workflows/release.yml ended at 'upload the jars and hope'. Nothing ever re-read the release that actually landed, so a release publishing zero assets - or only LICENSE - looked exactly like a healthy one: green run, release page exists, and the hole only surfaces later when a server owner's download link 404s.
The fix: a new unconditional 'Verify published release assets' step, placed after every upload step, that re-reads each PUBLISHED release via 'gh api /repos/.../releases/tags/' rather than trusting the upload steps it just ran. Trusting the step we just ran would rebuild the same 'green run, empty artifact' defect one layer up, so the guard deliberately asserts on the landed release, not on local build output. It classifies assets by name/type and requires a real downloadable BUILD artifact (the plugin jars), then range-requests the first byte of one to prove it is actually served. A positive asset count is explicitly NOT accepted as proof - connect-java uploads LICENSE alongside the jars, so a LICENSE-only release is connect-java's exact analogue of the geyserlite v0.3.6 header-only release that motivated the original guard.
Deliberate decisions a reviewer reading only the diff would not know:
Tests: core/src/test/java/com/minekube/connect/release/ReleaseAssetVerificationTest.java mirrors geyserlite's go/release_asset_verification_test.go, ported to JUnit + SnakeYAML since connect-java is Java/Gradle rather than Go. It pins the guard against removal, reordering before the uploads, being made conditional, having its classifier weakened, and dropping a release target. All five tests were verified to FAIL on the pre-fix workflow and pass on the fix, so they are non-vacuous. release.yml is declared a test task input in core/build.gradle.kts because Gradle's build cache otherwise served stale results and hid workflow edits from the test. snakeyaml 1.27 was added as an explicit testImplementation dependency pinned to the version already resolving transitively via configutils, rather than relying on that transitive.
The guard script itself was validated out-of-band beyond the unit tests: bash -n syntax check, the jq classifier exercised against empty/LICENSE-only/checksums+sig-only/real-jars/still-uploading/zero-byte/prerelease asset sets, and the whole step run end-to-end against mocked gh and curl confirming it exits 1 on an empty release, exits 1 on a LICENSE-only release, exits 1 when a listed jar 404s, and exits 0 on real jars plus LICENSE. './gradlew build' is green.
Important caveat that must survive into the PR description: this guard is proven WRITTEN, not yet proven to FIRE. Its tests pass here, but it will not have actually run against a real GitHub release until connect-java's next release executes the workflow. geyserlite's behavioural proof does not carry over to connect-java. The PR text says so plainly and should keep saying so.
AGENTS.md (CLAUDE.md is a symlink to it) gained a short pointer under Release Flow noting that release.yml now enforces the asset check in CI and that the test's step names must stay in sync with the workflow. Scope was kept tight deliberately: no unrelated cleanup, no changes to the release-please flow, no new release features.
What Changed
Risk Assessment
✅ Low: The release guard and regression-test fixes are well-bounded, with no remaining material source issues; live workflow behavior is deferred as explicitly acknowledged by the intent.
Testing
Focused JUnit, guard syntax, and mocked end-to-end release/dispatch/push scenarios passed; evidence was saved, and generated Gradle outputs were removed. Live release execution remains unverified as expected.
Evidence: Release asset guard E2E evidence
Mocked matrix passed: empty/LICENSE-only/metadata-only/still-uploading/zero-byte/404 cases exited 1; real jars passed for v-test, latest, and latest-prerelease. HARNESS: PASS.Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 2 issues found → auto-fixed (2) ✅
core/src/test/java/com/minekube/connect/release/ReleaseAssetVerificationTest.java:70-assumeTrue(Files.exists(WORKFLOW_PATH))skips all tests if the workflow is deleted or renamed, so the guard can disappear without failing CI. Only skip outside a checkout; fail when the file is missing in-repo.core/src/test/java/com/minekube/connect/release/ReleaseAssetVerificationTest.java:222- Target coverage uses raw substring checks; removinglatestfrom the actualTARGETSassignment can still pass because it appears in comments andlatest-prerelease. Assert the actual target assignments/iteration.🔧 Fix: Harden release asset guard regression tests
1 warning still open:
core/src/test/java/com/minekube/connect/release/ReleaseAssetVerificationTest.java:68- The revised missing-workflow branch inverts the out-of-checkout assumption:assumeTrue(!Files.exists(REPOSITORY_GIT_PATH), ...)passes when.gitis absent, then reachesassertTrue(false), so the test fails instead of skipping outside a checkout. Use an assumption that is false outside a checkout or return after skipping.🔧 Fix: Fix checkout-aware release test skipping
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
./gradlew :core:test --tests 'com.minekube.connect.release.ReleaseAssetVerificationTest'Extracted workflow guardbash -nsyntax checkMockedgh api/curlend-to-end asset matrixTransient Gradle output cleanup and clean-worktree check🔧 Fix: PR caveat remains pending; lint checks clean
1 warning still open:
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.