Skip to content

Make the Publish workflow reliable - #814

Merged
alexander-yevsyukov merged 11 commits into
masterfrom
publishing-reliability
Oct 22, 2026
Merged

alexander-yevsyukov merged 11 commits into
masterfrom
publishing-reliability

Conversation

@alexander-yevsyukov

Copy link
Copy Markdown
Contributor

Why

41 of 261 Publish runs failed between 2026-06-01 and 2026-10-21. Among those whose logs survive:

  • Duplicate runs racing. On 2026-10-21 a GitHub webhook incident delivered four push runs for one core-jvm commit. Artifact Registry answers 400 to a PUT of a file that already exists, so three of them failed on the first file the winner had stored.
  • Throttling. tool-base got 429 Too Many Requests on parallel uploads. Gradle retries a PUT only 3 times within about 3 seconds, and never retries a 400.
  • A dead step. Decrypt CloudRepo credentials failed 7 runs (delivery, elastic), although CloudRepo is deprecated and nothing publishes to it.

Every merge publishes a new immutable version, so neither a duplicate run nor a re-run of a partly published version can ever succeed.

What changes

  • One publication at a time per repository. The publish job of publishing.yml gets a concurrency group with queue: max: runs wait instead of racing, and a waiting run is not cancelled by a newer one.
  • publicationState, run before anything is built. A new root task looks for every file of each publication in each remote repository it is published to (Artifact Registry, and GitHub Packages read with GITHUB_TOKEN). It writes unpublished, or published when everything is complete and the published SBOMs name this very commit — a duplicate run or a re-run, which then ends green with nothing uploaded or attested. Any other state fails before a single upload: a gap in any repository (a failed earlier run, or GitHub Packages pruning old versions), a version published from another commit, or one whose commit cannot be determined.
  • Longer upload retries, fewer uploads. The publish step passes -Dorg.gradle.internal.network.retry.max.attempts=5, …initial.backOff=5000 (5 + 10 + 20 + 40 s between attempts) and -Dorg.gradle.internal.publish.checksums.insecure=true, which skips the .sha256/.sha512 files nobody reads.
  • No CloudRepo decrypt step, and the misnamed second "Decrypt Git SSH credentials" step is now "Decrypt Gradle Plugin Portal credentials".
  • RemoteFile reads the registries with timeouts and retries (server errors, 408, 429, GitHub's rate-limit 403), stops on other client errors, and follows redirects without carrying the token to another origin. checkVersionIncrement reads maven-metadata.xml through it too.
  • Small refactors: Repository.targets(), MavenPublication.publishedName() and repositoryPath in PublishingExts.kt, GitHubPackages.isRepository().

Verification

  • ./gradlew :buildSrc:build detekt: green, 284 tests. New: PublicationStateSpec, RemoteFileSpec, and PublicationStateIgTest, which really publishes to a loopback registry with a GitHub-Packages-like area (token required, downloads redirected to another origin).
  • Dry runs against the live registries with core-jvm's spine-core: 2.0.0-SNAPSHOT.576 → published from a67ffd4c (also when read from GitHub Packages only, through its real redirect); another commit → collision; .565 → partial (pruned from GitHub Packages); an unpublished version → unpublished; no token or a wrong one → a clear failure.
  • actionlint passes except for concurrency.queue, which GitHub supports since 2026-05-07 but actionlint 1.7.12 does not know yet (concurrency: Add support for queue key rhysd/actionlint#654).

Notes

  • Consumers get this with their next ./config/pull: the pinned publishing.yml and the buildSrc holding publicationState move together.
  • The GitHub Pages update stays as it is here. Separating API-docs publishing from Maven publishing (publishApiDocs, a publish-api-docs.yml workflow) is being done in a separate branch, and propagated by a summit wave.
  • Details and rationale: .agents/tasks/publishing-reliability.md.

🤖 Generated with Claude Code

alexander-yevsyukov and others added 2 commits October 21, 2026 19:18
- Serialize `Publish` runs per repository (`concurrency` with
  `queue: max`), so duplicate runs of one push no longer race for
  the same immutable files.
- Add `publicationState`, run before anything is built. A run of
  a commit that already published its version ends green with
  nothing uploaded; a version partially published, published from
  another commit, or published from a commit that cannot be told
  fails before any upload. It checks every file of each publication
  in each remote repository, GitHub Packages included, read with
  `GITHUB_TOKEN`.
- Retry uploads for longer (5 attempts from 5 s apart) and skip the
  `.sha256`/`.sha512` uploads, which neither Gradle nor Maven reads.
- Drop the `Decrypt CloudRepo credentials` step, which failed runs
  although nothing publishes to CloudRepo.
- Read registries through `RemoteFile`: timeouts, retries, and
  redirects that keep the token at its origin. `checkVersionIncrement`
  uses it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Catch only the exceptions resolving a redirect `Location` can throw,
  instead of every `Throwable`, and name each `Presence` in `when`.
- Name the arguments of the spec's `presenceOf` and `holding` calls,
  and catch only `IllegalArgumentException` in the test registry.
- Tell in the `RemoteFile` docs that a rate-limit `403` is retried and
  that `exists` does not follow a redirect to another origin; cover the
  rate-limit retry with a test.
- Word the pruning hint so it no longer contradicts the advice to bump
  the version, and fix the wording of a few workflow comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 19:53
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-21T20:48:05.205700Z 3fe0188 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21b6af44e3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/publishing.yml

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

SBOM validation can incorrectly accept conflicting or unconfirmed publications as already published.

1 open issue
What changed in this PR

Improves the shared publishing workflow’s reliability by serializing uploads and checking remote publication state before building.

Changes:

  • Adds publication-state checks and duplicate-run skipping.
  • Introduces retrying registry reads and longer upload retries.
  • Removes obsolete credential handling and adds tests.
File Description
README.md Documents duplicate-run behavior.
buildSrc/​src/​test/​kotlin/​io/​spine/​gradle/​publish/​RemoteFileSpec.kt Tests retries, redirects, and authentication.
buildSrc/​src/​test/​kotlin/​io/​spine/​gradle/​publish/​PublicationStateSpec.kt Tests publication-state decisions.
buildSrc/​src/​test/​kotlin/​io/​spine/​gradle/​publish/​PublicationStateIgTest.kt Exercises publishing against loopback registries.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​repo/​Repository.kt Exposes distinct repository targets.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​SpinePublishing.kt Registers the publication-state task.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​RemoteFile.kt Adds bounded, retrying registry reads.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​PublishingExts.kt Shares publication naming helpers.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​PublicationState.kt Checks remote completeness and commit identity.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​PublicationSbom.kt Exposes the SBOM extension constant.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​PublicationChecksums.kt Reuses publication filename helpers.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​MavenMetadata.kt Uses retrying metadata reads.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​IncrementGuard.kt Uses the shared repository-path helper.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​GitHubPackages.kt Adds repository identification helpers.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​CheckVersionIncrement.kt Uses shared repository targets.
.github/​workflows/​publishing.yml Serializes publishing, checks state, and tunes retries.
.agents/​tasks/​publishing-reliability.md Records implementation and verification details.

Review effort: Balanced

Comment thread buildSrc/src/main/kotlin/io/spine/gradle/publish/PublicationState.kt Outdated
alexander-yevsyukov and others added 3 commits October 21, 2026 22:58
`publicationState` checks the Maven repositories only, and a `published`
state skips the whole `publish` invocation, so the GitHub Pages update and
an npm publication wired into `publish` are not re-run. Say so in the
workflow and in the notice of such a run, instead of letting a green
re-run imply they completed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`publicationState` read the SBOM of a publication from one destination
only, so a copy in GitHub Packages uploaded by another commit went
unnoticed, and a copy naming no commit (`unknown`, as built outside
GitHub Actions) was accepted when another publication named this commit.

Read the SBOM from every destination: any copy naming another commit is
a collision, and any copy naming none leaves the version unconfirmed.
Publications without an SBOM, such as plugin markers, stay exempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexander-yevsyukov alexander-yevsyukov self-assigned this Oct 21, 2026
@alexander-yevsyukov alexander-yevsyukov moved this to In Review in v2.0 Oct 21, 2026
`GitHubPackages.isRepository` built a `Repository` with an empty GitHub
repository name only to call `name(snapshots)`, which also read
`GITHUB_ACTOR` and composed a meaningless URL. Move the `-snapshots`
suffix rule into `Repository.publishingName`, used by `name(snapshots)`
and by `isRepository` alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5db35fbdcc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread buildSrc/src/main/kotlin/io/spine/gradle/publish/RemoteFile.kt Outdated
A `429` or a rate-limit `403` with a `Retry-After` longer than the 1- and
2-second backoff used up all three attempts before the registry allowed
another request. `RemoteFile` now waits at least the delay the header
gives, in seconds or as an HTTP date, capped at a minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f16f163475

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread buildSrc/src/main/kotlin/io/spine/gradle/publish/RemoteFile.kt Outdated
alexander-yevsyukov and others added 2 commits October 21, 2026 21:37
`repoName` in `GitHubPackages.repository` and `PublishingRepos.gitHub`
read too close to the `GitHubPackages.repositoryName` constant, the name
of the Gradle repository. Call the GitHub repository name, such as
`core-jvm`, `slug`, and document it. Consumers pass it positionally, so
their calls are unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`RemoteFile.exists` took a redirect to another origin for proof that the
file exists. A server redirecting every request, e.g., from HTTP to HTTPS
or to a canonical host, made missing files look present, so a version
never published looked partially published. Follow every redirect and
judge the answer at its end, without the `Authorization` header on
another origin; Artifact Registry and GitHub Packages answer `HEAD`
without redirecting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29e2fdbfea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/publishing.yml
alexander-yevsyukov and others added 2 commits October 21, 2026 21:46
An already published re-run decrypted the Git SSH, GCS, Artifact Registry
and Plugin Portal credentials before `publicationState` could tell it had
nothing to upload, so a missing or rotated secret failed it. Run the check
right after the setup, as it needs only `GITHUB_TOKEN`, and prepare the
credentials only when the version is unpublished. The consumer's build
that the check runs no longer finds any publishing secret on disk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`.agents/guidelines/testing.md` and the `kotlin-jvm-tester` skill put
`@Nested inner class` on one line and the class name on the next, with
backticks only for a multi-word name or a Kotlin hard keyword. Apply the
layout to every `@Nested` class under `buildSrc/src/test`, 34 of the 38,
and drop the needless backticks from `keep` in `ArtifactComponentsSpec`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexander-yevsyukov
alexander-yevsyukov merged commit ad3dff0 into master Oct 22, 2026
2 checks passed
@alexander-yevsyukov
alexander-yevsyukov deleted the publishing-reliability branch October 22, 2026 10:56
@github-project-automation github-project-automation Bot moved this from In Review to ✅ Done in v2.0 Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

3 participants