From fc0938725a3525d23ad3b740f142711339360ca2 Mon Sep 17 00:00:00 2001 From: Chris Eberle Date: Thu, 23 Jul 2026 11:00:01 -0700 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20un-break=20the=20build=20=E2=80=94?= =?UTF-8?q?=20Go=201.26,=20golangci-lint=20v2,=20dep=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bump Go to 1.26.4 and update all dependencies - migrate .golangci.yml to golangci-lint v2 config and fix the resulting lint errors across the codebase - update CI workflows: actions/checkout v4, actions/setup-go v5 (go-version-file: go.mod), golangci-lint-action v8 with golangci-lint v2.9.0 - add CLAUDE.md Co-Authored-By: Claude Fable 5 --- .github/workflows/build.yml | 6 +- .github/workflows/linter.yml | 10 +-- .golangci.yml | 116 ++++++++++++++++++++++------------- CLAUDE.md | 109 ++++++++++++++++++++++++++++++++ appdata/appdata.go | 15 +++-- clerk/clerk.go | 14 +++-- cmd/delete_installation.go | 2 +- cmd/deploy.go | 19 +++--- cmd/deploy_destination.go | 25 +++++--- cmd/init.go | 33 ++++++---- cmd/list_destinations.go | 1 + cmd/list_installations.go | 2 +- cmd/listen.go | 27 +++++--- cmd/login.go | 26 +++++--- cmd/logout.go | 3 +- cmd/my_info.go | 7 ++- cmd/sync_ngrok.go | 22 ++++--- cmd/trigger.go | 23 ++++--- files/manifest.go | 42 ++++++++----- files/zip.go | 8 ++- flags/config.go | 22 +++++-- go.mod | 27 ++++---- go.sum | 59 +++++++++--------- internal/webhook/webhook.go | 7 ++- request/api.go | 6 +- request/request.go | 10 ++- utils/utils.go | 24 +++++--- 27 files changed, 461 insertions(+), 204 deletions(-) create mode 100644 CLAUDE.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1aea405..928558c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,12 +5,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: ">=1.24.3" + go-version-file: go.mod cache: false - name: Build diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index fe15bbb..b099737 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -5,13 +5,13 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: ">=1.24.3" + go-version-file: go.mod cache: false - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v8 continue-on-error: false with: - version: v1.64.3 + version: v2.9.0 diff --git a/.golangci.yml b/.golangci.yml index bd0ee36..6e3e8a3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,11 +1,10 @@ -run: - timeout: 10m +version: "2" output: formats: - - format: colored-line-number + text: path: stdout linters: - enable-all: true + default: all disable: # Disabled because we use inits all over the place - gochecknoinits @@ -37,45 +36,76 @@ linters: - maintidx # We have legitimate use cases for returning an interface - ireturn - # deprecated - - tenv # Sometimes it's useful to return nil, nil - nilnil -linters-settings: - # See https://golangci-lint.run/usage/linters#revive - revive: + # Deprecated alias of wsl_v5; keep it off so only wsl_v5 runs (avoids a deprecation warning). + - wsl + settings: + ireturn: + allow: + - error + - generic + - stdlib + - empty + misspell: + mode: restricted + locale: US + # See https://golangci-lint.run/usage/linters#revive + revive: + rules: + # Use var-naming, but add a special rule to accept "Id" rather than forcing "ID" + - name: var-naming + arguments: + - - ID + - API + disabled: false + # v2 merged stylecheck into staticcheck; keep "all" checks but drop ST1003 (var-naming), matching the prior config. + staticcheck: + checks: + - all + - -ST1003 + varnamelen: + # The longest distance, in source lines, that is being considered a "small scope" where it is ok to use short variable names. + # The default is 5, which is quite strict. + max-distance: 15 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling rules: - # Use var-naming, but add a special rule to accept "Id" rather than forcing "ID" - - name: var-naming - disabled: false - arguments: - - ["ID", "API"] - # See https://staticcheck.io/docs/configuration/options/#checks - staticcheck: - checks: ["all"] - stylecheck: - checks: ["all", "-ST1003"] - varnamelen: - # The longest distance, in source lines, that is being considered a "small scope" where it is ok to use short variable names. - # The default is 5, which is quite strict. - max-distance: 15 - ireturn: - allow: - - error - - generic - - stdlib - - empty - misspell: - locale: US - mode: restricted -issues: - exclude-rules: - - text: "don't use ALL_CAPS in Go names" - linters: - - revive - exclude-dirs: - - openapi - - scripts - - tmp - - playground - - testrun + - linters: + - revive + text: don't use ALL_CAPS in Go names + # `utils` is an established package name here; v2's revive flags it as "meaningless". + - linters: + - revive + text: avoid meaningless package names + paths: + - openapi + - scripts + - tmp + - playground + - testrun + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + exclusions: + generated: lax + paths: + - openapi + - scripts + - tmp + - playground + - testrun + - third_party$ + - builtin$ + - examples$ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5061f3b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,109 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +`amp` — the Ampersand CLI (Go, Cobra-based). It talks to the Ampersand API to manage +integrations, installations, connections, destinations, and projects, and provides local +webhook development tooling. User-facing docs: https://docs.withampersand.com/cli/overview + +## Build / lint / test + +Building requires [Task](https://taskfile.dev). The build bakes stage-specific config +(API URL, Clerk URL, login URL, version metadata) into the binary via `-ldflags -X`, so +you build *per stage* rather than with a plain `go build`. + +```sh +task build # alias for build-dev → produces bin/amp +task build-local # points API_URL at http://127.0.0.1:8080 +task build-staging +task build-prod +``` + +For local dev the README suggests symlinking `bin/amp` to `lamp` ("local amp") so it +coexists with an installed production `amp`. + +```sh +task lint # golangci-lint (config .golangci.yml) +task fix # wsl + gci + golangci-lint --fix (needs gci, wsl installed) +make fix-files FILES="cmd/a.go cmd/b.go" # format/vet specific files only + +go test ./... # run tests (only files/ has tests currently) +go test ./files -run TestGetRemovedReadObjects # run a single test +``` + +There is no `task test`; use `go test` directly. Linting is strict (`enable-all` minus a +curated disable list in `.golangci.yml`); the `openapi/` directory is excluded from lint. +Note revive is configured to accept `Id`/`Api` (not just `ID`/`API`). + +## Code generation + +`openapi/*.gen.go` are generated types (via `oapi-codegen`) pulled from remote specs in the +`amp-labs/openapi` repo — the manifest, problem, and catalog schemas. Regenerate with: + +```sh +cd openapi && make gen +``` + +Do not hand-edit the `.gen.go` files. The `Manifest`, `Integration`, `CatalogType`, etc. +types used throughout the code live in this generated `openapi` package. + +## Architecture + +**Command wiring (Cobra + Viper).** `main.go` → `cmd.Execute()` → `rootCmd` (in +`cmd/root.go`). Each command lives in its own file under `cmd/` and self-registers via an +`init()` that calls `rootCmd.AddCommand(...)`. Global persistent flags (`--debug/-d`, +`--project/-p`, `--key/-k`) are set up in `flags/config.go` (`flags.Init`) and bound to +Viper; `--key` also reads the `AMP_API_KEY` env var. Use the `flags` package helpers +(`GetProjectOrFail`, `GetAPIKey`, `GetOutputFormat`, `GetDebugMode`) rather than reading +Viper directly. + +**Stage configuration is build-time.** The `vars` package holds `ApiURL`, `ClerkRootURL`, +`LoginURL`, `Stage`, `Version`, etc., all defaulting to `"unset"` and overwritten at build +time by ldflags (see `Taskfile.yaml`). At runtime these can be overridden by env vars for +testing: `AMP_API_URL`, `AMP_CLERK_URL_OVERRIDE`, `AMP_STAGE_OVERRIDE`, `AMP_API_KEY`. + +**Authentication (two modes).** `request.APIClient.getAuthHeader` decides: +1. If an API key is present (`--key` / `AMP_API_KEY`) → sends `X-Api-Key`. +2. Otherwise → uses a Clerk JWT browser-login session. `amp login` performs the Clerk OAuth + flow and writes session data to the XDG config dir (`amp/jwt.json`, or + `amp/jwt-.json` for non-prod). The `clerk` package exchanges that stored session + for a fresh JWT on each request (`FetchJwt`). + +**API layer (`request/`).** `request.go` is the low-level HTTP client (`Client`) that adds +`X-Amp-Client*` headers, marshals/unmarshals JSON, and parses error responses. Non-2xx +responses with `application/problem+json` are decoded into `ProblemError` (RFC 7807). +`api.go` defines `APIClient` — one method per API endpoint (`ListIntegrations`, +`BatchUpsertIntegrations`, `GetPreSignedUploadURL`, etc.), each building a URL under +`{ApiURL}/v1/projects/{projectId}/...` and attaching the auth header. Response DTOs are in +`request/types.go`. + +**Deploy flow (`amp deploy`).** The most involved path, worth understanding end to end: +`files.Zip` locates and validates an `amp.yaml`/`amp.yml` manifest (parsed into the +generated `openapi.Manifest`, validated by `files/manifest.go` against `specVersion 1.0.0`), +zips it in memory → compute MD5 → `GetPreSignedUploadURL` → `storage.Upload` PUTs the zip to +GCS → `BatchUpsertIntegrations` is called with the resulting `gs://` URL. Before deploying, +`confirmReadObjectRemoval` compares the new manifest against existing integrations and, if +read objects were removed *and* there are live installations, interactively prompts (via +`promptui`) whether to pause reads (the `destructive` flag). + +**Local webhook development.** `amp listen` (hidden command) runs a local HTTP server that +pretty-prints and forwards incoming webhooks to your app, writing its chosen port to the OS +cache dir (`ampersand/webhook-port`). `amp trigger` sends fixture events (see +`internal/fixtures/`, e.g. hubspot/stripe payloads) to that listener; `sync-ngrok` bridges +to a public ngrok URL. Webhook helpers live in `internal/webhook/`. + +**Logging.** Use the `logger` package (`Info`, `Infof`, `Debugf`, `Fatal`, `FatalErr`). +`Debugf` output is gated on the `--debug` flag. Note: `logger` imports `flags`, so `flags` +must not import `logger` (there is an explicit circular-dependency workaround in +`flags/config.go`'s `GetProjectOrFail`). + +## Conventions + +- Command failures typically call `logger.Fatal`/`logger.FatalErr` (which `os.Exit(1)`), + rather than returning errors up the stack — follow the pattern of neighboring commands. +- Naming (`amp :`): list commands use `list:integrations`, + `list:installations`, etc.; deletes use `delete:integration`. Keep this colon style. +- Output format for list-style commands is controlled by a `--format/-f` flag (json|yaml); + wire it with `flags.InitAndBindFormatFlag` and render with `utils.WriteStruct`. diff --git a/appdata/appdata.go b/appdata/appdata.go index 57e833b..0be1ed5 100644 --- a/appdata/appdata.go +++ b/appdata/appdata.go @@ -11,9 +11,10 @@ import ( const fileName = "Ampersand/config.json" -// IMPORTANT: -// Do not modify the JSON labels in this struct without ensuring backwards compatibility, -// since those strings are written to the user's config file on their computer. +// Config is the user's persisted CLI configuration. +// +// IMPORTANT: Do not modify the JSON labels in this struct without ensuring backwards +// compatibility, since those strings are written to the user's config file on their computer. type Config struct { Token Token `json:"token"` } @@ -40,7 +41,9 @@ func Get() (Config, error) { } var c Config - if err := json.Unmarshal(data, &c); err != nil { + + err = json.Unmarshal(data, &c) + if err != nil { return Config{}, fmt.Errorf("can't parse config: %w", err) } @@ -56,7 +59,9 @@ func Set(config Config) error { } merged := config - if err := mergo.Merge(&merged, existing); err != nil { + + err = mergo.Merge(&merged, existing) + if err != nil { return fmt.Errorf("can't merge new config with existing config: %w", err) } diff --git a/clerk/clerk.go b/clerk/clerk.go index c893958..9a55cb8 100644 --- a/clerk/clerk.go +++ b/clerk/clerk.go @@ -162,7 +162,9 @@ func FetchJwt(ctx context.Context) (string, error) { //nolint:funlen,cyclop } data := &LoginData{} - if err := json.Unmarshal(contents, data); err != nil { + + err = json.Unmarshal(contents, data) + if err != nil { return "", fmt.Errorf("error unmarshalling jwt file: %w", err) } @@ -224,11 +226,13 @@ func FetchJwt(ctx context.Context) (string, error) { //nolint:funlen,cyclop } if rsp.StatusCode != http.StatusOK { - return "", fmt.Errorf("http %d (%s)", rsp.StatusCode, string(bb)) //nolint:goerr113 + return "", fmt.Errorf("http %d (%s)", rsp.StatusCode, string(bb)) //nolint:err113 } cr := &clientResponse{} - if err := json.Unmarshal(bb, cr); err != nil { + + err = json.Unmarshal(bb, cr) + if err != nil { return "", fmt.Errorf("error unmarshalling response body: %w", err) } @@ -274,7 +278,9 @@ func DecodeJWT(jwt string) (string, string, error) { func getHTML(emailStr string) (string, error) { // Render the HTML tmpl := mustache.New() - if err := tmpl.ParseString(HTML); err != nil { + + err := tmpl.ParseString(HTML) + if err != nil { return "", err } diff --git a/cmd/delete_installation.go b/cmd/delete_installation.go index 163b972..b64675c 100644 --- a/cmd/delete_installation.go +++ b/cmd/delete_installation.go @@ -14,7 +14,7 @@ var deleteInstallationCmd = &cobra.Command{ //nolint:gochecknoglobals Use: "delete:installation ", Short: "Delete installation", Long: "Delete installation", - Args: cobra.ExactArgs(2), //nolint:gomnd,mnd + Args: cobra.ExactArgs(2), //nolint:mnd Run: func(cmd *cobra.Command, args []string) { logger.Debug("Deleting installation") diff --git a/cmd/deploy.go b/cmd/deploy.go index f8722a8..0af5b5f 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -68,7 +68,8 @@ var deployCmd = &cobra.Command{ //nolint:gochecknoglobals logger.FatalErr("Unable to get pre-signed upload URL", err) } - if err := storage.Upload(cmd.Context(), zipResult.Data, signed.URL, md5String); err != nil { + err = storage.Upload(cmd.Context(), zipResult.Data, signed.URL, md5String) + if err != nil { logger.FatalErr("Unable to upload to Google Cloud Storage", err) } @@ -353,13 +354,17 @@ func formatGlobalPromptMessage(integrations []integrationRemovedObjectsInfo) str integrationWord := pluralizer.Pluralize("integration", len(integrations), false) message = fmt.Sprintf("⚠️ You are removing read action objects from %d %s:\n\n", len(integrations), integrationWord) + var lines strings.Builder + for _, info := range integrations { objectList := strings.Join(info.removedObjects, ", ") installationWord := pluralizer.Pluralize("installation", info.installationCount, false) - message += fmt.Sprintf(" • %s: %s (%d %s)\n", - info.integrationName, objectList, info.installationCount, installationWord) + lines.WriteString(fmt.Sprintf(" • %s: %s (%d %s)\n", + info.integrationName, objectList, info.installationCount, installationWord)) } + message += lines.String() + message += fmt.Sprintf( "\n\n❓ Do you want to stop reading these objects across all installations of these %d %s?\n\n"+ " Note: To stop reads for some integrations & not all, deploy changes to one integration at a time.", @@ -371,16 +376,16 @@ func formatGlobalPromptMessage(integrations []integrationRemovedObjectsInfo) str } func formatAffectedInstallations(groups []groupInfo, totalCount int) string { - var result string + var result strings.Builder for _, g := range groups { - result += fmt.Sprintf("\n - %s (%s)", g.name, g.ref) + result.WriteString(fmt.Sprintf("\n - %s (%s)", g.name, g.ref)) } if totalCount > len(groups) { - result += fmt.Sprintf("\n - and %d more", totalCount-len(groups)) + result.WriteString(fmt.Sprintf("\n - and %d more", totalCount-len(groups))) } - return result + return result.String() } func init() { diff --git a/cmd/deploy_destination.go b/cmd/deploy_destination.go index 3cabd48..93b3782 100644 --- a/cmd/deploy_destination.go +++ b/cmd/deploy_destination.go @@ -21,13 +21,16 @@ var deployDestinationCmd = &cobra.Command{ //nolint:gochecknoglobals Run: func(cmd *cobra.Command, args []string) { projectId := flags.GetProjectOrFail() apiKey := flags.GetAPIKey() + input := viper.GetString("input") if input == "" { logger.Fatal("Must provide an input file path") } var dest request.Destination - if _, err := utils.ReadStructFromFile(input, &dest); err != nil { + + _, err := utils.ReadStructFromFile(input, &dest) + if err != nil { logger.FatalErr("Unable to read destination file", err) } @@ -35,15 +38,15 @@ var deployDestinationCmd = &cobra.Command{ //nolint:gochecknoglobals oldDest := getOldDest(cmd.Context(), client, &dest) var output *request.Destination - var err error if oldDest == nil { output, err = client.CreateDestination(cmd.Context(), &dest) } else { patch := generatePatch(oldDest, &dest) if len(patch.UpdateMask) == 0 { - if err := utils.WriteStructToFile(viper.GetString("output"), - flags.GetOutputFormat(), oldDest); err != nil { + err := utils.WriteStructToFile(viper.GetString("output"), + flags.GetOutputFormat(), oldDest) + if err != nil { logger.FatalErr("Unable to write destination file", err) } @@ -57,8 +60,9 @@ var deployDestinationCmd = &cobra.Command{ //nolint:gochecknoglobals logger.FatalErr("Unable to deploy destination", err) } - if err := utils.WriteStructToFile(viper.GetString("output"), - flags.GetOutputFormat(), output); err != nil { + err = utils.WriteStructToFile(viper.GetString("output"), + flags.GetOutputFormat(), output) + if err != nil { logger.FatalErr("Unable to write destination file", err) } }, @@ -137,17 +141,20 @@ func findDestId(ctx context.Context, client *request.APIClient, dest *request.De func init() { deployDestinationCmd.Flags().StringP("input", "i", "", "The input file path") - if err := viper.BindPFlag("input", deployDestinationCmd.Flags().Lookup("input")); err != nil { + err := viper.BindPFlag("input", deployDestinationCmd.Flags().Lookup("input")) + if err != nil { logger.FatalErr("unable to bind flag", err) } deployDestinationCmd.Flags().StringP("output", "o", "-", "The output file path") - if err := viper.BindPFlag("output", deployDestinationCmd.Flags().Lookup("output")); err != nil { + err = viper.BindPFlag("output", deployDestinationCmd.Flags().Lookup("output")) + if err != nil { logger.FatalErr("unable to bind flag", err) } - if err := flags.InitAndBindFormatFlag(deployDestinationCmd); err != nil { + err = flags.InitAndBindFormatFlag(deployDestinationCmd) + if err != nil { logger.FatalErr("unable to bind flag", err) } diff --git a/cmd/init.go b/cmd/init.go index 720bcfd..b1d18bc 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -67,24 +67,28 @@ var initCmd = &cobra.Command{ //nolint:gochecknoglobals disp := "my integration" var integ openapi.Integration + integ.Name = name integ.DisplayName = disp integ.Provider = provider.Name if provider.Support.Read { - if err := setupRead(&integ, provider); err != nil { + err := setupRead(&integ, provider) + if err != nil { logger.FatalErr("Unable to setup read", err) } } if provider.Support.Write { - if err := setupWrite(&integ, provider); err != nil { + err := setupWrite(&integ, provider) + if err != nil { logger.FatalErr("Unable to setup write", err) } } if provider.Support.Proxy { - if err := setupProxy(&integ, provider); err != nil { + err := setupProxy(&integ, provider) + if err != nil { logger.FatalErr("Unable to setup proxy", err) } } @@ -104,7 +108,8 @@ var initCmd = &cobra.Command{ //nolint:gochecknoglobals logger.FatalErr("Unable to marshal manifest", err) } - if err := os.WriteFile("amp.yaml", ys, YamlFileMode); err != nil { //nolint:gosec + err = os.WriteFile("amp.yaml", ys, YamlFileMode) //nolint:gosec + if err != nil { logger.FatalErr("Unable to write manifest to file", err) } @@ -145,7 +150,8 @@ func setupWrite(integ *openapi.Integration, provider *openapi.ProviderInfo) erro write := &openapi.IntegrationWrite{} for { - if err := addWriteObject(write, provider); err != nil { + err := addWriteObject(write, provider) + if err != nil { return err } @@ -211,9 +217,10 @@ func getIntegrationField() (*openapi.IntegrationField, error) { //nolint:funlen, } if !remap { - if err := field.FromIntegrationFieldExistent(openapi.IntegrationFieldExistent{ + err := field.FromIntegrationFieldExistent(openapi.IntegrationFieldExistent{ FieldName: fieldName, - }); err != nil { + }) + if err != nil { return nil, err } @@ -264,7 +271,8 @@ func getIntegrationField() (*openapi.IntegrationField, error) { //nolint:funlen, mapping.Prompt = &promptText } - if err := field.FromIntegrationFieldMapping(mapping); err != nil { + err = field.FromIntegrationFieldMapping(mapping) + if err != nil { return nil, err } @@ -302,7 +310,8 @@ func addReadObject(read *openapi.IntegrationRead, provider *openapi.ProviderInfo } if wantBackfill { - if err := setupBackfill(obj); err != nil { + err := setupBackfill(obj) + if err != nil { return err } } @@ -416,7 +425,8 @@ func setupRead(integ *openapi.Integration, provider *openapi.ProviderInfo) error read := &openapi.IntegrationRead{} for { - if err := addReadObject(read, provider); err != nil { + err := addReadObject(read, provider) + if err != nil { return err } @@ -455,7 +465,8 @@ func promptString(prompt string, validate ...func(string) error) (string, error) Label: prompt, Validate: func(s string) error { for _, fn := range validate { - if err := fn(s); err != nil { + err := fn(s) + if err != nil { return err } } diff --git a/cmd/list_destinations.go b/cmd/list_destinations.go index e2285f1..32ba1ad 100644 --- a/cmd/list_destinations.go +++ b/cmd/list_destinations.go @@ -38,6 +38,7 @@ var listDestinationsCmd = &cobra.Command{ //nolint:gochecknoglobals if dest.Metadata.URL != "" { output += " (" + dest.Metadata.URL + ")" } + logger.Info(output) } }, diff --git a/cmd/list_installations.go b/cmd/list_installations.go index b603672..a37dcc1 100644 --- a/cmd/list_installations.go +++ b/cmd/list_installations.go @@ -16,7 +16,7 @@ var listInstallationsCmd = &cobra.Command{ //nolint:gochecknoglobals Use: "list:installations ", Short: "List installations", Long: "List installations", - Args: cobra.ExactArgs(1), //nolint:gomnd,mnd + Args: cobra.ExactArgs(1), //nolint:mnd Run: func(cmd *cobra.Command, args []string) { integrationId := args[0] projectId := flags.GetProjectOrFail() diff --git a/cmd/listen.go b/cmd/listen.go index cc8143d..a8af1f4 100644 --- a/cmd/listen.go +++ b/cmd/listen.go @@ -52,7 +52,9 @@ func runListen(cmd *cobra.Command, args []string) error { mux.HandleFunc("/", handleWebhook) // Start the server - listener, err := net.Listen("tcp", listenAddr) + var lc net.ListenConfig + + listener, err := lc.Listen(ctx, "tcp", listenAddr) if err != nil { return fmt.Errorf("failed to start listener: %w", err) } @@ -66,11 +68,14 @@ func runListen(cmd *cobra.Command, args []string) error { port := strconv.Itoa(addr.Port) // Save the port - if err := saveListenerPort(port); err != nil { + + err = saveListenerPort(port) + if err != nil { logger.FatalErr("failed to save port", err) } const serverTimeout = 10 * time.Second + srv := &http.Server{ Handler: mux, ReadHeaderTimeout: serverTimeout, @@ -80,7 +85,8 @@ func runListen(cmd *cobra.Command, args []string) error { go func() { logger.Info("starting webhook listener") - if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + err := srv.Serve(listener) + if err != nil && !errors.Is(err, http.ErrServerClosed) { logger.FatalErr("webhook listener failed", err) } }() @@ -96,11 +102,13 @@ func runListen(cmd *cobra.Command, args []string) error { // Create a deadline to wait for current connections to complete const shutdownTimeout = 5 * time.Second + shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() - if err := srv.Shutdown(shutdownCtx); err != nil { + err = srv.Shutdown(shutdownCtx) + if err != nil { logger.FatalErr("shutdown error", err) } @@ -127,7 +135,8 @@ func saveListenerPort(port string) error { // Create ampersand directory if it doesn't exist ampDir := filepath.Join(dir, "ampersand") - if err := os.MkdirAll(ampDir, mkdirPerm); err != nil { + err = os.MkdirAll(ampDir, mkdirPerm) + if err != nil { return err } @@ -168,7 +177,9 @@ func handleWebhook(writer http.ResponseWriter, req *http.Request) { req.Body.Close() // Log the webhook payload - if err := webhook.PrettyPrintJSON(body); err != nil { + + err = webhook.PrettyPrintJSON(body) + if err != nil { logger.FatalErr("error pretty printing JSON", err) } @@ -194,6 +205,7 @@ func handleWebhook(writer http.ResponseWriter, req *http.Request) { // Forward the request const clientTimeout = 5 * time.Second + client := &http.Client{Timeout: clientTimeout} resp, err := client.Do(forwardReq) @@ -216,7 +228,8 @@ func handleWebhook(writer http.ResponseWriter, req *http.Request) { writer.WriteHeader(resp.StatusCode) - if _, err := io.Copy(writer, resp.Body); err != nil { + _, err = io.Copy(writer, resp.Body) + if err != nil { logger.FatalErr("error copying response", err) } } diff --git a/cmd/login.go b/cmd/login.go index b24652e..f71d34b 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -79,13 +79,16 @@ const JwtFilePermissions = 0o600 // processLogin takes the JWT token, verifies it, and then stores it in the jwt.json file. func processLogin(ctx context.Context, payload []byte, write bool) (string, string, error) { //nolint:cyclop data := &clerk.LoginData{} - if err := json.Unmarshal(payload, data); err != nil { + + err := json.Unmarshal(payload, data) + if err != nil { return "", "", err } path := clerk.GetJwtPath() if write { - if err := os.WriteFile(path, pretty.Pretty(payload), JwtFilePermissions); err != nil { + err := os.WriteFile(path, pretty.Pretty(payload), JwtFilePermissions) + if err != nil { return "", "", err } } @@ -170,7 +173,8 @@ func canOpenBrowserLinux() bool { return false } - if _, err := exec.LookPath("xdg-open"); err != nil { + _, err := exec.LookPath("xdg-open") + if err != nil { logger.Info("'xdg-open' command not found, cannot open browser automatically.") return false @@ -185,7 +189,8 @@ func canOpenBrowserDarwin() bool { return false } - if _, err := exec.LookPath("open"); err != nil { + _, err := exec.LookPath("open") + if err != nil { logger.Info("'open' command not found, cannot open browser automatically.") return false @@ -200,7 +205,8 @@ func canOpenBrowserWindows() bool { return false } - if _, err := exec.LookPath("rundll32"); err != nil { + _, err := exec.LookPath("rundll32") + if err != nil { logger.Info("'rundll32' command not found, cannot open browser automatically.") return false @@ -213,15 +219,17 @@ func canOpenBrowserWindows() bool { func openBrowser(url string) { var err error + // The browser launch is intentionally detached from any request context: it must + // outlive this command, so we use context.Background() rather than a cancelable ctx. switch runtime.GOOS { case "linux": - err = exec.Command("xdg-open", url).Start() + err = exec.CommandContext(context.Background(), "xdg-open", url).Start() case OSWindows: - err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + err = exec.CommandContext(context.Background(), "rundll32", "url.dll,FileProtocolHandler", url).Start() case "darwin": - err = exec.Command("open", url).Start() + err = exec.CommandContext(context.Background(), "open", url).Start() default: - err = fmt.Errorf("unsupported platform: %s", runtime.GOOS) //nolint:goerr113 + err = fmt.Errorf("unsupported platform: %s", runtime.GOOS) //nolint:err113 } if err != nil { diff --git a/cmd/logout.go b/cmd/logout.go index 26c9663..916cdaf 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -35,7 +35,8 @@ func DoLogout(showLogs bool) { } } - if err := os.Remove(path); err != nil { + err = os.Remove(path) + if err != nil { logger.Fatal(err.Error()) } diff --git a/cmd/my_info.go b/cmd/my_info.go index e268283..4ce3c04 100644 --- a/cmd/my_info.go +++ b/cmd/my_info.go @@ -40,14 +40,17 @@ var myInfoCmd = &cobra.Command{ //nolint:gochecknoglobals } format := flags.GetOutputFormat() - if err := utils.WriteStruct(os.Stdout, format, info); err != nil { + + err = utils.WriteStruct(os.Stdout, format, info) + if err != nil { logger.FatalErr("Unable to write user info", err) } }, } func init() { - if err := flags.InitAndBindFormatFlag(myInfoCmd); err != nil { + err := flags.InitAndBindFormatFlag(myInfoCmd) + if err != nil { logger.FatalErr("unable to initialize flags", err) } diff --git a/cmd/sync_ngrok.go b/cmd/sync_ngrok.go index 5eb4b5f..0565786 100644 --- a/cmd/sync_ngrok.go +++ b/cmd/sync_ngrok.go @@ -232,7 +232,8 @@ func getPublicNgrokURL(ctx context.Context) (string, error) { // Ensure response body is properly closed to prevent resource leaks defer func() { - if closeErr := resp.Body.Close(); closeErr != nil { + closeErr := resp.Body.Close() + if closeErr != nil { _, _ = fmt.Fprintf(os.Stderr, "error closing response body: %v\n", closeErr) } }() @@ -248,7 +249,8 @@ func getPublicNgrokURL(ctx context.Context) (string, error) { decoder := json.NewDecoder(resp.Body) - if err := decoder.Decode(&ngrokResp); err != nil { + err = decoder.Decode(&ngrokResp) + if err != nil { // JSON parsing errors indicate malformed response from ngrok return "", fmt.Errorf("failed to parse ngrok response: %w", err) } @@ -309,7 +311,9 @@ func waitForNgrok(ctx context.Context) error { } // Perform initial connectivity check to see if ngrok is already running - conn, err := net.DialTimeout("tcp", address, connectTimeout) + dialer := net.Dialer{Timeout: connectTimeout} + + conn, err := dialer.DialContext(ctx, "tcp", address) if err == nil { // ngrok is already available, close connection and return immediately _ = conn.Close() @@ -349,7 +353,7 @@ func waitForNgrok(ctx context.Context) error { } // Attempt to connect to ngrok API endpoint - conn, err = net.DialTimeout("tcp", address, connectTimeout) + conn, err = dialer.DialContext(ctx, "tcp", address) if err == nil { // Success! ngrok is now available _ = conn.Close() @@ -370,7 +374,8 @@ func waitForNgrok(ctx context.Context) error { // This function is called by the Cobra framework when the ngrok command is invoked. func runSyncNgrok(cmd *cobra.Command, _ []string) error { // Validate protocol flag before proceeding - if err := validateProtocol(); err != nil { + err := validateProtocol() + if err != nil { return err } @@ -431,7 +436,8 @@ func getNgrokTunnelURL(ctx context.Context) (string, error) { // Step 1: Wait for ngrok service to become available logger.Info("waiting for ngrok to start...") - if err := waitForNgrok(ctx); err != nil { + err := waitForNgrok(ctx) + if err != nil { // Provide helpful context about ngrok not being available return "", fmt.Errorf("ngrok is not running: %w", err) } @@ -514,7 +520,9 @@ func updateDestinations(ctx context.Context, client *request.APIClient, } // Attempt to update the destination via API - if err := updateDestination(ctx, client, dest, publicURL); err != nil { + + err = updateDestination(ctx, client, dest, publicURL) + if err != nil { // Log API update failures but continue with other destinations logger.Infof("failed to update destination %s: %v", dest.NameOrId(), err) diff --git a/cmd/trigger.go b/cmd/trigger.go index 0abf5b7..6cd8c7d 100644 --- a/cmd/trigger.go +++ b/cmd/trigger.go @@ -71,8 +71,10 @@ func runTrigger(cmd *cobra.Command, args []string) error { payload = []byte(rawJSON) // Validate it's valid JSON - var jsonObj interface{} - if err := json.Unmarshal(payload, &jsonObj); err != nil { + var jsonObj any + + err := json.Unmarshal(payload, &jsonObj) + if err != nil { return fmt.Errorf("invalid JSON provided: %w", err) } case fixtureFile != "": @@ -91,7 +93,7 @@ func runTrigger(cmd *cobra.Command, args []string) error { // Let user edit the payload if requested if interactive { - payload, err = openInEditor(payload) + payload, err = openInEditor(cmd.Context(), payload) if err != nil { return fmt.Errorf("failed to edit payload: %w", err) } @@ -104,7 +106,7 @@ func runTrigger(cmd *cobra.Command, args []string) error { } // openInEditor opens the JSON payload in the default editor. -func openInEditor(data []byte) ([]byte, error) { +func openInEditor(ctx context.Context, data []byte) ([]byte, error) { // Create a temporary file tmpFile, err := os.CreateTemp("", "amp-webhook-*.json") if err != nil { @@ -113,11 +115,14 @@ func openInEditor(data []byte) ([]byte, error) { defer os.Remove(tmpFile.Name()) // Write the data to the file - if _, err := tmpFile.Write(data); err != nil { + + _, err = tmpFile.Write(data) + if err != nil { return nil, err } - if err := tmpFile.Close(); err != nil { + err = tmpFile.Close() + if err != nil { return nil, err } @@ -132,12 +137,13 @@ func openInEditor(data []byte) ([]byte, error) { } // Open the editor - cmd := exec.Command(editor, tmpFile.Name()) + cmd := exec.CommandContext(ctx, editor, tmpFile.Name()) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { + err = cmd.Run() + if err != nil { return nil, err } @@ -159,6 +165,7 @@ func sendWebhook(payload []byte) error { // Send the request const clientTimeout = 5 * time.Second + client := &http.Client{Timeout: clientTimeout} resp, err := client.Do(req) diff --git a/files/manifest.go b/files/manifest.go index e66732b..337df23 100644 --- a/files/manifest.go +++ b/files/manifest.go @@ -14,17 +14,18 @@ const manifestVersion = "1.0.0" func ParseManifest(yamlData []byte) (*openapi.Manifest, error) { manifest := &openapi.Manifest{} - if err := yaml.Unmarshal(yamlData, manifest); err != nil { + err := yaml.Unmarshal(yamlData, manifest) + if err != nil { return nil, fmt.Errorf("failed to parse yaml: %w", err) } return manifest, nil } -// nolint: goerr113 +// nolint: err113 func validationError(tracker *pathTracker, msg string, args ...any) error { err1 := fmt.Errorf(msg, args...) - err2 := fmt.Errorf("The validation error happened at the %s", tracker.String()) //nolint:stylecheck + err2 := fmt.Errorf("The validation error happened at the %s", tracker.String()) //nolint:staticcheck return errors.Join(ErrBadManifest, err1, err2) } @@ -47,7 +48,8 @@ func ValidateManifest(manifest *openapi.Manifest) error { } for idx, integ := range manifest.Integrations { - if err := validateIntegration(integ, tracker.PushObj("integrations").PushArr(idx)); err != nil { + err := validateIntegration(integ, tracker.PushObj("integrations").PushArr(idx)) + if err != nil { return err } } @@ -168,29 +170,33 @@ func validateSubscribe(sub *openapi.IntegrationSubscribe, path *pathTracker) err } if obj.AssociationChangeEvent != nil { - if err := validateSubscribeAssocChange(obj.AssociationChangeEvent, - path.PushArr(idx).PushObj("associationChangeEvent")); err != nil { + err := validateSubscribeAssocChange(obj.AssociationChangeEvent, + path.PushArr(idx).PushObj("associationChangeEvent")) + if err != nil { return err } } if obj.CreateEvent != nil { - if err := validateSubscribeCreateEvent(obj.CreateEvent, - path.PushArr(idx).PushObj("createEvent")); err != nil { + err := validateSubscribeCreateEvent(obj.CreateEvent, + path.PushArr(idx).PushObj("createEvent")) + if err != nil { return err } } if obj.UpdateEvent != nil { - if err := validateSubscribeUpdateEvent(obj.UpdateEvent, - path.PushArr(idx).PushObj("updateEvent")); err != nil { + err := validateSubscribeUpdateEvent(obj.UpdateEvent, + path.PushArr(idx).PushObj("updateEvent")) + if err != nil { return err } } if obj.DeleteEvent != nil { - if err := validateSubscribeDeleteEvent(obj.DeleteEvent, - path.PushArr(idx).PushObj("deleteEvent")); err != nil { + err := validateSubscribeDeleteEvent(obj.DeleteEvent, + path.PushArr(idx).PushObj("deleteEvent")) + if err != nil { return err } } @@ -213,25 +219,29 @@ func validateIntegration(integration openapi.Integration, path *pathTracker) err } if integration.Proxy != nil { - if err := validateProxy(integration.Proxy, path.PushObj("proxy")); err != nil { + err := validateProxy(integration.Proxy, path.PushObj("proxy")) + if err != nil { return err } } if integration.Read != nil { - if err := validateRead(integration.Read, path.PushObj("read")); err != nil { + err := validateRead(integration.Read, path.PushObj("read")) + if err != nil { return err } } if integration.Write != nil { - if err := validateWrite(integration.Write, path.PushObj("write")); err != nil { + err := validateWrite(integration.Write, path.PushObj("write")) + if err != nil { return err } } if integration.Subscribe != nil { - if err := validateSubscribe(integration.Subscribe, path.PushObj("subscribe")); err != nil { + err := validateSubscribe(integration.Subscribe, path.PushObj("subscribe")) + if err != nil { return err } } diff --git a/files/zip.go b/files/zip.go index 1ee55a4..b87334f 100644 --- a/files/zip.go +++ b/files/zip.go @@ -12,7 +12,7 @@ import ( "github.com/amp-labs/cli/openapi" ) -var ErrBadManifest = errors.New("Invalid manifest") //nolint:stylecheck +var ErrBadManifest = errors.New("Invalid manifest") //nolint:staticcheck const ( mode = 0o644 @@ -132,7 +132,8 @@ func importYaml(writer *zip.Writer) (*openapi.Manifest, error) { return nil, fmt.Errorf("error parsing manifest: %w", err) } - if err := ValidateManifest(manifest); err != nil { + err = ValidateManifest(manifest) + if err != nil { return nil, err } @@ -170,7 +171,8 @@ func Zip(source string) (*ZipResult, error) { // nolint:funlen,cyclop manifest = m - if err := writer.Close(); err != nil { + err = writer.Close() + if err != nil { return fmt.Errorf("error closing zip writer: %w", err) } diff --git a/flags/config.go b/flags/config.go index 71a81cd..7f4ec32 100644 --- a/flags/config.go +++ b/flags/config.go @@ -20,19 +20,23 @@ func Init(rootCmd *cobra.Command) error { rootCmd.PersistentFlags().StringP("project", "p", "", "Ampersand project name or ID") rootCmd.PersistentFlags().StringP("key", "k", "", "Ampersand API key") - if err := viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")); err != nil { + err := viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) + if err != nil { return err } - if err := viper.BindPFlag("project", rootCmd.PersistentFlags().Lookup("project")); err != nil { + err = viper.BindPFlag("project", rootCmd.PersistentFlags().Lookup("project")) + if err != nil { return err } - if err := viper.BindPFlag("key", rootCmd.PersistentFlags().Lookup("key")); err != nil { + err = viper.BindPFlag("key", rootCmd.PersistentFlags().Lookup("key")) + if err != nil { return err } - if err := viper.BindEnv("key", "AMP_API_KEY"); err != nil { + err = viper.BindEnv("key", "AMP_API_KEY") + if err != nil { panic(err) } @@ -43,7 +47,8 @@ func Init(rootCmd *cobra.Command) error { func InitAndBindFormatFlag(cmd *cobra.Command) error { cmd.Flags().StringP("format", "f", "json", "Output format, defaults to json. Options: json, yaml") - if err := viper.BindPFlag("format", cmd.Flags().Lookup("format")); err != nil { + err := viper.BindPFlag("format", cmd.Flags().Lookup("format")) + if err != nil { return err } @@ -65,6 +70,13 @@ func GetDebugMode() bool { return viper.GetBool("debug") } +// GetProject returns the configured project name or ID, or an empty string if +// none is set. Unlike GetProjectOrFail it does not exit, so callers that can +// operate without a project (e.g. offline validation) can degrade gracefully. +func GetProject() string { + return viper.GetString("project") +} + func GetProjectOrFail() string { p := viper.GetString("project") if p == "" { diff --git a/go.mod b/go.mod index 91b558a..c4972d0 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/amp-labs/cli -go 1.24.3 +go 1.26.4 require ( github.com/adrg/xdg v0.5.3 @@ -10,38 +10,39 @@ require ( github.com/gertd/go-pluralize v0.2.1 github.com/imdario/mergo v0.3.15 github.com/manifoldco/promptui v0.9.0 - github.com/oapi-codegen/runtime v1.4.0 + github.com/oapi-codegen/runtime v1.4.2 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/tidwall/pretty v1.2.1 - golang.org/x/term v0.38.0 + golang.org/x/term v0.45.0 sigs.k8s.io/yaml v1.6.0 ) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect ) require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/go-jose/go-jose/v3 v3.0.5 // 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-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.46.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index b517b39..37a993f 100644 --- a/go.sum +++ b/go.sum @@ -26,20 +26,20 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -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/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= @@ -47,26 +47,29 @@ github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+h github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/oapi-codegen/runtime v1.4.0 h1:KLOSFOp7UzkbS7Cs1ms6NBEKYr0WmH2wZG0KKbd2er4= -github.com/oapi-codegen/runtime v1.4.0/go.mod h1:5sw5fxCDmnOzKNYmkVNF8d34kyUeejJEY8HNT2WaPec= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/oapi-codegen/nullable v1.1.0 h1:eAh8JVc5430VtYVnq00Hrbpag9PFRGWLjxR1/3KntMs= +github.com/oapi-codegen/nullable v1.1.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= +github.com/oapi-codegen/runtime v1.4.2 h1:GMxFVYLzoYLua+/KvzgSphkyK1lLTReQI9Vf4hvATKE= +github.com/oapi-codegen/runtime v1.4.2/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= -github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -90,8 +93,8 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -99,8 +102,8 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -125,16 +128,16 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -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.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -142,8 +145,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -151,8 +154,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/webhook/webhook.go b/internal/webhook/webhook.go index 33c5e7f..39b488e 100644 --- a/internal/webhook/webhook.go +++ b/internal/webhook/webhook.go @@ -30,8 +30,10 @@ func LoadFixture(provider, event, customPath string) ([]byte, error) { data = bytes.ReplaceAll(data, []byte("{{NOW}}"), []byte(now)) // Validate it's valid JSON - var jsonObj interface{} - if err := json.Unmarshal(data, &jsonObj); err != nil { + var jsonObj any + + err = json.Unmarshal(data, &jsonObj) + if err != nil { return nil, fmt.Errorf("fixture contains invalid JSON: %w", err) } @@ -42,6 +44,7 @@ func LoadFixture(provider, event, customPath string) ([]byte, error) { // Format: provider.event_name (e.g., "stripe.payment_intent.created"). func ParseEvent(event string) (provider, eventName string) { const expectedParts = 2 + parts := strings.SplitN(event, ".", expectedParts) if len(parts) < expectedParts { diff --git a/request/api.go b/request/api.go index 3f5aa67..e7a9ecd 100644 --- a/request/api.go +++ b/request/api.go @@ -137,7 +137,8 @@ func (c *APIClient) DeleteIntegration(ctx context.Context, integrationId string) return err } - if _, err := c.Client.Delete(ctx, delURL, auth); err != nil { //nolint:bodyclose + _, err = c.Client.Delete(ctx, delURL, auth) //nolint:bodyclose + if err != nil { return fmt.Errorf("error deleting integration: %w", err) } @@ -336,7 +337,8 @@ func (c *APIClient) DeleteInstallation(ctx context.Context, integrationId string return err } - if _, err := c.Client.Delete(ctx, delURL, auth); err != nil { //nolint:bodyclose + _, err = c.Client.Delete(ctx, delURL, auth) //nolint:bodyclose + if err != nil { return fmt.Errorf("error deleting installation: %w", err) } diff --git a/request/request.go b/request/request.go index c4328f8..86935ac 100644 --- a/request/request.go +++ b/request/request.go @@ -215,7 +215,9 @@ func (c *Client) makeRequestAndParseJSONResult(req *http.Request, result any) (* if err == nil { if mt == "application/problem+json" { prob := &ProblemError{} - if err := json.Unmarshal(payload, prob); err == nil { + + err := json.Unmarshal(payload, prob) + if err == nil { if prob.Status == http.StatusNotFound { return res, fmt.Errorf("%w: %w\n%w", ErrNon200Status, ErrNotFound, prob) } else { @@ -233,7 +235,8 @@ func (c *Client) makeRequestAndParseJSONResult(req *http.Request, result any) (* } } - if err := json.Unmarshal(payload, result); err != nil { + err = json.Unmarshal(payload, result) + if err != nil { return nil, err } @@ -381,7 +384,8 @@ func (c *Client) sendRequest(req *http.Request) (*http.Response, []byte, error) defer func() { if res != nil && res.Body != nil { - if closeErr := res.Body.Close(); closeErr != nil { + closeErr := res.Body.Close() + if closeErr != nil { logger.Debugf("unable to close response body %v", closeErr) } } diff --git a/utils/utils.go b/utils/utils.go index fe43bd5..918e67e 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -39,16 +39,20 @@ func ReadStruct(r io.Reader, out any) (Format, error) { return Unknown, err } - if err := json.Unmarshal(data, out); err != nil { - var se *json.SyntaxError - if !errors.As(err, &se) { - return Unknown, err - } - } else { + err = json.Unmarshal(data, out) + if err == nil { return JSON, nil } - if err := yaml.Unmarshal(data, out); err != nil { + // A JSON syntax error means the data may still be YAML, so fall through. Any other + // error means the data is JSON-shaped but invalid (e.g. a type mismatch); report it. + var se *json.SyntaxError + if !errors.As(err, &se) { + return Unknown, err + } + + err = yaml.Unmarshal(data, out) + if err != nil { return Unknown, err } @@ -69,7 +73,8 @@ func ReadStructFromFile(filePath string, out any) (Format, error) { _ = f.Close() }() - if format, err := ReadStruct(f, out); err == nil { + format, err := ReadStruct(f, out) + if err == nil { return format, nil } @@ -85,7 +90,8 @@ func WriteStruct(writer io.Writer, format Format, data any) error { enc.SetIndent("", " ") enc.SetEscapeHTML(false) - if err := enc.Encode(data); err != nil { + err := enc.Encode(data) + if err != nil { return err } From d94c1a1c086f4a44574794626344a2efa7abacc4 Mon Sep 17 00:00:00 2001 From: Chris Eberle Date: Fri, 10 Jul 2026 16:13:59 -0700 Subject: [PATCH 2/2] =?UTF-8?q?fix(ci):=20satisfy=20semgrep=20=E2=80=94=20?= =?UTF-8?q?pin=20action=20SHAs,=20suppress=20editor=20exec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Semgrep's diff-aware scan blocked the PR on 6 findings: - github-actions-mutable-action-tag: pin every workflow action to a full commit SHA (with a version comment) across all workflows, not just the ones this PR touched. Also unifies actions/checkout to v4 everywhere (semgrep.yml was still on v3). - dangerous-exec-command in cmd/trigger.go: the command is the user's own $EDITOR (static vi/notepad fallback) run locally as themselves, so there is no injection surface. Add a scoped nosemgrep with a justification comment. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build.yml | 4 ++-- .github/workflows/codesee-arch-diagram.yml | 2 +- .github/workflows/linter.yml | 6 +++--- .github/workflows/semgrep.yml | 2 +- cmd/trigger.go | 5 ++++- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 928558c..43ca3ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,10 +5,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Set up go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: go.mod cache: false diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml index 806d41d..c97764d 100644 --- a/.github/workflows/codesee-arch-diagram.yml +++ b/.github/workflows/codesee-arch-diagram.yml @@ -17,7 +17,7 @@ jobs: continue-on-error: true name: Analyze the repo with CodeSee steps: - - uses: Codesee-io/codesee-action@v2 + - uses: Codesee-io/codesee-action@db076ce4b205a08da4d95bbefb3e278a958a4799 # v2 with: codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} codesee-url: https://app.codesee.io diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index b099737..fee3f6e 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -5,13 +5,13 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: go.mod cache: false - name: golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 continue-on-error: false with: version: v2.9.0 diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 889ebe2..22698df 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -30,7 +30,7 @@ jobs: steps: # Fetch project source with GitHub Actions Checkout. - - uses: actions/checkout@v3 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 # Run the "semgrep ci" command on the command line of the docker image. - run: semgrep ci env: diff --git a/cmd/trigger.go b/cmd/trigger.go index 6cd8c7d..443db71 100644 --- a/cmd/trigger.go +++ b/cmd/trigger.go @@ -136,7 +136,10 @@ func openInEditor(ctx context.Context, data []byte) ([]byte, error) { } } - // Open the editor + // Open the editor. The command is the user's own $EDITOR (or a static + // vi/notepad fallback) and runs locally as the invoking user, so there is + // no untrusted input and no injection surface here. + // nosemgrep: go.lang.security.audit.dangerous-exec-command.dangerous-exec-command cmd := exec.CommandContext(ctx, editor, tmpFile.Name()) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout