Skip to content

CI: publish a snapshot from main, same rule as the other JVM repos - #36

Merged
milyin merged 6 commits into
mainfrom
ci/snapshot-publish
Aug 11, 2026
Merged

CI: publish a snapshot from main, same rule as the other JVM repos#36
milyin merged 6 commits into
mainfrom
ci/snapshot-publish

Conversation

@milyin

@milyin milyin commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What is missing

This repository has no publication in CI at all. publish.yml is
workflow_call, release.yml is its only caller, and that is
workflow_dispatch — so the only builds that ever reached Central are two
hand-dispatched rehearsals, under versions nothing tracks:

$ curl -s https://central.sonatype.com/repository/maven-snapshots/org/eclipse/zenoh/zenoh-flat-jni/maven-metadata.xml
<version>1.9.0-rc4-SNAPSHOT</version>
<version>1.9.0-rc8-SNAPSHOT</version>

So the upload path — signing, credentials, and what Central accepts — is only
ever exercised when someone runs a release by hand. A break in it is discovered
during a release, which is the worst moment to discover it.

What this PR does

Publishes <version.txt>-SNAPSHOT from main through the same publish.yml a
rehearsal runs (snapshot: true → mutable snapshot repository, no staging, no
close/release):

org.eclipse.zenoh:zenoh-flat-jni:1.9.0-SNAPSHOT
org.eclipse.zenoh:zenoh-flat-jni-jvm:1.9.0-SNAPSHOT
org.eclipse.zenoh:zenoh-flat-jni-android:1.9.0-SNAPSHOT

Three mutable coordinates, rewritten in place, so storage does not grow with
merge rate. This is the repository's own upload test — that is the whole
argument for it. It moves the discovery of a broken publication path to the
merge that caused it.

The rule

zenoh-java and zenoh-kotlin have published their snapshot the same way since
long before the flat transition — gated to main, on every merge there; a branch
or a pull request never publishes. This PR applies that rule here.

Without their nightly, though. The obvious argument for one is that zenoh,
zenoh-ext and zenoh-flat are branch = "main" dependencies, so a timed
rebuild would catch one of them moving. It would not: Cargo.lock pins each to a
commit, and Cargo re-resolves a git dependency only on cargo update, so a timed
build rebuilds exactly what the last merge built. Upstream drift arrives as a
lockfile-sync PR, which runs CI like anything else. What is left is an expired
Central token or GPG key, caught only in a week with no merges at all — a thin
canary against five ten-target publications a week. PUBLISHING.md records this,
since "why is there no nightly here" will be asked again.

Update: that reasoning holds one repository further down.
eclipse-zenoh/zenoh-java#526 drops zenoh-java's weekday nightly for the same
reason — its Cargo.lock pins the zenoh-flat-jni commit, so a timed build
there rebuilds what the last merge built too. So "without their nightly" is now
"without zenoh-kotlin's": zenoh-kotlin is the only JVM repository still on a
schedule, and it is the same candidate for the same argument. The
PUBLISHING.md section this PR adds is updated to say so.

Publication is gated on the ci status-check job, matching how the downstream
repositories gate theirs.

Relationship to #37

No SDK consumes this coordinate, and none should. #37 is what makes that
true: each SDK builds and publishes its own copy of this library from the commit
its Cargo.lock pins, as 1.9.0-java-SNAPSHOT / 1.9.0-kotlin-SNAPSHOT. Their
CI does not wait on ours, and their snapshot depends on the commit it actually
compiled against rather than on whatever main held at the time.

That leaves this PR standing on the upload test alone, which is what the sections
above now say — an earlier revision of this description led with "a consumer has
nothing to resolve", and #37 is the real fix for that.

#37 is stacked on this branch — its base is ci/snapshot-publish, so this
one merges first and GitHub retargets it to main automatically. That ordering
also finishes the action pinning: #37's resolve-source job adds a checkout that
does not exist on this branch, so it pins that one itself and publish.yml
reaches main with nothing unpinned.

Review follow-ups included

