From db3496d8024c93bc3431dbb27b1a8b092cb90d33 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:59:12 +0200 Subject: [PATCH] chore: upgrade Go/deps/workflows to latest and fix gosec regressions (#193) --- .github/workflows/ci.yml | 5 ++++- .github/workflows/commitlint.yml | 2 +- .go-version | 2 +- .golangci.yml | 2 +- go.mod | 14 ++++++-------- go.sum | 22 ++++++++++------------ internal/apperrors/errors.go | 6 +++--- internal/config_test.go | 2 +- internal/dependencies/analyzer.go | 26 +++++++++++++++++++++----- internal/generator.go | 6 +++--- internal/wizard/wizard_test.go | 2 +- main_test.go | 2 +- testutil/testutil.go | 14 ++++++-------- testutil/testutil_test.go | 6 ++++-- 14 files changed, 63 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cd89f1..8032da6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,10 +19,13 @@ jobs: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version-file: "go.mod" + check-latest: true - name: Run golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: - version: v2.7.2 + version: v2.11.3 - name: Setup Node.js for EditorConfig tools uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 4de32b5..96dbba1 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -30,7 +30,7 @@ jobs: - name: Install commitlint run: | - npm install --save-dev @commitlint/cli@19.6.1 @commitlint/config-conventional@19.6.0 + npm install --save-dev @commitlint/cli@20.4.3 @commitlint/config-conventional@20.4.3 - name: Validate current commit (for single commits) if: github.event_name == 'push' diff --git a/.go-version b/.go-version index b45fe31..dd43a14 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.25.5 +1.26.1 diff --git a/.golangci.yml b/.golangci.yml index 364bb2f..5b890dd 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,7 +18,7 @@ version: "2" run: timeout: 5m - go: "1.24" + go: "1.26" linters: default: standard diff --git a/go.mod b/go.mod index 50425f8..669b855 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/ivuorinen/gh-action-readme -go 1.24.0 - -toolchain go1.26.1 +go 1.26.1 require ( github.com/adrg/xdg v0.5.3 @@ -14,12 +12,12 @@ require ( github.com/schollz/progressbar/v3 v3.19.0 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 - golang.org/x/oauth2 v0.35.0 + golang.org/x/oauth2 v0.36.0 ) require ( github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/go-querystring v1.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -33,8 +31,8 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index 686c5a2..08b0cda 100644 --- a/go.sum +++ b/go.sum @@ -83,8 +83,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -410,10 +410,8 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -475,14 +473,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -493,8 +491,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/apperrors/errors.go b/internal/apperrors/errors.go index ff38e78..f4a8d8d 100644 --- a/internal/apperrors/errors.go +++ b/internal/apperrors/errors.go @@ -39,19 +39,19 @@ func (ce *ContextualError) Error() string { // Primary error message if ce.Context != "" { - b.WriteString(fmt.Sprintf("%s: %v", ce.Context, ce.Err)) + fmt.Fprintf(&b, "%s: %v", ce.Context, ce.Err) } else { b.WriteString(ce.Err.Error()) } // Add error code for reference - b.WriteString(fmt.Sprintf(" [%s]", ce.Code)) + fmt.Fprintf(&b, " [%s]", ce.Code) // Add details if available if len(ce.Details) > 0 { b.WriteString("\n\nDetails:") for key, value := range ce.Details { - b.WriteString(fmt.Sprintf("\n %s: %s", key, value)) + fmt.Fprintf(&b, "\n %s: %s", key, value) } } diff --git a/internal/config_test.go b/internal/config_test.go index 45d5dc7..43a0d07 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -936,7 +936,7 @@ func TestNewGitHubClientEdgeCases(t *testing.T) { expectError: false, description: "Should create client with valid classic token", }, - { + { // #nosec G101 -- test token, not a real credential name: "valid fine-grained PAT", token: "github_pat_11AAAAAA0AAAAaAaaAaaaAaa_AaAAaAAaAAAaAAAAAaAAaAAaAaAAaAAAAaAAAAAAAAaAAaAAaAaaAA", expectError: false, diff --git a/internal/dependencies/analyzer.go b/internal/dependencies/analyzer.go index 429ee71..09b9e09 100644 --- a/internal/dependencies/analyzer.go +++ b/internal/dependencies/analyzer.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "os" + "path/filepath" "regexp" "strings" "time" @@ -591,15 +592,26 @@ func (a *Analyzer) determineUpdateType(currentParts, latestParts []string) strin // updateActionFile applies updates to a single action file. func (a *Analyzer) updateActionFile(filePath string, updates []PinnedUpdate) error { + // filepath.Clean normalises the path (removes redundant separators, ".", ".."). + // It does NOT validate containment within a root directory; the actual security + // justification for the #nosec annotations below is that filePath originates + // from the tool's own filesystem discovery (DiscoverActionFilesWithValidation), + // not from direct, uncontrolled user input. + cleanPath := filepath.Clean(filePath) + // Read the file - content, err := os.ReadFile(filePath) // #nosec G304 -- file path from function parameter + content, err := os.ReadFile(cleanPath) // #nosec G304 -- path from tool-internal filesystem scan if err != nil { return fmt.Errorf("failed to read file: %w", err) } // Create backup - backupPath := filePath + appconstants.BackupExtension - if err := os.WriteFile(backupPath, content, appconstants.FilePermDefault); err != nil { // #nosec G306 + backupPath := cleanPath + appconstants.BackupExtension + if err := os.WriteFile( // #nosec G306 G703 -- path from tool-internal filesystem scan + backupPath, + content, + appconstants.FilePermDefault, + ); err != nil { return fmt.Errorf("failed to create backup: %w", err) } @@ -609,12 +621,16 @@ func (a *Analyzer) updateActionFile(filePath string, updates []PinnedUpdate) err // Write updated content updatedContent := strings.Join(lines, "\n") - if err := os.WriteFile(filePath, []byte(updatedContent), appconstants.FilePermDefault); err != nil { // #nosec G306 + if err := os.WriteFile( // #nosec G306 G703 -- path from tool-internal filesystem scan + cleanPath, + []byte(updatedContent), + appconstants.FilePermDefault, + ); err != nil { return fmt.Errorf("failed to write updated file: %w", err) } // Validate and rollback on failure - if err := a.validateAndRollbackOnFailure(filePath, backupPath); err != nil { + if err := a.validateAndRollbackOnFailure(cleanPath, backupPath); err != nil { return err } diff --git a/internal/generator.go b/internal/generator.go index 692dc85..2b92c66 100644 --- a/internal/generator.go +++ b/internal/generator.go @@ -601,7 +601,7 @@ func (g *Generator) countValidationStats(results []ValidationResult) (validFiles // showValidationSummary displays the summary statistics. func (g *Generator) showValidationSummary(totalFiles, validFiles, totalIssues, resultCount, errorCount int) { g.Output.Bold("\nValidation Summary for %d files:", totalFiles) - g.Output.Printf("=" + strings.Repeat("=", 35) + "\n") + g.Output.Printf("%s", "="+strings.Repeat("=", 35)+"\n") g.Output.Success("Valid files: %d", validFiles) if resultCount-validFiles > 0 { @@ -622,7 +622,7 @@ func (g *Generator) showDetailedIssues(results []ValidationResult, totalIssues i } g.Output.Bold("\nDetailed Issues & Suggestions:") - g.Output.Printf("-" + strings.Repeat("-", 35) + "\n") + g.Output.Printf("%s", "-"+strings.Repeat("-", 35)+"\n") for _, result := range results { if len(result.MissingFields) > 1 || len(result.Warnings) > 0 { @@ -663,7 +663,7 @@ func (g *Generator) showParseErrors(errors []string) { } g.Output.Bold("\nParse Errors:") - g.Output.Printf("-" + strings.Repeat("-", 15) + "\n") + g.Output.Printf("%s", "-"+strings.Repeat("-", 15)+"\n") for _, errMsg := range errors { g.Output.Error(" - %s", errMsg) } diff --git a/internal/wizard/wizard_test.go b/internal/wizard/wizard_test.go index f2fc708..38c9c8f 100644 --- a/internal/wizard/wizard_test.go +++ b/internal/wizard/wizard_test.go @@ -644,7 +644,7 @@ func TestConfigureGitHubIntegration(t *testing.T) { wantTokenSet: false, wantTokenValue: "", }, - { + { // #nosec G101 -- test token, not a real credential name: "existing token skips setup", inputs: "", existingToken: "ghp_existing_token", diff --git a/main_test.go b/main_test.go index 72c6240..754099f 100644 --- a/main_test.go +++ b/main_test.go @@ -1005,7 +1005,7 @@ func TestValidateGitHubToken(t *testing.T) { token string want bool }{ - { + { // #nosec G101 -- test token, not a real credential name: "with valid token", token: "ghp_test_token_123", want: true, diff --git a/testutil/testutil.go b/testutil/testutil.go index 4512f84..994ff85 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -162,7 +162,7 @@ func WriteTestFile(t *testing.T, path, content string) { t.Fatalf("failed to create dir %s: %v", dir, err) } - // #nosec G306 -- test file permissions + // #nosec G306 G703 -- test file permissions, path is controlled by test infrastructure if err := os.WriteFile(path, []byte(content), appconstants.FilePermDefault); err != nil { t.Fatalf("failed to write test file %s: %v", path, err) } @@ -396,7 +396,7 @@ func AssertFileNotExists(t *testing.T, path string) { func CreateTestAction(name, description string, inputs map[string]string) string { var inputsYAML bytes.Buffer for key, desc := range inputs { - inputsYAML.WriteString(fmt.Sprintf(" %s:\n description: %s\n required: true\n", key, desc)) + fmt.Fprintf(&inputsYAML, " %s:\n description: %s\n required: true\n", key, desc) } result := fmt.Sprintf(appconstants.YAMLFieldName, name) @@ -445,7 +445,7 @@ func SetupTestTemplates(t *testing.T, dir string) { func CreateCompositeAction(name, description string, steps []string) string { var stepsYAML bytes.Buffer for i, step := range steps { - stepsYAML.WriteString(fmt.Sprintf(" - name: Step %d\n uses: %s\n", i+1, step)) + fmt.Fprintf(&stepsYAML, " - name: Step %d\n uses: %s\n", i+1, step) } result := fmt.Sprintf(appconstants.YAMLFieldName, name) @@ -521,11 +521,9 @@ func SetEnv(t *testing.T, key, value string) func() { } // WithContext creates a context with timeout for testing. -func WithContext(timeout time.Duration) context.Context { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - _ = cancel // Avoid lostcancel - we're intentionally creating a context without cleanup for testing - - return ctx +// The caller is responsible for calling the returned cancel function. +func WithContext(timeout time.Duration) (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), timeout) } // AssertNoError fails the test if err is not nil. diff --git a/testutil/testutil_test.go b/testutil/testutil_test.go index 7b0f073..cdf2bd9 100644 --- a/testutil/testutil_test.go +++ b/testutil/testutil_test.go @@ -697,7 +697,8 @@ func TestWithContext(t *testing.T) { t.Run("creates context with timeout", func(t *testing.T) { t.Parallel() timeout := 100 * time.Millisecond - ctx := WithContext(timeout) + ctx, cancel := WithContext(timeout) + defer cancel() if ctx == nil { t.Fatal("expected context to be created") @@ -719,7 +720,8 @@ func TestWithContext(t *testing.T) { t.Run("context eventually times out", func(t *testing.T) { t.Parallel() - ctx := WithContext(1 * time.Millisecond) + ctx, cancel := WithContext(1 * time.Millisecond) + defer cancel() // Wait a bit longer than the timeout time.Sleep(10 * time.Millisecond)