Caching
zipx restores sbt's cache on the CI runner so the test job does not start from zero every time. You can ignore the knobs on this page until CI feels slow.
sbt 2 caches task results across JVM runs. zipx restores that cache before the Verify test task, keyed by a
commit-stable epoch (zipxCacheEpoch: CacheEpoch.ShipCatalog when the catalog has Ship / ShipGroup rows,
else CacheEpoch.GitTags()). Every push within a PR reuses prior hits; a row bump or a release tag rolls the epoch. Remote backends share the same hits across
machines, including developer laptops when CI hydrates a shared store (see Remote cache for teams).
Hits need a version that is the same from commit to commit, such as <row>-ci (see Independent versions).
Miss path (amber → red) pays compile/test and writes digests; hit path (green) reuses them. Both land in the same
backend: LocalDir via actions/cache inside zipx-sbt-setup, or a remote action cache (grpc, grpcs, http, or
https). The restore key is the
commit-stable epoch
(zipxCacheEpoch), so PR pushes share hits and a release tag rolls a fresh namespace.
zipx wires cache into generated jobs (same planner as topology). It is not a standalone acceleration appliance: the goal is CI-from-graph plus content-addressed reuse, not a second product to configure beside hand-maintained YAML.
Epoch strategies
zipxCacheEpoch := CacheEpoch.ShipCatalog // default with Ship / ShipGroup rows: hash of row numbers
zipxCacheEpoch := CacheEpoch.GitTags() // default without rows: resolve from git tags on the runner
zipxCacheEpoch := CacheEpoch.GitTags(tagMatch = "v*") // same, explicit match glob
zipxCacheEpoch := CacheEpoch.Fixed(version.value) // bake at generate time
zipxCacheEpoch := CacheEpoch.Script(myEpochShell) // custom shell; must write epoch= and release=
GitTags (default without rows): a Resolve cache epoch step runs after checkout (fetch-depth: 0, fetch-tags: true). On a
v* tag ref, epoch = release = tag without v. Otherwise the latest matching tag becomes release and epoch is
${release}-SNAPSHOT. If local tags lag origin (or none match), the step emits an Actions ::warning titled
zipx cache epoch so shallow/missing tags are obvious in the run summary.
Fixed: embeds a literal into the workflow at zipxWorkflowGenerate (useful for scripted tests or unusual versioning).
Prefer GitTags so post-tag PRs warm from the release cache without a regenerate commit.
Script: supply your own shell; write epoch= and release= to $GITHUB_OUTPUT. Restore-keys use both outputs.
ShipCatalog (default with rows): bakes a SHA-256 of sorted row identity and number as the setup composite
cache-epoch input (same generate-time path as Fixed). The epoch rolls exactly when a row number moves, which is
exactly when <row>-ci version strings, and so digests, change. A release does not roll it: the release run
restores and never saves. GitTags() would roll one step late, at the tag, after the bump PR already moved the
digests.
{
val ships = List[PublishedRow](Ship("client", "0.3.0"), ShipGroup("libs", "1.4.2")("models", "coreLib"))
val hash = Modver.epochHash(ships)
val yaml = DocsRender.job("test")(Capability.test)(using
libGraph,
config.copy(cacheEpoch = CacheEpoch.ShipCatalog, shipEpochHash = Some(hash)),
)
s"hash: $hash\n$yaml"
}hash: 8f2e77a00c7c0f58
test:
name: test
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "8f2e77a00c7c0f58"
- name: test
run: sbt 'test'Backends
zipxCache := CacheBackend.LocalDir
zipxCache := CacheBackend.BazelRemoteSidecar(RemoteCacheProof.image, RemoteCacheProof.port)
zipxCache := CacheBackend.managedRemote("grpcs://cache.buildbuddy.io", "BUILDBUDDY_KEY")
LocalDir: persist
~/.sbt,~/.cache/sbt, and~/.cache/coursierwithactions/cacheinside the generatedzipx-sbt-setupcomposite. One job owns the build snapshot and saves it; every other sbt job restores it and saves nothing (see Who saves below). Keys are OS + JDK + epoch +build+ run id + job id; restore-keys prefer this run's build saves, then the epoch's latest build save, then (GitTags/Script) the release epoch's (steps.*.outputs.release) so the first post-tag PR can warm from the tag build, then any older OS+JDK sbt cache. No infrastructure. GitHub scopes cache entries to the branch that saved them; other PRs restore from the default branch. WithzipxSkipMergedPrPush, Verify does not run on the merge push, so by default a minimalcache-rehydratejob recreates a main-scoped save (see Verify). You cannot copy a PR cache onto main via the API.BazelRemoteSidecar: pinned
buchgr/bazel-remote-cacheas a job service. The published port is gRPC. Proof pins live inRemoteCacheProof(docs, planner tests, andRemoteCacheItSpecshare them).ManagedRemote: any remote URI.
grpcandgrpcsuse sbt's gRPC store.httpandhttpsuse bazel's HTTP API (/ac/<hash>,/cas/<hash>). The URI is written intoZIPX_REMOTE_CACHEunchanged. BuildBuddy, EngFlow, and NativeLink are the usualgrpcsendpoints; auth is a named repository secret. Plaintextgrpc://andhttp://warn on sbt 2.1. That warning is the scheme, not a failed cache.grpcs://andhttps://do not warn. TLS certificate files are for gRPC:httpsplusremoteCacheTls*is an sbt error. This is the path for CI-hydrated caches that developers reuse (see Remote cache for teams).
The remote-cache transport is bundled with zipx. For remote backends zipx also sets Global / cacheVersion from
(JDK, OS) so heterogeneous runners cannot poison the shared cache. CacheEpoch.ShipCatalog does not fold the Ship
hash into that value. A bump already changes that module's version, which is a digest input, so only that module's
remote entries miss. Remote backends pass cache-mode: off to zipx-sbt-setup (the remote store is the persistence);
LocalDir passes save or restore, so the composite runs epoch-keyed actions/cache or actions/cache/restore.
{
val local = DocsRender.job("test")(Capability.test)(using
libGraph,
config.copy(cache = CacheBackend.LocalDir),
)
val sidecar = DocsRender.job("test")(Capability.test)(using
libGraph,
config.copy(cache = RemoteCacheProof.sidecar),
)
val remote = DocsRender.job("test")(Capability.test)(using
libGraph,
config.copy(cache = CacheBackend.managedRemote("grpcs://cache.example", "CACHE_KEY")),
)
local + "\n---\n" + sidecar + "\n---\n" + remote
}test:
name: test
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
- name: test
run: sbt 'test'
---
test:
name: test
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
services:
bazel-remote:
image: buchgr/bazel-remote-cache:v2.6.1
ports:
- "9092:9092"
options: --max_size=1
env:
ZIPX_REMOTE_CACHE: grpc://localhost:9092
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "true"
cache-mode: "off"
cache-epoch: "0.1.0-SNAPSHOT"
- name: test
run: sbt 'test'
---
test:
name: test
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
env:
ZIPX_REMOTE_CACHE: grpcs://cache.example
ZIPX_REMOTE_CACHE_HEADER: ${{ secrets.CACHE_KEY }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "true"
cache-mode: "off"
cache-epoch: "0.1.0-SNAPSHOT"
- name: test
run: sbt 'test'Who saves
Every sbt job restores the LocalDir build snapshot. Only its owner saves one. The builtin test owns it on PRs
and direct pushes, and cache-rehydrate owns it on a merge push, where Verify is skipped. testLayers saves once per
wave, so each wave warms the next through the same-run key. Graph test jobs, coverage, publish, docker, and deploy
jobs restore through actions/cache/restore and never save. The snapshots job waits on verify (every Verify job,
including the cache owner) and, on a merge push, on cache-rehydrate, which owns that push's save. It restores this
run's save and only packages and uploads. Every other sbt Publish job waits on cache-rehydrate the same way, unless
it takes SnapshotSource.Prior: then it starts beside cache-rehydrate and restores the newest earlier save. That
suits a docker image matrix, whose jobs compile their own modules anyway. A zipx-release.yml job restores
and never saves: its jars carry release numbers, which no PR build would hit.
A pinned snapshot keeps the cache honest without making it cold. Only projects that depend on a -SNAPSHOT re-resolve
each session. Every generated sbt job also sets COURSIER_TTL: 0s, which revalidates only changing artifacts, so every
other update and every content-addressed compile still hits. See Snapshots and releases.
Capability.test // LocalCacheMode.Save: the default owner
Capability.testGraph // Restore: its jobs compile disjoint slices
myCheck.withLocalCache(LocalCacheMode.Save) // take ownership in place of the builtin test
Capability.dockerGraph.withSnapshotSource(SnapshotSource.Prior) // on a merge push, start beside cache-rehydrate
A capability that replaces the builtin test by name decides for itself. Coverage never saves, because an
instrumented snapshot is not the build: generate refuses a coverage capability with LocalCacheMode.Save or one named
test (see Verify, "Coverage"). It also refuses two owners, and an owner that would save once per job
(Graph-scoped, matrixed, or fanned out per target).
Budget. GitHub gives a repository 10 GB of actions/cache and evicts the least recently used entries past that.
One save per run means the quota holds about 10 GB / snapshot size runs, and the default branch's snapshot stays
fresh because every PR restores it. When every sbt job saved its own entry, one PR run of a small nine-job build wrote
about 3 GB, and a single wave of four PRs evicted the default branch's snapshot.
DocsRender.jobs("test", "publish")(Capability.test, Capability.publish.copy(gate = Gate.Always))test:
name: test
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
- name: test
run: sbt 'test'
publish:
name: publish
runs-on: ubuntu-latest
needs:
- verify
if: "!cancelled() && needs.verify.result != 'failure'"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: publish
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
cache-epoch: "0.1.0-SNAPSHOT"
- name: publish
run: sbt 'schema/publish; api/publish'Skipping a bad restore
cleanFull deletes declared outputs, target/out, and the disk action cache (~/Library/Caches/sbt/v2 on macOS,
~/.cache/sbt/v2 on Linux). It does not delete ~/.cache/coursier or ~/.sbt. Do not chmod that cache: its blobs
are mode 0444, and cleanFull deletes the directory from a writable parent. When a catalog snapshot is pinned, the
clean inside cleanFull also clears sbt's in-memory resolution cache. A qualifier snapshot is forceUpdatePeriod,
not that hook (see Snapshots and releases). actions/cache still restores ~/.sbt, ~/.cache/coursier,
and ~/.cache/sbt before sbt starts, and the save owner writes them back. target/ is not in that snapshot: sbt
replays products from the action cache. Rolling zipxCacheEpoch does
not drop them either:
a baked epoch's last restore-key is the OS+JDK prefix with no epoch in it, so a miss restores the newest entry under
that prefix.
A warm rerun of one commit in zipx-ci-lab, on that snapshot, compiled nothing in the test job. Its compiled list
was empty, and Greeter, Name, svc-a, svc-b, worker-a, and worker-b still ran. A job that compiles a module the cache
owner never compiled still compiles that module.
A PR labeled purge skips that restore for the run. The save owner still saves when the job succeeds, and the
save has no restore-keys: the primary key includes the run id, so the lookup misses, and the cache action's post
step writes the fresh snapshot. Restore-only jobs in the same run skip the cache step, so they do not pull the old
entry while that save is in flight. cache-mode: off is the wrong tool for this. That run starts empty, and the next
run restores the same entry, because nothing newer was saved.
zipxCachePurgeLabel := Some("purge") // default
The label is read from the pull_request payload, the same way clean is. ci.yml does not run on labeled, and
a rerun repeats the payload the run started with. A push carries no PR labels, so cache-rehydrate on a merge still
restores. The input is on that job anyway, because it is a save owner. A PR cannot replace the default branch's
entry. Other branches keep restoring main until a main-scoped save writes a newer one. None disables the check.
{
given PlanConfig = config.copy(
cachePurgeLabel = PlanConfig.cachePurgeLabel("purge"),
skipMergedPrPush = true,
)
DocsRender.jobs("test", "publish", "cache-rehydrate")(
Capability.test,
Capability.publish.copy(gate = Gate.Always),
)
}test:
name: test
runs-on: ubuntu-latest
needs:
- verify-gate
if: "!cancelled() && !startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch' && ((needs.verify-gate.result != 'success') || (needs.verify-gate.outputs.run == 'true'))"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
purge: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'purge') }}
cache-epoch: "0.1.0-SNAPSHOT"
- name: test
run: sbt 'test'
publish:
name: publish
runs-on: ubuntu-latest
needs:
- cache-rehydrate
- verify
if: "!cancelled() && needs.cache-rehydrate.result != 'failure' && needs.verify.result != 'failure'"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: publish
node-version: ""
sbt-disk-cache: "false"
cache-mode: restore
purge: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'purge') }}
cache-epoch: "0.1.0-SNAPSHOT"
- name: publish
run: sbt 'schema/publish; api/publish'
cache-rehydrate:
name: cache-rehydrate
runs-on: ubuntu-latest
needs:
- verify-gate
if: needs.verify-gate.result == 'success' && needs.verify-gate.outputs.run == 'false'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: cache-rehydrate
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
purge: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'purge') }}
cache-epoch: "0.1.0-SNAPSHOT"
- name: cache-rehydrate
run: sbt 'Test/compile'Sbt image
zipxSbtImage := true runs every sbt job in one pinned image. Default off. The first run in the image misses every
cache entry that was saved under /home/runner.
The pin is the light sbtscala/scala-sbt tag for the major in zipxJavaVersion. The version string is the tag and
the multi-arch manifest digest, so a JDK patch and a digest move are both updates:
Pin(
"sbt-image",
"sbtscala/scala-sbt",
"eclipse-temurin-21.0.12_8_2.x@sha256:<manifest>",
)
A fat tag (<jdk>_<sbt>_<scala>) is not the pin. It warms one Scala compiler into the image, and
project/build.properties still picks sbt. The image is the toolchain: JDK, sbt, curl, git, tar. It does not
contain ~/.sbt, ~/.cache/sbt, ~/.cache/coursier, or a token. Those stay the LocalDir snapshot,
restored into /home/sbtuser. target/ is not in the image and not in the snapshot. Jobs set HOME and
-Duser.home there, and mark every directory git-safe, because
that HOME is not the one actions/checkout configured. Actions overrides HOME, so a ~ path is
not that directory and the cache paths are absolute. setup-sbt's own disk cache stays off.
actions/setup-java, sbt/setup-sbt, actions/setup-node, and coursier/setup-action are omitted on those jobs.
The composite still restores and saves the snapshot.
When a capability sets withNodeVersion, version updates are on (they need cs), or a job publishes images (it
needs the Docker client), zipx writes .github/zipx-env/Dockerfile FROM that pin and bakes only those tools.
sbt-scalajs does not fetch Node. Without withNodeVersion the image has none, and a .jsPlatform test fails
to start node. The published tag is ghcr.io/<owner>/<repo>/zipx-env:df-<hash of the Dockerfile>. A zipx-env-image job builds and
pushes only when the tag is missing. The hash moves when a baked pin moves. latest
and lts are not baked: pin Pin("sbt-image", "node", "<x.y.z>"). Two capabilities asking for two Node versions is
a generate error. testAffected(onPush = true) calls gh from the affected job, and that job runs in this image.
The runner has the GitHub CLI. The image does not, until Pin("sbt-image", "gh", "<x.y.z>") bakes the official
cli/cli tarball. Generate refuses the image without that pin. A build that does not diff pushes omits the pin,
and the Dockerfile does not install the client. Every Linux job container already receives /var/run/docker.sock from the runner. A job that
publishes images also sets that mount. The image has the client, not a daemon. A container that must reach the test
JVM belongs on that test's Docker network. inContainer is refused while this setting is on: the shared image is
already the container. A job that sets its own permissions also gets packages: read, because that block does not
inherit the token that can pull a private env image.
A tool that is not an sbt output (a browser, a codegen CLI, a C toolchain) is a zipxSbtImageLayers entry, not a
file under target/ and not a path under /home/sbtuser. The snapshot does not keep target/, and the cache mount
hides /home/sbtuser. One layer is a Pin("sbt-image", "<id>", "<x.y.z>") plus a function of that version and the
CPU architecture. The function returns download and apt steps. zipx writes every RUN. There is no shell hook, and
the URL does not have to contain the version text: a browser revision in the path is enough, as long as the function
actually uses the version so a bump changes the Dockerfile. An apt step names an exact package version.
apt-get update is the one line that is not a function of that text. The tag freezes the first push, so a later run
with the same Dockerfile does not rebuild. An unpinned apt-get install cannot be expressed. Layer ENV is written
into the published image and onto each sbt job. The cloud agent copies the plain job env into its Dockerfile when
zipx builds one (a daemon, or a derived image). An upstream-only agent uses the public image ref and does not carry
that map. Secrets are not ENV. A second PinFeed named sbt-image is a generate error. The layer carries its own
lookup, and that lookup is what moves the pin.
{
val digest = "ab" * 32
val version = s"eclipse-temurin-21.0.12_8_2.x@sha256:$digest"
val pin = Pin(
PinFeedName.unsafeMake("sbt-image"),
SbtImage.ImageId,
DepVersion.unsafeMake(version),
None,
None,
)
SbtImage.resolve(config.javaVersion, Seq(pin), Nil, coursier = false, docker = false) match
case Left(err) => err.message
case Right(plan) =>
DocsRender.job("test")(Capability.test)(using libGraph, config.copy(sbtImage = Some(plan)))
}test:
name: test
runs-on: ubuntu-latest
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
container: sbtscala/scala-sbt:eclipse-temurin-21.0.12_8_2.x@sha256:abababababababababababababababababababababababababababababababab
env:
HOME: /home/sbtuser
JAVA_OPTS: -Duser.home=/home/sbtuser
SBT_OPTS: -Duser.home=/home/sbtuser
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: safe.directory
GIT_CONFIG_VALUE_0: "*"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
baked-toolchain: "true"
cache-home: /home/sbtuser
coursier: "false"
- name: test
run: sbt 'test'{
val digest = "ab" * 32
val version = s"eclipse-temurin-21.0.12_8_2.x@sha256:$digest"
val base = Pin(
PinFeedName.unsafeMake("sbt-image"),
SbtImage.ImageId,
DepVersion.unsafeMake(version),
None,
None,
)
val widget = Pin(PinFeedName.unsafeMake("sbt-image"), "widgets", DepVersion.unsafeMake("1.2.3"), None, None)
val layer = SbtImageLayer("widgets", _ => Right(None))
.install { (pinned, arch) =>
val name = arch match
case CpuArch.Amd64 => "x86_64"
case CpuArch.Arm64 => "aarch64"
List(
ImageStep.Tree(
url = s"https://example.test/widgets/$pinned/widgets-$name.tar.gz",
dest = "/opt/widgets",
format = ArchiveFormat.TarGz,
strip = 1,
),
ImageStep.EmptyFile("/opt/widgets/READY"),
ImageStep.Apt("libwidgets", "1.2.3-1"),
)
}
.withEnv("WIDGET_PATH" -> "/opt/widgets")
SbtImage.resolve(config.javaVersion, Seq(base, widget), Nil, coursier = false, docker = false, Seq(layer)) match
case Left(err) => err.message
case Right(plan) =>
plan.dockerfile.getOrElse("") + "\n" +
DocsRender.job("test")(Capability.test)(using libGraph, config.copy(sbtImage = Some(plan)))
}# Generated by zipx. Do not edit. Run 'sbt zipxWorkflowGenerate' to regenerate.
FROM sbtscala/scala-sbt:eclipse-temurin-21.0.12_8_2.x@sha256:abababababababababababababababababababababababababababababababab
USER root
RUN set -eux; \
case "$(uname -m)" in \
x86_64) url='https://example.test/widgets/1.2.3/widgets-x86_64.tar.gz' ;; \
aarch64) url='https://example.test/widgets/1.2.3/widgets-aarch64.tar.gz' ;; \
*) echo "zipx: unsupported architecture $(uname -m)" >&2; exit 1 ;; \
esac; \
mkdir -p -- '/opt/widgets'; \
curl -fsSL -o /tmp/zipx-layer "$url"; \
tar -xzf /tmp/zipx-layer -C '/opt/widgets' --strip-components=1; \
rm -f /tmp/zipx-layer
RUN set -eux; \
mkdir -p -- '/opt/widgets'; \
: > '/opt/widgets/READY'
RUN set -eux; \
set -f; \
apt-get update; \
apt-get install -y --no-install-recommends 'libwidgets=1.2.3-1'; \
set +f; \
rm -rf /var/lib/apt/lists/*
ENV WIDGET_PATH="/opt/widgets"
WORKDIR /root
test:
name: test
runs-on: ubuntu-latest
needs:
- zipx-env-image
if: "!startsWith(github.ref, 'refs/tags/') && github.event_name != 'workflow_dispatch'"
container: ${{ needs.zipx-env-image.outputs.image }}
env:
WIDGET_PATH: /opt/widgets
HOME: /home/sbtuser
JAVA_OPTS: -Duser.home=/home/sbtuser
SBT_OPTS: -Duser.home=/home/sbtuser
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: safe.directory
GIT_CONFIG_VALUE_0: "*"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: "0"
fetch-tags: "true"
- name: zipx sbt setup
uses: ./.github/actions/zipx-sbt-setup
with:
java-version: "21"
runner-os: ubuntu-latest
cache-key-suffix: test
node-version: ""
sbt-disk-cache: "false"
cache-mode: save
cache-epoch: "0.1.0-SNAPSHOT"
baked-toolchain: "true"
cache-home: /home/sbtuser
coursier: "false"
- name: test
run: sbt 'test'Action pins
Generated workflows use commit-SHA pins (not floating @v4 tags), with # vX.Y.Z comments for readability.
Catalog Action vals overlay jar defaults. Full guide: Action pins (overlay, zipxActionUpdate, leftover YAML,
jar defaults from the last zipx compile).