Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
fcc7ef0
chore(repo): enforce conventional commits and record release ADR
serialexperimentslainnnn Aug 5, 2026
9dd6c3e
feat(licensing): ship third-party attribution inside the artifact
serialexperimentslainnnn Aug 5, 2026
0e40116
feat(a11y): announce turn state and give keyboard focus a visible ring
serialexperimentslainnnn Aug 5, 2026
4b306a1
fix(deps): declare the protocol SDK as build tooling, not a dependency
serialexperimentslainnnn Aug 5, 2026
6eda134
docs(security): write down the adversary SensitiveGuard defends against
serialexperimentslainnnn Aug 5, 2026
3911662
build: mechanise the quality bar and enforce it on protected branches
serialexperimentslainnnn Aug 5, 2026
a7a5c18
feat: surface plan limits, and fix the defects the new tooling exposed
serialexperimentslainnnn Aug 5, 2026
6eea820
docs: record the 5.0.0 standards work, its gaps and what was measured
serialexperimentslainnnn Aug 5, 2026
26c4000
docs: record merge-commit-only, and why it is a signing decision
serialexperimentslainnnn Aug 5, 2026
b497b85
fix(test): parse shell.html with a parser, not regexes
serialexperimentslainnnn Aug 5, 2026
ffd0b87
fix: repair the tab-killing NPE and the silences it hid
serialexperimentslainnnn Aug 5, 2026
1d633a6
build: keep checkDrift out of the coverage graph
serialexperimentslainnnn Aug 5, 2026
4a86da6
build: revert the platform plugin bump, it hangs the headless suite
serialexperimentslainnnn Aug 5, 2026
33c1c9b
Merge pull request #16 from serialexperimentslainnnn/feature/release-…
serialexperimentslainnnn Aug 5, 2026
b5f97ab
feat(ci): release on merge to main, version read from the code
serialexperimentslainnnn Aug 5, 2026
9ad75f5
ci: enforce the zero-deprecation rule and stop duplicating work
serialexperimentslainnnn Aug 5, 2026
79aee8b
build(deps): group Dependabot updates instead of one PR per bump
serialexperimentslainnnn Aug 5, 2026
c820ea3
style: format the failureLevel assignment per ktlint
serialexperimentslainnnn Aug 5, 2026
d686836
Merge remote-tracking branch 'origin/develop' into feature/update-pip…
serialexperimentslainnnn Aug 5, 2026
a1997de
Merge pull request #26 from serialexperimentslainnnn/feature/update-p…
serialexperimentslainnnn Aug 5, 2026
f5bca7d
build(deps): bump org.junit:junit-bom from 5.11.4 to 6.1.2
dependabot[bot] Aug 5, 2026
adac8f8
build(deps): bump actions/download-artifact in the actions group
dependabot[bot] Aug 5, 2026
2af4aac
Merge pull request #29 from serialexperimentslainnnn/dependabot/githu…
serialexperimentslainnnn Aug 5, 2026
573054c
Merge branch 'develop' into dependabot/gradle/org.junit-junit-bom-6.1.2
serialexperimentslainnnn Aug 5, 2026
57a9456
ci: stop re-running every PR on each merge, and cache on topic branches
serialexperimentslainnnn Aug 5, 2026
9398de2
ci: run the verifier only on develop and main
serialexperimentslainnnn Aug 6, 2026
20184bd
ci: put the exhaustive gate on the develop -> main door
serialexperimentslainnnn Aug 6, 2026
c66700b
ci: only the two test suites gate a branch; everything gates main
serialexperimentslainnnn Aug 6, 2026
3ab2e94
Merge pull request #28 from serialexperimentslainnnn/dependabot/gradl…
serialexperimentslainnnn Aug 6, 2026
b518a21
Merge pull request #31 from serialexperimentslainnnn/feature/update-p…
serialexperimentslainnnn Aug 6, 2026
30a80b8
ci: trigger on pull requests only, never on push
serialexperimentslainnnn Aug 6, 2026
a0e8a9a
ci: run the Gradle and Node jobs in the prebuilt image
serialexperimentslainnnn Aug 6, 2026
fbfb64d
ci: pull the private image with the run's own token
serialexperimentslainnnn Aug 6, 2026
1249fbb
fix(ci-image): bake the npm cache, not node_modules
serialexperimentslainnnn Aug 6, 2026
2fad4bc
ci: run every job in the CI image and drop the caching action
serialexperimentslainnnn Aug 6, 2026
a3eb5d6
Merge branch 'develop' of github.com:serialexperimentslainnnn/claude-…
serialexperimentslainnnn Aug 6, 2026
04957a8
ci: drop the verifier IDEs from the image and fix the warm-up
serialexperimentslainnnn Aug 6, 2026
d37cb4b
Merge pull request #33 from serialexperimentslainnnn/feature/update-p…
serialexperimentslainnnn Aug 6, 2026
5b29eea
ci: cut the release tag before the build and release from it
serialexperimentslainnnn Aug 6, 2026
f1ce6c3
ci: require CodeQL on develop, not only on main
serialexperimentslainnnn Aug 6, 2026
53ff9df
fix(ci): strip every _comment key before applying a ruleset
serialexperimentslainnnn Aug 6, 2026
9d1a49d
ci: block the release door while a bot PR is open on develop
serialexperimentslainnnn Aug 6, 2026
769c5f6
ci: split the image so each job pulls only what it runs
serialexperimentslainnnn Aug 6, 2026
df99ffe
fix(ci): give CodeQL's tracer the name Fedora's loader expands to
serialexperimentslainnnn Aug 6, 2026
284233e
fix(ci): derive the tracer path from LD_PRELOAD, not from a glob
serialexperimentslainnnn Aug 6, 2026
7aca5e5
fix(ci): make CodeQL's LD_PRELOAD resolve on both sides of the container
serialexperimentslainnnn Aug 6, 2026
5c35d99
Merge pull request #36 from serialexperimentslainnnn/feature/update-p…
serialexperimentslainnnn Aug 6, 2026
ca0bb75
fix(release): make the tag step idempotent for job re-runs
serialexperimentslainnnn Aug 6, 2026
8a4d9fd
Merge pull request #37 from serialexperimentslainnnn/bugfix/release-t…
serialexperimentslainnnn Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Build context filter for .github/ci-image/Dockerfile.
#
# The `verify` stage does a `COPY . .` because `verifyPlugin` needs the real sources to resolve which IDEs
# to download. Without this file that COPY was a 2.09 GB layer: it swept in node_modules, build/ and .git,
# none of which the warm-up reads, and all of which stay in the image forever — a later `rm -rf` adds a
# layer, it never reclaims one.
#
# Anything the Gradle build genuinely needs must NOT be listed here. When in doubt, leave it in: a missing
# source file makes the warm-up silently resolve a different IDE set, which is the failure mode that looks
# like the cache "just not working".

