diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a65936c..db12004 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,6 +37,6 @@ jobs: contents: write id-token: write attestations: write - uses: simtabi/.github/.github/workflows/go-release.yml@745880bdb9b35a21eb16824cfc995f16e1e82fd5 # simtabi/.github#27 + uses: simtabi/.github/.github/workflows/go-release.yml@d648d8998f95a351001979f9c4527badca588bf1 # simtabi/.github#30 with: tag: ${{ inputs.tag || github.ref_name }} diff --git a/CHANGELOG.md b/CHANGELOG.md index cc2cc68..5bf0adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- `release.yml` calls simtabi/.github's shared `go-release.yml` at `d648d89` (simtabi/.github#30), its latest commit, which adds an optional `node-version` input and changes nothing a caller passes. +- `make check-build` asserts how many files it inspected in `build/`, not only that nothing is out of place: `make build` passes a floor of 2 (`targets.txt` and the binary) and `make cross` and `make dist` one per target plus `targets.txt` and `checksums.txt`, through `BUILD_LAYOUT_MIN`, so a check that reads an empty or wrong directory fails instead of passing. - The Go module moved into `src/` and is now `github.com/simtabi/osaat/src`. `go.mod`, `cmd/`, `internal/`, `.goreleaser.yaml` and the build and release scripts live under `src/`; `scripts/bash-fallback.sh` stays at the root so its published URL keeps working. A release now diff --git a/Makefile b/Makefile index d20648a..72a82cf 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,12 @@ HOST_OS := $(if $(filter darwin,$(HOST_GOOS)),macos,$(HOST_GOOS)) EXE := $(if $(filter windows,$(HOST_GOOS)),.exe,) BIN := build/$(HOST_OS)/$(BINARY)$(EXE) +# The fewest entries `make check-build` must inspect in build/: the files at its +# root and in each OS folder. Each build passes what it just wrote (the host +# build: targets.txt and the binary; cross: targets.txt, checksums.txt and one +# binary per target), so a check reading an empty or wrong directory fails. +BUILD_LAYOUT_MIN ?= 1 + .PHONY: help build build-local install test test-race lint vet fmt tidy cross dist check-build snapshot run clean help: ## Show this help @@ -40,7 +46,7 @@ build: build-local ## Build build//osaat (the same as build-local) build-local: ## Build build//osaat with the host's Go toolchain CGO_ENABLED=0 go -C src build -trimpath -ldflags '$(LDFLAGS)' -o ../$(BIN) $(CMD) - @$(MAKE) --no-print-directory check-build + @$(MAKE) --no-print-directory check-build BUILD_LAYOUT_MIN=2 BINDIR ?= $(HOME)/.local/bin @@ -70,14 +76,14 @@ lint: ## Run golangci-lint (must be installed) cross: ## Cross-compile every target in build/targets.txt into build// ARCHIVES=0 VERSION=$(VERSION) COMMIT=$(COMMIT) DATE=$(DATE) ./src/scripts/build-all.sh - @$(MAKE) --no-print-directory check-build + @$(MAKE) --no-print-directory check-build BUILD_LAYOUT_MIN=$$(( $$(grep -cvE '^[[:space:]]*(#|$$)' build/targets.txt) + 2 )) dist: ## cross, plus .tar.gz/.zip archives in build/archives/ (publishes nothing) ARCHIVES=1 VERSION=$(VERSION) COMMIT=$(COMMIT) DATE=$(DATE) ./src/scripts/build-all.sh - @$(MAKE) --no-print-directory check-build + @$(MAKE) --no-print-directory check-build BUILD_LAYOUT_MIN=$$(( $$(grep -cvE '^[[:space:]]*(#|$$)' build/targets.txt) + 2 )) check-build: ## Fail when build/ holds anything but the OS folders, inputs, checksums.txt and archives/ - go -C src test ./internal/release -run 'TestBuildLayout$$' -count=1 + BUILD_LAYOUT_MIN=$(BUILD_LAYOUT_MIN) go -C src test ./internal/release -run 'TestBuildLayout$$' -count=1 snapshot: ## Build the release artifacts into .cache/goreleaser/ without publishing (requires goreleaser) @command -v goreleaser >/dev/null 2>&1 || { echo "goreleaser not found — install from https://goreleaser.com"; exit 1; } diff --git a/src/internal/release/workflow_test.go b/src/internal/release/workflow_test.go index 2b55b4b..1a5d079 100644 --- a/src/internal/release/workflow_test.go +++ b/src/internal/release/workflow_test.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "regexp" + "strconv" "strings" "testing" ) @@ -115,6 +116,75 @@ func TestBuildLayout(t *testing.T) { if problems := buildLayoutProblems(t, dir); len(problems) > 0 { t.Errorf("build/ breaks the layout (one folder per OS, see the Makefile):\n %s", strings.Join(problems, "\n ")) } + n, floor := buildEntries(t, dir), buildFloor(t) + if n < floor { + t.Errorf("inspected %d entries in build/, expected at least %d (BUILD_LAYOUT_MIN): the check read less than the build wrote", n, floor) + } + t.Logf("inspected %d entries in build/ (floor %d)", n, floor) +} + +// buildFloor is the fewest entries build/ must hold: BUILD_LAYOUT_MIN, which the +// Makefile sets to what the build that just ran wrote, else 1 (targets.txt). +func buildFloor(t *testing.T) int { + t.Helper() + raw := os.Getenv("BUILD_LAYOUT_MIN") + if raw == "" { + return 1 + } + n, err := strconv.Atoi(raw) + if err != nil || n < 1 { + t.Fatalf("BUILD_LAYOUT_MIN=%q is not a positive number", raw) + } + return n +} + +// buildEntries counts what the layout check inspected: the files at the root of +// build/ and the files in each OS folder. Folders themselves do not count. +func buildEntries(t *testing.T, dir string) int { + t.Helper() + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("reading %s: %v", dir, err) + } + n := 0 + for _, e := range entries { + switch { + case e.Name() == ".DS_Store": + case !e.IsDir(): + n++ + case osFolders[e.Name()]: + inner, err := os.ReadDir(filepath.Join(dir, e.Name())) + if err != nil { + t.Fatal(err) + } + for _, f := range inner { + if !f.IsDir() && f.Name() != ".DS_Store" { + n++ + } + } + } + } + return n +} + +// The count is what the floor stands on, so it has to count what is there. +func TestBuildEntriesCountsWhatItInspected(t *testing.T) { + dir := t.TempDir() + for _, f := range []string{ + "targets.txt", "checksums.txt", ".DS_Store", + "macos/app_macos_arm64", "linux/app_linux_amd64", "archives/app_linux_amd64.tar.gz", + } { + p := filepath.Join(dir, filepath.FromSlash(f)) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, nil, 0o644); err != nil { + t.Fatal(err) + } + } + if got := buildEntries(t, dir); got != 5 { + t.Errorf("buildEntries counted %d, want 5", got) + } } // The layout check has to be able to fail: each of these trees breaks it.