From 880f6ad39fb477a773b23b3bfdc924fe811fae31 Mon Sep 17 00:00:00 2001 From: Michael McQuade Date: Wed, 5 Aug 2026 07:30:47 +0800 Subject: [PATCH 1/2] ci: harden the pipeline with lint, race tests, vulnerability, and CodeQL jobs The initial workflow ran a single serial job on ubuntu with no linter, no race detector, and no dependency scanning, and its actions were still on the deprecated Node 20 runtime. Split it into lint, test, and vulncheck jobs, add CodeQL, and add a dependabot config so the gomod and github-actions ecosystems stay current. Tests now run with -race and -shuffle=on across ubuntu, macos, and windows, since the generator is installed on all three. govulncheck deliberately runs on the stable toolchain: it reports standard-library advisories against whatever Go it runs with, and the go directive names the minimum supported release, not the one to build with. --- .github/dependabot.yml | 25 ++++++++++++ .github/workflows/ci.yml | 79 +++++++++++++++++++++++++++++++----- .github/workflows/codeql.yml | 43 ++++++++++++++++++++ .golangci.yml | 14 +++++++ 4 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .golangci.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f3495dd --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +version: 2 + +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: "chore(deps)" + groups: + go-dependencies: + patterns: + - "*" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + commit-message: + prefix: "chore(deps)" + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5de33da..3ec5a12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,16 +10,17 @@ permissions: contents: read concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - build: + lint: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true @@ -34,13 +35,71 @@ jobs: exit 1 fi - - name: build - run: go build ./... + - name: go mod tidy is a no-op + run: | + go mod tidy + git diff --exit-code -- go.mod go.sum - name: vet run: go vet ./... - # Generates clients from the testdata specs and compiles/runs them, so a - # broken template fails here rather than in a consumer's build. + - uses: golangci/golangci-lint-action@v8 + with: + version: v2.12.2 + + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - name: build + run: go build ./... + + # The generator's e2e tests write clients to a temp dir and compile and run + # them, so a broken template fails here rather than in a consumer's build. - name: test - run: go test ./... + run: go test -race -shuffle=on -coverprofile=coverage.out ./... + + - name: coverage summary + if: matrix.os == 'ubuntu-latest' + run: | + echo '### Coverage' >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + go tool cover -func=coverage.out | tail -n 20 >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@v4 + if: matrix.os == 'ubuntu-latest' + with: + name: coverage + path: coverage.out + retention-days: 7 + + vulncheck: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + + # Deliberately not go.mod's version: govulncheck reports standard-library + # advisories against the toolchain it runs with, and the go directive names + # the minimum supported release rather than the one to build with. + - uses: actions/setup-go@v6 + with: + go-version: stable + cache: true + + - name: govulncheck + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./... diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..b151bd4 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,43 @@ +name: codeql + +on: + pull_request: + push: + branches: + - canary + schedule: + # Rerun weekly so newly published queries reach existing code. + - cron: "27 4 * * 1" + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + analyze: + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + security-events: write + actions: read + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - uses: github/codeql-action/init@v3 + with: + languages: go + queries: security-and-quality + + - uses: github/codeql-action/autobuild@v3 + + - uses: github/codeql-action/analyze@v3 + with: + category: /language:go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..d5eddd1 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,14 @@ +version: "2" + +linters: + default: standard + enable: + - misspell + - usetesting + exclusions: + rules: + # Test helpers assert on the values they read; an unchecked error there is + # reported by the surrounding t.Fatal path. + - path: _test\.go + linters: + - errcheck From da2aed148c1ea0960f979c653546fb5720ce7243 Mon Sep 17 00:00:00 2001 From: Michael McQuade Date: Wed, 5 Aug 2026 07:37:41 +0800 Subject: [PATCH 2/2] ci: fix the failures the new lint and windows jobs surfaced errcheck flagged two unchecked MarkFlagRequired calls in generate's init. init cannot return, so the error is now joined into a package-level setupErr that Execute reports before running the command. TestGenerate_EmbeddedFields failed on windows because git checks templates out with CRLF there. The generator writes template bytes verbatim, so a windows clone emitted CRLF in every generated .go file; .gitattributes pins the working tree to LF. The test step itself failed to even start on windows: the runner defaults to PowerShell, which split -coverprofile=coverage.out into two arguments and left go test looking for a package named .out. Pin the workflow to bash. --- .gitattributes | 3 +++ .github/workflows/ci.yml | 6 ++++++ cmd/generate.go | 8 ++++++-- cmd/root.go | 6 ++++++ 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9e41817 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# The generator emits template bytes verbatim, so a CRLF checkout would leak +# CRLF into every generated .go file. +* text=auto eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ec5a12..3a573e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,12 @@ concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} +defaults: + run: + # The Windows runner defaults to PowerShell, which mangles `-flag=value.ext` + # into two arguments. + shell: bash + jobs: lint: runs-on: ubuntu-latest diff --git a/cmd/generate.go b/cmd/generate.go index 3a76a12..230d233 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "path/filepath" "strings" @@ -29,8 +30,11 @@ func init() { generateCmd.Flags().StringVar(&generateFlags.userAgent, "user-agent", "", `default User-Agent for generated clients (default "openapi-client-generator/1.0")`) generateCmd.Flags().BoolVar(&generateFlags.allowRemoteRefs, "allow-remote-refs", false, "allow fetching remote $ref targets") - generateCmd.MarkFlagRequired("spec") - generateCmd.MarkFlagRequired("out") + // init cannot return, so a flag-registration failure is deferred to Execute. + setupErr = errors.Join( + generateCmd.MarkFlagRequired("spec"), + generateCmd.MarkFlagRequired("out"), + ) } var generateCmd = &cobra.Command{ diff --git a/cmd/root.go b/cmd/root.go index f177da0..0f3cea0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,7 +10,13 @@ var rootCmd = &cobra.Command{ Long: "A code generator that produces feature-rich Go HTTP client packages from OpenAPI 3.1 specifications.", } +// setupErr carries a failure from a command's init, which cannot return one. +var setupErr error + // Execute runs the root command. func Execute() error { + if setupErr != nil { + return setupErr + } return rootCmd.Execute() }