Skip to content

CI: pin zenoh-flat-jni in a lockfile the org's bot can move, and resolve it in the build - #521

Merged
milyin merged 9 commits into
mainfrom
flat-jni-pin
Aug 10, 2026
Merged

CI: pin zenoh-flat-jni in a lockfile the org's bot can move, and resolve it in the build#521
milyin merged 9 commits into
mainfrom
flat-jni-pin

Conversation

@milyin

@milyin milyin commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

CI here has to answer one question: which zenoh-flat-jni does this SDK build
and test against? Both previous answers were bad in opposite ways. A hand-edited
SHA in ci.yml was reproducible and went stale, because moving it was somebody's
chore. #520 replaced it with zenoh-flat-jni's main, which is never stale and is
not reproducible — a CI result stopped being determined by this repository's
commit.

A lockfile never makes that trade, because a bot moves it. So this makes the pin
be a lockfile, and then lets the build — rather than the workflow — act on it.

The pin: a crate at the root

Cargo.toml, ci/pin.rs, rust-toolchain.toml, Cargo.lock. The crate
compiles to nothing anyone ships; its entire content is one dependency:

zenoh-flat-jni = { git = "https://github.com/eclipse-zenoh/zenoh-flat-jni.git", branch = "main" }

whose only job is to turn the commit under test into a resolved lockfile entry:

Cargo.lock:  source = "git+https://github.com/eclipse-zenoh/zenoh-flat-jni.git?branch=main#<40-hex commit>"

Nothing builds this crate. cargo build at the root would compile zenoh to
produce an empty library — Gradle only reads these two files.

Why a lockfile: the eclipse-zenoh/ci part

Because eclipse-zenoh/ci's sync-lockfiles already moves lockfiles, for every
zenoh dependant, whenever zenoh’s own Cargo.lock moves on main, and this
repository can just be one of them. Its
sequence, applied here:

  1. overwrite Cargo.lock with zenoh's — which carries no zenoh-flat-jni
    entry, so the pin is removed;
  2. rectify by resolving and compiling again (cargo clippy --all-targets --all-features -- --deny warnings) — which writes back zenoh-flat-jni's
    current main commit, and proves it compiles, so a broken combination never
    becomes a pull request;
  3. create-pull-request + gh pr merge --squash --auto, gated on this
    repository's own CI — which tests against the very commit that PR just wrote.

The pin makes a run reproducible from this repository's commit; the bot keeps it
from going stale. Neither property is new machinery — that is the point.

One related PR:

The other sync special cases don't apply: the clippy-features one is for the DDS
plugins, and the extra build-resources/opaque-types steps are zenoh-c's second
manifest.

One consequence worth naming: the sync's cargo update zenoh step re-pins
zenoh in this lockfile, where zenoh is only a transitive dependency of the pin
crate. Harmless — nothing is built from this lockfile — but the zenoh revision
recorded here is not necessarily the one inside the zenoh-flat-jni commit it
pins. The commit is the pin; the rest of the lockfile is a by-product.

Rehearsed the whole sequence locally against zenoh's real lockfile before
committing to it: the pin survives the round trip and comes back pointing at
main.

Acting on the pin: in the build, not the workflow

settings.gradle.kts decides where zenoh-flat-jni source comes from, if
anywhere, in this order:

says "use this source" where it comes from
1 -PlocalJniDir=<path> that directory, as it is
2 path = "…" in Cargo.toml that directory, as it is
3 -PuseLocalJni=true the commit Cargo.lock pins, fetched into .zenoh-flat-jni/
4 nothing Maven Central — no composite build at all

Row 2 is the reason Cargo.toml is parsed and not just Cargo.lock: pointing a
build at your own checkout is then the ordinary Cargo edit, in the tracked file
where a Rust developer would look for it, with no Gradle properties involved. Row
3 fetches only when .zenoh-flat-jni/ is not already at the pinned commit, and
-PlocalJniCommit=<sha> tries a different commit without touching the lockfile.

A release takes row 4, and must — with a composite build the published
artifact would be built from source on the builder's disk while the POM still
claimed the released version. Nothing opts in by default, and build.gradle.kts
now fails any publish* task while an included build is present, which catches a
leftover property or a path = left in the manifest.

