XDOCKER-18: Automate updating the stable version - #92
Conversation
| ENV MARIADB_JDBC_VERSION="3.5.9" | ||
| ENV MARIADB_JDBC_SHA256="11e3bb5bbf8ef0e806ae4d6c5d5033fedf7262cc777f0190bde8a2f3c8e6bd8d" | ||
| ENV MARIADB_JDBC_VERSION="3.5.7" | ||
| ENV MARIADB_JDBC_SHA256="07bb1229dc184f3313a5aef4c5a6b3207c8dbaa09db4a26814c936f004b4c526" |
There was a problem hiding this comment.
We need to check those. The script is supposed to take the values from our pom (see https://github.com/xwiki/xwiki-docker/pull/92/changes#diff-49a96e7eea8a94af862798a45174e6ac43eb4f8b4bd40759b5da63ba31ec3ef7R53-R56 ).
It probably means they have not been updated properly in the pom.
There was a problem hiding this comment.
Actually it's because we use the versions used in the last release of XWiki for the branch, see https://github.com/xwiki/xwiki-docker/pull/92/changes#diff-49a96e7eea8a94af862798a45174e6ac43eb4f8b4bd40759b5da63ba31ec3ef7R462-R463
That makes sense, since we should be aligned. It just means that these will be updated after we release XWiki, when we release the docker images.
There was a problem hiding this comment.
So your PR actually contains both changes in the script and changes performed by the script, right? Would feel cleaner to have a separated commit just for those changes
There was a problem hiding this comment.
After rebasing on master this is now moot: the PR no longer carries any generated-file changes. Cycle 16 is back to the exact values master ships, and the per-cycle POM values (9.6.0 / 3.5.7 / 42.7.9 for 16) will be applied automatically by updateJDBC at the next release — which is when the driver versions get refreshed anyway. So the diff is now script + config only, with no mixed generated output.
One reconciliation note: master's XWIKI-24618 bumped postgres to 42.7.13 (ahead of what the cycle POMs declare). I kept that value across the cycles here rather than reverting it, and updateJDBC will realign everything with the POMs at release time.
There was a problem hiding this comment.
ahead of what the cycle POMs declare
This is not accurate, as can be seen on https://github.com/xwiki/xwiki-platform/blob/master/pom.xml#L122.
There was a problem hiding this comment.
The changes have been reverted so yes it's not accurate, that's the point! :) Both Simon and you asked for no change to the version in the PR, ie to defer its execution till the PR is merged and when it's used to do a release....
RE <pgsql.version>42.7.13</pgsql.version> your change was not removed.
There was a problem hiding this comment.
The changes have been reverted so yes it's not accurate, that's the point! :)
You are mixing different things.
"Your" last comment was referring to master, so I answered that no, xwiki-docker is not ahead of master (or any other branch).
There was a problem hiding this comment.
Settled after the rebase, in two parts.
The PR carries no generated-file change at all any more: versions.json is seeded with exactly the driver versions and sha256 that master generates from today, so ./gradlew reproduces every */*/Dockerfile byte for byte. Script plus config only.
And updateJDBC no longer downgrades a driver (commit 58f7436). Read literally, each cycle's own POM would have taken cycle 18 back from mysql 26.7.0 / mariadb 3.5.10 to the 9.7.0 / 3.5.9 the released 18.6.0 POM declares, and cycle 16 to 9.6.0 / 3.5.7 / 42.7.9 — i.e. reverting XWIKI-24656 / XWIKI-24657, which were done here after those cycles were released, and rebuilding the images for nothing. The POM value is now a floor rather than an exact target: anything older than what is pinned is reported and skipped, upgrades still apply, and one crossing a driver major says so in the log since the diff gets reviewed before being committed. Running it today is a no-op on every cycle.
Two things:
|
e940758 to
260b153
Compare
|
@surli both good ideas:
Update: both are now implemented in this PR (commit e30aa1c) — see the follow-up comment below. |
|
@surli I went ahead and implemented both suggestions in this PR after all (commit e30aa1c) — they turned out small and self-contained:
Verified live: booted a rewritten instance on a non-default port, XWiki reached its renamed DB and |
| tasks.named('generate').configure { | ||
| mustRunAfter('updateXWiki', 'updateJDBC', 'updateLibreOffice') | ||
| } | ||
| tasks.named('smokeTest').configure { mustRunAfter('generate') } |
There was a problem hiding this comment.
This file is starting to get huge. I'm wondering if we shouldn't move each method in its own file?
There was a problem hiding this comment.
Agreed, done in the rebased commit. build.gradle goes from 280 to 141 lines, keeping only the version-model loading and generate/generateWorkflows; everything else moves into scripts applied from gradle/, grouped by area rather than one file per method (a file per method would scatter helpers that are only meaningful together):
helpers.gradle(114 lines): the HTTP, hashing, version-compare and process helpers shared by the tasks.update-versions.gradle(216):updateXWiki,updateJDBC,updateLibreOffice.run-instances.gradle(204):smokeTest,runAll,stopAll.official-image.gradle(217): thelibrary/xwikigeneration andsubmitOfficialImage.release.gradle(36): thereleaseumbrella and the task ordering.
They exchange the model and the helpers through the project's extra properties, bound at configuration time. That last part also fixed a real problem the split surfaced: reading project from inside doLast is deprecated in Gradle 9.7.1 (Invocation of Task.project at execution time … will fail with an error in Gradle 10), so smokeTest, runAll/stopAll and submitOfficialImage would have broken on Gradle 10, mid-release. The build is warning-free now.
Happy to slice it differently if you would rather have another grouping.
Automate the manual "Update Docker Images" release step as Gradle tasks, mirroring the existing updateLibreOffice precedent: * updateXWiki: resolve the latest final XWiki version on each existing cycle's line from XWiki's Maven metadata and, for the changed cycles, download the WAR and compute its sha256. * updateJDBC: read the three JDBC driver versions from each cycle's own xwiki-platform POM and, for the changed ones, download the driver from Maven Central and compute its sha256. Driver versions/sha256 are now stored per cycle, replacing the shared mysql/mariadb/postgres maps. * smokeTest: boot the postgres-tomcat variant of each changed cycle with docker compose, wait until its REST API reports the expected XWiki version, then tear it down. * runAll/stopAll: start each selected cycle/variant on its own host port as an isolated compose project and leave them running for manual inspection. * release: the pre-push umbrella chaining updateXWiki, updateJDBC, updateLibreOffice, generate, generateWorkflows and smokeTest. * submitOfficialImage: generate the docker-library 'library/xwiki' file from the version data model and open the official-images Pull Request from the maintainer's fork (-PdryRun previews the file and the diff only). The version data model (per-cycle XWiki + JDBC driver versions/sha256, per-cycle role and base image, and the shared LibreOffice values) moves out of build.gradle into the versions.json data file, keeping the configuration separate from the build logic and letting the update tasks persist it with JsonOutput. The values it starts with are the ones master currently generates from, so this change regenerates no image. The build logic itself is split out of build.gradle into scripts applied from gradle/: helpers.gradle (HTTP, hashing and process helpers), update-versions.gradle, run-instances.gradle, official-image.gradle and release.gradle. They exchange the model and the helpers through the project's extra properties and bind them at configuration time, so that no task action reaches for the project while it runs (deprecated in Gradle 9, removed in Gradle 10). External commands go through Groovy's Process API since Project.exec is gone from Gradle 9. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e30aa1c to
d781498
Compare
* Never downgrade a JDBC driver below the pinned version The JDBC driver version declared in a cycle's own xwiki-platform POM is a floor, not an exact target, so updateJDBC now keeps whatever is pinned in versions.json when the POM declares something older. A pin ahead of the POM is a deliberate bump made after that cycle was released (a security fix, or a Renovate upgrade such as XWIKI-24657 for the MySQL connector), and applying the POM value literally would silently revert it and rebuild the images for nothing. Upgrades still apply, including the ones crossing a driver major, but those now say so in the log since the release manager reviews the diff before committing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Rebased on master and addressed the outstanding review comment. History is a single squashed commit (force-pushed), since master had moved 8 commits and the file layout changed. Rebase. Rather than hand-merging the conflicts, the automation is re-applied on top of master's current files, so nothing master gained meanwhile is lost: No image change. Split of Gradle 10 fix found while splitting. The tasks read Verified in this round
|
|
@surli @tmortagne @manuelleduc : all comments addressed, please let me know if you're ok to merge and test this for the next release. Thx |
manuelleduc
left a comment
There was a problem hiding this comment.
LGTM but I let @tmortagne have the last word since he is he RM for 18.7.0 :)
|
Sure, let's try. |
* Fix the runAll/stopAll variant selection being shadowed
* The 'ext.variants' extra property holding the DB variants collided with the '-Pvariants' project property that
'runAll'/'stopAll' read to narrow their selection down. Since the extra property is set before the script declaring
those tasks is applied, 'project.hasProperty("variants")' was always true and 'project.property("variants")' always
returned the variant List rather than the command line String. Both tasks thus failed with "No signature of method:
java.util.ArrayList.tokenize()" on every invocation, with or without the flag.
* Renamed the extra property to 'imageVariants' so that it no longer shares the '-Pvariants' name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Build the image in smokeTest instead of pulling it * The generated docker-compose.yml has no 'build' section, only 'image: xwiki:<version>-<variant>', so 'smokeTest' pulled that tag from DockerHub. At release time that tag does not exist yet, since the official image for a version is only built once the 'submitOfficialImage' pull request has been merged, which happens after 'release' runs. The boot therefore failed with "manifest unknown" in exactly the scenario the task exists for, and when run on an already published version it silently exercised that published image rather than the freshly regenerated Dockerfile. * Build the image locally before booting it, so that the smoke test covers the Dockerfile that was just generated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Fix the captured command output being truncated or empty * 'capture' consumed the process output into two writers and then merely waited for the process, which races with the threads draining its streams: the process can exit while they still have bytes to write, so the captured stdout came back short or altogether empty. Measured on a 4 MB output, 4 runs out of 50 returned an empty String. * Everything 'capture' reads either decides something or ends up in the content submitted upstream, so a silently short read is not an option: 'git status --porcelain' coming back empty passes the dirty tree guard of 'submitOfficialImage', and an empty 'git log' answer emits an empty 'GitCommit' into the official images file. * Use 'waitForProcessOutput', which waits for the process and for both draining threads (and still lets 'exitValue' be read afterwards). Same measurement: 0 mismatches out of 50. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Build the images in runAll instead of pulling them * 'runAll' rewrote the ports and the container names of the generated docker-compose.yml but left its 'image' line alone, so it pulled 'xwiki:<version>-<variant>' from DockerHub exactly like 'smokeTest' used to. At release time that tag does not exist yet, so the boot failed with "manifest unknown" precisely when the images worth looking at are the ones just regenerated, and on an already published version it silently ran the published image instead of them. * Extract the build step into a 'buildImage' helper shared with 'smokeTest' and build each instance before starting it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Smoke test the cycles changed by any of the update tasks * 'smokeTest' picked the cycles to boot from what 'updateXWiki' reported, so a release bumping only a JDBC driver or the shared LibreOffice version regenerated images and then tested nothing at all, although those are the changes carrying a brand new download URL and sha256, i.e. the ones most likely to break the build. * 'updateJDBC' now reports the cycles whose drivers it changed, and 'updateLibreOffice' reports that all of them changed, since the LibreOffice version is shared by every cycle. The holder they share with 'smokeTest' is renamed to 'versionUpdate' accordingly. * Order 'smokeTest' after the update tasks themselves and not only after the 'generate' they order before, so that it still sees what they changed when it is chained without 'generate' (e.g. './gradlew updateJDBC smokeTest'). * Log the cycles being tested, since which ones they are is now the result of what three tasks found. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Report the docker output as it comes instead of swallowing it * The docker commands went through 'capture', which buffers the whole output and returns it, so booting an instance showed nothing at all until it was over. A 'docker build' downloading the XWiki WAR and the LibreOffice archive takes minutes and says what it is doing all along, and none of that was reported; the log was only held in memory to be thrown away, or quoted in an exception once the command had already failed. * Add a 'stream' helper next to 'capture' for the commands whose output is worth watching rather than parsing, and use it for 'docker build' and the 'docker compose' calls of 'smokeTest', 'runAll' and 'stopAll'. It merges stderr into stdout, reports each line as it arrives, and reads the stream to its end before waiting for the process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Build the images with the classic builder like the CI does * 'smokeTest' and 'runAll' built with whatever builder the local Docker defaults to, i.e. BuildKit, while the Docker Official Images infrastructure uses the classic builder and the Docker Build workflow sets DOCKER_BUILDKIT=0 to match it. The two do not accept the same Dockerfiles: the classic builder leaves Docker's automatic platform arguments empty, for one, so a Dockerfile reading TARGETARCH builds fine locally and breaks where the image is really built. * Ask for the classic builder in 'buildImage' too, so that a green boot check says something about the build that will actually be run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Require the commits submitted upstream to be on master * 'submitOfficialImage' refused an unpushed HEAD by looking for it in any remote branch at all, so running it from a Pull Request branch passed the check although docker-library fetches the images from the master branch of GitRepo alone. The GitCommit values emitted for such a run point at commits nobody but GitHub can fetch. * Check that HEAD is on the branch docker-library actually fetches, and fetch that remote-tracking branch first since it goes stale as soon as the change is pushed from elsewhere (a merged Pull Request, another clone). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* List the tasks in './gradlew tasks' * None of the tasks declared a group, so they all landed in the ungrouped bucket that './gradlew tasks' only prints with '--all'. The plain listing showed none of the entry points the README documents, and no description of what they do or of the order they are meant to be run in. * Give each of them a group and a description: 'generation' for the two tasks generating the images and the workflow, 'release' for the version updates and the two release entry points, and 'verification' for the ones booting images. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Compare the versions without failing on a qualifier * 'compareVersions' parsed every dot-separated component as an int, so a version carrying anything but digits threw a NumberFormatException. Every version it is given is plain numbers today, but it decides whether a driver found in a POM is an upgrade over the pinned one, i.e. it reads values this repository does not control, and aborting a release over the day one of them gains a qualifier is a poor trade. * Compare each component on the number it starts with, falling back to comparing the components as text when those numbers are equal, so that a qualifier orders instead of failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Look the JDBC driver properties up by their exact name * The regex reading a driver version out of a cycle's POM interpolated the property name as it is, so the '.' of 'mysql.version' was left as the regex metacharacter matching any character. Nothing in the POMs happens to match those patterns loosely today, but a property whose name only resembles the one being read would be picked up. * Quote the property name so that it is matched literally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Leave versions.json alone when LibreOffice has not moved * 'updateLibreOffice' rewrote the whole data file on every run, including when it had just resolved the very version and checksums already pinned, unlike the two other update tasks which only write what they changed. * Write only when the version or one of the two checksums differs, and report a checksum changing under an unchanged version for what it is, since that means the archive it was read from was republished. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Never close an official images Pull Request still open * 'submitOfficialImage' names its branch after the stable version and deleted that branch on the fork before recreating it. Running it twice for the same version, which happens whenever something has to be corrected, deleted the ref of the branch the pending Pull Request was opened from, and GitHub closes a Pull Request whose head branch is gone. A submission still being reviewed upstream is not ours to close. * Refuse to run when a Pull Request is still open from that branch, naming it so it can be looked at. * Also fail with an explanation rather than a NullPointerException when no cycle declares the 'stable' role, since that is the version the branch and the commit message are named after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Say what binding the model at configuration time really buys * The comments explaining why the applied scripts bind the model and the helpers into local variables claimed that no task action reaches for the project while it runs, which is not what the code does: 'generate' and 'generateWorkflows' call 'copy', and the tasks booting images resolve their paths with 'file'. A comment promising more than the code delivers is worse than no comment, and this one would have the next reader believe the build is configuration cache compatible when it is not. * State what the binding actually avoids, namely reading the project properties from inside the task actions, and say where the build still stands with the configuration cache. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Give each booted instance a compose project of its own * 'smokeTest' ran 'docker compose up -d' and 'down -v' straight in the version/variant directory, so its compose project was the directory basename - 'postgres-tomcat', which every cycle shares and which is also what a developer gets from 'cd 18/postgres-tomcat && docker compose up'. Booting cycle 18 while such an instance was running failed on the container name and the port 8080 already taken, and the teardown in the 'finally' then removed that instance's containers and, with '-v', its data volumes. A release check has no business deleting someone's wiki. * Rename 'runAllCompose' to 'composeInstance', move it above 'smokeTest' and use it there too, so the smoke test gets the port and container renaming plus the '-p' project that 'runAll' already had. Both derive their project name through 'composeProject', which prefixes it with the task so tearing one down never reaches the other. * Publish the smoke test on 18080 rather than 8080, so that a port a developer is already using cannot fail a boot for a reason unrelated to the image being checked. * Depend on 'generate' in 'smokeTest' and 'runAll'. The image is tagged with the model's XWiki version while the compose file names the one in the generated '.env', so './gradlew updateXWiki smokeTest' used to boot the previously published image and then wait ten minutes for a version it could never report. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Retry a download that failed rather than abort the release * 'release' streams the XWiki WAR and the LibreOffice archives, hundreds of megabytes from hosts that fail transiently often enough that the generated Dockerfile retries its own downloads for that very reason (CURL_OPTIONS in template/Dockerfile). Here a single blip aborted the run, possibly after 'versions.json' had already been rewritten for the cycles handled before it, leaving the release manager to work out how far it got. * Fold the redirect-following loop that 'httpGetText' and 'sha256OfUrl' each carried into a single 'httpGet' that hands the response stream to a consumer, and give it three attempts with a growing delay. A failure while the consumer is reading counts too, since that is where a large download dies. * Only retry what can get better on its own: an IOException and the 408/429/5xx statuses. A 404 on a version that does not exist still fails at once instead of burning the delays first. * Convert the command to plain Strings in 'stream'. ProcessBuilder only turns its list into an array when the process starts, so a GString slipped into it failed there with an ArrayStoreException rather than where it was written - and 'capture', which the same scripts use interchangeably, takes GStrings in its stride. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Reject a cycle role that docker-library would not understand * The 'role' of a cycle is not only ours: it drives the tag aliases and the section comments of the official Docker library file that 'submitOfficialImage' submits to docker-library. A typo in 'versions.json' went through unnoticed, emitting a literal 'null' section header, silently dropping that cycle's stable-*/lts-* aliases and putting '(null)' in the Pull Request body - noticed, if at all, by a reviewer upstream. * Check the roles when build.gradle reads the model, and check that exactly one cycle is the stable one, since the branch and the commit message of that Pull Request are named after its version. * Drop the check 'submitOfficialImage' made for a missing stable cycle, now that it cannot happen, and say where the invariant comes from. * Also drop 'ext.versionsFile', which nothing read: 'writeVersions' closes over it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Hash the WAR at the URL the image downloads it from * 'updateXWiki' resolved the versions and computed the sha256 from nexus.xwiki.org while the generated Dockerfile downloads the WAR from maven.xwiki.org, on the argument that the two serve the same artifacts. They do, but pinning the bytes the image actually fetches is the whole point of the hash, and it costs nothing to read them from the one URL that matters. The JDBC and LibreOffice hashes are already taken from their own download URL. * Read the maven-metadata.xml and the WAR from maven.xwiki.org/releases, sharing the one repository URL, and read the cycle POM of 'updateJDBC' from there as well rather than from a second repository. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Check the generated files on Pull Requests too * The Gradlew Check workflow is what guarantees that the committed generated files are what the templates and versions.json produce, but it only ran on a push to master - so a Pull Request changing either was merged before anything verified it, and master was where it broke. * Run it on Pull Requests as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Never downgrade the XWiki version of a cycle * Apply to 'updateXWiki' the floor rule 'updateJDBC' already has. A pinned version ahead of everything the Maven metadata lists means the metadata lost it (a release pulled for being broken, a repository half synced), not that the cycle should be rebuilt and republished backwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Smoke test what a change is about, and say why it failed * Boot, for each cycle, the variant of every database whose JDBC driver 'updateJDBC' moved, on top of the postgres-tomcat baseline. A driver bump is the one DB-specific change there is, and nothing else in the build ever connects XWiki to a database - the Docker Build workflow only checks that the jar landed in WEB-INF/lib - so a MySQL connector crossing a major went out entirely unbooted. 'updateJDBC' now reports which database moved, and '-Pvariants' overrides the selection. * Report the tail of the container logs when an instance never comes up, while the containers still exist: the 'down -v' takes them away, and a bare timeout message is not something a failed release can be diagnosed from. * Match the version reported by the REST API between boundaries instead of as a substring. One version string can be the prefix of another, so an instance answering 17.10.12 satisfied a smoke test expecting 17.10.1, i.e. the release gate went green on the wrong image. * Fail when the docker-compose.yml rewrites that isolate an instance find nothing to replace, instead of silently booting on 8080 in the shared project and tearing down, volumes included, whatever was started there by hand. * Tear down the instance's compose project before starting it too, so that a stale one left by a killed daemon or a reboot is not adopted and reported on in place of the image just built. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Keep the official images file honest about what it declares * Stop untracked files blocking 'submitOfficialImage'. What the guard is about is whether the commits it emits are reachable upstream, which a release manager's scratch notes have no bearing on, so it now looks at tracked changes only - and names them, instead of leaving 'git status' to be run by hand. * Define what a cycle 'role' means in a single map, the section comment and the Pull Request label together, and check the model against it there. The labels used to live apart from the check, leaving the Pull Request body free to render an unknown role as 'null' while everything else looked right. * Reject a variant that build.gradle generates but this file does not declare. It would be generated, committed and built by CI, yet never reach the file that publishes it, so the images would simply not exist on DockerHub. * Retry syncing the fork, which GitHub creates asynchronously and which a first-ever run can therefore fail to find. * Drop 'capture's working directory parameter, which no caller passes, and the unused ReplaceTokens import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Submit the current URL of this repository * Emit 'GitRepo: https://github.com/xwiki/xwiki-docker.git'. The published file still names xwiki-contrib/docker-xwiki, which only resolves through the redirect GitHub kept when this repository moved to the xwiki organisation, and that redirect goes away the day anything else is created under the old name - taking the official image builds with it. This is the one line by which the generated file now differs from the published one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* submitOfficialImage now refuses to submit images the Docker Build workflow has not built successfully: what docker-library rebuilds is the GitCommit(s) emitted in the library file, so that workflow is what says they build at all, and without the check the first thing to notice a broken Dockerfile is the upstream Pull Request. The workflow only triggers on the paths the images are made of, so a HEAD that triggered no run of its own is checked against the latest run of the release branch instead; no run at all is reported but not fatal. '-PskipCiCheck' waives it, since that workflow also goes red on the transient download failures it is prone to. * Add '-PtargetRepo=<you>/official-images' to submitOfficialImage, so that the write path - the one part a dry run cannot exercise - can be rehearsed against a repository of your own rather than run for the first time during a release. A target you own needs no fork (GitHub does not let you fork your own repository), so the branch then lives in it and the Pull Request is opened inside it; a rehearsal waives the guards on what is reachable upstream, that being the point of it. * Drop 'gradle/*.gradle' from the paths triggering the Docker Build workflow: none of those scripts takes part in the generation, so a change to one cannot change an image, and building the whole matrix for it costs 12 jobs for nothing. That they still configure and generate is what Gradlew Check verifies, now on Pull Requests too. * Move the ports runAll publishes off 8080 (18081 and up, '-PbasePort' to move them), which is what smokeTest already does and for the same reason: 8080 is the port a developer's own instance holds, and binding it fails the boot for a reason that has nothing to do with the image. * Parse the '-Pcycles' / '-Pvariants' selections once, through a 'parseList' helper, instead of repeating the same tokenize/trim/filter in four places. * Only tell the release manager to regenerate the images when something actually moved, rather than after every update task run. * Say in the README (and next to buildImage) that smokeTest and runAll overwrite the local image tag they build, the published official image of that version included, and document the new flags.
* Establish, rather than assume, that a Docker Build run describes the images being submitted. A commit that triggered no run of its own was taken to have changed no image, but a run GitHub has not created yet looks exactly the same over the API - and pushing then submitting a few seconds later is how a release goes - so the check could go green on images nothing had built. The fallback is now the latest run of the history being submitted, and it is accepted only when it contains the GitCommit of every cycle. * Stop the two 'gh api' guards from failing open. Both allowed the call to fail and read the resulting empty output as "nothing found", so a rate-limited or unauthenticated 'gh' passed them silently - and for the open Pull Request one, passing means deleting the branch, which closes the Pull Request the guard exists to protect. * Refuse to emit an empty GitCommit. A cycle directory that has never been committed has no last-touching commit and 'git log' says so by printing nothing, which submitted a 'GitCommit:' line with nothing on it. Adding a cycle leaves exactly such an untracked directory behind. * Run the captured commands in the project directory instead of wherever the build process happens to sit, since all of them ask git about this repository. * Drop the 'run ./gradlew' line that 'updateJDBC' logged whether or not anything had changed, and which repeated the one just above it when something had. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
49f6b9b to
a821df5
Compare
* Refuse to generate a library file declaring the same tag twice. The tags a
cycle contributes are derived from its version, so two cycles on the same
version line both claim it: a stable cycle '18' pinned to 18.4.4 while cycle
'18.4' is on 18.4.4 too - the state right after an intermediate LTS is
declared, if the stable line has not moved past it yet - makes both emit
'18.4', '18.4.4' and every per-variant form of them, which docker-library
rejects. Caught at configuration time, where the tags are decided, rather
than upstream in front of the docker-library maintainers.
* Read a 'merge-base --is-ancestor' that cannot answer as a no rather than as
an error, in the Docker Build check. The runs are walked newest first, so a
single commit missing from the clone (force-pushed away, never fetched) used
to abort a release on a run that was only going to be skipped anyway. It
cannot make the check pass on something unbuilt either, since it only ever
removes a run from the candidates.
* Name the web container after the whole variant in the compose rewrites,
which is how the template names it ('xwiki-${db}-tomcat-web' in a
'<db>-<servlet>' directory is 'xwiki-<variant>-web'), instead of hardcoding
'-tomcat-'. 'smokeTestVariants' deliberately assumes no servlet container,
and this now agrees with it.
* Validate '-PbasePort' and '-PtargetRepo' instead of coercing them. Both are
read at configuration time, so a missing or malformed value used to fail
every task of the build with a message naming neither the property nor what
it expected - and an invalid '-PtargetRepo' was taken for a rehearsal (it is
not the upstream repository) and only failed deep in the write path.
* Encode the submitted library file from its UTF-8 bytes rather than from the
platform default charset.
* Disconnect the smoke test's polling connection in a 'finally': the attempt
that throws is the common case there, a boot answering nothing for minutes.
* Say in the README that 'stopAll' removes the volumes of the instances it
stops, and therefore the wiki content created in them.
* Default 'stream' to running in the project directory, as 'capture' and
'exitCode' already do. A process inherits the working directory of the JVM
that starts it, which for a Gradle build is the daemon's and therefore not
something a script gets to assume. Every command run through it passes one
today, so this only ever matters for the next one written without.
* Validate the '-Pcycles' and '-Pvariants' entries against the cycles and
variants that exist, the way '-PbasePort' and '-PtargetRepo' already validate
theirs. Both ways of getting them wrong were silent: an empty value ('-Pcycles'
with nothing after it) left an empty list, which every caller reads as "no
selection" and therefore as "all of them" - the opposite of narrowing anything
down - and a typo such as '-Pvariants=mysql' surfaced only later as a missing
directory, naming a path rather than the property that named it. The message
lists the available values, which is what whoever got it wrong is looking for.
* Establish the submission guards before '-PdryRun' returns rather than after
it. "Am I ready to submit?" is what a dry run is reached for, and answering
only "here is what the file would look like" left the release manager to
discover a dirty tree, an unpushed HEAD or a red Docker Build run on the real
invocation instead. A dry run reports them and still succeeds where a real one
fails on them, since the natural moment to preview the file is right after
'./gradlew release', with the regenerated tree not yet committed.
* Collect those reasons instead of throwing on the first, so that one run names
everything that is in the way. 'checkImagesBuild' returns its reason rather
than raising it, for the same purpose.
* Check for a Pull Request still open from the working branch in a rehearsal
too. Unlike the guards on what is reachable upstream, that one is about not
destroying something on the target, which a target of your own has just as
much of.
* Say in the Docker Build check why looking at the most recent runs alone is
enough for the two questions it asks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Regenerate the images in 'submitOfficialImage', so that a 'versions.json' pushed without its regenerated directories is reported as a dirty tree instead of publishing tags the pushed Dockerfiles do not produce. * Refuse in 'updateXWiki' a bump that would put two cycles on the same version line, which the duplicate tag check rejects at configuration time and would therefore leave every later './gradlew' failing. * Boot in 'smokeTest' the cycles 'generate' rewrote as well as the ones whose versions moved, so that a 'template/' edit is smoke-tested by 'release' too. * Add '-Pfresh' to 'runAll', to remove each instance and its volumes before starting it. * Validate the boolean command line flags, '-PdryRun=false' having read as true. * Drop a comment justifying itself by an earlier revision, and a transient issue key from another. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Mount the database init script from the path it is generated at ('./mysql/init.sql' and './mariadb/init.sql'
rather than './init.sql'). Docker created a directory for the missing source, which the database images then
failed to read as a SQL script, so the db container of every mysql and mariadb variant exited at boot and XWiki
never came up. Broken since the compose files were reworked; only the postgres variants, which mount no script,
were unaffected.
* Fail generation when a generated compose file bind mounts a relative path that is not a file next to it, so that
the above cannot come back unnoticed: the image build never runs the compose file and 'smokeTest' boots one
variant per cycle by default, so nothing else covers the other two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Replace the raw NUL byte separating the path from the content in 'directorySnapshot' with a '\n' escape. The control byte in the source made git treat build.gradle as binary, so the script holding the whole generation logic was shown as "Binary file not shown" on a Pull Request and could only be merged by taking one whole side instead of as text.
* Drop 'runAll'/'stopAll' and their '-PbasePort'/'-Pfresh' flags: they boot-checked nothing that 'docker compose up' in a variant directory does not already do. * Drop the Docker Build workflow check of 'submitOfficialImage' and its '-PskipCiCheck' waiver: docker-library builds every image on its own Pull Request before merging it, unwaivably, so this only duplicated that gate - and its own waiver existed because the workflow fails on transient downloads often enough. * Drop the '-PtargetRepo' rehearsal mode: it was scaffolding for developing the write path, and it left two modes to read through the whole submission flow. * Drop the configuration-time duplicate tag check: 'updateXWiki' already refuses to put two cycles on the same version line, and it did so at configuration time, where a hand-edited 'versions.json' broke plain './gradlew generate' too. * Drop the generated-directory content snapshots: they read every generated file twice on every './gradlew' to pick the cycles a 'template/' edit changed, which 'smokeTest -Pcycles=...' asks for directly. * Have '-PdryRun' stop before the submission guards rather than collect their verdicts: the guards are about not writing something wrong, and a dry run writes nothing, so a real run now throws on the first problem instead. * Fail 'updateLibreOffice' when the sha256 published for the pinned version no longer matches the pinned one, instead of adopting the new hash and reporting it as an update: different bytes under a version already published is what pinning a hash exists to detect. * Simplify 'compareVersions' and drop the now unused 'exitCode' helper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What
Automates the manual Update Docker Images
release step as Gradle tasks, mirroring the existing
updateLibreOfficeprecedent. Target UX:./gradlew releasedoesthe pre-push work, then (after a manual review, commit and push)
./gradlew submitOfficialImageopens the upstreamofficial-images Pull Request. No JIRA issue and no manual sha256 computation are needed anymore.
Jira: XDOCKER-18
This PR regenerates no image.
versions.jsonstarts from exactly the valuesmastergenerates from today, so theonly generated file that changes is
.github/workflows/docker-build.yml, which gainsversions.jsonto its pathfilters. The scripts under
gradle/are deliberately not in those filters: none of them takes part in the generation,so a change to one cannot change an image, and building the whole matrix for it would cost 12 jobs for nothing - that
they still configure and generate is what
gradlew-checkverifies, now on Pull Requests too.Where the configuration lives now
The version data moves out of
build.gradleinto aversions.jsondata file, and the build logic moves into scriptsapplied from
gradle/, so that the configuration stays separate from the code updating it and the update tasks canpersist it with
JsonOutput.versions.jsonrole, the XWiki version + sha256, the three JDBC driver versions + sha256,tomcatImageTag, an optionallibraryComment. Shared: the LibreOffice version + one sha256 per architecture.build.gradlegenerate,generateWorkflows) and the model it reads.gradle/helpers.gradlegradle/update-versions.gradleupdateXWiki,updateJDBC,updateLibreOffice.gradle/run-instances.gradlesmokeTest.gradle/official-image.gradlesubmitOfficialImage, the generation of thelibrary/xwikifile, and the single map defining what each cyclerolemeans (section comment + PR label) against which the model is checked.gradle/release.gradlereleaseumbrella and the ordering the chained tasks need.The per-cycle JDBC versions replace the shared
mysql/mariadb/postgresmaps, since each cycle is now updatedfrom its own POM. The values are unchanged, cycle 16 included.
The tasks
All are runnable on their own and listed by
./gradlew tasks;releasechains the ones a release needs.updateXWikiupdateJDBCxwiki-platformPOM (mysql.version/mariadb.version/pgsql.version); for any that moved forward, downloads the JAR from Maven Central and hashes it.updateLibreOfficeversions.jsonand only when the version actually moved. Fails if the sha256 published for the pinned version no longer matches the pinned one.generate/generateWorkflowsversions.json.smokeTestpostgres-tomcatimage plus the image of any variant whose JDBC driver moved, boots each one withdocker composeas its own compose project on host port 18080, waits until/restreports the expected XWiki version, thendown -v. Dumps the tail of the container logs when an instance never comes up.-Pcycles=18,17,-Pvariants=mysql-tomcatpick what to boot by hand, which is also how atemplate/edit gets boot-checked; standalone it boots every cycle.releaseupdateXWiki→updateJDBC→updateLibreOffice→generate→generateWorkflows→smokeTest.submitOfficialImagelibrary/xwikifile from the model and opens the official-images Pull Request from your GitHub fork (contents API, no clone).-PdryRungenerates the file and shows the diff against the published one without opening a PR.Decisions worth a review
updateJDBC). A pin ahead of the POM is a deliberate bump madeafter that cycle was released, so applying the POM value literally would silently revert it and rebuild the images
for nothing. This is not hypothetical: 18.6.0 declares
mysql.version9.7.0 while 18.4.4 and 17.10.12 declare26.7.0, the XWIKI-24657 bump having landed on the maintenance branches first. Upgrades still apply, and the ones
crossing a driver major say so in the log since the release manager reviews the diff.
updateXWikiapplies the samerule to the XWiki version itself: a pin ahead of everything the Maven metadata lists means the metadata lost it (a
release pulled for being broken, a repository half synced), not that the cycle should be republished backwards.
updateLibreOffice). The DocumentFoundation serving different bytes under a version this repository has already published images for is exactly what
pinning a hash exists to detect, so the task fails and names both hashes per architecture. Adopting the new hash
would republish every image on bytes nobody looked at, and report "Updated the sha256" while doing it.
smokeTestbuilds the image locally and boots it in isolation. It is the release gate, so it is deliberatelypicky about what it boots and about what counts as green:
docker-compose.ymlhas nobuildsection, only animage:tag,and at release time that tag does not exist on DockerHub yet - the official image is only built once the
submitOfficialImagePull Request has been merged. Building here also means what boots is the Dockerfile that wasjust regenerated, not a previously published image.
DOCKER_BUILDKIT=0). That is what the Docker Official Images infrastructure and theDocker Build workflow use, and the two builders do not accept the same Dockerfiles, so it is the one whose verdict
counts. A check building differently from both would miss what they reject.
product version rather than on an HTTP 200.
another: an instance answering 17.10.12 contains
17.10.1, so a substring check would let the gate go green onthe wrong image.
smoketest-<cycle>-<variant>) on port 18080, not the variantdirectory's default project on 8080, so it can neither collide with an instance started by hand nor
down -vthat instance's volumes away.
there would cause exactly the collision above, which is the one failure this indirection exists to prevent.
finallycovers a normal run and acancelled one, but not a killed daemon or a reboot, and a smoke test adopting a stale container would report on
that one instead of on the image just built.
not something a failed release can be diagnosed from.
submitOfficialImagerefuses to submit something unreachable: uncommitted tracked changes,HEADnot onmaster(that is the branch docker-library fetches from, being on some remote branch is not enough), or a PullRequest still open from a previous run for the same version, whose head branch it would otherwise delete. Untracked
files are deliberately not part of that - a release manager's scratch note has no bearing on whether the commits we
emit are reachable - and the offending paths are named in the message rather than left to a
git status. Itregenerates the images before that check, so a
versions.jsonpushed without its regenerated directories shows up asthat dirty tree: nothing else reports it,
docker-build.ymlgoing green on the stale - and therefore fine -Dockerfiles while the library file would publish tags they do not produce.
before merging it, and that gate cannot be waived, so this repository does not pre-check the Docker Build workflow
result before submitting. See the cleanup round below for why the check that used to be here was removed.
stopping there is also what makes a preview usable at the moment it is reached for - right after
./gradlew release,with the regenerated tree not yet committed, which is exactly the state the guards refuse to submit from. A real run
throws on the first guard that objects.
gh apicall a guard reads is not allowed to fail. An empty answer there is indistinguishable from a greenverdict - a rate-limited or unauthenticated
ghwould read as "no open Pull Request" and take a branch someone isstill reviewing down with it - so a check that cannot be made is treated as a check that failed.
GitReponow namesxwiki/xwiki-docker, notxwiki-contrib/docker-xwiki. This is the one line by which thegenerated file differs from the published one. The published URL only resolves through the redirect GitHub kept when
this repository moved to the
xwikiorganisation, and that redirect goes away the day anything else is created underthe old name - taking the official image builds with it. Revert one line if you would rather do this as its own
upstream Pull Request.
role, more or less than onestablecycle, or a variant present in
build.gradlebut not in thelibrary/xwikigeneration all fail at configurationtime. The variant one matters most: such a variant would be generated, committed and built by CI, yet never reach the
file that publishes it, so its images would simply not exist on DockerHub.
two cycles on one line both claim it - and docker-library rejects a library file declaring a tag twice.
updateXWikitherefore refuses the bump that would walk into it, keeping the pin and naming the cycle that owns the line. It is
reachable: the stable cycle resolves to the latest final of its whole major, which is still on the line an
intermediate LTS owns until the next minor is released.
ProcessAPI, sinceProject.execis gone from Gradle 9.captureisfor output that gets parsed,
streamfor the docker commands, whose output is worth watching as it comes.Testing
./gradlew— regenerating produces no git diff, sogradlew-checkpasses../gradlew submitOfficialImage -PdryRun— the generatedlibrary/xwikireproduces the currently published filebyte for byte except the
GitCommitlines, which resolve to the real per-cycle last-touching commits, and theGitRepoline corrected above. Checked by diffing both with theGitCommitlines normalised away: theGitRepoline is the only other difference.
updateXWiki,updateJDBC,updateLibreOffice— run live against maven.xwiki.org, Maven Central and the DocumentFoundation; all four cycles resolve to what is pinned. The floor rule is load-bearing rather than defensive: run
today it holds back five drivers the POMs declare lower than the pins — cycle 18 mysql (9.7.0 < 26.7.0) and
mariadb (3.5.9 < 3.5.10), and all three of cycle 16 (mysql 9.6.0 < 9.7.0, mariadb 3.5.7 < 3.5.10, postgres
42.7.9 < 42.7.13). Without it,
updateJDBCwould downgrade those five and rebuild cycles 18 and 16 backwards.versions.jsonwere diffed against whatmaster's sharedmysql/mariadb/postgresmaps resolve to, for all four cycles x three drivers: identical, versions and sha256 alike.
versions.jsonround-trips byte-identically throughJsonSlurper+JsonOutput.prettyPrint, so an update taskrewrites only the values it changed rather than reformatting the file.
./gradlew release --dry-run— task ordering.smokeTestend to end: built cycle 18postgres-tomcatwith the classic builder, booted it,/restreported18.6.0 through the boundary match, teardown removed only the
smoketest-18-*resources. The failure path was forcedwith a 20s timeout (both containers' logs reported, then torn down), and the stale-instance path by planting a
running
smoketest-18-postgres-tomcatproject and watching the next run reclaim it.stablecycles, a fourth variant missing fromlibrary/xwiki, an unquoted port in a generateddocker-compose.yml, an XWiki version pinned ahead of the metadata,and a MySQL-only driver bump on cycle 17 (which then plans
17 (postgres-tomcat, mysql-tomcat)).-Pcyclesand-Pvariantsare validated against the cycles and variants that exist. Both ways of getting them wrongwere silent:
-Pcycles=left an empty list, which every caller reads as "no selection" and therefore as all ofthem - the opposite of narrowing anything down - and
-Pvariants=mysqlsurfaced only later as a missing directory,naming a path rather than the property that named it. Checked with an empty value, an unknown cycle (
19) and avariant typo (
mysql); each names the property and lists the available values.docker-compose.yml, anddocker compose configon a rewritten file reports the project-scoped container names,DB_HOSTwired to the renamed databasecontainer, port 18080, project-scoped network and volumes, and the image tag resolved from
.env.gradlew-checkpasses on each.submitOfficialImageagainstdocker-library/official-images, since running it opens a real upstream Pull Request. Its read-only plumbing
(
gh auth status, the upstream fetch, the diff, the guards) was exercised live.Review round (@claude review of the branch)
Six changes, all in the scripts plus
README.md; no generated file moves andlibrary/xwikiis unchanged.submitOfficialImageregenerates before it checks the tree. The library file is generated fromversions.jsonwhile docker-library builds the Dockerfiles committed at the emitted
GitCommit(s), and nothing tied the twotogether: a
versions.jsoncommitted without its regenerated directories would publish tags the pushed Dockerfilesdo not produce.
gradlew-checkwould go red onmaster, but no guard here read it. Depending ongenerate/generateWorkflowsturns the drift into a dirty tree, which the existing guard already reports, namingthe files.
updateXWikirefuses a bump onto another cycle's version line, keeping the pin and saying which cycle owns theline, rather than writing a
versions.jsonthat would make both cycles claim the same tags.smokeTestalso boots whatgeneraterewrote. It picked its cycles from what the update tasks changed, so anedit to
template/- the other thing that changes every image - left the set empty. (Superseded by the cleanupround below, which drops the mechanism in favour of
-Pcycles.)-PdryRunwas a barehasPropertytest, so-PdryRun=falseread as true - asking not to rehearse is what would have started the real submission. It now accepts no value,
trueorfalse, and rejects anything else by name, as-Pcyclesand-Pvariantsalready did.nobody reading the file later can see - and another carried a transient issue key. Both now state the reason as it
stands.
Verified after the changes:
./gradlewand./gradlew tasksstill produce no git diff;versions.jsonstillround-trips byte-identically;
submitOfficialImage -PdryRunstill reproduces the publishedlibrary/xwikiwith theGitRepoandGitCommitlines as its only differences;updateXWiki/updateJDBC/updateLibreOfficestill run greenand idempotent against the live sources, with no false positive from the new line check on today's
versions.json.Each fix was also triggered on purpose: a
versions.jsoncommitted without regenerating is reported as a dirty treenaming the stale files; a cycle resolving onto another's line is refused by name;
-PdryRun=falseno longer dry-runsand
-PdryRun=maybeis rejected by name.Bug found by that review round, fixed here
./gradlewthen booting cycle 17 by hand: the mysql and mariadb database containers exited (1):template/docker-compose.ymlmounted./init.sql, whilegeneratecopies that file to<variant>/mysql/init.sql(the include is
"${db}/*"). Docker creates a directory for a bind mount source that does not exist, and thedatabase entrypoints then fail to read it as their SQL script. Pre-existing on
master, not introduced here:git log -Sputs it at d1e6ca1 (XDOCKER-296), which changed the mount from./mysql/init.sqlto./init.sqlwithoutmoving the file.
cd 17/mysql-tomcat && docker compose upfails onmastertoday, in all 8 mysql/mariadb directories.It blocks this branch specifically, because
smokeTestVariantsexists to boot the variant whose JDBC driver moved -the one check that would exercise a MySQL connector crossing a major - and that boot could only ever fail. It went
unnoticed because every
smokeTestreported above was run onpostgres-tomcat, the one variant that mounts no script../mysql/init.sql/./mariadb/init.sql, which is what the template said before d1e6ca1. Thisis the only generated-file change in this Pull Request: 8
docker-compose.yml, one line each.generatenow fails when a generated compose file bind mounts arelative path that is not a file next to it. Nothing else covers this - the Docker Build workflow builds the image
rather than running the compose file, and
smokeTestboots one variant per cycle by default, so the other two canstay broken for as long as nobody starts them by hand. Checking it at generation time covers all 12 variants on every
./gradlew, and therefore on every push and Pull Request through Gradlew Check.Verified:
smokeTest -Pcycles=17 -Pvariants=mysql-tomcat,mariadb-tomcatnow boots both and/restreports 17.10.12,teardown leaving no container or volume behind; the init script genuinely runs,
show grants for 'xwiki'@'%'reportingthe global
ON *.*privileges that subwiki creation needs; and the guard fails generation on the reverted template,naming the file and the path.
Second review round (@claude, narrowed to defects that can produce a wrong release artifact or destroy local data)
One finding, fixed here; no behaviour change and no generated file moves.
build.gradleheld a raw NUL byte - the separator between the path and the content in the generated-directorysnapshots. Git therefore classified the script as binary:
git diff --numstatanswered-/-, GitHub rendered itas "Binary file not shown", and a concurrent edit could only be resolved by taking one whole side rather than by a
text merge - on the single file holding the generation logic and
workflowPathFilters. Fixed by writing theseparator as a
\nescape. (The snapshots themselves are gone as of the cleanup round below.)Verified in that round, by running rather than by reading:
./gradlewat the branch head: no git diff.library/xwikireproduces the currently published file byte for byte with theGitRepoandGitCommitlines normalised away - confirmed independently by drivinggenerateLibraryContentand diffing againstdocker-library/official-images@master.versions.jsonround-trips byte-identically throughJsonSlurper+JsonOutput.prettyPrint: no key reordering, nostring-to-number coercion.
updateXWiki/updateJDBC/updateLibreOfficeagainst the live sources: all no-ops today, matching the "this PRregenerates no image" claim. The floor rule holds back the five drivers listed above, and MySQL's calendar-versioning
jump is ordered correctly for the right reason - Maven Central publishes 26.7.0 on 2026-07-24 against 9.7.0 on
2026-04-22, so
compareVersionskeeping the 26.7.0 pin over the POM's 9.7.0 is an upgrade preserved, not a downgradeapplied.
GitRepo:xwiki/xwiki-dockeris the canonical name,xwiki/docker-xwikiandxwiki-contrib/docker-xwikibothanswer
301. The correction is right.docker compose config: projectsmoketest-18-mysql-tomcat,volumes
smoketest-18-mysql-tomcat_{mysql,xwiki}-data, project-scoped network, renamed containers, port 18080, imagetag resolved from
.env. Adown -vprovably cannot reach a hand-started instance'smysql-tomcat_*volumes. Thebind mount resolves to a real
<variant>/mysql/init.sql, i.e. the fix above holds.prune, norm, nogit reset/clean/checkout -f, nounscoped
docker compose down; everydown -vis-p-scoped.Cleanup round: what was removed, and the guarantee it did or did not carry
The branch had accumulated machinery whose cost was review surface rather than safety. Asked which parts of
gradle/could go without losing a real guarantee, the answer came to −348 lines (98 insertions, 446 deletions);
gradle/goes 1465 → 1148 lines. Nothing generated moves, and
library/xwikiis unchanged.Removed because they guaranteed nothing:
runAll/stopAll, with-PbasePortand-Pfresh(≈115 lines). They boot-checked nothing thatcd <cycle>/<variant> && docker compose up- which is what the documentation already tells users to run - does notalready do, and nothing in
releasetouched them.smokeTestkeeps the port/name isolation they shared.submitOfficialImage, with-PskipCiCheck(≈112 lines, taking theexitCodehelper with it). docker-library builds every image on its own Pull Request before merging it, unwaivably, so this
only duplicated an upstream gate - and its own waiver existed because that workflow fails on transient
maven.xwiki.org/download.documentfoundation.orgdownloads often enough that "a release cannot be held hostageto it". A guard whose documented usage includes routine bypass is not a guarantee. Roughly half its length was the
fallback-run and ancestor-containment reasoning for the case where GitHub had not created the run yet, which then
resolved to a warning and proceeded anyway.
-PtargetReporehearsal mode (≈40 lines). It was scaffolding for developing the write path, and it left twomodes to hold in your head through the whole submission flow.
on every
./gradlewto serve one case:releaserun straight after atemplate/edit.smokeTest -Pcycles=...asks for that directly, and the README and wiki text now say so.
problemsaccumulator. The first covered a once-per-machine forkcreation race; the second let a dry run list every objection, which no longer applies now that a dry run stops
before the guards.
updateXWikialready refuses towrite two cycles onto one version line, and docker-library rejects the file if one ever got through. Worse, it ran
at configuration time, so a hand-edited
versions.jsonbroke plain./gradlew generate- the very hazard theupdateXWikiguard was added to avoid. Verified: in that state./gradlewnow succeeds, andupdateXWikistillrefuses the bump by name.
compareVersions' qualifier tolerance. Its own comment conceded every version it handles is plain numbers.A
NumberFormatExceptionin front of a release manager at a terminal is the right failure for a version this buildhas no business ordering.
Fixed, in the opposite direction - something that looked like a guard but laundered a red flag:
updateLibreOfficenow fails when the sha256 published for the pinned version no longer matches the pinned one.It used to adopt the new hash and log
Updated the LibreOffice sha256 of the unchanged version 25.8.7. Differentbytes served under a version this repository has already published images for is precisely what pinning a hash exists
to detect; accepting it silently republishes every image on bytes nobody looked at.
Retest of every feature after the removals
Run against the live sources and a live Docker, not read:
./gradlew— configures and regenerates with no git diff, sogradlew-checkpasses../gradlew tasks— the surface isgenerate,generateWorkflows,updateXWiki,updateJDBC,updateLibreOffice,smokeTest,release,submitOfficialImage.runAll/stopAllare gone, and the removed flags are inert ratherthan fatal (
-PbasePort=nonsense -PskipCiCheck -PtargetRepo=bogus -Pfreshconfigures fine)../gradlew release --dry-run— ordering stillupdateXWiki→updateJDBC→updateLibreOffice→generate→generateWorkflows→smokeTest→release.floor rule still holding back the same five drivers, and
versions.jsonuntouched.25.8.6: each moved forward, and the WAR sha256
updateXWikicomputed for 16.10.18 matches the reference hash inversions.jsonbyte for byte -sha256OfUrlproven end to end, not just exercised. The simplifiedcompareVersionsis what orders MySQL 26.7.0 over the POM's 9.7.0 here.hashes, and leaves
versions.jsonunwritten.updateXWiki's two guards, forced: a pin ahead of the metadata (16.99.0) is kept with a message, and a cyclethat would move onto another's version line is refused by name.
stablecycles and nostablecycle each fail at configurationtime with the offending value.
-Pcycles=/-Pcycles=19/-Pvariants=mysql/-PdryRun=maybe— each rejected by name, listing the availablevalues;
-PdryRun=falsecorrectly reads as false.verifyBindMounts, forced by pointing the template's mysql bind mount at a file that is not generated: fails andnames the file and the path.
template/docker-compose.ymlpublishes its port: fails withthe "no longer match what template/docker-compose.yml generates" message before booting anything.
submitOfficialImage -PdryRunagainst a deliberately dirty tree: generates the file, shows the diff and skips theguards as intended. The result still reproduces the published
library/xwikiwithGitRepoand theGitCommitlines as its only differences, emits no tag twice, and renders all four role section headers.
smokeTestfor real, twice. Cycle 18postgres-tomcat: built with the classic builder, came up asxwiki-smoketest-18-postgres-tomcat-webon port 18080 alongside unrelated containers left untouched,/restreported 18.6.0, torn down. Then the release integration - cycle 16's mysql driver pinned low,
updateJDBC smokeTestin one invocation - which planned
16 (postgres-tomcat, mysql-tomcat), i.e. the driver bump pulled in thatdatabase's variant on top of the baseline, and booted both to 16.10.18. No container or volume left behind.
versions.json,template/and the generated tree restored and re-verified clean after every forced failure above.Docs
README.mdgains a "Releasing new versions" section (in this PR), updated for the removals above.CLAUDE.mdupdate, and the replacement below for the wiki's "Update Docker Images" section.Proposed replacement for the wiki "Update Docker Images" section
Update Docker Images
The version bumps and the DockerHub official-images Pull Request are automated as Gradle tasks. You only review, commit
and push the
docker-xwikichange in between.run
./gradlew release. For every supported cycle this resolves the latest final XWiki version (downloading the WARand computing its sha256 when it changed), updates the JDBC drivers from that cycle's own
xwiki-platformPOM,updates the supported LibreOffice version, regenerates all the Dockerfiles/resources and the build workflow, and
smoke-tests the cycles whose versions moved (builds the
postgres-tomcatimage with the classic builder, plus thatof any variant whose JDBC driver moved, boots each and waits until its REST API reports the expected version).
Docker must be running. Each step is also runnable on its own:
updateXWiki,updateJDBC,updateLibreOffice,generate,generateWorkflows,smokeTest.versions.jsontogether with the regenerated directories. To look at an image by hand,run
docker compose upin its version/variant directory as usual, or./gradlew smokeTest -Pcycles=18 -Pvariants=postgres-tomcatto boot-check a specific one; the old manual "TestDocker Image" procedure also remains available for a deeper test.
ghis authenticated, then run./gradlew submitOfficialImage.It regenerates the
library/xwikifile and opens a PR against docker-library/official-images from your fork,aborting if the tree is dirty, if HEAD is not on
master, or if a PR from a previous run is still open. Use-PdryRunto generate the file and show the diff without opening a PR.🤖 Generated with Claude Code