package main import ( "fmt" "os" "path/filepath" "runtime" "github.com/goccy/go-yaml" . "github.com/anchore/go-make" "github.com/anchore/go-make/file" "github.com/anchore/go-make/git" "github.com/anchore/go-make/lang" "github.com/anchore/go-make/run" "github.com/anchore/go-make/tasks/golint" "github.com/anchore/go-make/tasks/goreleaser" "github.com/anchore/go-make/tasks/gotest" ) // taskfileDescriptions maps Taskfile.yaml task names to their `desc:` field. // Loaded at package init so wrap() can use Taskfile.yaml as the single source // of truth for wrapped-task descriptions. var taskfileDescriptions = mustReadTaskfileDescriptions() func main() { Makefile( // shared anchore tasks golint.Tasks(), goreleaser.Tasks(), // unit tests: exclude packages under any test/ directory (matches the syft // Taskfile's prior `grep -v` against test paths). Coverage threshold of 62% // preserves the prior coverage gate that used to live in scripts/coverage.py. gotest.Tasks( gotest.Name("unit"), gotest.ExcludeGlob("**/test/**"), gotest.CoverageThreshold(62), ), // integration tests: native go-make Task. The race-detector smoke against a // real image stays bundled here (RunsOn integration) so `make integration` // behaves like the Taskfile version did. gotest.Tasks( gotest.Name("integration"), gotest.IncludeGlob("./cmd/syft/internal/test/integration/..."), gotest.Verbose(), gotest.NoCoverage(), ), Task{ Name: "integration:race-smoke", Description: "exercise the CLI with the race detector", RunsOn: lang.List("integration"), Run: func() { Run("go run -race cmd/syft/main.go anchore/test_images:grype-quality-dotnet-69f15d2") }, }, // cli tests: native go-make Task. Requires SYFT_BINARY_LOCATION pointing at // an *absolute* path to the snapshot binary. Intentionally does NOT depend // on snapshot: in CI we download a pre-built snapshot artifact and re-running // goreleaser here would both burn ~10m and clobber the downloaded binary. // Locally, the failure message tells you to run `make snapshot` first. Task{ Name: "cli", Description: "Run CLI tests", RunsOn: lang.List("test"), Run: func() { bin := snapshotBinPath() if !file.Exists(bin) { panic(fmt.Sprintf("snapshot binary not found at %s; run `make snapshot` first", bin)) } Log("testing binary: %s", bin) Run( "go test -count=1 -timeout=15m -v ./test/cli", run.Env("SYFT_BINARY_LOCATION", bin), ) }, }, // default validation pipeline (replaces Taskfile `default`/`pr-validations`/`validations`). Task{ Name: "default", Description: "Run all validation tasks", Dependencies: Deps("static-analysis", "test", "install-test"), }, // --- everything below is implemented in Taskfile.yaml and surfaced here // via wrap(). Descriptions come from Taskfile.yaml (single source of truth). // static analysis extras wrap("check-json-schema-drift").RunOn("static-analysis"), wrap("check-capability-drift"), wrap("check-binary-fixture-size").RunOn("static-analysis"), // test extras wrap("validate-cyclonedx-schema").RunOn("test"), wrap("test-utils").RunOn("test"), wrap("check-docker-cache").RunOn("test"), wrap("snapshot-smoke-test"), // update commands wrap("update-format-golden-files"), // fixture cache plumbing (heavy ORAS logic, lives in Taskfile). // refresh-fixtures hooks into "unit" so `make unit` triggers the // stale-cache detection + download just like `task unit` did on main // (its `deps: [tmpdir, fixtures]` is what kept the fixture cache fresh). wrap("fingerprints"), wrap("refresh-fixtures").RunOn("unit"), wrap("fixtures"), wrap("build-fixtures"), wrap("download-test-fixture-cache"), wrap("upload-test-fixture-cache"), wrap("show-test-image-cache"), // install-script tests (delegates to test/install/Makefile) wrap("install-test"), wrap("install-test-cache-save"), wrap("install-test-cache-load"), wrap("install-test-ci-mac"), // compare tests wrap("generate-compare-file"), wrap("compare-mac"), wrap("compare-linux"), wrap("compare-test-deb-package-install"), wrap("compare-test-rpm-package-install"), // code/data generation (umbrella + per-target; each lives in Taskfile) wrap("generate"), wrap("generate-json-schema"), wrap("generate-license-list"), wrap("generate-cpe-dictionary-index"), wrap("generate-capabilities"), // cleanup (each hooks into go-make's built-in `clean` label) wrap("clean-snapshot").RunOn("clean"), wrap("clean-docker-cache").RunOn("clean"), wrap("clean-oras-cache").RunOn("clean"), wrap("clean-cache").RunOn("clean"), wrap("clean-test-observations").RunOn("clean"), ) } // wrap creates a go-make Task that delegates execution to `task `. The // task's description is pulled from Taskfile.yaml's `desc:` field — descriptions // for wrapped tasks must always live in Taskfile.yaml, never here. func wrap(name string) Task { desc, ok := taskfileDescriptions[name] if !ok || desc == "" { // loud-fail at startup so missing descs can't sneak through review. panic(fmt.Sprintf("Taskfile.yaml task %q is missing a `desc:` field; please add one", name)) } return Task{ Name: name, Description: desc, Run: func() { Run("task " + name) }, } } // mustReadTaskfileDescriptions parses Taskfile.yaml at the repo root and returns // a map of task name -> desc. Runs at package init time so wrap() can use it. func mustReadTaskfileDescriptions() map[string]string { root := git.Root() if root == "" { return nil } path := filepath.Join(root, "Taskfile.yaml") data, err := os.ReadFile(path) //nolint:gosec // G304: path resolved from git.Root() if err != nil { return nil } var tf struct { Tasks map[string]struct { Desc string `yaml:"desc"` Aliases []string `yaml:"aliases"` } `yaml:"tasks"` } lang.Throw(yaml.Unmarshal(data, &tf)) out := make(map[string]string, len(tf.Tasks)) for name, t := range tf.Tasks { out[name] = t.Desc // aliases inherit the canonical task's description so wrap() can find them. for _, alias := range t.Aliases { out[alias] = t.Desc } } return out } // snapshotBinPath replicates the SNAPSHOT_BIN computation from the prior Taskfile: // /snapshot/-build__/syft, where arch maps amd64->amd64_v1 // and arm64->arm64_v8.0 to match goreleaser's per-target output directory naming. // Returns an absolute path: the cli tests' getSyftBinaryLocation contract requires // SYFT_BINARY_LOCATION to be absolute because subtests run with cmd.Dir = t.TempDir(). func snapshotBinPath() string { osName := runtime.GOOS var arch string switch runtime.GOARCH { case "amd64": arch = "amd64_v1" case "arm64": arch = "arm64_v8.0" default: arch = runtime.GOARCH } return filepath.Join(RootDir(), "snapshot", osName+"-build_"+osName+"_"+arch, "syft") }