Skip to content

ci+bleep: publish dev builds from CI, and resolve them from a snapshot version - #632

Merged
oyvindberg merged 7 commits into
masterfrom
ci/dev-build-artifacts
Jul 31, 2026
Merged

ci+bleep: publish dev builds from CI, and resolve them from a snapshot version#632
oyvindberg merged 7 commits into
masterfrom
ci/dev-build-artifacts

Conversation

@oyvindberg

Copy link
Copy Markdown
Owner

The native images CI already builds are downloadable but unusable on their own. A bleep binary asks Coursier for build.bleep:bleep-bsp at the version baked into it, and that version — derived from the run's git state, e.g. 1.0.0-M10+81-236bd0c5-SNAPSHOT — was never published anywhere. Client, server and test-runner must be the same version, so a downloaded binary resolves nothing and fails at first use.

Almost nothing was missing. publish-local already runs in the build job for the dev script's own benefit, and constants.DefaultRepos already puts ~/.ivy2/local ahead of Maven Central. This just keeps the output.

Using a dev build

gh run download <run-id> -n bleep-jars -D /tmp/j
cp -R /tmp/j/build.bleep ~/.ivy2/local/

gh run download <run-id> -n bleep-arm64-apple-darwin -D /tmp/b
chmod +x /tmp/b/bleep
/tmp/b/bleep compile

Only this run's version

Uploading ~/.ivy2/local/build.bleep wholesale would have been wrong, and measurably so — my own copy holds 31 versions of bleep-bsp_3, 453 MB, including released ones. That directory is whatever the machine accumulated, and people are being asked to unpack this artifact over their own ivy cache.

Selecting on the version, which carries the commit sha, gives exactly what this build produced:

version: 1.0.0-M10+78-b3992db4-SNAPSHOT
modules: 15, size: 15M
strays: none

The step fails loudly if a module the client needs is absent, or if anything from another version rides along. Both are silent until first use otherwise.

Staged with the shell rather than pointing upload-artifact at a ~ path — the action does not run path through a shell, so a leading ~ is not guaranteed to expand and would silently match nothing.

Retention

Binaries go 1 → 7 days. One day was correct when the only consumer was the release job in the same run; it is no use to someone trying a build before it ships. Seven days still expires. The repo is public, so Actions storage is free and unmetered — this costs tidiness, not money.

Rough footprint: ~200 MB of binaries + 15 MB of jars per run, held 7 days.

Not broken

prepare-release.sh names each artifact directory explicitly, so the extra bleep-jars is ignored — it already coexists with the bsp-diagnostics-* artifacts, which proves the pattern.

Open question

This runs on every branch, since a PR build is exactly when you want to try a binary before merging. Restricting to master is a one-line if: on the two steps if the artifact count is unwelcome.

🤖 Generated with Claude Code

oyvindberg and others added 3 commits July 31, 2026 10:06
The native images CI already builds are downloadable but unusable on their own.
A bleep binary asks Coursier for `build.bleep:bleep-bsp` at the version baked
into it, and that version — derived from this run's git state — was never
published anywhere. Client, server and test-runner must match, so a downloaded
binary resolves nothing.

`publish-local` already runs in `build`, for the dev script's own benefit. This
keeps its output. `constants.DefaultRepos` puts `~/.ivy2/local` ahead of Maven
Central, so unpacking the artifact there is all a downloaded binary needs:

    gh run download <run-id> -n bleep-jars -D /tmp/j
    cp -R /tmp/j/build.bleep ~/.ivy2/local/
    gh run download <run-id> -n bleep-x86_64-apple-darwin -D /tmp/b
    chmod +x /tmp/b/bleep && /tmp/b/bleep compile

Only this run's version is copied, never the whole of `~/.ivy2/local`. That
directory holds whatever the runner accumulated, and on a dev machine it is
worse — mine has 31 versions of bleep-bsp_3, 453MB, including released ones.
Shipping that would put jars nobody built here into an artifact people unpack
over their own ivy cache. Selecting on the version, which carries the commit
sha, yields exactly what this build produced: 15 modules, 15MB. The step fails
if a module the client needs is missing, or if anything from another version
rides along — both are silent until first use otherwise.

Staged with the shell rather than pointing `upload-artifact` at a `~` path,
which it does not expand and would silently match nothing.

Retention on the binaries goes 1 -> 7 days. One day was right when the only
consumer was the `release` job in the same run; it is no use to someone trying a
build before it ships. Seven still expires. Storage is free and unmetered on
public repositories, so this costs nothing but tidiness.

`prepare-release.sh` names each artifact directory explicitly, so the extra one
is ignored — it already coexists with the bsp-diagnostics-* artifacts.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
`~/.ivy2/local` accumulates — every version ever published or resolved on that
machine. Mine holds 31 versions of bleep-bsp_3, 453MB, including released ones.
Anything that wants to hand one build to someone else needs somewhere clean to
put it, and filtering that directory afterwards is a worse answer than not
mixing them in the first place.

`Target.LocalIvy` carries an optional path; None keeps the existing behaviour,
which is also what `constants.DefaultRepos` searches ahead of Maven Central.
`--to` is wired onto the local-ivy subcommand only.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Uses the new `publish local-ivy --to`, so the artifact holds this build and
nothing else by construction rather than by filtering `~/.ivy2/local` — which
is whatever the runner accumulated, and which people are asked to unpack over
their own ivy cache.

The dev script rather than `bleep`: `--to` is new in this build, so the
released binary on PATH does not have it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@oyvindberg
oyvindberg force-pushed the ci/dev-build-artifacts branch from d89ec13 to c6df0b7 Compare July 31, 2026 08:06
oyvindberg and others added 4 commits July 31, 2026 10:17
Every other publish in this workflow passes `--version` from the generated
BleepVersion; this one relied on dynver at runtime, which appends a timestamp
when the working tree is dirty. The jars are only useful paired with a binary
that names them, so the version cannot be left to depend on the tree being
clean. Also ignores the staging directory, which is untracked output.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A version in bleep.yaml that names a release is a URL — `releases/download/
v$version/bleep-$arch.tar.gz` — and FetchBleepRelease just builds it. A snapshot
has no release, so `$version: 1.0.0-M10+83-fbe31c09-SNAPSHOT` failed with a 404
and no explanation. It now resolves from the artifacts of the run for its commit,
which is what #632 started uploading.

Measured rather than assumed: listing a public repository's artifacts is
anonymous (HTTP 200), downloading one is not (HTTP 401). There is no anonymous
path, so a snapshot cannot be made to "just work" the way a release does. Token
from GITHUB_TOKEN, GH_TOKEN, or `gh auth token`; without one the error says so
and says how to fix it, rather than surfacing a 401.

The lookup goes commit -> run -> that run's artifacts. The obvious shape —
`?name=<artifact>` filtered by commit — is quietly wrong: that endpoint pages,
and it already returns a full page of 100 for a single name in this repository,
so a snapshot whose run has fallen off page one would report "not found" while
its artifact sat there unexpired. Going via the run is exact and one listing
covers both artifacts. The extra `/commits/{sha}` call is because `?head_sha=`
matches only a full 40-character sha while the version carries eight — verified
against the API, a short sha returns zero runs.

The jars are fetched too, not just the binary. Client, server and test-runner
must be the same version, so a binary alone starts and then cannot resolve its
own server. They are unpacked into `~/.ivy2/local`, which
`constants.DefaultRepos` searches ahead of Maven Central.

Routed inside FetchBleepRelease rather than at the call sites, so both
`Main.maybeRunWithDifferentVersion` and `BleepExecutable` get it.

The sha regex tolerates the dirty-tree shape `+82-c6df0b78+20260731-1016-SNAPSHOT`,
which turns up whenever the working tree is not clean — taking everything after
the dash would have produced a sha no run ever had. That case has a test because
I hit it three times while building this.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Downloading the bleep-jars artifact this branch produced turned up two things
that would each have broken snapshot resolution on exactly the artifacts we are
publishing.

**The version had no tag in it.** `VERSION: 0.0.0+1-53645ab8-SNAPSHOT`. The
default depth-1 checkout fetches no tags, so dynver finds none and falls back to
0.0.0 — which is what the jars and every native image were published under, and
what a bleep.yaml would have had to name to ask for one. `fetch-depth: 0` on both
checkouts that publish.

**The sha named a commit with no run.** For a pull_request event
`actions/checkout` checks out `refs/pull/N/merge`, so the build derives its
version from an ephemeral merge commit — 53645ab, `Merge fbe31c0 into
082c384` — while the run is recorded against the branch head. Looking up the
merge commit finds the commit and then zero runs, confirmed against the API.

GitHub writes those parents as [base, head], so the resolver retries with the
second when the direct lookup comes back empty. Reached only then, so a push
build — every master build and every release — never depends on it.

Both were found by fetching the artifact rather than trusting that a green build
meant a usable one. It is structurally fine: 15 modules, one version, VERSION
matching what is inside, all four modules the client names present.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
`status == "completed"` was doing work it cannot do. A run cancelled by
`concurrency.cancel-in-progress` also reads completed, with conclusion
cancelled, and may have uploaded some artifacts and not others — observed here,
where a superseded run held bleep-jars and one of the five binaries.

So the run is chosen rather than taken: prefer completed with conclusion
success, then any completed, then the newest. A commit can have several runs,
and only a successful one is certain to have uploaded everything.

And when an artifact is genuinely absent, the error says why the build did not
produce it — still running, cancelled and probably superseded, or failed before
uploading. Each is a different thing for the reader to do, and "artifact not
found" was the same sentence for all of them.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@oyvindberg oyvindberg changed the title ci: keep the jars that go with the dev binaries, for 7 days ci+bleep: publish dev builds from CI, and resolve them from a snapshot version Jul 31, 2026
@oyvindberg
oyvindberg merged commit 2b126be into master Jul 31, 2026
9 checks passed
@oyvindberg
oyvindberg deleted the ci/dev-build-artifacts branch July 31, 2026 09:51
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