# Reproduced from the lockfile by `npm ci` inside the image, and it MUST match the lockfile of whatever
# commit CI checks out rather than the one current when the image was cut.
node_modules/

# Outputs, not inputs.
build/
out/
.gradle/

# History is not a build input, and it is the single largest thing here after node_modules.
.git/
.github/ci-image/

# Local IDE and editor state.
.idea/
*.iml

# Never let a local secret or env file reach a layer.
.env
.env.*
*.log
51 changes: 51 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Line endings and binary handling (git-workflow-standards §5.2, §5.3).
#
# This repository is developed on Linux but SHIPS AND IS TESTED ON WINDOWS (ClaudeBinaryLocator resolves npm,
# scoop, volta and chocolatey install paths; TerminalLauncher emits a PowerShell call operator). Without an
# explicit policy, a contributor cloning on Windows with core.autocrlf=true rewrites every checked-in file's
# line endings, which turns a one-line change into a whole-file diff and silently breaks anything that is
# byte-sensitive. Normalisation is declared here so it does not depend on each developer's local git config.

# Default: let Git decide what is text, and store text as LF in the repository.
* text=auto eol=lf

# --- Scripts whose line endings are load-bearing -----------------------------------------------------------
# A shell script or the Gradle wrapper with CRLF fails at exec time with a confusing "bad interpreter" error.
*.sh text eol=lf
gradlew text eol=lf
bin/fake-claude text eol=lf

