diff --git a/pkg/build/builders/docs.go b/pkg/build/builders/docs.go index fd49701..60d47ef 100644 --- a/pkg/build/builders/docs.go +++ b/pkg/build/builders/docs.go @@ -62,7 +62,8 @@ func (b *DocsBuilder) Build(ctx context.Context, cfg *build.Config, targets []bu return nil, coreerr.E("DocsBuilder.Build", "failed to create output directory", err) } - if !b.hasMkDocsConfig(cfg.FS, cfg.ProjectDir) { + configPath := b.resolveMkDocsConfigPath(cfg.FS, cfg.ProjectDir) + if configPath == "" { return nil, coreerr.E("DocsBuilder.Build", "mkdocs.yml or mkdocs.yaml not found", nil) } @@ -83,7 +84,7 @@ func (b *DocsBuilder) Build(ctx context.Context, cfg *build.Config, targets []bu return artifacts, coreerr.E("DocsBuilder.Build", "failed to create site directory", err) } - args := []string{"build", "--clean", "--site-dir", siteDir} + args := []string{"build", "--clean", "--site-dir", siteDir, "--config-file", configPath} output, err := ax.CombinedOutput(ctx, cfg.ProjectDir, cfg.Env, mkdocsCommand, args...) if err != nil { return artifacts, coreerr.E("DocsBuilder.Build", "mkdocs build failed: "+output, err) @@ -104,9 +105,9 @@ func (b *DocsBuilder) Build(ctx context.Context, cfg *build.Config, targets []bu return artifacts, nil } -// hasMkDocsConfig reports whether the project contains a MkDocs config file. -func (b *DocsBuilder) hasMkDocsConfig(fs io.Medium, projectDir string) bool { - return fs.IsFile(ax.Join(projectDir, "mkdocs.yml")) || fs.IsFile(ax.Join(projectDir, "mkdocs.yaml")) +// resolveMkDocsConfigPath returns the MkDocs config file path if present. +func (b *DocsBuilder) resolveMkDocsConfigPath(fs io.Medium, projectDir string) string { + return build.ResolveMkDocsConfigPath(fs, projectDir) } // resolveMkDocsCli returns the executable path for the mkdocs CLI. diff --git a/pkg/build/builders/docs_test.go b/pkg/build/builders/docs_test.go index 29dbf0c..7621c3c 100644 --- a/pkg/build/builders/docs_test.go +++ b/pkg/build/builders/docs_test.go @@ -106,6 +106,41 @@ func TestDocs_DocsBuilderBuild_Good(t *testing.T) { assert.Contains(t, string(content), "FOO=bar") } +func TestDocs_DocsBuilderBuild_Good_NestedConfig(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("mkdocs test fixture uses a shell script") + } + + dir := t.TempDir() + require.NoError(t, ax.MkdirAll(ax.Join(dir, "docs"), 0o755)) + require.NoError(t, ax.WriteFile(ax.Join(dir, "docs", "mkdocs.yaml"), []byte("site_name: Demo\n"), 0o644)) + + binDir := t.TempDir() + mkdocsPath := ax.Join(binDir, "mkdocs") + script := "#!/bin/sh\nset -eu\nif [ -n \"${DOCS_BUILD_LOG_FILE:-}\" ]; then\n printf '%s\\n' \"$@\" >> \"${DOCS_BUILD_LOG_FILE}\"\nfi\nsite_dir=\"\"\nwhile [ $# -gt 0 ]; do\n if [ \"$1\" = \"--site-dir\" ]; then\n shift\n site_dir=\"$1\"\n fi\n shift\ndone\nmkdir -p \"$site_dir\"\nprintf '%s' 'demo docs' > \"$site_dir/index.html\"\n" + require.NoError(t, ax.WriteFile(mkdocsPath, []byte(script), 0o755)) + t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH")) + logPath := ax.Join(t.TempDir(), "docs.args") + + cfg := &build.Config{ + FS: io.Local, + ProjectDir: dir, + OutputDir: ax.Join(dir, "dist"), + Name: "demo-site", + Env: []string{"DOCS_BUILD_LOG_FILE=" + logPath}, + } + + builder := NewDocsBuilder() + artifacts, err := builder.Build(context.Background(), cfg, []build.Target{{OS: "linux", Arch: "amd64"}}) + require.NoError(t, err) + require.Len(t, artifacts, 1) + + content, err := ax.ReadFile(logPath) + require.NoError(t, err) + assert.Contains(t, string(content), "--config-file") + assert.Contains(t, string(content), "docs/mkdocs.yaml") +} + func TestDocs_DocsBuilderBuild_Bad(t *testing.T) { builder := NewDocsBuilder() diff --git a/pkg/build/discovery.go b/pkg/build/discovery.go index 810641c..90eef4d 100644 --- a/pkg/build/discovery.go +++ b/pkg/build/discovery.go @@ -15,6 +15,8 @@ const ( markerComposer = "composer.json" markerMkDocs = "mkdocs.yml" markerMkDocsYAML = "mkdocs.yaml" + markerDocsMkDocs = "docs/mkdocs.yml" + markerDocsMkDocsYAML = "docs/mkdocs.yaml" markerPyProject = "pyproject.toml" markerRequirements = "requirements.txt" markerCargo = "Cargo.toml" @@ -72,6 +74,7 @@ func Discover(fs io.Medium, dir string) ([]ProjectType, error) { projectType ProjectType detected bool }{ + {ProjectTypeDocs, IsMkDocsProject(fs, dir)}, {ProjectTypeDocker, IsDockerProject(fs, dir)}, {ProjectTypeLinuxKit, IsLinuxKitProject(fs, dir)}, {ProjectTypeCPP, IsCPPProject(fs, dir)}, @@ -137,12 +140,28 @@ func IsCPPProject(fs io.Medium, dir string) bool { return fileExists(fs, ax.Join(dir, "CMakeLists.txt")) } -// IsMkDocsProject checks for MkDocs config at the project root. +// IsMkDocsProject checks for MkDocs config at the project root or in docs/. // // ok := build.IsMkDocsProject(io.Local, ".") func IsMkDocsProject(fs io.Medium, dir string) bool { - return fileExists(fs, ax.Join(dir, markerMkDocs)) || - fileExists(fs, ax.Join(dir, markerMkDocsYAML)) + return ResolveMkDocsConfigPath(fs, dir) != "" +} + +// ResolveMkDocsConfigPath returns the first MkDocs config path that exists. +// +// configPath := build.ResolveMkDocsConfigPath(io.Local, ".") +func ResolveMkDocsConfigPath(fs io.Medium, dir string) string { + for _, path := range []string{ + ax.Join(dir, markerMkDocs), + ax.Join(dir, markerMkDocsYAML), + ax.Join(dir, "docs", "mkdocs.yml"), + ax.Join(dir, "docs", "mkdocs.yaml"), + } { + if fileExists(fs, path) { + return path + } + } + return "" } // HasSubtreeNpm checks for package.json within depth 2 subdirectories. @@ -248,7 +267,8 @@ func DiscoverFull(fs io.Medium, dir string) (*DiscoveryResult, error) { // Record raw marker presence allMarkers := []string{ markerGoMod, markerWails, markerNodePackage, markerComposer, - markerMkDocs, markerMkDocsYAML, markerPyProject, markerRequirements, markerCargo, + markerMkDocs, markerMkDocsYAML, markerDocsMkDocs, markerDocsMkDocsYAML, + markerPyProject, markerRequirements, markerCargo, "CMakeLists.txt", markerDockerfile, markerLinuxKitYAML, markerLinuxKitYAMLAlt, markerTaskfileYML, markerTaskfileYAML, markerTaskfileBare, markerTaskfileLowerYML, markerTaskfileLowerYAML, diff --git a/pkg/build/discovery_test.go b/pkg/build/discovery_test.go index a518824..07d149f 100644 --- a/pkg/build/discovery_test.go +++ b/pkg/build/discovery_test.go @@ -66,6 +66,16 @@ func TestDiscovery_Discover_Good(t *testing.T) { assert.Equal(t, []ProjectType{ProjectTypeDocs}, types) }) + t.Run("detects docs project in docs directory", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, ax.MkdirAll(ax.Join(dir, "docs"), 0755)) + require.NoError(t, ax.WriteFile(ax.Join(dir, "docs", "mkdocs.yml"), []byte("site_name: Demo\n"), 0644)) + + types, err := Discover(fs, dir) + assert.NoError(t, err) + assert.Equal(t, []ProjectType{ProjectTypeDocs}, types) + }) + t.Run("detects Python project with pyproject.toml", func(t *testing.T) { dir := setupTestDir(t, "pyproject.toml") types, err := Discover(fs, dir) @@ -609,6 +619,18 @@ func TestDiscovery_DiscoverFull_Good(t *testing.T) { assert.True(t, result.Markers["mkdocs.yaml"]) }) + t.Run("detects docs project markers in docs directory", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, ax.MkdirAll(ax.Join(dir, "docs"), 0755)) + require.NoError(t, ax.WriteFile(ax.Join(dir, "docs", "mkdocs.yaml"), []byte("site_name: Demo\n"), 0644)) + + result, err := DiscoverFull(fs, dir) + require.NoError(t, err) + assert.Equal(t, []ProjectType{ProjectTypeDocs}, result.Types) + assert.Equal(t, "docs", result.PrimaryStack) + assert.True(t, result.Markers["docs/mkdocs.yaml"]) + }) + t.Run("detects Rust project markers", func(t *testing.T) { dir := setupTestDir(t, "Cargo.toml") result, err := DiscoverFull(fs, dir)