What that does to ci.yml

It removes work rather than moving it: -34 lines, five steps left, and not one
of them names a commit or runs Cargo.

- run: rustup show                 # install the toolchain up front
- run: ./gradlew jvmTest --info -PuseLocalJni=true

The Gradle build fetches its own pinned bindings, and their test task depends on
their native build, so Gradle drives both git and cargo. Which means the second
line reproduces a full CI run anywhere — that is the property the redesign was
for. Formatting, clippy, the feature-leak test and the native build were also
deleted here: they are zenoh-flat-jni's own CI, on three platforms, for the very
commit pinned here, and re-running them from this repository only added ways for
two toolchains to disagree.

The identical change in eclipse-zenoh/zenoh-kotlin#700 is green on ubuntu and
macos with no checkout step, which is the proof that the fetch path works end to
end; the same run here locally passes 111/112, the one failure being a LAN peer
discovery test unrelated to this.

Moving the pin by hand

Normally you don't; the bot's PR does. When you need to:

cargo update -p zenoh-flat-jni                 # to zenoh-flat-jni's main tip
cargo update -p zenoh-flat-jni --precise <sha> # to one specific commit

CI.md documents this along with the two ways to defeat the mechanism
rather than steer it: rev = "…" in the manifest freezes resolution so the sync
can no longer move the pin, and a committed path = "…" leaves the lockfile
pinning no commit at all (CI says so and fails, rather than checking out
something arbitrary).

Afterwards

When org.eclipse.zenoh:zenoh-flat-jni is on Maven Central, all of this retires
in favour of zenohFlatJniVersion in gradle.properties — one pin for CI, local
builds and releases, with no fetch, no Rust toolchain, and no pin crate.

CI tracked zenoh-flat-jni's default branch, which never went stale and was
never reproducible: a CI result here stopped being determined by this
repository's commit.

Record the commit under test in a Cargo.lock instead, through a pin crate at
the root that compiles to nothing anyone ships, and let settings.gradle.kts
decide where zenoh-flat-jni source comes from - a directory given by
-PflatJniDir, a path= in Cargo.toml, or the pinned commit fetched on demand
with -PuseLocalFlatJni. A release takes none of those and resolves the Maven
artifact; build.gradle.kts fails any publish task while a composite build is
present.

A lockfile is the pin eclipse-zenoh/ci's lockfile sync already knows how to
move, so this repository stays an ordinary dependant of it.

The workflow loses its zenoh-flat-jni checkout: the build fetches its own
bindings, which is what makes 'gradlew jvmTest -PuseLocalFlatJni=true'
reproduce a CI run anywhere.
milyin added 6 commits August 10, 2026 23:59
Pointing a build at zenoh-flat-jni source is how you develop against it, not
how you publish. PUBLISHING.md keeps only the part that is a release
constraint: a composite build must never reach a publication.
The README answers how to work on this project, including across the stack:
how to point a build at zenoh-flat-jni source, and how to reproduce a CI run.
Which commit that is, and the lockfile sync that keeps it current, is CI's
business - and it does not touch publishing, which resolves a release from
Maven Central.
The README now answers what a user asks: use the published bindings (default,
no Rust), build them from source as Cargo.toml says, or point at a checkout.
Everything behind that - the pin crate, the fetch, the lockfile sync - moved to
CI.md, which is where it belongs.

-PuseLocalFlatJni now follows the manifest rather than only the lockfile: a git
dependency with no Cargo.lock resolves one with Cargo instead of failing, so a
fresh tree works the way a Rust developer expects. -PflatJniDir accepts an
absolute path.
Also localJniCommit, so the three share one prefix.
Review on the zenoh-kotlin twin caught that the lockfile sync only fires when
zenoh's Cargo.lock moves, so a zenoh-flat-jni-only fix would leave this pin
sitting still - reproducible, but not current.

update-flat-jni.yml runs cargo update -p zenoh-flat-jni on a schedule and opens
the same kind of auto-merging pull request, gated on this repository's CI.

