ADR-0040: RESTest on Homebrew, from a tap of its own
Status: Accepted Date: 2026-10-27
Context
Section titled “Context”Since 2.2.0, RESTest is installed with Docker, or with Java 21 or later beside a download, an
unpack and a folder added to the PATH (ADR-0032). Java is
the part people most often lack: a developer's Mac seldom has a Java 21, and a Linux machine often
has an older one. Of the tools that test REST APIs and are still maintained, none asks for a runtime
of its own: CATS and WuppieFuzz ship native programs, Schemathesis is Python, and EvoMaster's Python
package downloads a Java the first time it runs. Three of them are on Homebrew: CATS from a tap of
its own (Endava/homebrew-tap), WuppieFuzz in homebrew-core, and Schemathesis through a formula in
homebrew-core that its community keeps.
Homebrew, on macOS and Linux, installs a program and what it needs with one command, puts it on the
PATH, and moves it to a newer version with brew upgrade. It reads how from a formula, a short
Ruby file kept in a git repository it calls a tap. homebrew-core is the tap Homebrew comes with;
anybody may publish another, as a repository named homebrew-<name>.
ADR-0032 asked three things of a later installer: that it install the release archive as it is and
call its launcher (§5), and that it pass check-restest.sh (§6); and it supposed that JReleaser
would be "the obvious way to publish to Homebrew or SDKMAN later". It also considered a second
workflow started by a release's publication and did not build it (§2), and it froze, at a tag,
everything a release is made from (§3).
What Homebrew's own code says, read in Homebrew 7.0.11:
- A formula is installed only from a tap. One given as a path is refused unless Homebrew is in developer mode.
openjdk@21is keg-only: Homebrew installs it but puts nojavaon thePATH. On macOS its Java is inlibexec/openjdk.jdk/Contents/Home, on Linux inlibexec.Language::Java.java_home_env("21")gives a formula the folder of that Java, andwrite_env_scriptwrites a two-line script,JAVA_HOME="…" exec "<target>" "$@", which hands the exit code on unchanged.- Its
openjdkwas 27.0.1, a version RESTest's continuous integration, on Java 21, 25 and 26, does not test.
JReleaser's formula for a Java program (templates/java-binary/brew/formula.rb.tpl) puts the
program's folder in libexec, links its launcher into bin and sets no JAVA_HOME. With RESTest's
launcher and a keg-only Java, the command it installs answers 127 on a machine with no Java of its
own, and the formula's test, which runs --version, fails there for that reason.
Decision
Section titled “Decision”1. A tap of RESTest's own
Section titled “1. A tap of RESTest's own”isa-group/homebrew-tap, holding Formula/restest.rb. A person types
brew install isa-group/tap/restest, and Homebrew fetches the tap the first time.
2. The formula installs the archive as it is, with a Java 21 it brings
Section titled “2. The formula installs the archive as it is, with a Java 21 it brings”- It downloads the release's
restest-<v>.tgz, checked against its SHA-256, and depends onopenjdk@21. It puts the archive's folder inlibexec, withoutbin/restest.cmd, and writesbin/restest, which setsJAVA_HOMEto that Java and starts the archive's launcher. The launcher stays the one place that knows how RESTest starts. openjdk@21: the Java the image runs on, the oldest RESTest supports and one its continuous integration tests.openjdkmoves to every new Java as it comes out, before RESTest is tested on it.JAVA_HOMEis set, not offered as a default (java_home_env, notoverridable_java_home_env). AJAVA_HOMEnaming a Java 17, which many developers have, would otherwise make the launcher refuse to start with 127, although Homebrew installed a Java 21 beside it. The command Homebrew installs ignoresJAVA_HOME; whoever wants RESTest on another Java uses the archive.- Its test, which
brew testruns, checks the version it says, and that a run against an address where nothing listens answers 3 and leaves its report. - A tap holds one version, the latest. The documentation says to pin a version with the image or the archive.
3. The formula is kept in RESTest, and filled in for each archive
Section titled “3. The formula is kept in RESTest, and filled in for each archive”dist/homebrew/restest.rb is the formula with @URL@ and @SHA256@ in place of the archive's
address and checksum, and dist/homebrew/formula.sh fills them in, refusing an address or a
checksum that is not one. The archive does not carry it.
The formula and the scripts that check it are those of the commit a workflow runs on; the archive is
the release's. A tag freezes the image and the archives it releases (ADR-0032 §3), not the formula,
which a later commit can correct for a release already published - as long as that commit's
check-restest.sh still passes on the release, since the same run checks it. ADR-0032 §7 refused to put files
from a later commit over an older release; nothing here touches a release, since the formula only
points at its archive. So a release made before this record, 2.2.0, reaches the tap when the
workflow below is started by hand from master.
4. Checked before a release is drafted, and again before the tap is written
Section titled “4. Checked before a release is drafted, and again before the tap is written”check-restest.shgains--own-java, for a command that starts RESTest on a Java of its own whatever Java the machine has. It is checked for what every way of starting RESTest is checked for, for starting through a symbolic link, and for starting - rather than answering 127 - with no Java on thePATHand aJAVA_HOMEthat holds none. The mode is named for that difference, not for Homebrew: an installer that brings no Java is checked as a launcher..github/scripts/check-homebrew.shinstalls a formula from a local tap, runs its test, and checks it withbrew style, or withbrew audit --strict --onlinewhen asked. It then compares what was installed with the archive Homebrew downloaded - every file butbin/restest.cmd, the JSON Schemas andTHIRD-PARTY.txtamong them, and nothing more - and runscheck-restest.sh --own-javaon the command. Homebrew movesLICENSE,NOTICEandREADME.mdfromlibexecto the top of what it installs, as it does for every formula, and the comparison allows for that. The script refuses to run where RESTest is installed with Homebrew already, removes only what it installed, leaving the Java and what that needs, and turns Homebrew's developer mode off again where it was off, sincebrew test,brew styleandbrew auditturn it on for good.release.ymlruns it on macOS and Linux for the formula filled in with the archive just built, and drafts the release only once it passes, as for the archives and the image.
5. Written into the tap once the release is published
Section titled “5. Written into the tap once the release is published”Homebrew cannot download a draft's files, so the formula goes into the tap from
.github/workflows/homebrew.yml, started by a release's publication, or by hand with a tag. A
workflow of the kind ADR-0032 §2 considered for the image, started by the publication, now exists for
the tap alone; the image is still public from the tag. In order, it:
- downloads the release's
checksums_sha256.txtwith no token, as anybody's Homebrew downloads the archive, and fills in the formula with the archive's address and its checksum from there; - checks that formula with
brew audit --strict --online, and installs and checks it on macOS and Linux - Homebrew downloading the archive and checking it against that checksum; - writes it into the tap - never over a higher version, and over the same version only when the
formula changed, which is how a fix to it reaches the tap. A formula's
revision, which makes Homebrew install a version again where it is installed already, is raised by hand when what the formula installs changes; - installs RESTest from the tap and checks it once more.
A pull request that touches the formula or its scripts runs the first two steps against the latest published release, and writes nothing.
The tap is written with a deploy key, which can write to that repository and no other and
belongs to no person. It is kept in an environment of RESTest's, homebrew-tap, which only master
and version tags may use, so a workflow pushed to another branch cannot read it. Where an
organisation forbids deploy keys, a GitHub App does the same with no person behind it.
Consequences
Section titled “Consequences”- On macOS and Linux,
brew install isa-group/tap/restestinstalls RESTest and the Java it runs on, andbrew upgrademoves it to the latest release. Homebrew does not serve Windows. With every formula they need, a first installation kept 625 MB on macOS and 823 MB on Ubuntu 24.04 in the continuous integration of the pull request that brought this record, and 1,506 MB in Homebrew's own Linux image, whose C library is older than Homebrew's: there Homebrew brings its own, and a compiler with it. - The formula reaches the tap some minutes after the release is published, and not at all when its
check fails: the tap stays as it was until the workflow is started again by hand.
release.ymlchecked the formula withbrew styleand afile://address before the draft;homebrew.ymlchecks it withbrew audit --strict --onlineand the release's own address, which reads the project's page and the release on GitHub, and applies any rule a newer Homebrew has added. A failure there can be the formula's, as well as Homebrew's or the network's. - Publishing a release now also writes the tap. The release steps in
docs/ci.mdsay so, and what the workflow tries from outside. - An owner of the organisation sets it up once: the tap, its deploy key, and the environment that holds the key.
- A tag no longer freezes everything a release is installed from: the formula of a release already published can change.
Alternatives considered
Section titled “Alternatives considered”- homebrew-core, where the command would be
brew install restestand Homebrew's own bot would follow each release. Its maintainers review a formula and set its terms - they preferopenjdkto a versioned Java - and take a project that submits itself once it has 225 stars on GitHub, 90 forks or 90 watchers; RESTest had 232 stars, 34 forks and 15 watchers on 27 October 2026. A tap is under RESTest's own control and checked in its own continuous integration, and the same formula would serve in homebrew-core. - JReleaser's formula, for the reason in the context: a command that answers 127 on a machine with no Java of its own. A formula of RESTest's own is some twenty lines.
- Written into the tap by hand at each release, with no workflow and no secret: one more step of every release that can be forgotten, and a check run on a person's machine.
openjdk, and aJAVA_HOMEthe person may override: §2.- The formula kept in the tap alone: it would be checked only once a release exists, never in a pull request of RESTest's.
- A personal access token in place of the deploy key: it ties the tap to one person, and expires.