diff --git a/.claude/.gitignore b/.claude/.gitignore new file mode 100644 index 000000000..f830ad137 --- /dev/null +++ b/.claude/.gitignore @@ -0,0 +1,5 @@ +plans/ +skills/ +commands/ +agents/ +hooks/ diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index ff1859def..5172f8366 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -4,601 +4,78 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is the go-openapi fork of the testify testing package. The main goal is to remove external dependencies while maintaining a clean, focused API for testing in Go. This fork strips out unnecessary features (mocks, suite) and internalizes dependencies (go-spew, difflib) to ensure `github.com/go-openapi/testify/v2` is the only import needed. +Zero-dependency fork of [stretchr/testify](https://github.com/stretchr/testify) used across +all go-openapi repositories. The fork strips external dependencies (go-spew, difflib are +internalized), removes mock/suite packages, and uses extensive code generation to maintain +consistency across assertion variants. -The project uses sophisticated code generation to: -1. Generate assert and require packages from a single source (`internal/assertions/`) -2. Generate comprehensive tests for all assertion variants -3. Generate domain-organized documentation for a Hugo-based documentation site +**Key difference:** `testifylint` does **not** work with this fork — it only recognizes +`stretchr/testify` imports. -## Key Architecture +### Mono-repo structure -### Single Source of Truth: `internal/assertions/` +This is a mono-repo with multiple Go modules tied together by `go.work`. -All assertion implementations live in `internal/assertions/`, organized by domain: -- **boolean.go** - True, False -- **collection.go** - Contains, Empty, Len, ElementsMatch, Subset, etc. -- **compare.go** - Greater, Less, comparison assertions -- **equal.go** - Equal, EqualValues, NotEqual, Same, etc. -- **error.go** - Error, NoError, ErrorIs, ErrorAs, etc. -- **file.go** - FileExists, DirExists, FileEmpty, FileNotEmpty -- **http.go** - HTTPSuccess, HTTPError, HTTPStatusCode, etc. -- **json.go** - JSONEq -- **number.go** - InDelta, InEpsilon, Positive, Negative -- **panic.go** - Panics, NotPanics, PanicsWithValue -- **string.go** - Regexp, NotRegexp -- **time.go** - WithinDuration -- **type.go** - IsType, Zero, NotZero, Implements -- **yaml.go** - YAMLEq +| Module | Purpose | +|--------|---------| +| `.` (root) | Main module `github.com/go-openapi/testify/v2` — package-level doc, `go:generate` directive | +| `codegen/` | Code generator: scans `internal/assertions/`, generates `assert/`, `require/`, and docs | +| `enable/yaml` | Optional module — blank-import to activate YAML assertions | +| `enable/colors` | Optional module — blank-import to activate colorized test output | +| `hack/migrate-testify` | Migration tool for converting from `stretchr/testify` | +| `internal/testintegration` | Integration tests | -**Key principle:** Write assertions once in `internal/assertions/` with comprehensive tests. Everything else is generated. +### Key packages (within root module) -### Core Packages (Generated) -- **assert**: Provides non-fatal test assertions (tests continue after failures) - - Generated from `internal/assertions/` by `codegen/` - - Returns `bool` to indicate success/failure -- **require**: Provides fatal test assertions (tests stop immediately on failure via `FailNow()`) - - Generated from `internal/assertions/` by `codegen/` - - Void functions that call `FailNow()` on failure +| Package | Contents | +|---------|----------| +| `assert/` | Non-fatal test assertions (generated from `internal/assertions/`) | +| `require/` | Fatal test assertions via `FailNow()` (generated from `internal/assertions/`) | +| `internal/assertions/` | **Single source of truth** — all assertion implementations, organized by domain | +| `internal/spew/` | Internalized go-spew for pretty-printing values | +| `internal/difflib/` | Internalized go-difflib for generating diffs | +| `internal/fdleak/` | File descriptor leak detection | +| `internal/leak/` | Goroutine leak detection | +| `enable/stubs/` | Public API for enabling optional features (yaml, colors) | -Both packages are 100% generated and maintain API consistency mechanically. +### Code generation architecture -### Code Generation Architecture +All assertion implementations live in `internal/assertions/`, organized by domain files +(boolean.go, collection.go, equal.go, error.go, etc.). Each assertion function generates +up to 8 variants: -The codebase uses sophisticated code generation via the `codegen/` directory: +1. `assert.Equal(t, ...)` — package-level function +2. `assert.Equalf(t, ..., "msg")` — format variant +3. `a.Equal(...)` — forward method (`a := assert.New(t)`) +4. `a.Equalf(..., "msg")` — forward format variant +5–8. Same four variants in `require/` (fatal on failure) -**Structure:** -``` -codegen/ -├── internal/ -│ ├── scanner/ # Parses internal/assertions using go/packages and go/types -│ │ ├── comments/ # Doc comment extraction -│ │ ├── comments-parser/ # Domain, examples, and tag parsing -│ │ └── signature/ # Function signature analysis -│ ├── generator/ # Template-based code generation engine -│ │ ├── doc_generator.go # Documentation generator (domain-organized) -│ │ ├── render.go # Markdown rendering utilities -│ │ └── templates/ # Go templates for code and docs -│ ├── model/ # Data model for assertions -│ │ └── documentation.go # Documentation structures -├── main.go # CLI orchestration -├── docs/ # Generated documentation output -└── (generated outputs in assert/ and require/) -``` +Run `go generate ./...` to regenerate `assert/` and `require/` packages from source. -**Generated code files include:** -- **assert/assertion_assertions.go** - Package-level assertion functions -- **assert/assertion_format.go** - Format string variants (Equalf, Truef, etc.) -- **assert/assertion_forward.go** - Forwarded assertion methods for chaining -- **assert/assertion_*_test.go** - Generated tests for all assert variants -- **require/requirement_assertions.go** - Fatal assertion functions -- **require/requirement_format.go** - Fatal format variants -- **require/requirement_forward.go** - Fatal forwarded methods -- **require/requirement_*_test.go** - Generated tests for all require variants +### Dependencies -**Generated documentation files include:** -- **docs/doc-site/api/_index.md** - API index page listing all domains -- **docs/doc-site/api/{domain}.md** - Domain-specific pages (boolean.md, collection.md, etc.) -- Documentation is organized by domain (boolean, string, error, etc.) rather than by package -- Each domain page shows both assert and require variants together +The root module has **zero external dependencies** by design. Do not add any. +Optional functionality uses the "enable" pattern: users blank-import a sub-module +(e.g., `_ "github.com/go-openapi/testify/v2/enable/yaml"`) to activate features +that require external deps. -**Each assertion function generates 8 variants:** -1. `assert.Equal(t, ...)` - package-level function -2. `assert.Equalf(t, ..., "msg")` - format variant -3. `a.Equal(...)` - forward method (where `a := assert.New(t)`) -4. `a.Equalf(..., "msg")` - forward format variant -5. `require.Equal(t, ...)` - fatal package-level -6. `require.Equalf(t, ..., "msg")` - fatal format variant -7. `r.Equal(...)` - fatal forward method -8. `r.Equalf(..., "msg")` - fatal forward format variant +### Key API -With 127 assertion functions, this generates 840 functions automatically. +- `assert.Equal(t, expected, actual)` — non-fatal equality check +- `require.Equal(t, expected, actual)` — fatal equality check +- `assert.New(t)` / `require.New(t)` — create forwarded assertion objects +- All assertions in `internal/assertions/` follow the pattern: `func Name(t T, args..., msgAndArgs ...any) bool` -### Dependency Isolation Strategy -- **internal/spew**: Internalized copy of go-spew for pretty-printing values -- **internal/difflib**: Internalized copy of go-difflib for generating diffs -- **internal/assertions/enable**: Internal stubs that panic by default if YAML/color assertions are used -- **enable/stubs**: Public API for enabling optional features (yaml, colors) -- **enable/yaml**: Optional module that activates YAML support via init() when imported -- **enable/colors**: Optional module that activates colorized output via init() when imported +### Notable design decisions -The "enable" pattern allows optional functionality to be opt-in: import `_ "github.com/go-openapi/testify/v2/enable/yaml"` to activate YAML assertions, or `_ "github.com/go-openapi/testify/v2/enable/colors"` to enable colorized output, without forcing dependencies on all users. - -## Development Commands - -### Running Tests -```bash -# Run all tests -go test ./... - -# Run tests in specific packages -go test ./internal/assertions # Source of truth with exhaustive tests -go test ./assert # Generated package tests -go test ./require # Generated package tests -go test ./codegen/internal/... # Scanner and generator tests - -# Run a single test -go test ./internal/assertions -run TestEqual - -# Run with coverage -go test -cover ./internal/assertions # Should be 90%+ -go test -cover ./assert # Should be ~100% -go test -cover ./require # Should be ~100% -go test -cover ./codegen/internal/scanner/... # Scanner tests - -# Run tests with verbose output -go test -v ./... -``` - -### Working with Documentation - -```bash -# Generate documentation (included in go generate) -go generate ./... - -# Or generate documentation explicitly -cd codegen && go run . -output-packages assert,require -include-doc - -# Preview documentation site locally -cd hack/doc-site/hugo -go run gendoc.go -# Visit http://localhost:1313/testify/ - -# The Hugo site auto-reloads on changes to docs/doc-site/ -# To see changes, re-run: go generate ./... -``` - -### Adding a New Assertion - -**The entire workflow:** -1. Add function to appropriate file in `internal/assertions/` -2. Add "Examples:" section to doc comment -3. Add "domain:" tag to doc comment for documentation organization -4. Add tests to corresponding `*_test.go` file -5. Run `go generate ./...` -6. Done - all 8 variants generated with tests + documentation - -**Example - Adding a new assertion:** -```go -import ( - "fmt" - "strings" -) - -// In internal/assertions/string.go - -// StartsWith asserts that the string starts with the given prefix. -// -// domain: string -// -// Examples: -// -// success: "hello world", "hello" -// failure: "hello world", "bye" -func StartsWith(t T, str, prefix string, msgAndArgs ...any) bool { - if h, ok := t.(H); ok { - h.Helper() - } - if !strings.HasPrefix(str, prefix) { - return Fail(t, fmt.Sprintf("Expected %q to start with %q", str, prefix), msgAndArgs...) - } - return true -} -``` - -**Note on placeholder values in Examples:** -- For complex values that can't be easily represented (pointers, structs, etc.), use `// NOT IMPLEMENTED` marker: - ```go - // Examples: - // success: &customStruct{Field: "value"}, // NOT IMPLEMENTED - // failure: complexType{}, // NOT IMPLEMENTED - ``` -- **Never use `// TODO`** - it triggers false positives in code quality analyzers and project management tools - -Then add tests in `internal/assertions/string_test.go` and run `go generate ./...`. - -This generates: -- `assert.StartsWith(t, str, prefix)` -- `assert.StartsWithf(t, str, prefix, "msg")` -- `a.StartsWith(str, prefix)` (forward method) -- `a.StartsWithf(str, prefix, "msg")` -- `require.StartsWith(t, str, prefix)` -- `require.StartsWithf(t, str, prefix, "msg")` -- `r.StartsWith(str, prefix)` (forward method) -- `r.StartsWithf(str, prefix, "msg")` -- Tests for all 8 variants -- Documentation entry in `docs/doc-site/api/string.md` - -### Code Generation -```bash -# Generate all code and documentation from internal/assertions -go generate ./... - -# Or run the generator directly with all outputs -cd codegen && go run . -output-packages assert,require -include-doc - -# Run code generation only (skip documentation) -cd codegen && go run . -output-packages assert,require -include-doc=false - -# The generator workflow: -# 1. Scans internal/assertions/ for exported functions and types -# 2. Extracts doc comments, "Examples:", and domain tags -# 3. Generates assert/ package with all variants + tests -# 4. Generates require/ package with all variants + tests -# 5. Reorganizes by domain and generates docs/doc-site/ markdown -# 6. Ensures 100% test coverage via example-driven tests -``` - -### Example-Driven Test Generation - -The generator reads "Examples:" sections from doc comments: - -```go -// Equal asserts that two objects are equal. -// -// Examples: -// -// success: 123, 123 -// failure: 123, 456 -func Equal(t T, expected, actual any, msgAndArgs ...any) bool { - // implementation -} -``` - -From this, it generates tests that verify: -- Success case works correctly -- Failure case works correctly and calls appropriate failure methods -- Format variants work with message parameter -- Forward methods work with chaining - -**Test case types:** -- `success: ` - Test should pass -- `failure: ` - Test should fail -- `panic: ` - Test should panic (followed by assertion message on next line) - `` - -### Documentation Generation - -The codegen also generates domain-organized documentation for a Hugo static site: - -**Documentation workflow:** -1. Scanner extracts domain tags from doc comments (e.g., `// domain: string`) -2. Scanner collects domain descriptions from `internal/assertions/doc.go` -3. Generator merges documentation from assert and require packages -4. DocGenerator reorganizes by domain instead of package -5. Markdown files generated in `docs/doc-site/api/` (19 domain pages) - -**Domain organization:** -- Functions are grouped by domain (boolean, collection, comparison, equality, error, etc.) -- 19 domains total covering all assertion categories -- Each domain page shows assert and require variants together -- Index page lists all domains with descriptions -- Uses Hugo front matter for metadata - -**Hugo static site setup:** -Located in `hack/doc-site/hugo/`: -- **hugo.yaml** - Main Hugo configuration -- **gendoc.go** - Development server script -- **themes/hugo-relearn** - Documentation theme -- Mounts generated content from `docs/doc-site/` - -**Running the documentation site locally:** -```bash -# First generate the documentation -go generate ./... - -# Then start the Hugo dev server -cd hack/doc-site/hugo -go run gendoc.go - -# Visit http://localhost:1313/testify/ -``` - -**Domain tagging in source code:** -To assign a function to a domain, add a domain tag in its doc comment: -```go -// Equal asserts that two objects are equal. -// -// domain: equality -// -// Examples: -// -// success: 123, 123 -// failure: 123, 456 -func Equal(t T, expected, actual any, msgAndArgs ...any) bool { - // implementation -} -``` - -Add domain descriptions in `internal/assertions/doc.go`: -```go -// domain-description: equality -// Assertions for comparing values for equality, including deep equality, -// value equality, and pointer equality. -``` - -### Build and Verify -```bash -# Tidy dependencies -go mod tidy - -# Build code generator -cd codegen && go build - -# Format code -go fmt ./... - -# Run all tests -go test ./... - -# Check coverage -go test -cover ./internal/assertions -go test -cover ./assert -go test -cover ./require -``` - -## Important Constraints - -### API Stability -The following assertions are guaranteed stable (used by go-openapi/go-swagger): -- Condition, Contains, Empty, Equal, EqualError, EqualValues, Error, ErrorContains, ErrorIs -- Fail, FailNow, False, Greater, Implements, InDelta, IsType, JSONEq, Len -- Nil, NoError, NotContains, NotEmpty, NotEqual, NotNil, NotPanics, NotZero -- Panics, PanicsWithValue, Subset, True, YAMLEq, Zero - -Other APIs may change without notice as the project evolves. - -### Zero External Dependencies -Do not add external dependencies to the main module. If new functionality requires a dependency: -1. Consider internalizing it (copy into `internal/` with proper licensing) -2. Or create an "enable" package that users import to activate the feature - -### YAML Support Pattern -When using YAML assertions (YAMLEq, YAMLEqf): -- Tests must import: `_ "github.com/go-openapi/testify/v2/enable/yaml"` -- Without this import, YAML assertions will panic with helpful error message -- This pattern keeps gopkg.in/yaml.v3 as an optional dependency +- **Single source of truth** — write assertions once in `internal/assertions/`, generate everything else. +- **Example-driven test generation** — `Examples:` sections in doc comments drive generated tests for all 8 variants. +- **Domain tagging** — `// domain: equality` tags in doc comments organize documentation by concern. +- **Enable pattern** — optional features (YAML, colors) activated via blank imports, keeping the core dependency-free. +- **v2.0.0 is retracted** — see `go.mod`. ## Module Information - Module path: `github.com/go-openapi/testify/v2` - Go version: 1.24.0 -- v2.0.0 is retracted (see go.mod) -- License: Apache-2.0 (forked from MIT-licensed stretchr/testify) - -## Testing Philosophy - -Keep tests simple and focused. The assert package provides detailed failure messages automatically, so test code should be minimal and readable. Use `require` when a test cannot continue meaningfully after a failure, and `assert` when subsequent checks might provide additional context. - -### Testing Strategy: Layered Coverage - -**Layer 1: Exhaustive Tests in `internal/assertions/`** (94% coverage) -- Comprehensive table-driven tests using Go 1.23 `iter.Seq` patterns -- Error message content and format validation -- Edge cases, nil handling, type coercion scenarios -- Domain-organized test files mirroring implementation -- Source of truth for assertion correctness - -**Layer 2: Generated Smoke Tests in `assert/` and `require/`** (~100% coverage) -- Minimal mechanical tests proving functions exist and work -- Success case: verify correct return value / no FailNow -- Failure case: verify correct return value / FailNow called -- Generated from "Examples:" in doc comments -- No error message testing (already covered in Layer 1) - -**Layer 3: Meta Tests for Generator** (future) -- Test that code generation produces correct output -- Verify function signatures, imports, structure -- Optional golden file testing - -This layered approach ensures: -- Deep testing where it matters (source implementation) -- Complete coverage of generated forwarding code -- Simple, maintainable test generation -- No duplication of complex test logic - -### Iterator Pattern for Table-Driven Tests - -**This repository uses a signature testing pattern** based on Go 1.23's `iter.Seq` for all table-driven tests: - -```go -import ( - "iter" - "slices" - "testing" -) - -// Define test case struct -type parseTestExamplesCase struct { - name string - input string - expected []model.Test -} - -// Create iterator function returning iter.Seq[caseType] -func parseTestExamplesCases() iter.Seq[parseTestExamplesCase] { - return slices.Values([]parseTestExamplesCase{ - { - name: "success and failure examples", - input: `Examples: - success: 123, 456 - failure: 789, 012`, - expected: []model.Test{ - {TestedValue: "123, 456", ExpectedOutcome: model.TestSuccess}, - {TestedValue: "789, 012", ExpectedOutcome: model.TestFailure}, - }, - }, - // More cases... - }) -} - -// Test function iterates over cases -func TestParseTestExamples(t *testing.T) { - t.Parallel() - - for c := range parseTestExamplesCases() { - t.Run(c.name, func(t *testing.T) { - t.Parallel() - - result := ParseTestExamples(c.input) - // Assertions... - }) - } -} -``` - -**Benefits of this pattern:** -- Clean separation between test data (iterator functions) and test logic (test functions) -- Easy to add new test cases by appending to the slice -- Test case structs can include helper functions or complex setup logic -- Subtests run in parallel automatically with `t.Parallel()` -- Iterator functions can be reused across multiple tests -- Type-safe test case definitions - -**When to use this pattern:** -- Any test with 2+ test cases -- Tests that need complex setup or helper functions -- Tests that benefit from parallel execution -- Any table-driven test scenario - -**Examples in codebase:** -- `codegen/internal/scanner/comments-parser/examples_test.go` -- `codegen/internal/generator/domains/domains_test.go` -- `internal/assertions/*_test.go` (most assertion tests) - -## Architecture Benefits - -### Why This Design Wins - -**For Contributors:** -- Add assertion in focused, domain-organized file -- Write tests once in single location -- Run `go generate` and get all variants for free -- Clear separation: source vs generated code - -**For Maintainers:** -- Mechanical consistency across 608 generated functions -- Template changes affect all functions uniformly -- Easy to add new variants (e.g., generics) -- Single source of truth prevents drift - -**For Users:** -- Comprehensive API with 76 assertions -- All expected variants (package, format, forward, require) -- Zero external dependencies -- Drop-in replacement for stretchr/testify - -**The Math:** -- 127 assertion functions × 4-8 variants = 840 functions -- Old model: Manually maintain 840 functions across multiple packages -- New model: Write 127 functions once, generate the rest -- Result: 85% reduction in manual code maintenance - -### Technical Innovations - -**Go AST/Types Integration:** -- Scanner uses `go/packages` and `go/types` for semantic analysis -- Position-based lookup bridges AST and type information -- Import alias resolution for accurate code generation -- Handles complex Go constructs (generics, interfaces, variadic args) - -**Scanner Architecture (Refactored):** -- Modular sub-packages for different extraction concerns: - - `comments/` - Doc comment extraction with integration tests - - `comments-parser/` - Domain tags, examples, and metadata parsing - - `signature/` - Function signature analysis -- Comprehensive test coverage (~1,435 lines across 4 test files) -- Extracts domain tags, domain descriptions, examples, and other metadata -- Integration tests validate end-to-end comment extraction - -**Example-Driven Testing:** -- "Examples:" sections in doc comments drive test generation -- success/failure/panic cases extracted automatically -- Tests generated for all 8 variants per function -- Achieves 100% coverage with minimal test complexity - -**Template Architecture:** -- Separate templates for code (assert/require) and documentation -- Code templates handle return values vs void functions -- Doc templates (doc_index.md.gotmpl, doc_page.md.gotmpl) generate Hugo markdown -- Mock selection based on FailNow requirements -- Consistent formatting and structure across all output - -**Documentation Generator:** -- Merges package-based documentation into domain-based organization -- Reorganizes functions from assert/require packages by domain -- Generates Hugo-compatible markdown with front matter -- Creates navigable API reference organized by concern (boolean, string, error, etc.) - -## Documentation Site - -A Hugo-based documentation site is automatically generated from the source code: -- **Generated content**: `docs/doc-site/api/` - Domain-organized markdown files (19 domain pages) -- **Hugo site**: `hack/doc-site/hugo/` - Hugo configuration and theme (temporary location) -- **Target URL**: `https://go-openapi.github.io/testify/` - -**Site workflow:** -1. `codegen` generates markdown in `docs/doc-site/api/` -2. Hugo builds static site from the generated markdown -3. Hugo mounts the `docs/doc-site/` directory as content - -**Generated domain pages in `docs/doc-site/api/`:** -- `_index.md` - API index page -- `boolean.md`, `collection.md`, `common.md`, `comparison.md`, `condition.md` -- `equality.md`, `error.md`, `file.md`, `http.md`, `json.md` -- `number.md`, `ordering.md`, `panic.md`, `string.md`, `testing.md` -- `time.md`, `type.md`, `yaml.md` - -**Hugo configuration in `hack/doc-site/hugo/`:** -``` -hack/doc-site/hugo/ # Note: Temporary location -├── hugo.yaml # Main Hugo configuration -├── testify.yaml # Generated config with version info -├── testify.yaml.template # Template for testify.yaml -├── gendoc.go # Development server launcher -├── README.md, TODO.md # Documentation and planning -├── themes/ -│ └── hugo-relearn/ # Documentation theme -├── layouts/ # Custom layout overrides -└── content/ # Mounted from docs/doc-site/ -``` - -## Example Coverage Status - -Most assertion functions now have "Examples:" sections in their doc comments. The generator extracts these to create both tests and documentation examples. - -**Coverage notes:** -- Basic assertions (Equal, Error, Contains, Len, True, False) have complete examples -- Complex values that can't be easily represented should use `// NOT IMPLEMENTED` as placeholder marker - - **Never use `// TODO`** - it triggers false positives in code quality analyzers - - Use: `success: &structValue{}, // NOT IMPLEMENTED` - - Not: `success: &structValue{}, // TODO` -- All new assertions should include Examples and domain tags before merging -- Domain tags organize assertions into logical groups for documentation - -For the complete guide on adding examples, see `docs/MAINTAINERS.md` section "Maintaining Generated Code". - -## Scanner Testing and Architecture - -The scanner package has comprehensive test coverage to ensure reliable code generation: - -**Test files (~1,435 lines total):** -- `comments-parser/examples_test.go` (350 lines) - Tests for example extraction from doc comments -- `comments-parser/matchers_test.go` (171 lines) - Tests for pattern matching in comments -- `comments-parser/tags_test.go` (407 lines) - Tests for domain and metadata tag extraction -- `comments/extractor_integration_test.go` (507 lines) - End-to-end comment extraction tests - -**Scanner responsibilities:** -1. Parse Go packages using `go/packages` and `go/types` -2. Extract doc comments and parse structured metadata -3. Identify assertion functions and their signatures -4. Extract domain tags for documentation organization -5. Parse "Examples:" sections for test generation -6. Collect domain descriptions from special comment tags -7. Build the model used by code and doc generators - -**Testing strategy:** -- Unit tests for individual parsers (examples, tags, matchers) -- Integration tests validate complete extraction pipeline -- Tests use real Go code samples from `testdata/` -- Ensures generated code and docs stay in sync with source +- License: Apache-2.0 diff --git a/.claude/rules/contributions.md b/.claude/rules/contributions.md new file mode 100644 index 000000000..58027b9cc --- /dev/null +++ b/.claude/rules/contributions.md @@ -0,0 +1,52 @@ +--- +paths: + - "**/*" +--- + +# Contribution rules (go-openapi) + +Read `.github/CONTRIBUTING.md` before opening a pull request. + +## Commit hygiene + +- Every commit **must** be DCO signed-off (`git commit -s`) with a real email address. + PGP-signed commits are appreciated but not required. +- Agents may be listed as co-authors (`Co-Authored-By:`) but the commit **author must be the human sponsor**. + We do not accept commits solely authored by bots or agents. +- Squash commits into logical units of work before requesting review (`git rebase -i`). + +## Linting + +Before pushing, verify your changes pass linting against the base branch: + +```sh +golangci-lint run --new-from-rev master +``` + +Install the latest version if you don't have it: + +```sh +go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest +``` + +## Problem statement + +- Clearly describe the problem the PR solves, or reference an existing issue. +- PR descriptions must not be vague ("fix bug", "improve code") — explain *what* was wrong and *why* the change is correct. + +## Tests are mandatory + +- Every bug fix or feature **must** include tests that demonstrate the problem and verify the fix. +- The only exceptions are documentation changes and typo fixes. +- Aim for at least 80% coverage of your patch. +- Run the full test suite before submitting: + +For mono-repos: +```sh +go test work ./... +``` + +For single module repos: +```sh +go test ./... +``` diff --git a/.claude/rules/github-workflows-conventions.md b/.claude/rules/github-workflows-conventions.md new file mode 100644 index 000000000..33800d0eb --- /dev/null +++ b/.claude/rules/github-workflows-conventions.md @@ -0,0 +1,297 @@ +--- +paths: + - ".github/workflows/**.yml" + - ".github/workflows/**.yaml" +--- + +# GitHub Actions Workflows Formatting and Style Conventions + +This rule captures YAML and bash formatting rules to provide a consistent maintainer's experience across CI workflows. + +## File Structure + +**REQUIRED**: All github action workflows are organized as a flat structure beneath `.github/workflows/`. + +> GitHub does not support a hierarchical organization for workflows yet. + +**REQUIRED**: YAML files are conventionally named `{workflow}.yml`, with the `.yml` extension. + +## Code Style & Formatting + +### Expression Spacing + +**REQUIRED**: All GitHub Actions expressions must have spaces inside the braces: + +```yaml +# ✅ CORRECT +env: + PR_URL: ${{ github.event.pull_request.html_url }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + +# ❌ WRONG +env: + PR_URL: ${{github.event.pull_request.html_url}} + TOKEN: ${{secrets.GITHUB_TOKEN}} +``` + +> Provides a consistent formatting rule. + +### Conditional Syntax + +**REQUIRED**: Always use `${{ }}` in `if:` conditions: + +```yaml +# ✅ CORRECT +if: ${{ inputs.enable-signing == 'true' }} +if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} + +# ❌ WRONG (works but inconsistent) +if: inputs.enable-signing == 'true' +``` + +> Provides a consistent formatting rule. + +### GitHub Workflow Commands + +**REQUIRED**: Use workflow commands for status messages that should appear as annotations, with **double colon separator**: + +```bash +# ✅ CORRECT - Double colon (::) separator after title +echo "::notice title=build::Build completed successfully" +echo "::warning title=race-condition::Merge already in progress" +echo "::error title=deployment::Failed to deploy" + +# ❌ WRONG - Single colon separator (won't render as annotation) +echo "::notice title=build:Build completed" # Missing second ':' +echo "::warning title=x:message" # Won't display correctly +``` + +**Syntax pattern:** `::LEVEL title=TITLE::MESSAGE` +- `LEVEL`: notice, warning, or error +- Double `::` separator is required between title and message + +> Wrong syntax may raise untidy warnings and produce botched output. + +### YAML arrays formatting + +For steps, YAML arrays are formatted with the following indentation: + +```yaml +# ✅ CORRECT - Clear spacing between steps + steps: + - + name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + - + name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + +# ❌ WRONG - Dense format, more difficult to read + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + +# ❌ WRONG - YAML comment or blank line could be avoided + steps: + # + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 +``` + +## Security Best Practices + +### Version Pinning using SHAs + +**REQUIRED**: Always pin action versions to commit SHAs: + +> Runs must be repeatable with known pinned version. Automated updates are pushed frequently (e.g. daily or weekly) +> to keep pinned versions up-to-date. + +```yaml +# ✅ CORRECT - Pinned to commit SHA with version comment +uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 +uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 + +# ❌ WRONG - Mutable tag reference +uses: actions/checkout@v6 +``` + +### Permission settings + +**REQUIRED**: Always set minimal permissions at the workflow level. + +```yaml +# ✅ CORRECT - Workflow level permissions set to minimum +permissions: + contents: read + +# ❌ WRONG - Workflow level permissions with undue privilege escalation +permissions: + contents: write + pull-requests: write +``` + +**REQUIRED**: Whenever a job needs elevated privileges, always raise required permissions at the job level. + +```yaml +# ✅ CORRECT - Job level permissions set to the specific requirements for that job +jobs: + dependabot: + permissions: + contents: write + pull-requests: write + uses: ./.github/workflows/auto-merge.yml + secrets: inherit + +# ❌ WRONG - Same permissions but set at workflow level instead of job level +permissions: + contents: write + pull-requests: write +``` + +> (Security best practice detected by CodeQL analysis) + +### Undue secret exposure + +**NEVER** use `secrets[inputs.name]` — always use explicit secret parameters. + +> Using keyed access to secrets forces the runner to expose ALL secrets to the job, which causes a security risk +> (caught and reported by CodeQL security analysis). + +```yaml +# ❌ SECURITY VULNERABILITY +# This exposes ALL organization and repository secrets to the runner +on: + workflow_call: + inputs: + secret-name: + type: string +jobs: + my-job: + steps: + - uses: some-action@v1 + with: + token: ${{ secrets[inputs.secret-name] }} # ❌ DANGEROUS! +``` + +**SOLUTION**: Use explicit secret parameters with fallback for defaults: + +```yaml +# ✅ SECURE +on: + workflow_call: + secrets: + gpg-private-key: + required: false +jobs: + my-job: + steps: + - uses: go-openapi/gh-actions/ci-jobs/bot-credentials@master + with: + # Falls back to go-openapi default if not explicitly passed + gpg-private-key: ${{ secrets.gpg-private-key || secrets.CI_BOT_GPG_PRIVATE_KEY }} +``` + +## Common Gotchas + +### Description fields containing parsable expressions + +**REQUIRED**: **DO NOT** use `${{ }}` expressions in description fields: + +> They may be parsed by the runner, wrongly interpreted or causing failure (e.g. "not defined in this context"). + +```yaml +# ❌ WRONG - Can cause YAML parsing errors +description: | + Pass it as: gpg-private-key: ${{ secrets.MY_KEY }} + +# ✅ CORRECT +description: | + Pass it as: secrets.MY_KEY +``` + +### Boolean inputs + +**Boolean inputs are forbidden**: NEVER use `type: boolean` for workflow inputs due to unpredictable type coercion + +> gh-action expressions using boolean job inputs are hard to predict and come with many quirks. + + ```yaml + # ❌ FORBIDDEN - Boolean inputs have type coercion issues + on: + workflow_call: + inputs: + enable-feature: + type: boolean # ❌ NEVER USE THIS + default: true + + # The pattern `x == 'true' || x == true` seems safe but fails when: + # - x is not a boolean: `x == true` evaluates to true if x != null + # - Type coercion is unpredictable and error-prone + + # ✅ CORRECT - Always use string type for boolean-like inputs + on: + workflow_call: + inputs: + enable-feature: + type: string # ✅ Use string instead + default: 'true' # String value + + jobs: + my-job: + # Simple, reliable comparison + if: ${{ inputs.enable-feature == 'true' }} + + # ✅ In bash, this works perfectly (inputs are always strings in bash): + if [[ '${{ inputs.enable-feature }}' == 'true' ]]; then + echo "Feature enabled" + fi + ``` + + **Rule**: Use `type: string` with values `'true'` or `'false'` for all boolean-like workflow inputs. + + **Note**: Step outputs and bash variables are always strings, so `x == 'true'` works fine for those. + +### YAML fold scalars in action inputs + +**NEVER** use `>` or `>-` (fold scalars) for `with:` input values: + +> The YAML spec says fold scalars replace newlines with spaces, but the GitHub Actions runner +> does not reliably honor this for action inputs. The action receives the literal multi-line string +> instead of a single folded line, which breaks flag parsing. + +```yaml +# ❌ BROKEN - Fold scalar, args received with embedded newlines +- uses: goreleaser/goreleaser-action@... + with: + args: >- + release + --clean + --release-notes /tmp/notes.md + +# ✅ CORRECT - Single line +- uses: goreleaser/goreleaser-action@... + with: + args: release --clean --release-notes /tmp/notes.md + +# ✅ CORRECT - Literal block scalar (|) is fine for run: scripts +- run: | + echo "line 1" + echo "line 2" +``` + +**Rule**: Use single-line strings for `with:` inputs. Only use `|` (literal block scalar) for `run:` scripts where multi-line is intentional. diff --git a/.claude/rules/go-conventions.md b/.claude/rules/go-conventions.md new file mode 100644 index 000000000..9c2c92406 --- /dev/null +++ b/.claude/rules/go-conventions.md @@ -0,0 +1,11 @@ +--- +paths: + - "**/*.go" +--- + +# Code conventions (go-openapi) + +- All files must have SPDX license headers (Apache-2.0). +- Go version policy: support the 2 latest stable Go minor versions. +- Commits require DCO sign-off (`git commit -s`). +- use `golangci-lint fmt` to format code (not `gofmt` or `gofumpt`) diff --git a/.claude/rules/linting.md b/.claude/rules/linting.md new file mode 100644 index 000000000..a4456d423 --- /dev/null +++ b/.claude/rules/linting.md @@ -0,0 +1,17 @@ +--- +paths: + - "**/*.go" +--- + +# Linting conventions (go-openapi) + +```sh +golangci-lint run +``` + +Config: `.golangci.yml` — posture is `default: all` with explicit disables. +See `docs/STYLE.md` for the rationale behind each disabled linter. + +Key rules: +- Every `//nolint` directive **must** have an inline comment explaining why. +- Prefer disabling a linter over scattering `//nolint` across the codebase. diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 000000000..6974abaa1 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,47 @@ +--- +paths: + - "**/*_test.go" +--- + +# Testing conventions (go-openapi) + +## Running tests + +**Single module repos:** + +```sh +go test ./... +``` + +**Mono-repos (with `go.work`):** + +```sh +# All modules +go test work ./... + +# Single module +go test ./conv/... +``` + +Note: in mono-repos, plain `go test ./...` only tests the root module. +The `work` pattern expands to all modules listed in `go.work`. + +CI runs tests on `{ubuntu, macos, windows} x {stable, oldstable}` with `-race` via `gotestsum`. + +## Fuzz tests + +```sh +# List all fuzz targets +go test -list Fuzz ./... + +# Run a specific target (go test -fuzz cannot span multiple packages) +go test -fuzz=Fuzz -run='FuzzTargetName$' -fuzztime=1m30s ./package +``` + +Fuzz corpus lives in `testdata/fuzz/` within each package. CI runs each fuzz target for 1m30s +with a 5m minimize timeout. + +## Test framework + +`github.com/go-openapi/testify/v2` — a zero-dep fork of `stretchr/testify`. +Because it's a fork, `testifylint` does not work. diff --git a/.github/copilot b/.github/copilot new file mode 120000 index 000000000..526948310 --- /dev/null +++ b/.github/copilot @@ -0,0 +1 @@ +../.claude/rules \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..f24f91764 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,44 @@ +# Copilot Instructions + +This is the `go-openapi/testify` repository — a zero-dependency fork of +`stretchr/testify` used across all go-openapi repositories. + +## Architecture + +All assertion implementations live in `internal/assertions/`, organized by domain. +The `assert/` and `require/` packages are **generated** from that source via +`go generate ./...`. Do not edit generated files directly. + +Each assertion function generates up to 8 variants (package-level, format, forward, +forward-format, in both assert and require). + +## Adding assertions + +1. Add function to appropriate domain file in `internal/assertions/` +2. Add `Examples:` and `domain:` tags in the doc comment +3. Add tests in the corresponding `*_test.go` file +4. Run `go generate ./...` + +## Optional features + +YAML and color support require blank imports of enable packages: + +```go +import _ "github.com/go-openapi/testify/v2/enable/yaml" +import _ "github.com/go-openapi/testify/v2/enable/colors" +``` + +## Conventions + +Coding conventions are found beneath `.github/copilot` + +### Summary + +- All `.go` files must have SPDX license headers (Apache-2.0). +- Commits require DCO sign-off (`git commit -s`). +- Linting: `golangci-lint run` — config in `.golangci.yml` (posture: `default: all` with explicit disables). +- Every `//nolint` directive **must** have an inline comment explaining why. +- Tests: `go test work ./...` (mono-repo). CI runs on `{ubuntu, macos, windows} x {stable, oldstable}` with `-race`. +- Test framework: this IS the test framework (`github.com/go-openapi/testify/v2`); `testifylint` does not work with this fork. + +See `.github/copilot/` (symlinked to `.claude/rules/`) for detailed rules on Go conventions, linting, testing, and contributions. diff --git a/.gitignore b/.gitignore index 36bbc154d..1680db44c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,3 @@ Godeps .idea *.out .mcp.json -.claude/**.md -!.claude/CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 000000000..02dd13412 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +.github/copilot-instructions.md \ No newline at end of file