Skip to content

Commit 1359cc7

Browse files
authored
perf: eliminate bufio.Scanner allocations and redundant file read in parse/YAML hot paths (#28557)
1 parent def67ce commit 1359cc7

11 files changed

Lines changed: 241 additions & 62 deletions
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# ADR-28557: Zero-Allocation Fast-Path Strategy for Parser Hot Paths
2+
3+
**Date**: 2026-04-26
4+
**Status**: Draft
5+
**Deciders**: pelikhan (copilot-swe-agent)
6+
7+
---
8+
9+
## Part 1 — Narrative (Human-Friendly)
10+
11+
### Context
12+
13+
Benchmark profiling revealed a 12–13.5% performance regression in `BenchmarkYAMLGeneration` and `BenchmarkParseWorkflow`. The root causes were: (a) `bufio.Scanner` being allocated on every parse invocation — 6+ instances per call, accounting for approximately 20% of total allocations — and (b) a redundant `os.ReadFile` call in `generateYAML` that re-read a file whose content had already been parsed and held in memory. Both issues occur in the hot path executed on every workflow compilation. The `pkg/parser` package is called on each compile, making allocation pressure and redundant I/O directly proportional to throughput.
14+
15+
### Decision
16+
17+
We will replace `bufio.Scanner` in parser hot paths with Go 1.24 zero-allocation string iterators (`strings.Lines` and `strings.SplitSeq`), add fast-path pre-checks (`hasIncludeDirectives`) that return early before any scanner or buffer is allocated when no directives are present, and cache the raw markdown body in `WorkflowData.RawMarkdown` so that `generateYAML` can compute the frontmatter hash from pre-parsed data rather than re-reading the file from disk. The fast-path pre-check strategy is the primary driver: most workflow files contain no `@include`/`@import`/`{{#import` directives, so the common case can be handled without any scanner allocation.
18+
19+
### Alternatives Considered
20+
21+
#### Alternative 1: `sync.Pool` for `bufio.Scanner` reuse
22+
23+
A pool of pre-allocated scanners could amortize the per-call allocation cost. This was considered because it requires no Go version change and keeps the existing scanning logic intact. It was rejected because it adds pool lifecycle complexity (reset state between uses, handle concurrent access), whereas Go 1.24 iterators are simpler, have zero ongoing cost, and the fast-path pre-check eliminates the allocation entirely for the common case rather than merely amortizing it.
24+
25+
#### Alternative 2: Pre-allocated fixed-size byte buffer
26+
27+
Allocating a reusable `[]byte` buffer at startup and passing it to a custom scanner would reduce heap pressure. This was not chosen because it requires manual buffer sizing, risks stack-overflow or truncation for unexpectedly large content, and still allocates the scanner struct itself; the `strings.Lines` iterator avoids all of this by operating directly on the string without a separate buffer.
28+
29+
#### Alternative 3: Optimize only the redundant file read (partial fix)
30+
31+
Addressing only the `generateYAML` re-read without changing the scanner strategy would recover 3–6% of the regression. This was rejected as a standalone approach because profiling showed the scanner allocations were the dominant cost (~20% of total allocations); a partial fix would leave the larger problem unaddressed.
32+
33+
### Consequences
34+
35+
#### Positive
36+
- `BenchmarkParseWorkflow`: 11% throughput improvement, 39% memory reduction per operation; `bufio` allocations eliminated.
37+
- `BenchmarkYAMLGeneration`: 3% throughput improvement, 6% memory reduction per operation; redundant disk read eliminated.
38+
- The fast-path pre-check (`hasIncludeDirectives`) is a string-contains scan — O(n) but cache-friendly — and pays for itself whenever directives are absent (the common case).
39+
- `RawMarkdown` caching in `WorkflowData` makes the data flow more explicit: content read once during parsing flows through to YAML generation without additional I/O.
40+
41+
#### Negative
42+
- `strings.Lines` and `strings.SplitSeq` require Go 1.24, setting a hard minimum runtime version for this package.
43+
- `generateYAML` now has two code paths (fast path using `RawMarkdown`, fallback using disk read), adding branching complexity that must be kept in sync when the hashing logic changes.
44+
- The `hasIncludeDirectives` pre-check can produce false positives (content containing `@include` in a comment but not as a directive), causing unnecessary scanner allocation in those edge cases; however, the check is conservative and correct — it never produces false negatives.
45+
46+
#### Neutral
47+
- The `RawMarkdown` field is added to `WorkflowData`, a central struct; callers that construct `WorkflowData` externally without setting `RawMarkdown` automatically fall back to the disk-read path via the explicit `else` branch, preserving backward compatibility.
48+
- H1 header scanning in `ExtractWorkflowNameFromMarkdownBody` is now bounded to the first 64 lines (previously unbounded), which is a behavior change for pathologically large files but is semantically correct because H1 headers appear at the top of Markdown documents.
49+
50+
---
51+
52+
## Part 2 — Normative Specification (RFC 2119)
53+
54+
> 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).
55+
56+
### Fast-Path Pre-Checks
57+
58+
1. Functions that iterate over content lines **MUST** call `hasIncludeDirectives(content)` before allocating any `bufio.Scanner` or buffer when the only reason to iterate is to process include/import directives.
59+
2. `hasIncludeDirectives` **MUST** return `true` if and only if the content string contains at least one of the substrings `@include`, `@import`, or `{{#import`.
60+
3. Functions that return early via the fast path **MUST** preserve the behavioral contract of the full code path, including trailing-newline normalization for content mode and returning `"{}"` for tool-extraction mode.
61+
4. Fast-path pre-checks **MUST NOT** be added to functions whose iteration purpose is not exclusively include/import directive processing.
62+
63+
### String Iteration in Hot Paths
64+
65+
1. New line-scanning code in `pkg/parser` **MUST** use `strings.Lines` or `strings.SplitSeq` (Go 1.24) instead of `bufio.NewScanner(strings.NewReader(...))` when the input is an in-memory string.
66+
2. Implementations **MUST NOT** wrap an in-memory string in `strings.NewReader` solely to pass it to `bufio.NewScanner`; use the zero-allocation iterators instead.
67+
3. When an upper bound on lines to scan is required (e.g., searching for an H1 header), the iteration **MUST** break after the configured maximum (currently 64 lines for H1 header extraction) to bound worst-case cost.
68+
69+
### `WorkflowData.RawMarkdown` Caching
70+
71+
1. Code that constructs `WorkflowData` from a parsed result **MUST** populate `RawMarkdown` with the raw markdown body (before include expansion) when that content is available in memory at construction time.
72+
2. `generateYAML` **MUST** use `ComputeFrontmatterHashFromParsedContent` when `WorkflowData.RawMarkdown` is non-empty, and **MUST** fall back to `ComputeFrontmatterHashFromFileWithParsedFrontmatter` (disk read) when `RawMarkdown` is empty.
73+
3. The fallback path **SHOULD** log a debug message identifying the file path when it reads from disk, to aid future performance investigations.
74+
4. Callers that construct `WorkflowData` externally **MAY** leave `RawMarkdown` empty; the fallback path **MUST** remain functional in this case.
75+
76+
### Conformance
77+
78+
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.
79+
80+
---
81+
82+
*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/24954772505) workflow. The PR author must review, complete, and finalize this document before the PR can merge.*

