From a42968afcd8c093a1468e6149c801ead63e56619 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:12:26 +0000 Subject: [PATCH 1/8] Initial plan From 1bbbbd8fb9eb9b5a21214cef4077f44ed8db6771 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:35:40 +0000 Subject: [PATCH 2/8] fix: skip non-frontmatter markdown files during workflow compilation Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3aba940b-5a8f-4680-be99-b6aedaf23188 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/commands_file_watching_test.go | 29 ++++++++++++++++++++++++++ pkg/cli/compile_file_operations.go | 4 ++++ pkg/cli/compile_pipeline.go | 4 ++++ pkg/cli/workflows.go | 25 ++++++++++++++++++++++ pkg/cli/workflows_test.go | 29 ++++++++++++++++++++++++++ 5 files changed, 91 insertions(+) diff --git a/pkg/cli/commands_file_watching_test.go b/pkg/cli/commands_file_watching_test.go index 1aebbc0b737..c08348bc868 100644 --- a/pkg/cli/commands_file_watching_test.go +++ b/pkg/cli/commands_file_watching_test.go @@ -15,6 +15,7 @@ import ( "github.com/github/gh-aw/pkg/testutil" "github.com/github/gh-aw/pkg/workflow" + "github.com/stretchr/testify/assert" ) // TestWatchAndCompileWorkflows tests the watchAndCompileWorkflows function @@ -189,6 +190,34 @@ func TestCompileAllWorkflowFiles(t *testing.T) { } }) + t.Run("compile all skips markdown files without frontmatter", func(t *testing.T) { + tempDir := testutil.TempDir(t, "test-*") + workflowsDir := filepath.Join(tempDir, ".github/workflows") + os.MkdirAll(workflowsDir, 0o755) + + validWorkflow := filepath.Join(workflowsDir, "valid.md") + validContent := "---\non: push\nengine: claude\n---\n# Valid Workflow\n\nContent" + os.WriteFile(validWorkflow, []byte(validContent), 0o644) + + docsFile := filepath.Join(workflowsDir, "docs.md") + os.WriteFile(docsFile, []byte("# Documentation\n\nNo frontmatter here."), 0o644) + + compiler := workflow.NewCompiler() + stats, err := compileAllWorkflowFiles(compiler, workflowsDir, false) + if err != nil { + t.Fatalf("compileAllWorkflowFiles failed: %v", err) + } + + assert.Equal(t, 1, stats.Total, "Should compile only frontmatter-based markdown workflows") + assert.Equal(t, 0, stats.Errors, "Valid workflow should compile without errors") + + validLockFile := filepath.Join(workflowsDir, "valid.lock.yml") + assert.FileExists(t, validLockFile, "Expected lock file for valid workflow") + + docsLockFile := filepath.Join(workflowsDir, "docs.lock.yml") + assert.NoFileExists(t, docsLockFile, "Should not emit lock file for documentation markdown without frontmatter") + }) + t.Run("compile all handles glob error", func(t *testing.T) { // Use a malformed glob pattern that will cause filepath.Glob to error invalidDir := "/tmp/gh-aw/[invalid" diff --git a/pkg/cli/compile_file_operations.go b/pkg/cli/compile_file_operations.go index 5b0cdc82f99..843fc3f161d 100644 --- a/pkg/cli/compile_file_operations.go +++ b/pkg/cli/compile_file_operations.go @@ -125,6 +125,10 @@ func compileAllWorkflowFiles(compiler *workflow.Compiler, workflowsDir string, v if err != nil { return stats, fmt.Errorf("failed to find markdown files: %w", err) } + mdFiles, err = filterMarkdownFilesWithFrontmatter(mdFiles) + if err != nil { + return stats, fmt.Errorf("failed to filter markdown files: %w", err) + } if len(mdFiles) == 0 { compileHelpersLog.Printf("No markdown files found in %s", workflowsDir) if verbose { diff --git a/pkg/cli/compile_pipeline.go b/pkg/cli/compile_pipeline.go index 561d8d1a605..72077b1786a 100644 --- a/pkg/cli/compile_pipeline.go +++ b/pkg/cli/compile_pipeline.go @@ -233,6 +233,10 @@ func compileAllFilesInDirectory( if err != nil { return nil, fmt.Errorf("failed to find markdown files: %w", err) } + mdFiles, err = filterMarkdownFilesWithFrontmatter(mdFiles) + if err != nil { + return nil, fmt.Errorf("failed to filter markdown files: %w", err) + } if len(mdFiles) == 0 { return nil, fmt.Errorf("no markdown files found in %s", workflowsDir) diff --git a/pkg/cli/workflows.go b/pkg/cli/workflows.go index 0efdb3e66e7..e82d4d04da8 100644 --- a/pkg/cli/workflows.go +++ b/pkg/cli/workflows.go @@ -288,6 +288,31 @@ func getMarkdownWorkflowFiles(workflowDir string) ([]string, error) { return mdFiles, nil } +// filterMarkdownFilesWithFrontmatter keeps only markdown files that begin with frontmatter. +func filterMarkdownFilesWithFrontmatter(mdFiles []string) ([]string, error) { + workflowFiles := make([]string, 0, len(mdFiles)) + for _, file := range mdFiles { + content, err := os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("failed to read workflow file %s: %w", file, err) + } + + firstLine := string(content) + if newline := strings.IndexByte(firstLine, '\n'); newline >= 0 { + firstLine = firstLine[:newline] + } + + if strings.TrimSpace(firstLine) != "---" { + workflowsLog.Printf("Skipping markdown file without frontmatter: %s", file) + continue + } + + workflowFiles = append(workflowFiles, file) + } + + return workflowFiles, nil +} + // fastParseTitle scans markdown content for the first H1 header, skipping an // optional frontmatter block, without performing a full YAML parse. // diff --git a/pkg/cli/workflows_test.go b/pkg/cli/workflows_test.go index f7a2f33b790..b057dd83d1c 100644 --- a/pkg/cli/workflows_test.go +++ b/pkg/cli/workflows_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIsWorkflowFile(t *testing.T) { @@ -235,3 +236,31 @@ func TestGetMarkdownWorkflowFilesExcludesREADME(t *testing.T) { // Verify total count assert.Len(t, files, 5, "Should have exactly 5 workflow files (excluding README variants)") } + +func TestFilterMarkdownFilesWithFrontmatter(t *testing.T) { + tempDir := t.TempDir() + workflowsDir := filepath.Join(tempDir, ".github", "workflows") + err := os.MkdirAll(workflowsDir, 0o755) + require.NoError(t, err) + + testFiles := map[string]string{ + "workflow1.md": "---\non: push\n---\n# Workflow 1", + "docs.md": "# This is documentation", + } + + for filename, content := range testFiles { + path := filepath.Join(workflowsDir, filename) + err := os.WriteFile(path, []byte(content), 0o644) + require.NoError(t, err) + } + + inputFiles := []string{ + filepath.Join(workflowsDir, "workflow1.md"), + filepath.Join(workflowsDir, "docs.md"), + } + + filtered, err := filterMarkdownFilesWithFrontmatter(inputFiles) + require.NoError(t, err) + assert.Len(t, filtered, 1) + assert.Equal(t, filepath.Join(workflowsDir, "workflow1.md"), filtered[0]) +} From ef3dc912517d0ee634e0caab6cd97f68787ce14b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:41:29 +0000 Subject: [PATCH 3/8] perf: tighten frontmatter prefix scan in workflow filter Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3aba940b-5a8f-4680-be99-b6aedaf23188 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/workflows.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/cli/workflows.go b/pkg/cli/workflows.go index e82d4d04da8..279469352d6 100644 --- a/pkg/cli/workflows.go +++ b/pkg/cli/workflows.go @@ -1,6 +1,7 @@ package cli import ( + "bytes" "encoding/json" "errors" "fmt" @@ -297,12 +298,12 @@ func filterMarkdownFilesWithFrontmatter(mdFiles []string) ([]string, error) { return nil, fmt.Errorf("failed to read workflow file %s: %w", file, err) } - firstLine := string(content) - if newline := strings.IndexByte(firstLine, '\n'); newline >= 0 { + firstLine := content + if newline := bytes.IndexByte(firstLine, '\n'); newline >= 0 { firstLine = firstLine[:newline] } - if strings.TrimSpace(firstLine) != "---" { + if string(firstLine) != "---" { workflowsLog.Printf("Skipping markdown file without frontmatter: %s", file) continue } From c98f7b7695f94fa84f2c3f52ee7b9240e586e027 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:47:53 +0000 Subject: [PATCH 4/8] style: refine frontmatter line extraction for markdown filter Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3aba940b-5a8f-4680-be99-b6aedaf23188 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/workflows.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cli/workflows.go b/pkg/cli/workflows.go index 279469352d6..0c9b9e1673f 100644 --- a/pkg/cli/workflows.go +++ b/pkg/cli/workflows.go @@ -299,11 +299,11 @@ func filterMarkdownFilesWithFrontmatter(mdFiles []string) ([]string, error) { } firstLine := content - if newline := bytes.IndexByte(firstLine, '\n'); newline >= 0 { - firstLine = firstLine[:newline] + if line, _, found := bytes.Cut(content, []byte("\n")); found { + firstLine = line } - if string(firstLine) != "---" { + if !bytes.Equal(firstLine, []byte("---")) { workflowsLog.Printf("Skipping markdown file without frontmatter: %s", file) continue } From 15e88a96183d20168dc38331e6dcf4a199ee4009 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:53:06 +0000 Subject: [PATCH 5/8] test: tighten frontmatter filter tests and setup assertions Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3aba940b-5a8f-4680-be99-b6aedaf23188 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/commands_file_watching_test.go | 10 +++++++--- pkg/cli/workflows.go | 5 +---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/cli/commands_file_watching_test.go b/pkg/cli/commands_file_watching_test.go index c08348bc868..bac66f6883e 100644 --- a/pkg/cli/commands_file_watching_test.go +++ b/pkg/cli/commands_file_watching_test.go @@ -16,6 +16,7 @@ import ( "github.com/github/gh-aw/pkg/testutil" "github.com/github/gh-aw/pkg/workflow" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // TestWatchAndCompileWorkflows tests the watchAndCompileWorkflows function @@ -193,14 +194,17 @@ func TestCompileAllWorkflowFiles(t *testing.T) { t.Run("compile all skips markdown files without frontmatter", func(t *testing.T) { tempDir := testutil.TempDir(t, "test-*") workflowsDir := filepath.Join(tempDir, ".github/workflows") - os.MkdirAll(workflowsDir, 0o755) + err := os.MkdirAll(workflowsDir, 0o755) + require.NoError(t, err) validWorkflow := filepath.Join(workflowsDir, "valid.md") validContent := "---\non: push\nengine: claude\n---\n# Valid Workflow\n\nContent" - os.WriteFile(validWorkflow, []byte(validContent), 0o644) + err = os.WriteFile(validWorkflow, []byte(validContent), 0o644) + require.NoError(t, err) docsFile := filepath.Join(workflowsDir, "docs.md") - os.WriteFile(docsFile, []byte("# Documentation\n\nNo frontmatter here."), 0o644) + err = os.WriteFile(docsFile, []byte("# Documentation\n\nNo frontmatter here."), 0o644) + require.NoError(t, err) compiler := workflow.NewCompiler() stats, err := compileAllWorkflowFiles(compiler, workflowsDir, false) diff --git a/pkg/cli/workflows.go b/pkg/cli/workflows.go index 0c9b9e1673f..bdf8330ca87 100644 --- a/pkg/cli/workflows.go +++ b/pkg/cli/workflows.go @@ -298,10 +298,7 @@ func filterMarkdownFilesWithFrontmatter(mdFiles []string) ([]string, error) { return nil, fmt.Errorf("failed to read workflow file %s: %w", file, err) } - firstLine := content - if line, _, found := bytes.Cut(content, []byte("\n")); found { - firstLine = line - } + firstLine := bytes.SplitN(content, []byte("\n"), 2)[0] if !bytes.Equal(firstLine, []byte("---")) { workflowsLog.Printf("Skipping markdown file without frontmatter: %s", file) From 9bffc223f74089a4b6df9a3cd408058e811a2d50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:58:34 +0000 Subject: [PATCH 6/8] test: cover frontmatter filter edge cases Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3aba940b-5a8f-4680-be99-b6aedaf23188 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/workflows.go | 4 ++++ pkg/cli/workflows_test.go | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/cli/workflows.go b/pkg/cli/workflows.go index bdf8330ca87..07e67b2498d 100644 --- a/pkg/cli/workflows.go +++ b/pkg/cli/workflows.go @@ -297,6 +297,10 @@ func filterMarkdownFilesWithFrontmatter(mdFiles []string) ([]string, error) { if err != nil { return nil, fmt.Errorf("failed to read workflow file %s: %w", file, err) } + if len(content) == 0 { + workflowsLog.Printf("Skipping empty markdown file: %s", file) + continue + } firstLine := bytes.SplitN(content, []byte("\n"), 2)[0] diff --git a/pkg/cli/workflows_test.go b/pkg/cli/workflows_test.go index b057dd83d1c..181636c9c23 100644 --- a/pkg/cli/workflows_test.go +++ b/pkg/cli/workflows_test.go @@ -244,8 +244,11 @@ func TestFilterMarkdownFilesWithFrontmatter(t *testing.T) { require.NoError(t, err) testFiles := map[string]string{ - "workflow1.md": "---\non: push\n---\n# Workflow 1", - "docs.md": "# This is documentation", + "workflow1.md": "---\non: push\n---\n# Workflow 1", + "docs.md": "# This is documentation", + "empty.md": "", + "leading-whitespace.md": " ---\non: push\n---\n# Not Valid Frontmatter Start", + "delimiter-not-first.md": "# Header\n---\non: push\n---\n# Not Valid Frontmatter Start", } for filename, content := range testFiles { @@ -257,6 +260,9 @@ func TestFilterMarkdownFilesWithFrontmatter(t *testing.T) { inputFiles := []string{ filepath.Join(workflowsDir, "workflow1.md"), filepath.Join(workflowsDir, "docs.md"), + filepath.Join(workflowsDir, "empty.md"), + filepath.Join(workflowsDir, "leading-whitespace.md"), + filepath.Join(workflowsDir, "delimiter-not-first.md"), } filtered, err := filterMarkdownFilesWithFrontmatter(inputFiles) From 7451b8e24f4317fea28592be242d3b6775a96c5e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:08:13 +0000 Subject: [PATCH 7/8] docs(adr): add draft ADR-27387 for frontmatter-based markdown filtering Co-Authored-By: Claude Sonnet 4.6 --- ...frontmatter-markdown-during-compile-all.md | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/adr/27387-filter-non-frontmatter-markdown-during-compile-all.md diff --git a/docs/adr/27387-filter-non-frontmatter-markdown-during-compile-all.md b/docs/adr/27387-filter-non-frontmatter-markdown-during-compile-all.md new file mode 100644 index 00000000000..7cdd8b83d0c --- /dev/null +++ b/docs/adr/27387-filter-non-frontmatter-markdown-during-compile-all.md @@ -0,0 +1,78 @@ +# ADR-27387: Filter Non-Frontmatter Markdown Files During compile-all Discovery + +**Date**: 2026-04-20 +**Status**: Draft +**Deciders**: pelikhan, Copilot + +--- + +## Part 1 — Narrative (Human-Friendly) + +### Context + +The gh-aw `compile-all` command discovers workflow sources by listing every `*.md` file (excluding `README.md`) found in `.github/workflows/`. Some repositories legitimately place non-workflow documentation — notes, runbooks, guides — as Markdown files in that same directory. These files have no YAML frontmatter, so the compiler encounters them, fails to parse them, and emits noisy `no frontmatter found` errors. The result is inflated compatibility-failure counts and degraded signal-to-noise for developers running compile-all. + +### Decision + +We will add a `filterMarkdownFilesWithFrontmatter` step that runs immediately after Markdown file discovery and before any compilation attempt. The filter reads the first line of each candidate file and keeps only those whose first line is exactly `---` — the YAML frontmatter delimiter used by every gh-aw workflow. Files that are empty or whose first line is anything other than `---` are silently skipped. The filter is applied in both `compileAllWorkflowFiles` (in `compile_file_operations.go`) and `compileAllFilesInDirectory` (in `compile_pipeline.go`); the underlying `getMarkdownWorkflowFiles` listing function is left unchanged so that non-compile callers are not affected. + +### Alternatives Considered + +#### Alternative 1: Suppress the "no frontmatter" error in the parser + +The parser could downgrade the `no frontmatter found` diagnostic from an error to a debug-level log, letting compile-all silently continue. This approach avoids the additional I/O of a pre-scan. However, it only masks the symptom: the compiler still attempts a full parse of every non-workflow file, wasting CPU, and the file-discovery boundary between "any Markdown" and "workflow Markdown" remains blurred. It was rejected because it papers over the root cause rather than establishing a clean separation. + +#### Alternative 2: Enforce a separate directory for non-workflow docs + +Repositories could be required to keep documentation Markdown in a directory other than `.github/workflows/`. This would make the directory semantics unambiguous and eliminate the need for a filter entirely. It was rejected because it is a breaking change for existing repositories that already co-locate docs and workflows, and it imposes a structural constraint on users that has no benefit beyond compile-time convenience. + +#### Alternative 3: Name-based convention for workflow files + +Workflow Markdown files could be required to follow a specific naming pattern (e.g., end with `-workflow.md`), and compile-all would only process matching files. This would make the filter a simple `filepath.Match` with no I/O. It was rejected because it would require renaming every existing workflow, making it a major backwards-incompatible change. + +### Consequences + +#### Positive +- `compile-all` no longer generates false `no frontmatter found` errors for documentation files co-located with workflows. +- Compatibility-failure counts become more accurate, improving the signal value of compile-all output. +- The fix is surgically scoped: `getMarkdownWorkflowFiles` and all non-compile callers are untouched. + +#### Negative +- A workflow file whose frontmatter block is accidentally missing or malformed (e.g., starts with a BOM or a blank line before `---`) will be silently skipped rather than producing a clear error, potentially hiding real authoring mistakes. +- The filter reads file contents for every discovered Markdown file before compilation begins, adding one extra `os.ReadFile` per file over the previous behavior. + +#### Neutral +- The first-line check is intentionally strict (`bytes.Equal(firstLine, []byte("---"))`); any leading whitespace or UTF-8 BOM before `---` will cause the file to be skipped. This is consistent with how YAML frontmatter is defined in the gh-aw spec. +- Both compile pipelines now share a single filtering function, reducing future drift risk. + +--- + +## Part 2 — Normative Specification (RFC 2119) + +> The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). + +### Markdown File Discovery + +1. Implementations **MUST** apply the frontmatter filter to the list of Markdown files produced by `getMarkdownWorkflowFiles` before passing any file to the compiler. +2. Implementations **MUST** retain a Markdown file for compilation if and only if the file's first line (the bytes before the first `\n`) is exactly the three-byte sequence `---`. +3. Implementations **MUST NOT** pass a Markdown file to the compiler when the file is empty (zero bytes). +4. Implementations **MUST NOT** pass a Markdown file to the compiler when its first line contains any bytes other than `---` (including leading whitespace or BOM characters). +5. Implementations **SHOULD** emit a debug-level log entry naming each skipped file so that developers can diagnose unexpected omissions. + +### Compile Pipeline Integration + +1. Implementations **MUST** apply the frontmatter filter in every code path that calls `getMarkdownWorkflowFiles` and subsequently compiles the resulting files. +2. Implementations **MUST NOT** modify `getMarkdownWorkflowFiles` to incorporate the filter; the filter **MUST** remain a separate, composable step. +3. Implementations **MAY** cache file-read results within a single compile-all invocation to avoid reading the same file twice if the filter and the compiler would otherwise both read it. + +### Error Handling + +1. Implementations **MUST** propagate any `os.ReadFile` error that occurs during filtering as a compile-time error; they **MUST NOT** silently skip a file whose content cannot be read due to an I/O error. + +### Conformance + +An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. + +--- + +*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/24679557357) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* From c2c4172d22bc58a3362fb189016cb17378fa4f68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:21:01 +0000 Subject: [PATCH 8/8] fix: align frontmatter discovery with parser semantics Agent-Logs-Url: https://github.com/github/gh-aw/sessions/d59aec7b-aba8-41e6-a13d-92a4ca0198b5 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/compile_file_operations.go | 4 ++-- pkg/cli/compile_pipeline.go | 2 +- pkg/cli/workflows.go | 22 ++++++++++++++++------ pkg/cli/workflows_test.go | 10 +++++++--- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/pkg/cli/compile_file_operations.go b/pkg/cli/compile_file_operations.go index 843fc3f161d..171f0fedc54 100644 --- a/pkg/cli/compile_file_operations.go +++ b/pkg/cli/compile_file_operations.go @@ -130,9 +130,9 @@ func compileAllWorkflowFiles(compiler *workflow.Compiler, workflowsDir string, v return stats, fmt.Errorf("failed to filter markdown files: %w", err) } if len(mdFiles) == 0 { - compileHelpersLog.Printf("No markdown files found in %s", workflowsDir) + compileHelpersLog.Printf("No workflow markdown files found in %s after frontmatter filtering", workflowsDir) if verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No markdown files found in "+workflowsDir)) + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No workflow markdown files found in "+workflowsDir+" (workflow files must start with a frontmatter opener on the first line)")) } return stats, nil } diff --git a/pkg/cli/compile_pipeline.go b/pkg/cli/compile_pipeline.go index 72077b1786a..b72e0f8d8fa 100644 --- a/pkg/cli/compile_pipeline.go +++ b/pkg/cli/compile_pipeline.go @@ -239,7 +239,7 @@ func compileAllFilesInDirectory( } if len(mdFiles) == 0 { - return nil, fmt.Errorf("no markdown files found in %s", workflowsDir) + return nil, fmt.Errorf("no workflow markdown files found in %s (workflow files must start with a frontmatter opener on the first line)", workflowsDir) } compileOrchestrationLog.Printf("Found %d markdown files to compile", len(mdFiles)) diff --git a/pkg/cli/workflows.go b/pkg/cli/workflows.go index 07e67b2498d..e4a597cf27d 100644 --- a/pkg/cli/workflows.go +++ b/pkg/cli/workflows.go @@ -1,10 +1,11 @@ package cli import ( - "bytes" + "bufio" "encoding/json" "errors" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -293,18 +294,27 @@ func getMarkdownWorkflowFiles(workflowDir string) ([]string, error) { func filterMarkdownFilesWithFrontmatter(mdFiles []string) ([]string, error) { workflowFiles := make([]string, 0, len(mdFiles)) for _, file := range mdFiles { - content, err := os.ReadFile(file) + fd, err := os.Open(file) if err != nil { return nil, fmt.Errorf("failed to read workflow file %s: %w", file, err) } - if len(content) == 0 { + + reader := bufio.NewReader(fd) + firstLine, readErr := reader.ReadString('\n') + closeErr := fd.Close() + if closeErr != nil { + return nil, fmt.Errorf("failed to close workflow file %s: %w", file, closeErr) + } + if readErr != nil && !errors.Is(readErr, io.EOF) { + return nil, fmt.Errorf("failed to read workflow file %s: %w", file, readErr) + } + + if firstLine == "" { workflowsLog.Printf("Skipping empty markdown file: %s", file) continue } - firstLine := bytes.SplitN(content, []byte("\n"), 2)[0] - - if !bytes.Equal(firstLine, []byte("---")) { + if strings.TrimSpace(firstLine) != "---" { workflowsLog.Printf("Skipping markdown file without frontmatter: %s", file) continue } diff --git a/pkg/cli/workflows_test.go b/pkg/cli/workflows_test.go index 181636c9c23..b96e611dded 100644 --- a/pkg/cli/workflows_test.go +++ b/pkg/cli/workflows_test.go @@ -245,9 +245,10 @@ func TestFilterMarkdownFilesWithFrontmatter(t *testing.T) { testFiles := map[string]string{ "workflow1.md": "---\non: push\n---\n# Workflow 1", + "workflow-crlf.md": "---\r\non: push\r\n---\r\n# Workflow CRLF", "docs.md": "# This is documentation", "empty.md": "", - "leading-whitespace.md": " ---\non: push\n---\n# Not Valid Frontmatter Start", + "leading-whitespace.md": " ---\non: push\n---\n# Valid Frontmatter Start", "delimiter-not-first.md": "# Header\n---\non: push\n---\n# Not Valid Frontmatter Start", } @@ -259,6 +260,7 @@ func TestFilterMarkdownFilesWithFrontmatter(t *testing.T) { inputFiles := []string{ filepath.Join(workflowsDir, "workflow1.md"), + filepath.Join(workflowsDir, "workflow-crlf.md"), filepath.Join(workflowsDir, "docs.md"), filepath.Join(workflowsDir, "empty.md"), filepath.Join(workflowsDir, "leading-whitespace.md"), @@ -267,6 +269,8 @@ func TestFilterMarkdownFilesWithFrontmatter(t *testing.T) { filtered, err := filterMarkdownFilesWithFrontmatter(inputFiles) require.NoError(t, err) - assert.Len(t, filtered, 1) - assert.Equal(t, filepath.Join(workflowsDir, "workflow1.md"), filtered[0]) + assert.Len(t, filtered, 3) + assert.Contains(t, filtered, filepath.Join(workflowsDir, "workflow1.md")) + assert.Contains(t, filtered, filepath.Join(workflowsDir, "workflow-crlf.md")) + assert.Contains(t, filtered, filepath.Join(workflowsDir, "leading-whitespace.md")) }