ci: fence the test recipes and rebuild the pipelines around the gate set
Test / test (push) Failing after 24s
Test / test (push) Failing after 24s
Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
1 parent
e3dda5e115
commit
9a1b922712
10 files changed
+165
-263
No files matched your search
@@ -1,36 +0,0 @@
|
|||||||
# Fuzz smoke, Go. Dispatched by hand when a change asks for it.
|
|
||||||
#
|
|
||||||
# Fuzzing is exploration, so it never belongs to the push pipeline; a 30 second
|
|
||||||
# smoke per target on a hand dispatch checks a change without holding the
|
|
||||||
# shared box. The targets run the seeds and whatever the corpus has gathered; a
|
|
||||||
# failure leaves its crashing input in testdata/fuzz, which the ordinary suite
|
|
||||||
# then reproduces on every push.
|
|
||||||
#
|
|
||||||
# Every step is one command, so the step that fails is the gate that failed.
|
|
||||||
name: Fuzz
|
|
||||||
|
|
||||||
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:
|
|
||||||
fuzz:
|
|
||||||
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: Fuzz the parser
|
|
||||||
run: go test -run '^$' -fuzz FuzzParse -fuzztime=30s -timeout 10m .
|
|
||||||
|
|
||||||
- name: Fuzz the encoder
|
|
||||||
run: go test -run '^$' -fuzz FuzzMarshal -fuzztime=30s -timeout 10m .
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Race, Go. Dispatched by hand.
|
|
||||||
#
|
|
||||||
# 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 an explicit decision rather than a routine, a hand dispatch
|
|
||||||
# when a change asks for one. Development carries its race gate on every push through
|
|
||||||
# that local gate.
|
|
||||||
#
|
|
||||||
# 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: 45
|
|
||||||
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 ./...
|
|
||||||
@@ -1,49 +1,33 @@
|
|||||||
# Release, Go library. Runs on version tags (v1.2.3) pushed to main.
|
# Release, Go library. Runs on a version tag (v1.2.3) pushed to main.
|
||||||
#
|
#
|
||||||
# A library ships no binaries, so there is no build matrix and no smoke test: the release
|
# The pipeline does one thing: it publishes the release from the tag, notes and
|
||||||
# carries the CHANGELOG section as its body and nothing else. The gates still run first,
|
# all. No gate runs here. The tagged tree was tested on every push to
|
||||||
# in their own job and once, minus the race detector: race never runs on a push path or a
|
# development, the deep suite is the suite workflow's business, and race never
|
||||||
# tag, and the local gate raced this tree before the tag was cut. The write permission
|
# runs in CI at all. A library ships no assets, so there is no build and
|
||||||
# sits on the release job alone, and the version the binary reports is the one the
|
# nothing to upload.
|
||||||
# toolchain records from the tag, with nothing injected.
|
|
||||||
#
|
#
|
||||||
# Every step is one command, so the step that fails is the gate that failed, and no shell
|
# Every step is one command, and the scripted steps are Perl with builtins
|
||||||
# option has to be trusted for the run to stop. The scripted steps are Perl, not shell and
|
# only. Perl drives curl through a list, so no argument is ever word-split,
|
||||||
# not Python: Perl behaves the same on both runner images, there is no bashism to trip over
|
# globbed or quoted wrong.
|
||||||
# on ash, and it is one language instead of two. The Perl uses builtins only, because
|
|
||||||
# Fedora packages the Perl modules separately and nothing beyond `perl` itself may be
|
|
||||||
# assumed present, which is why the release body is escaped by hand and curl is the
|
|
||||||
# transport.
|
|
||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ["v*"]
|
tags: ["v*"]
|
||||||
|
|
||||||
env:
|
|
||||||
# The box is shared with the forge, so parallelism is bounded on purpose. The gates job
|
|
||||||
# needs it most, since it runs the suite.
|
|
||||||
GOFLAGS: -p=1
|
|
||||||
GOMAXPROCS: "2"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
gates:
|
release:
|
||||||
runs-on: fedora
|
runs-on: fedora
|
||||||
timeout-minutes: 10
|
permissions:
|
||||||
|
# contents: read is required for the checkout: a job that declares any
|
||||||
|
# permissions gets a token scoped to exactly those, and releases: write
|
||||||
|
# alone leaves the fetch with no read access, which Gitea answers with
|
||||||
|
# a 404 "Repository not found". Verified on the instance 2026-09-16.
|
||||||
|
contents: read
|
||||||
|
releases: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- 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: 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
|
- name: Validate the tag
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ gitea.ref_name }}
|
VERSION: ${{ gitea.ref_name }}
|
||||||
@@ -55,68 +39,12 @@ jobs:
|
|||||||
print qq{tag $v\n};
|
print qq{tag $v\n};
|
||||||
'
|
'
|
||||||
|
|
||||||
- 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: Build
|
|
||||||
run: go build ./...
|
|
||||||
|
|
||||||
- name: Tests
|
|
||||||
# Keep the pattern equal to `packages` in the project's justfile.
|
|
||||||
run: go test -count=1 -timeout 10m -coverprofile=coverage.out ./...
|
|
||||||
|
|
||||||
- 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);
|
|
||||||
'
|
|
||||||
|
|
||||||
release:
|
|
||||||
runs-on: fedora
|
|
||||||
timeout-minutes: 15
|
|
||||||
needs: gates
|
|
||||||
permissions:
|
|
||||||
# contents: read is required for the checkout: a job that declares any
|
|
||||||
# permissions gets a token scoped to exactly those, and releases: write
|
|
||||||
# alone leaves the fetch with no read access, which Gitea answers with
|
|
||||||
# a 404 "Repository not found". Verified on the instance 2026-09-16.
|
|
||||||
contents: read
|
|
||||||
releases: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- name: Install Perl
|
|
||||||
# The runner images are minimal and Perl is not guaranteed. The install is a
|
|
||||||
# no-op where it is already present; drop this step once verified on the box.
|
|
||||||
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 }}
|
||||||
run: |
|
run: |
|
||||||
# Each step derives what it needs from the tag, so no value has to travel between
|
# Each step derives what it needs from the tag, so no value has to travel
|
||||||
# jobs. The separator after the version is never read, so the heading format's
|
# between steps.
|
||||||
# separator stays free.
|
|
||||||
perl -e '
|
perl -e '
|
||||||
my $v = $ENV{VERSION} // q{};
|
my $v = $ENV{VERSION} // q{};
|
||||||
$v =~ s{^v}{};
|
$v =~ s{^v}{};
|
||||||
@@ -150,8 +78,8 @@ jobs:
|
|||||||
open(my $in, q{<:raw}, q{release-body.md}) or die qq{release-body.md: $!};
|
open(my $in, q{<:raw}, q{release-body.md}) or die qq{release-body.md: $!};
|
||||||
my $body = do { local $/; <$in> };
|
my $body = do { local $/; <$in> };
|
||||||
close($in);
|
close($in);
|
||||||
# Byte-oriented escaping: JSON is UTF-8, so non-ASCII passes through and only the
|
# Byte-oriented escaping: JSON is UTF-8, so non-ASCII passes through and
|
||||||
# characters JSON forbids are rewritten.
|
# only the characters JSON forbids are rewritten.
|
||||||
$body =~ s/([\\"])/\\$1/g;
|
$body =~ s/([\\"])/\\$1/g;
|
||||||
$body =~ s/\t/\\t/g;
|
$body =~ s/\t/\\t/g;
|
||||||
$body =~ s/\r//g;
|
$body =~ s/\r//g;
|
||||||
@@ -188,5 +116,5 @@ jobs:
|
|||||||
close($r);
|
close($r);
|
||||||
$code eq q{201} or die qq{ERROR: the release was not created, HTTP $code: $body\n};
|
$code eq q{201} or die qq{ERROR: the release was not created, HTTP $code: $body\n};
|
||||||
$body =~ m{"id"\s*:\s*([0-9]+)} or die qq{ERROR: no release id in the response: $body\n};
|
$body =~ m{"id"\s*:\s*([0-9]+)} or die qq{ERROR: no release id in the response: $body\n};
|
||||||
print qq{release id $1\n};
|
print qq{release v$ENV{VERSION} is live (id $1)\n};
|
||||||
'
|
'
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 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 gate, 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 time and 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
|
||||||
|
# The examples are main programs; the build is what compiles them.
|
||||||
|
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, every skip layer 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 ./...
|
||||||
|
|
||||||
|
- 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
-54
@@ -1,23 +1,19 @@
|
|||||||
# 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
|
||||||
# runner box cannot afford the race detector on every push. Race has its own
|
# gates that fit it: the format check, `go vet`, the suite's short layer and the
|
||||||
# pipeline, dispatched by hand, and the local `just gates` runs it once per
|
# coverage floor. There is no build step (`go test` compiles what it runs) and
|
||||||
# task. The box is one core and 2 GB beside Gitea, so
|
# no install step: the fedora job image carries git, perl and node (verified on
|
||||||
# parallelism is bounded on purpose and everything runs in one job. Extra jobs would
|
# the runner, 2026-10-04), and no pipeline ever installs gcc or runs the race
|
||||||
# duplicate the checkout, the Go setup and the dependency download three times without
|
# detector. The modernisation gate (`go fix -diff`) and the full suite live in
|
||||||
# buying any parallelism.
|
# the suite workflow.
|
||||||
#
|
#
|
||||||
# Every step is one command, so the step that fails is the gate that failed, and no shell
|
# A test too slow for the push budget marks itself with `testing.Short` and the
|
||||||
# option has to be trusted for the run to stop. The scripted steps are Perl, not shell and
|
# suite workflow runs it; the push path runs what fits its budget.
|
||||||
# not Python: Perl behaves the same on both runner images, there is no bashism to trip over
|
|
||||||
# on ash, and it is one language instead of two. The Perl uses builtins only, because
|
|
||||||
# Fedora packages the Perl modules separately and nothing beyond `perl` itself may be
|
|
||||||
# assumed present.
|
|
||||||
#
|
#
|
||||||
# After the gates runs the toml-test compliance suite, which is this library's conformance
|
# Every step is one command, so the step that fails is the gate that failed,
|
||||||
# record rather than a gate. It is fixed cases, not exploration, and it fits the box, so it
|
# and no shell option has to be trusted for the run to stop. The scripted
|
||||||
# is none of the three burdens the push pipeline refuses: no fuzz, no race, no heavy sweep.
|
# steps are Perl with builtins only.
|
||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -31,10 +27,10 @@ env:
|
|||||||
GOFLAGS: -p=1
|
GOFLAGS: -p=1
|
||||||
GOMAXPROCS: "2"
|
GOMAXPROCS: "2"
|
||||||
|
|
||||||
# A superseded run of the same ref is cancelled instead of queueing behind one
|
# A superseded run of the same ref is cancelled instead of queueing behind one that
|
||||||
# that no longer matters. Verified on this Gitea on 2026-09-17: a queued run
|
# no longer matters. Verified on Gitea 1.27.1 on 2026-09-17: a queued run whose ref
|
||||||
# whose ref moved on is cancelled before it ever reaches the runner, while a
|
# moved on is cancelled before it ever reaches the runner, while a run already
|
||||||
# run already dispatched there runs to completion.
|
# dispatched there runs to completion.
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ gitea.workflow }}-${{ gitea.ref }}
|
group: ${{ gitea.workflow }}-${{ gitea.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -42,7 +38,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
|
||||||
|
|
||||||
@@ -52,11 +47,6 @@ jobs:
|
|||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Install Perl
|
|
||||||
# The runner images are minimal and Perl is not guaranteed. The install is a
|
|
||||||
# no-op where it is already present; drop this step once verified on the box.
|
|
||||||
run: dnf install -y perl
|
|
||||||
|
|
||||||
- name: Format
|
- name: Format
|
||||||
run: |
|
run: |
|
||||||
perl -e '
|
perl -e '
|
||||||
@@ -70,17 +60,14 @@ 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: Build
|
|
||||||
run: go build ./...
|
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
# Scope the pattern to the packages that hold the logic when a thin cmd/ drags the
|
# Equal to `packages` in the project's justfile, so the floor is the same
|
||||||
# total under the floor, and keep it equal to `packages` in the project's justfile.
|
# number the local gate reports. The short layer is what runs; the
|
||||||
run: go test -count=1 -timeout 10m -coverprofile=coverage.out ./...
|
# persistent build cache on the runner makes a warm run seconds, not
|
||||||
|
# minutes. No timeout anywhere: `-timeout 0` disables go test's own
|
||||||
|
# ten-minute default, because a run that does not finish is a defect to
|
||||||
|
# find and a timeout only hides it.
|
||||||
|
run: go test -short -count=1 -timeout 0 -coverprofile=coverage.out ./...
|
||||||
|
|
||||||
- name: Coverage floor
|
- name: Coverage floor
|
||||||
run: |
|
run: |
|
||||||
@@ -93,21 +80,3 @@ jobs:
|
|||||||
printf qq{Total coverage: %s%%\n}, $total;
|
printf qq{Total coverage: %s%%\n}, $total;
|
||||||
exit($total < 80 ? 1 : 0);
|
exit($total < 80 ? 1 : 0);
|
||||||
'
|
'
|
||||||
|
|
||||||
- name: Install toml-test
|
|
||||||
# A dev and CI tool only; it is not a module dependency of interpres. GOBIN pins
|
|
||||||
# the destination, so the later step invokes the tool by path and no command
|
|
||||||
# output has to be captured into a variable.
|
|
||||||
env:
|
|
||||||
GOBIN: ${{ gitea.workspace }}/bin
|
|
||||||
run: go install github.com/toml-lang/toml-test/v2/cmd/toml-test@v2.2.0
|
|
||||||
|
|
||||||
- name: Build the decoder
|
|
||||||
run: go build -o bin/interpres-decode ./cmd/interpres-decode
|
|
||||||
|
|
||||||
- name: Compliance suite
|
|
||||||
# interpres implements TOML 1.1, and the suite runs both directions: the decoder
|
|
||||||
# on the valid and invalid corpora, the encoder on the tagged JSON of the valid
|
|
||||||
# one. The mode is pinned so an upstream default change cannot silently move the
|
|
||||||
# corpus.
|
|
||||||
run: bin/toml-test test -decoder=bin/interpres-decode -encoder='bin/interpres-decode -encode' -toml=1.1
|
|
||||||
+10
-11
@@ -53,11 +53,10 @@ just test
|
|||||||
7. Open a pull request against `development`.
|
7. Open a pull request against `development`.
|
||||||
|
|
||||||
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`. The
|
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`. The
|
||||||
release workflow validates the tag, runs the static gates and the test suite
|
release workflow validates the tag and publishes the Gitea release with the
|
||||||
with the coverage floor, and publishes the Gitea release with the matching
|
matching `CHANGELOG.md` section as its notes. No gate runs at the tag: the
|
||||||
`CHANGELOG.md` section as its notes. The race detector is not in that set: race
|
tree was tested on every push to `development`, and race is local to
|
||||||
never runs on a push path, and the local `just gates` raced the tree before the
|
`just gates`, which raced the tree before the tag was cut.
|
||||||
tag was cut.
|
|
||||||
|
|
||||||
## Code style
|
## Code style
|
||||||
|
|
||||||
@@ -114,13 +113,13 @@ 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` | format check, vet, modernisation, build, the test suite with the coverage floor, the toml-test compliance suite |
|
| Test | push or pull request to `development` | format check, vet, and the suite's short layer with the coverage floor, inside the two-minute push budget |
|
||||||
| Race | `workflow_dispatch`, by hand | the suite under the race detector, the same race gate the local `just gates` runs |
|
| Suite | `workflow_dispatch`, by hand | the complete gate set minus race: build, format, vet, modernisation, the full suite and the coverage floor |
|
||||||
| Fuzz | `workflow_dispatch`, by hand | a 30 second fuzz smoke per target over the seeds and the gathered corpus |
|
| Release | a `v*` tag | tag validation, then the Gitea release created from the `CHANGELOG.md` section and nothing else; no gate runs at the tag |
|
||||||
| Release | a `v*` tag | tag validation, format, vet, modernisation, build and the test suite with the coverage floor, then the Gitea release created from the `CHANGELOG.md` section; no race detector |
|
|
||||||
|
|
||||||
The local equivalent is `just gates`, which is the same set plus the race
|
The local equivalent is `just gates`, which is the whole set including race.
|
||||||
detector.
|
Race never runs in CI: it belongs to the machine at the keyboard, once per
|
||||||
|
task, before the commit.
|
||||||
|
|
||||||
## Reporting bugs
|
## Reporting bugs
|
||||||
|
|
||||||
|
|||||||
@@ -138,5 +138,5 @@ sequenceDiagram
|
|||||||
|
|
||||||
None. `go.mod` declares the module and the Go version and carries no requires;
|
None. `go.mod` declares the module and the Go version and carries no requires;
|
||||||
the library imports the standard library only, which is the point of the
|
the library imports the standard library only, which is the point of the
|
||||||
project. The `toml-test` binary is a development and CI tool, never a module
|
project. The `toml-test` binary is a development tool, never a module
|
||||||
dependency.
|
dependency.
|
||||||
+3
-3
@@ -59,9 +59,9 @@ option printed as a comment above it, and the `default=` option as the value
|
|||||||
where one is set. It is the inverse of `--struct`, for config-driven
|
where one is set. It is the inverse of `--struct`, for config-driven
|
||||||
applications that generate their example configuration from the type.
|
applications that generate their example configuration from the type.
|
||||||
|
|
||||||
`--version` prints the binary's version and exits. The release pipeline builds
|
`--version` prints the binary's version and exits. The version is the one the
|
||||||
at the tag, so a released binary prints its own tag; a build from a working
|
toolchain records at build time, so a binary built from a tagged tree prints
|
||||||
tree prints `(devel)`.
|
its tag and a build from a working tree prints `(devel)`.
|
||||||
|
|
||||||
## Flags
|
## Flags
|
||||||
|
|
||||||
|
|||||||
+20
-17
@@ -29,11 +29,11 @@ prints the same list.
|
|||||||
| Recipe | What it does |
|
| Recipe | What it does |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `just gates` | the definition of done: build, format check, vet, the test suite with the coverage floor, and the race detector |
|
| `just gates` | the definition of done: build, format check, vet, the test suite with the coverage floor, and the race detector |
|
||||||
| `just build` | compiles `./cmd/interpres-decode` into `bin/interpres-decode` |
|
| `just build` | compiles `./cmd/interpres-decode` into `bin/interpres-decode`, with `-trimpath -buildvcs=true` |
|
||||||
| `just test` | the suite with no cache, then the coverage floor of 80 percent from `coverage.out` |
|
| `just test` | the suite with no cache, then the coverage floor of 80 percent from `coverage.out`, under the memory fence |
|
||||||
| `just race` | the same suite under the race detector |
|
| `just race` | the same suite under the race detector, fenced too |
|
||||||
| `just unit ./... TestName` | a fast scoped run for iterating; the second argument is a `-run` pattern, `.*` by default |
|
| `just unit ./... TestName` | a fast scoped run for iterating; the second argument is a `-run` pattern, `.*` by default, fenced |
|
||||||
| `just fuzz FuzzParse . 30s` | time-boxed fuzzing of one target in exactly one package; `go test -fuzz` rejects `./...`; never a gate |
|
| `just fuzz FuzzParse . 30s` | time-boxed fuzzing of one target in exactly one package; `go test -fuzz` rejects `./...`; never a gate, fenced |
|
||||||
| `just bench` | benchmarks, `-benchmem -count=5`, on an idle machine only |
|
| `just bench` | benchmarks, `-benchmem -count=5`, on an idle machine only |
|
||||||
| `just fmt` | `gofmt -w .`, format in place |
|
| `just fmt` | `gofmt -w .`, format in place |
|
||||||
| `just fmt-check` | `gofmt -l .`, zero diff |
|
| `just fmt-check` | `gofmt -l .`, zero diff |
|
||||||
@@ -94,22 +94,25 @@ go build -gcflags='-S' ./... # what the compiler generated
|
|||||||
## 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.
|
||||||
They are written by hand rather than through `just`, but they enforce the same
|
They are written by hand rather than through `just`, but between them they
|
||||||
set of gates, so a green `just gates` locally is the fastest way to a green
|
carry the same set of gates, so a green `just gates` locally is the fastest
|
||||||
pipeline.
|
way to a green pipeline.
|
||||||
|
|
||||||
| Workflow | Trigger | What it does |
|
| Workflow | Trigger | What it does |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `test.yml` | push or pull request to `development` | format check, vet, modernisation, build, the test suite with the 80 percent coverage floor, then the toml-test compliance suite |
|
| `test.yml` | push or pull request to `development` | format check, vet, and the suite's short layer with the 80 percent coverage floor, inside the two-minute push budget |
|
||||||
| `race.yml` | `workflow_dispatch`, by hand | the suite under the race detector; the same race gate `just gates` runs locally |
|
| `suite.yml` | `workflow_dispatch`, by hand | the complete gate set minus race: build, format, vet, modernisation, the full suite and the coverage floor |
|
||||||
| `fuzz.yml` | `workflow_dispatch`, by hand | a 30 second fuzz smoke per target over the seeds and the gathered corpus |
|
| `release.yml` | a `v*` tag | tag validation, then the Gitea release from the CHANGELOG section and nothing else. No gate runs at the tag: the tree was tested on every push, and a library ships no assets |
|
||||||
| `release.yml` | a `v*` tag | tag validation, then format, vet, modernisation, build and the test suite with the coverage floor, then the Gitea release from the CHANGELOG section. No race detector: race never runs on a push path, and the local `just gates` raced the tree before the tag was cut |
|
|
||||||
|
Race never runs in CI: it belongs to the local `just gates`, which races the
|
||||||
|
tree on the machine at the keyboard before the commit. The toml-test
|
||||||
|
compliance suite is a local recipe (`just toml-test`) and a development record
|
||||||
|
rather than a push gate.
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`. The
|
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`. The
|
||||||
tag drives the release workflow: it validates the tag, runs the static gates
|
tag drives the release workflow: it validates the tag, extracts the matching
|
||||||
and the test suite with the coverage floor, extracts the matching `## [X.Y.Z]`
|
`## [X.Y.Z]` section from `CHANGELOG.md`, and publishes the release with that
|
||||||
section from `CHANGELOG.md`, and publishes the release with that section as its
|
section as its body. No gate runs at the tag; a library ships no binaries, so
|
||||||
body. A library ships no binaries, so the release carries the notes and nothing
|
the release carries the notes and nothing else.
|
||||||
else.
|
|
||||||
@@ -11,19 +11,26 @@ package := "./cmd/interpres-decode"
|
|||||||
# failure, not an empty run.
|
# failure, not an empty run.
|
||||||
packages := "./..."
|
packages := "./..."
|
||||||
|
|
||||||
|
# The memory fence for the test recipes: a cgroup ceiling with swap off, so a
|
||||||
|
# runaway run dies as a failed run and never eats the machine. 4G is the
|
||||||
|
# default; raise it only with a reason recorded here.
|
||||||
|
memlimit := "4G"
|
||||||
|
|
||||||
bindir := env_var_or_default("BINDIR", env_var("HOME") / ".local" / "bin")
|
bindir := env_var_or_default("BINDIR", env_var("HOME") / ".local" / "bin")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@just --list
|
@just --list
|
||||||
|
|
||||||
# Compile. Zero errors, zero warnings.
|
# Compile. Zero errors, zero warnings; -trimpath and -buildvcs 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 suite, no cache, the coverage floor, under the memory fence.
|
||||||
test:
|
test:
|
||||||
#!/usr/bin/env perl
|
#!/usr/bin/env perl
|
||||||
system(q{go}, q{test}, q{-count=1}, q{-timeout}, q{30m},
|
my @fence = (q{systemd-run}, q{--user}, q{--scope},
|
||||||
|
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};
|
||||||
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: $!};
|
||||||
@@ -34,21 +41,21 @@ 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:
|
||||||
go test -race -count=1 -timeout 30m {{packages}}
|
systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 go test -race -count=1 -timeout 0 {{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=".*":
|
||||||
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":
|
||||||
go test -run '^$' -fuzz '{{target}}' -fuzztime={{fuzztime}} {{pkg}}
|
systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 go test -run '^$' -timeout 0 -fuzz '{{target}}' -fuzztime={{fuzztime}} {{pkg}}
|
||||||
|
|
||||||
# Benchmarks. On an idle machine only.
|
# Benchmarks. On an idle machine only, deliberately unfenced: a ceiling would distort the measurement.
|
||||||
bench pkgs=packages:
|
bench pkgs=packages:
|
||||||
go test -run '^$' -bench=. -benchmem -count=5 {{pkgs}}
|
go test -run '^$' -timeout 0 -bench=. -benchmem -count=5 {{pkgs}}
|
||||||
|
|
||||||
# Format in place.
|
# Format in place.
|
||||||
fmt:
|
fmt:
|
||||||
|
|||||||
Reference in new issue
Block a user