pkg/actionpins/data/action_pins.json

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@
158158
"version": "v2.11",
159159
"sha": "1cb3cd8a008d80c9fa129c0f0823d69584905f5b"
160160
},
161+
"microsoft/apm-action@v1.4.2": {
162+
"repo": "microsoft/apm-action",
163+
"version": "v1.4.2",
164+
"sha": "9fe9337ef58b5e620e0113071ceb47a6a8a232f7"
165+
},
161166
"oven-sh/setup-bun@v2.2.0": {
162167
"repo": "oven-sh/setup-bun",
163168
"version": "v2.2.0",
@@ -172,14 +177,14 @@
172177
"repo": "super-linter/super-linter",
173178
"version": "v8.6.0",
174179
"sha": "9e863354e3ff62e0727d37183162c4a88873df41"
175-
},
176-
"microsoft/apm-action@v1.4.2": {
177-
"repo": "microsoft/apm-action",
178-
"version": "v1.4.2",
179-
"sha": "9fe9337ef58b5e620e0113071ceb47a6a8a232f7"
180180
}
181181
},
182182
"containers": {
183+
"alpine:latest": {
184+
"image": "alpine:latest",
185+
"digest": "sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11",
186+
"pinned_image": "alpine:latest@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11"
187+
},
183188
"docker.io/mcp/brave-search": {
184189
"image": "docker.io/mcp/brave-search",
185190
"digest": "sha256:ca96b8acb27d8cf601a8faef86a084602cffa41d8cb18caa1e29ba4d16989d22",
@@ -195,6 +200,11 @@
195200
"digest": "sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682",
196201
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682"
197202
},
203+
"ghcr.io/github/gh-aw-firewall/agent:0.25.28": {
204+
"image": "ghcr.io/github/gh-aw-firewall/agent:0.25.28",
205+
"digest": "sha256:a8834e285807654bf680154faa710d43fe4365a0868142f5c20e48c85e137a7a",
206+
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.25.28@sha256:a8834e285807654bf680154faa710d43fe4365a0868142f5c20e48c85e137a7a"
207+
},
198208
"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18": {
199209
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.18",
200210
"digest": "sha256:d16a40a3ca6e989896d0cef9f31b9412bb1fcc8755bafcafb95012ae1078539b",
@@ -205,6 +215,16 @@
205215
"digest": "sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519",
206216
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519"
207217
},
218+
"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.28": {
219+
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.28",
220+
"digest": "sha256:93290f2393752252911bd7c39a047f776c0b53063575e7bde4e304962a9a61cb",
221+
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.28@sha256:93290f2393752252911bd7c39a047f776c0b53063575e7bde4e304962a9a61cb"
222+
},
223+
"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.28": {
224+
"image": "ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.28",
225+
"digest": "sha256:fdf310e4678ce58d248c466b89399e9680a3003038fd19322c388559016aaac7",
226+
"pinned_image": "ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.28@sha256:fdf310e4678ce58d248c466b89399e9680a3003038fd19322c388559016aaac7"
227+
},
208228
"ghcr.io/github/gh-aw-firewall/squid:0.25.18": {
209229
"image": "ghcr.io/github/gh-aw-firewall/squid:0.25.18",
210230
"digest": "sha256:eb102afcfbae26ffcec016adebb74d3be7b0a5bf376ba306599cdf3effbe288e",
@@ -215,16 +235,31 @@
215235
"digest": "sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236",
216236
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236"
217237
},
238+
"ghcr.io/github/gh-aw-firewall/squid:0.25.28": {
239+
"image": "ghcr.io/github/gh-aw-firewall/squid:0.25.28",
240+
"digest": "sha256:844c18280f82cd1b06345eb2f4e91966b34185bfc51c9f237c3e022e848fb474",
241+
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.25.28@sha256:844c18280f82cd1b06345eb2f4e91966b34185bfc51c9f237c3e022e848fb474"
242+
},
218243
"ghcr.io/github/gh-aw-mcpg:v0.2.19": {
219244
"image": "ghcr.io/github/gh-aw-mcpg:v0.2.19",
220245
"digest": "sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd",
221246
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd"
222247
},
248+
"ghcr.io/github/gh-aw-mcpg:v0.2.30": {
249+
"image": "ghcr.io/github/gh-aw-mcpg:v0.2.30",
250+
"digest": "sha256:e950e6d39f003862d33bfb8d4eb93e242d919cf6ca874b90728e5e0ea7434c6f",
251+
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.2.30@sha256:e950e6d39f003862d33bfb8d4eb93e242d919cf6ca874b90728e5e0ea7434c6f"
252+
},
223253
"ghcr.io/github/github-mcp-server:v0.32.0": {
224254
"image": "ghcr.io/github/github-mcp-server:v0.32.0",
225255
"digest": "sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28",
226256
"pinned_image": "ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28"
227257
},
258+
"ghcr.io/github/github-mcp-server:v1.0.0": {
259+
"image": "ghcr.io/github/github-mcp-server:v1.0.0",
260+
"digest": "sha256:d2550953f8050bc5a1c8f80d1678766f66f60bbfbcd953fdeaf661fe4269bd95",
261+
"pinned_image": "ghcr.io/github/github-mcp-server:v1.0.0@sha256:d2550953f8050bc5a1c8f80d1678766f66f60bbfbcd953fdeaf661fe4269bd95"
262+
},
228263
"ghcr.io/github/serena-mcp-server:latest": {
229264
"image": "ghcr.io/github/serena-mcp-server:latest",
230265
"digest": "sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5",
@@ -279,41 +314,6 @@
279314
"image": "semgrep/semgrep:latest",
280315
"digest": "sha256:17d89ddd91a7729bbd5de09402f7f79a70204289e2a94635086e9db532a495f2",
281316
"pinned_image": "semgrep/semgrep:latest@sha256:17d89ddd91a7729bbd5de09402f7f79a70204289e2a94635086e9db532a495f2"
282-
},
283-
"ghcr.io/github/gh-aw-firewall/agent:0.25.28": {
284-
"image": "ghcr.io/github/gh-aw-firewall/agent:0.25.28",
285-
"digest": "sha256:a8834e285807654bf680154faa710d43fe4365a0868142f5c20e48c85e137a7a",
286-
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.25.28@sha256:a8834e285807654bf680154faa710d43fe4365a0868142f5c20e48c85e137a7a"
287-
},
288-
"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.28": {
289-
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.28",
290-
"digest": "sha256:93290f2393752252911bd7c39a047f776c0b53063575e7bde4e304962a9a61cb",
291-
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.28@sha256:93290f2393752252911bd7c39a047f776c0b53063575e7bde4e304962a9a61cb"
292-
},
293-
"ghcr.io/github/gh-aw-firewall/squid:0.25.28": {
294-
"image": "ghcr.io/github/gh-aw-firewall/squid:0.25.28",
295-
"digest": "sha256:844c18280f82cd1b06345eb2f4e91966b34185bfc51c9f237c3e022e848fb474",
296-
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.25.28@sha256:844c18280f82cd1b06345eb2f4e91966b34185bfc51c9f237c3e022e848fb474"
297-
},
298-
"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.28": {
299-
"image": "ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.28",
300-
"digest": "sha256:fdf310e4678ce58d248c466b89399e9680a3003038fd19322c388559016aaac7",
301-
"pinned_image": "ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.28@sha256:fdf310e4678ce58d248c466b89399e9680a3003038fd19322c388559016aaac7"
302-
},
303-
"ghcr.io/github/gh-aw-mcpg:v0.2.30": {
304-
"image": "ghcr.io/github/gh-aw-mcpg:v0.2.30",
305-
"digest": "sha256:e950e6d39f003862d33bfb8d4eb93e242d919cf6ca874b90728e5e0ea7434c6f",
306-
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.2.30@sha256:e950e6d39f003862d33bfb8d4eb93e242d919cf6ca874b90728e5e0ea7434c6f"
307-
},
308-
"ghcr.io/github/github-mcp-server:v1.0.0": {
309-
"image": "ghcr.io/github/github-mcp-server:v1.0.0",
310-
"digest": "sha256:d2550953f8050bc5a1c8f80d1678766f66f60bbfbcd953fdeaf661fe4269bd95",
311-
"pinned_image": "ghcr.io/github/github-mcp-server:v1.0.0@sha256:d2550953f8050bc5a1c8f80d1678766f66f60bbfbcd953fdeaf661fe4269bd95"
312-
},
313-
"alpine:latest": {
314-
"image": "alpine:latest",
315-
"digest": "sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11",
316-
"pinned_image": "alpine:latest@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11"
317317
}
318318
}
319319
}

