Validate expression block-scalar chomping

This commit is contained in:
eric sciple
2025-11-22 05:43:40 +00:00
parent 7f8bba4305
commit 667b1273e1
12 changed files with 2923 additions and 10 deletions
@@ -0,0 +1,58 @@
import {registerLogger} from "./log";
import {createDocument} from "./test-utils/document";
import {TestLogger} from "./test-utils/logger";
import {clearCache} from "./utils/workflow-cache";
import {validate} from "./validate";
registerLogger(new TestLogger());
beforeEach(() => {
clearCache();
});
describe("block scalar chomping - allowed cases", () => {
it("does NOT warn for step.run with clip chomping (exception)", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: |
echo \${{ github.event_name }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
it("does not warn for inline expression", async () => {
const input = `
on: push
jobs:
build:
if: \${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
it("does not warn for quoted string", async () => {
const input = `
on: push
jobs:
build:
if: "\${{ github.event_name == 'push' }}"
runs-on: ubuntu-latest
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
@@ -0,0 +1,142 @@
import {DiagnosticSeverity} from "vscode-languageserver-types";
import {registerLogger} from "./log";
import {createDocument} from "./test-utils/document";
import {TestLogger} from "./test-utils/logger";
import {clearCache} from "./utils/workflow-cache";
import {validate} from "./validate";
registerLogger(new TestLogger());
beforeEach(() => {
clearCache();
});
describe("block scalar chomping - boolean fields", () => {
describe("job continue-on-error", () => {
it("errors with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
continue-on-error: |
\${{ matrix.experimental }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("errors with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
continue-on-error: |+
\${{ matrix.experimental }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("does not error with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
continue-on-error: |-
\${{ matrix.experimental }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
describe("step continue-on-error", () => {
it("errors with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hi
continue-on-error: |
\${{ matrix.experimental }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("errors with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hi
continue-on-error: |+
\${{ matrix.experimental }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("does not error with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hi
continue-on-error: |-
\${{ matrix.experimental }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
});
@@ -0,0 +1,188 @@
import {DiagnosticSeverity} from "vscode-languageserver-types";
import {registerLogger} from "./log";
import {createDocument} from "./test-utils/document";
import {TestLogger} from "./test-utils/logger";
import {clearCache} from "./utils/workflow-cache";
import {validate} from "./validate";
registerLogger(new TestLogger());
beforeEach(() => {
clearCache();
});
describe("block scalar chomping - if fields", () => {
describe("job-if", () => {
it("errors with clip chomping", async () => {
const input = `
on: push
jobs:
build:
if: |
\${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("errors with keep chomping", async () => {
const input = `
on: push
jobs:
build:
if: |+
\${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("does not error with strip chomping", async () => {
const input = `
on: push
jobs:
build:
if: |-
\${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
it("errors without ${{ }} (isExpression)", async () => {
const input = `
on: push
jobs:
build:
if: |
github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("uses > indicator in error message for folded scalars", async () => {
const input = `
on: push
jobs:
build:
if: >
\${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks boolean evaluation. Use '>-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
});
describe("step-if", () => {
it("errors with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- if: |
\${{ github.event_name == 'push' }}
run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("errors with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- if: |+
\${{ github.event_name == 'push' }}
run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks boolean evaluation. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("does not error with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- if: |-
\${{ github.event_name == 'push' }}
run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
});
@@ -0,0 +1,428 @@
import {DiagnosticSeverity} from "vscode-languageserver-types";
import {registerLogger} from "./log";
import {createDocument} from "./test-utils/document";
import {TestLogger} from "./test-utils/logger";
import {clearCache} from "./utils/workflow-cache";
import {validate} from "./validate";
registerLogger(new TestLogger());
beforeEach(() => {
clearCache();
});
describe("block scalar chomping - number fields", () => {
describe("job timeout-minutes", () => {
it("errors with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: |
\${{ matrix.timeout }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("errors with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: |+
\${{ matrix.timeout }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("does not error with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: |-
\${{ matrix.timeout }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
describe("container.ports", () => {
it("errors with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:16
ports: |
\${{ fromJSON('[80, 443]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("errors with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:16
ports: |+
\${{ fromJSON('[8080, 9090]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("does not error with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:16
ports: |-
\${{ fromJSON('[80, 443]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
describe("container.volumes", () => {
it("errors with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:16
volumes: |
\${{ fromJSON('["/data:/data"]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("errors with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:16
volumes: |+
\${{ fromJSON('["/data:/data"]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("does not error with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:16
volumes: |-
\${{ fromJSON('["/data:/data"]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
describe("services.ports", () => {
it("errors with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
ports: |
\${{ fromJSON('[5432]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("errors with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
ports: |+
\${{ fromJSON('[5432]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("does not error with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
ports: |-
\${{ fromJSON('[5432]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
describe("services.volumes", () => {
it("errors with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
volumes: |
\${{ fromJSON('["/var/lib/postgresql/data"]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("errors with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
volumes: |+
\${{ fromJSON('["/var/lib/postgresql/data"]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("does not error with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
volumes: |-
\${{ fromJSON('["/var/lib/postgresql/data"]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
describe("step timeout-minutes", () => {
it("errors with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hi
timeout-minutes: |
\${{ matrix.timeout }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("errors with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hi
timeout-minutes: |+
\${{ matrix.timeout }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline which breaks number parsing. Use '|-' to strip trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Error
})
);
});
it("does not error with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hi
timeout-minutes: |-
\${{ matrix.timeout }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,227 @@
import {DiagnosticSeverity} from "vscode-languageserver-types";
import {registerLogger} from "./log";
import {createDocument} from "./test-utils/document";
import {TestLogger} from "./test-utils/logger";
import {clearCache} from "./utils/workflow-cache";
import {validate} from "./validate";
registerLogger(new TestLogger());
beforeEach(() => {
clearCache();
});
describe("expression validation", () => {
describe("block scalar chomping - fields that warn only for clip", () => {
describe("env", () => {
it("warns with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: |
echo $VAR
env:
VAR: |
\${{ matrix.value }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline to expression result. Use '|-' to strip or '|+' to keep trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Warning
})
);
});
it("does not warn with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: |
echo $VAR
env:
VAR: |+
\${{ matrix.value }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
it("does not warn with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: |
echo $VAR
env:
VAR: |-
\${{ matrix.value }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
it("uses > indicator in warning message for folded scalars", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: |
echo $VAR
env:
VAR: >
\${{ matrix.value }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline to expression result. Use '>-' to strip or '>+' to keep trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Warning
})
);
});
});
describe("action input", () => {
it("warns with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: |
\${{ github.ref }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline to expression result. Use '|-' to strip or '|+' to keep trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Warning
})
);
});
it("does not warn with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: |+
\${{ github.ref }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
it("does not warn with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: |-
\${{ github.ref }}
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
describe("matrix value", () => {
it("warns with clip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
version: |
\${{ fromJSON('[1, 2, 3]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result).toContainEqual(
expect.objectContaining({
message:
"Block scalar adds trailing newline to expression result. Use '|-' to strip or '|+' to keep trailing newlines.",
code: "expression-block-scalar-chomping",
severity: DiagnosticSeverity.Warning
})
);
});
it("does not warn with keep chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
version: |+
\${{ fromJSON('[1, 2, 3]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
it("does not warn with strip chomping", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
version: |-
\${{ fromJSON('[1, 2, 3]') }}
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
expect(result.filter(d => d.code === "expression-block-scalar-chomping")).toEqual([]);
});
});
});
});
+99
View File
@@ -3,12 +3,14 @@ import {Expr} from "@actions/expressions/ast";
import {ParseWorkflowResult, WorkflowTemplate, isBasicExpression, isString} from "@actions/workflow-parser";
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
import {splitAllowedContext} from "@actions/workflow-parser/templates/allowed-context";
import {Definition} from "@actions/workflow-parser/templates/schema/definition";
import {BasicExpressionToken} from "@actions/workflow-parser/templates/tokens/basic-expression-token";
import {StringToken} from "@actions/workflow-parser/templates/tokens/string-token";
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
import {TokenRange} from "@actions/workflow-parser/templates/tokens/token-range";
import {File} from "@actions/workflow-parser/workflows/file";
import {FileProvider} from "@actions/workflow-parser/workflows/file-provider";
import {Scalar} from "yaml";
import {TextDocument} from "vscode-languageserver-textdocument";
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
import {ActionMetadata, ActionReference} from "./action";
@@ -106,6 +108,8 @@ async function additionalValidations(
config?.contextProviderConfig,
getProviderContext(documentUri, template, root, token.range)
);
validateChomp(diagnostics, token, parent, key, validationDefinition);
}
if (token.definition?.key === "regular-step" && token.range) {
@@ -217,3 +221,98 @@ async function validateExpression(
);
}
}
function validateChomp(
diagnostics: Diagnostic[],
token: BasicExpressionToken,
parent: TemplateToken | undefined,
key: TemplateToken | undefined,
validationDefinition: Definition | undefined
): void {
// Not "clip" or "keep" chomp style?
if (token.chompStyle !== "clip" && token.chompStyle !== "keep") {
return;
}
// No definition? This can happen when the token is in an invalid position or the workflow has parse errors.
if (!validationDefinition) {
return;
}
// Step "run"?
if (parent?.definition?.key === "run-step" && key?.isScalar && key.toString() === "run") {
return;
}
// Block scalar indicator, i.e. | or >
const scalarIndicator = token.scalarType === Scalar.BLOCK_LITERAL ? "|" : ">";
const defKey = validationDefinition.key;
const parentDefKey = parent?.definition?.key;
// Error for boolean fields
if (
defKey === "job-if" ||
defKey === "step-if" ||
defKey === "step-continue-on-error" ||
(parentDefKey === "job-factory" && key?.isScalar && key.toString() === "continue-on-error")
) {
diagnostics.push({
message: `Block scalar adds trailing newline which breaks boolean evaluation. Use '${scalarIndicator}-' to strip trailing newlines.`,
range: mapRange(token.range),
severity: DiagnosticSeverity.Error,
code: "expression-block-scalar-chomping",
source: "github-actions"
});
}
// Error for number fields
else if (
defKey === "step-timeout-minutes" ||
(parentDefKey === "container-mapping" && key?.isScalar && ["ports", "volumes"].includes(key.toString())) ||
(parentDefKey === "job-factory" && key?.isScalar && key.toString() === "timeout-minutes")
) {
diagnostics.push({
message: `Block scalar adds trailing newline which breaks number parsing. Use '${scalarIndicator}-' to strip trailing newlines.`,
range: mapRange(token.range),
severity: DiagnosticSeverity.Error,
code: "expression-block-scalar-chomping",
source: "github-actions"
});
}
// Error for specific string fields
else if (
defKey === "run-name" ||
defKey === "step-name" ||
defKey === "container" ||
defKey === "services-container" ||
defKey === "job-environment" ||
defKey === "job-environment-name" ||
defKey === "runs-on" ||
defKey === "runs-on-labels" ||
(parentDefKey === "container-mapping" && key?.isScalar && ["image", "credentials"].includes(key.toString())) ||
(parentDefKey === "job-defaults-run" && key?.isScalar && ["shell", "working-directory"].includes(key.toString())) ||
(parentDefKey === "job-environment-mapping" && key?.isScalar && key.toString() === "url") ||
(parentDefKey === "job-factory" && key?.isScalar && key.toString() === "name") ||
(parentDefKey === "workflow-job" && key?.isScalar && key.toString() === "name") ||
(parentDefKey === "run-step" && key?.isScalar && key.toString() === "working-directory") ||
(parentDefKey === "runs-on-mapping" && key?.isScalar && key.toString() === "group")
) {
diagnostics.push({
message: `Block scalar adds trailing newline. Use '${scalarIndicator}-' to strip trailing newlines.`,
range: mapRange(token.range),
severity: DiagnosticSeverity.Error,
code: "expression-block-scalar-chomping",
source: "github-actions"
});
}
// Warning for everything else, but only on clip (default)
else if (token.chompStyle === "clip") {
diagnostics.push({
message: `Block scalar adds trailing newline to expression result. Use '${scalarIndicator}-' to strip or '${scalarIndicator}+' to keep trailing newlines.`,
range: mapRange(token.range),
severity: DiagnosticSeverity.Warning,
code: "expression-block-scalar-chomping",
source: "github-actions"
});
}
}