Also: the pin crate pointed at PUBLISHING.md instead of CI.md, checkoutPinned
hard-failed on a half-initialised checkout and refetched for an abbreviated
commit, and the test section did not mention the two source options.
Verified rather than assumed: zenoh's Cargo.lock carries no zenoh-flat-jni
entry, so overwriting this lockfile with it removes the pin, and rectifying
re-resolves it to zenoh-flat-jni's main tip. zenoh's lockfile moves every day or
two, and the resulting PR is gated on this repository's CI, so a zenoh-flat-jni
commit that breaks these tests cannot land - detection and gating are already
there without a second bot.
@milyin

milyin commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

On the staleness objection: no new automation, and here is the measurement

Review on the zenoh-kotlin twin (eclipse-zenoh/zenoh-kotlin#700) raised a blocking point: the pin is only as good as the bot that
moves it, and sync-lockfiles fires on changes to zenoh's Cargo.lock, not
on zenoh-flat-jni's main. So a JNI-only fix — one that never touches zenoh —
would have no mechanism moving the pin toward it.

I first answered by adding one: a scheduled cargo update -p zenoh-flat-jni
workflow in this repository. That is now removed (06c5740). It was the wrong
answer, and testing the premise rather than reasoning about it is what showed
why.

What I measured

On this repository's actual lockfile, simulating what a sync run does:

step result
zenoh's Cargo.lock contains zenoh-flat-jni no — 0 occurrences
overwrite this lockfile with zenoh's the pin is removed, not carried forward
rectify (resolve the manifest again) pin written back as e75529ce…
zenoh-flat-jni main tip at that moment e75529ce… — the same commit

So a sync run does not preserve a stale pin: it re-resolves it to whatever
zenoh-flat-jni's main is at that moment.

And the trigger fires far more often than I assumed. trigger-sync-lockfiles in
eclipse-zenoh/zenoh runs on every push to main that touches its Cargo.lock
Aug 1, 2, 3, 4, 5, 6, 7, 9, and similarly through July. Roughly every other day.

Why that settles it

The sync's pull request auto-merges only if this repository's CI passes, and
that CI builds and tests against the newly written commit. So:

  • a zenoh-flat-jni change that breaks this SDK surfaces within a day or two, and
  • it cannot land here while the tests fail.

Detection and gating both already exist. A dedicated bot would have bought two
smaller things: attribution (the sync PR rewrites the whole lockfile and jumps
the pin, so a failure takes a moment's reading to place) and independence from
zenoh's activity during a release freeze. Neither justifies standing
infrastructure in a repository that currently has none of its own — this is
infrastructure, and it should be justified by something better than my
assumption.

What I did instead

Documentation, where a claim was doing work it had not earned. CI.md now says
outright that the pin advances as a side effect of a zenoh lockfile change
rather than because zenoh-flat-jni moved, that in practice this happens every day
or two, and that cargo update -p zenoh-flat-jni is the way to pick a commit up
sooner. No implied automation that does not exist.

The other review points

All fixed in 4f86578:

  • the Running the tests section still described the removed sibling-checkout
    behaviour — following it while changing both repositories would have silently
    tested the pinned commit instead of your working tree;
  • the pin crate's comment pointed at PUBLISHING.md instead of CI.md, and
    credited CI with reading the commit when it is the Gradle build, anywhere;
  • checkoutPinned hard-failed on a directory that was git init-ed but never
    fetched, and refetched every run for an abbreviated -PlocalJniCommit. Now a
    runCatching on rev-parse plus a prefix compare; verified at the pinned
    commit (no fetch), half-initialised (fetches and recovers), and abbreviated sha
    (no refetch).

Ordering is unchanged: eclipse-zenoh/ci#465 lands first — it now carries both the
new matrix entries and the crate-path removal, since zenoh-jni/ is gone from
both SDKs and the sync is broken for them until it does.

milyin added 2 commits August 11, 2026 01:14
The prefix compare only avoided a refetch when an abbreviation happened to match
a cached HEAD; on a cold or different checkout it reached 'git fetch <url>
<abbrev>', which no remote can answer. An empty value was accepted outright.
Validate 40 hex digits before anything is fetched.
The rehearsal table and the test section were updated by renaming the property
rather than by saying what it now does - it fetches the pinned commit, not a
sibling checkout - and the test section did not mention that both source options
need a Rust toolchain.
@milyin
milyin merged commit 9adc7eb into main Aug 10, 2026
7 checks passed
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.

1 participant