Skip to content

CI: make the GitHub release runnable on its own - #533

Open
milyin wants to merge 2 commits into
mainfrom
ci/release-github-standalone
Open

CI: make the GitHub release runnable on its own#533
milyin wants to merge 2 commits into
mainfrom
ci/release-github-standalone

Conversation

@milyin

@milyin milyin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

The problem

Nothing is broken in this repository today: release.yml has a publish-github job, and 1.10.0 has its GitHub release. What is missing is the way back when that job does not run.

The steps that create the release sit inline in release.yml, so the only way to run them is to run release.yml — which also cuts the release branch, force-pushes the tag, and publishes to Maven Central. A Maven Central publication cannot be withdrawn, so a version that reaches Central without a GitHub release cannot be fixed by re-running: the version cannot be republished, and the re-run would force-move the tag onto a different commit than the artifact was built from.

zenoh-kotlin reached exactly that state — 1.10.0 published to Central, no GitHub release, no way to create one — and fixed it in zenoh-kotlin#708. That pull request notes this repository needs the same treatment. This is it.

How the workflows fit together

release.yml is the entry point, and it is the workflow behind Actions → Release. It calls the other three as jobs; nothing calls release.yml.

release.yml                          Actions → Release
  ├─ tag                             inline: create-release-branch, bump-and-tag.bash
  ├─ publish        ──► publish.yml            Maven Central
  ├─ publish-dokka  ──► publish-dokka.yml      gh-pages          ← also runs alone
  └─ publish-github ──► release-github.yml     GitHub release    ← new, also runs alone

ci.yml                               push to main
  └─ publish_snapshot_package ──► publish.yml  snapshot upload

publish-dokka and publish-github both wait for publish, so neither announces a version whose upload failed. publish-dokka.yml builds the API documentation with Dokka, the Kotlin documentation generator, and deploys it to gh-pages, the branch GitHub serves as a website.

The two workflows marked also runs alone are what this pull request is about.

Two inputs of release.yml decide what a run does, and both predate this pull request. live-run separates a rehearsal from a real release. maven_publish, on by default, switches off the upload to Maven Central.

What this changes

The GitHub release job moves into a workflow that also runs alone

The steps move verbatim out of release.yml into the new .github/workflows/release-github.yml, which release.yml calls as its last job. You can also start that workflow yourself from the Actions tab, which is the point: recovering a missing GitHub release must not require re-running the tag and publish jobs.

The runner changes from macos-latest to ubuntu-latest, matching zenoh-kotlin: nothing in this job needs macOS.

Two checks before the release is created

version is an input of release-github.yml: a release number such as 1.10.0. ci/scripts/bump-and-tag.bash, the script the tag job runs, writes that number into version.txt at the repository root and passes it verbatim to git tag, so one string names both the release and its tag. eclipse-zenoh/ci/publish-crates-github — the shared action that creates the release, and the same one this repository already used — passes --verify-tag to gh release create, the GitHub CLI command for creating a release, which refuses a version with no tag of that name. Two further checks run before the action:

Check Question Rejects
version.txt at the tag equals the version input do the tag and its commit name the same version? a tag pointing at a commit whose version.txt names a different version
check-maven was this version published? a tag for a version that never shipped

The first check is narrow. bump-and-tag.bash writes version.txt and creates the tag in the same run, so every tag the pipeline made passes. What that check rejects is a tag pointing at a commit whose version.txt names a different version. A tag made by hand on the right commit passes, so the check does not establish who made the tag.

check-maven, an input of release-github.yml, is what rejects a rehearsal tag. A rehearsal is a release run under a throwaway version number — 1.10.0 was rehearsed as 1.10.0-rc1 — and it pushes a real tag. Rehearsal tags are self-consistent, so they pass the first check: version.txt at 1.10.0-rc1 reads 1.10.0-rc1.

check-maven reports its two failures separately, because they call for different responses: Central says the version is not there, or Central does not answer. The release pipeline switches check-maven off, because the publish job that just uploaded the version is proof enough. A manual run has no such proof, so check-maven defaults to on there.

The documentation deploy also runs alone now

publish-dokka.yml declared a manual trigger with no inputs, so a manual run received nothing: live-run was false, the deploy step was skipped, and the build used main. Starting that workflow by hand built the documentation site and discarded it, while reporting success.

publish-dokka.yml now declares live-run and branch for manual runs as well. branch is required, so a manual run cannot overwrite the published documentation with the unreleased API from main.

Nothing is announced when the upload is skipped

