In YAML, block scalars (`|` and `>`) silently add a trailing newline by default
("clip" chomping). This can cause subtle bugs when the newline is unintentional.
This PR adds a warning when clip chomping is used in fields where trailing
newlines commonly cause issues:
- Environment variables (workflow, job, step, container, service levels)
- Action inputs (`with:`)
- Reusable workflow inputs and secrets
- Job outputs
- Matrix values (including `include` and `exclude`)
- Concurrency groups
The warning suggests using `|-` (strip) or `|+` (keep) to be explicit.
Intentionally does NOT warn for:
- `run:` scripts (trailing newlines are normal)
- Fields trimmed server-side (`if:`, `name:`, `runs-on:`, etc.)
The feature is gated behind the `blockScalarChompingWarning` feature flag.
85 lines
2.4 KiB
TypeScript
85 lines
2.4 KiB
TypeScript
import {TemplateToken} from "./index.js";
|
|
import {MappingToken} from "./mapping-token.js";
|
|
import {SequenceToken} from "./sequence-token.js";
|
|
import {TokenType} from "./types.js";
|
|
|
|
export class TraversalState {
|
|
private readonly _token: TemplateToken;
|
|
private index = -1;
|
|
private isKey = false;
|
|
public readonly parent: TraversalState | undefined;
|
|
public current: TemplateToken | undefined;
|
|
public currentKey: TemplateToken | undefined;
|
|
|
|
public constructor(parent: TraversalState | undefined, token: TemplateToken) {
|
|
this.parent = parent;
|
|
this._token = token;
|
|
this.current = token;
|
|
}
|
|
|
|
public moveNext(omitKeys: boolean): boolean {
|
|
switch (this._token.templateTokenType) {
|
|
case TokenType.Sequence: {
|
|
const sequence = this._token as SequenceToken;
|
|
if (++this.index < sequence.count) {
|
|
this.current = sequence.get(this.index);
|
|
return true;
|
|
}
|
|
this.current = undefined;
|
|
return false;
|
|
}
|
|
|
|
case TokenType.Mapping: {
|
|
const mapping = this._token as MappingToken;
|
|
|
|
// Already returned the key, now return the value
|
|
if (this.isKey) {
|
|
this.isKey = false;
|
|
this.currentKey = this.current;
|
|
this.current = mapping.get(this.index).value;
|
|
return true;
|
|
}
|
|
|
|
// Move next
|
|
if (++this.index < mapping.count) {
|
|
// Skip the key, return the value
|
|
if (omitKeys) {
|
|
this.isKey = false;
|
|
this.currentKey = mapping.get(this.index).key;
|
|
this.current = mapping.get(this.index).value;
|
|
return true;
|
|
}
|
|
|
|
// Return the key
|
|
this.isKey = true;
|
|
this.currentKey = undefined;
|
|
this.current = mapping.get(this.index).key;
|
|
return true;
|
|
}
|
|
|
|
this.currentKey = undefined;
|
|
this.current = undefined;
|
|
return false;
|
|
}
|
|
|
|
default:
|
|
throw new Error(`Unexpected token type '${this._token.templateTokenType}' when traversing state`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the ancestor tokens from root to the current token's parent container.
|
|
*/
|
|
public getAncestors(): TemplateToken[] {
|
|
const ancestors: TemplateToken[] = [];
|
|
let state: TraversalState | undefined = this.parent;
|
|
while (state) {
|
|
if (state.current) {
|
|
ancestors.unshift(state.current);
|
|
}
|
|
state = state.parent;
|
|
}
|
|
return ancestors;
|
|
}
|
|
}
|