ci: run the affordable gate set on push and publish at the tag
Test / test (push) Successful in 1m45s
Test / test (push) Successful in 1m45s
Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 }}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user