-
-
Notifications
You must be signed in to change notification settings - Fork 2
1085 lines (1022 loc) · 54.4 KB
/
Copy pathci.yml
File metadata and controls
1085 lines (1022 loc) · 54.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
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/[email protected] -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/[email protected] -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/[email protected] 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/[email protected] 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