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/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..3a573e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,16 +10,23 @@ 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' }} + +defaults: + run: + # The Windows runner defaults to PowerShell, which mangles `-flag=value.ext` + # into two arguments. + shell: bash 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 +41,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 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() }