preset: filename-handling, and names nobody can read shown as escapes #501
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| # Only main, because every other branch reaches this through a pull request | |
| # and would otherwise run the whole suite twice for one change. Measured on | |
| # pull request 4, the first one this project had: every job appeared twice, | |
| # once for the branch push and once for the pull request, and the race | |
| # detector ran both times at about ten minutes each. | |
| # | |
| # The cost of the narrower trigger is that pushing a branch with no pull | |
| # request open gives no signal. Since 2026-08-27 main only takes pull | |
| # requests, so that state is a step on the way rather than a place work sits. | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| schedule: | |
| # Weekly, for the fuzzing job below only. Every other job here runs on | |
| # push, and fuzzing deliberately does not - see that job for why. | |
| - cron: "17 4 * * 1" | |
| permissions: | |
| contents: read | |
| env: | |
| # The exact toolchain used for tests and releases. go.mod declares a | |
| # minimum - this is the pin. Raising it can change generated bytes, so the | |
| # byte stability guard has to be green before it moves. | |
| GO_VERSION: "1.27.0" | |
| jobs: | |
| test: | |
| name: test on ${{ matrix.os }} | |
| # A hung job otherwise holds a runner until the GitHub default of six | |
| # hours. Remeasured 2026-09-03, because the sentence here said "the matrix | |
| # runs in about a minute" and had not been true for a long time: the test | |
| # step alone takes 399 s on ubuntu, 476 s on windows and 491 s on macOS. | |
| # The race detector took 148 s when it was measured and has its own job | |
| # and its own numbers now, and fuzzing is given 5 minutes a target by its | |
| # own loop. | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| runs-on: ${{ matrix.os }} | |
| env: | |
| # The engine, the command line and the tests build without CGO on every | |
| # system. Only the desktop window needs a C compiler, and it is built | |
| # separately. | |
| # | |
| # macOS is the exception and it is the toolkit's, not ours. Measured on | |
| # 2026-08-20, the first run this matrix had since the toolkit arrived: | |
| # with CGO off, vet cannot type check fyne's own internal/widget on | |
| # darwin - subscribeScrollerStyle and three others are undefined, because | |
| # the file defining them is behind a build tag that CGO off removes. | |
| # Nothing of ours is involved and there is nothing of ours to fix, so that | |
| # one runner pays for a C toolchain it already has. | |
| CGO_ENABLED: ${{ matrix.os == 'macos-latest' && '1' || '0' }} | |
| # The one guard that builds the window WITH cgo is read by the imports | |
| # job below instead, and this asks it to skip here. Measured 2026-09-17: | |
| # with CGO off, nothing else in this job compiles the OpenGL binding or | |
| # GLFW, so that guard was the run's one cold cgo build - 822 s for the | |
| # Windows test step against 434 s warm, four minutes under the timeout, | |
| # and cold again after every change to go.sum. The variable is named in | |
| # exactly one place, internal/guard/noimport_test.go, and a guard there | |
| # holds this job to setting it and the imports job to not setting it. | |
| TFG_IMPORT_TABLE_JOB: "1" | |
| steps: | |
| # Every checkout in these workflows turns the token off. The checkout | |
| # keeps the job's token in .git/config unless told not to, and the jobs | |
| # go on to run code from the pull request under test. Nothing after a | |
| # checkout here pushes or fetches - the release and the pages talk to | |
| # GitHub through gh and through actions that carry their own token - so | |
| # the credential has no use once the tree is on disk. A guard asks each | |
| # checkout (checkoutcredentials_test.go), so a new one cannot forget. | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| check-latest: false | |
| # Keyed on go.sum. The one dependency is the YAML parser behind the | |
| # recipe - see docs/STACK.md. | |
| cache: true | |
| - name: formatting | |
| run: | | |
| test -z "$(gofmt -l .)" || { echo "gofmt found unformatted files:"; gofmt -l .; exit 1; } | |
| shell: bash | |
| - name: vet | |
| run: go vet -tags "$(cat .github/build-tags)" ./... | |
| - name: the dependency list has not grown by accident | |
| # Every dependency is a licence question and a byte stability | |
| # question. A new one arriving as somebody's transitive import has | |
| # to be visible rather than discovered later. | |
| # | |
| # Two questions rather than one, since the graphics toolkit arrived | |
| # on 2026-08-05. The graph below is 65 modules and most of them are | |
| # Fyne's. Their licences were measured before it was added and every | |
| # one in the linked set is permissive and one way compatible with | |
| # GPL-3.0: 13 BSD-3, 9 MIT, 1 Apache-2.0, 1 BSD-2, 1 ISC. | |
| # | |
| # Changed once since, on 2026-08-05, when the window gained a | |
| # folder picker. github.com/FyshOS/fancyfs was already named here | |
| # because the toolkit requires it - importing the dialog package | |
| # moved it from named to downloaded, checksummed and compiled in. | |
| # Checked before it was accepted: BSD-3, 129 lines, written by the | |
| # toolkit's own author, and reached from one line of the dialog | |
| # package where it decorates folder icons. Tidying after it pruned | |
| # github.com/kr/text from go.mod, which took github.com/creack/pty | |
| # out of the graph, taking the count from 69 to 68. | |
| # | |
| # Changed again on 2026-08-27, when the toolkit went to 2.8.1. Three | |
| # modules left the graph and none of them was ours to begin with - | |
| # github.com/felixge/fgprof, github.com/google/pprof and | |
| # github.com/pkg/profile all arrived through Fyne's own tooling and | |
| # that release no longer asks for them, taking the count from 68 to | |
| # 65. A fourth, github.com/fredbi/uri, stayed in the graph and left | |
| # the linked set instead: 2.8.1 parses addresses itself. That is the | |
| # difference these two questions exist to keep apart, so it is worth | |
| # saying which one moved - the notices file tracks the linked set and | |
| # dropped it, this list tracks the graph and keeps it. | |
| # | |
| # Changed on 2026-08-31 by a Dependabot bump, and it is the exact case | |
| # the second paragraph of .github/dependabot.yml warns about. Taking | |
| # github.com/nicksnyder/go-i18n/v2 from 2.5.1 to 2.6.1 pulled a module | |
| # nobody asked for into the graph: go.yaml.in/yaml/v3, which is the | |
| # same yaml library as gopkg.in/yaml.v3 under the module path that | |
| # project moved to. Both are in the graph now, because other modules | |
| # still ask for the old path. | |
| # | |
| # Its licence was read from the pinned version rather than off a web | |
| # page: LICENSE is MIT and Apache-2.0 together - the files ported from | |
| # libyaml keep the MIT of the original C, the rest is Apache-2.0 with a | |
| # NOTICE from Canonical - and both are one way compatible with GPL-3.0. | |
| # It is in the GRAPH and linked into NEITHER binary, which is the | |
| # distinction these two questions exist to keep apart, so it belongs | |
| # here and not in the notices. The count went 67 to 68. | |
| # | |
| # Changed on 2026-08-29 when AVIF arrived, and again on 2026-08-31 | |
| # when JPEG XL did: github.com/gen2brain/gav1d and then | |
| # github.com/gen2brain/jxl, both picture encoders written in Go with | |
| # empty go.mod files of their own, so neither brought anything else | |
| # into the graph. Both licences were read out of the pinned module | |
| # rather than off a web page - BSD-2-Clause with an AOM patent grant, | |
| # and BSD-3-Clause with a Google patent grant - and both are one way | |
| # compatible with GPL-3.0. The count went 65 to 66 to 67. | |
| # | |
| # Worth saying plainly, because it is the failure this comment exists | |
| # to prevent: the sentence above went stale on 2026-08-29 and nobody | |
| # noticed. It still said 65 while the list held 66, because AVIF | |
| # updated the list and not the prose. The list is checked by the job | |
| # below and the prose is checked by nobody. | |
| # | |
| # Unchanged on 2026-09-17, when github.com/go-gl/gl gained a replace | |
| # directive pointing at a copy under third_party, and that is worth | |
| # saying because it looks like the kind of change this list exists to | |
| # notice. go list still reports the module under its own path, so | |
| # the list below is the same - what moved is where the bytes come | |
| # from, and two guards hold that instead: one keeps the copy equal to | |
| # the published version plus exactly the one patch its PATCH.md | |
| # describes, and one reads the built window binary's import table. | |
| # go.sum no longer carries the module's sum, because a replaced | |
| # module has none there. The sum lives in that PATCH.md now. | |
| # | |
| # The second question protects what ships to most people. The | |
| # command line binary links exactly four external modules and the | |
| # toolkit is not among them, so a build for a server carries no | |
| # window, no OpenGL and - see internal/guard - no socket. | |
| run: | | |
| set -euo pipefail | |
| # Built with printf rather than written across several lines. A | |
| # continuation starting in column one reads as the end of the jobs | |
| # block to anything scanning this file by indentation, and the | |
| # guard on job timeouts does exactly that - it counted one job | |
| # instead of seven and went green whatever it was given. | |
| expected=$(printf '%s\n' \ | |
| fyne.io/fyne/v2 \ | |
| fyne.io/systray \ | |
| github.com/BurntSushi/toml \ | |
| github.com/FyshOS/fancyfs \ | |
| github.com/akavel/rsrc \ | |
| github.com/anthonynsimon/bild \ | |
| github.com/clipperhouse/uax29/v2 \ | |
| github.com/cpuguy83/go-md2man/v2 \ | |
| github.com/davecgh/go-spew \ | |
| github.com/fogleman/gg \ | |
| github.com/fredbi/uri \ | |
| github.com/fsnotify/fsnotify \ | |
| github.com/fyne-io/gl-js \ | |
| github.com/fyne-io/glfw-js \ | |
| github.com/fyne-io/image \ | |
| github.com/fyne-io/oksvg \ | |
| github.com/gen2brain/gav1d \ | |
| github.com/gen2brain/jxl \ | |
| github.com/go-gl/gl \ | |
| github.com/go-gl/glfw/v3.4/glfw \ | |
| github.com/go-ole/go-ole \ | |
| github.com/go-text/render \ | |
| github.com/go-text/typesetting \ | |
| github.com/go-text/typesetting-utils \ | |
| github.com/goccy/go-yaml \ | |
| github.com/godbus/dbus/v5 \ | |
| github.com/golang/freetype \ | |
| github.com/hack-pad/go-indexeddb \ | |
| github.com/hack-pad/safejs \ | |
| github.com/inconshreveable/mousetrap \ | |
| github.com/jackmordaunt/icns/v2 \ | |
| github.com/jeandeaual/go-locale \ | |
| github.com/josephspurrier/goversioninfo \ | |
| github.com/jsummers/gobmp \ | |
| github.com/kr/text \ | |
| github.com/lucor/goinfo \ | |
| github.com/mattn/go-runewidth \ | |
| github.com/mcuadros/go-version \ | |
| github.com/natefinch/atomic \ | |
| github.com/nfnt/resize \ | |
| github.com/nicksnyder/go-i18n/v2 \ | |
| github.com/niemeyer/pretty \ | |
| github.com/pmezard/go-difflib \ | |
| github.com/russross/blackfriday/v2 \ | |
| github.com/rymdport/portal \ | |
| github.com/spf13/cobra \ | |
| github.com/spf13/pflag \ | |
| github.com/srwiley/oksvg \ | |
| github.com/srwiley/rasterx \ | |
| github.com/stretchr/objx \ | |
| github.com/stretchr/testify \ | |
| github.com/urfave/cli/v2 \ | |
| github.com/xrash/smetrics \ | |
| github.com/yuin/goldmark \ | |
| go.yaml.in/yaml/v3 \ | |
| golang.org/x/crypto \ | |
| golang.org/x/image \ | |
| golang.org/x/mobile \ | |
| golang.org/x/mod \ | |
| golang.org/x/net \ | |
| golang.org/x/sync \ | |
| golang.org/x/sys \ | |
| golang.org/x/term \ | |
| golang.org/x/text \ | |
| golang.org/x/tools \ | |
| golang.org/x/tools/go/vcs \ | |
| gopkg.in/check.v1 \ | |
| gopkg.in/yaml.v3) | |
| # LC_ALL=C on every sort here, and on the written list too. Measured on | |
| # 2026-08-20, the first run this job ever had on macOS: both lists | |
| # held the same 68 modules and the comparison still failed, because | |
| # BSD sort and GNU sort disagree about where a hyphen goes. The check | |
| # is a string comparison, so an ordering difference reads exactly like | |
| # a dependency appearing out of nowhere. | |
| expected=$(printf '%s\n' "$expected" | LC_ALL=C sort) | |
| actual=$(go list -m -f '{{.Path}}' all | grep -v '^github.com/donislawdev/TestingFilesGenerator$' | LC_ALL=C sort) | |
| if [ "$actual" != "$expected" ]; then | |
| echo "the module list changed." | |
| echo "expected: $expected" | |
| echo "actual : $actual" | |
| exit 1 | |
| fi | |
| echo "dependencies unchanged, $(echo "$expected" | wc -l) modules" | |
| linked=$(go list -deps -tags "$(cat .github/build-tags)" -f '{{if .Module}}{{.Module.Path}}{{end}}' ./cmd/tfg | LC_ALL=C sort -u | grep -v '^github.com/donislawdev/TestingFilesGenerator$' | grep .) | |
| wanted=$(printf '%s\n' github.com/gen2brain/gav1d github.com/gen2brain/jxl github.com/goccy/go-yaml golang.org/x/text) | |
| if [ "$linked" != "$wanted" ]; then | |
| echo "the command line binary links a different set of modules." | |
| echo "expected: $wanted" | |
| echo "actual : $linked" | |
| exit 1 | |
| fi | |
| echo "the command line binary links only: $linked" | |
| shell: bash | |
| - name: test | |
| # The timeout is stated for the reason the race job and the coverage | |
| # gate both state theirs: Go allows ten minutes PER PACKAGE by default, | |
| # this job allows twenty for all of it, and internal/guard is one | |
| # package holding almost every test there is. A run that went past the | |
| # first without approaching the second would die as a stack trace out | |
| # of whichever test happened to be running, which is what the coverage | |
| # gate did on 2026-09-03. | |
| # | |
| # Measured that day, on the run that caught it: 399 s on ubuntu, 476 s | |
| # on windows, 491 s on macOS. macOS therefore had 109 s of room under a | |
| # limit nobody had chosen, and the same fleet was measured swinging by | |
| # more than 25 percent between two runs of one branch. | |
| run: go test -tags "$(cat .github/build-tags)" ./... -count=1 -timeout 18m | |
| - name: build the command line binary | |
| run: go build -tags "$(cat .github/build-tags)" ./cmd/tfg | |
| - name: build the window binary | |
| # The toolkit arrived on 2026-08-05 and this step still passes, which | |
| # is the point of how it was wired in. Reaching the toolkit's app | |
| # package needs CGO and, on Linux, X11 development headers no runner | |
| # carries by default - so that one file sits behind a cgo build tag and | |
| # everything building a widget tree sits outside it. | |
| # | |
| # On Linux and Windows CGO_ENABLED is 0, so this proves the tree still | |
| # compiles where there is no compiler and no graphics. The tests go | |
| # further: they render a screen to an image and read it, also with CGO | |
| # off. | |
| # | |
| # On macOS it proves something else, and better - the windowed binary | |
| # actually LINKS. That runner has CGO on because the toolkit cannot be | |
| # type checked on darwin without it, so the build here is the real one. | |
| # Which closes half of the gap this comment used to name: linking was | |
| # done natively and nowhere but on the owner's machine. | |
| # | |
| # The flags below are Windows only. -H windowsgui names a Windows | |
| # subsystem, and handing it to the darwin linker is what turned this | |
| # step red on 2026-08-20 the moment macOS gained a C toolchain. | |
| # | |
| # The linker flags come from the file rather than from this line, so | |
| # that the release build and this one cannot drift apart. The file is | |
| # the single place they live, the same way the coverage threshold is. | |
| # Without them Windows attaches a console window to the program, which | |
| # it decides from one number in the PE header - and that number is what | |
| # the guard reads, out of a binary it builds rather than out of this | |
| # command. | |
| run: | | |
| set -euo pipefail | |
| if [ "$RUNNER_OS" = "Windows" ]; then | |
| go build -tags "$(cat .github/build-tags)" -ldflags="$(cat .github/gui-ldflags)" ./cmd/tfg-gui | |
| else | |
| go build -tags "$(cat .github/build-tags)" ./cmd/tfg-gui | |
| fi | |
| shell: bash | |
| imports: | |
| name: import table of the window binary | |
| # One guard, in a job of its own, because it is the one guard that builds | |
| # the window WITH cgo: TestTheWindowBinaryDoesNotImportOpenGLAtLoadTime | |
| # links tfg-gui.exe the way a release does and reads its import table, | |
| # which is how the software renderer's whole premise is held (O218). In | |
| # the test matrix that build was the run's only cold cgo build - measured | |
| # 2026-09-17, 822 s for the Windows test step against 434 s warm, four | |
| # minutes under the timeout, and cold again after every change to go.sum, | |
| # which is every Dependabot pull request. The matrix asks the guard to | |
| # skip there (TFG_IMPORT_TABLE_JOB), and this job runs it. | |
| # | |
| # The cache is this job's own. setup-go keys its cache on the hash of the | |
| # files named here (source of v7.0.0, cache-restore.ts), and the matrix | |
| # job on the same runner keys on go.sum alone. Under one key the matrix | |
| # would save first, without a single cgo object, and this job would pay | |
| # the cold build on every run. The second file is the sum of the patched | |
| # OpenGL binding, which is what the cgo build actually compiles - so the | |
| # key moves when either input to those objects does. | |
| # | |
| # Two things make the step fail rather than pass on nothing, and both were | |
| # measured before they were written: go test with a -run pattern that | |
| # matches no test exits 0 saying "no tests to run", and the guard skips | |
| # with exit 0 on a runner without gcc. So the log is read for the PASS | |
| # line of the one test this job exists for, under pipefail so a failing | |
| # go test is not hidden behind tee. The name in the pattern is held equal | |
| # to the function in internal/guard/noimport_test.go by a guard. | |
| runs-on: windows-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| check-latest: false | |
| cache: true | |
| cache-dependency-path: | | |
| go.sum | |
| third_party/go-gl-gl/PATCH.md | |
| - name: the window binary imports no opengl32.dll at load time | |
| run: | | |
| set -euo pipefail | |
| go test -tags "$(cat .github/build-tags)" ./internal/guard/ -count=1 -run '^TestTheWindowBinaryDoesNotImportOpenGLAtLoadTime$' -v -timeout 12m 2>&1 | tee import-table.log | |
| grep -q -- '--- PASS: TestTheWindowBinaryDoesNotImportOpenGLAtLoadTime' import-table.log | |
| shell: bash | |
| govulncheck: | |
| name: known vulnerabilities | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| check-latest: false | |
| cache: true | |
| - name: the headers the toolkit needs | |
| # The window reaches OpenGL through C, so anything that builds or | |
| # analyses the whole tree on Linux compiles GLFW - and no runner carries | |
| # its headers. Measured on 2026-08-20, the first run after the | |
| # repository went public and so the first one this job has ever had | |
| # against the toolkit: "wayland-client-core.h: No such file or | |
| # directory", and the job never reached our code at all. | |
| # | |
| # Taken from the toolkit's own CI rather than from a remembered list. | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends libgl1-mesa-dev libwayland-dev libx11-dev libxkbcommon-dev xorg-dev | |
| shell: bash | |
| - name: govulncheck | |
| # The official Go vulnerability scanner. BSD-3-Clause, read from the | |
| # LICENSE file of the pinned version rather than recalled. Run rather | |
| # than imported, so it never enters go.mod. | |
| # | |
| # It reports only what is actually reachable from our code, which is | |
| # what makes it worth having: a scanner that lists every advisory | |
| # touching the module graph produces noise, and noise gets switched off. | |
| # Measured before switching it on, 2026-08-02: no vulnerabilities found. | |
| # v1.8.0 since 2026-09-16, run on the tree before the pin moved: the same | |
| # verdict as v1.7.0, exit 0 under Go 1.27 - the two pins before this one | |
| # had stopped working with the compiler without anybody noticing. | |
| run: go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 -tags "$(cat .github/build-tags)" ./... | |
| staticcheck: | |
| name: staticcheck | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| check-latest: false | |
| cache: true | |
| - name: the headers the toolkit needs | |
| # The window reaches OpenGL through C, so anything that builds or | |
| # analyses the whole tree on Linux compiles GLFW - and no runner carries | |
| # its headers. Measured on 2026-08-20, the first run after the | |
| # repository went public and so the first one this job has ever had | |
| # against the toolkit: "wayland-client-core.h: No such file or | |
| # directory", and the job never reached our code at all. | |
| # | |
| # Taken from the toolkit's own CI rather than from a remembered list. | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends libgl1-mesa-dev libwayland-dev libx11-dev libxkbcommon-dev xorg-dev | |
| shell: bash | |
| - name: staticcheck | |
| # MIT, pinned. Run rather than imported, so it never enters go.mod and | |
| # the dependency gate above does not see it - measured twice on | |
| # 2026-08-02, go.mod and go.sum both untouched afterwards. | |
| # | |
| # The version is pinned because an unpinned analyser turns somebody | |
| # else's release into a red build on a commit that changed nothing. | |
| # | |
| # Which checks run, and why ST1005 does not, is in staticcheck.conf. | |
| # Measured before switching this on: two findings in the whole tree, | |
| # both of them the word "Pillow" at the start of an error string, which | |
| # is the name of the library that refused the image rather than a | |
| # sentence. Zero findings with the config in place. | |
| run: go run honnef.co/go/tools/cmd/staticcheck@v0.8.1 -tags "$(cat .github/build-tags)" ./... | |
| lint: | |
| name: linters | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| check-latest: false | |
| cache: true | |
| - name: the headers the toolkit needs | |
| # The same reason as in the staticcheck job above: anything that | |
| # analyses the whole tree on Linux compiles GLFW through C, and no | |
| # runner carries its headers. | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends libgl1-mesa-dev libwayland-dev libx11-dev libxkbcommon-dev xorg-dev | |
| shell: bash | |
| - name: the configuration is valid | |
| # Run separately and first, because a plain run ACCEPTS a configuration | |
| # this refuses. A key that does not exist, or a value outside what the | |
| # schema allows, is otherwise ignored in silence - and a linter reading | |
| # a setting nobody applied is a gate that reports what it feels like. | |
| run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2 config verify | |
| - name: golangci-lint | |
| # Run rather than imported, so it never enters go.mod and the | |
| # dependency gate above does not see it - the same arrangement as | |
| # staticcheck, measured there on 2026-08-02. | |
| # | |
| # The version is pinned because an unpinned analyser turns somebody | |
| # else's release into a red build on a commit that changed nothing. | |
| # | |
| # Which linters run, and the measurement behind each, is in | |
| # .golangci.yml. Measured before switching this on: zero findings from | |
| # ineffassign and one from misspell, which turned out to be a comment | |
| # written in the wrong language rather than a typo. | |
| run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2 run --build-tags "$(cat .github/build-tags)" ./... | |
| semgrep: | |
| name: semgrep | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.14" | |
| - name: the scanner, pinned | |
| # Pinned for the reason staticcheck and golangci-lint are pinned: an | |
| # unpinned analyser turns somebody else's release into a red build on a | |
| # commit that changed nothing. | |
| # | |
| # The version lives in .github/requirements-semgrep.txt rather than on | |
| # this line, and the reason is in that file: a version written into a | |
| # workflow step is watched by nothing, which is how staticcheck came to | |
| # be two releases behind a compiler it could not read. A requirements | |
| # file is an ecosystem Dependabot reads, so this one now arrives as a | |
| # pull request when it moves. | |
| # | |
| # The licence note and the reason for pinning moved there with it. | |
| run: pip install -r .github/requirements-semgrep.txt | |
| - name: scan | |
| # No account and no token. p/default is fetched anonymously from the | |
| # registry, used here, and never carried in this repository, because the | |
| # Semgrep Rules License allows the first and forbids the second. | |
| # | |
| # No --error and no --severity: the scan reports and the gate decides. | |
| # --severity knows INFO, WARNING and ERROR only, while registry rules | |
| # also carry HIGH and CRITICAL, so a gate built on that flag would | |
| # ignore exactly the severities it was asked to block. | |
| # | |
| # Metrics are off. This repository does not send telemetry about its own | |
| # source anywhere, which is the same promise the tool itself makes. | |
| # | |
| # One rule is excluded, and the reason is measured rather than assumed. | |
| # missing-integrity asks for a subresource integrity attribute, which | |
| # applies to a script or a stylesheet fetched from another origin. On | |
| # every page of web/public it matches lines 8 to 11 - the canonical link | |
| # and three hreflang alternates - while the one real stylesheet, at line | |
| # 31, it does not match at all. 48 findings, none of them about a | |
| # subresource. This site loads nothing from another origin by design. | |
| # | |
| # Excluded here rather than through a .semgrepignore file, and that is | |
| # also from measurement: a .semgrepignore REPLACES semgrep own default | |
| # patterns instead of adding to them. Measured 2026-08-27, adding one | |
| # took the scan from 217 files to 366 by pulling in all 166 guard test | |
| # files, and with them a new blocking finding. Security scanning of test | |
| # files is a question this project already answered for gosec, and the | |
| # answer was no. | |
| run: | | |
| semgrep scan --config p/default --metrics=off --oss-only --json --output semgrep.json --quiet --exclude-rule html.security.audit.missing-integrity.missing-integrity | |
| - name: decide | |
| # Separate from the scan on purpose. Measured on the development machine | |
| # 2026-08-27: a semgrep whose core failed printed four errors, exited 0, | |
| # and wrote a 23 byte file that was not JSON. Reading the exit code would | |
| # have called that a clean tree. The gate reads the report. | |
| run: python .github/scripts/semgrep_gate.py semgrep.json | |
| sbom: | |
| name: bill of materials | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.14" | |
| - name: graphics and windowing headers | |
| # The window links OpenGL through C, so without these the toolkit does | |
| # not compile at all - and a scan of a binary that was never built is a | |
| # scan of nothing. Taken from the toolkit's own CI, the same list the | |
| # release workflow uses. | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| libgl1-mesa-dev \ | |
| libwayland-dev \ | |
| libx11-dev \ | |
| libxkbcommon-dev \ | |
| xorg-dev | |
| - name: build what a release would publish | |
| # Both binaries, because they do not carry the same code: the command | |
| # line links two modules and the window twenty-eight plus the fonts and | |
| # drawings they bring. Scanning one of them would ask half the question. | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist | |
| CGO_ENABLED=0 go build -tags "$(cat .github/build-tags)" -trimpath -o dist/tfg ./cmd/tfg | |
| CGO_ENABLED=1 go build -tags "$(cat .github/build-tags)" -trimpath -o dist/tfg-gui ./cmd/tfg-gui | |
| ls -l dist | |
| - name: the document we would publish | |
| # Generated from internal/legal, the reviewed list of what we ship, with | |
| # versions read from the build. Not from a scan: measured 2026-08-27, | |
| # syft reading the window binary names every module with an exact | |
| # version and attaches a licence to one of thirty. | |
| run: go run -tags "$(cat .github/build-tags)" ./internal/legal/cmd/sbom -seed "${GITHUB_SHA}" -o ours.spdx.json | |
| - name: scan the binaries | |
| # Pinned by commit like every other action here. The scan is evidence, | |
| # not the document: its job is to ask whether the built binaries contain | |
| # something the registry does not know about. | |
| uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.0 | |
| with: | |
| path: dist | |
| format: syft-json | |
| output-file: scan.json | |
| upload-artifact: false | |
| upload-release-assets: false | |
| - name: the registry has to account for everything the scan found | |
| # Separate from the scan for the reason the semgrep gate is separate: a | |
| # scanner that falls over can still exit zero, and a gate reading the | |
| # exit code would call that a clean tree. This one reads the report, and | |
| # refuses when the report is unusable rather than treating it as empty. | |
| run: python .github/scripts/sbom_gate.py scan.json ours.spdx.json | |
| touched: | |
| name: what this push touched | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| concurrency: ${{ steps.look.outputs.concurrency }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # The comparison needs the earlier commit, and the default checkout | |
| # fetches one. | |
| fetch-depth: 0 | |
| - name: did anything concurrent change | |
| id: look | |
| # The three files concurrency is allowed to live in are not a guess. | |
| # They are the map in internal/guard/concurrency_test.go. | |
| # | |
| # This used to say the map guard was enough to keep the two in step: "a | |
| # fourth file growing a goroutine turns that guard red before it gets | |
| # here, so this list cannot quietly fall behind the tree". That was not | |
| # true, and adding the fourth file on 2026-09-05 is what showed it. The | |
| # map guard is red only while the new file is NOT in the map. The moment | |
| # somebody adds it there - which is what the guard's own message tells | |
| # them to do - it goes green, and nothing at all asks whether this line | |
| # was updated too. The list could then fall behind exactly when it | |
| # mattered: concurrency living in a file the detector is not run for. | |
| # | |
| # TestTheRaceDetectorIsRunForEveryFileThatDeclaresConcurrency now reads | |
| # this line and compares it against the map, so the two cannot drift. | |
| # | |
| # go.mod is watched as well. A toolchain or dependency change can alter | |
| # what the detector sees even when none of our own lines moved. | |
| # | |
| # So is this file, since 2026-09-24 and the owner's decision. A change | |
| # to the race job itself used to leave it skipped, so the first run of | |
| # a new definition was the weekly sweep, days after it merged. And so | |
| # is .github/build-tags, which every command of the job reads and which | |
| # decides the files it compiles - the same reason as go.mod, named by | |
| # an outside review of the pull request that split the job. | |
| # | |
| # Anything unclear counts as touched. A first push, a branch with no | |
| # comparable parent, a pull request event with no before - all answer | |
| # true, because the cost of running the detector when it was not needed | |
| # is ten minutes and the cost of skipping it when it was is a data race | |
| # in somebody else's file. | |
| run: | | |
| set -euo pipefail | |
| watched='internal/format/registry.go internal/damage/damage.go cmd/tfg/main.go internal/gui/window/run.go internal/gui/run_cgo.go internal/gui/window/tidy.go internal/audit/parallel.go internal/engine/parallel.go go.mod .github/workflows/ci.yml .github/build-tags' | |
| # On a pull request there is no "before" - the field belongs to a push | |
| # - so this asked for something empty and every pull request answered | |
| # "touched". That quietly undid the decision of 2026-08-20, because | |
| # from the day this project started taking pull requests the detector | |
| # was back to running on everything. The base of the pull request is | |
| # the commit to compare against, and github.sha is the merge commit, | |
| # so the difference between them is exactly what the pull request | |
| # changes. | |
| before="${{ github.event.pull_request.base.sha || github.event.before }}" | |
| if [ -z "$before" ] \ | |
| || [ "$before" = "0000000000000000000000000000000000000000" ] \ | |
| || ! git cat-file -e "${before}^{commit}" 2>/dev/null | |
| then | |
| echo "no earlier commit to compare against, so this counts as touched" | |
| echo "concurrency=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| changed="$(git diff --name-only "$before" "${{ github.sha }}")" | |
| echo "changed in this push:" | |
| echo "$changed" | |
| hit=false | |
| for f in $watched; do | |
| if printf '%s\n' "$changed" | grep -qx "$f"; then | |
| echo " -> $f is one of the files the race detector is for" | |
| hit=true | |
| fi | |
| done | |
| echo "concurrency=$hit" >> "$GITHUB_OUTPUT" | |
| shell: bash | |
| race: | |
| name: race detector (part ${{ strategy.job-index }} of ${{ strategy.job-total }}) | |
| needs: touched | |
| # Not on every push, decided on 2026-08-20 after the owner asked what it was | |
| # costing. Measured that day: 10m31s on the runner, against about a minute | |
| # for the whole matrix - so it was the longest thing in the run by a factor | |
| # of ten, on every push, including the ones that only touched a document. | |
| # | |
| # What makes that safe to change rather than a corner cut: concurrency in | |
| # this tree is confined to the files declared in | |
| # internal/guard/concurrency_test.go, by a guard that fails if another one | |
| # grows a goroutine. A push that does not touch them cannot introduce a race | |
| # for this to find, so running it there bought nothing. | |
| # | |
| # Three ways in now. A push that touches one of those files or this | |
| # workflow, the weekly sweep that fuzzing already uses, and by hand. The | |
| # weekly run is what catches a race that arrives through a dependency | |
| # rather than through us. | |
| if: >- | |
| github.event_name == 'schedule' || | |
| github.event_name == 'workflow_dispatch' || | |
| needs.touched.outputs.concurrency == 'true' | |
| runs-on: ubuntu-latest | |
| # Four parts since 2026-09-24, the owner's decision. The whole package under | |
| # the detector took 32 to 42 minutes, and six of the twelve runs on | |
| # 2026-09-23 and 2026-09-24 were killed at the ceiling with no data race in | |
| # their logs. Both ceilings had been raised once already, on 2026-08-31 - | |
| # the job's from 30 to 45 minutes and Go's from 25 to 40 - for the same | |
| # reason, and a problem that keeps coming back is the wrong shape rather | |
| # than a missing notch. The guards run one after another - none calls | |
| # t.Parallel - so four processes share the time between them, and every | |
| # test still runs under the detector once, in one of them. | |
| # | |
| # Every part reports. One red part does not cancel the others, whose tests | |
| # would then go unrun. | |
| # | |
| # The ceiling is the sum of the limits inside it, with room left over, so | |
| # that each of them fires first and names what was slow: about four minutes | |
| # to set up and compile (the compile alone took 3m03s on the first run), | |
| # twenty for the guards, five for the other packages. Measured on the first | |
| # run, split by a sorted list: 677, 635, 164 and 154 s of guards in the | |
| # four parts, 14m55s for the longest part. The two slowest guards take 329 | |
| # and 222 s, and wherever they land together, that part is the slow one. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| part: [0, 1, 2, 3] | |
| timeout-minutes: 35 | |
| env: | |
| # The one thing in this project that needs a C toolchain. Linux runners | |
| # ship one, so this job carries the cost and the matrix above stays on | |
| # CGO_ENABLED=0 and stays fast. | |
| CGO_ENABLED: "1" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| check-latest: false | |
| cache: true | |
| - name: the headers the toolkit needs | |
| # The window reaches OpenGL through C, so anything that builds or | |
| # analyses the whole tree on Linux compiles GLFW - and no runner carries | |
| # its headers. Measured on 2026-08-20, the first run after the | |
| # repository went public and so the first one this job has ever had | |
| # against the toolkit: "wayland-client-core.h: No such file or | |
| # directory", and the job never reached our code at all. | |
| # | |
| # Taken from the toolkit's own CI rather than from a remembered list. | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends libgl1-mesa-dev libwayland-dev libx11-dev libxkbcommon-dev xorg-dev | |
| shell: bash | |
| - name: test this part under the race detector | |
| # A data race is the one defect class here that nothing else notices. It | |
| # does not change a size, and on the run that happens to interleave the | |
| # safe way it does not change a byte either - so determinism and the | |
| # pinned values both stay green while the file is wrong once a month on | |
| # somebody else's machine. The guard that keeps concurrency confined to | |
| # the declared files lives in internal/guard, so this and that one | |
| # answer different halves of the same worry. | |
| # | |
| # Which tests are this part's is worked out here, from the test binary, | |
| # rather than written down. A list kept by hand would miss the next | |
| # guard somebody adds, and miss it green. The list is asked with -race, | |
| # because a file in the package builds only without it, and it keeps | |
| # Fuzz and Example because go test runs those as tests too. | |
| # | |
| # A test's part is the checksum of its name (cksum) modulo the number | |
| # of parts, so adding or removing a test moves no other test. It was | |
| # every fourth name of a sorted list at first, and one guard added on | |
| # this job's own pull request moved every name after it - the part | |
| # holding the slowest guards changed from one commit to the next. The | |
| # parts are even only on average, and which one is slow now changes only | |
| # with what is in it. | |
| # | |
| # Three things fail the part rather than let it pass on nothing - | |
| # go test with a -run pattern that matches no test exits 0. An index the | |
| # matrix does not agree with (the documentation does not say that | |
| # job-index counts from nought, so this asks), a part given no test, | |
| # and a part that never started a test it was given. That last one | |
| # compares names rather than counts, so a stray line that looks like | |
| # a start cannot stand in for a test that did not run. | |
| # | |
| # The timeout is stated rather than left to Go's ten minutes a package, | |
| # and stays below the job's ceiling, so a slow part fails as a test with | |
| # a name rather than as a killed job with no output. Go's ten minutes | |
| # killed this job on 2026-08-25, before a timeout was stated here. | |
| # Twenty minutes is 1.8 times the slowest part measured. | |
| env: | |
| PART: ${{ strategy.job-index }} | |
| PARTS: ${{ strategy.job-total }} | |
| LISTED: ${{ matrix.part }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$PART" != "$LISTED" ] || [ "$PART" -ge "$PARTS" ]; then | |
| echo "job $PART of $PARTS is part $LISTED in the matrix, and the split needs the two equal and below the total." | |
| echo "List strategy.matrix.part as 0, 1, 2 and on, in order, and check the context names in this step's env." | |
| exit 1 | |
| fi | |
| go test -tags "$(cat .github/build-tags)" ./internal/guard/ -race -list '.*' > listed.txt || { cat listed.txt; exit 1; } | |
| grep -E '^(Test|Fuzz|Example)' listed.txt > names.txt || { echo "the test binary listed no Test, Fuzz or Example function. What it printed:"; cat listed.txt; exit 1; } | |
| while IFS= read -r name; do | |
| read -r sum _ < <(printf '%s' "$name" | cksum) | |
| if [ $((sum % PARTS)) -eq "$PART" ]; then | |
| echo "$name" | |
| fi | |
| done < names.txt > mine.txt | |
| planned=$(wc -l < mine.txt) | |
| echo "part $PART of $PARTS runs $planned of $(wc -l < names.txt) tests" | |
| if [ "$planned" -eq 0 ]; then | |
| echo "part $PART of $PARTS was given no test - the checksum of no listed name lands here." | |
| echo "With this few tests, use fewer parts in strategy.matrix.part." | |
| exit 1 | |
| fi | |
| go test -tags "$(cat .github/build-tags)" ./internal/guard/ -count=1 -race -timeout 20m -v -run "^($(paste -sd'|' mine.txt))\$" 2>&1 | tee part.log | |
| missing=$(comm -23 <(sort mine.txt) <(sed -n 's/^=== RUN \([^/]*\)$/\1/p' part.log | sort -u)) | |
| if [ -n "$missing" ]; then | |
| echo "part $PART was given $planned tests and never ran these:" | |
| echo "$missing" | |
| echo "Each of these is a name the -run pattern did not reach, or a test the binary skipped before it started." | |
| exit 1 | |
| fi | |
| shell: bash | |
| - name: the other packages under the race detector | |
| # Every package outside internal/guard, in part 0 alone. None holds a | |
| # test today - the first run took one second for all of them - and one | |
| # written there has to reach the detector as well. | |
| # | |
| # A step of its own with a ceiling of its own, rather than a second | |
| # command after the guards. Go's -timeout holds one test binary, so two | |
| # commands in one step add up under nothing but the job's ceiling, and | |
| # the job would be killed past it with no name in the log - an outside | |
| # review of the pull request that split the job. Runs after red guards | |
| # as well, so its answer is not lost with theirs. | |
| if: ${{ !cancelled() && matrix.part == 0 }} | |
| timeout-minutes: 5 | |
| run: | | |
| set -euo pipefail | |
| go list -tags "$(cat .github/build-tags)" ./... | grep -v '/internal/guard$' > others.txt | |
| mapfile -t others < others.txt | |
| go test -tags "$(cat .github/build-tags)" -count=1 -race -timeout 4m "${others[@]}" | |
| shell: bash | |
| coverage: | |
| name: coverage gate | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| env: | |
| CGO_ENABLED: "0" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| check-latest: false | |
| # Keyed on go.sum. The one dependency is the YAML parser behind the | |
| # recipe - see docs/STACK.md. | |
| cache: true | |
| - name: measure | |
| # -coverpkg is not optional here. The guard tests live in their own | |
| # package, and by default Go credits coverage only to the package | |
| # under test - which reports 0.0% and makes the gate meaningless. | |
| # Measured, not assumed. | |
| # | |
| # The timeout is stated rather than left to Go, since 2026-09-03, and | |
| # for the same reason the race job above states its own. Go allows ten | |
| # minutes PER PACKAGE by default while this job allows twenty for all of | |
| # it, so internal/guard died on a limit nobody had chosen - a stack | |
| # trace out of whichever test was running when the alarm went off, | |
| # instead of a failure naming something. | |
| # | |
| # Measured on the runner rather than guessed. This step took 375 s and | |
| # 457 s on two consecutive main runs of 2026-09-02 and 2026-09-03, which | |
| # is 22 percent of variance on code that barely moved between them, and | |
| # the default cuts in at 600 s. A branch adding eight seconds of | |
| # coverage instrumented work then timed out. Eight seconds is not what | |
| # went wrong: 457 against 600 was never a margin, and a limit that | |
| # decides on how busy the runner is tells you nothing about the code. | |
| # | |
| # Atomic counters are the cost. Every statement in every internal | |
| # package pays one, and this package renders twenty five screens and | |
| # generates files for twenty four formats. Eighteen minutes sits under | |
| # the job's own ceiling on purpose, so a genuinely stuck run still fails | |
| # as a test with output rather than as a killed job without any. | |
| run: > | |
| go test -tags "$(cat .github/build-tags)" ./... -count=1 -covermode=atomic | |
| -coverpkg=./internal/...,./cmd/... | |
| -coverprofile=coverage.out -timeout 18m | |
| - name: gate | |
| # The threshold lives in exactly one place, .github/coverage-threshold. | |
| # It rises with coverage and is never lowered to turn a red run green. | |
| # Lowering it is a decision for the owner, not a way to get unblocked. | |
| run: | | |
| set -euo pipefail | |
| threshold=$(tr -d '[:space:]' < .github/coverage-threshold) | |
| actual=$(go tool cover -func=coverage.out | awk '/^total:/ {gsub("%","",$3); print $3}') | |
| echo "coverage ${actual}% - threshold ${threshold}%" | |
| awk -v a="$actual" -v t="$threshold" 'BEGIN { exit (a+0 >= t+0) ? 0 : 1 }' \ | |
| || { echo "coverage ${actual}% is below the threshold ${threshold}%"; exit 1; } | |
| shell: bash | |
| fidelity: | |
| name: reference tools actually installed | |
| # The oracle guards skip when the tool they need is missing, loudly, and a | |
| # skip is not a check. The matrix runners have python and node and nothing | |
| # else, so on an ordinary push most of those guards report a skip and the | |
| # run is green having verified almost nothing about the files themselves. | |
| # | |
| # This job installs Inkscape, 7z, ffmpeg and poppler so the same guards run | |
| # for real. It is separate from the matrix because installing a graphics | |
| # stack takes minutes and a push should not wait for it. | |
| # | |
| # What it does NOT do, said plainly: it does not walk many sizes. The | |
| # guards it runs check the realistic size and the smallest ones, which is | |
| # where the one defect this ever found was hiding - an SVG that rendered to | |
| # a blank canvas at exactly its minimum, 2026-08-03. The wider sweep across | |
| # sizes, seeds and label settings lives in tools/probes/fidelity-sweep.py | |
| # and is run by hand, because tools/ is outside the repository. See O51. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| env: | |
| CGO_ENABLED: "0" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| check-latest: false | |
| cache: true | |
| - name: the reference tools | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends \ | |
| p7zip-full ffmpeg poppler-utils inkscape python3-pil python3-yaml | |
| # The distribution's Pillow cannot open an AVIF - measured on this | |
| # runner, PIL.UnidentifiedImageError on a file the structural checker | |
| # had just called sound. Installed for the SAME interpreter the oracle | |
| # reaches for, then asked whether it can actually read one. No version | |
| # is named: the question is what this Pillow can do, not when the | |
| # support landed. | |
| python -m pip install --break-system-packages --upgrade pillow | |
| python - <<'PY' | |
| from PIL import Image, features | |
| print("pillow", Image.__version__, "avif:", features.check("avif")) | |
| if not features.check("avif"): | |
| raise SystemExit("this Pillow cannot read AVIF, so the oracle would fail on a good file") | |
| PY | |
| shell: bash | |
| - name: every format past its reference tool | |
| # -v so that a skip is visible in the log. A tool that failed to install | |
| # would otherwise turn this job back into the green nothing it exists | |
| # to replace. | |
| run: go test -tags "$(cat .github/build-tags)" ./internal/guard/ -count=1 -run 'ReferenceTool' -v | |
| shell: bash | |
| fuzz: | |
| name: fuzzing with a time budget | |
| # Weekly and by hand, never on a push. Fuzzing searches without end, so a | |
| # budget picked to fit a commit gate would be a gate that says "fuzzing | |
| # passed" after twenty seconds of looking - which is worth less than not | |
| # claiming it. See docs/OBSERVATIONS.md, O34. | |
| # | |
| # What the push jobs do run is the seed corpus in testdata/fuzz, because | |
| # go test executes fuzz targets as ordinary tests. That is regression | |
| # cover for what has already been found, and it is not the same thing as | |
| # searching. | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| # Four targets at five minutes each, plus the build and the baseline pass. | |
| timeout-minutes: 45 | |
| env: | |
| CGO_ENABLED: "0" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| check-latest: false | |
| cache: true | |
| - name: search | |
| # One target at a time, because -fuzz takes exactly one. Five minutes | |
| # each - long enough to be a search rather than a gesture, short enough | |
| # that a weekly run stays cheap. | |
| # | |
| # A finding here is not fixed by this job. It has to be pulled into | |
| # testdata/fuzz and committed, or it disappears with the runner and the | |
| # next search starts from nothing. | |
| run: | | |
| set -euo pipefail | |
| for target in FuzzParseSize FuzzParseRecipe FuzzNameTemplate FuzzCanonicalRecipe; do | |
| echo "=== $target ===" | |
| go test -tags "$(cat .github/build-tags)" ./internal/guard/ -run "^$" -fuzz "^${target}$" -fuzztime 5m | |
| done | |
| shell: bash | |
| - name: keep what was found | |
| # The corpus a failing run leaves behind is the whole value of the run, | |
| # and it lives in the runner's cache directory rather than the tree. | |
| if: failure() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: fuzz-findings | |
| path: | | |
| internal/guard/testdata/fuzz/** | |
| ~/.cache/go-build/fuzz/** | |
| if-no-files-found: warn |