ADR-0032: A release is a draft until its image and archives are checked, and RESTest starts through the archive's launcher
Status: Accepted, amended on 13, 23, 27 and 30 October and 1 November 2026 Date: 2026-10-11
Context
Section titled “Context”Until now RESTest was installed one way: clone the repository, build it with Maven, run ./restest.
2.0.0 and 2.1.0 are GitHub Releases whose only file was the manual's PDF, which
ADR-0031 has since dropped.
ADR-0024 counted a container image and a distribution archive
published from the tag as part of what a closed version is, and left them to follow.
What they have to be:
- One command, with nothing but Docker or Java. A person, a build server or a coding agent should get a working RESTest from a URL, pinned to a version, with no account and no clone.
- RESTest's exit codes, unchanged. ADR-0015 gives each number a
meaning a build server acts on:
1is "the API has faults",3is "nothing could be tested". Whatever stands between a person and Java - a launcher, a container - must hand the number on. - Both processors. Continuous integration is
linux/amd64; the laptops RESTest is developed on arelinux/arm64when they run containers. - Something later installers can take as it is. Homebrew, SDKMAN, or a package for a language's own installer would each want a file to download and a command to run.
How other tools that test REST APIs from an OpenAPI document are released, read in their repositories on 11 October 2026:
- Schemathesis (commit
ca01bab8): a signed tag starts one workflow, which publishes the GitHub Release at once, with notes taken from itsCHANGELOG.md, beside the package and the images. For v4.30.1 the image jobs failed and were run again, and the images arrived about ten hours after the release that announced them (Actions run 37994644700). Its image runs as a user of its own, and its Docker guide asks for the folders a run writes to to be created first, for that reason. - EvoMaster (v6.2.0,
efad6f2b): a tag starts one workflow that publishes everything at once. Its tag v6.1.0 has an image and no GitHub Release (Actions run 28924054236), and the Python package published from it downloads a jar that does not exist. Its image runs as root. - WuppieFuzz (v1.8.0,
06c96f26): cargo-dist publishes the release as soon as the tag arrives, with a SHA-256 for every file. - Tcases (
tcases-4.1.1,dbabf908): a folder with launchers written by hand in its repository,bin/tcasesandbin/tcases.bat, whose.batfiles end on thejavaline and so return Java's exit code. - RESTest 1.x: the releases 1.5.0 and 1.6.0 tell the reader to download a zip they do not carry.
JReleaser, the tool usually reached for to package a Java program, was tried. Version 1.26.0 builds
the right folder, but its Windows launcher
(META-INF/jreleaser/templates/java-archive/java-archive/bin/launcher.bat.tpl) ends with if %ERRORLEVEL% equ 0 goto mainEnd and then exit /b 1: every number but 0 becomes 1, so a run
that could test nothing would read as one that found faults. Its Unix launcher answers 1 when
there is no Java.
Decision
Section titled “Decision”1. What a release carries
Section titled “1. What a release carries”| What | Where |
|---|---|
| The container image | ghcr.io/isa-group/restest:<v>, for linux/amd64 and linux/arm64, made of :<v>-amd64 and :<v>-arm64; :latest is the highest version tagged |
| The archive for any machine with Java 21 or later | restest-<v>.zip and restest-<v>.tgz on the GitHub Release, both unpacking into one folder restest-<v>/ with bin/restest, bin/restest.cmd, lib/, LICENSE, NOTICE and README.md |
| Their checksums | checksums_sha256.txt on the GitHub Release, in the form sha256sum writes and checks |
The manual of a release is docs/manual/ at its tag
(ADR-0031).
2. A release is a draft until everything in it has been checked
Section titled “2. A release is a draft until everything in it has been checked”A version tag starts .github/workflows/release.yml, which compiles RESTest, assembles the
archives, checks them on Linux, macOS and Windows, builds and checks the image on an amd64 and on
an arm64 machine, pushes the image, and leaves a draft release holding the archives and the
checksums. A person writes the notes in the draft and publishes it. docs/ci.md has the steps.
So a published release always holds every file its notes name. Every tool read above publishes the moment its tag arrives, and three of them, RESTest 1.x included, left a release published without what it announced.
The image is public from the tag, before the notes: :latest moves then, when no higher version is
tagged. Holding it back until the release is published would take a second workflow started by the
publication, which was considered and not built.
3. Nothing published is replaced, and a tag never moves
Section titled “3. Nothing published is replaced, and a tag never moves”A tag is never moved or deleted; a release whose draft cannot be published as it is is put right in
the next patch. A run of the workflow for a tag that has run before replaces nothing: an image
already pushed is left as it is, a draft keeps the files it has and takes its checksums from them,
and a published release is not touched. When what failed is the workflow itself, the same release
is made by starting the workflow on master by hand with the tag; everything else it uses - the
Dockerfile, dist/, the check - is still the tag's, and a fault in those is put right in the next
patch.
4. The image
Section titled “4. The image”- Built from the checkout, in two stages. The first compiles RESTest with JDK 25 and assembles the archive's folder; the second copies that folder onto a Java 21 runtime. The image needs nothing but Docker to build, from any checkout, and holds what a release archive holds. The first stage runs on the building machine's own processor; the second only copies files.
- Java 21, the oldest release RESTest supports.
- Built and checked on each processor, then pushed as it is. Each of the two machines builds the image for its own processor, checks it, and pushes that very image; the registry then joins the two under one name, without building anything. The image a release names is made of the images that were checked.
- Started through the archive's own launcher,
ENTRYPOINT ["/opt/restest/bin/restest"], which ends withexec java: Java is the container's own process, sodocker stopreaches it and the run stops as it does on Ctrl-C, answering143, as the M12.1b amendment of ADR-0015 asks. - As root, in
/work. A container running as root can write into any folder mounted at/work, whoever owns it; one running as a user of its own can write only into a folder that user may write to, which on Linux is seldom the folder of the person running it. On Linux, where files written as root then belong to root, the documentation passes--user "$(id -u):$(id -g)"; Docker without root, and Podman, map root to the person already. - Pinned by digest, both base images, so that one commit always builds on the same Java. Moving them to a newer patch of Java is a step of every release while Dependabot is switched off.
- Labelled with what it is: its version, its commit and when that commit was made, set by the release workflow and empty in a build of any other checkout, rather than the base image's own, which name a version of Ubuntu. No provenance attestation, which registries show as an image for an unknown platform; that comes back with signatures, if they come.
5. The archive's launchers are the one place that knows how RESTest starts
Section titled “5. The archive's launchers are the one place that knows how RESTest starts”dist/bin/restest, for macOS, Linux and Git Bash, and dist/bin/restest.cmd, for Windows, are
written by hand. They hold everything RESTest needs to start: the main class, the class path
lib/*, --enable-native-access=ALL-UNNAMED, and Java 21 or later. The image starts the first;
anything that installs RESTest later installs the archive and calls them, rather than saying the
same things again. ./restest stays as the checkout's own, whose jars are elsewhere.
- They hand Java's exit code on unchanged. The Windows one runs every line on its own, so that the
number is read after Java has run; uses no delayed expansion, which would remove every
!from a key handed over with--auth; and passes the arguments on as they were typed. - When there is no Java they can use - none, a
JAVA_HOMEwith none in it, or one older than 21 - they say so and answer127, a number RESTest never answers with. An older Java would otherwise refuse to start RESTest and answer1. They read the line ofjava -versionthat gives the version, not its first line, which is a notice whenJAVA_TOOL_OPTIONSis set. - Their line endings are set by
.gitattributes- LF forbin/restest, CRLF forbin\restest.cmd- and
dist/assemble.shcopies them as they are.
- and
6. One check for every way of starting RESTest
Section titled “6. One check for every way of starting RESTest”.github/scripts/check-restest.sh starts RESTest the way it is given - a launcher, an image, or a
checkout's ./restest - and checks that it says which version it is, that a wrong option answers
2, and that a run against an address where nothing listens answers 3 and leaves its files. A
launcher and an image are also checked for answering 127 with no Java they can use, and an image
for answering 143 when docker stop ends a run. Every pull request runs it on the image and on
./restest; the release workflow on the archive's launchers on three operating systems, and on the
image on both processors before anything is pushed, and on the published image once more. A later
installer passes the same check.
7. 2.0.0 and 2.1.0 get no image and no archive
Section titled “7. 2.0.0 and 2.1.0 get no image and no archive”Neither tag holds a Dockerfile or a launcher. Making their images would mean building old code
with files from a later commit, adding files to releases already published, and a first run of the
workflow against a public release. 2.2.0 is the first release that carries them;
ADR-0024 is amended to say so.
Consequences
Section titled “Consequences”- RESTest can be run with one command and nothing but Docker -
docker run ghcr.io/isa-group/restest:<v>- or nothing but Java, from 2.2.0 on. Building from the source stays as it was.
- A release is made with one tag and one publication, each by a person, with the steps around them
that
docs/ci.mdlists: the base images moved while Dependabot is off, the version and the pages that name it prepared beforehand, the notes written, and the installation tried from outside. - Sizes, measured at 2.1.1-SNAPSHOT: each archive is 34 MB; the image is 145 MB compressed on
amd64and 143 MB onarm64, of which 32 MB is RESTest and the rest the Java runtime on Ubuntu. - The registry shows two more tags per version,
<v>-amd64and<v>-arm64, which the version's own tag is made of. - The first image pushed creates the package
ghcr.io/isa-group/restest, which GitHub makes private; an owner of the organisation makes it public, once. - Between the tag and the publication,
:<v>and:latestare public while the release is a draft. - Users of 2.0.0 and 2.1.0 build them from the source, as before.
- The archive and the image hand on the libraries RESTest runs with, where until now Maven fetched them from their own sources. Half of the jars carry no licence text of their own, and some licences, such as MIT, ask that their notice travel with every copy. A file listing every library with its version and its licence, with the texts those licences ask for, goes into the archive and the image before the first release that carries them, 2.2.0.
Alternatives considered
Section titled “Alternatives considered”- Publish the release as the tag arrives, as every tool read above does. Simpler by one step; it is how releases came to be published without the files they named.
- JReleaser. Its launchers change the exit code, as above. With launchers of RESTest's own in its place, what was left for it to do was a zip and a tar. It remains the obvious way to publish to Homebrew or SDKMAN later, and it can take this archive as it is.
- One executable jar, as CATS and EvoMaster ship. ADR-0015 rejected a shaded jar, for a second copy of every class in a jar the architecture rules read; and two versions of the OpenAPI parser's module, and files of the same name in several libraries, would have to be merged into one.
- A user of its own in the image, as Schemathesis has. A folder mounted from Linux could then be written to only if made writable for that user first.
- Both processors built at once with Buildx, under emulation or by splitting one build afterwards. Either way the image checked is not, without more machinery, the image pushed.
- Docker Hub as well as GitHub's registry. It needs an account and a secret; GitHub's registry takes the workflow's own token. It can be added later without changing anything here.
- A Java runtime cut down with
jlink, or an image without a shell. Smaller, but the launcher is a shell script, and RESTest's libraries are not all modules. - Images and archives for 2.0.0 and 2.1.0 (§7).
Amendment (13 October 2026): the JSON Schemas
Section titled “Amendment (13 October 2026): the JSON Schemas”Date: 2026-10-13
The archive's folder also carries schemas/, with the JSON Schemas of a dictionary and a plan
(ADR-0035), and the image has them in
/opt/restest/schemas/; check-restest.sh checks that the image, and a launcher in a release's
folder, carry both.
Amendment (23 October 2026): the libraries and their licences
Section titled “Amendment (23 October 2026): the libraries and their licences”Date: 2026-10-23
What the consequences above asked for before 2.2.0 is dist/THIRD-PARTY.txt, which dist/assemble.sh
puts beside LICENSE and NOTICE in the archive's folder, and so in the image's
/opt/restest/. It lists the 63 libraries in lib/, each under the licence it is used under - 49
under the Apache License, Version 2.0; 8 under that licence or the LGPL, used under the first; 3
under MIT; 2 under the Eclipse Distribution License; and Rhino under the Mozilla Public License,
Version 2.0 - and gives the texts that MIT, the Eclipse Distribution License, the Mozilla Public
License and the notices Rhino and SQLite JDBC carry ask to travel with every copy. The Apache
License's text is LICENSE itself.
It is written by hand, from the licences each library's own Maven description declares, and
dist/assemble.sh refuses to assemble a release whose lib/ holds a library the file does not
list, or lacks one it does. So a dependency that is added, dropped or moved to another version
fails the image job of continuous integration until its line is written, and its licence read: a
new version of a library can change its licence, and no tool can say what a licence asks of a copy.
check-restest.sh checks that the image and a release's folder carry the file.
Amendment (23 October 2026): the package was public from its first image
Section titled “Amendment (23 October 2026): the package was public from its first image”Date: 2026-10-23
The consequences above expected the first image to create ghcr.io/isa-group/restest as a private
package, for an owner of the organisation to make public. The first image, pushed for 2.2.0 by
release.yml, created it public, linked to this repository: on the day of the tag, before the
release was published, docker pull ghcr.io/isa-group/restest:2.2.0 worked with no one signed in to
the registry, for linux/amd64 and linux/arm64. So the step that made it public is gone from
docs/ci.md's release steps; trying the installation from outside, with no one signed in, is still
one of them, and would show a package that was not public.
Amendment (27 October 2026): Homebrew
Section titled “Amendment (27 October 2026): Homebrew”Date: 2026-10-27
RESTest is also installed with Homebrew, from a tap of its own, by a formula that installs the
archive as it is and calls its launcher, as §5 asks (ADR-0040). Three things
above no longer hold as written. A workflow started by the publication, of the kind §2 considered
and did not build for the image, exists for the tap alone; the image is still public from the
tag. A tag does not freeze that formula, which is taken from the commit the
workflow runs on (§3). And §6's check has a fourth way of starting RESTest, --own-java, for a
command that brings its own Java. JReleaser, named above as the obvious way to publish to Homebrew,
writes a formula whose command finds no Java when Homebrew's is the only one; ADR-0040 says why it
was not used.
Amendment (30 October 2026): Maven Central
Section titled “Amendment (30 October 2026): Maven Central”Date: 2026-10-30
A release also carries RESTest's eight modules on Maven Central (ADR-0044).
§1's table gains a row: es.us.isa.restest:restest-*:<v>, each module with its jar, its sources,
its Javadoc and their signatures. §2's draft is made only once the Central Portal has validated what
the release uploads there, and a person now publishes twice - the deployment on the Portal first,
then the release - with an approval of the upload before either. The jars Central gets come from the
build the archives are assembled from, and are checked with them. §3 holds on Central too: a version
Central has is never uploaded again, and nothing published there is replaced. §5 is unchanged:
nothing on Central starts RESTest.
Amendment (1 November 2026): jbang
Section titled “Amendment (1 November 2026): jbang”Date: 2026-11-01
RESTest is also run with jbang, from a catalog of the ISA group's own whose alias names RESTest on
Maven Central (ADR-0045). jbang cannot read the archive, so §5 no longer holds as
written: the launchers stop being the one place that knows how RESTest starts. The alias says the main
class, --enable-native-access=ALL-UNNAMED and Java 21 or later again, and JbangAliasTest fails
whenever it and the two launchers disagree on any of them. §6's --own-java steps with no Java run
with the PATH as it is, every java and javac taken out, rather than the five tools a launcher
needs, since jbang needs its own script's tools and itself; --launcher keeps the five tools. §2's
draft also waits for check-jbang, which runs the catalog for the version being released. Like
Homebrew's formula, the catalog is written by a workflow started by the publication, and a tag does not
freeze it (§3).
Amendment (1 November 2026): the skill for coding agents
Section titled “Amendment (1 November 2026): the skill for coding agents”Date: 2026-11-01
The archive's folder also carries skills/restest/SKILL.md, the skill that teaches a coding agent
to use RESTest (ADR-0046), and so the image has it in
/opt/restest/skills/restest/ and Homebrew in its libexec/skills/restest/. check-restest.sh
checks that the image, and a launcher in a release's folder, carry it, as it does the JSON Schemas.