publish.yml gates only its upload step on maven_publish, so the publish job succeeds whether or not anything was uploaded. Both the GitHub release and the documentation deploy are now gated on maven_publish as well. Without that gate, a live run with uploads disabled would announce a version that was never published.

release.yml also gains github_release (default on), so a Maven-only release stays expressible.

A live run with uploads disabled is rejected

maven_publish suppresses the upload; it does not turn a run into a rehearsal. A live run with maven_publish off cut the real release branch and force-pushed the real tag while publishing nothing, leaving a tag for a version that exists nowhere. The tag job now refuses that combination before creating anything.

Nothing is lost. Rehearsing without uploading is live-run off, which runs the same build and leaves a disposable dry-run branch.

live-run maven_publish
off on rehearsal — a real signed upload of <version>-SNAPSHOT to the mutable snapshot repository
off off build the artifacts, upload nothing
on on the real release
on off refused

PUBLISHING.md

The document said the release path had never run — 1.10.0 published from it on 2026-08-17 — and said zenoh-flat-jni had never been released, which stopped being true with zenoh-flat-jni:1.10.0. Both corrected, and four sections added: the table above, how to create a GitHub release on its own, what a failed release leaves behind, and how to recover a step that failed after Maven Central.

Recovery after Maven Central has accepted a version

At that point the release cannot be re-run, for the reason given at the top. So each remaining step recovers on its own:

Produced by Recover with
publish nothing — Maven Central is immutable and done
tag nothing — the branch and tag were pushed before publish ran
publish-github Release (GitHub), started from the Actions tab
publish-dokka Publish (Dokka), started from the Actions tab, with branch set to refs/tags/<version>

main is deliberately untouched by a release — version.txt is bumped on the release branch only — so no post-release commit needs reconstructing.

Nothing here needs recovering right now. 1.10.0 has its release and its documentation; this is the machinery for the next time a release stops halfway.

Two upstream defects

Filed by zenoh-kotlin#708 as eclipse-zenoh/ci#470, and this repository has been running the same action:

  • publish-crates-github builds its gh command by joining strings and runs the result through a shell, with the bot token in the environment. The version and branch the action is given therefore reach sh -c. The action should use the argument-array helper it already has.
  • It installs a Rust helper from crates.io before it can run gh, on every invocation. That costs every release about 40 seconds and changes nothing the action produces.
  • It bounds generated notes with the newest release rather than the one preceding the version being released, which affects creating a release for a version that is not the newest.

Only the first defect affects safety. release-github.yml constrains its own two inputs to plain ref characters, which is worth doing but is not a fix: the action also passes the newest release's tag name into that command string, and Git accepts tag names carrying shell syntax. The boundary closes upstream or not at all.

None of the three is worked around here: a fix upstream reaches every Zenoh repository, a workaround here reaches one.

Testing

Not run end to end — this is release machinery, and running it live is the situation being fixed. What was checked:

  • check-maven against a published version (zenoh-java:1.10.0), a rehearsal version (1.10.0-rc1), and an unreachable host.
  • The input validation accepts 1.10.0, release/1.10.0 and 1.10.0-rc1, and rejects a value carrying shell syntax.
  • version.txt at both existing tags is self-consistent, confirming that the first check passes a rehearsal tag and only check-maven stops it.
  • All four workflow files parse. The job graph is tag → publish → {publish-dokka, publish-github}. markdownlint-cli2 is clean on PUBLISHING.md, and every internal link in it resolves.

Difference from zenoh-kotlin#708

The two repositories now run the same workflow, with the same guards and the same inputs. Two things differ, both because this repository never lost the job: release-github.yml here says the steps moved out of release.yml rather than that they were deleted in the flat-jni transition, and there is no missing release to recover.

The steps that create the GitHub release were inline in `release.yml`, a
workflow that cannot be started without also running its tag and publish
jobs. Once Maven Central has accepted a version it cannot be republished,
so a release that reaches Central without a GitHub release has no way back.
zenoh-kotlin hit that case with 1.10.0 and fixed it in #708; this is the
same change here.

The steps move to `.github/workflows/release-github.yml`, which `release.yml`
calls as its last job and which you can also dispatch from the Actions tab.
Three checks guard a manual run: `version` and `branch` are constrained to
plain ref characters before `eclipse-zenoh/ci/publish-crates-github` builds
a shell command from them; `version.txt` at the tag must name the version
being released; and `check-maven` requires the version to resolve from
Maven Central, which is what rejects a rehearsal tag.

