diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 3e5eed3..51ee0ca 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -2,7 +2,7 @@ name: autofix.ci on: pull_request: push: - branches: [ "master" ] + branches: ["master"] permissions: contents: read @@ -11,13 +11,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Task uses: go-task/setup-task@v1 - uses: actions/setup-go@v6 - with: { go-version-file: 'go.mod' } + with: { go-version-file: "go.mod" } - name: Setup go deps run: | @@ -34,7 +34,7 @@ jobs: run: golangci-lint run --fix - name: Run golangci-lint format - run: golangci-lint format + run: golangci-lint fmt - name: Run go mod tidy run: go mod tidy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01787a7..6b95144 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: ['master', 'develop'] + branches: ["master", "develop"] pull_request: env: @@ -21,12 +21,12 @@ jobs: contents: read pull-requests: read steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version: stable - name: golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v9 with: { version: latest } test: @@ -45,7 +45,7 @@ jobs: - 1.25.x steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v6 @@ -206,7 +206,7 @@ jobs: - name: Upload test artifacts if: failure() - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: test-results-go-${{ matrix.go }} path: | @@ -297,7 +297,7 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 @@ -342,10 +342,10 @@ jobs: contents: read if: github.event_name == 'pull_request' steps: - - name: 'Checkout Repository' - uses: actions/checkout@v5 + - name: "Checkout Repository" + uses: actions/checkout@v6 - - name: 'Dependency Review' + - name: "Dependency Review" uses: actions/dependency-review-action@v4 with: fail-on-severity: moderate @@ -364,7 +364,7 @@ jobs: startsWith(github.ref, 'refs/heads/feature/docker')) steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Login to Docker Hub uses: docker/login-action@v3 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6299e32..a9a85a4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -61,7 +61,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 60358ad..ca1a6f8 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: 'Dependency Review' uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13ac330..98e2bb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -88,7 +88,7 @@ jobs: packages: write steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Login to Docker Hub uses: docker/login-action@v3 diff --git a/AGENTS.md b/AGENTS.md index 4f5eb1b..d0b5c1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,56 +1,178 @@ # Agent Guidelines for articulate-parser +A Go CLI tool that parses Articulate Rise courses from URLs or local JSON files and exports them to Markdown, HTML, or DOCX formats. + ## Build/Test Commands -- **Build**: `task build` or `go build -o bin/articulate-parser main.go` -- **Run tests**: `task test` or `go test -race -timeout 5m ./...` -- **Run single test**: `go test -v -race -run ^TestName$ ./path/to/package` -- **Test with coverage**: - - `task test:coverage` or - - `go test -race -coverprofile=coverage/coverage.out -covermode=atomic ./...` -- **Lint**: `task lint` (runs vet, fmt check, staticcheck, golangci-lint) -- **Format**: `task fmt` or `gofmt -s -w .` -- **CI checks**: `task ci` (deps, lint, test with coverage, build) + +### Primary Commands (using Taskfile) + +```bash +task build # Build binary to bin/articulate-parser +task test # Run all tests with race detection +task lint # Run all linters (vet, fmt, staticcheck, golangci-lint) +task fmt # Format all Go files +task ci # Full CI pipeline: deps, lint, test with coverage, build +task qa # Quick QA: fmt + lint + test +``` + +### Direct Go Commands + +```bash +# Build +go build -o bin/articulate-parser main.go + +# Run all tests +go test -race -timeout 5m ./... + +# Run single test by name +go test -v -race -run ^TestMarkdownExporter_Export$ ./internal/exporters + +# Run tests in specific package +go test -v -race ./internal/services + +# Run tests matching pattern +go test -v -race -run "TestParser" ./... + +# Test with coverage +go test -race -coverprofile=coverage/coverage.out -covermode=atomic ./... +go tool cover -html=coverage/coverage.out -o coverage/coverage.html + +# Benchmarks +go test -bench=. -benchmem ./... +go test -bench=BenchmarkMarkdownExporter ./internal/exporters +``` + +### Security & Auditing + +```bash +task security:check # Run gosec security scanner +task security:audit # Run govulncheck for vulnerabilities +``` ## Code Style Guidelines ### Imports + - Use `goimports` with local prefix: `github.com/kjanat/articulate-parser` -- Order: stdlib, external, internal packages -- Group related imports together +- Order: stdlib, blank line, external packages, blank line, internal packages + +```go +import ( + "context" + "fmt" + + "github.com/fumiama/go-docx" + + "github.com/kjanat/articulate-parser/internal/interfaces" +) +``` ### Formatting + - Use `gofmt -s` (simplify) and `gofumpt` with extra rules - Function length: max 100 lines, 50 statements -- Cyclomatic complexity: max 15 -- Cognitive complexity: max 20 +- Cyclomatic complexity: max 15; Cognitive complexity: max 20 ### Types & Naming + - Use interface-based design (see `internal/interfaces/`) -- Export types/functions with clear godoc comments ending with period +- Exported types/functions require godoc comments ending with period - Use descriptive names: `ArticulateParser`, `MarkdownExporter` - Receiver names: short (1-2 chars), consistent per type ### Error Handling + - Always wrap errors with context: `fmt.Errorf("operation failed: %w", err)` - Use `%w` verb for error wrapping to preserve error chain - Check all error returns (enforced by `errcheck`) - Document error handling rationale in defer blocks when ignoring close errors +```go +// Good: Error wrapping with context +if err := json.Unmarshal(body, &course); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) +} + +// Good: Documented defer with error handling +defer func() { + if err := resp.Body.Close(); err != nil { + p.Logger.Warn("failed to close response body", "error", err) + } +}() +``` + ### Comments + - All exported types/functions require godoc comments - End sentences with periods (`godot` linter enforced) - Mark known issues with TODO/FIXME/HACK/BUG/XXX ### Security -- Use `#nosec` with justification for deliberate security exceptions (G304 for CLI file paths, G306 for export file permissions) -- Run `gosec` and `govulncheck` for security audits + +- Use `#nosec` with justification for deliberate security exceptions +- G304: File paths from CLI args; G306: Export file permissions + +```go +// #nosec G304 - File path provided by user via CLI argument +data, err := os.ReadFile(filePath) +``` ### Testing -- Enable race detection: `-race` flag + +- Enable race detection: `-race` flag always - Use table-driven tests where applicable - Mark test helpers with `t.Helper()` +- Use `t.TempDir()` for temporary files - Benchmarks in `*_bench_test.go`, examples in `*_example_test.go` +- Test naming: `Test_` or `Test` + +```go +func TestMarkdownExporter_ProcessItemToMarkdown_AllTypes(t *testing.T) { + tests := []struct { + name, itemType, expectedText string + }{ + {"text item", "text", ""}, + {"divider item", "divider", "---"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // test implementation + }) + } +} +``` ### Dependencies -- Minimal external dependencies (currently: go-docx, golang.org/x/net, golang.org/x/text) + +- Minimal external dependencies (go-docx, golang.org/x/net, golang.org/x/text) - Run `task deps:tidy` after adding/removing dependencies +- CGO disabled by default (`CGO_ENABLED=0`) + +## Project Structure + +``` +articulate-parser/ + internal/ + config/ # Configuration loading + exporters/ # Export implementations (markdown, html, docx) + interfaces/ # Core interfaces (Exporter, CourseParser, Logger) + models/ # Data models (Course, Lesson, Item, Media) + services/ # Core services (parser, html cleaner, app, logger) + version/ # Version information + main.go # Application entry point +``` + +## Common Patterns + +### Creating a new exporter + +1. Implement `interfaces.Exporter` interface +2. Add factory method to `internal/exporters/factory.go` +3. Register format in `NewFactory()` +4. Add tests following existing patterns + +### Adding configuration options + +1. Add field to `Config` struct in `internal/config/config.go` +2. Load from environment variable with sensible default +3. Document in config struct comments