Build, release and dependencies#
Jib with a pinned Java 25 base image#
Decision. The backend image is built by Jib onto a pinned
eclipse-temurin:25.0.4.1_1-jre-ubi10-minimal. All four Dockerfiles Quarkus generated under
backend/src/main/docker/ have been deleted, along with backend/.dockerignore; the
directory no longer exists.
Why. Jib needs no Dockerfile and no daemon-side build. The pin is not optional: Jib's default base ships JDK 21 and the container exited silently on class file version 69.
Why the Dockerfiles were deleted rather than left alone. The two JVM ones were
ubi9/openjdk-17 based, so building from either would have failed on class file version 69 in
exactly the way the pin exists to prevent — a trap for anyone who found them before finding
this page. The two native ones were no better: equally unbuilt, and none of the four was
ever exercised by a test or by CI, so nothing would have caught them rotting. Which is what
happened — they sat at JDK 17 while the project moved to 25. They also cost review time:
Renovate raised pull requests to advance the openjdk-17 and ubi-minimal tags on files
nothing built. Verified before deleting: the only references to any of them were inside their
own comment headers, every docker build in the repository targets the frontend image, the
native profile also builds through Jib, and
./mvnw package -Dquarkus.container-image.build=true still produces the image afterwards on
the same base image digest.
Temurin, on a Red Hat base — which is both things at once. This entry twice said something
narrower. It first preferred UBI minimal with a JDK installed on top; then, when that was
settled, it said a plain Temurin image and not UBI. -ubi10-minimal is the resolution rather
than a reversal: the JRE is the same Temurin build the project compiles and tests with, so
there is still one Java version to track, and the base underneath it is Red Hat's, which is
what the preference for UBI was ever about. Installing a JDK by hand was the part worth
dropping, not the base.
Why the tag is pinned to an exact build. 25-jre-ubi10-minimal moves, so two builds of the
same commit could sit on different bases and only one of them fail. 25.0.4.1_1-jre-ubi10-minimal
does not move.
How a pin that nobody edits stays current. Renovate, through a custom regex manager in
.github/renovate.json. No built-in manager reads the value: it lives in a Quarkus properties
file, and moving it into pom.xml would not have helped, because the Maven manager updates
dependency versions rather than container references. The manager was checked against the real
file rather than assumed — it resolves eclipse-temurin and the tag, with the docker
datasource.
sun_checks with documented relaxations#
Decision. Checkstyle's sun_checks.xml as the style baseline, with a short list of
relaxations recorded in the header of backend/checkstyle.xml, and type-level Javadoc
required via MissingJavadocType.
Why. A standard ruleset beats a bespoke one, and the relaxations that remain each have a stated reason rather than being silently dropped. Per-member Javadoc stays optional because it fights JAX-RS and CDI code; type-level Javadoc is required because "what is this type for?" is the question that code cannot answer for itself.
Apache License 2.0, as a LICENSE file only#
What. The full Apache-2.0 text as LICENSE, verbatim from apache.org, plus a copyright
line in the README. No per-file license headers, and no enforcement of them.
Why. A permissive licence that grants patent rights and is well understood suits a project whose point is to be read. The file is unmodified, including the appendix that carries the header boilerplate template — Apache asks that the text not be altered, so the actual copyright statement lives in the README instead of being substituted into the appendix.
Rejected. Adding the boilerplate header to every .java and .ts file and failing the
build on a file without one. Every other quality convention here is enforced in CI, so this
is a genuine exception: it would touch every source file in the repository to restate
something one file already says, and this is a single-author demo project rather than a
codebase whose files get copied out individually. Also rejected, for now: a NOTICE file,
which matters when redistributing someone else's Apache-licensed work and there is none here.
A release is a git tag, and the version lives nowhere else#
What. Pushing v1.2.3 runs release.yml, which builds at that version and publishes
container images tagged 1.2.3. backend/pom.xml declares <version>${revision}</version>
and the release build passes -Drevision=1.2.3. No file in the repository records a release
version, before or after.
Why. The alternative flows all require editing a version and committing it — either by
hand before tagging, or by a workflow that rewrites pom.xml and package.json and pushes
back to main. The second needs a way around the main-branch ruleset that rejects direct
pushes, and it means a release mutates the branch it is releasing. Deriving the version from
the tag removes the bookkeeping entirely: main is permanently 1.0.0-SNAPSHOT and nobody
has to remember to open the next development version.
Why no Maven artifact. Nothing outside this repository consumes the backend's POM, so
there is no reason to publish to a Maven repository, and the one real caveat of
${revision} — that an installed POM keeps the literal property unless
flatten-maven-plugin rewrites it — does not bite. It would have to be added if that ever
changes.
Cost. latest still means the tip of main, because publish-images.yml already
pushed it there on every merge and a release moving it would have the two workflows fighting
over one tag. That is the opposite of the usual container convention and is the single most
likely thing to surprise someone. doc/releasing.md says so and says where the change would
go.
SNAPSHOT dependencies fail every build, not just releases#
What. requireReleaseDeps runs at validate on every backend build. requireReleaseVersion
is release-only, in the release profile. The frontend's counterpart,
scripts/check-no-prerelease-deps.mjs, fails on a semver pre-release anywhere in
package-lock.json.
Why. The request was for a check that a release has no SNAPSHOT dependencies, and the
narrow reading would put it only in the release path. But a SNAPSHOT dependency is wrong the
moment it is added — it makes the build unreproducible immediately, and finding out at
release time means finding out when the cost of fixing it is highest. The check is cheap
enough to run always. requireReleaseVersion genuinely is release-only: on main the
version is always a SNAPSHOT, so it would fail every build.
Why the lockfile, not package.json. A caret range in package.json says nothing about
what a transitive dependency resolved to, and a pre-release that arrives transitively is
exactly the case worth catching.
Renovate merges the small updates and asks about the large ones#
What. .github/renovate.json. Patch, minor, digest and pin updates carry
automerge: true; major updates open a pull request and wait. Quarkus artifacts, React and
its type definitions, and the frontend test tooling are each grouped into one pull request.
Why grouped. React and @types/react moving separately produces a pull request that
cannot pass tsc on its own, and the Quarkus BOM and its extensions have to move together
by construction.
Why platformAutomerge: false. This is the subtle one. GitHub's own auto-merge, which
platformAutomerge: true delegates to, merges as soon as the required status checks pass
— and the main-branch ruleset requires none. It would therefore merge immediately, before
any test had run, which is the exact opposite of the intent. With it off, Renovate merges
through the API only once it has seen every check on the branch succeed. The alternative fix
is to make the checks required in the ruleset, which is a repository-settings change; until
that happens this flag is load-bearing and must not be flipped.
Cost. An automerged minor Quarkus upgrade goes to main without anyone looking at it.
That is the stated intent, and it rests on the suites actually being a gate — which they are:
coverage, Checkstyle and the end-to-end run all fail the build. It also rests on Renovate
being installed as a GitHub App on the repository; the config file alone does nothing.
The schedule restricts branch creation only, and stays. "before 6am on monday" limits
when Renovate opens pull requests, so updates arrive as one weekly batch rather than
trickling in. It does not gate merging: Renovate reports Automerge: At any time (no schedule
defined) in every pull request body, and an already-open pull request merges as soon as its
checks go green regardless of the day.
A Renovate run outside that window is not a broken schedule. The first 25 pull requests
appeared on a Saturday, which looked like the setting being ignored. It was not: Renovate can
be told to run directly from the Mend dashboard, and that is what happened. Two cheaper
explanations were checked and ruled out first — the schedule string is valid (it passes
renovate-config-validator as repository config on Renovate 44; validating the file by path
alone checks it as global config, which silently skips schedule and packageRules), and the
schedule was not suppressing automerge. So do not read off-schedule activity as a
misconfiguration, and do not remove this setting as leftover debris from the setup — it is
deliberate. The Mend job log, which is the only place that says why a given run happened, is
behind the account rather than the repository API.
CodeQL scanning, unfiltered by path#
What. codeql.yml analyses java-kotlin and javascript-typescript on every push to
main, every pull request, and weekly. The Java build mode is manual, compiling with
-DskipTests -Dcheckstyle.skip -Dquarkus.build.skip.
Why unfiltered. backend-ci.yml and frontend-ci.yml are path-filtered, which is right
for them and wrong here: the README carries a CodeQL badge, and a path-filtered workflow
makes its badge report whichever run last touched a matching directory, possibly many commits
ago. The weekly run exists because the queries improve on GitHub's side, so a scheduled scan
finds things that were not findable when the code landed.
Why a manual build. The extractor needs compiled classes and nothing more. Skipping Quarkus's augmentation step and Checkstyle keeps a red CodeQL badge meaning a CodeQL finding rather than a failure already reported by another workflow.
Why -Dmaven.test.skip and not -DskipTests. skipTests only skips running the
tests; testCompile still runs, so backend/src/test was compiled into the CodeQL database
and scanned. That is close to pure noise — a test's hard-coded Keycloak password is the point
of the test, not a finding — and paths-ignore cannot fix it, because that setting applies
only to interpreted languages. For a compiled language the scope is whatever the build
compiles, so not compiling the tests is the only lever.
The query suite is security-extended, not security-and-quality. The quality half of
that suite is maintainability and style analysis, which SonarCloud already performs on both
languages here. Running it in CodeQL as well would bury the security findings under a second
opinion on style. security-extended keeps CodeQL doing the one job nothing else in this
repository does.
Note on what configFileFound: false means. Every CodeQL run logs
"configFileFound": false against /home/runner/.config/codeql/config. That is the CodeQL
CLI's own per-user config file on the runner, which never exists on a hosted runner and is
not something a repository supplies. It is not a sign of a missing
.github/codeql/codeql-config.yml, and it still says false now that one exists.
The runner is pinned, so an image migration is a decision#
Decision. Every runs-on in .github/workflows/ names ubuntu-24.04 rather than
ubuntu-latest — 13 entries across 10 files.
Why. ubuntu-latest began annotating every run with notice that the label migrates to
Ubuntu 26 on 19 October 2026. Riding that would have changed the image under every job on a date
nobody here chose, and a build that breaks because its runner moved is a confusing failure: the
commit that reveals it is unrelated to the cause.
It does not become a manual chore, which was the obvious objection. Renovate's
github-actions manager — already the one updating actions/* here — extracts a runner label as
a github-runner dependency. So ubuntu-26.04 arrives as a pull request, and because 24 → 26 is
a major update, .github/renovate.json makes it wait for a human rather than automerging. The
pin turns the migration from something that happens into something that is approved.
Rejected: pinning only the workflow that raised the notice. The notice was on all 13 entries, not on one. Pinning one would have left the other nine files riding a label whose meaning changes, and made the pinned one look like an exception with a forgotten reason.
Pages deploys only from main#
Decision. pages.yml's deploy job is guarded with if: github.ref == 'refs/heads/main', and
the workflow does not subscribe to release: published. release.yml asks it to run instead,
with gh workflow run pages.yml --ref main, once the release exists.
Why. The github-pages environment carries a deployment branch policy permitting the single
branch main, so a deployment from any other ref is refused before its first step. Two things
follow, and the second was a real bug.
A workflow_dispatch on a branch — the only way to exercise this workflow, since none of its
triggers is pull_request — used to fail on the deploy job. Now it builds the site and skips the
deployment, so a change to the workflow can be checked without leaving a red run behind.
A tag is not main. A release: published event runs with github.ref set to
refs/tags/v1.2.3, so the release-triggered deployment would have been refused too — the
published /api/v1.2.3/ would never have appeared, and nothing would have found that out until
the first release. Dispatching on main keeps every deployment coming from main, and by then
the tag is in history, so build-api-site.py sees the new version anyway.
Why guard the job as well, when the environment already refuses it. So the refusal is a deliberate skip with a reason in the file, rather than a red run whose cause is a setting in the repository's web UI.
Misconfiguration scanning is static, in CI, with Trivy — not AWS Config#
Most of what a
best-practice rule set checks — public buckets, open ingress, unencrypted storage, wildcard IAM —
is readable from the .tf files, so Trivy fails the pull request before the change exists. It
fails at every severity, and an exception is a #trivy:ignore:<ID> comment beside the resource
with the reason, so the list of exceptions is the code rather than a dashboard. Rejected for now:
AWS Config, which adds what a static scan cannot see — changes made outside OpenTofu, and the
history of a resource's configuration — at about $2–5 a month for recording and a handful of
managed rules, and $10–20 with a full conformance pack, because this project's up/down cycles
and deploys churn configuration items. With one human applying infrastructure that is a small
risk for a fixed cost; it is worth revisiting when this stops being a demo. Not chosen: Checkov,
which would have done the same job equally well; Trivy is simply the one chosen.
SonarCloud runs on main only, and a failed quality gate becomes an issue#
On pull requests it
re-ran the entire backend suite for its coverage report, and with no path filter it was the check
every pull request waited for — including the infrastructure and documentation changes that make
up most of them. It now runs after the merge. Until then it also never failed on findings: the scan
uploaded an analysis without waiting for the quality gate. It now waits
(sonar.qualitygate.wait), and sonar-gate-issue.py turns a failure into one open issue,
labelled sonarcloud-gate: commented on while the gate stays red, closed when it is green again.
Rejected: keeping it on pull requests without waiting for it, because Renovate merges only when
every check is green and cannot leave one out. Not done: reusing backend CI's coverage report
instead of re-running the tests, which would need artifacts handed between workflows for a
saving that no longer blocks anything. The cost is that a finding is seen after the merge, not
before — accepted, because it then becomes an issue like any other piece of work.