`publish-dokka.yml` declared a manual trigger with no inputs, so a dispatch
received an empty `inputs`: the deploy step was skipped and the job built
the documentation site and discarded it, reporting success. It now declares
`live-run` and a required `branch`.

Two gates come with this. `release.yml` gains `github_release`, and both the
release and the documentation deploy are gated on `maven_publish` as well,
so a live run with uploads disabled announces nothing. The `tag` job now
refuses `live-run` with `maven_publish` off outright — that combination
force-pushed the real tag while publishing nothing.

@milyin milyin left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I re-reviewed the complete workflow/documentation change and checked it against the rewritten PR description. The extraction, job dependencies, Maven/tag checks, and recovery instructions are internally consistent, but I found one unresolved safety issue and a few claims that need correction.

  1. The input regex does not close the shared action's shell-injection boundary. It protects version and branch, but publish-crates-github also obtains the latest release's tagName, appends it as --notes-start-tag, joins the entire argument list into one string, and passes that string to sh(..., { shell: true }) with BOT_TOKEN_WORKFLOW in the environment. That tag is not validated here. Git accepts shell-bearing tag names (for example, git check-ref-format accepts refs/tags/x$(id), x;id, x|id, x&whoami, and x>pwn), so a crafted latest release remains a command-injection source even when the two workflow inputs pass this regex. This matters more now that the bot-backed action is directly dispatchable. Please either fix/use the upstream argument-array implementation before enabling this path, or implement the release command locally without a shell. The PR description's statement that the constraint “closes it here” is currently too strong.

  2. The prose contradicts the implementation and the upstream issue. The PR first says the regex works around the first defect, then says “None of the three is worked around here.” The workflow and PUBLISHING.md describe “two” remaining defects and say neither affects a normal release, while the same action compiles/installs toml-cli2 on its normal path; that is an operational dependency and roughly 40-second cost on ordinary releases. Please distinguish the locally attempted safety mitigation from the two unmitigated operational/notes defects.

  3. “Builds nothing” / “contains no Rust” are inaccurate as written. The shared action builds the Rust helper, so the Ubuntu-runner rationale should be that nothing requires macOS, not that the job builds nothing. Also, this repository does contain the pin-only Rust crate (Cargo.toml and ci/pin.rs); the accurate publishing claim is that the released SDK artifacts contain no Rust/native code and this repository builds no native libraries.

Validation performed on head f0ec0e0: all workflow YAML parses, git diff --check is clean, markdownlint-cli2 PUBLISHING.md passes, the Maven/tag/release facts cited in the docs match the current public state, and all PR checks are green.

— Codex (GPT-5)

Copilot AI 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.

Pull request overview

Adds standalone recovery workflows for GitHub releases and documentation deployments while preventing announcements when Maven publication is skipped.

Changes:

  • Extracts GitHub release creation into a reusable, manually dispatchable workflow.
  • Adds release gating and manual Dokka deployment inputs.
  • Documents release modes and recovery procedures.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
