Development guidelines for DocBuilder - a Go CLI that aggregates Git repository documentation into Hugo sites with Relearn theme
You are assisting with DocBuilder, a Go CLI tool that aggregates documentation from multiple Git repositories into a single Hugo static site using the Relearn theme.
DocBuilder follows a pipeline pattern:
1. **Configuration** (`internal/config/`) - YAML config with environment variable expansion
2. **Workspace** (`internal/workspace/`) - Temporary directories for Git operations
3. **Git Client** (`internal/git/`) - Repository cloning/updating with authentication
4. **Discovery** (`internal/docs/`) - Markdown file discovery in configured paths
5. **Hugo Generator** (`internal/hugo/`) - Hugo site generation with theme-specific optimizations
**Data flow:** `Config → Git Clone → Doc Discovery → Hugo Site Generation`
Uses Kong for CLI parsing. Main commands in `cmd/docbuilder/main.go`:
Development command: `go run ./cmd/docbuilder <command> -v`
Example:
```yaml
repositories:
- url: https://github.com/org/repo.git
name: repo-name
branch: main
paths: ["docs", "documentation"]
auth:
type: token
token: "${GITHUB_TOKEN}"
```
**DocBuilder hard-pins to Relearn theme.** All theme configuration normalizes to `github.com/McShelby/hugo-theme-relearn`.
Config generation (`internal/hugo/config_writer.go`):
1. Core defaults (title/baseURL/markup)
2. Apply Relearn defaults via `applyRelearnThemeDefaults()`
3. Deep-merge user overrides from `hugo.params`
4. Add dynamic params (build_date, version)
5. Configure Hugo Modules for Relearn with `go.mod`
6. Enable math passthrough
7. Configure outputs/taxonomies/language for Relearn
8. Convert `hugo.menu` to Hugo format
**Do not add multi-theme scaffolding** without full refactor (config model + generator + tests).
Discovery (`internal/docs/discovery.go`):
Hugo path patterns:
Auth manager/providers (`internal/auth/`, wired via `internal/git/`):
Common env vars: `${GIT_ACCESS_TOKEN}`, `${GITHUB_TOKEN}`
**Prefer ripgrep (`rg`) when available:**
```bash
rg "pattern" # Search (respects .gitignore)
rg -i "pattern" # Case-insensitive
rg -i "pattern" --stats # With statistics
rg -t go "pattern" # Go files only
rg -l "pattern" # List files containing pattern
```
**Why ripgrep:** Respects `.gitignore`, faster, better defaults, handles binary files correctly.
```bash
make build
./bin/docbuilder init -c test-config.yaml
./bin/docbuilder build -c test-config.yaml -v
./bin/docbuilder discover -c test-config.yaml -v
```
1. **Write test first** - Watch it fail
2. **Implement change** - Make test pass
3. **Bug fixes require reproducer test** - Add before fixing
4. **Keep regression tests** - No temporary/throwaway tests
#### Unit Tests (Match Source Files)
Co-locate with source using `<source_file>_test.go` suffix:
```
internal/lint/
├── fixer.go
├── fixer_test.go # Tests for fixer.go
├── fixer_result.go
├── fixer_result_test.go # Tests for fixer_result.go
├── fixer_utils.go
└── fixer_utils_test.go # Tests for fixer_utils.go
```
**Unit test conventions:**
#### Integration Tests (Feature-Based)
Name by feature/workflow:
```
internal/lint/
├── fixer_workflow_test.go # End-to-end workflows
└── golden_test.go # Golden file comparisons
```
**Integration test conventions:**
#### When to Split Test Files
Split when:
1. File exceeds ~500 lines with tests for multiple sources
2. Tests cover distinct modules split into separate files
3. Unit and integration tests mixed unclearly
4. Adding tests becomes difficult due to unrelated setup
**Splitting procedure:**
1. Identify tests belonging to each source file
2. Create `<source_file>_test.go` for unit tests
3. Move tests to corresponding files
4. Rename remaining integration tests to `<feature>_test.go`
5. Run `go test ./...` to verify
6. Compare test counts before/after
#### Test File Checklist
**When adding features that modify output (Hugo config, content structure, assets), create golden tests:**
#### 1. Determine Test Type
**Unit Golden Test:**
**Integration Golden Test:**
#### 2. Integration Golden Test Structure
**Required files:**
1. Test repo: `test/testdata/repos/<feature>/docs/*.md`
2. Config: `test/testdata/configs/<feature>.yaml`
3. Test function: `test/integration/<feature>_golden_test.go`
4. Golden dir: `test/testdata/golden/<feature>/`
**Test config pattern:**
```yaml
version: "2.0"
repositories:
- name: feature-demo
url: PLACEHOLDER # Replaced by setupTestRepo()
branch: main
paths: [docs]
hugo:
title: "Feature Demo"
base_url: "http://localhost:1313/"
enable_feature: true
params:
navbar:
displayTitle: true
output:
directory: PLACEHOLDER # Replaced by t.TempDir()
clean: true
```
**Test function pattern:**
```go
func TestGolden_<Feature>(t *testing.T) {
if testing.Short() {
t.Skip("Skipping golden test in short mode")
}
repoPath := setupTestRepo(t, "../../test/testdata/repos/<feature>")
cfg := loadGoldenConfig(t, "../../test/testdata/configs/<feature>.yaml")
cfg.Repositories[0].URL = repoPath
outputDir := t.TempDir()
cfg.Output.Directory = outputDir
svc := build.NewBuildService().
WithHugoGeneratorFactory(func(cfgAny any, outDir string) build.HugoGenerator {
return hugo.NewGenerator(cfgAny.(*config.Config), outDir)
})
result, err := svc.Run(context.Background(), build.BuildRequest{
Config: cfg,
OutputDir: outputDir,
})
require.NoError(t, err)
require.Equal(t, build.BuildStatusSuccess, result.Status)
goldenDir := "../../test/testdata/golden/<feature>"
verifyHugoConfig(t, outputDir, goldenDir+"/hugo-config.golden.yaml", *updateGolden)
verifyContentStructure(t, outputDir, goldenDir+"/content-structure.golden.json", *updateGolden)
}
```
#### 3. Custom Verification Helper (if needed)
Add to `test/integration/helpers.go` for feature-specific assets:
```go
type FeatureAssets struct {
Files map[string]FeatureFile `json:"files"`
Config map[string]interface{} `json:"config"`
}
func verifyFeatureAssets(t *testing.T, outputDir, goldenPath string, updateGolden bool) {
t.Helper()
actual := FeatureAssets{Files: make(map[string]FeatureFile)}
featurePath := filepath.Join(outputDir, "static", "feature-file.ext")
actual.Files["feature-file.ext"] = verifyAssetFile(t, featurePath,
[]string{"marker1", "marker2"})
if updateGolden {
data, _ := json.MarshalIndent(actual, "", " ")
os.MkdirAll(filepath.Dir(goldenPath), 0755)
os.WriteFile(goldenPath, data, 0644)
return
}
goldenData, _ := os.ReadFile(goldenPath)
// Compare actual vs goldenData
}
```
1. **No theme-selection code** unless full refactor with tests
2. **Always use ripgrep** for codebase searches when available
3. **Strict TDD** - test first, then implement
4. **Regression tests required** for all bugs
5. **Match Go conventions** for test file organization
6. **Golden tests required** for output-changing features
7. **Avoid hard-coding** theme references (normalize to Relearn)
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/github-copilot-instructions-for-docbuilder/raw