Skip to content

Commit 12f1adb

Browse files
committed
ci: the shared build gate
Runs what `make check` runs locally: make test, then make test-wasm as a compile gate for the js/wasm-tagged half, then golangci-lint in both build contexts. A host run cannot see //go:build js && wasm files at all, and in several of these repos that is most of the code.
1 parent f7f70ad commit 12f1adb

2 files changed

Lines changed: 227 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: ci
2+
on:
3+
push:
4+
branches: [main]
5+
pull_request:
6+
7+
# What this runs is what `make check` runs locally, so a green tick here and a
8+
# clean tree there mean the same thing. The Makefile is the single definition;
9+
# this file only supplies a machine to run it on.
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v5
15+
- uses: actions/setup-go@v6
16+
with:
17+
go-version-file: go.mod
18+
# make test, not go test ./...: it recurses into nested modules and falls
19+
# back to the js/wasm tests under Node where nothing builds for the host,
20+
# which is the whole of some of these repos.
21+
- run: make test
22+
# A compile gate for the js/wasm-tagged half. A wasm-only compile error is
23+
# invisible to a host build, and in these repos that is most of the code.
24+
- run: make test-wasm
25+
26+
lint:
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@v5
30+
- uses: actions/setup-go@v6
31+
with:
32+
go-version-file: go.mod
33+
- name: install golangci-lint
34+
# The installer is taken at the same tag as the binary it installs.
35+
# Fetching it from master broke every run here: that script matches the
36+
# checksums file by prefix, so for a release that also publishes an
37+
# SBOM it reads the hash of golangci-lint-*.tar.gz.sbom.json and then
38+
# rejects a download that was in fact correct. Pinning also means CI
39+
# stops running whatever happens to be on a branch today.
40+
env:
41+
GOLANGCI_VERSION: v2.12.2
42+
run: |
43+
curl -sSfL "https://raw.githubusercontent.com/golangci/golangci-lint/${GOLANGCI_VERSION}/install.sh" |
44+
sh -s -- -b "$(go env GOPATH)/bin" "${GOLANGCI_VERSION}"
45+
# Two passes inside make lint: the host config, then again with GOOS=js,
46+
# because a host run cannot see //go:build js && wasm files at all.
47+
- run: make lint