.github/workflows/release.yml Adds release gating and delegates GitHub release creation.
.github/workflows/release-github.yml Implements guarded standalone GitHub release recovery.
.github/workflows/publish-dokka.yml Enables manual documentation deployment.
PUBLISHING.md Documents release and recovery procedures.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +34 to +37
branch:
description: Ref to build the documentation from - use refs/tags/<version> for a released version
type: string
required: true
Comment on lines +202 to +205
# Two known upstream defects are tracked in eclipse-zenoh/ci#470; neither
# affects a normal release. They are deliberately not worked around here:
# a fix upstream reaches every Zenoh repository, one here reaches this
# repository only.
Comment thread PUBLISHING.md
Comment on lines +541 to +544
Two upstream defects are known and tracked in
[eclipse-zenoh/ci#470](https://github.com/eclipse-zenoh/ci/issues/470). Neither
affects a normal release, and both are left upstream deliberately — a fix there
reaches every Zenoh repository, whereas working around them here would fix one.
Comment thread PUBLISHING.md
Comment on lines +227 to +229
a GitHub release or the documentation for a version already on Central, use the
standalone workflows under [If a step after Maven Central
fails](#if-a-step-after-maven-central-fails), which verify rather than assume.
Three comments in the extracted workflow said things zenoh-kotlin's copy
does not, and one of them was wrong: the shared action installs a Rust
helper before it runs `gh`, so the job does not "build nothing". All
three go, leaving only the differences the repository forces - the
coordinate checked on Maven Central, this repository's rehearsal tag, and
why the steps moved.

PUBLISHING.md picks up the hunk of #708 that was missed: this repository
holds `zenoh-flat-jni-pin` (`Cargo.toml` and `ci/pin.rs`), so the claim
that it contains no Rust was wrong. What the released artifacts contain
is unchanged - no Rust, no native library.
@milyin

milyin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — all three points looked at. The governing constraint on what changed: this pull request exists to give zenoh-java the same release machinery zenoh-kotlin merged in #708, so a difference between the two repositories has to earn its place. 0da8442 removes the differences that had not.

1. The regex does not close the injection boundary — not fixed here, deliberately. You are right that it does not: publish-crates-github also reads the newest release's tag name and appends it to the same command string, and Git accepts tag names carrying shell syntax, so constraining the two workflow inputs narrows the boundary without closing it. The fix belongs in eclipse-zenoh/ci, which every Zenoh repository runs; reimplementing the release command locally would leave zenoh-java and zenoh-kotlin running different code for the same release step, and would still leave the shared action unfixed for everyone else. The constraint stays exactly as zenoh-kotlin has it. The overclaim was in the pull request description, and that is what I corrected: it now says the constraint is worth doing but is not a fix, and gives your reason.

2. The contradiction is gone from the description. It now separates the input constraint, which mitigates without closing, from the two unmitigated defects, and states that the toml-cli2 installation costs about 40 seconds on every ordinary release rather than implying it costs nothing.

The same "two known upstream defects … neither affects a normal release" sentence sits in zenoh-kotlin's release-github.yml and PUBLISHING.md, word for word. I have left it alone in both places here: correcting it is a two-repository change, and doing it in one produces exactly the drift this pull request is meant to remove. Same answer for the two inline comments below on publish-dokka.yml's unconstrained branch input and on "verify rather than assume" in PUBLISHING.md — both are inherited verbatim, and both are worth fixing across the pair.

3. Both inaccuracies fixed.

  • The comment claiming the job "builds nothing" is removed, since the shared action does build a Rust helper. zenoh-kotlin has no comment on that runner, so removing it also removes a difference; the macOS point now lives only in the pull request description, where it belongs.
  • PUBLISHING.md picks up the hunk of #708 I had missed: this repository does hold zenoh-flat-jni-pin (Cargo.toml and ci/pin.rs), which builds nothing that ships and exists so Cargo.lock records the zenoh-flat-jni commit this SDK is tested against. The claim about the released artifacts — no Rust, no native library — is unchanged and was always the accurate one.

What still differs from zenoh-kotlin, and why. release.yml and publish-dokka.yml: nothing this pull request added; the remaining differences predate it. release-github.yml: the Maven coordinate it checks, this repository's rehearsal tag 1.10.0-rc1 in an example, and the paragraph explaining why the steps moved — zenoh-kotlin's says the job had been deleted, which never happened here.

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

.github/workflows/release-github.yml:200

  • The current shared action always uses the newest existing release as --notes-start-tag. If this recovery is delayed until a later version has shipped, creating the older missing release produces reversed or empty generated notes. Since delayed standalone recovery is this workflow's purpose, fix and pin the upstream action or compute the recovered version's actual predecessor instead.
      - uses: eclipse-zenoh/ci/publish-crates-github@main

PUBLISHING.md:549

  • The linked issue tracks three defects, not two. It is also inaccurate that none affects normal releases: the shell construction is a security exposure and the unused helper installation delays every invocation; only the generated-note bounds are recovery-specific.
Two upstream defects are known and tracked in
[eclipse-zenoh/ci#470](https://github.com/eclipse-zenoh/ci/issues/470). Neither
affects a normal release, and both are left upstream deliberately — a fix there
reaches every Zenoh repository, whereas working around them here would fix one.

.github/workflows/release-github.yml:199

  • The linked issue tracks three defects, not two, and the statement that none affects a normal release is inaccurate: shell command construction is a security exposure and the unused Rust-helper installation delays every invocation. Update this comment so operators do not underestimate the known impact.
      # Two known upstream defects are tracked in eclipse-zenoh/ci#470; neither
      # affects a normal release. They are deliberately not worked around here:
      # a fix upstream reaches every Zenoh repository, one here reaches this
      # repository only.

# affects a normal release. They are deliberately not worked around here:
# a fix upstream reaches every Zenoh repository, one here reaches this
# repository only.
- uses: eclipse-zenoh/ci/publish-crates-github@main
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.

2 participants