From 0d71519a2f2d0f4cf7862968ec7bfb69194d9d16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 22:06:42 +0000 Subject: [PATCH 1/4] Bump org.beryx.jlink from 4.1.1 to 4.2.0 Bumps org.beryx.jlink from 4.1.1 to 4.2.0. --- updated-dependencies: - dependency-name: org.beryx.jlink dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ab2278b..8fa4e0e 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ plugins { id 'jacoco' // The platform image (design §7.3). forceMerge folds the jars that are not proper // modules into one that is, which jlink needs. - id 'org.beryx.jlink' version '4.1.1' + id 'org.beryx.jlink' version '4.2.0' } group = 'com.darkcollective.relix' From 0ca5e708e4044dfa65a97dd3585c8578f0991206 Mon Sep 17 00:00:00 2001 From: David Tooke Date: Thu, 5 Nov 2026 12:46:28 -0500 Subject: [PATCH 2/4] Fix consistency issues: terminal detection on JDK 22+, doc --function categories, --sandbox help, README status, development version - Host asks Console.isTerminal() when the JVM has it, so a pipe is not a terminal on 22+. - relix doc --function CATEGORY lists the category's functions (categories from the engine's libraries). - --sandbox help and the command page describe the file and how it combines with the limits. - README status and install section reflect the 0.1.x and 1.0.0-rc releases. - Development builds report 1.0.0-SNAPSHOT rather than 0.1.0-SNAPSHOT. --- README.md | 8 ++-- build.gradle | 5 +- docs/guide/command-line/commands/doc.md | 2 +- docs/guide/command-line/commands/relix.md | 2 +- .../relix/cli/command/DocCommand.java | 24 ++++++++-- .../relix/cli/command/GlobalOptions.java | 9 +++- .../com/darkcollective/relix/cli/io/Host.java | 37 ++++++++++++--- .../cli/command/ManagementCommandsTest.java | 18 +++++++ .../darkcollective/relix/cli/io/HostTest.java | 47 +++++++++++++++++++ 9 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 src/test/java/com/darkcollective/relix/cli/io/HostTest.java diff --git a/README.md b/README.md index 8e9f3ca..a59608d 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Its documentation is the command-line section of the Relix programming guide, in ## Status -Under development. There is no release yet, and the command line described above is -the design being built, not a shipped interface. +Released. 0.1.x are stable releases; 1.0.0 is being prepared through release candidates +(1.0.0-rcN), which GitHub lists as pre-releases. ## Building @@ -54,7 +54,9 @@ scoop bucket add relix https://github.com/DarkCollective/scoop-relix scoop install relix ``` -Or download the archive for your platform from the +Homebrew and Scoop install the latest stable release; a release candidate leaves the tap +and the bucket alone. To try one, or to install without a package manager, download the +archive for your platform from the [releases](https://github.com/DarkCollective/relix-cli/releases), check it against `checksums.txt`, unpack it and put its `bin/` on your `PATH`. diff --git a/build.gradle b/build.gradle index 8fa4e0e..7508636 100644 --- a/build.gradle +++ b/build.gradle @@ -15,8 +15,9 @@ plugins { } group = 'com.darkcollective.relix' -// A release is built with -PcliVersion=; anything else is a snapshot. -version = findProperty('cliVersion') ?: '0.1.0-SNAPSHOT' +// A release is built with -PcliVersion=; anything else is a snapshot of the line +// being worked towards (1.0.0, which sorts above 0.1.x and below its own release candidates). +version = findProperty('cliVersion') ?: '1.0.0-SNAPSHOT' // The engine pin: the one place the engine's version is named. // diff --git a/docs/guide/command-line/commands/doc.md b/docs/guide/command-line/commands/doc.md index e08bb14..e4c0165 100644 --- a/docs/guide/command-line/commands/doc.md +++ b/docs/guide/command-line/commands/doc.md @@ -18,7 +18,7 @@ relix doc [OPTIONS] [TOPIC] | Option | Description | |---|---| -| `--function` | Look TOPIC up among the functions only, or list only them. | +| `--function` | Look TOPIC up among the functions only, or list only them. TOPIC may also be a function category (string, json), which lists its functions. | | `--markdown` | Print the page's markdown as it is, not rendered. | | `--no-pager` | Write the page to standard output even on a terminal. | | `-o`, `--output=FORMAT` | The rows' format: table, tsv, csv, ndjson, json or markdown (default: $RELIX_OUTPUT, else relixrc's, else table on a terminal, tsv in a pipe). | diff --git a/docs/guide/command-line/commands/relix.md b/docs/guide/command-line/commands/relix.md index d8ba5ea..3758fb2 100644 --- a/docs/guide/command-line/commands/relix.md +++ b/docs/guide/command-line/commands/relix.md @@ -55,7 +55,7 @@ Every command takes these, before or after its name. | `--max-materialized-rows=N` | Fail a query in which one blocking operator buffers more than N rows. | | `--max-processed-rows=N` | Fail a query that processes more than N rows in all. | | `--timeout=DURATION` | Fail a query that runs longer than DURATION: 500ms, 30s, 5m, 1h, or ISO-8601 (PT30S). | -| `--sandbox=FILE` | Run under the sandbox FILE describes. | +| `--sandbox=FILE` | Run under the sandbox FILE describes: a JSON file, relative paths in it resolving against its own directory, with optional keys "declarations" (a .relix file of the only external sources, connections and imports scripts may use) and "limits" (maxInputChars, maxOutputRows, maxMaterializedRows, maxFixpointRounds, maxProcessedRows, timeoutMillis). An unknown key is an error. Where FILE and a --max-* option or --timeout both limit, the smaller applies. | | `--remote` | Permit http(s) file locations. | | `--allow-download` | Permit fetching a missing JDBC driver or connector during the run. | | `-h`, `--help` | Show this help message and exit. | diff --git a/src/main/java/com/darkcollective/relix/cli/command/DocCommand.java b/src/main/java/com/darkcollective/relix/cli/command/DocCommand.java index f1dc6cc..3b4a7b0 100644 --- a/src/main/java/com/darkcollective/relix/cli/command/DocCommand.java +++ b/src/main/java/com/darkcollective/relix/cli/command/DocCommand.java @@ -42,7 +42,7 @@ *

A topic is found by glyph, keyword or name: {@code σ}, {@code select} and * {@code selection} are one page. A language keyword keeps a name it shares with a * function, so {@code relix doc fix} is the recursion operator and - * {@code relix doc --function fix} is the function. The page is rendered for a terminal + * {@code relix doc --function fix} is the function. With {@code --function}, a category name lists its functions. The page is rendered for a terminal * and, on one, shown through {@code $PAGER}. With no topic, the pages are listed, as rows. * *

Pages are found by {@code relix-docs}' {@link ReferenceLookup}, over the language pages @@ -67,7 +67,7 @@ final class DocCommand implements Callable { String topic; @Option(names = "--function", - description = "Look TOPIC up among the functions only, or list only them.") + description = "Look TOPIC up among the functions only, or list only them. TOPIC may also be a function category (string, json), which lists its functions.") boolean function; @Option(names = "--markdown", description = "Print the page's markdown as it is, not rendered.") @@ -87,6 +87,17 @@ public Integer call() { list(invocation, index); return ExitCode.SUCCESS.status(); } + if (function && index.function(topic).isEmpty()) { + // A function category, such as string or json, lists its functions. The + // categories are those the installed libraries declare, not a list kept here. + Optional> category = index.functionsByCategory().entrySet().stream() + .filter(entry -> entry.getKey().equalsIgnoreCase(topic)) + .map(java.util.Map.Entry::getValue).findFirst(); + if (category.isPresent()) { + listing(invocation, category.get()); + return ExitCode.SUCCESS.status(); + } + } Optional found = function ? index.function(topic) : index.entry(topic); ReferencePage page = found.orElseThrow(() -> new CommandFailure(ExitCode.USAGE, "no reference page for '" + topic + "'" + (function ? " among the functions" : "") @@ -107,13 +118,18 @@ public Integer call() { /** The language pages, in the reference's order, unless --function; then the functions'. */ private void list(Invocation invocation, ReferenceLookup index) { - Listing listing = new Listing("topic", ScalarType.STRING, "category", ScalarType.STRING, - "title", ScalarType.STRING, "summary", ScalarType.STRING); List pages = new ArrayList<>(); if (!function) { pages.addAll(RelixDocs.referencePages()); } index.functionsByCategory().values().forEach(pages::addAll); + listing(invocation, pages); + } + + /** The given pages as the listing's rows. */ + private void listing(Invocation invocation, List pages) { + Listing listing = new Listing("topic", ScalarType.STRING, "category", ScalarType.STRING, + "title", ScalarType.STRING, "summary", ScalarType.STRING); for (ReferencePage page : pages) { listing.add(page.symbol(), page.category(), page.title(), page.summary()); } diff --git a/src/main/java/com/darkcollective/relix/cli/command/GlobalOptions.java b/src/main/java/com/darkcollective/relix/cli/command/GlobalOptions.java index 1974246..a44d21c 100644 --- a/src/main/java/com/darkcollective/relix/cli/command/GlobalOptions.java +++ b/src/main/java/com/darkcollective/relix/cli/command/GlobalOptions.java @@ -93,7 +93,14 @@ final class GlobalOptions { Duration timeout; @Option(names = "--sandbox", paramLabel = "FILE", scope = ScopeType.INHERIT, - description = "Run under the sandbox FILE describes.") + description = { + "Run under the sandbox FILE describes: a JSON file, relative paths in it", + "resolving against its own directory, with optional keys \"declarations\"", + "(a .relix file of the only external sources, connections and imports scripts", + "may use) and \"limits\" (maxInputChars, maxOutputRows, maxMaterializedRows,", + "maxFixpointRounds, maxProcessedRows, timeoutMillis). An unknown key is an", + "error. Where FILE and a --max-* option or --timeout both limit, the smaller", + "applies."}) Path sandbox; @Option(names = "--remote", scope = ScopeType.INHERIT, diff --git a/src/main/java/com/darkcollective/relix/cli/io/Host.java b/src/main/java/com/darkcollective/relix/cli/io/Host.java index cf7d7b6..5d160f8 100644 --- a/src/main/java/com/darkcollective/relix/cli/io/Host.java +++ b/src/main/java/com/darkcollective/relix/cli/io/Host.java @@ -15,6 +15,7 @@ */ package com.darkcollective.relix.cli.io; +import java.io.Console; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.InputStream; @@ -67,18 +68,42 @@ public record Host(InputStream in, OutputStream out, PrintStream err, * @return the real streams, environment and directories */ public static Host system() { - // On Java 21 a null console is the pipe check: System.console() is null when - // standard input or output is redirected, and it cannot tell which. Both are taken - // for pipes then, so `echo … | relix` writes tsv to a terminal; pass -o to choose. - // Java 22 returns a console for a pipe too, and this has to move to - // Console.isTerminal() with it. - boolean terminal = System.console() != null; + // Java 21: System.console() is null when standard input or output is redirected, and + // it cannot tell which, so both are taken for pipes then and `echo … | relix` writes + // tsv to a terminal; pass -o to choose. Java 22 returns a console for a pipe too and + // adds Console.isTerminal(), which is asked for when it exists. The command is built + // on 21, so the method is reached reflectively. + boolean terminal = terminal(System.console() != null, isTerminal(System.console())); return new Host(System.in, new FileOutputStream(FileDescriptor.out), System.err, System.getenv(), Path.of(System.getProperty("user.dir")), Path.of(System.getProperty("user.home")), terminal, terminal); } + /** + * Whether the process is on a terminal. + * + * @param console the JVM's console, or null when it has none + * @param verdict what {@code Console.isTerminal()} says of it, or null on a JVM that + * has no such method (before 22), where a console is all there is to go on + * @return false without a console; otherwise the verdict, else true + */ + static boolean terminal(boolean console, Boolean verdict) { + return console && (verdict == null || verdict); + } + + /** {@code Console.isTerminal()} of the JVM's console, or null before Java 22. */ + private static Boolean isTerminal(Console console) { + if (console == null) { + return null; + } + try { + return (Boolean) Console.class.getMethod("isTerminal").invoke(console); + } catch (ReflectiveOperationException e) { + return null; + } + } + /** * An environment variable, when it is set and not empty. * diff --git a/src/test/java/com/darkcollective/relix/cli/command/ManagementCommandsTest.java b/src/test/java/com/darkcollective/relix/cli/command/ManagementCommandsTest.java index bf546ee..f179858 100644 --- a/src/test/java/com/darkcollective/relix/cli/command/ManagementCommandsTest.java +++ b/src/test/java/com/darkcollective/relix/cli/command/ManagementCommandsTest.java @@ -16,7 +16,9 @@ package com.darkcollective.relix.cli.command; import com.darkcollective.relix.cli.Cli; +import com.darkcollective.relix.docs.ReferenceLookup; import com.darkcollective.relix.docs.RelixDocs; +import com.darkcollective.relix.function.FunctionCatalog; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -226,6 +228,22 @@ void functions() { assertThat(result.out().lines().skip(1)).isNotEmpty().allMatch(l -> l.split("\t")[1].equals("function")); } + @Test + @DisplayName("--function with a function category lists that category's functions, " + + "whatever categories the installed engine has") + void category() { + var categories = ReferenceLookup.of(FunctionCatalog.discover()).functionsByCategory(); + assertThat(categories).isNotEmpty(); + categories.forEach((name, pages) -> { + var result = cli().piped().run("doc", "--function", name); + + assertThat(result.status()).as(name).isZero(); + assertThat(result.out().lines().skip(1).map(l -> l.split("\t")[0]).toList()) + .as(name).containsExactlyInAnyOrderElementsOf( + pages.stream().map(page -> page.symbol()).toList()); + }); + } + @Test @DisplayName("resolves every key of every language page") void everyKey() { diff --git a/src/test/java/com/darkcollective/relix/cli/io/HostTest.java b/src/test/java/com/darkcollective/relix/cli/io/HostTest.java new file mode 100644 index 0000000..f408171 --- /dev/null +++ b/src/test/java/com/darkcollective/relix/cli/io/HostTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2026 Darkcollective, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.darkcollective.relix.cli.io; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** How the process decides that it is on a terminal. */ +@DisplayName("The host's terminal test") +class HostTest { + + @Test + @DisplayName("is false without a console, whatever isTerminal would say") + void noConsole() { + assertThat(Host.terminal(false, true)).isFalse(); + assertThat(Host.terminal(false, null)).isFalse(); + } + + @Test + @DisplayName("is the console's own answer where the JVM has Console.isTerminal (22+), " + + "so a pipe is not a terminal although it has a console") + void verdict() { + assertThat(Host.terminal(true, false)).isFalse(); + assertThat(Host.terminal(true, true)).isTrue(); + } + + @Test + @DisplayName("is the console's presence on a JVM without isTerminal (21)") + void noVerdict() { + assertThat(Host.terminal(true, null)).isTrue(); + } +} From cb0e4ee761878daed5a3700d86c695482a100771 Mon Sep 17 00:00:00 2001 From: David Tooke Date: Thu, 5 Nov 2026 15:24:48 -0500 Subject: [PATCH 3/4] Let a release candidate update the Homebrew tap and the Scoop bucket The tap job was skipped for a tag with a suffix, so brew and scoop installed 0.1.2 while the command was at 1.0.0-rc2. A candidate now pushes the formula and the manifest like a release does; Homebrew and Scoop both order 1.0.0-rcN below 1.0.0, so the final release replaces the candidates. The release scripts gain tests, in check and in the workflow. --- .github/workflows/release.yml | 8 ++- CONTRIBUTING.md | 5 +- README.md | 7 ++- build.gradle | 33 +++++++++++ packaging/test_release.py | 100 ++++++++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 packaging/test_release.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f785d14..e0a5d3b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,8 +4,10 @@ # Homebrew formula and Scoop manifest that install them, pushed to the tap # (DarkCollective/homebrew-relix) and the bucket (DarkCollective/scoop-relix). # -# A tag vX.Y.Z publishes X.Y.Z. A tag with a suffix (v1.2.0-rc1) publishes a pre-release -# and leaves the tap and the bucket alone. +# A tag vX.Y.Z publishes X.Y.Z. A tag with a suffix (v1.2.0-rc1) publishes a GitHub +# pre-release; the tap and the bucket follow it all the same, so `brew install relix` and +# `scoop install relix` give the newest release, a release candidate included. Both order +# 1.2.0-rc1 below 1.2.0, so the final release replaces its candidates as the install. # # Run from the Actions tab, it is a dry run: it builds and tests every archive, writes the # formula and the manifest and checks their checksums against the archives, and uploads @@ -127,6 +129,7 @@ jobs: pattern: archive-* path: dist merge-multiple: true + - run: python3 -m unittest discover -s packaging -p 'test_*.py' # Fails unless every archive the formula and the manifest name is in dist with the # checksum they give it. - run: python3 packaging/release.py "$VERSION" dist release-files @@ -168,7 +171,6 @@ jobs: tap: name: Update the Homebrew tap and the Scoop bucket needs: [version, publish] - if: needs.version.outputs.stable == 'true' runs-on: ubuntu-latest env: VERSION: ${{ needs.version.outputs.version }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 939e03a..4aa1e1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -168,8 +168,9 @@ property `baseDir`). workflow runs again, attaches the archives and `checksums.txt` to the GitHub Release, and pushes the formula to `DarkCollective/homebrew-relix` and the manifest to `DarkCollective/scoop-relix`, with the `HOMEBREW_TAP_PAT` and `SCOOP_BUCKET_PAT` - secrets. A tag with a suffix (`v1.2.0-rc1`) makes a pre-release and leaves the tap and - the bucket alone. + secrets. A tag with a suffix (`v1.2.0-rc1`) makes a GitHub pre-release and updates the + tap and the bucket in the same way, so they carry the newest release candidate until + the final release replaces it (Homebrew and Scoop both order `1.2.0-rc1` below `1.2.0`). The formula and the manifest are written whole from `packaging/` on every release, so a change to either is made there, not in the tap or the bucket. diff --git a/README.md b/README.md index a59608d..ccecef7 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,10 @@ scoop bucket add relix https://github.com/DarkCollective/scoop-relix scoop install relix ``` -Homebrew and Scoop install the latest stable release; a release candidate leaves the tap -and the bucket alone. To try one, or to install without a package manager, download the -archive for your platform from the +Homebrew and Scoop install the newest release, a release candidate included: while 1.0.0 +is being prepared, `relix` is the newest `1.0.0-rcN`, and the final 1.0.0 replaces it +(both order a candidate below its release). To install without a package manager, +download the archive for your platform from the [releases](https://github.com/DarkCollective/relix-cli/releases), check it against `checksums.txt`, unpack it and put its `bin/` on your `PATH`. diff --git a/build.gradle b/build.gradle index 7508636..5762cd5 100644 --- a/build.gradle +++ b/build.gradle @@ -380,6 +380,39 @@ DIR="$(cd "$(dirname "$LINK")" && pwd)"''') } } +// The release scripts' own tests (packaging/test_release.py): the formula and the manifest +// written for a release candidate and a final release, and the rule that the tap and the +// bucket follow both. Skipped, with a warning, on a machine with no Python; the release +// workflow runs them too. +def packagingScriptTest = tasks.register('packagingScriptTest') { + description = 'Runs the release scripts\' tests.' + inputs.dir(layout.projectDirectory.dir('packaging')) + inputs.file(layout.projectDirectory.file('.github/workflows/release.yml')) + def dir = layout.projectDirectory.dir('packaging').asFile + def stamp = layout.buildDirectory.file('packagingScriptTest.stamp') + outputs.file(stamp) + doLast { + def python = ['python3', 'python'].find { + try { new ProcessBuilder(it, '--version').redirectErrorStream(true).start().waitFor() == 0 } + catch (IOException ignored) { false } + } + if (python == null) { + logger.warn('No Python on the PATH: the release scripts\' tests were not run.') + } else { + def process = new ProcessBuilder(python, '-m', 'unittest', 'discover', '-s', dir.path, '-p', 'test_*.py') + .redirectErrorStream(true).start() + def output = process.inputStream.text + if (process.waitFor() != 0) { + throw new GradleException("The release scripts' tests failed:\n${output}") + } + } + stamp.get().asFile.text = 'ok' + } +} +tasks.named('check') { + dependsOn packagingScriptTest +} + // The man pages and completion scripts, from the command's picocli model. def releaseFilesDir = layout.buildDirectory.dir('release-files') def releaseFiles = tasks.register('releaseFiles', JavaExec) { diff --git a/packaging/test_release.py b/packaging/test_release.py new file mode 100644 index 0000000..62026d5 --- /dev/null +++ b/packaging/test_release.py @@ -0,0 +1,100 @@ +"""Tests of release.py and of the release workflow's rule for the tap and the bucket. + +Run with: python3 -m unittest discover -s packaging -p 'test_*.py' +""" +import json +import pathlib +import re +import tempfile +import unittest + +import release + +ROOT = pathlib.Path(__file__).resolve().parent.parent +WORKFLOW = (ROOT / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8") + + +def archives(version: str, directory: pathlib.Path) -> None: + for platform in release.PLATFORMS: + (directory / release.archive_name(version, platform)).write_bytes(platform.encode()) + + +class ReleaseFiles(unittest.TestCase): + def render(self, version: str): + with tempfile.TemporaryDirectory() as tmp: + dist, out = pathlib.Path(tmp, "dist"), pathlib.Path(tmp, "out") + dist.mkdir() + archives(version, dist) + import sys + argv, sys.argv = sys.argv, ["release.py", version, str(dist), str(out)] + try: + release.main() + finally: + sys.argv = argv + return ((out / "relix.rb").read_text(encoding="utf-8"), + json.loads((out / "relix.json").read_text(encoding="utf-8")), + (out / "checksums.txt").read_text(encoding="utf-8")) + + def test_a_release_candidate_fills_the_formula_and_the_manifest(self): + formula, manifest, sums = self.render("1.0.0-rc3") + self.assertIn('version "1.0.0-rc3"', formula) + self.assertEqual("1.0.0-rc3", manifest["version"]) + self.assertEqual("relix-1.0.0-rc3-windows-x86_64.zip", + manifest["architecture"]["64bit"]["url"].rsplit("/", 1)[1]) + self.assertEqual("relix-1.0.0-rc3", manifest["architecture"]["64bit"]["extract_dir"]) + self.assertEqual(5, len(sums.splitlines())) + + def test_a_stable_release_fills_them_too(self): + formula, manifest, _ = self.render("1.0.0") + self.assertIn('version "1.0.0"', formula) + self.assertEqual("1.0.0", manifest["version"]) + + def test_the_formula_names_the_archives_release_py_checks(self): + formula, _, _ = self.render("1.0.0-rc3") + self.assertEqual( + {release.archive_name("1.0.0-rc3", p) for p in release.PLATFORMS if "windows" not in p}, + set(release.formula_archives(formula, "1.0.0-rc3"))) + + def test_a_missing_archive_fails(self): + with tempfile.TemporaryDirectory() as tmp: + import sys + argv, sys.argv = sys.argv, ["release.py", "1.0.0-rc3", tmp, tmp + "/out"] + try: + with self.assertRaises(SystemExit): + release.main() + finally: + sys.argv = argv + + def test_a_version_that_is_not_one_fails(self): + import sys + argv, sys.argv = sys.argv, ["release.py", "rc3", ".", "out"] + try: + with self.assertRaises(SystemExit): + release.main() + finally: + sys.argv = argv + + +class TapWorkflow(unittest.TestCase): + def job(self, name: str) -> str: + match = re.search(rf"^ {name}:\n(.*?)(?=^ \w[\w-]*:\n|\Z)", WORKFLOW, re.S | re.M) + self.assertIsNotNone(match, f"no job {name}") + return match.group(1) + + def test_the_tap_job_runs_for_a_release_candidate(self): + tap = self.job("tap") + self.assertNotIn("stable", tap, "the tap job must not be skipped for a pre-release") + self.assertNotRegex(tap, r"(?m)^ if:") + self.assertIn("publish", re.search(r"needs: (.*)", tap).group(1)) + + def test_both_package_managers_are_pushed(self): + tap = self.job("tap") + self.assertIn("homebrew-relix", tap) + self.assertIn("scoop-relix", tap) + + def test_a_candidate_is_still_a_github_pre_release(self): + self.assertIn("--prerelease", self.job("publish")) + + +if __name__ == "__main__": + unittest.main() From 1d48f6bcbb7d4566ffb12b891e5f09753f061838 Mon Sep 17 00:00:00 2001 From: David Tooke Date: Thu, 5 Nov 2026 16:16:55 -0500 Subject: [PATCH 4/4] Pin the engine at 1.0.0-rc13 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5762cd5..31cc222 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ version = findProperty('cliVersion') ?: '1.0.0-SNAPSHOT' // branch instead (1.0.0-SNAPSHOT), which relix-core publishes to Central's snapshot // repository on every push. A release pins a released engine (checkEnginePin). // -PrelixEngineVersion= overrides it for one build, to try a candidate engine. -ext.relixEngineVersion = (findProperty('relixEngineVersion') ?: '1.0.0-rc12') as String +ext.relixEngineVersion = (findProperty('relixEngineVersion') ?: '1.0.0-rc13') as String def engineIsSnapshot = relixEngineVersion.endsWith('-SNAPSHOT')