From f6a96379e69d6308315404d206766f14ccc1ecd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Tue, 22 Sep 2026 01:26:48 +0200 Subject: [PATCH] ci: schedule nightly race and fuzz, pin actions, add release-check and docs drift Assisted-by: GLM 5.3 Flash --- .gitea/workflows/fuzz.yml | 40 ++++++++++++++++++++++++++++++++++++ .gitea/workflows/race.yml | 14 +++++++++---- .gitea/workflows/release.yml | 6 +++--- .gitea/workflows/test.yml | 4 ++-- justfile | 28 ++++++++++++++++++++++++- scripts/docs-drift.pl | 34 ++++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 .gitea/workflows/fuzz.yml create mode 100755 scripts/docs-drift.pl diff --git a/.gitea/workflows/fuzz.yml b/.gitea/workflows/fuzz.yml new file mode 100644 index 0000000..5042b81 --- /dev/null +++ b/.gitea/workflows/fuzz.yml @@ -0,0 +1,40 @@ +# Fuzz smoke, Go. A nightly time-boxed run of the fuzz targets, and a hand +# dispatch when a change asks for it. +# +# Fuzzing is exploration, so it never belongs to the push pipeline; a 30 second +# smoke per target, run nightly, is the compromise that catches a new crash +# within a day 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: + schedule: + # Nightly at 03:30 UTC, after the race sweep has had the box first. + - cron: "30 3 * * *" + +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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # 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 . diff --git a/.gitea/workflows/race.yml b/.gitea/workflows/race.yml index d8bb2c5..bd6e96f 100644 --- a/.gitea/workflows/race.yml +++ b/.gitea/workflows/race.yml @@ -1,14 +1,20 @@ -# Race, Go. Dispatched by hand, and run as part of the release gates. +# Race, Go. Dispatched by hand, run nightly on a schedule, and run as part of +# the release gates. # # 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. +# task; here it is an explicit decision rather than a routine. The schedule is the +# nightly sweep: a failing race on development is known by morning without anyone +# remembering to dispatch it. # # Every step is one command, so the step that fails is the gate that failed. name: Race on: workflow_dispatch: + schedule: + # Nightly at 03:00 UTC, the quietest hours of the shared box. + - cron: "0 3 * * *" env: # One core: parallelism buys no speed here and costs memory the box does not have. @@ -20,9 +26,9 @@ jobs: runs-on: fedora timeout-minutes: 45 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: actions/setup-go@v6 + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: go-version-file: go.mod cache: true diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index be2c7f9..c4e5934 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -31,9 +31,9 @@ jobs: runs-on: fedora timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: actions/setup-go@v6 + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: # The module is the source of truth for the version, so it cannot drift. go-version-file: go.mod @@ -103,7 +103,7 @@ jobs: contents: read releases: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Install Perl # The runner images are minimal and Perl is not guaranteed. The install is a diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 888447c..ea80b6f 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -43,9 +43,9 @@ jobs: runs-on: fedora timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: actions/setup-go@v6 + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 with: # The module is the source of truth for the version, so it cannot drift. go-version-file: go.mod diff --git a/justfile b/justfile index 931a442..1506a29 100644 --- a/justfile +++ b/justfile @@ -100,11 +100,13 @@ toml-test: build coverage-html: test go tool cover -html=coverage.out -o coverage.html -# Cross-compile smoke: the library and the command build for the foreign architectures; not a gate, it is a nightly convenience and runs std-lib only. +# Cross-compile smoke: the library and the command build for the foreign architectures and the browser and edge runtimes; not a gate, it is a nightly convenience and runs std-lib only. cross: GOARCH=arm64 go build ./... GOARCH=loong64 go build ./... GOARCH=riscv64 go build ./... + GOOS=js GOARCH=wasm go build ./... + GOOS=wasip1 GOARCH=wasm go build ./... GOARCH=arm64 CGO_ENABLED=0 go build -o /dev/null {{package}} GOARCH=loong64 CGO_ENABLED=0 go build -o /dev/null {{package}} GOARCH=riscv64 CGO_ENABLED=0 go build -o /dev/null {{package}} @@ -112,3 +114,27 @@ cross: # Runs the example program under examples/basic; not standard because `run` runs the adapter, and an example is documentation, not the product. example: go run ./examples/basic + +# The release pre-flight from the release skill, in one command: the branch, a clean tree, a sync with origin, the gates, and a CHANGELOG section ready to release. Not a gate, it is the checklist before a release may even be discussed. +release-check version: + #!/usr/bin/env perl + my ($version) = @ARGV; + $version =~ m{\Av?\d+\.\d+\.\d+\z} or die qq{usage: just release-check X.Y.Z\n}; + my $branch = qx{git rev-parse --abbrev-ref HEAD}; + chomp $branch; + $branch eq q{development} or die qq{release-check: on '$branch', cut releases from development\n}; + my $dirty = qx{git status --porcelain}; + $dirty eq q{} or die qq{release-check: the working tree is dirty\n}; + system(qw{git fetch origin}) == 0 or die qq{release-check: git fetch failed\n}; + my $local = qx{git rev-parse development}; + my $remote = qx{git rev-parse origin/development}; + $local eq $remote or die qq{release-check: development is out of sync with origin\n}; + my $changelog = do { open(my $fh, q{<}, q{CHANGELOG.md}) or die qq{release-check: cannot read CHANGELOG.md: $!\n}; local $/; <$fh> }; + $changelog =~ m{## \[development\]\n\n### \w+} or die qq{release-check: the [development] section of CHANGELOG.md is missing or empty\n}; + print qq{branch, tree, sync and changelog verified; running the gates\n}; + system(qw{just gates}) == 0 or die qq{release-check: the gates failed\n}; + print qq{release-check: ready to release $version\n}; + +# Compares the toml-test counts the documentation names with the live suite run; not standard, it exists because a corpus change used to be corrected by hand. +docs-drift: + perl scripts/docs-drift.pl diff --git a/scripts/docs-drift.pl b/scripts/docs-drift.pl new file mode 100755 index 0000000..87a224c --- /dev/null +++ b/scripts/docs-drift.pl @@ -0,0 +1,34 @@ +#!/usr/bin/env perl +# docs-drift compares the toml-test suite counts the documentation names with +# the run this repository produces now. A corpus change moves the counts, and +# README.md and docs/ARCHITECTURE.md quote them; this is the check that keeps +# the quotation honest. Builtins only, and the toml-test binary on PATH. + +use v5.40; + +my $out = qx{toml-test test -decoder=bin/interpres-decode -encoder='bin/interpres-decode -encode' -toml=1.1 2>&1}; +die "docs-drift: toml-test failed to run; build the adapter first (just build)\n" + if !defined $out || $out =~ /not found|No such file/; + +my ($valid) = $out =~ /valid tests:\s+(\d+) passed/; +my ($invalid) = $out =~ /invalid tests:\s+(\d+) passed/; +die "docs-drift: could not read the suite counts from the toml-test output\n" + unless defined $valid && defined $invalid; +print "docs-drift: the suite now stands at $valid valid and $invalid invalid cases\n"; + +my $drift = 0; +for my $file ('README.md', 'docs/ARCHITECTURE.md') { + open(my $fh, '<', $file) or die "docs-drift: cannot read $file: $!\n"; + my $text = do { local $/; <$fh> }; + close($fh); + while ($text =~ /(\d+)\s+(valid|invalid)/g) { + my ($quoted, $kind) = ($1, $2); + my $live = $kind eq 'valid' ? $valid : $invalid; + if ($quoted != $live) { + print "docs-drift: $file quotes $quoted $kind cases, the suite says $live\n"; + $drift = 1; + } + } +} +die "docs-drift: the documentation has drifted from the suite\n" if $drift; +print "docs-drift: the documentation matches the suite\n";