-
Notifications
You must be signed in to change notification settings - Fork 52
Validate expression block-scalar chomping #215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
59e3d03 to
94a0b69
Compare
77c2614 to
3223373
Compare
3223373 to
667b127
Compare
| // Extract block scalar header. For example |-, |+, >- | ||
| // | ||
| // This relies on undocumented internal behavior (srcToken.props). | ||
| // Feature request for official support: https://github.com/eemeli/yaml/issues/643 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I opened an issue and a corresponding PR with a proposed solution
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive validation for block scalar chomping indicators in GitHub Actions workflow expressions. Block scalars (| and >) add trailing newlines by default, which can break expression evaluation in fields expecting booleans, numbers, or single-line strings.
Key changes:
- Enhanced token classes to capture and propagate block scalar metadata (type and chomp style) from YAML parsing through expression validation
- Implemented validation logic that produces errors for boolean/number/string fields and warnings for other fields when improper chomping is used
- Added extensive test coverage across 6 test files covering different field types and severity levels
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
workflow-parser/src/workflows/yaml-object-reader.ts |
Extracts block scalar header information from YAML tokens using undocumented internal YAML library APIs |
workflow-parser/src/templates/tokens/string-token.ts |
Extends StringToken to store scalar type and block scalar header metadata |
workflow-parser/src/templates/tokens/basic-expression-token.ts |
Extends BasicExpressionToken with scalar type and chomp style fields to enable validation |
workflow-parser/src/templates/template-reader.ts |
Parses block scalar headers to extract chomp indicators and propagates this information to expression tokens |
languageservice/src/validate.ts |
Implements validation logic that errors on improper chomping for booleans/numbers/strings and warns for other fields |
workflow-parser/src/expressions.test.ts |
Unit tests verifying block scalar metadata preservation across various chomping indicators |
languageservice/src/validate.expressions-chomp-*.test.ts |
Integration tests for validation across error/warning cases organized by field type |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const blockScalarMatch = header.match(/^(\||>)([-+])?(\d)?/); | ||
|
|
||
| if (!blockScalarMatch) { | ||
| // Assume clip if we can't parse the indicator | ||
| return {scalarType, chompStyle: "clip"}; | ||
| } | ||
|
|
||
| const chompIndicator = blockScalarMatch[2]; |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex pattern /^(\||>)([-+])?(\d)?/ only matches chomp indicator before indentation (e.g., |-2), but YAML allows both orders: |-2 and |2-. The pattern should be /^(\||>)(?:([-+])(\d)?|(\d)([-+])?)$/ to correctly parse both |2- and |-2 formats.
| const blockScalarMatch = header.match(/^(\||>)([-+])?(\d)?/); | |
| if (!blockScalarMatch) { | |
| // Assume clip if we can't parse the indicator | |
| return {scalarType, chompStyle: "clip"}; | |
| } | |
| const chompIndicator = blockScalarMatch[2]; | |
| const blockScalarMatch = header.match(/^(\||>)(?:([-+])(\d)?|(\d)([-+])?)$/); | |
| if (!blockScalarMatch) { | |
| // Assume clip if we can't parse the indicator | |
| return {scalarType, chompStyle: "clip"}; | |
| } | |
| // The chomp indicator may be in group 2 or 5 depending on the order | |
| const chompIndicator = blockScalarMatch[2] || blockScalarMatch[5]; |
| // Error for number fields | ||
| else if ( | ||
| defKey === "step-timeout-minutes" || | ||
| (parentDefKey === "container-mapping" && key?.isScalar && ["ports", "volumes"].includes(key.toString())) || |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The volumes field contains an array of volume mount strings (e.g., /data:/data), not numbers. It should be categorized as a string field (line 283-306) rather than a number field. The error message "breaks number parsing" is incorrect for volumes.
| } | ||
|
|
||
| // Block scalar indicator, i.e. | or > | ||
| const scalarIndicator = token.scalarType === Scalar.BLOCK_LITERAL ? "|" : ">"; |
Copilot
AI
Nov 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code assumes scalarType is either BLOCK_LITERAL or BLOCK_FOLDED when determining the scalar indicator. For defensive programming, consider adding a check to ensure scalarType is defined, or adding a fallback case to handle unexpected values. For example: const scalarIndicator = token.scalarType === Scalar.BLOCK_LITERAL ? "|" : token.scalarType === Scalar.BLOCK_FOLDED ? ">" : "|";
| const scalarIndicator = token.scalarType === Scalar.BLOCK_LITERAL ? "|" : ">"; | |
| const scalarIndicator = | |
| token.scalarType === Scalar.BLOCK_LITERAL | |
| ? "|" | |
| : token.scalarType === Scalar.BLOCK_FOLDED | |
| ? ">" | |
| : "|"; |
| defKey === "step-continue-on-error" || | ||
| (parentDefKey === "job-factory" && key?.isScalar && key.toString() === "continue-on-error") | ||
| ) { | ||
| diagnostics.push({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually we might be able to safely trim the trailing newlines on the server for all boolean cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then we won't need an error here.
| (parentDefKey === "container-mapping" && key?.isScalar && ["ports", "volumes"].includes(key.toString())) || | ||
| (parentDefKey === "job-factory" && key?.isScalar && key.toString() === "timeout-minutes") | ||
| ) { | ||
| diagnostics.push({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, for numbers we can likely safely trim trailing newlines on the server for all number cases
| (parentDefKey === "run-step" && key?.isScalar && key.toString() === "working-directory") || | ||
| (parentDefKey === "runs-on-mapping" && key?.isScalar && key.toString() === "group") | ||
| ) { | ||
| diagnostics.push({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For all of these cases as well, we can likely safely trim trailing newlines on the server
| } | ||
| // Warning for everything else, but only on clip (default) | ||
| else if (token.chompStyle === "clip") { | ||
| diagnostics.push({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For everything else, it's ambiguous. Warn and suggest the user specify keep or strip (clear intent)
Summary
Adds validation for multi-line expressions. Block scalars (
|and>) add trailing newlines by default, which can cause unintentional expression results.In fields where trailing newlines don't make sense (booleans, numbers, and many strings), the validation produces an error:
Boolean:
Number:
String:
In other fields where newlines are unlikely, a warning is produced:
The
runstep is the exception to the rule, since trailing newlines don't matter:Testing
Added comprehensive test coverage across 6 test files organized by severity and field type:
iffields. Special because this is the only place${{ }}is optional.continue-on-errortimeout-minutes,ports,volumesname,runs-on,container,environmentrunEach test validates behavior across different chomping indicators - e.g.
|,|-,|+,>, etc.