pkg/gitutil/spec_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ func TestSpec_PublicAPI_ReadFileFromHEADWithRoot(t *testing.T) {
294294
}
295295

296296
t.Run("reads known file from HEAD without error", func(t *testing.T) {
297-
content, err := ReadFileFromHEADWithRoot("go.mod", root)
297+
content, err := ReadFileFromHEADWithRoot(filepath.Join(root, "go.mod"), root)
298298
assert.NoError(t, err, "ReadFileFromHEADWithRoot should read go.mod without error")
299299
assert.NotEmpty(t, content, "content of go.mod should not be empty")
300300
})

pkg/parser/frontmatter_content.go

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,24 @@ func ExtractMarkdownContent(content string) (string, error) {
207207
return result.Markdown, nil
208208
}
209209

210+
// findH1WorkflowName scans at most the first 64 lines of markdownBody for an H1 header
211+
// and returns the trimmed title. Returns "" if no H1 is found within those lines.
212+
func findH1WorkflowName(markdownBody string) string {
213+
const maxLines = 64
214+
lineCount := 0
215+
for line := range strings.Lines(markdownBody) {
216+
lineCount++
217+
if lineCount > maxLines {
218+
break
219+
}
220+
trimmed := strings.TrimSpace(line)
221+
if strings.HasPrefix(trimmed, "# ") {
222+
return strings.TrimSpace(trimmed[2:])
223+
}
224+
}
225+
return ""
226+
}
227+
210228
// ExtractWorkflowNameFromMarkdownBody extracts the workflow name from an already-extracted
211229
// markdown body (i.e. the content after the frontmatter has been stripped). This is more
212230
// efficient than ExtractWorkflowNameFromMarkdown or ExtractWorkflowNameFromContent because it
@@ -215,14 +233,9 @@ func ExtractMarkdownContent(content string) (string, error) {
215233
func ExtractWorkflowNameFromMarkdownBody(markdownBody string, virtualPath string) (string, error) {
216234
log.Printf("Extracting workflow name from markdown body: virtualPath=%s, size=%d bytes", virtualPath, len(markdownBody))
217235

218-
scanner := bufio.NewScanner(strings.NewReader(markdownBody))
219-
for scanner.Scan() {
220-
line := strings.TrimSpace(scanner.Text())
221-
if strings.HasPrefix(line, "# ") {
222-
workflowName := strings.TrimSpace(line[2:])
223-
log.Printf("Found workflow name from H1 header: %s", workflowName)
224-
return workflowName, nil
225-
}
236+
if name := findH1WorkflowName(markdownBody); name != "" {
237+
log.Printf("Found workflow name from H1 header: %s", name)
238+
return name, nil
226239
}
227240

228241
defaultName := generateDefaultWorkflowName(virtualPath)
@@ -241,14 +254,9 @@ func ExtractWorkflowNameFromContent(content string, virtualPath string) (string,
241254
return "", err
242255
}
243256

244-
scanner := bufio.NewScanner(strings.NewReader(markdownContent))
245-
for scanner.Scan() {
246-
line := strings.TrimSpace(scanner.Text())
247-
if strings.HasPrefix(line, "# ") {
248-
workflowName := strings.TrimSpace(line[2:])
249-
log.Printf("Found workflow name from H1 header: %s", workflowName)
250-
return workflowName, nil
251-
}
257+
if name := findH1WorkflowName(markdownContent); name != "" {
258+
log.Printf("Found workflow name from H1 header: %s", name)
259+
return name, nil
252260
}
253261

254262
defaultName := generateDefaultWorkflowName(virtualPath)

pkg/parser/frontmatter_hash.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,28 @@ func marshalSorted(data any) string {
122122
}
123123
}
124124

125+
// ComputeFrontmatterHashFromParsedContent computes the frontmatter hash from already-parsed
126+
// workflow data, avoiding a redundant file read when content has already been loaded.
127+
// frontmatterText is the raw text between the --- delimiters (e.g. WorkflowData.FrontmatterYAML).
128+
// markdownBody is the raw markdown body before include expansion (e.g. WorkflowData.RawMarkdown).
129+
// parsedFrontmatter is used to detect the inlined-imports flag.
130+
// baseDir is the directory containing the workflow file, used for resolving imports.
131+
func ComputeFrontmatterHashFromParsedContent(frontmatterText, markdownBody string, parsedFrontmatter map[string]any, baseDir string, cache *ImportCache, fileReader FileReader) (string, error) {
132+
frontmatterHashLog.Printf("Computing hash from parsed content (baseDir=%s)", baseDir)
133+
134+
inlinedImports := parseBoolFromFrontmatter(parsedFrontmatter, "inlined-imports")
135+
136+
var relevantExpressions []string
137+
var fullBody string
138+
if inlinedImports {
139+
fullBody = normalizeFrontmatterText(markdownBody)
140+
} else {
141+
relevantExpressions = extractRelevantTemplateExpressions(markdownBody)
142+
}
143+
144+
return computeFrontmatterHashTextBasedWithReader(frontmatterText, fullBody, baseDir, cache, relevantExpressions, fileReader)
145+
}
146+
125147
// ComputeFrontmatterHashFromFile computes the frontmatter hash for a workflow file
126148
// using text-based approach (no YAML parsing) to match JavaScript implementation
127149
func ComputeFrontmatterHashFromFile(filePath string, cache *ImportCache) (string, error) {

0 commit comments

Comments
 (0)