diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ab12852e..6a62e6154 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.11 + version: v2.14 args: --timeout=5m - name: Run tests @@ -170,10 +170,10 @@ jobs: shell: bash env: CLI_TEST_MODIFY_CERT_STORE: '1' - run: GOEXPERIMENT=jsonv2 go test -v -count=1 -timeout 3m -run TestWindows ./internal/legacy/ + run: go test -v -count=1 -timeout 3m -run TestWindows ./internal/legacy/ # Reading the certificate store happens before every legacy command. - name: Measure building the CA bundle if: always() shell: bash - run: GOEXPERIMENT=jsonv2 go test -run '^$' -bench BenchmarkWindows -benchmem ./internal/legacy/ + run: go test -run '^$' -bench BenchmarkWindows -benchmem ./internal/legacy/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b43ef2fd2..f8491a3e7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ stages: - test -image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/cimg/go:1.25 +image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/cimg/go:1.27 workflow: auto_cancel: @@ -40,7 +40,7 @@ test: golangci-lint: stage: test extends: .go-setup - image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v2.4 + image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v2.14 script: golangci-lint run --timeout 0 --verbose legacy-php: diff --git a/CLAUDE.md b/CLAUDE.md index 213f1851d..877ee04f0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,7 @@ Run tests: ```bash make test # or directly: -GOEXPERIMENT=jsonv2 go test -v -race -cover -count=1 ./... +go test -v -race -cover -count=1 ./... ``` Run linters: diff --git a/Makefile b/Makefile index 958a75076..f50d2f545 100644 --- a/Makefile +++ b/Makefile @@ -124,13 +124,12 @@ endif PHP_VERSION=$(PHP_VERSION) goreleaser release --clean .PHONY: test -# "We encourage users of encoding/json to test their programs with GOEXPERIMENT=jsonv2 enabled" (https://tip.golang.org/doc/go1.25) test: ## Run unit tests (excludes integration tests) - GOEXPERIMENT=jsonv2 go test -v -race -cover -count=1 $$(go list ./... | grep -v /integration-tests) + go test -v -race -cover -count=1 $$(go list ./... | grep -v /integration-tests) .PHONY: integration-test integration-test: single ## Run integration tests (requires built CLI) - cd integration-tests && GOEXPERIMENT=jsonv2 go test -v -count=1 ./... + cd integration-tests && go test -v -count=1 ./... .PHONY: lint lint: lint-gomod lint-golangci ## Run linters. diff --git a/cmd/platform/main.go b/cmd/platform/main.go index d2976b2cb..eb7e37be5 100644 --- a/cmd/platform/main.go +++ b/cmd/platform/main.go @@ -10,7 +10,6 @@ import ( "github.com/symfony-cli/terminal" "github.com/upsun/cli/commands" - "github.com/upsun/cli/internal/certs" "github.com/upsun/cli/internal/config" ) @@ -27,12 +26,6 @@ func main() { os.Exit(1) } - // Trust the same certificates as the legacy CLI. This is not fatal: the - // system certificates are used instead, as they were before. - if err := certs.UseEnvCertFile(); err != nil { - fmt.Fprintln(os.Stderr, "Warning: "+err.Error()) - } - // When Cobra starts, load Viper config from the environment. cobra.OnInitialize(func() { viper.SetEnvPrefix(strings.TrimSuffix(cnf.Application.EnvPrefix, "_")) diff --git a/go.mod b/go.mod index c83b794a0..992dc6047 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/upsun/cli -go 1.26.2 +go 1.27.1 require ( github.com/AlecAivazis/survey/v2 v2.3.7 diff --git a/internal/certs/certs.go b/internal/certs/certs.go deleted file mode 100644 index 3630f82ed..000000000 --- a/internal/certs/certs.go +++ /dev/null @@ -1,64 +0,0 @@ -// Package certs lets the Go part of the CLI trust the certificates named by -// SSL_CERT_FILE, which the legacy PHP part already trusts on every platform. -package certs - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "net/http" - "os" - "runtime" -) - -// EnvVar names a file holding the certificates to trust. -const EnvVar = "SSL_CERT_FILE" - -// UseEnvCertFile makes the default HTTP transport verify against the -// certificates named by SSL_CERT_FILE. -// -// Go reads that variable itself on Unix, but not on Windows or macOS, where it -// verifies through the operating system instead. The legacy CLI reads it on -// every platform, through Composer\CaBundle, so without this the two parts of -// the CLI disagree about which certificates to trust. -// -// The file replaces the system certificates rather than adding to them, which -// is what Go does on Unix and what the legacy CLI does everywhere. -func UseEnvCertFile() error { - if goReadsEnvCertFile() { - return nil - } - transport, ok := http.DefaultTransport.(*http.Transport) - if !ok { - return fmt.Errorf("the default HTTP transport cannot be configured") - } - return useCertFile(transport, os.Getenv(EnvVar)) -} - -// goReadsEnvCertFile reports whether Go's own verification reads the variable. -func goReadsEnvCertFile() bool { - return runtime.GOOS != "windows" && runtime.GOOS != "darwin" -} - -// useCertFile points a transport at a certificate file. An empty path is -// ignored, leaving the system certificates in use. -func useCertFile(transport *http.Transport, path string) error { - if path == "" { - return nil - } - pemCerts, err := os.ReadFile(path) //nolint:gosec // the user names the file. - if err != nil { - return fmt.Errorf("could not read %s: %w", EnvVar, err) - } - pool := x509.NewCertPool() - if !pool.AppendCertsFromPEM(pemCerts) { - return fmt.Errorf("no certificates found in %s: %s", EnvVar, path) - } - if transport.TLSClientConfig == nil { - // TLS 1.2 is the minimum Go uses for a client anyway, so this sets no - // policy of its own. - transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12} - } - transport.TLSClientConfig.RootCAs = pool - return nil -} diff --git a/internal/certs/certs_test.go b/internal/certs/certs_test.go deleted file mode 100644 index aea50a9e1..000000000 --- a/internal/certs/certs_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package certs - -import ( - "encoding/pem" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestUseCertFile checks that a server signed by a certificate in the file is -// trusted, and that one signed by an unrelated certificate is not. -func TestUseCertFile(t *testing.T) { - server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte("hello")) - })) - defer server.Close() - - certFile := filepath.Join(t.TempDir(), "cert.pem") - require.NoError(t, os.WriteFile(certFile, serverCertPEM(t, server), 0o600)) - - cases := []struct { - name string - path string - wantTrust bool - }{ - {"no file, so the system certificates are used", "", false}, - {"a file holding the server's certificate", certFile, true}, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - transport := &http.Transport{} - require.NoError(t, useCertFile(transport, c.path)) - - resp, err := (&http.Client{Transport: transport}).Get(server.URL) - if !c.wantTrust { - require.Error(t, err, "expected the certificate not to be trusted") - return - } - require.NoError(t, err) - defer resp.Body.Close() - assert.Equal(t, http.StatusOK, resp.StatusCode) - - // Setting the roots is what makes the file replace the system - // certificates rather than add to them. - assert.NotNil(t, transport.TLSClientConfig.RootCAs) - }) - } -} - -func TestUseCertFileErrors(t *testing.T) { - empty := filepath.Join(t.TempDir(), "empty.pem") - require.NoError(t, os.WriteFile(empty, []byte("not a certificate"), 0o600)) - - assert.ErrorContains(t, useCertFile(&http.Transport{}, filepath.Join(t.TempDir(), "missing.pem")), - "could not read SSL_CERT_FILE") - assert.ErrorContains(t, useCertFile(&http.Transport{}, empty), - "no certificates found in SSL_CERT_FILE") -} - -// serverCertPEM returns the certificate a test server signs with. -func serverCertPEM(t *testing.T, server *httptest.Server) []byte { - t.Helper() - - require.NotNil(t, server.Certificate()) - return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw}) -} diff --git a/internal/init/command.go b/internal/init/command.go index 539614284..e20c323d5 100644 --- a/internal/init/command.go +++ b/internal/init/command.go @@ -171,8 +171,7 @@ func RunAIConfig( yamlContent := strings.TrimSpace(apiOutput.ConfigYAML) // Check if stdout is a terminal and supports color - // G115: file descriptors fit comfortably in an int on all supported platforms. - if f, ok := stdout.(*os.File); ok && term.IsTerminal(int(f.Fd())) { //nolint:gosec + if f, ok := stdout.(*os.File); ok && term.IsTerminal(int(f.Fd())) { if err := quick.Highlight(stdout, yamlContent+"\n", "yaml", "terminal", "bw"); err != nil { // Fall back to plain text if highlighting fails fmt.Fprintln(stdout, yamlContent)