CI/CD Setup
Bleep's CI story has two parts:
- A basic workflow that runs
bleep cion every push. This page covers it. - Two features that make CI fast: build only what changed
(
--invalidated, see CI project invalidation) and reuse compiled classes across runs and machines (remote build cache). The consolidated workflow at the end of this page combines both.
If your CI is your bottleneck, skip to Build only what changed and Reuse work across runs.
GitHub Actions
Basic CI
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bleep-build/bleep-setup-action@v1
- run: bleep --no-color --no-bsp-progress ci --no-tui
bleep ci compiles every project and runs
every test — in one build pass, not a compile run followed by a test
run. One task graph, one summary, full parallelism, and a library
upstream of a test project is compiled once. It does exactly those two
things: no formatting check, no publish, nothing you did not ask for.
bleep-setup-action reads $version out of bleep.yaml and
installs that exact bleep build. --no-color and --no-bsp-progress
make CI logs scriptable and small, details under
CI flags below.
Cache the Coursier and JVM downloads
The first CI run downloads every dependency, the JVM bleep manages,
and the Scala/Kotlin compilers via Coursier. That's slow, but it's
also identical between runs as long as your bleep.yaml doesn't
change. Cache it:
- uses: actions/cache@v4
with:
path: |
~/.cache/coursier
~/.bleep
key: ${{ runner.os }}-bleep-${{ hashFiles('**/bleep.yaml') }}
This is not the same thing as bleep's remote build cache (which caches compiled classes, not downloads). See Reuse work across runs below for that.
Build only what changed
--invalidated=<ref> scopes a build to the projects whose source or
config changed vs a base commit, plus every project transitively
depending on them. CI then does the minimum amount of work:
- name: Build and test only what changed
run: bleep --no-color --no-bsp-progress ci --no-tui --invalidated=origin/${{ github.event.pull_request.base.ref }}
fetch-depth: 0 on the checkout step is required so git diff
against the base ref works.
The flag needs no guard around it. When nothing is invalidated it
prints Nothing invalidated vs origin/main. Nothing to compile and test. and exits 0.
That is worth stating loudly, because the recipe this replaces did the opposite. It used to read:
# DON'T. This fails open.
- run: bleep build invalidated --base origin/main | xargs bleep compile
With nothing invalidated, bleep build invalidated prints nothing,
xargs runs the command anyway with no arguments, and bleep compile with no arguments compiles every project. The pipeline
you added to make CI fast turned into a full build exactly in the
case where the right answer was to do nothing at all — silently, with
a green check at the end. If you ever do pipe project names into a
command, use xargs -r, which runs nothing on empty input.
--invalidated works on bleep compile
and bleep test too, when you want just
one half. For the full pattern (bare --invalidated, GitLab CI
example, JSON output for matrix expansion), see
CI project invalidation.
Reuse work across runs
The remote build cache stores compiled
classes (plus their Zinc analysis) in S3 / R2 / MinIO so a CI
runner can pull what's been built elsewhere instead of recompiling.
Pair it with invalidated and CI compiles only the projects that
genuinely changed and whose digest isn't in the cache.
Whoever can write to the cache decides the compiled classes every later build executes, so pull-request builds get read-only credentials and never push — a PR runs code chosen by whoever opened it, and on GitHub a PR from a branch in the same repository does receive your secrets. Entries are written by builds of the branch you protect. Issue two credential pairs: one whose policy allows only reads, one that may write.
- name: Pull cache
run: bleep remote-cache pull
env:
# Read-only key: safe in any job, including pull requests.
BLEEP_REMOTE_CACHE_S3_ACCESS_KEY_ID: ${{ secrets.BLEEP_CACHE_READ_ACCESS_KEY_ID }}
BLEEP_REMOTE_CACHE_S3_SECRET_ACCESS_KEY: ${{ secrets.BLEEP_CACHE_READ_SECRET_ACCESS_KEY }}
# ... compile, test ...
- name: Push cache
# Only post-merge builds of the protected branch write to the cache.
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main'
run: bleep remote-cache push
env:
BLEEP_REMOTE_CACHE_S3_ACCESS_KEY_ID: ${{ secrets.BLEEP_CACHE_WRITE_ACCESS_KEY_ID }}
BLEEP_REMOTE_CACHE_S3_SECRET_ACCESS_KEY: ${{ secrets.BLEEP_CACHE_WRITE_SECRET_ACCESS_KEY }}
An if: on the step is not by itself enough. On a pull_request
event GitHub runs the workflow file from the pull request branch,
so a PR can edit the workflow and help itself to any secret the event
can reach. Put the pushing steps in their own job and attach a GitHub
Environment
whose deployment-branch rule allows only your protected branch — that
is what actually withholds the write credential from pull requests.
The remote cache page shows
the full two-job workflow.
Configuration (the bucket URI plus credentials) lives in
bleep.yaml and ~/.config/bleep/config.yaml, see
Remote build cache for setup, S3-compatible
backends (MinIO / Cloudflare R2 / GCS), bucket-lifecycle
expiration commands, and the
trust model — read that before
handing any job a write credential.
Consolidated workflow: invalidation + remote cache
The two features compose. Pull the cache first, compute the invalidated set, compile/test against it (cache hits skip work, genuine changes recompile), push the cache so the next run picks up the new artifacts.
Two jobs, not one: the pull-request job reads the cache with a read-only key, and the post-merge job — the only one that runs code that has been reviewed — is the one that writes.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# Untrusted: builds the PR branch. Reads the cache, never writes it.
pr:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required for `--invalidated`
- uses: bleep-build/bleep-setup-action@v1
- uses: actions/cache@v4
with:
path: |
~/.cache/coursier
~/.bleep
key: ${{ runner.os }}-bleep-${{ hashFiles('**/bleep.yaml') }}
- name: Pull remote cache
run: bleep remote-cache pull
env:
BLEEP_REMOTE_CACHE_S3_ACCESS_KEY_ID: ${{ secrets.BLEEP_CACHE_READ_ACCESS_KEY_ID }}
BLEEP_REMOTE_CACHE_S3_SECRET_ACCESS_KEY: ${{ secrets.BLEEP_CACHE_READ_SECRET_ACCESS_KEY }}
- name: Build and test invalidated projects
run: bleep --no-color --no-bsp-progress ci --no-tui --invalidated=origin/${{ github.event.pull_request.base.ref }}
# Trusted: reachable only after merge to main, and the write credential
# lives in an Environment whose deployment-branch rule allows only main.
main:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: build-cache-write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: bleep-build/bleep-setup-action@v1
- uses: actions/cache@v4
with:
path: |
~/.cache/coursier
~/.bleep
key: ${{ runner.os }}-bleep-${{ hashFiles('**/bleep.yaml') }}
- name: Pull remote cache
run: bleep remote-cache pull
env:
BLEEP_REMOTE_CACHE_S3_ACCESS_KEY_ID: ${{ secrets.BLEEP_CACHE_READ_ACCESS_KEY_ID }}
BLEEP_REMOTE_CACHE_S3_SECRET_ACCESS_KEY: ${{ secrets.BLEEP_CACHE_READ_SECRET_ACCESS_KEY }}
- name: Compile and test
run: bleep --no-color --no-bsp-progress ci --no-tui
- name: Push remote cache
if: success()
run: bleep remote-cache push
env:
BLEEP_REMOTE_CACHE_S3_ACCESS_KEY_ID: ${{ secrets.BLEEP_CACHE_WRITE_ACCESS_KEY_ID }}
BLEEP_REMOTE_CACHE_S3_SECRET_ACCESS_KEY: ${{ secrets.BLEEP_CACHE_WRITE_SECRET_ACCESS_KEY }}
Release workflow
Tag-driven publish:
name: Release
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # dynver reads tag history
- uses: bleep-build/bleep-setup-action@v1
- name: Publish to Sonatype
env:
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
PGP_SECRET: ${{ secrets.PGP_SECRET }}
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
run: bleep publish --assert-release sonatype
--assert-release fails the job if dynver would emit a snapshot
version (uncommitted changes, commits past the tag. No tags at
all). See Publish to Maven Central
for the full release-pipeline story.
CI flags
bleep --no-color --no-bsp-progress ci --no-tui
| Flag | Effect |
|---|---|
--no-color | Strip ANSI codes from log output. |
--no-bsp-progress | Drop the live BSP progress display so log files stay small. |
Both are appropriate for any non-interactive run.
On compile, test, run, link and dist, three flags control how much
of the run is printed:
| Flag | Effect |
|---|---|
--no-tui | Full streaming trace without the TUI: one log line per project and test event. For CI logs you want to read afterwards. |
--quiet, -q | Only failures and the final summary. Per-event streaming and daemon-connection chatter are suppressed; a red build still prints the full failure section and exits non-zero. |
--summary-only | Alias for --quiet (test only). |
Quiet success, loud failure: --quiet is the right default for agents and
for CI jobs where the log only matters when something broke. Combined with
--diff it prints just the summary and
what changed — nothing else.
Matrix builds
Test multiple Scala versions in parallel jobs:
jobs:
test:
strategy:
matrix:
scala: [2.13, 3]
steps:
- uses: actions/checkout@v4
- uses: bleep-build/bleep-setup-action@v1
- run: bleep test mylib@jvm${{ matrix.scala }}*
For invalidation-driven matrix expansion (build the matrix
dynamically from what's actually changed), pipe
bleep build invalidated --output json
through jq into the matrix. See
CI project invalidation for that
pattern.
Other CI systems
Bleep works with any CI that can run shell commands. Install via the curl installer or Coursier:
curl -fsSL https://bleep.build/install | sh
# or
cs install --channel https://raw.githubusercontent.com/oyvindberg/bleep/master/coursier-channel.json bleep
Remote build cache and CI project invalidation include GitLab CI examples alongside the GitHub Actions ones.
See also
- CI project invalidation , build only what changed.
- Remote build cache: reuse compiled classes across runs and machines.
- Publish to Maven Central : the release-flow side.