@diogomatsubara's review, applied — all of it in a second commit so the delta is
readable:

  • ci status-check job, as in zenoh-java. Branch protection lists required
    checks by job name, and three of the four jobs are 3-OS matrices, so without it
    the list has to enumerate nine names that change whenever the matrix does. The
    publication now gates on that one job.
  • Triggers, following the main zenoh repository: push on
    ["main", "release/*", "!release/dry-run/*"] — a release is built from a
    branch create-release-branch makes, and CI never ran on it — and
    pull_request on ["**"], so a backport against a release branch gets CI. Not
    push on ["**"]: with pull_request already on, that runs the nine-job
    matrix twice per branch. Plus concurrency, which zenoh has and this
    repository did not; cancelling reaches a publication in flight, which is safe
    precisely because nothing consumes the coordinate.
  • Third-party actions pinned to a commit with the version in a trailing
    comment, as in zenoh-pico#1281, across all three workflows.
    eclipse-zenoh/* actions are ours and stay on a branch. This also picked up
    four stale majors — setup-java v3/v4 and setup-gradle v3 were on retired
    Node runtimes. setup-gradle stops at v5: v6 moved caching into a
    proprietary component under Gradle's own terms of use, which is not ours to
    accept for an Eclipse project.
  • No toolchain action. rust-toolchain.toml pins the channel, the runner
    ships rustup, and rustup show installs what the file says — which is what
    publish.yml already did. The workflow named 1.97.1 a second time, a pin
    that could disagree with the file; the components move into the file with it.
  • swatinem/rust-cache in place of nine hand-rolled actions/cache blocks:
    keyed on the toolchain and the lockfile, and it prunes what a plain
    path: target cache grows forever.

ci.yml comes out at −104/+66.

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml
zenoh-java and zenoh-kotlin have always published their snapshot from main
only - gated on refs/heads/main, on every merge there plus the weekday 06:00
nightly, never from a branch or a pull request. This repository has no
publication in CI at all: publish.yml is workflow_call, and release.yml is the
only caller, so the only builds that ever reached Central are two hand-dispatched
rehearsals (1.9.0-rc4, 1.9.0-rc8) under versions nothing tracks.

Apply the same rule here. <version.txt>-SNAPSHOT from main, through the same
publish.yml a rehearsal runs, so the upload path stays exercised between
releases and a consumer has something to resolve before the first release
exists. One mutable coordinate set, rewritten in place.
@milyin milyin changed the title CI: publish a snapshot from main so downstream SDKs can resolve it CI: publish a snapshot from main, same rule as the other JVM repos Aug 11, 2026
@milyin
milyin force-pushed the ci/snapshot-publish branch from 81ee85a to f523eba Compare August 11, 2026 12:46
milyin added 3 commits August 11, 2026 23:01
…t-cache

Three things the review asked for, all of them the rule the rest of the org
already follows:

- A `ci` aggregate job, as in zenoh-java. Branch protection lists required
  checks by job name, and three of the four jobs are 3-OS matrices, so without
  it the list has to enumerate nine names that change whenever the matrix does.
  The snapshot publication now gates on that one job instead of on four.

- Third-party actions pinned to a commit, version in a trailing comment, as in
  zenoh-pico#1281. A tag is mutable; a moved tag runs code nobody reviewed.
  eclipse-zenoh/* actions are ours and stay on a branch. This also picked up
  four majors that had gone stale - setup-java v3/v4 and setup-gradle v3 were
  on retired Node runtimes. setup-gradle stops at v5: v6 moved caching into a
  proprietary component under Gradle's own terms of use, which is not ours to
  accept for an Eclipse project.

- No toolchain action. rust-toolchain.toml pins the channel, the runner ships
  rustup, and `rustup show` installs what the file says - which is what
  publish.yml already did. The workflow named 1.97.1 a second time, a pin that
  could disagree with the file; the components move into the file with it.

- swatinem/rust-cache in place of three hand-rolled actions/cache blocks per
  job: keyed on the toolchain and the lockfile, and it prunes what a plain
  `path: target` cache grows forever.
The triggers follow the main zenoh repository, as the review asked, with one
deliberate difference and one addition.

- `release/*` on push. A release is built from a branch the shared
  create-release-branch action creates, and CI never ran on it. The dry-run
  branches that same action produces are excluded.
- `pull_request` on every branch, so a backport against a release branch gets
  CI too. Not `push` on every branch: with pull_request already on, that runs
  the nine-job matrix twice per branch.
- The weekday nightly stays, which zenoh has no need of. This repository tracks
  zenoh, zenoh-ext and zenoh-flat by git *branch*, so a nightly build is the
  only thing that catches one of them moving under us between merges. The
  publication rides along on a trigger that has to exist anyway.
- `concurrency`, which zenoh has and this repository did not. Cancelling
  reaches a snapshot publication in flight, and that is safe: no build resolves
  the coordinate this publishes, so a cancelled upload leaves an artifact
  nobody consumes, in a mutable repository with no staging state to unwind.

That last point is the one the comments got wrong, so they are rewritten here.
Downstream SDKs publish their own copy of this library from the commit each one
pins - they do not resolve ours, and their CI does not wait on it. What this
publication is for is the upload path itself: signing, credentials and what
Central accepts are otherwise exercised only when someone dispatches a release
by hand, which makes a release the moment a break in them is discovered.
The previous commit justified the schedule by saying a nightly build is what
catches zenoh, zenoh-ext or zenoh-flat moving under us, since all three are
`branch = "main"` dependencies. That is wrong. Cargo.lock pins each to a commit:

    source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#773126fd…"

and Cargo re-resolves a git dependency only on `cargo update` or a missing lock
entry, so a timed build rebuilds exactly what the last merge built. Upstream
drift arrives here as a lockfile-sync pull request, which runs CI like anything
else.

What is left is an expired Central token or GPG key, caught only in a week with
no merges at all - every merge already exercises them. That is a thin canary
against five ten-target publications a week, so the trigger goes. Dropping it
also makes the trigger block exactly the one the review asked for, with no
difference left to defend. workflow_dispatch still runs the path on demand, and
a weekly schedule would buy the canary at a fifth of the cost if it is ever
wanted.

PUBLISHING.md records the reasoning, because "why does this repository not have
the nightly the other JVM repositories have" is a question that will be asked
again.

@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.

@diogomatsubara Thank you for review! All changes applied

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml
#37 is now stacked on this branch rather than merging before it, so the
PUBLISHING.md link to "Downstream copies of the snapshot" and the ci.yml
comment naming it both pointed at something that would not exist on main until
#37 lands. The statement they support - that the SDKs publish their own copies
and do not resolve ours - stands on its own, so it stays and the pointer goes.
#37 adds the link back in the commit that adds the section.
@milyin
milyin force-pushed the ci/snapshot-publish branch from 49108b3 to 99caec1 Compare August 11, 2026 22:25
milyin added a commit that referenced this pull request Aug 11, 2026
Stacked on #36, which pins every third-party action in the three workflows, this
branch arrives with one exception: the checkout in the `resolve-source` job it
introduces, which did not exist on #36's base and so could not be pinned there.
Same SHA and trailing-comment format as the other seventeen.

The PUBLISHING.md pointer to "Downstream copies of the snapshot" comes back here
too. #36 carries the sentence without it, because the section it names is the
one this branch adds.
"The other JVM repositories in the org publish their snapshot on a weekday 06:00
schedule" was true when written and is not any more: zenoh-java#526 drops its
nightly on exactly the argument this section makes - its Cargo.lock pins the
zenoh-flat-jni commit, so a timed build there rebuilds what the last merge built
too. zenoh-kotlin is the one that is left, so name it, and record that the
reasoning travelled rather than being a local peculiarity.
@milyin
milyin merged commit e33c9eb into main Aug 11, 2026
13 checks passed
milyin added a commit that referenced this pull request Aug 11, 2026
* CI: publish a snapshot from main, same rule as the other JVM repos

zenoh-java and zenoh-kotlin have always published their snapshot from main
only - gated on refs/heads/main, on every merge there plus the weekday 06:00
nightly, never from a branch or a pull request. This repository has no
publication in CI at all: publish.yml is workflow_call, and release.yml is the
only caller, so the only builds that ever reached Central are two hand-dispatched
rehearsals (1.9.0-rc4, 1.9.0-rc8) under versions nothing tracks.

Apply the same rule here. <version.txt>-SNAPSHOT from main, through the same
publish.yml a rehearsal runs, so the upload path stays exercised between
releases and a consumer has something to resolve before the first release
exists. One mutable coordinate set, rewritten in place.

* CI: review follow-ups - status-check job, pinned actions, rustup, rust-cache

Three things the review asked for, all of them the rule the rest of the org
already follows:

- A `ci` aggregate job, as in zenoh-java. Branch protection lists required
  checks by job name, and three of the four jobs are 3-OS matrices, so without
  it the list has to enumerate nine names that change whenever the matrix does.
  The snapshot publication now gates on that one job instead of on four.

- Third-party actions pinned to a commit, version in a trailing comment, as in
  zenoh-pico#1281. A tag is mutable; a moved tag runs code nobody reviewed.
  eclipse-zenoh/* actions are ours and stay on a branch. This also picked up
  four majors that had gone stale - setup-java v3/v4 and setup-gradle v3 were
  on retired Node runtimes. setup-gradle stops at v5: v6 moved caching into a
  proprietary component under Gradle's own terms of use, which is not ours to
  accept for an Eclipse project.

- No toolchain action. rust-toolchain.toml pins the channel, the runner ships
  rustup, and `rustup show` installs what the file says - which is what
  publish.yml already did. The workflow named 1.97.1 a second time, a pin that
  could disagree with the file; the components move into the file with it.

- swatinem/rust-cache in place of three hand-rolled actions/cache blocks per
  job: keyed on the toolchain and the lockfile, and it prunes what a plain
  `path: target` cache grows forever.

* CI: triggers and concurrency, and what the snapshot is actually for

The triggers follow the main zenoh repository, as the review asked, with one
deliberate difference and one addition.

- `release/*` on push. A release is built from a branch the shared
  create-release-branch action creates, and CI never ran on it. The dry-run
  branches that same action produces are excluded.
- `pull_request` on every branch, so a backport against a release branch gets
  CI too. Not `push` on every branch: with pull_request already on, that runs
  the nine-job matrix twice per branch.
- The weekday nightly stays, which zenoh has no need of. This repository tracks
  zenoh, zenoh-ext and zenoh-flat by git *branch*, so a nightly build is the
  only thing that catches one of them moving under us between merges. The
  publication rides along on a trigger that has to exist anyway.
- `concurrency`, which zenoh has and this repository did not. Cancelling
  reaches a snapshot publication in flight, and that is safe: no build resolves
  the coordinate this publishes, so a cancelled upload leaves an artifact
  nobody consumes, in a mutable repository with no staging state to unwind.

That last point is the one the comments got wrong, so they are rewritten here.
Downstream SDKs publish their own copy of this library from the commit each one
pins - they do not resolve ours, and their CI does not wait on it. What this
publication is for is the upload path itself: signing, credentials and what
Central accepts are otherwise exercised only when someone dispatches a release
by hand, which makes a release the moment a break in them is discovered.

* CI: drop the nightly, and correct the reason given for it

The previous commit justified the schedule by saying a nightly build is what
catches zenoh, zenoh-ext or zenoh-flat moving under us, since all three are
`branch = "main"` dependencies. That is wrong. Cargo.lock pins each to a commit:

    source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#773126fd…"

and Cargo re-resolves a git dependency only on `cargo update` or a missing lock
entry, so a timed build rebuilds exactly what the last merge built. Upstream
drift arrives here as a lockfile-sync pull request, which runs CI like anything
else.

What is left is an expired Central token or GPG key, caught only in a week with
no merges at all - every merge already exercises them. That is a thin canary
against five ten-target publications a week, so the trigger goes. Dropping it
also makes the trigger block exactly the one the review asked for, with no
difference left to defend. workflow_dispatch still runs the path on demand, and
a weekly schedule would buy the canary at a fifth of the cost if it is ever
wanted.

PUBLISHING.md records the reasoning, because "why does this repository not have
the nightly the other JVM repositories have" is a question that will be asked
again.

* docs: do not point forward to a section this branch does not add

#37 is now stacked on this branch rather than merging before it, so the
PUBLISHING.md link to "Downstream copies of the snapshot" and the ci.yml
comment naming it both pointed at something that would not exist on main until
#37 lands. The statement they support - that the SDKs publish their own copies
and do not resolve ours - stands on its own, so it stays and the pointer goes.
#37 adds the link back in the commit that adds the section.

* CI: let a downstream SDK publish its own copy of this library

A downstream snapshot cannot depend on this repository's CI having run, and
its dependency has to be the commit it compiled against. Both follow if the
SDK builds and publishes its own copy from the commit it pins — which needs
three things this workflow did not have.

- `source-repository`: a called reusable workflow runs with the caller's
  context, so the unqualified checkouts fetched the *caller's* repository.
  Defaults to `github.repository`, so a fork still builds its own sources.
- `version-qualifier`: inserted before `-SNAPSHOT`, so the copies do not
  overwrite each other or ours. Rejected for a non-snapshot: releases come
  from here only.
- `zenoh.flatJniCommit` in every POM, from `git rev-parse HEAD`, so a
  publisher can tell whether the published copy is already current without
  downloading a 39 MB jar. Absent outside a git checkout, which reads as
  unknown and makes a comparing consumer rebuild rather than reuse.

Verified locally: `generatePomFileFor{Jvm,KotlinMultiplatform}Publication
-PSNAPSHOT -PversionQualifier=java` produce 1.9.0-java-SNAPSHOT carrying the
commit property, and the same without -PSNAPSHOT fails the guard.

* CI: resolve the source once, and keep the qualifier out of the shell

Review of #37.

- **Mixed-commit artifacts.** `branch` accepts a branch name and every job
  resolved it independently, over a workflow that runs about half an hour: `main`
  moving mid-run would package native libraries from one commit into an artifact
  whose POM is stamped with another, and a downstream stamp check would read that
  as current and reuse a JNI-incompatible build. `resolve-source` now resolves it
  to a commit once — `git ls-remote` for a branch or tag, as-is for a SHA, the
  caller's own commit when nothing is given — and every checkout takes that.

- **The downstream pin has to be new enough.** The qualifier and the stamp are
  produced by the *checked-out* `build.gradle.kts`, so a caller pinning an older
  commit would publish an unqualified, unstamped artifact and then fail resolving
  a coordinate nobody uploaded. Checked before anything is built, with a message
  saying what to do.

- **Injection.** `version-qualifier` is caller input and was interpolated into
  script text, unquoted. It now travels by environment variable, is validated
  against `^[a-z0-9]+(-[a-z0-9]+)*$` before any job builds, and is expanded as a
  single array element rather than spliced into a command line.

Also resolves the conflict with main, keeping #34/#35's `shell: bash` and
`-PprebuiltAndroidLibs=true` on the dry-run publish.

* CI: judge the whole request before spending half an hour on it

Re-review of #37.

- **`git ls-remote <name>` matches by ref pattern**, so a tag name could resolve
  to `refs/heads/release/dry-run/<name>` instead. `resolve-source` now checks out
  with the same `actions/checkout` every job below uses and reports `git rev-parse
  HEAD`, so the ref resolves exactly once and exactly the way it would have there.
  Shorter than the ls-remote branch it replaces, and it subsumes the second
  checkout the capability check needed.
- **A qualifier with `snapshot: false`** was rejected by build.gradle.kts, but not
  until the consumer test configured — after six desktop targets and the Android
  build. Rejected in the preflight now, with an explicit `branch`, which a
  qualified copy also needs: without one the checkout takes the default branch,
  which is the moving target pinning was meant to avoid.
- **`expected-base-version`**: the coordinate comes from the checked-out
  version.txt while the caller names a fixed version of its own. Let the pin move
  past a version bump here and the run publishes 1.10.0-java-SNAPSHOT to an SDK
  still resolving 1.9.0-java-SNAPSHOT, failing only after the upload. The caller
  passes what it expects and the mismatch is caught in the preflight.

* CI: no caller string in script text, and no optional half of a check

Re-review of #37.

- `branch` was interpolated into the preflight's own `[[ -n … ]]`, and git
  permits a branch name with quotes and metacharacters — `git check-ref-format
  'refs/heads/x";id;#'` succeeds, and this step runs before the ref has to exist.
  It goes through the environment now, like the qualifier. So does the qualifier
  in the version-mismatch message, which had the same shape.
- `expected-base-version` was compared only when non-empty, so a qualified caller
  that omitted it recreated exactly the drift the input was added to catch. It is
  now required whenever a qualifier is given, and the comparison is
  unconditional.

* CI: pin the action this branch adds, and link the section it adds

Stacked on #36, which pins every third-party action in the three workflows, this
branch arrives with one exception: the checkout in the `resolve-source` job it
introduces, which did not exist on #36's base and so could not be pinned there.
Same SHA and trailing-comment format as the other seventeen.

The PUBLISHING.md pointer to "Downstream copies of the snapshot" comes back here
too. #36 carries the sentence without it, because the section it names is the
one this branch adds.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
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