Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6156197
Bump org.assertj:assertj-core from 3.27.4 to 3.27.7
dependabot[bot] Oct 7, 2026
4d2497e
Bump gradle/actions from 4 to 6
dependabot[bot] Oct 7, 2026
1a154e7
Bump actions/github-script from 7 to 9
dependabot[bot] Oct 7, 2026
5fb209f
Bump org.junit:junit-bom from 5.13.4 to 6.1.3
dependabot[bot] Oct 7, 2026
ef4790c
Bump actions/checkout from 4 to 7
dependabot[bot] Oct 7, 2026
cc83658
Bump actions/upload-artifact from 4 to 6
dependabot[bot] Oct 7, 2026
9bcc77a
Bump actions/setup-java from 4 to 6
dependabot[bot] Oct 8, 2026
c4af165
Bump gradle-wrapper from 8.14.4 to 9.8.0
dependabot[bot] Oct 7, 2026
99fbbed
Bump org.gradle.toolchains.foojay-resolver-convention
dependabot[bot] Oct 7, 2026
885a877
Store gradlew.bat with LF line endings, as .gitattributes asks
david-Darkcollective Oct 8, 2026
0a2e5a2
Say that building against a local engine needs a relix-core on Gradle 9
david-Darkcollective Oct 8, 2026
1965668
Add the TSV and NDJSON renderers
david-Darkcollective Oct 8, 2026
8ad6817
Choose the output format, and end a run cleanly at a closed pipe or a…
david-Darkcollective Oct 8, 2026
07435ac
Bind data inputs with -i NAME=[FORMAT:]LOCATION, standard input included
david-Darkcollective Oct 8, 2026
88a5f75
Load the catalog from the .relix/ directories, nearest declaration wi…
david-Darkcollective Oct 8, 2026
abc065c
Load a project's .relix/ only once the user has trusted it
david-Darkcollective Oct 8, 2026
805f7c2
Inspect the catalog: relix catalog files, ls, schema and where
david-Darkcollective Oct 8, 2026
889005f
Add the report commands: check, explain, optimize, trace, bundle, ir,…
david-Darkcollective Oct 8, 2026
663abbc
Add relix fmt: print scripts canonically, keeping their comments
david-Darkcollective Oct 8, 2026
52c9114
Add the management commands: drivers, connectors, doc, help, version,…
david-Darkcollective Oct 8, 2026
c892456
Adopt the relix-core#102 engine changes and drop their workarounds (#37)
david-Darkcollective Oct 8, 2026
b7d33f3
Align a table row that ends in a comment, and the rows after it
david-Darkcollective Oct 8, 2026
5814bdf
Add -a/--arg NAME=VALUE: bind query parameters outside the script text
david-Darkcollective Oct 8, 2026
49ef122
Re-enable fmt's test that a blank line after a comment is kept
david-Darkcollective Oct 9, 2026
7d02521
Make zsh's completion script loadable from $fpath as _relix
david-Darkcollective Oct 9, 2026
fdb7829
Package the command: jlink image, AppCDS, man pages, completions, rel…
david-Darkcollective Oct 9, 2026
619e33f
Publish the command-line guide: a page per command, every example run
david-Darkcollective Oct 9, 2026
f93237c
Name the guide's command-line chapters so they read among the guide's…
david-Darkcollective Oct 9, 2026
2141f69
Run the guide's examples in a UTF-8 locale, and keep its pages' LF en…
david-Darkcollective Oct 9, 2026
4721e94
Write the command pages with LF endings on Windows too
david-Darkcollective Oct 9, 2026
b542ce1
Pin the engine at 1.0.0-rc9
david-Darkcollective Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ gradlew text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Relix scripts: the tests compare what fmt and optimize write with them byte for byte.
*.relix text eol=lf
# The command-line guide: its command pages are generated, and compared with the
# generator's output byte for byte.
docs/guide/command-line/** text eol=lf
docs/command-examples/** text eol=lf

# Binary files.
*.jar binary
Expand Down
20 changes: 14 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# The gate: `./gradlew clean build` on Linux, macOS and Windows, for every pull request
# and every push to main or develop.
# The gate: `./gradlew clean build`, then the platform archive's tests, on Linux, macOS
# and Windows, for every pull request and every push to main or develop.
name: CI

on:
Expand All @@ -20,17 +20,25 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: corretto
java-version: '21'
# Also validates the checked-in wrapper jar against Gradle's published checksums.
- uses: gradle/actions/setup-gradle@v4
- uses: gradle/actions/setup-gradle@v6
# The completion scripts are syntax-checked by each shell that is installed; the
# runners have bash and PowerShell, and Linux gets zsh and fish here.
- name: Install zsh and fish
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y zsh fish
- run: ./gradlew clean build
# The platform archive, run as it ships: a pipeline through its launcher, the CDS
# archive mapped, the man pages rendered.
- run: ./gradlew packageTest
- name: Upload test and coverage reports
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: reports-${{ matrix.os }}
path: build/reports/
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/close-issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@v9
with:
script: |
const pr = context.payload.pull_request;
Expand Down
205 changes: 205 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# Releases the command (design §7.3): a platform archive for each of macOS, Linux and
# Windows, attached with their checksums to this repository's GitHub Release with the
# command-line guide's archive (relix-<version>-docs.zip, design §7.4), and the
# 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.
#
# 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
# them as the run's `release-files` artifact. It publishes nothing, and since it may run
# on develop, whose engine pin is a snapshot, it skips checkEnginePin.
name: Release

on:
push:
tags: ['v[0-9]*.[0-9]*.[0-9]*']
workflow_dispatch:
inputs:
version:
description: 'The version to build, without the v (a dry run publishes nothing)'
required: true
default: '0.0.0-dryrun'

permissions:
contents: read

jobs:
version:
name: Version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
publish: ${{ steps.version.outputs.publish }}
stable: ${{ steps.version.outputs.stable }}
steps:
- id: version
env:
EVENT: ${{ github.event_name }}
TAG: ${{ github.ref_name }}
INPUT: ${{ inputs.version }}
run: |
if [ "$EVENT" = push ]; then
version="${TAG#v}"
publish=true
else
version="$INPUT"
publish=false
fi
if ! printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; then
echo "::error::'$version' is not a version such as 1.2.0 or 1.2.0-rc1"
exit 1
fi
case "$version" in *-*) stable=false ;; *) stable=true ;; esac
{
echo "version=$version"
echo "publish=$publish"
echo "stable=$stable"
} >> "$GITHUB_OUTPUT"

package:
name: Package (${{ matrix.platform }})
needs: version
strategy:
fail-fast: true
matrix:
include:
- { os: macos-latest, platform: macos-aarch64 }
- { os: macos-15-intel, platform: macos-x86_64 }
- { os: ubuntu-latest, platform: linux-x86_64 }
- { os: ubuntu-24.04-arm, platform: linux-aarch64 }
- { os: windows-latest, platform: windows-x86_64 }
runs-on: ${{ matrix.os }}
timeout-minutes: 45
env:
VERSION: ${{ needs.version.outputs.version }}
# A dry run may build develop, whose engine pin is a snapshot; a release may not.
SKIP_PIN: ${{ needs.version.outputs.publish != 'true' && '-x checkEnginePin' || '' }}
steps:
- uses: actions/checkout@v7
# Corretto's JDK carries the jmods jlink links from.
- uses: actions/setup-java@v6
with:
distribution: corretto
java-version: '21'
- uses: gradle/actions/setup-gradle@v6
# The gate, then the archive, then the archive's own tests: a pipeline through its
# launcher, the CDS archive mapped, the man pages rendered.
- name: Build, package and test the archive
shell: bash
run: ./gradlew clean build releaseArchive packageTest -PcliVersion="$VERSION" $SKIP_PIN
# The command-line guide, the same on every platform, so it is built once. Its
# examples have just run in the build above.
- name: Build the command-line guide's archive
if: matrix.platform == 'linux-x86_64'
shell: bash
run: ./gradlew docsArchive -PcliVersion="$VERSION"
- name: Check the archive is this platform's
shell: bash
run: |
case "${{ matrix.platform }}" in windows-*) ext=zip ;; *) ext=tar.gz ;; esac
test -f "build/distributions/relix-$VERSION-${{ matrix.platform }}.$ext"
- uses: actions/upload-artifact@v6
with:
name: archive-${{ matrix.platform }}
path: build/distributions/relix-*
if-no-files-found: error
- name: Upload test reports
if: failure()
uses: actions/upload-artifact@v6
with:
name: reports-${{ matrix.platform }}
path: build/reports/
if-no-files-found: ignore

release-files:
name: Checksums, formula and manifest
needs: [version, package]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v7
with:
pattern: archive-*
path: dist
merge-multiple: true
# 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
- uses: actions/upload-artifact@v6
with:
name: release-files
path: release-files/
if-no-files-found: error

publish:
name: Publish the release
needs: [version, release-files]
if: needs.version.outputs.publish == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
env:
VERSION: ${{ needs.version.outputs.version }}
STABLE: ${{ needs.version.outputs.stable }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/download-artifact@v7
with:
pattern: archive-*
path: dist
merge-multiple: true
- uses: actions/download-artifact@v7
with:
name: release-files
path: release-files
- name: Create the GitHub Release
run: |
prerelease=()
if [ "$STABLE" != true ]; then prerelease=(--prerelease); fi
gh release create "v$VERSION" --repo "$GITHUB_REPOSITORY" --verify-tag \
--title "relix $VERSION" --generate-notes "${prerelease[@]}" \
dist/relix-"$VERSION"-* release-files/checksums.txt

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 }}
steps:
- uses: actions/download-artifact@v7
with:
name: release-files
path: release-files
- name: Push the formula to DarkCollective/homebrew-relix
env:
TOKEN: ${{ secrets.HOMEBREW_TAP_PAT }}
run: |
git clone "https://x-access-token:${TOKEN}@github.com/DarkCollective/homebrew-relix.git" tap
mkdir -p tap/Formula
cp release-files/relix.rb tap/Formula/relix.rb
cd tap
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Formula/relix.rb
git commit -m "relix $VERSION"
git push
- name: Push the manifest to DarkCollective/scoop-relix
env:
TOKEN: ${{ secrets.SCOOP_BUCKET_PAT }}
run: |
git clone "https://x-access-token:${TOKEN}@github.com/DarkCollective/scoop-relix.git" bucket
mkdir -p bucket/bucket
cp release-files/relix.json bucket/bucket/relix.json
cd bucket
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add bucket/relix.json
git commit -m "relix $VERSION"
git push
68 changes: 65 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,11 @@ below. You need no Java 21 installed beforehand: Gradle provisions it.
source set, which is a module; `EngineBoundaryTest` enforces it for every source
set, tests included. Run it alone with `./gradlew test --tests '*EngineBoundaryTest'`.
- **The engine pin.** `relixEngineVersion` in `build.gradle` is the one place the
engine's version is named. On `develop` it is `1.0.0-SNAPSHOT`, resolved from
Central's snapshot repository, which relix-core publishes to on every push to its
own `develop`. `checkEnginePin` fails a release build that still pins a snapshot:
engine's version is named. On `develop` it is a released engine while one has
everything the command needs, and `1.0.0-SNAPSHOT` while it needs engine work that
is not yet released, resolved from Central's snapshot repository, which relix-core
publishes to on every push to its own `develop`. `checkEnginePin` fails a release
build that still pins a snapshot:
`./gradlew checkEnginePin -PcliVersion=<release version>`.
- **One module, no library.** The module exports nothing (`ModuleShapeTest`) and
the build publishes no Maven artifact (`checkNoPublication`).
Expand All @@ -118,6 +120,22 @@ below. You need no Java 21 installed beforehand: Gradle provisions it.
the build unpacks into `build/engine-docs`. A page that disagrees is fixed in
relix-core.

### The release archive

The command ships as a platform archive: a jlink image with an AppCDS archive, the man
pages and the completion scripts (design §7.3). CI also runs its tests on each
platform, after `build`:

```bash
./gradlew packageTest
```

It builds the archive (`releaseArchive`, a minute or so), unpacks it with the system's
`tar`, and runs the launcher it holds: a script on standard input over a catalog in a
temporary tree, OPTIMIZE and `relix doc` (each finds a provider that jlink would leave
out unnoticed), `-Xshare:on` (which fails unless the CDS archive is used), and `man` over
every page. The man pages need `man` installed, and are not checked on Windows.

### Building against a local engine checkout

To try an engine change before it is published, build against your relix-core
Expand All @@ -133,6 +151,29 @@ together with the engine-internal module it implements, so the runtime class pat
holds some engine classes twice. That does no harm on a class path, but do not judge
a module-path launch by it.

The checkout's build runs under this repository's Gradle, so it needs a relix-core
that builds on Gradle 9. An older one fails while configuring, in a plugin that
Gradle 9 broke (for example `info.solidsoft.pitest`, which reports an unknown
property `baseDir`).

### Cutting a release (maintainers)

1. Pin a released engine in `build.gradle` (`checkEnginePin` refuses a snapshot) and
promote to `main`.
2. Run the **Release** workflow from the Actions tab with the version. It is a dry run:
it builds and tests the five platform archives, writes the Homebrew formula and the
Scoop manifest from [`packaging/`](packaging), checks their checksums against the
archives, and uploads them as the `release-files` artifact. Nothing is published.
3. Tag `main`: `git tag -a v1.2.0 -m "relix 1.2.0" && git push origin v1.2.0`. The
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.

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.

## Branches and commits

Work on a branch named with a prefix — `feat/`, `fix/`, `docs/`, `chore/` or
Expand All @@ -150,6 +191,27 @@ because that is the interface a script or a pipeline depends on.
A change to a command, an option or an exit status updates its help text, which is
also what the manual page and the site's command reference are generated from.

The command-line section of the programming guide is in
[`docs/guide/command-line`](docs/guide/command-line/README.md); the site renders it,
and each release attaches it as `relix-<version>-docs.zip`. Three things keep it true:

- **The command pages are generated.** `docs/guide/command-line/commands/*.md` and
`index.json` come from the command's picocli model, followed by the hand-written
examples in `docs/command-examples/<command>.md`. After changing an option or one of
those files, run `./gradlew commandPages` and commit what it writes;
`GuideStructureTest` fails while they are stale.
- **Every example runs.** A ```` ```shell ```` block is an example, and the plain block on
the line after it is what it prints on standard output; `GuideExamplesTest` runs it in
a copy of the fixture under `src/test/resources/docs-fixture` (the `~/shop` the pages
describe) and compares. An example exits 0 unless it ends `; echo "exit $?"`, which
prints its status for the page to show. Write commands a reader would type in a
```` ```shell ```` block, never `bash` or `console`, and keep anything else — an install
command, a syntax summary — in a ```` ```text ```` block.
- **Every link resolves**, to a page or a heading, here or in the engine's manuals:
the section sits at `docs/guide/command-line` in the engine's tree, so
`../secrets.md` is the guide's page and `../../reference/README.md` the language
reference. A new page is listed in the section's `README.md`.

## Questions

Ask in [Discussions](https://github.com/DarkCollective/relix-cli/discussions).
Loading
Loading