# Windows-native scripts must keep CRLF or cmd.exe/PowerShell can misparse them.
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf

# --- Binary: never diffed, never line-ending-converted ------------------------------------------------------
# Plugin distribution artifacts and their detached signatures: a single byte of mangling invalidates the
# GPG signature and the SHA-256 the release publishes.
*.zip binary
*.jar binary
*.asc binary
*.gpg binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg text eol=lf
*.woff binary
*.woff2 binary

# --- Diff readability --------------------------------------------------------------------------------------
# Vendored frontend libraries (marked, DOMPurify, highlight.js) are minified single-line bundles. Marking them
# linguist-vendored keeps them out of the language stats, and -diff stops a bundle bump from rendering as an
# unreadable multi-thousand-column diff nobody can review.
src/main/resources/jcef/marked.min.js linguist-vendored -diff
src/main/resources/jcef/purify.min.js linguist-vendored -diff
src/main/resources/jcef/highlight.min.js linguist-vendored -diff

# Lockfiles are generated: reviewers should read the manifest change, not the lockfile churn.
package-lock.json -diff linguist-generated

# The SDK reference is protocol documentation we vendor but do not author or ship.
node_modules/** linguist-vendored
64 changes: 64 additions & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Conventional Commits gate (git-workflow-standards §3.2). Local, because this repo has no CI gate yet.
#
# Enable once per clone: git config core.hooksPath .githooks
# The hook is VERSIONED so the rule travels with the repository instead of living in one laptop's .git/hooks,
# where it is invisible to everyone else and lost on the next clone.
#
# Design rule: this hook may block a BAD MESSAGE, but it must never block because the tool itself is broken.
# Those two failures are indistinguishable from an exit code, so the hook SELF-TESTS first against a message
# known to be valid. If that self-test fails, commitlint (or its runtime) is at fault, not the author — warn
# and let the commit through. A hook that fails closed on its own bugs gets bypassed with --no-verify within a
# day, and after that it protects nothing.
set -uo pipefail

msg_file="$1"
cli="node_modules/.bin/commitlint"

[ -x "$cli" ] || {
echo "commit-msg: commitlint not installed (npm install) — Conventional Commits check skipped." >&2
exit 0
}

run_lint() {
"$cli" --edit "$1" 2>&1
}

# --- self-test: can the tool validate a message we know is well-formed? -------------------------------------
# This decides TWO things at once: whether commitlint works at all, and — because Node 24 aborts on some
# hosts' system OpenSSL config (a documented local quirk, absent from clean images) — whether this host needs
# OPENSSL_CONF neutralised. Settling that here, on a message known to be valid, means the real check below
# runs exactly ONCE. Retrying the real check instead would print the whole failure report twice.
probe="$(mktemp)"; trap 'rm -f "$probe"' EXIT
printf 'chore: commitlint self-test\n' > "$probe"
if ! run_lint "$probe" >/dev/null 2>&1; then
export OPENSSL_CONF=/dev/null
if ! run_lint "$probe" >/dev/null 2>&1; then
echo "commit-msg: commitlint could not run (toolchain issue, not your message) — check skipped." >&2
exit 0
fi
fi

# --- the real check ----------------------------------------------------------------------------------------
if output="$(run_lint "$msg_file")"; then
exit 0
fi

echo "$output" >&2
cat >&2 <<'EOF'

The commit message is not a Conventional Commit.

<type>[optional scope]: <description>

feat: a user-visible capability -> minor
fix: a user-visible bug fix -> patch
docs, refactor, perf, test, build, ci, chore -> no version bump
Breaking: add ! after the type, or a "BREAKING CHANGE:" footer -> major

This is not style policing: the CHANGELOG and the version bump are derived from these
messages, and the release tooling SILENTLY SKIPS what it cannot parse. An unparseable
message is a change that never appears in a release note.

EOF
exit 1
8 changes: 3 additions & 5 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
blank_issues_enabled: false

contact_links:
# TODO: replace with the actual JetBrains Marketplace URL once the listing
# short-link is confirmed.
- name: JetBrains Marketplace review
url: https://plugins.jetbrains.com/plugin/dev.lain.claude-code-for-jetbrains
url: https://plugins.jetbrains.com/plugin/31965-claude-code-native/reviews
about: Leave a rating or short review on the Marketplace listing.

# TODO: replace with the GitHub Discussions URL once enabled for the repo.
Expand All @@ -15,5 +13,5 @@ contact_links:
about: Ask a question, share a workflow, or discuss ideas before filing an issue.

- name: Security vulnerability
url: https://github.com/serialexperimentslainnnn/claude-code-for-jetbrains/security/policy
about: Do NOT open a public issue. See SECURITY.md and email lain.agent604@passmail.com.
url: https://github.com/serialexperimentslainnnn/claude-code-for-jetbrains/security/advisories/new
about: Do NOT open a public issue. Report it privately here; see SECURITY.md.
39 changes: 32 additions & 7 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,53 @@ Closes #<issue-number> <!-- or "Refs #..." / "n/a" -->
- [ ] Docs / build / CI
- [ ] Security fix

## Risk and rollback

**Risk:** what breaks if this is wrong, and for whom? (`none` is a valid
answer for docs-only changes — say so rather than leaving it blank.)

**Rollback:** how is this undone once released? Reverting the commit is not a
rollback for a published plugin — a user on the bad version stays there until
they update. If the change touches persisted settings, the transcript format,
or the permission surface, say what happens to a user who already ran it.

## Checklist

- [ ] PR targets the `develop` branch (or `main` only for hotfixes).
- [ ] Commits follow Conventional Commits (the `commit-msg` hook enforces it —
install once with `git config core.hooksPath .githooks`).
- [ ] `./gradlew test verifyPlugin buildPlugin` passes locally.
- [ ] `verifyPlugin` is **Compatible** with IU-261 and IU-262 and reports
no new internal-API usage (`@ApiStatus.Internal`).
- [ ] `verifyPlugin` is **Compatible** across the declared range (251 → 263.\*)
and reports no new internal-API usage (`@ApiStatus.Internal`).
The CDN download is unreliable here; use
`-PlocalIdePath=<dir>[,<dir>…]` with locally-extracted IDEs.
- [ ] No new deprecated or scheduled-for-removal IntelliJ Platform APIs.
- [ ] Tests added or updated under `src/test/kotlin/...` for the new
behaviour.
- [ ] Tests added or updated for the new behaviour — `src/test/kotlin/…` for
Kotlin, `src/test/frontend/…` (`npm test`) for anything under
`src/main/resources/jcef/`.
- [ ] Protocol changes: `./gradlew checkDrift` is green and the baseline in
`scripts/drift-baseline.properties` matches what was verified.
- [ ] New dependency? Its licence is compatible with GPL-3.0-only and it is
recorded in [`THIRD-PARTY-NOTICES.md`](../THIRD-PARTY-NOTICES.md) if it
ships in the artifact.
- [ ] User-visible changes are documented in [`CHANGELOG.md`](../CHANGELOG.md)
and [`RELEASE_NOTES.md`](../RELEASE_NOTES.md) under `Unreleased`.
- [ ] No secrets, tokens, conversation transcripts, or personal absolute
paths in the diff or commit messages.
- [ ] Follows the conventions in [`CONTRIBUTING.md`](../CONTRIBUTING.md)
and the architectural contract in [`CLAUDE.md`](../CLAUDE.md).
- [ ] Follows the conventions in [`CONTRIBUTING.md`](../CONTRIBUTING.md), the
architectural contract in [`CLAUDE.md`](../CLAUDE.md), and the recorded
decisions in [`docs/adr/`](../docs/adr/README.md).

## How was this tested?

- [ ] Unit tests (`./gradlew test`)
- [ ] Unit tests (`./gradlew test`) and frontend tests (`npm test`)
- [ ] Manual sandbox (`./gradlew runIde`) — describe the scenarios you
exercised.
- [ ] Smoke test on a real IDE install — describe.
- [ ] **UI changes only:** driven with the keyboard alone, with the focus ring
visible on every control touched. Automated checks catch roughly half of
real accessibility barriers and none of the judgement calls, so this one
is not delegable to a tool.

## Notes for reviewers

Expand Down
141 changes: 141 additions & 0 deletions .github/ci-image/jvm-test.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# jvm-test — the CI image for every job that runs Gradle. Built ON TOP of node-test.
#
# WHO USES IT
# `JVM tests`, `Static analysis` and `Plugin verifier` in ci.yml, `CodeQL (java-kotlin)`, the weekly drift
# check, and the release gate in release.yml.
#
# WHY IT IS BUILT FROM node-test RATHER THAN FROM fedora
# Two reasons, and the second is the one that matters.
#
# 1. No duplication. The base package list and the npm cache warm-up are written once, in
# node-test.Dockerfile. Two standalone files would have to keep them in step by hand, and the failure
# mode of that is not a build error — it is two images that quietly disagree about the Node version.
# 2. It needs Node anyway. Three of the jobs above run Gradle AND npm in a single job: `Static analysis`
# (detekt and spotless, then eslint and prettier), `drift` (`npm install` then `checkDrift`), and the
# release gate (`npm test` then `test verifyPlugin`). Splitting Node out would mean splitting those jobs
# in two, and a new job is a whole extra image pull — the exact cost this segmentation exists to remove.
#
# The registry stores the shared layers ONCE, so this is not a second copy of the small image. A job that
# needs neither image runs on a bare runner and is not served from here at all — `Build plugin` is the
# example: it downloads an artifact and runs `unzip`, and used to pull GB to do it.
#
# WHAT IS DELIBERATELY NOT IN HERE: THE VERIFIER'S IDEs
# Baking what `verifyPlugin` downloads once made the single image 38.1 GB, 29.1 GB of it extracted IDEs.
# The verifier is their only consumer and runs ONLY on a pull request from develop into main — a handful of
# times a month. Paying 29 GB on every job's pull, permanently, to save ten minutes on the rarest job is the
# wrong side of that trade by two orders of magnitude. There is a second reason that would have bitten
# silently: the verifier resolves IDEs from the EAP/RC channels, so the set MOVES, and the day JetBrains
# publishes a new build the baked copies stop matching and Gradle downloads the new one anyway.
#
# BUILDING — node-test FIRST, since this image starts from it. From the repository ROOT, so /.dockerignore
# applies:
#
# V=v1.0.0
# docker build -f .github/ci-image/node-test.Dockerfile -t ghcr.io/OWNER/node-test:$V .
# docker build -f .github/ci-image/jvm-test.Dockerfile -t ghcr.io/OWNER/jvm-test:$V \
# --build-arg NODE_IMAGE=ghcr.io/OWNER/node-test:$V .
# docker push ghcr.io/OWNER/node-test:$V
# docker push ghcr.io/OWNER/jvm-test:$V
#
# The tag is `vMAJOR.MINOR.PATCH`, never `latest`. Bumping it is a commit: change the tag here and in every
# workflow that references it, so the two move together in one reviewable diff. Both images share a version
# because this one is derived from that one — they are not independently versionable.

# Declared before FROM so it can be used there. The default names this repository's own package; a fork
# overrides it with --build-arg rather than editing the file.
ARG NODE_IMAGE=ghcr.io/serialexperimentslainnnn/node-test:v1.0.0
FROM ${NODE_IMAGE}

# NB there is no dnf tuning here and that is not an omission: `max_parallel_downloads=20` and
# `fastestmirror=True` were written into /etc/dnf/dnf.conf by node-test, and this image starts from its
# filesystem — so the JDK transaction below already runs with them. Adding the lines again would append a
# SECOND copy of each key to dnf.conf rather than overriding anything.
#
# Temurin, not Fedora's OpenJDK.
#
# Fedora 44 no longer packages java-21-openjdk — it has moved on to a newer LTS — and the JDK version is not
# ours to float: build.gradle.kts pins the toolchain to 21 because the IDE runs on JBR 21, which is the
# ceiling. Building on 25 would produce class files no target IDE can load. Adoptium's repository is the
# same source the `setup-java` action uses on the hosted runners, so the image and the pipeline compile
# against the same JDK rather than two different builds of "21".
#
# There is deliberately no `dnf-plugins-core`: nothing here calls `dnf config-manager` — the repo file is
# written with `printf` — and `curl` is already in the base image, so installing it dragged in a ~150 MB
# Python stack to run a command nobody ran.
#
# `python3` is EXPLICIT, and that is a correctness requirement rather than a convenience: `bin/fake-claude`,
# the deterministic stand-in the integration tests drive a real ClaudeSession against, is a
# `#!/usr/bin/env python3` script. It used to arrive only as a transitive dependency of that unused package
# — a load-bearing dependency held up by an accident.
#
# `zip` is added here rather than in node-test because only the Gradle side packages archives.
RUN curl -fsSL https://packages.adoptium.net/artifactory/api/gpg/key/public \
-o /etc/pki/rpm-gpg/RPM-GPG-KEY-Adoptium \
&& rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-Adoptium \
&& printf '%s\n' \
'[Adoptium]' \
'name=Adoptium' \
'baseurl=https://packages.adoptium.net/artifactory/rpm/fedora/$releasever/$basearch' \
'enabled=1' \
'gpgcheck=1' \
'gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Adoptium' \
> /etc/yum.repos.d/adoptium.repo \
&& dnf -y --setopt=install_weak_deps=False --setopt=tsflags=nodocs install \
temurin-21-jdk \
python3 \
zip \
&& dnf clean all \
&& rm -rf /var/cache/dnf \
&& rm -rf /usr/share/locale

# JAVA_HOME is resolved rather than hardcoded: the exact path carries the package's build number and would
# silently break on the next base-image bump. The symlink keeps the ENV below stable across rebuilds.
RUN JH="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")" \
&& echo "JAVA_HOME=$JH" >> /etc/environment \
&& ln -sfn "$JH" /opt/java-21 \
&& "$JH/bin/java" -version
ENV JAVA_HOME=/opt/java-21
ENV PATH="${JAVA_HOME}/bin:${PATH}"

# Gradle writes here, and the path MUST match GRADLE_USER_HOME in the workflow. If they diverge, the warm
# cache below is invisible and every run silently re-resolves what this image already has.
ENV GRADLE_USER_HOME=/opt/gradle-home

# The npm cache is inherited from node-test — `npm_config_cache=/opt/npm-cache` and the warmed store are
# already in the layers below this one, so `Static analysis`, `drift` and the release gate get it for free.
WORKDIR /warmup

# The build definition first, on purpose: this layer is invalidated by a dependency change, not by every
# edit to the Kotlin sources.
COPY gradle/ gradle/
COPY gradlew settings.gradle.kts build.gradle.kts gradle.properties* ./

# Downloads the Gradle distribution itself. Kept separate from the warm-up below so a network problem here
# is distinguishable from a build problem there.
RUN ./gradlew --no-daemon --version

# The sources, needed because the warm-up below compiles. Filtered by /.dockerignore, so this is ~3 MB of
# Kotlin and resources rather than the 2 GB it was with node_modules, build/ and .git swept in.
COPY . .

# THE WARM-UP, and the reason it is `testClasses` rather than `dependencies`.
#
# It used to be `./gradlew dependencies --configuration compileClasspath > /dev/null 2>&1 || true`, and that
# command does NOT warm this cache. It resolves dependency METADATA; it never triggers the artifact
# transform that EXTRACTS the IntelliJ Platform, which is where the several GB actually are. Measured: that
# command leaves caches/*/transforms at 179 MB with no extracted IDE in it. The image looked warm and every
# job re-downloaded and re-extracted the platform — invisibly, because of the redirect and the `|| true`.
#
# `testClasses` compiles main and test sources, so it resolves AND extracts everything the Gradle jobs need.
#
# No `> /dev/null`, and no `|| true`. A warm-up that fails must fail the image build: the whole point of
# this image is the cache, so "the cache step failed but the image is fine" is not a state worth being able
# to reach. Verified with the network disabled — `testClasses` compiles offline in 33s from this cache.
RUN ./gradlew --no-daemon testClasses

# The sources were only ever scaffolding; keeping them would ship a stale copy of the repository inside the
# image, which someone would eventually mistake for the real one. This does not reclaim the space (layers
# are additive) — it prevents the confusion.
RUN rm -rf /warmup/* /warmup/.[!.]* 2>/dev/null || true

WORKDIR /workspace
Loading