ci: run the affordable gate set on push and publish at the tag
Test / test (push) Successful in 1m45s

Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
2026-10-04 21:17:40 +02:00
parent 5a8e9acbf3
commit ae1baa0e61
9 changed files with 196 additions and 257 deletions
-1
View File
@@ -22,7 +22,6 @@ env:
jobs: jobs:
build: build:
runs-on: fedora runs-on: fedora
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
-37
View File
@@ -1,37 +0,0 @@
# Race, Go. Dispatched by hand, and never a gate on a push or a tag: the release tag is
# cut only after `just gates` has already raced the tree, so this workflow is the
# explicit second opinion, not a step of the release.
#
# The race detector roughly doubles both time and memory, which the shared runner box
# cannot afford on every push. Locally it belongs to `just gates`, which runs it once per
# task; here it is a decision rather than a routine.
#
# Every step is one command, so the step that fails is the gate that failed.
name: Race
on:
workflow_dispatch:
env:
# One core: parallelism buys no speed here and costs memory the box does not have.
GOFLAGS: -p=1
GOMAXPROCS: "2"
jobs:
race:
runs-on: fedora
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Install gcc
# The race detector needs cgo and the runner image carries no C compiler.
run: dnf install -y gcc
- name: Race
run: go test -race -count=1 -timeout 10m ./...
+19 -116
View File
@@ -1,20 +1,25 @@
# Release, Go binaries. Runs on version tags (v1.2.3) pushed to main. # Release, Go binaries. Runs on version tags (v1.2.3) pushed to main.
# #
# The module sits at the repository root: the toolchain records a version only for a root # No gate runs at the tag: the tagged tree was tested on every push to development,
# module, measured on go1.27.1, so a build of a module in a subdirectory reports (devel) # the full suite is the suite workflow's business, and race never runs in CI at all.
# even at its own <module>/vX.Y.Z tag and this workflow's smoke test can never pass for # This pipeline publishes and nothing else. The version contract has no injection
# it. A Go repository is one module at the root. # step: the toolchain records the tag into the binary's build information, so the
# build simply has to happen at the tag, which the trigger guarantees.
# #
# The version contract these steps implement: nothing is injected. The toolchain records # The module sits at the repository root: the toolchain records a version only for a
# the tag into the binary's build information, so the build simply has to happen at the # root module, measured on go1.27.1, so a build of a module in a subdirectory reports
# tag, which the trigger guarantees. # (devel) even at its own <module>/vX.Y.Z tag and this workflow's smoke test can never
# pass for it. A Go repository is one module at the root.
# #
# The gates run in their own job, once, before the matrix, minus the race detector: race # The matrix carries the platforms the project ships: Linux on amd64, arm64, loong64
# never runs on a push path or a tag, and the local gate raced this tree before the tag # and riscv64. The FreeBSD port compiles in its own dispatched workflow and ships no
# was cut. Putting the gates inside the matrix would run the whole suite once per target # binary. Nothing is installed: the fedora job image carries git, perl and node
# on the box that also hosts the forge. Each job validates the tag for itself rather than # (verified on the runner, 2026-10-04).
# passing a value between jobs, so no workflow feature has to be trusted for the version #
# to reach the file name. # Each job validates the tag for itself rather than passing a value between jobs, so
# no workflow feature has to be trusted for the version to reach the file name. Every
# step is one command, and the scripted steps are Perl with builtins only: Perl drives
# curl through a list, so no argument is ever word-split, globbed or quoted wrong.
name: Release name: Release
on: on:
@@ -22,111 +27,16 @@ on:
tags: ["v*"] tags: ["v*"]
env: env:
# The box is shared with the forge, so parallelism is bounded on purpose. The gates job # The box is shared with the forge, so parallelism is bounded on purpose.
# needs it most; the build jobs inherit it for their parallel compilation.
GOFLAGS: -p=1 GOFLAGS: -p=1
GOMAXPROCS: "2" GOMAXPROCS: "2"
jobs: jobs:
gates:
runs-on: fedora
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Install Perl
# Perl for the steps below. The install is a no-op where the package
# is already present.
run: dnf install -y perl
- name: Validate the tag
env:
VERSION: ${{ gitea.ref_name }}
run: |
perl -e '
my $v = $ENV{VERSION} // q{};
$v =~ m{^v[0-9]+(\.[0-9]+){0,2}([-+].*)?$}
or die qq{ERROR: expected a semver tag like v1.2.3, got: $v\n};
print qq{tag $v\n};
'
- name: Security policy names this release
# The supported-versions table is the one part of SECURITY.md that
# carries a version, so it goes stale the moment a tag is cut. Fail
# here rather than publish a policy naming the previous release.
env:
VERSION: ${{ gitea.ref_name }}
run: |
perl -e '
my $v = $ENV{VERSION} // q{};
(my $nv = $v) =~ s/^v//;
open(my $f, q{<}, q{SECURITY.md}) or die qq{SECURITY.md: $!\n};
local $/;
my $t = <$f>;
close $f;
$t =~ m{^\|\s*\Q$nv\E\s*\|\s*yes\s*\|}m
or die qq{ERROR: SECURITY.md does not name $nv as supported; update the table before releasing.\n};
print qq{SECURITY.md names $nv\n};
'
- name: Build
run: go build ./...
- name: Format
run: |
perl -e '
open(my $g, q{-|}, q{gofmt}, q{-l}, q{.}) or die qq{gofmt: $!};
my @bad = <$g>;
close($g);
print @bad;
exit(@bad ? 1 : 0);
'
- name: Vet
run: go vet ./...
- name: Modernise
run: go fix -diff ./...
- name: Tests
# The same command as in test.yml, so the floor is the same number everywhere.
run: go test -count=1 -timeout 10m -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
- name: Tests outside the coverage set
# The same command as in test.yml: the CLI's exit codes and manual-page guard,
# and the debugger's architecture-neutral units, run outside the floor.
run: go test -count=1 -timeout 10m ./cmd/... ./debug/...
- name: Coverage floor
run: |
perl -e '
open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!};
my $total;
while (my $l = <$c>) { $total = $1 if $l =~ m{^total:\s+\S+\s+([0-9.]+)%} }
close($c);
die qq{no total line in coverage.out\n} unless defined $total;
printf qq{Total coverage: %s%%\n}, $total;
exit($total < 80 ? 1 : 0);
'
build: build:
runs-on: fedora runs-on: fedora
timeout-minutes: 25
needs: gates
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# Portable targets: amd64, arm64, loong64 and riscv64 on Linux, at the toolchain
# default level. No 32-bit, no wasm, no macOS, no Windows. FreeBSD stays out until
# verify/jit.go ports off syscall.Mprotect: the Go syscall package defines no
# Mprotect for freebsd, and verify/jit.go:50 calls it to drop the write bit from
# the JIT mapping, so every freebsd target fails to build with "undefined:
# syscall.Mprotect" (verified for amd64, arm64 and riscv64 on go1.27.1).
include: include:
- goos: linux - goos: linux
goarch: amd64 goarch: amd64
@@ -144,9 +54,6 @@ jobs:
go-version-file: go.mod go-version-file: go.mod
cache: true cache: true
- name: Install Perl
run: dnf install -y perl
- name: Validate the tag - name: Validate the tag
id: version id: version
env: env:
@@ -209,7 +116,6 @@ jobs:
release: release:
runs-on: fedora runs-on: fedora
timeout-minutes: 15
needs: build needs: build
permissions: permissions:
# contents: read is required for the checkout: a job that declares any # contents: read is required for the checkout: a job that declares any
@@ -226,9 +132,6 @@ jobs:
with: with:
path: dist path: dist
- name: Install Perl
run: dnf install -y perl
- name: Extract the CHANGELOG section - name: Extract the CHANGELOG section
env: env:
VERSION: ${{ gitea.ref_name }} VERSION: ${{ gitea.ref_name }}
+76
View File
@@ -0,0 +1,76 @@
# Suite, Go. Dispatched by hand, on development. Never a push gate.
#
# The complete gate set minus race: the build, both static gates, the full suite with
# every short-layer skip unskipped, and the coverage floor. It is the pipeline form of
# the local `just test`, for the moments when the tree must be proven end to end and
# nobody is at the keyboard.
#
# Race never runs in CI. It roughly doubles the time and the memory on a box shared
# with the forge, and the local `just gates` races the tree on the machine at the
# keyboard, which is where that gate belongs.
name: Suite
on:
workflow_dispatch:
env:
# One core: parallelism buys no speed here and costs memory the box does not have.
GOFLAGS: -p=1
GOMAXPROCS: "2"
jobs:
suite:
runs-on: fedora
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
# The module is the source of truth for the version, so it cannot drift.
go-version-file: go.mod
cache: true
- name: Build
run: go build ./...
- name: Format
run: |
perl -e '
open(my $g, q{-|}, q{gofmt}, q{-l}, q{.}) or die qq{gofmt: $!};
my @bad = <$g>;
close($g);
print @bad;
exit(@bad ? 1 : 0);
'
- name: Vet
run: go vet ./...
- name: Modernise
# Exits non-zero when it has something to rewrite, so it needs no output capture.
run: go fix -diff ./...
- name: Tests
# The full suite over the logic packages (`packages` in the justfile), every
# short-layer skip lifted, with the coverage profile. No timeout: a run that
# does not finish is a defect to find.
run: go test -count=1 -timeout 0 -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
- name: Tests outside the coverage set
# The same second invocation as the local gate: the CLI's exit codes and
# manual-page guard, and the debugger's units, live ptrace sessions included.
# They run without a profile, because a thin main and a ptrace-bound package
# would drag the floor down rather than measure the product.
run: go test -count=1 -timeout 0 ./cmd/... ./debug/...
- name: Coverage floor
run: |
perl -e '
open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!};
my $total;
while (my $l = <$c>) { $total = $1 if $l =~ m{^total:\s+\S+\s+([0-9.]+)%} }
close($c);
die qq{no total line in coverage.out\n} unless defined $total;
printf qq{Total coverage: %s%%\n}, $total;
exit($total < 80 ? 1 : 0);
'
+23 -53
View File
@@ -1,22 +1,20 @@
# Test, Go. Push and pull request to development. Never on main. # Test, Go. Push and pull request to development. Never on main.
# #
# The gates are the ones the justfile's `gates` recipe runs, minus race: the shared # The push path owns a two-minute budget end to end, so it carries exactly the gates
# runner box cannot afford the race detector on every push, so it lives in race.yml. # that fit it: the format check, go vet, the suite's short layer and the coverage
# The box is one core and 2 GB beside Gitea, so parallelism is bounded on purpose and # floor. There is no build step (go test compiles what it runs) and the modernisation
# everything runs in one job. Extra jobs would duplicate the checkout, the Go setup and # gate (`go fix -diff`) belongs to the suite workflow, which is dispatched by hand.
# the dependency download three times without buying any parallelism. # Nothing is installed: the fedora job image carries git, perl and node (verified on
# the runner, 2026-10-04), and no pipeline ever installs gcc or runs the race detector.
# #
# The budget is part of the contract: a push run is fast and light, about two minutes, # The live ptrace sessions and the other deliberate-run categories skip under
# and nothing that cannot run natively on the runner belongs here. The FreeBSD compile # testing.Short: they need the machine to themselves and belong to the suite workflow
# gates live in freebsd.yml behind workflow_dispatch for that reason; the GOOBJ link # and to the local `just test`, never to every push.
# parity campaign is an opt-in local verification (just link-parity).
# #
# Every step is one command, so the step that fails is the gate that failed, and no shell # Every step is one command, so the step that fails is the gate that failed, and no
# option has to be trusted for the run to stop. The scripted steps are Perl, not shell and # shell option has to be trusted for the run to stop. The scripted steps are Perl with
# not Python: Perl behaves the same on both runner images, there is no bashism to trip over # builtins only: Fedora packages the Perl modules separately, so nothing beyond `perl`
# on ash, and it is one language instead of two. The Perl uses builtins only, because # itself may be assumed present, and there is no bashism to trip over.
# Fedora packages the Perl modules separately and nothing beyond `perl` itself may be
# assumed present.
name: Test name: Test
on: on:
@@ -41,7 +39,6 @@ concurrency:
jobs: jobs:
test: test:
runs-on: fedora runs-on: fedora
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
@@ -51,15 +48,6 @@ jobs:
go-version-file: go.mod go-version-file: go.mod
cache: true cache: true
# No Install Perl step: the fedora image carries perl (verified by run
# 76: the install degraded into a package upgrade costing ~50 s), and a
# dnf on the push path is network work the budget does not need.
# The steps follow the `gates` order of the justfile contract: build, format,
# vet, test. The vet gate is go vet and go fix -diff, two steps here.
- name: Build
run: go build ./...
- name: Format - name: Format
run: | run: |
perl -e ' perl -e '
@@ -73,37 +61,19 @@ jobs:
- name: Vet - name: Vet
run: go vet ./... run: go vet ./...
- name: Modernise
# Exits non-zero when it has something to rewrite, so it needs no output capture.
run: go fix -diff ./...
- name: Tests - name: Tests
# The suite must be fast: a push pipeline that cannot finish in a few minutes moves # The pattern is `packages` in the project's justfile, so the floor is the
# its heavy part behind a dispatch. The inner timeout matches the job's, so a # same number the local gate reports: the logic packages, since a thin cmd/
# hanging test reports its own goroutine dump rather than a silent job kill. # and a ptrace-bound debug package would drag the total under it. No timeout
# The pattern is `packages` in the project's justfile: the logic packages, since a # anywhere: `-timeout 0` disables go test's own ten-minute default, because a
# thin cmd/ would drag the total under the floor. release.yml runs the same # run that does not finish is a defect to find and a timeout only hides it.
# command, so the floor is the same number everywhere. ./verify/... carries the run: go test -short -count=1 -timeout 0 -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
# live oracle-parity comparison against `go tool asm` (the TestGroundTruth
# suites); the runner's Go setup provides both the tool and GOROOT.
# -short skips the deliberate-run categories inside the suites (the live
# ptrace sessions above all): they need the machine to themselves and a
# starved single-core runner turns each into a timeout the budget cannot
# carry. The local `just test` gate runs everything, in full.
run: go test -short -count=1 -timeout 10m -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
- name: Tests outside the coverage set - name: Tests outside the coverage set
# The CLI and the debugger sit outside `packages` because a thin main and a # The CLI's exit codes and manual-page guard and the debugger's
# ptrace-bound package pull the total under the floor, but their tests guard # architecture-neutral units, still tested but outside the profile the floor
# shipped surfaces: the command exit codes, the manual pages against the # is computed from. `-short` skips the debugger's live ptrace sessions.
# binary's own help, and the debugger's architecture-neutral units. They run run: go test -short -count=1 -timeout 0 ./cmd/... ./debug/...
# here so the floor stays a product measure and nothing is left untested.
# -short skips the debugger's live ptrace sessions, the deliberate-run
# category the runner cannot starve-proof. The live go-tool-asm oracle
# comparison (TestGroundTruth in ./verify/...) runs inside the coverage
# sweep above; it is not re-run as its own step, because every second on
# this box is budget.
run: go test -short -count=1 -timeout 10m ./cmd/... ./debug/...
- name: Coverage floor - name: Coverage floor
run: | run: |
+7 -5
View File
@@ -117,12 +117,14 @@ Workflows live in `.gitea/workflows/` and run on the project's own runners:
| Workflow | Trigger | What it does | | Workflow | Trigger | What it does |
|---|---|---| |---|---|---|
| Test | push or pull request to `development` | build, format check, vet, modernisation, the test suite with the coverage floor, the CLI and debugger tests outside the profile, then the oracle-parity rerun against `go tool asm` | | Test | push or pull request to `development` | the format check, `go vet`, and the short layer of the test suite with the coverage profile and the 80 % floor, inside the two-minute budget |
| Release | a `v*` tag | the same gates as Test minus the oracle-parity step, then the matrix build, the version smoke test and the release itself; the race detector runs locally in `just gates` before the tag is cut | | Suite | dispatched by hand | the complete gate set minus race: the build, both static gates, the full suite and the coverage floor |
| FreeBSD build | dispatched by hand | the three FreeBSD compile gates for amd64, arm64 and riscv64 |
| Release | a `v*` tag | the matrix build, the version smoke test and the release with its assets; no gate runs at the tag |
The local equivalent is `just gates`, which is the same set plus the race detector. The The local equivalent is `just gates`, which is the same set plus the race detector. Race
race detector also has its own workflow, dispatched by hand; it never runs on a push or a never runs in CI, on a push or a tag: it would double the time and the memory a shared
tag, where it would double the time and the memory a shared runner cannot spare. runner cannot spare, so the local `just gates` runs it before the tag is cut.
## Reporting bugs ## Reporting bugs
+4 -3
View File
@@ -162,9 +162,10 @@ other. Encoding parity is the exception: the byte comparison against the
toolchain runs on the host for every architecture, so no emulator stands toolchain runs on the host for every architecture, so no emulator stands
between the claim and the evidence. The debugger is the weakest case: on between the claim and the evidence. The debugger is the weakest case: on
the three emulated architectures its per-architecture ptrace code has the three emulated architectures its per-architecture ptrace code has
been compiled and read, never executed. Its architecture-neutral units been compiled and read, never executed. Its units run under
run under `go test ./...`, which the race workflow and a manual run `go test ./...`, which `just test` sweeps in a second invocation beside
perform; the default `just test` gate does not sweep `./debug/...`. the coverage profile; the live ptrace sessions are skipped in short
mode, so they run locally or in the dispatched suite, never on a push.
## The documentation goal ## The documentation goal
+37 -21
View File
@@ -32,12 +32,13 @@ Every recipe in the `justfile`, and what it does.
| Recipe | What it does | | Recipe | What it does |
|---|---| |---|---|
| `default` (bare `just`) | prints the recipe list (`@just --list`) | | `default` (bare `just`) | prints the recipe list (`@just --list`) |
| `just build` | compiles `bin/gasm` with `CGO_ENABLED=0` and stripped symbols; zero errors and zero warnings | | `just build` | compiles `bin/gasm` with `CGO_ENABLED=0`, `-trimpath` and `-buildvcs=true`, symbols stripped; zero errors and zero warnings |
| `just test` | the test gate: the suite with `-count=1`, the coverage profile and the 80 % floor, then the CLI and debugger tests outside the profile | | `just test` | the test gate: the full suite with `-count=1` and `-timeout 0` under the 4 GiB memory fence, the coverage profile and the 80 % floor, then the CLI and debugger tests outside the profile |
| `just race` | the same suite under the race detector; the expensive one, so it runs once, inside `gates` | | `just race` | the same suite under the race detector, under its own 16 GiB fence; the expensive one, so it runs once, inside `gates` |
| `just unit [packages] [run]` | fast, cached, scoped run for iterating: no race and no coverage, so an unchanged package reports instantly | | `just unit [packages] [run]` | fast, cached, scoped run for iterating, under the fence: no race and no coverage, so an unchanged package reports instantly |
| `just fuzz <target> <pkg> [fuzztime]` | time-boxed fuzz of one target; the package is required, because `go test -fuzz` refuses more than one | | `just fuzz <target> <pkg> [fuzztime]` | time-boxed fuzz of one target, under the fence; the package is required, because `go test -fuzz` refuses more than one |
| `just bench [packages]` | benchmarks (`-benchmem -count=5`); on an idle machine only | | `just link-parity` | the opt-in cmd/link GOOBJ parity gate; it costs minutes no pipeline can afford |
| `just bench [packages]` | benchmarks (`-benchmem -count=5`); deliberately unfenced, on an idle machine only |
| `just fmt` | formats the tree in place with `gofmt` | | `just fmt` | formats the tree in place with `gofmt` |
| `just fmt-check` | zero diff; prints nothing when everything is formatted, which is the shape the CI step wants | | `just fmt-check` | zero diff; prints nothing when everything is formatted, which is the shape the CI step wants |
| `just vet` | both static gates: `go vet` and `go fix -diff` | | `just vet` | both static gates: `go vet` and `go fix -diff` |
@@ -54,7 +55,7 @@ Every recipe in the `justfile`, and what it does.
### `just test` ### `just test`
```sh ```sh
go test -count=1 -timeout 10m -coverprofile=coverage.out \ go test -count=1 -timeout 0 -coverprofile=coverage.out \
./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... \ ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... \
./lint/... ./lsp/... ./parser/... ./token/... ./verify/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
``` ```
@@ -67,14 +68,18 @@ would drag the coverage total under the floor. Their tests still run, in
a second invocation without a profile: a second invocation without a profile:
```sh ```sh
go test -count=1 -timeout 10m ./cmd/... ./debug/... go test -count=1 -timeout 0 ./cmd/... ./debug/...
``` ```
That covers the CLI's exit codes and the guard that compares the manual That covers the CLI's exit codes and the guard that compares the manual
pages with the binary's own help, and the debugger's architecture-neutral pages with the binary's own help, and the debugger's units. The floor
units. The floor fails if the total is below 80 %. CI runs the same two fails if the total is below 80 %. Both invocations run under a 4 GiB
commands with the same ten-minute bound, so cgroup fence with swap off, so a runaway test dies as a failed run
the number is the same everywhere. instead of eating the machine, and no timeout is set: `-timeout 0`
disables the ten-minute default `go test` would otherwise impose. The
push pipeline runs the same two commands with `-short`, the suite
workflow and the local gate run them in full, so the number is the same
everywhere.
### `just run` ### `just run`
@@ -136,19 +141,30 @@ crash later.
## Continuous integration ## Continuous integration
Workflows live in `.gitea/workflows/` and run on the project's own runners: Workflows live in `.gitea/workflows/` and run on the project's own runners:
Test on a push or pull request to `development`, race dispatched by hand, and
the release on a `v*` tag. They are written by hand rather than through | Workflow | Trigger | What it does |
`just`, but they enforce the same set of gates minus the race detector, which |---|---|---|
the shared runner cannot afford on a push; a green `just gates` locally is | Test | push or pull request to `development` | the format check, `go vet`, the short layer of the suite with the coverage profile and the 80 % floor, inside the two-minute budget |
therefore the fastest way to a green pipeline. | Suite | dispatched by hand | the complete gate set minus race: the build, the format check, `go vet`, `go fix -diff`, the full suite and the coverage floor |
| FreeBSD build | dispatched by hand | the three FreeBSD compile gates: `GOOS=freebsd` for amd64, arm64 and riscv64 |
| Release | a `v*` tag | the matrix build, the version smoke test, and the release with its assets; no gate runs at the tag |
The pipelines are written by hand rather than through `just`, but they enforce
the same gates: the push path carries the affordable subset and the heavy tests
skip under `testing.Short`, so the dispatched suite is the moment to prove the
tree end to end. Race never runs in CI, on a push or a tag: it roughly doubles
the time and the memory on a shared runner, and the local `just gates` races
the tree on the machine at the keyboard before the tag is cut.
## Releases ## Releases
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`, Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`,
which triggers the release workflow: it builds the portable Linux targets, which triggers the release workflow: it builds the portable Linux targets
takes the notes from the matching `CHANGELOG.md` section and uploads the (amd64, arm64, loong64, riscv64), takes the notes from the matching
assets. `SECURITY.md` carries the supported-versions table, so that table `CHANGELOG.md` section and uploads the assets. No gate runs at the tag, so
moves with the release; the pipeline refuses a tag the policy does not name. `just gates` must be green on the tree before the tag is cut. `SECURITY.md`
carries the supported-versions table, naming the version being released; the
release commit updates it by hand.
The version is never injected. `gasm --version` prints what the The version is never injected. `gasm --version` prints what the
toolchain recorded in the build information: the tag on a tagged toolchain recorded in the build information: the tag on a tagged
+30 -21
View File
@@ -1,8 +1,9 @@
# gasm-sdk. # gasm-sdk: Go's Plan 9 assembler and the tooling around it.
# #
# Replace gasm-sdk here and the values in the variable block. Everything below the block # A binary: `just install` copies gasm into bindir. Everything below the variable block
# is the standard recipe set: the names and their meanings are fixed, identical in every # is the standard recipe set: the names and their meanings are fixed, and `gates` is the
# repository, and `gates` is the definition of done. # definition of done. link-parity, install-man, uninstall-man and gen are the project
# extensions.
binary := "gasm" binary := "gasm"
package := "./cmd/gasm" package := "./cmd/gasm"
@@ -12,6 +13,16 @@ package := "./cmd/gasm"
# failure, not an empty run. # failure, not an empty run.
packages := "./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/..." packages := "./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/..."
# The memory fence for the test recipes: a cgroup ceiling with swap off, so a
# runaway run dies at the ceiling as a failed run and never eats the machine. 4G is the
# default; raise it only with a reason recorded here.
memlimit := "4G"
# The race recipe's own fence: with the logic packages run in parallel the race
# footprint peaks over 8G (8G died in the parser package, 16G passed, measured
# 2026-10-02), and the wide machine at the keyboard can carry it.
racememlimit := "16G"
bindir := env_var_or_default("BINDIR", env_var("HOME") / ".local" / "bin") bindir := env_var_or_default("BINDIR", env_var("HOME") / ".local" / "bin")
# Where `install-man` puts the gzip-compressed pages (man1 below it). Exported because # Where `install-man` puts the gzip-compressed pages (man1 below it). Exported because
@@ -21,22 +32,23 @@ export MANDIR := env_var_or_default("MANDIR", env_var("HOME") / ".local" / "shar
default: default:
@just --list @just --list
# Compile. Zero errors, zero warnings. # Compile. Zero errors, zero warnings; -trimpath and -buildvcs=true make the binary place-independent and version-stamped.
build: build:
CGO_ENABLED=0 go build -ldflags "-s -w" -o bin/{{binary}} {{package}} CGO_ENABLED=0 go build -trimpath -buildvcs=true -ldflags "-s -w" -o bin/{{binary}} {{package}}
# The test gate: the suite, no cache, the coverage floor. # The test gate: the full suite, no cache, the coverage floor, under the memory fence.
test: test:
#!/usr/bin/env perl #!/usr/bin/env perl
my @fence = qw{systemd-run --user --scope -p MemoryMax=4G -p MemorySwapMax=0}; my @fence = (q{systemd-run}, q{--user}, q{--scope},
system(@fence, q{go}, q{test}, q{-count=1}, q{-timeout}, q{10m}, q{-p}, q{MemoryMax={{memlimit}}}, q{-p}, q{MemorySwapMax=0});
system(@fence, q{go}, q{test}, q{-count=1}, q{-timeout}, q{0},
q{-coverprofile}, q{coverage.out}, qw({{packages}})) == 0 q{-coverprofile}, q{coverage.out}, qw({{packages}})) == 0
or die qq{the test suite failed\n}; or die qq{the test suite failed\n};
# The packages outside the coverage set carry tests of their own: the CLI's # The packages outside the coverage set carry tests of their own: the CLI's
# exit codes and manual-page guard, and the debugger's architecture-neutral # exit codes and manual-page guard, and the debugger's architecture-neutral
# units. They run without a profile, because a thin main and a ptrace-bound # units. They run without a profile, because a thin main and a ptrace-bound
# package would drag the floor down rather than measure the product. # package would drag the floor down rather than measure the product.
system(@fence, q{go}, q{test}, q{-count=1}, q{-timeout}, q{10m}, system(@fence, q{go}, q{test}, q{-count=1}, q{-timeout}, q{0},
q{./cmd/...}, q{./debug/...}) == 0 q{./cmd/...}, q{./debug/...}) == 0
or die qq{the tests outside the coverage set failed\n}; or die qq{the tests outside the coverage set failed\n};
open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!}; open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!};
@@ -47,28 +59,25 @@ test:
printf qq{Total coverage: %s%%\n}, $total; printf qq{Total coverage: %s%%\n}, $total;
exit($total < 80 ? 1 : 0); exit($total < 80 ? 1 : 0);
# The same suite under the race detector. The expensive one. # The same suite under the race detector. The expensive one, still fenced.
race: race:
# Ceiling 16G, not the default 4G: with every package in parallel the race systemd-run --user --scope -p MemoryMax={{racememlimit}} -p MemorySwapMax=0 go test -race -count=1 -timeout 0 {{packages}}
# footprint peaks over 8G (8G died in the parser package, 16G passed,
# measured 2026-10-02), and the wide machine at the keyboard can carry it.
systemd-run --user --scope -p MemoryMax=16G -p MemorySwapMax=0 go test -race -count=1 -timeout 10m {{packages}}
# Fast scoped run for iterating. This is the one that runs after every edit. # Fast scoped run for iterating. This is the one that runs after every edit.
unit pkgs=packages run=".*": unit pkgs=packages run=".*":
systemd-run --user --scope -p MemoryMax=4G -p MemorySwapMax=0 go test {{pkgs}} -run '{{run}}' systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 go test -timeout 0 {{pkgs}} -run '{{run}}'
# Time-boxed fuzz of one target in one package. The package is required; never a gate. # Time-boxed fuzz of one target in one package. The package is required; never a gate.
fuzz target pkg fuzztime="60s": fuzz target pkg fuzztime="60s":
systemd-run --user --scope -p MemoryMax=4G -p MemorySwapMax=0 go test -run '^$' -fuzz '{{target}}' -fuzztime={{fuzztime}} {{pkg}} systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 go test -timeout 0 -run '^$' -fuzz '{{target}}' -fuzztime={{fuzztime}} {{pkg}}
# The cmd/link GOOBJ parity gate, opt-in: the per-push pipeline cannot afford it. # The cmd/link GOOBJ parity gate, opt-in: it costs minutes no pipeline can afford.
link-parity: link-parity:
systemd-run --user --scope -p MemoryMax=4G -p MemorySwapMax=0 env GASM_LINK_PARITY=1 go test -count=1 -timeout 10m -run 'TestGOOBJLinkRegression' ./verify/ systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 env GASM_LINK_PARITY=1 go test -count=1 -timeout 0 -run 'TestGOOBJLinkRegression' ./verify/
# Benchmarks. On an idle machine only. # Benchmarks. On an idle machine only, and deliberately unfenced.
bench pkgs=packages: bench pkgs=packages:
go test -run '^$' -bench=. -benchmem -count=5 {{pkgs}} go test -timeout 0 -run '^$' -bench=. -benchmem -count=5 {{pkgs}}
# Format in place. # Format in place.
fmt: fmt: