diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c58bf0a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,111 @@ +name: CI + +# Public repo → GitHub-hosted Actions are free. Kept fast: Build and Vet share +# one job so `go vet` reuses the compile cache `go build` just populated +# (standalone `go vet ./...` recompiles everything and is slow). Test runs in +# parallel; the quick checks (Tidy/Format) give sub-minute feedback. Every job +# restores the Go build+module cache. One run per PR (+ push to master); +# superseded runs cancel. +on: + pull_request: + push: + branches: [master] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-vet: + name: Build & Vet + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache: false + # Manual cache with restore-keys so a changed go.sum still restores the + # previous build+module cache (only the changed deps recompile) instead + # of a full cold compile of the whole cloud-SDK dependency tree. + - uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-${{ runner.os }}- + - name: go build + run: go build ./... + - name: go vet (reuses the build cache) + run: go vet ./... + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache: false + - uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-${{ runner.os }}- + - run: go test ./... + + tidy: + name: Tidy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache: true + - name: go mod tidy is clean + run: | + go mod tidy + git diff --exit-code go.mod go.sum + + format: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache: true + - name: gofmt (report-only) + run: | + unformatted="$(gofmt -l .)" + if [ -n "$unformatted" ]; then + echo "::warning::gofmt needed on the following files (advisory):" + echo "$unformatted" + else + echo "all files gofmt-clean" + fi + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache: true + - name: install golangci-lint v2 + run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b "$(go env GOPATH)/bin" + - name: golangci-lint run (report-only) + run: "$(go env GOPATH)/bin/golangci-lint run --timeout=9m ./... || echo '::warning::golangci-lint reported findings (advisory)'" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..c2932b0 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,83 @@ +name: Security + +# Security scanning, separate from CI so it runs in parallel and is easy to +# read. Each scanner is its own cached job. Runs on PRs, on push to master, and +# weekly (so newly-disclosed CVEs are caught even without a push). +on: + pull_request: + push: + branches: [master] + schedule: + - cron: "0 6 * * 1" # Mondays 06:00 UTC + +permissions: + contents: read + +concurrency: + group: security-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + govulncheck: + name: Vulnerabilities (govulncheck) + runs-on: ubuntu-latest + # Advisory to start: flags known CVEs in dependencies the code reaches. + # Flip to blocking once the baseline is confirmed clean. + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache: true + - name: install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + - name: govulncheck (report-only) + run: "$(go env GOPATH)/bin/govulncheck ./... || echo '::warning::govulncheck reported findings (advisory)'" + + gosec: + name: SAST (gosec) + runs-on: ubuntu-latest + # Advisory to start: static security analysis (hardcoded creds, weak crypto, + # unsafe patterns). Reports without blocking until the baseline is triaged. + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache: true + - name: install gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest + - name: gosec (report-only) + run: "$(go env GOPATH)/bin/gosec -exclude-generated ./... || echo '::warning::gosec reported findings (advisory)'" + + codeql: + name: CodeQL (SAST) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache: true + - uses: github/codeql-action/init@v3 + with: + languages: go + - uses: github/codeql-action/autobuild@v3 + - uses: github/codeql-action/analyze@v3 + + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + - name: Review dependency changes for known vulnerabilities + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high diff --git a/kubernetes/handlers_http_test.go b/kubernetes/handlers_http_test.go index 9b0e2b2..67b9b54 100644 --- a/kubernetes/handlers_http_test.go +++ b/kubernetes/handlers_http_test.go @@ -496,7 +496,10 @@ func TestConfigMap_PatchBadContentTypeReturns400(t *testing.T) { base+"/api/v1/namespaces/default/configmaps/p", bytes.NewReader([]byte(`[]`))) req.Header.Set("Content-Type", "application/strategic-merge-patch+json") - resp, _ := http.DefaultClient.Do(req) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("request: %v", err) + } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { @@ -595,7 +598,10 @@ func TestConfigMap_PatchBadJSONReturns400(t *testing.T) { ) req.Header.Set("Content-Type", "application/merge-patch+json") - resp, _ := http.DefaultClient.Do(req) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("request: %v", err) + } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { @@ -618,7 +624,10 @@ func TestConfigMap_PatchTypeIncompatibleReturns400(t *testing.T) { ) req.Header.Set("Content-Type", "application/merge-patch+json") - resp, _ := http.DefaultClient.Do(req) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("request: %v", err) + } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { @@ -636,7 +645,10 @@ func TestNamespace_PatchOnMissingReturns404(t *testing.T) { ) req.Header.Set("Content-Type", "application/merge-patch+json") - resp, _ := http.DefaultClient.Do(req) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("request: %v", err) + } defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound {