diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b92398..b7ac4c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,47 +37,17 @@ jobs: go-version: ${{ matrix.go }} check-latest: true - - name: Download dependencies with retry - run: | - set -e - echo "Downloading Go dependencies..." + - name: Install Task + uses: go-task/setup-task@v1 - # Function to download with retry - download_with_retry() { - local attempt=1 - local max_attempts=3 + - name: Show build info + run: task info - while [ $attempt -le $max_attempts ]; do - echo "Attempt $attempt of $max_attempts" - - if go mod download; then - echo "Download successful on attempt $attempt" - return 0 - else - echo "Download failed on attempt $attempt" - if [ $attempt -lt $max_attempts ]; then - echo "Cleaning cache and retrying..." - go clean -modcache - go clean -cache - sleep 2 - fi - attempt=$((attempt + 1)) - fi - done - - echo "All download attempts failed" - return 1 - } - - # Try download with retry logic - download_with_retry - - echo "Verifying module dependencies..." - go mod verify - echo "Dependencies verified successfully" + - name: Download dependencies + run: task deps - name: Build - run: go build -v ./... + run: task build - name: Run tests with enhanced reporting id: test @@ -89,7 +59,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "Running tests with coverage..." - go test -v -race -coverprofile=coverage.out ./... 2>&1 | tee test-output.log + task test:coverage 2>&1 | tee test-output.log # Extract test results for summary TEST_STATUS=$? @@ -151,9 +121,8 @@ jobs: - name: Generate coverage report if: always() run: | - if [ -f coverage.out ]; then - go tool cover -html=coverage.out -o coverage.html - COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}') + if [ -f coverage/coverage.out ]; then + COVERAGE=$(go tool cover -func=coverage/coverage.out | grep total | awk '{print $3}') echo "## ๐Ÿ“Š Code Coverage (Go ${{ matrix.go }})" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -171,7 +140,7 @@ jobs: temp_coverage=$(mktemp) # Extract package-level coverage data - go tool cover -func=coverage.out | grep -v total | while read line; do + go tool cover -func=coverage/coverage.out | grep -v total | while read line; do if [[ $line == *".go:"* ]]; then # Extract package path from file path (everything before the filename) filepath=$(echo "$line" | awk '{print $1}') @@ -217,16 +186,16 @@ jobs: name: test-results-go-${{ matrix.go }} path: | test-output.log - coverage.out - coverage.html + coverage/ retention-days: 7 - - name: Run go vet + - name: Run linters run: | echo "## ๐Ÿ” Static Analysis (Go ${{ matrix.go }})" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - VET_OUTPUT=$(go vet ./... 2>&1 || echo "") + # Run go vet + VET_OUTPUT=$(task lint:vet 2>&1 || echo "") VET_STATUS=$? if [ $VET_STATUS -eq 0 ]; then @@ -240,13 +209,11 @@ jobs: fi echo "" >> $GITHUB_STEP_SUMMARY - exit $VET_STATUS + # Run go fmt check + FMT_OUTPUT=$(task lint:fmt 2>&1 || echo "") + FMT_STATUS=$? - - name: Run go fmt - run: | - FMT_OUTPUT=$(gofmt -s -l . 2>&1 || echo "") - - if [ -z "$FMT_OUTPUT" ]; then + if [ $FMT_STATUS -eq 0 ]; then echo "โœ… **go fmt:** All files properly formatted" >> $GITHUB_STEP_SUMMARY else echo "โŒ **go fmt:** Files need formatting" >> $GITHUB_STEP_SUMMARY @@ -254,7 +221,10 @@ jobs: echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "$FMT_OUTPUT" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY + fi + + # Exit with error if any linter failed + if [ $VET_STATUS -ne 0 ] || [ $FMT_STATUS -ne 0 ]; then exit 1 fi @@ -276,6 +246,7 @@ jobs: - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 with: + files: ./coverage/coverage.out flags: Go ${{ matrix.go }} slug: kjanat/articulate-parser token: ${{ secrets.CODECOV_TOKEN }} @@ -297,40 +268,37 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + check-latest: true + + - name: Install Task + uses: go-task/setup-task@v1 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Capture build date - run: echo "BUILD_TIME=$(git log -1 --format=%cd --date=iso-strict)" >> $GITHUB_ENV + - name: Build Docker image using Task + run: task docker:build - - name: Build Docker image (test) - uses: docker/build-push-action@v6 - with: - context: . - push: false - load: true - tags: test:latest - build-args: | - VERSION=test - BUILD_TIME=${{ env.BUILD_TIME }} - GIT_COMMIT=${{ github.sha }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Test Docker image + - name: Test Docker image using Task run: | echo "## ๐Ÿงช Docker Image Tests" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - # Test that the image runs and shows help + # Run Task docker test + task docker:test + echo "**Testing help command:**" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - docker run --rm test:latest --help >> $GITHUB_STEP_SUMMARY + docker run --rm articulate-parser:latest --help >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Test image size - IMAGE_SIZE=$(docker image inspect test:latest --format='{{.Size}}' | numfmt --to=iec-i --suffix=B) + IMAGE_SIZE=$(docker image inspect articulate-parser:latest --format='{{.Size}}' | numfmt --to=iec-i --suffix=B) echo "**Image size:** $IMAGE_SIZE" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 7b7417a..96ec799 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,5 @@ main_coverage # Editors .vscode/ .idea/ + +.task/ diff --git a/Dockerfile.dev b/Dockerfile.dev index 8f06374..47e34b9 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -49,7 +49,7 @@ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \ RUN file /app/articulate-parser || echo "file command not available" # Development stage - uses Alpine for shell access -FROM alpine:3.22.1 +FROM alpine:3 # Install minimal dependencies RUN apk add --no-cache ca-certificates tzdata diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..d861982 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,571 @@ +# yaml-language-server: $schema=https://taskfile.dev/schema.json +# Articulate Parser - Task Automation +# https://taskfile.dev +version: '3' + +# Global output settings +output: prefixed + +# Shell settings (only applied on Unix-like systems) +# Note: These are ignored on Windows where PowerShell/cmd is used +set: [errexit, pipefail] +shopt: [globstar] + +# Watch mode interval +interval: 500ms + +# Global variables +vars: + APP_NAME: articulate-parser + MAIN_FILE: main.go + OUTPUT_DIR: bin + COVERAGE_DIR: coverage + TEST_TIMEOUT: 5m + + # Version info + VERSION: + sh: git describe --tags --always --dirty 2>/dev/null || echo "dev" + GIT_COMMIT: + sh: git rev-parse --short HEAD 2>/dev/null || echo "unknown" + BUILD_TIME: '{{now | date "2006-01-02T15:04:05Z07:00"}}' + + # Go settings + CGO_ENABLED: 0 + GO_FLAGS: -v + LDFLAGS: >- + -s -w + -X github.com/kjanat/articulate-parser/internal/version.Version={{.VERSION}} + -X github.com/kjanat/articulate-parser/internal/version.BuildTime={{.BUILD_TIME}} + -X github.com/kjanat/articulate-parser/internal/version.GitCommit={{.GIT_COMMIT}} + + # Platform detection (using Task built-in variables) + GOOS: + sh: go env GOOS + GOARCH: + sh: go env GOARCH + EXE_EXT: '{{if eq OS "windows"}}.exe{{end}}' + +# Environment variables +env: + CGO_ENABLED: '{{.CGO_ENABLED}}' + GO111MODULE: on + +# Load .env files if present +dotenv: ['.env', '.env.local'] + +# Task definitions +tasks: + # Default task - show help + default: + desc: Show available tasks + cmds: + - task --list + silent: true + + # Development tasks + dev: + desc: Run the application in development mode (with hot reload) + aliases: [run, start] + interactive: true + watch: true + sources: + - '**/*.go' + - go.mod + - go.sum + cmds: + - task: build + - '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} --help' + + # Build tasks + build: + desc: Build the application binary + aliases: [b] + deps: [clean-bin] + sources: + - '**/*.go' + - go.mod + - go.sum + generates: + - '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}}' + cmds: + - task: mkdir + vars: { DIR: '{{.OUTPUT_DIR}}' } + - go build {{.GO_FLAGS}} -ldflags="{{.LDFLAGS}}" -o {{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} {{.MAIN_FILE}} + method: checksum + + build:all: + desc: Build binaries for all major platforms + aliases: [build-all, cross-compile] + deps: [clean-bin] + cmds: + - task: mkdir + vars: { DIR: '{{.OUTPUT_DIR}}' } + - for: + matrix: + GOOS: [linux, darwin, windows] + GOARCH: [amd64, arm64] + task: build:platform + vars: + TARGET_GOOS: '{{.ITEM.GOOS}}' + TARGET_GOARCH: '{{.ITEM.GOARCH}}' + - echo "Built binaries for all platforms in {{.OUTPUT_DIR}}/" + + build:platform: + internal: true + vars: + TARGET_EXT: '{{if eq .TARGET_GOOS "windows"}}.exe{{end}}' + OUTPUT_FILE: '{{.OUTPUT_DIR}}/{{.APP_NAME}}-{{.TARGET_GOOS}}-{{.TARGET_GOARCH}}{{.TARGET_EXT}}' + env: + GOOS: '{{.TARGET_GOOS}}' + GOARCH: '{{.TARGET_GOARCH}}' + cmds: + - echo "Building {{.OUTPUT_FILE}}..." + - go build {{.GO_FLAGS}} -ldflags="{{.LDFLAGS}}" -o "{{.OUTPUT_FILE}}" {{.MAIN_FILE}} + + # Install task + install: + desc: Install the binary to $GOPATH/bin + deps: [test] + cmds: + - go install -ldflags="{{.LDFLAGS}}" {{.MAIN_FILE}} + - echo "Installed {{.APP_NAME}} to $(go env GOPATH)/bin" + + # Testing tasks + test: + desc: Run all tests + aliases: [t] + cmds: + - go test {{.GO_FLAGS}} -race -timeout {{.TEST_TIMEOUT}} ./... + + test:coverage: + desc: Run tests with coverage report + aliases: [cover, cov] + deps: [clean-coverage] + cmds: + - task: mkdir + vars: { DIR: '{{.COVERAGE_DIR}}' } + - go test {{.GO_FLAGS}} -race -coverprofile={{.COVERAGE_DIR}}/coverage.out -covermode=atomic -timeout {{.TEST_TIMEOUT}} ./... + - go tool cover -html={{.COVERAGE_DIR}}/coverage.out -o {{.COVERAGE_DIR}}/coverage.html + - go tool cover -func={{.COVERAGE_DIR}}/coverage.out + - echo "Coverage report generated at {{.COVERAGE_DIR}}/coverage.html" + + test:verbose: + desc: Run tests with verbose output + aliases: [tv] + cmds: + - go test -v -race -timeout {{.TEST_TIMEOUT}} ./... + + test:watch: + desc: Run tests in watch mode + aliases: [tw] + watch: true + sources: + - '**/*.go' + cmds: + - task: test + + test:bench: + desc: Run benchmark tests + aliases: [bench] + cmds: + - go test -bench=. -benchmem -timeout {{.TEST_TIMEOUT}} ./... + + test:integration: + desc: Run integration tests + status: + - '{{if eq OS "windows"}}if not exist "main_test.go" exit 1{{else}}test ! -f "main_test.go"{{end}}' + cmds: + - go test -v -race -tags=integration -timeout {{.TEST_TIMEOUT}} ./... + + # Code quality tasks + lint: + desc: Run all linters + aliases: [l] + cmds: + - task: lint:vet + - task: lint:fmt + - task: lint:staticcheck + + lint:vet: + desc: Run go vet + cmds: + - go vet ./... + + lint:fmt: + desc: Check code formatting + vars: + UNFORMATTED: + sh: gofmt -s -l . + cmds: + - | + {{if ne .UNFORMATTED ""}} + echo "โŒ The following files need formatting:" + echo "{{.UNFORMATTED}}" + exit 1 + {{else}} + echo "All files are properly formatted" + {{end}} + + lint:staticcheck: + desc: Run staticcheck (install if needed) + vars: + HAS_STATICCHECK: + sh: '{{if eq OS "windows"}}where staticcheck 2>NUL{{else}}command -v staticcheck 2>/dev/null{{end}}' + cmds: + - '{{if eq .HAS_STATICCHECK ""}}echo "Installing staticcheck..." && go install honnef.co/go/tools/cmd/staticcheck@latest{{end}}' + - staticcheck ./... + ignore_error: true + + fmt: + desc: Format all Go files + aliases: [format] + cmds: + - gofmt -s -w . + - echo "Formatted all Go files" + + modernize: + desc: Modernize Go code to use modern idioms + aliases: [modern] + cmds: + - go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./... + - echo "Code modernized" + + # Dependency management + deps: + desc: Download and verify dependencies + aliases: [mod] + cmds: + - go mod download + - go mod verify + - echo "Dependencies downloaded and verified" + + deps:tidy: + desc: Tidy go.mod and go.sum + aliases: [tidy] + cmds: + - go mod tidy + - echo "Dependencies tidied" + + deps:update: + desc: Update all dependencies to latest versions + aliases: [update] + cmds: + - go get -u ./... + - go mod tidy + - echo "Dependencies updated" + + deps:graph: + desc: Display dependency graph + cmds: + - go mod graph + + # Docker tasks + docker:build: + desc: Build Docker image + aliases: [db] + cmds: + - | + docker build \ + --build-arg VERSION={{.VERSION}} \ + --build-arg BUILD_TIME={{.BUILD_TIME}} \ + --build-arg GIT_COMMIT={{.GIT_COMMIT}} \ + -t {{.APP_NAME}}:{{.VERSION}} \ + -t {{.APP_NAME}}:latest \ + . + - > + echo "Docker image built: {{.APP_NAME}}:{{.VERSION}}" + + docker:build:dev: + desc: Build development Docker image + cmds: + - docker build -f Dockerfile.dev -t {{.APP_NAME}}:dev . + - > + echo "Development Docker image built: {{.APP_NAME}}:dev" + + docker:run: + desc: Run Docker container + aliases: [dr] + deps: [docker:build] + cmds: + - docker run --rm {{.APP_NAME}}:{{.VERSION}} --help + + docker:test: + desc: Test Docker image + deps: [docker:build] + cmds: + - docker run --rm {{.APP_NAME}}:{{.VERSION}} --version + - echo "Docker image tested successfully" + + docker:compose:up: + desc: Start services with docker-compose + cmds: + - docker-compose up -d + + docker:compose:down: + desc: Stop services with docker-compose + cmds: + - docker-compose down + + # Cleanup tasks + clean: + desc: Clean all generated files + aliases: [c] + cmds: + - task: clean-bin + - task: clean-coverage + - task: clean-cache + - echo "All generated files cleaned" + + clean-bin: + desc: Remove built binaries + internal: true + cmds: + - task: rmdir + vars: { DIR: '{{.OUTPUT_DIR}}' } + + clean-coverage: + desc: Remove coverage files + internal: true + cmds: + - task: rmdir + vars: { DIR: '{{.COVERAGE_DIR}}' } + + clean-cache: + desc: Clean Go build and test cache + cmds: + - go clean -cache -testcache -modcache + - echo "Go caches cleaned" + + # CI/CD tasks + ci: + desc: Run all CI checks (test, lint, build) + cmds: + - task: deps + - task: lint + - task: test:coverage + - task: build + - echo "All CI checks passed" + + ci:local: + desc: Run CI checks locally with detailed output + cmds: + - echo "๐Ÿ” Running local CI checks..." + - echo "" + - echo "๐Ÿ“ฆ Checking dependencies..." + - task: deps + - echo "" + - echo "๐Ÿ”ง Running linters..." + - task: lint + - echo "" + - echo "๐Ÿงช Running tests with coverage..." + - task: test:coverage + - echo "" + - echo "๐Ÿ—๏ธ Building application..." + - task: build:all + - echo "" + - echo "All CI checks completed successfully!" + + # Release tasks + release:check: + desc: Check if ready for release + cmds: + - task: ci + - git diff --exit-code + - git diff --cached --exit-code + - echo "Ready for release" + + release:tag: + desc: Tag a new release (requires VERSION env var) + requires: + vars: [VERSION] + preconditions: + - sh: 'git diff --exit-code' + msg: 'Working directory is not clean' + - sh: 'git diff --cached --exit-code' + msg: 'Staging area is not clean' + cmds: + - git tag -a v{{.VERSION}} -m "Release v{{.VERSION}}" + - echo "Tagged v{{.VERSION}}" + - > + echo "Push with: git push origin v{{.VERSION}}" + + # Documentation tasks + docs:serve: + desc: Serve documentation locally + vars: + HAS_GODOC: + sh: '{{if eq OS "windows"}}where godoc 2>NUL{{else}}command -v godoc 2>/dev/null{{end}}' + cmds: + - '{{if eq .HAS_GODOC ""}}echo "Installing godoc..." && go install golang.org/x/tools/cmd/godoc@latest{{end}}' + - echo "๐Ÿ“š Serving documentation at http://localhost:6060" + - godoc -http=:6060 + interactive: true + + docs:coverage: + desc: Open coverage report in browser + deps: [test:coverage] + cmds: + - '{{if eq OS "darwin"}}open {{.COVERAGE_DIR}}/coverage.html{{else if eq OS "windows"}}start {{.COVERAGE_DIR}}/coverage.html{{else}}xdg-open {{.COVERAGE_DIR}}/coverage.html 2>/dev/null || echo "Please open {{.COVERAGE_DIR}}/coverage.html in your browser"{{end}}' + + # Info tasks + info: + desc: Display build information + vars: + GO_VERSION: + sh: go version + cmds: + - task: info:print + silent: true + + info:print: + internal: true + silent: true + vars: + GO_VERSION: + sh: go version + cmds: + - echo "Application Info:" + - echo " Name{{":"}} {{.APP_NAME}}" + - echo " Version{{":"}} {{.VERSION}}" + - echo " Git Commit{{":"}} {{.GIT_COMMIT}}" + - echo " Build Time{{":"}} {{.BUILD_TIME}}" + - echo "" + - echo "Go Environment{{":"}}" + - echo " Go Version{{":"}} {{.GO_VERSION}}" + - echo " GOOS{{":"}} {{.GOOS}}" + - echo " GOARCH{{":"}} {{.GOARCH}}" + - echo " CGO{{":"}} {{.CGO_ENABLED}}" + - echo "" + - echo "Paths{{":"}}" + - echo " Output Dir{{":"}} {{.OUTPUT_DIR}}" + - echo " Coverage{{":"}} {{.COVERAGE_DIR}}" + + # Security tasks + security:check: + desc: Run security checks with gosec + vars: + HAS_GOSEC: + sh: '{{if eq OS "windows"}}where gosec 2>NUL{{else}}command -v gosec 2>/dev/null{{end}}' + cmds: + - '{{if eq .HAS_GOSEC ""}}echo "Installing gosec..." && go install github.com/securego/gosec/v2/cmd/gosec@latest{{end}}' + - gosec ./... + ignore_error: true + + security:audit: + desc: Audit dependencies for vulnerabilities + vars: + HAS_GOVULNCHECK: + sh: '{{if eq OS "windows"}}where govulncheck 2>NUL{{else}}command -v govulncheck 2>/dev/null{{end}}' + cmds: + - '{{if eq .HAS_GOVULNCHECK ""}}echo "Installing govulncheck..." && go install golang.org/x/vuln/cmd/govulncheck@latest{{end}}' + - govulncheck ./... + + # Example/Demo tasks + demo:markdown: + desc: Demo - Convert sample to Markdown + status: + - '{{if eq OS "windows"}}if not exist "articulate-sample.json" exit 1{{else}}test ! -f "articulate-sample.json"{{end}}' + deps: [build] + cmds: + - '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} articulate-sample.json md output-demo.md' + - echo "Demo Markdown created{{:}} output-demo.md" + - defer: + task: rmfile + vars: { FILE: 'output-demo.md' } + + demo:html: + desc: Demo - Convert sample to HTML + status: + - '{{if eq OS "windows"}}if not exist "articulate-sample.json" exit 1{{else}}test ! -f "articulate-sample.json"{{end}}' + deps: [build] + cmds: + - '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} articulate-sample.json html output-demo.html' + - echo "Demo HTML created{{:}} output-demo.html" + - defer: + task: rmfile + vars: { FILE: 'output-demo.html' } + + demo:docx: + desc: Demo - Convert sample to DOCX + status: + - '{{if eq OS "windows"}}if not exist "articulate-sample.json" exit 1{{else}}test ! -f "articulate-sample.json"{{end}}' + deps: [build] + cmds: + - '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} articulate-sample.json docx output-demo.docx' + - echo "Demo DOCX created{{:}} output-demo.docx" + - defer: + task: rmfile + vars: { FILE: 'output-demo.docx' } + + # Performance profiling + profile:cpu: + desc: Run CPU profiling + cmds: + - go test -cpuprofile=cpu.prof -bench=. ./... + - go tool pprof -http=:8080 cpu.prof + - defer: + task: rmfile + vars: { FILE: 'cpu.prof' } + + profile:mem: + desc: Run memory profiling + cmds: + - go test -memprofile=mem.prof -bench=. ./... + - go tool pprof -http=:8080 mem.prof + - defer: + task: rmfile + vars: { FILE: 'mem.prof' } + + # Git hooks + hooks:install: + desc: Install git hooks + cmds: + - task: mkdir + vars: { DIR: '.git/hooks' } + - '{{if eq OS "windows"}}echo "#!/bin/sh" > .git/hooks/pre-commit && echo "task lint:fmt" >> .git/hooks/pre-commit{{else}}cat > .git/hooks/pre-commit << ''EOF''{{printf "\n"}}#!/bin/sh{{printf "\n"}}task lint:fmt{{printf "\n"}}EOF{{printf "\n"}}chmod +x .git/hooks/pre-commit{{end}}' + - echo "Git hooks installed" + + # Quick shortcuts + qa: + desc: Quick quality assurance (fmt + lint + test) + aliases: [q, quick] + cmds: + - task: fmt + - task: lint + - task: test + - echo "Quick QA passed" + + all: + desc: Build everything (clean + deps + test + build:all + docker:build) + cmds: + - task: clean + - task: deps:tidy + - task: test:coverage + - task: build:all + - task: docker:build + - echo "Full build completed!" + + # Cross-platform helper tasks + mkdir: + internal: true + requires: + vars: [DIR] + cmds: + - '{{if eq OS "windows"}}powershell -Command "New-Item -ItemType Directory -Force -Path ''{{.DIR}}'' | Out-Null"{{else}}mkdir -p "{{.DIR}}"{{end}}' + silent: true + + rmdir: + internal: true + requires: + vars: [DIR] + cmds: + - '{{if eq OS "windows"}}powershell -Command "if (Test-Path ''{{.DIR}}'') { Remove-Item -Recurse -Force ''{{.DIR}}'' }"{{else}}rm -rf "{{.DIR}}" 2>/dev/null || true{{end}}' + silent: true + + rmfile: + internal: true + requires: + vars: [FILE] + cmds: + - '{{if eq OS "windows"}}powershell -Command "if (Test-Path ''{{.FILE}}'') { Remove-Item -Force ''{{.FILE}}'' }"{{else}}rm -f "{{.FILE}}"{{end}}' + silent: true diff --git a/go.sum b/go.sum index f5a6c11..a03a79a 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,5 @@ github.com/fumiama/go-docx v0.0.0-20250506085032-0c30fd09304b h1:/mxSugRc4SgN7Xg github.com/fumiama/go-docx v0.0.0-20250506085032-0c30fd09304b/go.mod h1:ssRF0IaB1hCcKIObp3FkZOsjTcAHpgii70JelNb4H8M= github.com/fumiama/imgsz v0.0.4 h1:Lsasu2hdSSFS+vnD+nvR1UkiRMK7hcpyYCC0FzgSMFI= github.com/fumiama/imgsz v0.0.4/go.mod h1:bISOQVTlw9sRytPwe8ir7tAaEmyz9hSNj9n8mXMBG0E= -golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= -golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= diff --git a/internal/exporters/docx_test.go b/internal/exporters/docx_test.go index d16e4ee..6ef0f5c 100644 --- a/internal/exporters/docx_test.go +++ b/internal/exporters/docx_test.go @@ -615,8 +615,7 @@ func BenchmarkDocxExporter_Export(b *testing.B) { // Create temporary directory tempDir := b.TempDir() - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { outputPath := filepath.Join(tempDir, "benchmark-course.docx") _ = exporter.Export(course, outputPath) // Clean up for next iteration @@ -641,7 +640,7 @@ func BenchmarkDocxExporter_ComplexCourse(b *testing.B) { } // Fill with test data - for i := 0; i < 10; i++ { + for i := range 10 { lesson := models.Lesson{ ID: "lesson-" + string(rune(i)), Title: "Lesson " + string(rune(i)), @@ -649,13 +648,13 @@ func BenchmarkDocxExporter_ComplexCourse(b *testing.B) { Items: make([]models.Item, 5), // 5 items per lesson } - for j := 0; j < 5; j++ { + for j := range 5 { item := models.Item{ Type: "text", Items: make([]models.SubItem, 3), // 3 sub-items per item } - for k := 0; k < 3; k++ { + for k := range 3 { item.Items[k] = models.SubItem{ Heading: "

Heading " + string(rune(k)) + "

", Paragraph: "

Paragraph content with formatting for performance testing.

", @@ -670,8 +669,7 @@ func BenchmarkDocxExporter_ComplexCourse(b *testing.B) { tempDir := b.TempDir() - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { outputPath := filepath.Join(tempDir, "benchmark-complex.docx") _ = exporter.Export(course, outputPath) os.Remove(outputPath) diff --git a/internal/exporters/factory_test.go b/internal/exporters/factory_test.go index f0cf853..464564b 100644 --- a/internal/exporters/factory_test.go +++ b/internal/exporters/factory_test.go @@ -449,8 +449,7 @@ func BenchmarkFactory_CreateExporter(b *testing.B) { htmlCleaner := services.NewHTMLCleaner() factory := NewFactory(htmlCleaner) - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { _, _ = factory.CreateExporter("markdown") } } @@ -460,8 +459,7 @@ func BenchmarkFactory_CreateExporter_Docx(b *testing.B) { htmlCleaner := services.NewHTMLCleaner() factory := NewFactory(htmlCleaner) - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { _, _ = factory.CreateExporter("docx") } } @@ -471,8 +469,7 @@ func BenchmarkFactory_GetSupportedFormats(b *testing.B) { htmlCleaner := services.NewHTMLCleaner() factory := NewFactory(htmlCleaner) - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { _ = factory.GetSupportedFormats() } } diff --git a/internal/exporters/html_test.go b/internal/exporters/html_test.go index 78e95a4..cb1da1d 100644 --- a/internal/exporters/html_test.go +++ b/internal/exporters/html_test.go @@ -841,8 +841,7 @@ func BenchmarkHTMLExporter_Export(b *testing.B) { // Create temporary directory tempDir := b.TempDir() - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { outputPath := filepath.Join(tempDir, "benchmark-course.html") _ = exporter.Export(course, outputPath) // Clean up for next iteration @@ -865,8 +864,7 @@ func BenchmarkHTMLExporter_ProcessTextItem(b *testing.B) { }, } - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { var buf bytes.Buffer exporter.processTextItem(&buf, item) } @@ -889,7 +887,7 @@ func BenchmarkHTMLExporter_ComplexCourse(b *testing.B) { } // Fill with test data - for i := 0; i < 10; i++ { + for i := range 10 { lesson := models.Lesson{ ID: "lesson-" + string(rune(i)), Title: "Lesson " + string(rune(i)), @@ -897,13 +895,13 @@ func BenchmarkHTMLExporter_ComplexCourse(b *testing.B) { Items: make([]models.Item, 5), // 5 items per lesson } - for j := 0; j < 5; j++ { + for j := range 5 { item := models.Item{ Type: "text", Items: make([]models.SubItem, 3), // 3 sub-items per item } - for k := 0; k < 3; k++ { + for k := range 3 { item.Items[k] = models.SubItem{ Heading: "

Heading " + string(rune(k)) + "

", Paragraph: "

Paragraph content with formatting for performance testing.

", @@ -918,8 +916,7 @@ func BenchmarkHTMLExporter_ComplexCourse(b *testing.B) { tempDir := b.TempDir() - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { outputPath := filepath.Join(tempDir, "benchmark-complex.html") _ = exporter.Export(course, outputPath) os.Remove(outputPath) diff --git a/internal/exporters/markdown_test.go b/internal/exporters/markdown_test.go index 47e55ac..ec0db18 100644 --- a/internal/exporters/markdown_test.go +++ b/internal/exporters/markdown_test.go @@ -661,8 +661,7 @@ func BenchmarkMarkdownExporter_Export(b *testing.B) { // Create temporary directory tempDir := b.TempDir() - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { outputPath := filepath.Join(tempDir, "benchmark-course.md") _ = exporter.Export(course, outputPath) // Clean up for next iteration @@ -685,8 +684,7 @@ func BenchmarkMarkdownExporter_ProcessTextItem(b *testing.B) { }, } - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { var buf bytes.Buffer exporter.processTextItem(&buf, item, "###") } diff --git a/internal/models/lesson.go b/internal/models/lesson.go index cd0d654..26a9aa5 100644 --- a/internal/models/lesson.go +++ b/internal/models/lesson.go @@ -18,7 +18,7 @@ type Lesson struct { // Items is an ordered array of content items within the lesson Items []Item `json:"items"` // Position stores the ordering information for the lesson - Position interface{} `json:"position"` + Position any `json:"position"` // Ready indicates whether the lesson is marked as complete Ready bool `json:"ready"` // CreatedAt is the timestamp when the lesson was created @@ -41,9 +41,9 @@ type Item struct { // Items contains the actual content elements (sub-items) of this item Items []SubItem `json:"items"` // Settings contains configuration options specific to this item type - Settings interface{} `json:"settings"` + Settings any `json:"settings"` // Data contains additional structured data for the item - Data interface{} `json:"data"` + Data any `json:"data"` // Media contains any associated media for the item Media *Media `json:"media,omitempty"` } diff --git a/internal/models/models_test.go b/internal/models/models_test.go index 9bbd344..704efdd 100644 --- a/internal/models/models_test.go +++ b/internal/models/models_test.go @@ -133,7 +133,7 @@ func TestLesson_JSONMarshalUnmarshal(t *testing.T) { Ready: true, CreatedAt: "2023-06-01T12:00:00Z", UpdatedAt: "2023-06-01T13:00:00Z", - Position: map[string]interface{}{"x": 1, "y": 2}, + Position: map[string]any{"x": 1, "y": 2}, Items: []Item{ { ID: "item-test", @@ -154,8 +154,8 @@ func TestLesson_JSONMarshalUnmarshal(t *testing.T) { }, }, }, - Settings: map[string]interface{}{"autoplay": false}, - Data: map[string]interface{}{"metadata": "test"}, + Settings: map[string]any{"autoplay": false}, + Data: map[string]any{"metadata": "test"}, }, }, } @@ -197,11 +197,11 @@ func TestItem_JSONMarshalUnmarshal(t *testing.T) { Feedback: "Well done!", }, }, - Settings: map[string]interface{}{ + Settings: map[string]any{ "allowRetry": true, "showAnswer": true, }, - Data: map[string]interface{}{ + Data: map[string]any{ "points": 10, "weight": 1.5, }, @@ -475,7 +475,7 @@ func TestLabelSet_JSONMarshalUnmarshal(t *testing.T) { func TestEmptyStructures(t *testing.T) { testCases := []struct { name string - data interface{} + data any }{ {"Empty Course", Course{}}, {"Empty CourseInfo", CourseInfo{}}, @@ -626,8 +626,7 @@ func BenchmarkCourse_JSONMarshal(b *testing.B) { }, } - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { _, _ = json.Marshal(course) } } @@ -660,17 +659,16 @@ func BenchmarkCourse_JSONUnmarshal(b *testing.B) { jsonData, _ := json.Marshal(course) - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { var result Course _ = json.Unmarshal(jsonData, &result) } } // compareMaps compares two interface{} values that should be maps -func compareMaps(original, unmarshaled interface{}) bool { - origMap, origOk := original.(map[string]interface{}) - unMap, unOk := unmarshaled.(map[string]interface{}) +func compareMaps(original, unmarshaled any) bool { + origMap, origOk := original.(map[string]any) + unMap, unOk := unmarshaled.(map[string]any) if !origOk || !unOk { // If not maps, use deep equal diff --git a/internal/services/html_cleaner_test.go b/internal/services/html_cleaner_test.go index d3f9252..a0ca1e3 100644 --- a/internal/services/html_cleaner_test.go +++ b/internal/services/html_cleaner_test.go @@ -168,7 +168,7 @@ func TestHTMLCleaner_CleanHTML_LargeContent(t *testing.T) { // Create a large HTML string var builder strings.Builder builder.WriteString("") - for i := 0; i < 1000; i++ { + for i := range 1000 { builder.WriteString("

Paragraph ") builder.WriteString(string(rune('0' + i%10))) builder.WriteString(" with some content & entities.

") @@ -299,8 +299,7 @@ func BenchmarkHTMLCleaner_CleanHTML(b *testing.B) { cleaner := NewHTMLCleaner() input := "

Course Title

This is a great course about & HTML entities like   and "quotes".

" - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { cleaner.CleanHTML(input) } } @@ -311,15 +310,14 @@ func BenchmarkHTMLCleaner_CleanHTML_Large(b *testing.B) { // Create a large HTML string var builder strings.Builder - for i := 0; i < 100; i++ { + for i := range 100 { builder.WriteString("

Paragraph ") builder.WriteString(string(rune('0' + i%10))) builder.WriteString(" with some content & entities <test>.

") } input := builder.String() - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { cleaner.CleanHTML(input) } } diff --git a/internal/services/parser_test.go b/internal/services/parser_test.go index 98a0814..e591ef7 100644 --- a/internal/services/parser_test.go +++ b/internal/services/parser_test.go @@ -420,8 +420,7 @@ func BenchmarkExtractShareID(b *testing.B) { parser := &ArticulateParser{} uri := "https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO#/" - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { _, _ = parser.extractShareID(uri) } } @@ -433,8 +432,7 @@ func BenchmarkBuildAPIURL(b *testing.B) { } shareID := "N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO" - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { _ = parser.buildAPIURL(shareID) } } diff --git a/main_test.go b/main_test.go index 523bdcb..864b860 100644 --- a/main_test.go +++ b/main_test.go @@ -89,8 +89,7 @@ func TestIsURI(t *testing.T) { func BenchmarkIsURI(b *testing.B) { testStr := "https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO#/" - b.ResetTimer() - for i := 0; i < b.N; i++ { + for b.Loop() { isURI(testStr) } }