From 98c93048d0468de12bf4f5d5dce4b4833348535d Mon Sep 17 00:00:00 2001 From: Jesse Houwing Date: Sun, 18 Oct 2026 11:49:49 +0200 Subject: [PATCH 1/6] Pin test fixtures to LF line endings Prevents core.autocrlf checkouts from breaking byte-exact patch tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jesse Houwing --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..0324a0e29cc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Test fixtures are compared byte-for-byte; keep LF line endings on every platform. +**/testdata/** text=auto eol=lf From c963c949d454aa795170f0974ccaf82cd5f922cd Mon Sep 17 00:00:00 2001 From: Jesse Houwing Date: Sun, 18 Oct 2026 12:04:11 +0200 Subject: [PATCH 2/6] :bug: Fix tests and path handling on Windows - Close archive input files in the tarball extractors and test setups so temp dirs can be removed on Windows. - Treat repo-relative paths as slash-separated (path vs. filepath) in IsWorkflowFile, fileIsInVendorDir and the localdir client. - Close files read in tests before cleanup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jesse Houwing --- checks/fileparser/github_workflow.go | 3 +-- checks/raw/pinned_dependencies.go | 4 ++-- checks/raw/pinned_dependencies_test.go | 2 +- clients/azuredevopsrepo/zip_test.go | 3 +++ clients/githubrepo/tarball.go | 1 + clients/githubrepo/tarball_test.go | 3 +++ clients/gitlabrepo/tarball.go | 1 + clients/gitlabrepo/tarball_test.go | 3 +++ clients/localdir/client.go | 9 +++++---- internal/gitfile/gitfile_test.go | 3 +-- 10 files changed, 21 insertions(+), 11 deletions(-) diff --git a/checks/fileparser/github_workflow.go b/checks/fileparser/github_workflow.go index 0274db6c43a..6a069e92677 100644 --- a/checks/fileparser/github_workflow.go +++ b/checks/fileparser/github_workflow.go @@ -17,7 +17,6 @@ package fileparser import ( "fmt" "path" - "path/filepath" "regexp" "strings" @@ -305,7 +304,7 @@ func IsWorkflowFile(pathfn string) bool { // "Workflow files use YAML syntax, and must have either a .yml or .yaml file extension." switch path.Ext(pathfn) { case ".yml", ".yaml": - return filepath.Dir(strings.ToLower(pathfn)) == ".github/workflows" + return path.Dir(strings.ToLower(pathfn)) == ".github/workflows" default: return false } diff --git a/checks/raw/pinned_dependencies.go b/checks/raw/pinned_dependencies.go index 4bef03213f0..51a0794445f 100644 --- a/checks/raw/pinned_dependencies.go +++ b/checks/raw/pinned_dependencies.go @@ -18,7 +18,7 @@ import ( "bytes" "errors" "fmt" - "path/filepath" + "path" "reflect" "regexp" "strings" @@ -323,7 +323,7 @@ func collectDockerfileInsecureDownloads(c *checker.CheckRequest, r *checker.Pinn } func fileIsInVendorDir(pathfn string) bool { - cleanedPath := filepath.Clean(pathfn) + cleanedPath := path.Clean(pathfn) splitCleanedPath := strings.Split(cleanedPath, "/") for _, d := range splitCleanedPath { diff --git a/checks/raw/pinned_dependencies_test.go b/checks/raw/pinned_dependencies_test.go index 3a506433a48..3c0be8a27af 100644 --- a/checks/raw/pinned_dependencies_test.go +++ b/checks/raw/pinned_dependencies_test.go @@ -475,7 +475,7 @@ func TestDockerfilePinning(t *testing.T) { } var r checker.PinningDependenciesData - _, err = validateDockerfilesPinning(filepath.Join("testdata", tt.filename), content, &r) + _, err = validateDockerfilesPinning(filepath.ToSlash(filepath.Join("testdata", tt.filename)), content, &r) if !errCmp(err, tt.err) { t.Error(cmp.Diff(err, tt.err, cmpopts.EquateErrors())) } diff --git a/clients/azuredevopsrepo/zip_test.go b/clients/azuredevopsrepo/zip_test.go index 6fc956b4393..909fdea10ab 100644 --- a/clients/azuredevopsrepo/zip_test.go +++ b/clients/azuredevopsrepo/zip_test.go @@ -58,6 +58,8 @@ func setup(inputFile string) (zipHandler, error) { if err != nil { return zipHandler{}, fmt.Errorf("unable to open testfile: %w", err) } + defer tempFile.Close() + defer testFile.Close() if _, err := io.Copy(tempFile, testFile); err != nil { return zipHandler{}, fmt.Errorf("unable to do io.Copy: %w", err) } @@ -203,6 +205,7 @@ func TestExtractZip(t *testing.T) { } if getcontenttest.err == nil { content, err := io.ReadAll(f) + f.Close() if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/clients/githubrepo/tarball.go b/clients/githubrepo/tarball.go index b3117bc3f87..67bff4ebd25 100644 --- a/clients/githubrepo/tarball.go +++ b/clients/githubrepo/tarball.go @@ -167,6 +167,7 @@ func (handler *tarballHandler) extractTarball() error { if err != nil { return fmt.Errorf("os.OpenFile: %w", err) } + defer in.Close() gz, err := gzip.NewReader(in) if err != nil { return fmt.Errorf("%w: gzip.NewReader %v %w", errTarballCorrupted, handler.tempTarFile, err) diff --git a/clients/githubrepo/tarball_test.go b/clients/githubrepo/tarball_test.go index c2069c34b03..d36b222209a 100644 --- a/clients/githubrepo/tarball_test.go +++ b/clients/githubrepo/tarball_test.go @@ -63,6 +63,8 @@ func setup(inputFile string) (tarballHandler, error) { if err != nil { return tarballHandler{}, fmt.Errorf("unable to open testfile: %w", err) } + defer tempFile.Close() + defer testFile.Close() if _, err := io.Copy(tempFile, testFile); err != nil { return tarballHandler{}, fmt.Errorf("unable to do io.Copy: %w", err) } @@ -165,6 +167,7 @@ func TestExtractTarball(t *testing.T) { } if getcontenttest.err == nil { content, err := io.ReadAll(f) + f.Close() if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/clients/gitlabrepo/tarball.go b/clients/gitlabrepo/tarball.go index 77ade58724b..299cbea3bc3 100644 --- a/clients/gitlabrepo/tarball.go +++ b/clients/gitlabrepo/tarball.go @@ -210,6 +210,7 @@ func (handler *tarballHandler) extractTarball() error { if err != nil { return fmt.Errorf("os.OpenFile: %w", err) } + defer in.Close() gz, err := gzip.NewReader(in) if err != nil { return fmt.Errorf("%w: gzip.NewReader %v %w", errTarballCorrupted, handler.tempTarFile, err) diff --git a/clients/gitlabrepo/tarball_test.go b/clients/gitlabrepo/tarball_test.go index 21f199950fc..d9b27fd8635 100644 --- a/clients/gitlabrepo/tarball_test.go +++ b/clients/gitlabrepo/tarball_test.go @@ -56,6 +56,8 @@ func setup(inputFile string) (tarballHandler, error) { if err != nil { return tarballHandler{}, fmt.Errorf("unable to open testfile: %w", err) } + defer tempFile.Close() + defer testFile.Close() if _, err := io.Copy(tempFile, testFile); err != nil { return tarballHandler{}, fmt.Errorf("unable to do io.Copy: %w", err) } @@ -158,6 +160,7 @@ func TestExtractTarball(t *testing.T) { } if getcontenttest.err == nil { content, err := io.ReadAll(f) + f.Close() if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/clients/localdir/client.go b/clients/localdir/client.go index c5ecda23fe3..6bc5cd6010e 100644 --- a/clients/localdir/client.go +++ b/clients/localdir/client.go @@ -23,7 +23,6 @@ import ( "io" "io/fs" "os" - "path" "path/filepath" "strings" "sync" @@ -85,9 +84,11 @@ func isDir(p string) (bool, error) { } func trimPrefix(pathfn, clientPath string) string { - cleanPath := path.Clean(pathfn) - prefix := fmt.Sprintf("%s%s", clientPath, string(os.PathSeparator)) - return strings.TrimPrefix(cleanPath, prefix) + rel, err := filepath.Rel(clientPath, pathfn) + if err != nil { + return filepath.ToSlash(filepath.Clean(pathfn)) + } + return filepath.ToSlash(rel) } func listFiles(clientPath string, logger *log.Logger) ([]string, error) { diff --git a/internal/gitfile/gitfile_test.go b/internal/gitfile/gitfile_test.go index bb1ccdcc5be..210e2fc6688 100644 --- a/internal/gitfile/gitfile_test.go +++ b/internal/gitfile/gitfile_test.go @@ -52,9 +52,8 @@ func TestHandler(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - t.Cleanup(func() { r.Close() }) - contents, err := io.ReadAll(r) + r.Close() if err != nil { t.Fatalf("unexpected error: %v", err) } From 25de69db59085f3d9f2099e9f53b8cb2f742d10c Mon Sep 17 00:00:00 2001 From: Jesse Houwing Date: Sun, 18 Oct 2026 13:38:53 +0200 Subject: [PATCH 3/6] Enforce LF line endings repo-wide via .gitattributes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jesse Houwing --- .gitattributes | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 0324a0e29cc..53e9a55bc64 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ -# Test fixtures are compared byte-for-byte; keep LF line endings on every platform. -**/testdata/** text=auto eol=lf +# Keep LF line endings on every platform so Windows checkouts (core.autocrlf=true) don't rewrite files; +# test fixtures are compared byte-for-byte. +* text=auto eol=lf From e9bafa006042c905edcf1515c2a97c6bb05f77d3 Mon Sep 17 00:00:00 2001 From: Jesse Houwing Date: Sun, 18 Oct 2026 14:22:16 +0200 Subject: [PATCH 4/6] :bug: Build valid file URLs for schema paths on Windows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jesse Houwing --- pkg/scorecard/json_test.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/scorecard/json_test.go b/pkg/scorecard/json_test.go index c98bcf41c09..eee83b874b8 100644 --- a/pkg/scorecard/json_test.go +++ b/pkg/scorecard/json_test.go @@ -19,7 +19,7 @@ import ( "encoding/json" "fmt" "os" - "path" + "path/filepath" "strings" "testing" "time" @@ -473,7 +473,7 @@ func TestJSONOutput(t *testing.T) { if err != nil { t.Fatalf("os.Getwd: %s", err) } - schemaLoader := gojsonschema.NewReferenceLoader(fmt.Sprintf("file://%s", path.Join(cwd, "json.v2.schema"))) + schemaLoader := gojsonschema.NewReferenceLoader(fileURL(filepath.Join(cwd, "json.v2.schema"))) schema, err := gojsonschema.NewSchema(schemaLoader) if err != nil { t.Fatalf("gojsonschema.NewSchema: %s", err) @@ -531,7 +531,7 @@ func TestJSONOutput(t *testing.T) { } // Validate schema. - docLoader := gojsonschema.NewReferenceLoader(fmt.Sprintf("file://%s", path.Join(cwd, tt.expected))) + docLoader := gojsonschema.NewReferenceLoader(fileURL(filepath.Join(cwd, tt.expected))) rr, err := schema.Validate(docLoader) if err != nil { t.Fatalf("%s: Validate error: %s", tt.name, err.Error()) @@ -586,3 +586,12 @@ func TestExperimentalFromJSON2_time(t *testing.T) { }) } } + +// fileURL builds a valid file URL on every OS (Windows paths need a leading slash). +func fileURL(p string) string { + p = filepath.ToSlash(p) + if !strings.HasPrefix(p, "/") { + p = "/" + p + } + return "file://" + p +} From 4f0912f63daac2b40b387be7d430f557c0c3c28f Mon Sep 17 00:00:00 2001 From: Jesse Houwing Date: Sun, 18 Oct 2026 13:13:23 +0200 Subject: [PATCH 5/6] :seedling: Run unit tests on Windows in CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jesse Houwing --- .github/workflows/main.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3abe6998714..c4dadf903d6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -83,6 +83,30 @@ jobs: with: files: "*e2e-coverage.out" verbose: true + unit-test-windows: + # Repo paths are slash-separated and Windows cannot delete open files, so this catches bugs that + # are invisible on Linux. `make` is not guaranteed on the Windows image, so run go test directly. + name: unit-test (windows) + runs-on: windows-latest + permissions: + contents: read + steps: + - name: Harden Runner + uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 + with: + egress-policy: audit # Windows runners support audit mode only + - name: Clone the code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: ${{ env.GO_VERSION }} + check-latest: true + - name: Run unit-tests + shell: bash + env: + SKIP_GINKGO: 1 + run: go test -race -covermode=atomic $(go list ./...) generate-mocks: name: generate-mocks runs-on: ubuntu-latest From 9f30bfcee2c0cf6e70326fcc34243988fad055d5 Mon Sep 17 00:00:00 2001 From: Jesse Houwing Date: Sun, 18 Oct 2026 13:47:32 +0200 Subject: [PATCH 6/6] :seedling: Disable autocrlf before checkout in Windows CI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jesse Houwing --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c4dadf903d6..f20cb4bf98a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -95,6 +95,11 @@ jobs: uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1 with: egress-policy: audit # Windows runners support audit mode only + - name: Keep LF line endings + # Must run before checkout: test fixtures are compared byte-for-byte. + run: | + git config --global core.autocrlf false + git config --global core.eol lf - name: Clone the code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Go