diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3252278fac..1a2fd424d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -321,7 +321,7 @@ jobs: - name: Install Wails CLI run: | echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0 + go install "github.com/wailsapp/wails/v2/cmd/wails@$(cat "$GITHUB_WORKSPACE/.wails-version")" - name: gofmt run: | @@ -449,7 +449,7 @@ jobs: - name: Install Wails CLI run: | echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0 + go install "github.com/wailsapp/wails/v2/cmd/wails@$(cat "$GITHUB_WORKSPACE/.wails-version")" # go:embed of frontend/dist needs a built frontend before the package # compiles, same as the ubuntu desktop leg. @@ -492,6 +492,11 @@ jobs: - name: repo standards run: go run ./tools/repolint + - name: wails version pin + run: | + bash scripts/check-wails-pin.sh + bash scripts/check-wails-pin.test.sh + # `make lint-install` reads the same file, so a local run and this job # cannot drift onto different linter versions. - id: golangci diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 23d14fe3fd..473774da2a 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -371,7 +371,7 @@ jobs: rm -f "$RUNNER_TEMP/cert.p12" - name: Install Wails CLI - run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0 + run: go install "github.com/wailsapp/wails/v2/cmd/wails@$(cat "$GITHUB_WORKSPACE/.wails-version")" # Official Windows releases must fail closed. Forks can still exercise # unsigned packaging without repository secrets, but esengine releases diff --git a/.wails-version b/.wails-version new file mode 100644 index 0000000000..c757c9824b --- /dev/null +++ b/.wails-version @@ -0,0 +1 @@ +v2.13.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c0cf82e3e..e58daeea57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,8 @@ everything you need to get started. - **Go 1.25+** — the project targets the latest stable Go release - **Git** — for version control - **Node.js** (optional) — only if you work on the desktop app (`desktop/`) +- **Wails CLI** (desktop only) — run `make wails-install` so the CLI matches + `desktop/go.mod` ## Getting started diff --git a/Makefile b/Makefile index 1908482e1f..cdfa6d12e3 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,9 @@ LDFLAGS := -s -w \ GOEXE := $(shell go env GOEXE) # One pin for the Makefile and the CI lint job; see .github/workflows/ci.yml. GOLANGCI_VERSION := $(shell cat .golangci-version) +WAILS_VERSION := $(shell tr -d '[:space:]' < .wails-version) -.PHONY: build vet fmt lint lint-go lint-install lint-cross lint-update test desktop-test desktop-test-short desktop-test-times sdk-test sdk-test-race hooks cross clean +.PHONY: build vet fmt lint lint-go lint-install lint-cross lint-update wails-install test desktop-test desktop-test-short desktop-test-times sdk-test sdk-test-race hooks cross clean build: CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/reasonix$(GOEXE) ./cmd/reasonix @@ -26,6 +27,8 @@ fmt: # particular never surface in `go vet`. lint: lint-go go run ./tools/repolint + bash scripts/check-wails-pin.sh + bash scripts/check-wails-pin.test.sh lint-go: @command -v golangci-lint >/dev/null || { echo "golangci-lint not installed; run: make lint-install"; exit 1; } @@ -41,6 +44,10 @@ lint-install: lint-update: go run ./tools/repolint -update +wails-install: + bash scripts/check-wails-pin.sh + go install "github.com/wailsapp/wails/v2/cmd/wails@$(WAILS_VERSION)" + # Linting one GOOS leaves every //go:build windows and darwin file unchecked. lint-cross: @for t in "linux ." "darwin ." "windows ." "linux desktop" "windows desktop"; do \ diff --git a/desktop/README.md b/desktop/README.md index 5a6cadff10..c0769eb534 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -35,7 +35,8 @@ vet / test ./...` skip this directory, while the import path stays under - Go (matches the parent module). - Node + **pnpm** (`npm i -g pnpm`). -- Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest` +- Wails CLI matching the library: run `make wails-install` from the repository + root. The target reads the shared `.wails-version` pin. - Platform webview libs: macOS ships WebKit; Windows needs the Edge **WebView2** runtime; Linux needs `libgtk-3-dev` plus WebKitGTK. The default build links against **WebKitGTK 4.0**; distros that only ship **4.1** (Fedora 40+, Ubuntu diff --git a/prod_test b/prod_test index 36ba5b5e05..1b274a8020 100755 --- a/prod_test +++ b/prod_test @@ -63,6 +63,14 @@ go_bin_dir() { printf '%s' "$gobin" } +installed_wails_version() { + local output + if ! output="$(wails version 2>/dev/null)"; then + return 1 + fi + awk 'match($0, /v[0-9]+\.[0-9]+\.[0-9]+/) { print substr($0, RSTART, RLENGTH); exit }' <<<"$output" +} + sanitize_semver_id() { local value="$1" value="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^0-9A-Za-z]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')" @@ -127,9 +135,11 @@ default_version() { } install_or_check_tools() { - local os install_tools + local os install_tools wails_pin wails_version os="${1%/*}" install_tools="${PROD_TEST_INSTALL_TOOLS:-1}" + wails_pin="$(tr -d '[:space:]' < "$ROOT/.wails-version")" + [[ -n "$wails_pin" ]] || fail ".wails-version is empty" need_cmd git need_cmd go @@ -151,14 +161,21 @@ install_or_check_tools() { fi fi - if ! command -v wails >/dev/null 2>&1; then + wails_version="" + if command -v wails >/dev/null 2>&1; then + wails_version="$(installed_wails_version || true)" + fi + if [[ "$wails_version" != "$wails_pin" ]]; then if [[ "$install_tools" == "1" ]]; then - echo "==> installing Wails CLI" - go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0 + echo "==> installing Wails CLI $wails_pin (found ${wails_version:-none})" + go install "github.com/wailsapp/wails/v2/cmd/wails@$wails_pin" + hash -r else - fail "wails not found. Install with: go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0" + fail "Wails CLI ${wails_version:-not found} does not match .wails-version $wails_pin. Run: make wails-install" fi fi + wails_version="$(installed_wails_version || true)" + [[ "$wails_version" == "$wails_pin" ]] || fail "Wails CLI ${wails_version:-not found} does not match .wails-version $wails_pin after installation" case "$os" in darwin) diff --git a/scripts/check-wails-pin.sh b/scripts/check-wails-pin.sh new file mode 100755 index 0000000000..ca5e006616 --- /dev/null +++ b/scripts/check-wails-pin.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# The Wails CLI and the Wails library must be the same version. +# +# They drift silently in both directions. Dependabot bumps the library in +# desktop/go.mod but cannot see the CLI, whose version is a string inside three +# workflow files; and `wails dev` rewrites desktop/go.mod down to whatever CLI +# the developer has installed, so a local run can quietly revert a dependency +# upgrade. One pin, read by everyone, and this check to prove it held. +set -euo pipefail + +root="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +pin="$(tr -d '[:space:]' < "$root/.wails-version")" +mod="$(awk '/github.com\/wailsapp\/wails\/v2 v/ {print $2; exit}' "$root/desktop/go.mod")" +module="github.com/wailsapp/wails/v2/cmd/wails" + +if [ -z "$pin" ]; then + echo "check-wails-pin: .wails-version is empty" >&2 + exit 1 +fi +if [[ ! "$pin" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "check-wails-pin: .wails-version must contain one stable semantic version (vMAJOR.MINOR.PATCH), got: $pin" >&2 + exit 1 +fi +if [ "$pin" != "$mod" ]; then + cat >&2 <&2 + exit 1 + fi + # Several desktop steps run with working-directory: desktop, where a bare + # `cat .wails-version` resolves to nothing and installs wails@"". + if grep -q 'cat \.wails-version' "$workflow"; then + echo "check-wails-pin: $(basename "$workflow") reads .wails-version relative to the job's working directory; use \$GITHUB_WORKSPACE" >&2 + exit 1 + fi +done + +# The workflows are not the only way developers build a release. Reject every +# tracked install source except the three supported ways to read the shared pin. +install_lines="$(git -C "$root" grep -nF "$module@" -- . || true)" +unexpected_sources="$(awk -v module="$module" ' + index($0, ".github/workflows/ci.yml:") == 1 && index($0, module "@$(cat \"$GITHUB_WORKSPACE/.wails-version\")") { next } + index($0, ".github/workflows/release-desktop.yml:") == 1 && index($0, module "@$(cat \"$GITHUB_WORKSPACE/.wails-version\")") { next } + index($0, "Makefile:") == 1 && index($0, module "@$(WAILS_VERSION)") { next } + index($0, "prod_test:") == 1 && index($0, module "@$wails_pin") { next } + { print } +' <<<"$install_lines")" +if [[ -n "$unexpected_sources" ]]; then + cat >&2 <&2 <&2 + exit 1 + } +done +grep -Fq 'WAILS_VERSION := $(shell tr -d '\''[:space:]'\'' < .wails-version)' "$root/Makefile" || { + echo "check-wails-pin: Makefile must read WAILS_VERSION from .wails-version" >&2 + exit 1 +} +grep -Fq "go install \"$module@\$(WAILS_VERSION)\"" "$root/Makefile" || { + echo "check-wails-pin: make wails-install must install WAILS_VERSION" >&2 + exit 1 +} +grep -Fq 'make wails-install' "$root/desktop/README.md" || { + echo "check-wails-pin: desktop/README.md must direct developers to make wails-install" >&2 + exit 1 +} +grep -Fq 'wails version' "$root/prod_test" || { + echo "check-wails-pin: prod_test must verify the installed Wails CLI version" >&2 + exit 1 +} +grep -Fq "go install \"$module@\$wails_pin\"" "$root/prod_test" || { + echo "check-wails-pin: prod_test must install the shared Wails pin" >&2 + exit 1 +} + +echo "check-wails-pin: CLI and library both pinned at $pin" diff --git a/scripts/check-wails-pin.test.sh b/scripts/check-wails-pin.test.sh new file mode 100755 index 0000000000..9b2b49b93f --- /dev/null +++ b/scripts/check-wails-pin.test.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +scratch="$(mktemp -d)" +trap 'rm -rf "$scratch"' EXIT +wails_module="github.com/wailsapp/wails/v2/cmd/wails" +fixture_pin="v9.8.7" +repo_pin="$(tr -d '[:space:]' < "$root/.wails-version")" +fake_mismatch="v0.0.0" +if [[ "$fake_mismatch" == "$repo_pin" ]]; then + fake_mismatch="v0.0.1" +fi + +make_fixture() { + local name="$1" + local fixture="$scratch/$name" + mkdir -p "$fixture/.github/workflows" "$fixture/desktop" "$fixture/scripts" + cp "$root/scripts/check-wails-pin.sh" "$fixture/scripts/check-wails-pin.sh" + printf '%s\n' "$fixture_pin" > "$fixture/.wails-version" + printf 'github.com/wailsapp/wails/v2 %s\n' "$fixture_pin" > "$fixture/desktop/go.mod" + printf 'run: go install "%s@$(cat "$GITHUB_WORKSPACE/.wails-version")"\n' "$wails_module" > "$fixture/.github/workflows/ci.yml" + cp "$fixture/.github/workflows/ci.yml" "$fixture/.github/workflows/release-desktop.yml" + printf 'go install "%s@$wails_pin"\nwails version\n' "$wails_module" > "$fixture/prod_test" + printf 'Run `make wails-install`.\n' > "$fixture/desktop/README.md" + printf 'WAILS_VERSION := $(shell tr -d '\''[:space:]'\'' < .wails-version)\nwails-install:\n\tgo install "%s@$(WAILS_VERSION)"\n' "$wails_module" > "$fixture/Makefile" + git -C "$fixture" init -q + git -C "$fixture" add . + printf '%s' "$fixture" +} + +expect_failure() { + local fixture="$1" + local expected="$2" + local output + if output="$(bash "$fixture/scripts/check-wails-pin.sh" "$fixture" 2>&1)"; then + echo "check-wails-pin test: expected failure containing: $expected" >&2 + exit 1 + fi + grep -Fq "$expected" <<<"$output" || { + echo "check-wails-pin test: missing failure text: $expected" >&2 + echo "$output" >&2 + exit 1 + } +} + +fixture="$(make_fixture valid)" +bash "$fixture/scripts/check-wails-pin.sh" "$fixture" >/dev/null + +fixture="$(make_fixture prod-test-hard-code)" +printf 'go install %s@v2.12.0\n' "$wails_module" >> "$fixture/prod_test" +git -C "$fixture" add prod_test +expect_failure "$fixture" 'prod_test:' + +fixture="$(make_fixture docs-latest)" +printf 'go install %s@latest\n' "$wails_module" >> "$fixture/desktop/README.md" +git -C "$fixture" add desktop/README.md +expect_failure "$fixture" 'desktop/README.md:' + +fixture="$(make_fixture module-drift)" +printf 'github.com/wailsapp/wails/v2 v9.8.8\n' > "$fixture/desktop/go.mod" +expect_failure "$fixture" 'version drift' + +fixture="$(make_fixture workflow-cwd)" +printf 'run: go install "%s@$(cat .wails-version)"\n' "$wails_module" > "$fixture/.github/workflows/ci.yml" +expect_failure "$fixture" "reads .wails-version relative to the job's working directory" + +fixture="$(make_fixture workflow-other-source)" +printf 'run: go install "%s@$WAILS_VERSION"\n' "$wails_module" > "$fixture/.github/workflows/ci.yml" +expect_failure "$fixture" 'unsupported sources' + +fixture="$(make_fixture makefile-hard-code)" +printf 'WAILS_VERSION := %s\nwails-install:\n\tgo install "%s@$(WAILS_VERSION)"\n' "$fixture_pin" "$wails_module" > "$fixture/Makefile" +expect_failure "$fixture" 'Makefile must read WAILS_VERSION from .wails-version' + +fixture="$(make_fixture invalid-pin)" +printf '%s;unexpected\n' "$fixture_pin" > "$fixture/.wails-version" +printf 'github.com/wailsapp/wails/v2 %s;unexpected\n' "$fixture_pin" > "$fixture/desktop/go.mod" +expect_failure "$fixture" '.wails-version must contain one stable semantic version' + +fake_bin="$scratch/fake-bin" +mkdir -p "$fake_bin" +printf '#!/usr/bin/env bash\nprintf "%s\\n"\n' "$fake_mismatch" > "$fake_bin/wails" +printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/pnpm" +chmod +x "$fake_bin/wails" "$fake_bin/pnpm" +if output="$(GOBIN="$fake_bin" PATH="$fake_bin:$PATH" PROD_TEST_INSTALL_TOOLS=0 PROD_TEST_FAST=1 DESKTOP_BUILD_SKIP_DMG=1 PROD_TEST_OPEN_DIST=0 "$root/prod_test" darwin/arm64 2>&1)"; then + echo "check-wails-pin test: prod_test accepted a mismatched installed Wails CLI" >&2 + exit 1 +fi +grep -Fq "Wails CLI $fake_mismatch does not match .wails-version $repo_pin" <<<"$output" || { + echo "check-wails-pin test: prod_test did not report the installed-version mismatch" >&2 + echo "$output" >&2 + exit 1 +} + +printf '#!/usr/bin/env bash\nexit 1\n' > "$fake_bin/wails" +if output="$(GOBIN="$fake_bin" PATH="$fake_bin:$PATH" PROD_TEST_INSTALL_TOOLS=0 PROD_TEST_FAST=1 DESKTOP_BUILD_SKIP_DMG=1 PROD_TEST_OPEN_DIST=0 "$root/prod_test" darwin/arm64 2>&1)"; then + echo "check-wails-pin test: prod_test accepted a broken installed Wails CLI" >&2 + exit 1 +fi +grep -Fq "Wails CLI not found does not match .wails-version $repo_pin" <<<"$output" || { + echo "check-wails-pin test: prod_test did not report the broken installed CLI" >&2 + echo "$output" >&2 + exit 1 +} + +echo "check-wails-pin tests: ok"