Makefile

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
.DEFAULT_GOAL := help
2+
.PHONY: help format tidy lint vet test test-wasm test-browser cover check install-linters docs
3+
4+
# The targets that matter are `format` and `check`, and they mean the same
5+
# thing here as in 0pcom/skywire, which is the reference for these repos.
6+
# .golangci.yml is copied from there.
7+
8+
PROJECT_BASE := github.com/0magnet/netscrape
9+
OPTS ?= GO111MODULE=on
10+
11+
# Whether the host checks run with cgo. 0 matches skywire, which is pure Go.
12+
# Repos whose real build needs cgo — a GUI, audio, anything binding a C library
13+
# — set this to 1, because linting them without it type-checks almost nothing.
14+
CGO ?= 0
15+
16+
# Packages this toolchain cannot build at all — firmware for a different
17+
# target, say. Not the same as code that merely does not build for this host:
18+
# js/wasm is handled below by running the checks again in that context, which
19+
# is the better answer whenever it is available. Empty in most repos.
20+
SKIP ?=
21+
22+
# Directories rather than import paths, because golangci-lint resolves a bare
23+
# import path against the working directory and then cannot find it.
24+
#
25+
# Listed with the same CGO_ENABLED the checks use. Listed with cgo on and
26+
# linted with it off, a cgo-only package is named and then found to have no
27+
# files in it, which fails the run rather than reporting anything about it.
28+
#
29+
# -e so that one package that cannot be listed does not empty the list. Without
30+
# it, a single unbuildable package makes `go list` fail for the whole module,
31+
# this comes back blank, and the run below reports that there is nothing to
32+
# check — which reads exactly like passing.
33+
# Nested modules. `go list ./...` STOPS AT A MODULE BOUNDARY, so a repo that
34+
# keeps a second go.mod — desk's panes, pisano's web — had that half silently
35+
# unchecked: not failing, not skipped with a message, simply absent, which reads
36+
# exactly like passing. Each one is handled by re-running this same Makefile
37+
# inside it, so a submodule is checked the way the root is by construction
38+
# rather than by remembering to add it somewhere.
39+
#
40+
# Discovered rather than listed, so adding a module does not also mean editing
41+
# this. SUBMODULES= on the recursive call is what stops it recursing forever.
42+
THIS := $(abspath $(firstword $(MAKEFILE_LIST)))
43+
# The one lint config, by absolute path: a submodule has no .golangci.yml of
44+
# its own, and the recursive call runs with that submodule as the working
45+
# directory, so a relative -c would look for it there and fail.
46+
LINTCFG := $(dir $(THIS)).golangci.yml
47+
# Vendor mode only where there IS a vendor directory. The config names one mode
48+
# for the whole repo, and a submodule that does not vendor cannot satisfy it —
49+
# golangci-lint then reports "inconsistent vendoring" for a tree that is
50+
# perfectly consistent, because it was told to look for something that was
51+
# never there. Evaluated in the working directory, so the recursion gets the
52+
# answer for the module it is actually in.
53+
MODMODE = $(if $(wildcard vendor/modules.txt),vendor,readonly)
54+
SUBMODULES := $(patsubst %/go.mod,%,$(shell find . -mindepth 2 -name go.mod -not -path './vendor/*' -not -path '*/vendor/*' 2>/dev/null | sort))
55+
56+
define recurse
57+
@for m in $(SUBMODULES); do \
58+
echo "--- $$m"; \
59+
$(MAKE) --no-print-directory -C $$m -f $(THIS) $@ SUBMODULES= || exit 1; \
60+
done
61+
endef
62+
63+
PKGS = $(shell CGO_ENABLED=$(CGO) go list -e -f '{{.Dir}}' ./... 2>/dev/null $(if $(SKIP),| grep -vE '$(SKIP)'))
64+
JSPKGS = $(shell CGO_ENABLED=0 GOOS=js GOARCH=wasm go list -e -f '{{.Dir}}' ./... 2>/dev/null $(if $(SKIP),| grep -vE '$(SKIP)'))
65+
66+
help: ## Show this help
67+
@grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
68+
| awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-18s\033[0m %s\n", $$1, $$2}'
69+
70+
tidy: ## Tidy dependencies
71+
${OPTS} go mod tidy -v
72+
73+
format: tidy ## Format the code. Needs goimports (make install-linters)
74+
@if grep -qE '^(replace|exclude)' go.mod; then \
75+
echo "ERROR: go.mod contains replace or exclude directives which break go install @version"; \
76+
grep -E '^(replace|exclude)' go.mod; \
77+
exit 1; \
78+
fi
79+
${OPTS} goimports -w -local ${PROJECT_BASE} $(shell go list -f '{{.Dir}}' ./... 2>/dev/null | grep -v /vendor/)
80+
81+
lint: ## Run golangci-lint. Needs it installed (make install-linters)
82+
command -v golangci-lint || go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
83+
golangci-lint --version
84+
@# Some of these repos are entirely js/wasm-tagged, so the host context has
85+
@# nothing in it and linting it is an error rather than a pass.
86+
@if [ -n "$(PKGS)" ]; then \
87+
CGO_ENABLED=$(CGO) ${OPTS} golangci-lint run --modules-download-mode=$(MODMODE) -c $(LINTCFG) $(PKGS); \
88+
else \
89+
echo '--- nothing builds for this host; skipping the host pass'; \
90+
fi
91+
@# A host run cannot see js/wasm-tagged files, so anything only they use
92+
@# reads as dead — and anything wrong inside them is never checked at all.
93+
@if grep -rlq '^//go:build js' --include='*.go' . 2>/dev/null; then \
94+
echo '--- again in the js/wasm build context'; \
95+
CGO_ENABLED=0 GOOS=js GOARCH=wasm ${OPTS} golangci-lint run --modules-download-mode=$(MODMODE) -c $(LINTCFG) $(JSPKGS); \
96+
fi
97+
$(recurse)
98+
99+
vet: ## Run go vet
100+
@if [ -n "$(PKGS)" ]; then \
101+
CGO_ENABLED=$(CGO) ${OPTS} go vet $(PKGS); \
102+
fi
103+
@if grep -rlq '^//go:build js' --include='*.go' . 2>/dev/null; then \
104+
CGO_ENABLED=0 GOOS=js GOARCH=wasm ${OPTS} go vet $(JSPKGS); \
105+
fi
106+
$(recurse)
107+
108+
test: ## Run tests. Falls back to the js/wasm ones where nothing builds here
109+
@if [ -n "$(PKGS)" ]; then \
110+
${OPTS} go test $(PKGS); \
111+
else \
112+
echo '--- nothing builds for this host; running the js/wasm tests instead'; \
113+
$(MAKE) --no-print-directory -f $(THIS) test-wasm; \
114+
fi
115+
$(recurse)
116+
117+
# The exec wrapper Go ships for running a js/wasm binary under Node. It is what
118+
# makes `go test` work for code the host cannot build at all.
119+
# The wrapper moved from misc/wasm to lib/wasm in Go 1.24, and CI may install
120+
# either side of that, so both are tried. Empty means this toolchain cannot
121+
# run js/wasm tests at all.
122+
WASMEXEC = $(shell for d in lib misc; do p="$$(go env GOROOT)/$$d/wasm/go_js_wasm_exec"; if [ -x "$$p" ]; then echo "$$p"; break; fi; done)
123+
124+
test-wasm: ## Run the js/wasm tests under Node
125+
@# One shell for the whole target. An `exit 0` in a recipe line of its own
126+
@# ends that line and nothing else, so the earlier form announced it was
127+
@# skipping and then ran the tests regardless, against the exec wrapper it
128+
@# had just reported missing.
129+
@# Node has no DOM: document, window and requestAnimationFrame are all
130+
@# undefined. JS core is there, and a fake document can be installed from
131+
@# Go with js.Global().Set, which is how the DOM-facing code is covered.
132+
@if [ -z "$(WASMEXEC)" ]; then \
133+
echo 'no js/wasm exec wrapper in this Go installation; skipping'; \
134+
elif ! command -v node >/dev/null; then \
135+
echo 'node is not installed; skipping'; \
136+
elif [ -n "$(JSPKGS)" ]; then \
137+
CGO_ENABLED=0 GOOS=js GOARCH=wasm ${OPTS} go test -exec="$(WASMEXEC)" $(JSPKGS); \
138+
else \
139+
echo 'no js/wasm packages'; \
140+
fi
141+
$(recurse)
142+
143+
# Running the js/wasm tests in a real browser instead of Node. Node has no DOM;
144+
# a browser has one, plus canvas and WebGL. Tests that build a fake DOM should
145+
# detect a real one and use it, so the same tests run both ways — anything the
146+
# fake gets wrong then shows up as a test that passes under Node and fails here.
147+
#
148+
# CHROME may name any Chrome-compatible binary. Brave is one. Not called BROWSER:
149+
# that is a conventional environment variable (xdg uses it) and ?= would take
150+
# whatever it happens to be set to — firefox, on this machine.
151+
CHROME ?= $(shell command -v google-chrome chromium chromium-browser brave 2>/dev/null | head -1)
152+
153+
test-browser: ## Run the js/wasm tests in a headless browser (needs wasmbrowsertest)
154+
@command -v wasmbrowsertest >/dev/null || { \
155+
echo 'wasmbrowsertest is not installed; go install github.com/agnivade/wasmbrowsertest@latest'; exit 0; }
156+
@[ -n "$(CHROME)" ] || { echo 'no Chrome-compatible browser found (set CHROME=); skipping'; exit 0; }
157+
@if [ -z "$(JSPKGS)" ]; then echo 'no js/wasm packages'; exit 0; fi
158+
@# chromedp looks for a binary called google-chrome and does not pass
159+
@# --no-sandbox, which some environments require. A shim supplies both.
160+
@d=$$(mktemp -d); printf '#!/bin/sh\nexec %s --no-sandbox "$$@"\n' '$(CHROME)' > $$d/google-chrome; \
161+
chmod +x $$d/google-chrome; \
162+
PATH=$$d:$$PATH CGO_ENABLED=0 GOOS=js GOARCH=wasm ${OPTS} \
163+
go test -exec=wasmbrowsertest $(JSPKGS); \
164+
rc=$$?; rm -rf $$d; exit $$rc
165+
$(recurse)
166+
167+
cover: ## Report test coverage per package
168+
@if [ -n "$(PKGS)" ]; then \
169+
CGO_ENABLED=$(CGO) ${OPTS} go test -cover $(PKGS) 2>&1 | grep -v '^ok.*no test files'; \
170+
fi
171+
$(recurse)
172+
173+
check: lint vet test ## Run linters, vet and tests
174+
175+
install-linters: ## Install the linters
176+
${OPTS} go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
177+
${OPTS} go install golang.org/x/tools/cmd/goimports@latest
178+
179+
docs: ## Regenerate the dependency graph and code counts in the README
180+
./gendocs.sh

0 commit comments

Comments
 (0)