Compare commits

...

2 Commits

Author SHA1 Message Date
Fraser
c55483d2ad Quote GITHUB_OUTPUT redirect in uberjar workflow (shellcheck SC2086)
Pre-existing issue surfaced by actionlint once this file was touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 16:22:31 +12:00
Fraser
23c541f613 Build frontend and backend uberjar concurrently behind MB_PARALLEL_BUILD
Opt-in via MB_PARALLEL_BUILD=true (enabled in uberjar.yml): when running the
full step set, the cheap steps (version, translations, licenses, python) run
sequentially up front, then the frontend build forks onto a future while the
backend runs namespace topo-sort + AOT compilation. The uberjar step joins on
the frontend build after compile-sources! and before copy-resources! — the
frontend writes to resources/frontend_client, which the resource copy sweeps
into the jar, so that join point is the only ordering constraint.

Sequential behavior is unchanged when the flag is unset or a step subset is
requested.

Validated locally with A/B builds: parallel and sequential jars have identical
entry sets (modulo rspack content-hash filenames), all entries outside AOT
classes and FE bundles byte-identical, and the residual class/bundle diffs are
indistinguishable from the noise floor between two sequential builds (Clojure
AOT bytecode ordering and rspack hash churn are nondeterministic run to run).
Post-build, every file under resources/frontend_client is byte-identical to
its jar entry, and the jar boots with /api/health OK and the static-viz bundle
present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 16:20:14 +12:00
3 changed files with 54 additions and 11 deletions

View File

@@ -78,7 +78,7 @@ jobs:
echo "Skipping build: artifacts exist for this commit hash"
fi
echo "should_build=$SHOULD_BUILD" >> $GITHUB_OUTPUT
echo "should_build=$SHOULD_BUILD" >> "$GITHUB_OUTPUT"
# if this is on a release branch, we need to check the build requirements
get-build-requirements:
@@ -127,6 +127,7 @@ jobs:
env:
MB_EDITION: ${{ matrix.edition }}
INTERACTIVE: false
MB_PARALLEL_BUILD: "true"
SKIP_LICENSES: ${{ github.event_name == 'pull_request' }} # faster builds for dev
# Emit bundle stats on trunk/release builds (tracked over time) and on PRs
# (the bundle-size check reads them for both the PR head and its base ref).

View File

@@ -96,6 +96,37 @@
:uberjar (fn [{:keys [edition]}]
(build-uberjar! edition))))
(defn- build-parallel!
"Run the full build with the frontend bundle building concurrently with backend AOT compilation. The uberjar step
joins on the frontend build after compiling Clojure sources, right before resources (which include the frontend
build output under resources/frontend_client) are copied into the jar.
The cheap steps (version, translations, licenses, python) run sequentially up front: licenses invokes bun, and
running it concurrently with the frontend build's `bun install` could race on node_modules."
[{:keys [edition version]}]
(let [timer (u/start-timer)]
(version-properties/generate-version-properties-file! edition version)
(i18n/create-all-artifacts!)
(build-licenses! edition)
(build.python/build-python-deps! edition)
(u/announce "Did pre-fork steps in %d ms." (u/since-ms timer))
(let [frontend (future
(try
(build-frontend! edition)
::success
(catch Throwable e
e)))]
(u/delete-file-if-exists! uberjar/uberjar-filename)
(u/step (format "Build uberjar with profile %s (frontend building in parallel)" edition)
(uberjar/uberjar {:edition edition
:await-frontend! (fn []
(let [result @frontend]
(when (instance? Throwable result)
(throw result))))})
(u/assert-file-exists uberjar/uberjar-filename)
(u/announce "Uberjar built successfully."))
(u/announce "Did parallel frontend + uberjar in %d ms." (u/since-ms timer)))))
(defn build!
"Programmatic entrypoint."
([]
@@ -106,20 +137,24 @@
steps (keys all-steps)}}]
(let [version (or version
(version-properties/current-snapshot-version edition))
timer (u/start-timer)]
steps (map u/parse-as-keyword steps)
timer (u/start-timer)]
(u/step (format "Running build steps for %s version %s: %s"
(case edition
:oss "Community (OSS) Edition"
:ee "Enterprise Edition")
version
(str/join ", " (map name steps)))
(doseq [step-name steps
:let [step-fn (or (get all-steps (u/parse-as-keyword step-name))
(throw (ex-info (format "Invalid step: %s" step-name)
{:step step-name
:valid-steps (keys all-steps)})))]]
(step-fn {:version version, :edition edition})
(u/announce "Did %s in %d ms." step-name (u/since-ms timer)))
(if (and (= (env/env :mb-parallel-build) "true")
(= (set steps) (set (keys all-steps))))
(build-parallel! {:version version, :edition edition})
(doseq [step-name steps
:let [step-fn (or (get all-steps step-name)
(throw (ex-info (format "Invalid step: %s" step-name)
{:step step-name
:valid-steps (keys all-steps)})))]]
(step-fn {:version version, :edition edition})
(u/announce "Did %s in %d ms." step-name (u/since-ms timer))))
(u/announce "All build steps finished.")))))
(defn build-cli

View File

@@ -263,13 +263,20 @@
"Build just the uberjar (no i18n, FE, or anything else). You can run this from the CLI like:
clojure -X:build:build/uberjar
clojure -X:build:build/uberjar :edition :ee"
[{:keys [edition], :or {edition :oss}}]
clojure -X:build:build/uberjar :edition :ee
`:await-frontend!`, when provided, is called after Clojure compilation and before resources are copied into the
jar — the frontend build writes its output under resources/frontend_client, so a concurrently-running frontend
build must be joined here for its output to make it into the jar."
[{:keys [edition await-frontend!], :or {edition :oss}}]
(u/step (format "Build %s uberjar" edition)
(with-duration-ms [duration-ms]
(clean!)
(let [basis (create-basis edition)]
(compile-sources! basis)
(when await-frontend!
(u/step "Wait for concurrent frontend build to finish"
(await-frontend!)))
(copy-resources! basis)
(create-uberjar! basis)
(add-non-aot-driver-sources!)