You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #2 reconstructs source positions for ShellCheck diagnostics from the decoded run: value and the original workflow text. The local mapper deliberately supports plain and literal scalars and falls back to the run: key when an exact mapping is unsafe.
The current go.yaml.in/yaml/v4 public Node API exposes the decoded value, scalar style, and starting line/column, but not the raw scalar text, byte offsets, end mark, or decoded-character mapping. Its internal scanner/parser carries start and end marks, but the composer does not retain all of them on Node.
Option 1: propose support in go-yaml
Investigate an upstream issue or pull request for one or more of:
raw scalar source text or a stable source slice;
start and end byte/line/column marks on nodes;
decoded-scalar-to-source mapping spans produced while resolving escapes, indentation, folding, and chomping;
a lower-level public token/event API that retains this information without changing ordinary decoding.
goccy/go-yaml exposes AST tokens with decoded Value, raw Origin, and Position.Offset, which may be sufficient to construct comprehensive mappings for plain, literal, folded, single-quoted, and double-quoted scalars. It does not appear to expose the final per-character mapping directly.
Evaluate:
correctness across escapes, folding, chomping, blank lines, Unicode, tabs, comments, anchors, and aliases;
Prefer an approach only if it materially expands exact coverage beyond plain and literal scalars without weakening parse compatibility, diagnostics, or maintenance. Keep the existing conservative fallback when no trustworthy mapping is available.
Research findings (2026-08-20)
Decision
Keep go.yaml.in/yaml/v4 as the semantic parser. Do not adopt goccy/go-yaml as either a source-location sidecar or a full parser replacement. Extend the existing source mapper directly from the original workflow text, and accept a mapping only when its reconstructed decoded value exactly matches yaml.Node.Value.
This keeps the script passed to ShellCheck under the current parser's control. The mapping layer supplies positions only; it must never replace or reinterpret the effective run: value.
Corrections to the original investigation
goccy/go-yaml#858 is unrelated to offsets. It requests a different custom marshaling API.
The applicable open offset report is goccy/go-yaml#856: every preceding comment shifts subsequent offsets.
Related open position defects include incorrect CRLF line counting in goccy/go-yaml#560 and end-like positions for multiline tokens in goccy/go-yaml#813.
A matching go-yaml request already exists. yaml/go-yaml#373 asks for node indentation and end marks specifically for actionlint's ShellCheck mapping use case.
yaml/go-yaml#391 is an open, mergeable PR adding EndLine and EndColumn. It does not add indentation, raw source, byte offsets, or decoded-character mapping, and no released version contains it yet.
The public token/event direction has already met resistance upstream. The CLI work merged in yaml/go-yaml#140 exposes parser internals only inside the module rather than as a supported public library API.
goccy/go-yaml evaluation
The proposed Origin plus Position.Offset design is not safe:
Position.Offset is not a stable byte offset. The scanner operates on []rune and advances offsets by rune counts. In a probe where run: followed café, its reported position was one byte short of the 1-based byte position.
Literal and folded scalar content tokens reported positions at the end of their multiline content rather than its start, matching #813.
Origin is not reliably source-identical. For a double-quoted \u00e9 escape, v1.19.2 omitted the escape text from Origin; current master retains the scanner path that does not append the x, u, or U escape digits to the origin buffer.
The API still provides no decoded-character-to-source spans. Escapes, quote collapsing, folding, indentation, and chomping would still have to be reconstructed in actionlint.
Semantic probes also rule out a full migration:
yes, no, and on decoded as strings, matching YAML 1.2 expectations.
Duplicate keys were rejected.
010 decoded as octal 8; goccy/go-yaml#894 tracks this YAML 1.1 behavior. GitHub's runner applies YAML 1.2 decimal handling.
Merge keys were expanded. This differs from actionlint's intended GitHub compatibility behavior and would require separate hosted-service validation.
The GitHub implementations remain useful references. The current runner uses YAML 1.2 core scalar resolution, while @actions/workflow-parser retains decoded values, raw scalar source, block-scalar headers, and source ranges. The latter demonstrates that preserving source tokens is practical, but it does not provide the per-character map needed here.
Performance and dependency cost
A local five-sample parser benchmark used a synthetic 32,818-byte workflow containing 30 jobs and 360 steps across all scalar styles:
Operation
Time
Bytes allocated
Allocations
Existing go-yaml node parse
about 1.61 ms
about 0.70 MB
10,781
Additional goccy AST parse
about 4.02 ms
about 2.47 MB
42,878
Goccy tokenization only
about 1.95 ms
about 1.28 MB
21,357
Adding the AST sidecar would make the YAML parsing stage about 3.5 times slower and 4.5 times heavier in allocated bytes before lint rules run. Whole-lint impact would be smaller when external checks dominate.
goccy/go-yaml has no external module dependencies. In a minimal linked-binary comparison, adding its parser alongside go-yaml increased the binary by about 734 KB. Correctness remains the blocking concern; the performance and binary costs are secondary.
Implementation plan
1. Preserve a single semantic authority
Continue parsing workflows and deriving run: values exclusively with go.yaml.in/yaml/v4.
Keep the source mapper internal to parser metadata. It must not change the public AST or the script sent to ShellCheck.
Do not add goccy/go-yaml.
2. Build a scalar mapping corpus
Add table-driven parser tests for:
plain, literal, folded, single-quoted, and double-quoted scalars;
|, |-, |+, >, >-, and >+;
escaped characters and escaped physical line breaks;
doubled single quotes;
folded line joins, more-indented lines, blank lines, and chomping;
leading and trailing indentation, tabs where valid, comments, Unicode, CRLF, anchors, and aliases.
Each case must record the expected decoded script and expected YAML positions for selected script characters. The reconstructed decoded bytes must equal yaml.Node.Value before any mapping is accepted.
3. Extend the existing mapper incrementally
Implement independent style-specific decoders in this order:
single-line and multiline single-quoted scalars;
double-quoted scalars, including escapes and escaped line breaks;
folded block scalars, including folding and chomping rules.
Each decoder should emit sparse scriptSourceSpan records while reconstructing the decoded script. Source characters that synthesize no decoded character, and decoded whitespace synthesized by YAML folding, should remain outside mapped spans.
4. Enforce conservative per-diagnostic fallback
Reject the entire candidate map if reconstructed bytes differ from yaml.Node.Value or if the scalar boundary cannot be established safely.
Preserve gaps for synthesized characters rather than guessing a position.
Use exact source ranges only when both ShellCheck endpoints map safely.
Otherwise report that diagnostic at run.RunPos, preserving the existing behavior.
5. Track the upstream improvement without blocking
If more upstream support is pursued, continue Provide more node information like indent and end marks yaml/go-yaml#373 rather than opening a duplicate. The most useful addition would be opt-in raw scalar/source-span metadata or decoded-to-source spans; EndLine and EndColumn alone do not solve scalar transformations.
Do not depend on an unreleased go-yaml commit for this work.
6. Keep hosted compatibility testing separate
A GitHub-hosted conformance probe remains valuable for actionlint's parser compatibility as a whole, especially for scalar resolution and rejecting constructs. It is not a prerequisite for this mapping implementation because the mapper does not become a semantic parser.
If the hosted probe is implemented, record dated observations for accepted values and isolate parser-rejection cases into separate workflow revisions so one invalid construct cannot mask the rest of the corpus.
Acceptance criteria
Exact mapping expands beyond plain and literal scalars with no new YAML parser dependency.
Every accepted mapping reproduces yaml.Node.Value byte-for-byte.
Unsafe scalar boundaries, transformations, and diagnostic endpoints retain the run:-key fallback.
Existing plain/literal mappings and Unicode-aware ShellCheck ranges remain unchanged.
Parser and ShellCheck fixtures cover all supported and fallback cases.
go test ./..., go test -race ./..., go vet ./..., the repository's static checks, and git diff --check pass.
Context
PR #2 reconstructs source positions for ShellCheck diagnostics from the decoded
run:value and the original workflow text. The local mapper deliberately supports plain and literal scalars and falls back to therun:key when an exact mapping is unsafe.The current
go.yaml.in/yaml/v4publicNodeAPI exposes the decoded value, scalar style, and starting line/column, but not the raw scalar text, byte offsets, end mark, or decoded-character mapping. Its internal scanner/parser carries start and end marks, but the composer does not retain all of them onNode.Option 1: propose support in go-yaml
Investigate an upstream issue or pull request for one or more of:
Relevant implementation: yaml/go-yaml
Node, internal marks, and composer.Option 2: evaluate goccy/go-yaml
goccy/go-yamlexposes AST tokens with decodedValue, rawOrigin, andPosition.Offset, which may be sufficient to construct comprehensive mappings for plain, literal, folded, single-quoted, and double-quoted scalars. It does not appear to expose the final per-character mapping directly.Evaluate:
Relevant implementation: token API and AST scalar nodes.
Decision criteria
Prefer an approach only if it materially expands exact coverage beyond plain and literal scalars without weakening parse compatibility, diagnostics, or maintenance. Keep the existing conservative fallback when no trustworthy mapping is available.
Research findings (2026-08-20)
Decision
Keep
go.yaml.in/yaml/v4as the semantic parser. Do not adoptgoccy/go-yamlas either a source-location sidecar or a full parser replacement. Extend the existing source mapper directly from the original workflow text, and accept a mapping only when its reconstructed decoded value exactly matchesyaml.Node.Value.This keeps the script passed to ShellCheck under the current parser's control. The mapping layer supplies positions only; it must never replace or reinterpret the effective
run:value.Corrections to the original investigation
EndLineandEndColumn. It does not add indentation, raw source, byte offsets, or decoded-character mapping, and no released version contains it yet.goccy/go-yaml evaluation
The proposed
OriginplusPosition.Offsetdesign is not safe:Position.Offsetis not a stable byte offset. The scanner operates on[]runeand advances offsets by rune counts. In a probe whererun:followedcafé, its reported position was one byte short of the 1-based byte position.Originis not reliably source-identical. For a double-quoted\u00e9escape, v1.19.2 omitted the escape text fromOrigin; current master retains the scanner path that does not append thex,u, orUescape digits to the origin buffer.Semantic probes also rule out a full migration:
yes,no, andondecoded as strings, matching YAML 1.2 expectations.010decoded as octal8; goccy/go-yaml#894 tracks this YAML 1.1 behavior. GitHub's runner applies YAML 1.2 decimal handling.The GitHub implementations remain useful references. The current runner uses YAML 1.2 core scalar resolution, while
@actions/workflow-parserretains decoded values, raw scalar source, block-scalar headers, and source ranges. The latter demonstrates that preserving source tokens is practical, but it does not provide the per-character map needed here.Performance and dependency cost
A local five-sample parser benchmark used a synthetic 32,818-byte workflow containing 30 jobs and 360 steps across all scalar styles:
Adding the AST sidecar would make the YAML parsing stage about 3.5 times slower and 4.5 times heavier in allocated bytes before lint rules run. Whole-lint impact would be smaller when external checks dominate.
goccy/go-yamlhas no external module dependencies. In a minimal linked-binary comparison, adding its parser alongside go-yaml increased the binary by about 734 KB. Correctness remains the blocking concern; the performance and binary costs are secondary.Implementation plan
1. Preserve a single semantic authority
run:values exclusively withgo.yaml.in/yaml/v4.goccy/go-yaml.2. Build a scalar mapping corpus
Add table-driven parser tests for:
|,|-,|+,>,>-, and>+;Each case must record the expected decoded script and expected YAML positions for selected script characters. The reconstructed decoded bytes must equal
yaml.Node.Valuebefore any mapping is accepted.3. Extend the existing mapper incrementally
Implement independent style-specific decoders in this order:
Each decoder should emit sparse
scriptSourceSpanrecords while reconstructing the decoded script. Source characters that synthesize no decoded character, and decoded whitespace synthesized by YAML folding, should remain outside mapped spans.4. Enforce conservative per-diagnostic fallback
yaml.Node.Valueor if the scalar boundary cannot be established safely.run.RunPos, preserving the existing behavior.5. Track the upstream improvement without blocking
EndLineandEndColumnalone do not solve scalar transformations.6. Keep hosted compatibility testing separate
A GitHub-hosted conformance probe remains valuable for actionlint's parser compatibility as a whole, especially for scalar resolution and rejecting constructs. It is not a prerequisite for this mapping implementation because the mapper does not become a semantic parser.
If the hosted probe is implemented, record dated observations for accepted values and isolate parser-rejection cases into separate workflow revisions so one invalid construct cannot mask the rest of the corpus.
Acceptance criteria
yaml.Node.Valuebyte-for-byte.run:-key fallback.go test ./...,go test -race ./...,go vet ./..., the repository's static checks, andgit diff --checkpass.