Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
667b1273e1 |
-152
@@ -1,152 +0,0 @@
|
|||||||
# Using GitHub Actions Language Server in Neovim
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Node.js 18+
|
|
||||||
- Neovim 0.11+ with the new LSP config format
|
|
||||||
|
|
||||||
## Setup Options
|
|
||||||
|
|
||||||
### Option 1: Install from npm (Recommended)
|
|
||||||
|
|
||||||
Once published, you can install globally:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g @actions/languageserver
|
|
||||||
```
|
|
||||||
|
|
||||||
Then configure Neovim to use the installed binary:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- ~/.config/nvim/lsp/actionsls.lua
|
|
||||||
return {
|
|
||||||
cmd = { "actions-languageserver" },
|
|
||||||
filetypes = { "yaml.ghaction" }, -- GitHub Actions workflow files only
|
|
||||||
root_markers = { ".git" },
|
|
||||||
init_options = {
|
|
||||||
sessionToken = vim.fn.system("gh auth token"):gsub("%s+", ""),
|
|
||||||
logLevel = "info",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** This requires the package to be published to npm first.
|
|
||||||
|
|
||||||
### Option 2: Local Development Build
|
|
||||||
|
|
||||||
For development or if the npm package isn't published yet:
|
|
||||||
|
|
||||||
### 1. Clone and build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/actions/languageservices.git
|
|
||||||
cd languageservices
|
|
||||||
npm install
|
|
||||||
npm run build --workspaces --if-present
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Bundle the server
|
|
||||||
|
|
||||||
The server needs to be bundled into a single file to avoid ESM module resolution issues:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd languageserver
|
|
||||||
npx esbuild src/index.ts \
|
|
||||||
--bundle \
|
|
||||||
--platform=node \
|
|
||||||
--target=node18 \
|
|
||||||
--format=cjs \
|
|
||||||
--outfile=dist/server-bundled.cjs \
|
|
||||||
--external:vscode \
|
|
||||||
--loader:.json=json
|
|
||||||
```
|
|
||||||
|
|
||||||
This creates `dist/server-bundled.cjs` (~5.6MB) that contains the entire server.
|
|
||||||
|
|
||||||
### 3. Configure Neovim
|
|
||||||
|
|
||||||
Create `~/.config/nvim/lsp/actionsls.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
return {
|
|
||||||
cmd = {
|
|
||||||
"/absolute/path/to/languageservices/languageserver/bin/actions-languageserver",
|
|
||||||
},
|
|
||||||
filetypes = { "yaml.ghaction" }, -- GitHub Actions workflow files only
|
|
||||||
root_markers = { ".git" },
|
|
||||||
init_options = {
|
|
||||||
sessionToken = vim.fn.system("gh auth token"):gsub("%s+", ""),
|
|
||||||
logLevel = "info",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important:** Replace `/absolute/path/to/languageservices` with your actual clone path.
|
|
||||||
|
|
||||||
## Filetype Detection for GitHub Actions Workflows
|
|
||||||
|
|
||||||
To ensure the LSP only runs on GitHub Actions workflow files (not all YAML files), set up filetype detection:
|
|
||||||
|
|
||||||
**Option A:** In `~/.config/nvim/init.lua`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
vim.api.nvim_create_autocmd({"BufRead", "BufNewFile"}, {
|
|
||||||
pattern = ".github/workflows/*.{yml,yaml}",
|
|
||||||
callback = function()
|
|
||||||
vim.bo.filetype = "yaml.ghaction"
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option B:** Create `~/.config/nvim/ftdetect/ghaction.vim`:
|
|
||||||
|
|
||||||
```vim
|
|
||||||
au BufRead,BufNewFile .github/workflows/*.yml,*.yaml setfiletype yaml.ghaction
|
|
||||||
```
|
|
||||||
|
|
||||||
This sets the filetype to `yaml.ghaction` for files in `.github/workflows/`, matching the `filetypes` setting in your LSP config.
|
|
||||||
|
|
||||||
### 4. Enable the LSP in your init.lua
|
|
||||||
|
|
||||||
Add to your Neovim configuration:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
vim.lsp.enable('actionsls')
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Restart Neovim
|
|
||||||
|
|
||||||
Open any `.github/workflows/*.yml` file. The filetype detection will set it to `yaml.ghaction`, and the language server will attach automatically.
|
|
||||||
|
|
||||||
## Files Created
|
|
||||||
|
|
||||||
- `languageserver/dist/server-bundled.cjs` - Bundled server (~5.6MB)
|
|
||||||
- `languageserver/bin/actions-languageserver` - Shell wrapper script
|
|
||||||
|
|
||||||
The `dist/` directory is gitignored; you'll need to rebuild after pulling updates.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
Check if the server is running:
|
|
||||||
|
|
||||||
```vim
|
|
||||||
:lua =vim.lsp.get_clients()
|
|
||||||
```
|
|
||||||
|
|
||||||
View LSP logs:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tail -f ~/.local/state/nvim/lsp.log
|
|
||||||
```
|
|
||||||
|
|
||||||
Manually start the server to test:
|
|
||||||
|
|
||||||
```vim
|
|
||||||
:lua vim.lsp.start({name='actionsls', cmd={'/path/to/bin/actions-languageserver'}, root_dir=vim.fn.getcwd(), init_options={sessionToken='', logLevel='info'}})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- The main code change is in `languageserver/src/index.ts` to use dynamic imports, avoiding loading browser modules in Node.js
|
|
||||||
- The bundling step is necessary because TypeScript outputs ESM with bare imports that Node.js can't resolve
|
|
||||||
- Only workflow files in git repositories will activate the LSP (due to `root_markers = { ".git" }`)
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
import "../dist/cli.bundle.cjs";
|
|
||||||
@@ -32,7 +32,6 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --build tsconfig.build.json",
|
"build": "tsc --build tsconfig.build.json",
|
||||||
"build:cli": "esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/cli.bundle.cjs",
|
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"format": "prettier --write '**/*.ts'",
|
"format": "prettier --write '**/*.ts'",
|
||||||
"format-check": "prettier --check '**/*.ts'",
|
"format-check": "prettier --check '**/*.ts'",
|
||||||
@@ -43,9 +42,6 @@
|
|||||||
"test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
|
"test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
|
||||||
"watch": "tsc --build tsconfig.build.json --watch"
|
"watch": "tsc --build tsconfig.build.json --watch"
|
||||||
},
|
},
|
||||||
"bin": {
|
|
||||||
"actions-languageserver": "./bin/actions-languageserver"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/languageservice": "^0.3.20",
|
"@actions/languageservice": "^0.3.20",
|
||||||
"@actions/workflow-parser": "^0.3.20",
|
"@actions/workflow-parser": "^0.3.20",
|
||||||
@@ -65,7 +61,6 @@
|
|||||||
"@types/jest": "^29.0.3",
|
"@types/jest": "^29.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||||
"@typescript-eslint/parser": "^5.56.0",
|
"@typescript-eslint/parser": "^5.56.0",
|
||||||
"esbuild": "^0.27.1",
|
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
npx esbuild src/index.ts \
|
|
||||||
--bundle \
|
|
||||||
--platform=node \
|
|
||||||
--target=node18 \
|
|
||||||
--format=cjs \
|
|
||||||
--outfile=dist/server-bundled.cjs \
|
|
||||||
--external:vscode \
|
|
||||||
--loader:.json=json
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
import {documentLinks, getCodeActions, hover, validate, ValidationConfig} from "@actions/languageservice";
|
import {documentLinks, hover, validate, ValidationConfig} from "@actions/languageservice";
|
||||||
import {registerLogger, setLogLevel} from "@actions/languageservice/log";
|
import {registerLogger, setLogLevel} from "@actions/languageservice/log";
|
||||||
import {clearCache, clearCacheEntry} from "@actions/languageservice/utils/workflow-cache";
|
import {clearCache, clearCacheEntry} from "@actions/languageservice/utils/workflow-cache";
|
||||||
import {Octokit} from "@octokit/rest";
|
import {Octokit} from "@octokit/rest";
|
||||||
import {
|
import {
|
||||||
CodeAction,
|
|
||||||
CodeActionKind,
|
|
||||||
CodeActionParams,
|
|
||||||
CompletionItem,
|
CompletionItem,
|
||||||
Connection,
|
Connection,
|
||||||
DocumentLink,
|
DocumentLink,
|
||||||
@@ -75,9 +72,6 @@ export function initConnection(connection: Connection) {
|
|||||||
hoverProvider: true,
|
hoverProvider: true,
|
||||||
documentLinkProvider: {
|
documentLinkProvider: {
|
||||||
resolveProvider: false
|
resolveProvider: false
|
||||||
},
|
|
||||||
codeActionProvider: {
|
|
||||||
codeActionKinds: [CodeActionKind.QuickFix]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -164,16 +158,6 @@ export function initConnection(connection: Connection) {
|
|||||||
return documentLinks(getDocument(documents, textDocument), repoContext?.workspaceUri);
|
return documentLinks(getDocument(documents, textDocument), repoContext?.workspaceUri);
|
||||||
});
|
});
|
||||||
|
|
||||||
connection.onCodeAction(async (params: CodeActionParams): Promise<CodeAction[]> => {
|
|
||||||
return timeOperation("codeAction", async () => {
|
|
||||||
return getCodeActions({
|
|
||||||
uri: params.textDocument.uri,
|
|
||||||
diagnostics: params.context.diagnostics,
|
|
||||||
only: params.context.only
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make the text document manager listen on the connection
|
// Make the text document manager listen on the connection
|
||||||
// for open, change and close text document events
|
// for open, change and close text document events
|
||||||
documents.listen(connection);
|
documents.listen(connection);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Connection } from "vscode-languageserver";
|
import {Connection} from "vscode-languageserver";
|
||||||
import {
|
import {
|
||||||
BrowserMessageReader,
|
BrowserMessageReader,
|
||||||
BrowserMessageWriter,
|
BrowserMessageWriter,
|
||||||
createConnection as createBrowserConnection
|
createConnection as createBrowserConnection
|
||||||
} from "vscode-languageserver/browser";
|
} from "vscode-languageserver/browser";
|
||||||
import { createConnection as createNodeConnection } from "vscode-languageserver/node";
|
import {createConnection as createNodeConnection} from "vscode-languageserver/node";
|
||||||
|
|
||||||
import { initConnection } from "./connection";
|
import {initConnection} from "./connection";
|
||||||
|
|
||||||
/** Helper function determining whether we are executing with node runtime */
|
/** Helper function determining whether we are executing with node runtime */
|
||||||
function isNode(): boolean {
|
function isNode(): boolean {
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import {CodeAction, CodeActionKind, Diagnostic} from "vscode-languageserver-types";
|
|
||||||
import {CodeActionContext, CodeActionProvider} from "./types";
|
|
||||||
import {quickfixProviders} from "./quickfix";
|
|
||||||
|
|
||||||
// Aggregate all providers by kind
|
|
||||||
const providersByKind: Map<string, CodeActionProvider[]> = new Map([
|
|
||||||
[CodeActionKind.QuickFix, quickfixProviders]
|
|
||||||
// [CodeActionKind. Refactor, refactorProviders],
|
|
||||||
// [CodeActionKind.Source, sourceProviders],
|
|
||||||
// etc
|
|
||||||
]);
|
|
||||||
|
|
||||||
export interface CodeActionConfig {
|
|
||||||
// TODO: actionsMetadataProvider, fileProvider, etc.
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CodeActionParams {
|
|
||||||
uri: string;
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
only?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCodeActions(params: CodeActionParams, config?: CodeActionConfig): CodeAction[] {
|
|
||||||
const actions: CodeAction[] = [];
|
|
||||||
const context: CodeActionContext = {
|
|
||||||
uri: params.uri
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter to requested kinds, or use all if none specified
|
|
||||||
const requestedKinds = params.only;
|
|
||||||
const kindsToCheck = requestedKinds
|
|
||||||
? [...providersByKind.keys()].filter(kind => requestedKinds.some(requested => kind.startsWith(requested)))
|
|
||||||
: [...providersByKind.keys()];
|
|
||||||
|
|
||||||
for (const diagnostic of params.diagnostics) {
|
|
||||||
for (const kind of kindsToCheck) {
|
|
||||||
const providers = providersByKind.get(kind) ?? [];
|
|
||||||
for (const provider of providers) {
|
|
||||||
if (provider.diagnosticCodes.includes(diagnostic.code)) {
|
|
||||||
const action = provider.createCodeAction(context, diagnostic);
|
|
||||||
if (action) {
|
|
||||||
action.kind = kind;
|
|
||||||
action.diagnostics = [diagnostic];
|
|
||||||
actions.push(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type {CodeActionContext, CodeActionProvider} from "./types";
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { CodeAction, TextEdit } from "vscode-languageserver-types";
|
|
||||||
import { CodeActionContext, CodeActionProvider } from "../types";
|
|
||||||
import { DiagnosticCode, MissingInputsDiagnosticData } from "../../validate-action";
|
|
||||||
|
|
||||||
export const addMissingInputsProvider: CodeActionProvider = {
|
|
||||||
diagnosticCodes: [DiagnosticCode.MissingRequiredInputs],
|
|
||||||
|
|
||||||
createCodeAction(context, diagnostic): CodeAction | undefined {
|
|
||||||
const data = diagnostic.data as MissingInputsDiagnosticData | undefined;
|
|
||||||
if (!data) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const edits = createInputEdits(data);
|
|
||||||
if (!edits) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputNames = data.missingInputs.map(i => i.name).join(", ");
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: `Add missing input${data.missingInputs.length > 1 ? "s" : ""}: ${inputNames}`,
|
|
||||||
edit: {
|
|
||||||
changes: {
|
|
||||||
[context.uri]: edits,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createInputEdits(data: MissingInputsDiagnosticData): TextEdit[] | undefined {
|
|
||||||
const edits: TextEdit[] = [];
|
|
||||||
|
|
||||||
if (data.hasWithKey && data.withIndent !== undefined) {
|
|
||||||
// `with:` exists - use its indentation + 2 for inputs
|
|
||||||
const inputIndent = " ".repeat(data.withIndent + 2);
|
|
||||||
|
|
||||||
const inputLines = data.missingInputs.map(input => {
|
|
||||||
const value = input.default !== undefined ? input.default : '""';
|
|
||||||
return `${inputIndent}${input.name}: ${value}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
edits.push({
|
|
||||||
range: { start: data.insertPosition, end: data.insertPosition },
|
|
||||||
newText: inputLines.map(line => line + "\n").join(""),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// No `with:` key - `with:` at step indentation, inputs at step indentation + 2
|
|
||||||
const withIndent = " ".repeat(data.stepIndent);
|
|
||||||
const inputIndent = " ".repeat(data.stepIndent + 2);
|
|
||||||
|
|
||||||
const inputLines = data.missingInputs.map(input => {
|
|
||||||
const value = input.default !== undefined ? input.default : '""';
|
|
||||||
return `${inputIndent}${input.name}: ${value}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const newText = [`${withIndent}with:\n`, ...inputLines.map(line => `${line}\n`)].join("");
|
|
||||||
|
|
||||||
edits.push({
|
|
||||||
range: { start: data.insertPosition, end: data.insertPosition },
|
|
||||||
newText,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return edits;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import {CodeActionProvider} from "../types";
|
|
||||||
import {addMissingInputsProvider} from "./add-missing-inputs";
|
|
||||||
|
|
||||||
export const quickfixProviders: CodeActionProvider[] = [addMissingInputsProvider];
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import * as path from "path";
|
|
||||||
import {fileURLToPath} from "url";
|
|
||||||
import {loadTestCases, runTestCase} from "./runner";
|
|
||||||
import {ValidationConfig} from "../../validate";
|
|
||||||
import {ActionMetadata, ActionReference} from "../../action";
|
|
||||||
import {clearCache} from "../../utils/workflow-cache";
|
|
||||||
|
|
||||||
// ESM-compatible __dirname
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
// Mock action metadata provider for tests
|
|
||||||
const validationConfig: ValidationConfig = {
|
|
||||||
actionsMetadataProvider: {
|
|
||||||
fetchActionMetadata: (ref: ActionReference): Promise<ActionMetadata | undefined> => {
|
|
||||||
const key = `${ref.owner}/${ref.name}@${ref.ref}`;
|
|
||||||
|
|
||||||
const metadata: Record<string, ActionMetadata> = {
|
|
||||||
"actions/cache@v1": {
|
|
||||||
name: "Cache",
|
|
||||||
description: "Cache dependencies",
|
|
||||||
inputs: {
|
|
||||||
path: {
|
|
||||||
description: "A list of files to cache",
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
key: {
|
|
||||||
description: "Cache key",
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
"restore-keys": {
|
|
||||||
description: "Restore keys",
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"actions/setup-node@v3": {
|
|
||||||
name: "Setup Node",
|
|
||||||
description: "Setup Node. js",
|
|
||||||
inputs: {
|
|
||||||
"node-version": {
|
|
||||||
description: "Node version",
|
|
||||||
required: true,
|
|
||||||
default: "16"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return Promise.resolve(metadata[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Point to the source testdata directory
|
|
||||||
const testdataDir = path.join(__dirname, "testdata");
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
clearCache();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("code action golden tests", () => {
|
|
||||||
const testCases = loadTestCases(testdataDir);
|
|
||||||
|
|
||||||
if (testCases.length === 0) {
|
|
||||||
it.todo("no test cases found - add . yml files to testdata/");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const testCase of testCases) {
|
|
||||||
it(testCase.name, async () => {
|
|
||||||
const result = await runTestCase(testCase, validationConfig);
|
|
||||||
|
|
||||||
if (!result.passed) {
|
|
||||||
let errorMessage = result.error || "Test failed";
|
|
||||||
|
|
||||||
if (result.expected !== undefined && result.actual !== undefined) {
|
|
||||||
errorMessage += "\n\n";
|
|
||||||
errorMessage += "=== EXPECTED (golden file) ===\n";
|
|
||||||
errorMessage += result.expected;
|
|
||||||
errorMessage += "\n\n";
|
|
||||||
errorMessage += "=== ACTUAL ===\n";
|
|
||||||
errorMessage += result.actual;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
import { TextEdit } from "vscode-languageserver-types";
|
|
||||||
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
||||||
import { validate, ValidationConfig } from "../../validate";
|
|
||||||
import { getCodeActions, CodeActionParams } from "../index";
|
|
||||||
|
|
||||||
// Marker pattern: # want "diagnostic message" fix="code-action-name"
|
|
||||||
const MARKER_PATTERN = /#\s*want\s+"([^"]+)"(?:\s+fix="([^"]+)")?/;
|
|
||||||
|
|
||||||
export interface TestCase {
|
|
||||||
name: string;
|
|
||||||
inputPath: string;
|
|
||||||
goldenPath: string;
|
|
||||||
input: string;
|
|
||||||
golden: string;
|
|
||||||
markers: Marker[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Marker {
|
|
||||||
line: number;
|
|
||||||
message: string;
|
|
||||||
fix?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestResult {
|
|
||||||
name: string;
|
|
||||||
passed: boolean;
|
|
||||||
error?: string;
|
|
||||||
expected?: string;
|
|
||||||
actual?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse markers from input file content
|
|
||||||
*/
|
|
||||||
export function parseMarkers(content: string): Marker[] {
|
|
||||||
const lines = content.split("\n");
|
|
||||||
const markers: Marker[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const match = lines[i].match(MARKER_PATTERN);
|
|
||||||
if (match) {
|
|
||||||
markers.push({
|
|
||||||
line: i,
|
|
||||||
message: match[1],
|
|
||||||
fix: match[2]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return markers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strip markers from content (for processing)
|
|
||||||
*/
|
|
||||||
export function stripMarkers(content: string): string {
|
|
||||||
return content
|
|
||||||
.split("\n")
|
|
||||||
.map(line => line.replace(MARKER_PATTERN, "").trimEnd())
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load all test cases from a testdata directory
|
|
||||||
*/
|
|
||||||
export function loadTestCases(testdataDir: string): TestCase[] {
|
|
||||||
const testCases: TestCase[] = [];
|
|
||||||
|
|
||||||
function walkDir(dir: string) {
|
|
||||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(dir, entry.name);
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
walkDir(fullPath);
|
|
||||||
} else if (entry.isFile() && entry.name.endsWith(".yml") && !entry.name.endsWith(". golden.yml")) {
|
|
||||||
const goldenPath = fullPath.replace(".yml", ".golden.yml");
|
|
||||||
|
|
||||||
if (fs.existsSync(goldenPath)) {
|
|
||||||
const input = fs.readFileSync(fullPath, "utf-8");
|
|
||||||
const golden = fs.readFileSync(goldenPath, "utf-8");
|
|
||||||
|
|
||||||
testCases.push({
|
|
||||||
name: path.relative(testdataDir, fullPath),
|
|
||||||
inputPath: fullPath,
|
|
||||||
goldenPath,
|
|
||||||
input,
|
|
||||||
golden,
|
|
||||||
markers: parseMarkers(input)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
walkDir(testdataDir);
|
|
||||||
return testCases;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply text edits to a document
|
|
||||||
*/
|
|
||||||
export function applyEdits(content: string, edits: TextEdit[]): string {
|
|
||||||
// Sort edits in reverse order by position to apply from bottom to top
|
|
||||||
const sortedEdits = [...edits].sort((a, b) => {
|
|
||||||
if (b.range.start.line !== a.range.start.line) {
|
|
||||||
return b.range.start.line - a.range.start.line;
|
|
||||||
}
|
|
||||||
return b.range.start.character - a.range.start.character;
|
|
||||||
});
|
|
||||||
|
|
||||||
const lines = content.split("\n");
|
|
||||||
|
|
||||||
for (const edit of sortedEdits) {
|
|
||||||
const startLine = edit.range.start.line;
|
|
||||||
const startChar = edit.range.start.character;
|
|
||||||
const endLine = edit.range.end.line;
|
|
||||||
const endChar = edit.range.end.character;
|
|
||||||
|
|
||||||
const before = lines[startLine].slice(0, startChar);
|
|
||||||
const after = lines[endLine].slice(endChar);
|
|
||||||
|
|
||||||
const newLines = edit.newText.split("\n");
|
|
||||||
newLines[0] = before + newLines[0];
|
|
||||||
newLines[newLines.length - 1] = newLines[newLines.length - 1] + after;
|
|
||||||
|
|
||||||
lines.splice(startLine, endLine - startLine + 1, ...newLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a single test case
|
|
||||||
*/
|
|
||||||
export async function runTestCase(testCase: TestCase, validationConfig: ValidationConfig): Promise<TestResult> {
|
|
||||||
const strippedInput = stripMarkers(testCase.input);
|
|
||||||
const document = TextDocument.create("file:///test.yml", "yaml", 1, strippedInput);
|
|
||||||
|
|
||||||
// 1. Validate and get diagnostics
|
|
||||||
const diagnostics = await validate(document, validationConfig);
|
|
||||||
|
|
||||||
// 2. Verify all expected diagnostics are present
|
|
||||||
const missingDiagnostics: string[] = [];
|
|
||||||
for (const marker of testCase.markers) {
|
|
||||||
const found = diagnostics.find(d => d.range.start.line === marker.line && d.message.includes(marker.message));
|
|
||||||
console.log(found);
|
|
||||||
if (!found) {
|
|
||||||
missingDiagnostics.push(`line ${marker.line}: "${marker.message}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingDiagnostics.length > 0) {
|
|
||||||
return {
|
|
||||||
name: testCase.name,
|
|
||||||
passed: false,
|
|
||||||
error: `Missing expected diagnostics:\n ${missingDiagnostics.join(
|
|
||||||
"\n "
|
|
||||||
)}\n\nActual diagnostics:\n ${diagnostics.map(d => `line ${d.range.start.line}: "${d.message}"`).join("\n ")}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Collect all edits from all matching code actions
|
|
||||||
const allEdits: TextEdit[] = [];
|
|
||||||
|
|
||||||
for (const marker of testCase.markers) {
|
|
||||||
if (!marker.fix) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const diagnostic = diagnostics.find(d => d.range.start.line === marker.line && d.message.includes(marker.message));
|
|
||||||
|
|
||||||
if (!diagnostic) {
|
|
||||||
continue; // Already reported above
|
|
||||||
}
|
|
||||||
|
|
||||||
const params: CodeActionParams = {
|
|
||||||
uri: document.uri,
|
|
||||||
diagnostics: [diagnostic]
|
|
||||||
};
|
|
||||||
|
|
||||||
const actions = getCodeActions(params);
|
|
||||||
const matchingAction = actions.find(a => a.title.toLowerCase().includes(marker.fix!.toLowerCase()));
|
|
||||||
|
|
||||||
if (!matchingAction) {
|
|
||||||
return {
|
|
||||||
name: testCase.name,
|
|
||||||
passed: false,
|
|
||||||
error: `Code action "${marker.fix}" not found for diagnostic on line ${marker.line}.\nAvailable actions: ${actions.map(a => a.title).join(", ") || "(none)"
|
|
||||||
}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matchingAction.edit?.changes) {
|
|
||||||
return {
|
|
||||||
name: testCase.name,
|
|
||||||
passed: false,
|
|
||||||
error: `Code action "${marker.fix}" has no edits`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const edits = matchingAction.edit.changes[document.uri] || [];
|
|
||||||
allEdits.push(...edits);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Apply all edits and compare to golden file
|
|
||||||
const actualOutput = applyEdits(strippedInput, allEdits);
|
|
||||||
const expectedOutput = testCase.golden;
|
|
||||||
|
|
||||||
if (actualOutput.trim() !== expectedOutput.trim()) {
|
|
||||||
return {
|
|
||||||
name: testCase.name,
|
|
||||||
passed: false,
|
|
||||||
error: "Output does not match golden file",
|
|
||||||
expected: expectedOutput,
|
|
||||||
actual: actualOutput
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: testCase.name,
|
|
||||||
passed: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
languageservice/src/code-actions/tests/testdata/quickfix/existing-with-key-without-inputs.golden.yml
Vendored
-9
@@ -1,9 +0,0 @@
|
|||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ""
|
|
||||||
key: ""
|
|
||||||
Vendored
-7
@@ -1,7 +0,0 @@
|
|||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/cache@v1
|
|
||||||
with: # want "Missing required inputs: `path`, `key`" fix="Add missing inputs: path, key"
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
restore-keys: ${{ runner.os }}-
|
|
||||||
path: ""
|
|
||||||
key: ""
|
|
||||||
-8
@@ -1,8 +0,0 @@
|
|||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/cache@v1
|
|
||||||
with: # want "Missing required inputs: `path`, `key`" fix="Add missing inputs: path, key"
|
|
||||||
restore-keys: ${{ runner.os }}-
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ""
|
|
||||||
key: ""
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/cache@v1 # want "Missing required inputs: `path`, `key`" fix="Add missing inputs: path, key"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import {CodeAction, Diagnostic} from "vscode-languageserver-types";
|
|
||||||
|
|
||||||
export interface CodeActionContext {
|
|
||||||
uri: string;
|
|
||||||
// TODO: add things like workflow template, parsed content, etc.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A provider that can produce a code action for a given diagnostic
|
|
||||||
*/
|
|
||||||
export interface CodeActionProvider {
|
|
||||||
/**
|
|
||||||
* The diagnostic codes this provider handles
|
|
||||||
*/
|
|
||||||
diagnosticCodes: (string | number | undefined)[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a code action for the diagnostic, if applicable
|
|
||||||
*/
|
|
||||||
createCodeAction(context: CodeActionContext, diagnostic: Diagnostic): CodeAction | undefined;
|
|
||||||
}
|
|
||||||
@@ -69,59 +69,6 @@ jobs:
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("job-level if condition without status function (gets wrapped)", () => {
|
|
||||||
expect(
|
|
||||||
testMapToExpressionPos(`on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: git|hub.event_name == 'push'
|
|
||||||
runs-on: ubuntu-latest`)
|
|
||||||
).toEqual<ExpressionPos>({
|
|
||||||
expression: "success() && (github.event_name == 'push')",
|
|
||||||
position: {line: 0, column: 17}, // "success() && (".length + 3 = 17
|
|
||||||
documentRange: {
|
|
||||||
start: {line: 3, character: 8},
|
|
||||||
end: {line: 3, character: 35} // End of the original condition in the document
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("job-level if condition with status function (not wrapped)", () => {
|
|
||||||
expect(
|
|
||||||
testMapToExpressionPos(`on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: alw|ays()
|
|
||||||
runs-on: ubuntu-latest`)
|
|
||||||
).toEqual<ExpressionPos>({
|
|
||||||
expression: "always()",
|
|
||||||
position: {line: 0, column: 3},
|
|
||||||
documentRange: {
|
|
||||||
start: {line: 3, character: 8},
|
|
||||||
end: {line: 3, character: 16}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("step-level if condition without status function (gets wrapped)", () => {
|
|
||||||
expect(
|
|
||||||
testMapToExpressionPos(`on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- if: steps.test.outc|ome == 'success'
|
|
||||||
run: echo hello`)
|
|
||||||
).toEqual<ExpressionPos>({
|
|
||||||
expression: "success() && (steps.test.outcome == 'success')",
|
|
||||||
position: {line: 0, column: 29}, // Actual position in the wrapped expression
|
|
||||||
documentRange: {
|
|
||||||
start: {line: 5, character: 12},
|
|
||||||
end: {line: 5, character: 43} // End of the original condition in the document
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function testMapToExpressionPos(input: string) {
|
function testMapToExpressionPos(input: string) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {Pos} from "@actions/expressions/lexer";
|
import {Pos} from "@actions/expressions/lexer";
|
||||||
import {ensureStatusFunction} from "@actions/workflow-parser/model/converter/if-condition";
|
|
||||||
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
||||||
import {isBasicExpression, isString} from "@actions/workflow-parser/templates/tokens/type-guards";
|
import {isBasicExpression} from "@actions/workflow-parser/templates/tokens/type-guards";
|
||||||
import {Position, Range as LSPRange} from "vscode-languageserver-textdocument";
|
import {Position, Range as LSPRange} from "vscode-languageserver-textdocument";
|
||||||
import {mapRange} from "../utils/range";
|
import {mapRange} from "../utils/range";
|
||||||
import {posWithinRange} from "./pos-range";
|
import {posWithinRange} from "./pos-range";
|
||||||
@@ -17,52 +16,12 @@ export type ExpressionPos = {
|
|||||||
documentRange: LSPRange;
|
documentRange: LSPRange;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a document position to an expression position for hover/completion features.
|
|
||||||
*
|
|
||||||
* This handles both explicit expressions (with ${{ }}) and implicit expressions (like if conditions).
|
|
||||||
* For if conditions without ${{ }}, this applies the same conversion as the parser's convertToIfCondition,
|
|
||||||
* wrapping them in `success() && (...)` when no status function is present.
|
|
||||||
*
|
|
||||||
* @param token The template token at the position
|
|
||||||
* @param position The position in the document
|
|
||||||
* @returns Expression and adjusted position, or undefined if not an expression
|
|
||||||
*/
|
|
||||||
export function mapToExpressionPos(token: TemplateToken, position: Position): ExpressionPos | undefined {
|
export function mapToExpressionPos(token: TemplateToken, position: Position): ExpressionPos | undefined {
|
||||||
const pos: Pos = {
|
const pos: Pos = {
|
||||||
line: position.line + 1,
|
line: position.line + 1,
|
||||||
column: position.character + 1
|
column: position.character + 1
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle if conditions that are string tokens (job-if, step-if, snapshot-if)
|
|
||||||
const definitionKey = token.definition?.key;
|
|
||||||
if (
|
|
||||||
isString(token) &&
|
|
||||||
token.range &&
|
|
||||||
(definitionKey === "job-if" || definitionKey === "step-if" || definitionKey === "snapshot-if")
|
|
||||||
) {
|
|
||||||
const condition = token.value.trim();
|
|
||||||
if (condition) {
|
|
||||||
// Ensure the condition has a status function, wrapping if needed
|
|
||||||
const finalCondition = ensureStatusFunction(condition, token.definitionInfo);
|
|
||||||
|
|
||||||
const exprRange = mapRange(token.range);
|
|
||||||
|
|
||||||
// Calculate offset: find where the original condition appears in the final expression
|
|
||||||
// If wrapped, it will be after "success() && (", otherwise it's at position 0
|
|
||||||
const offset = finalCondition.indexOf(condition);
|
|
||||||
|
|
||||||
return {
|
|
||||||
expression: finalCondition,
|
|
||||||
position: {
|
|
||||||
line: pos.line - exprRange.start.line - 1,
|
|
||||||
column: pos.column - exprRange.start.character - 1 + offset
|
|
||||||
},
|
|
||||||
documentRange: exprRange
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isBasicExpression(token)) {
|
if (!isBasicExpression(token)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,8 +155,8 @@ jobs:
|
|||||||
contents:
|
contents:
|
||||||
"Causes the step to always execute, and returns `true`, even when canceled. The `always` expression is best used at the step level or on tasks that you expect to run even when a job is canceled. For example, you can use `always` to send logs even when a job is canceled.",
|
"Causes the step to always execute, and returns `true`, even when canceled. The `always` expression is best used at the step level or on tasks that you expect to run even when a job is canceled. For example, you can use `always` to send logs even when a job is canceled.",
|
||||||
range: {
|
range: {
|
||||||
start: {line: 3, character: 8},
|
start: {line: 3, character: 11},
|
||||||
end: {line: 3, character: 14}
|
end: {line: 3, character: 17}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,4 +5,3 @@ export {hover} from "./hover";
|
|||||||
export {Logger, LogLevel, registerLogger, setLogLevel} from "./log";
|
export {Logger, LogLevel, registerLogger, setLogLevel} from "./log";
|
||||||
export {validate, ValidationConfig, ActionsMetadataProvider} from "./validate";
|
export {validate, ValidationConfig, ActionsMetadataProvider} from "./validate";
|
||||||
export {ValueProviderConfig, ValueProviderKind} from "./value-providers/config";
|
export {ValueProviderConfig, ValueProviderKind} from "./value-providers/config";
|
||||||
export {getCodeActions} from "./code-actions";
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import {isString} from "@actions/workflow-parser";
|
import {isString} from "@actions/workflow-parser";
|
||||||
|
import {DefinitionType} from "@actions/workflow-parser/templates/schema/definition-type";
|
||||||
|
import {StringDefinition} from "@actions/workflow-parser/templates/schema/string-definition";
|
||||||
import {OPEN_EXPRESSION} from "@actions/workflow-parser/templates/template-constants";
|
import {OPEN_EXPRESSION} from "@actions/workflow-parser/templates/template-constants";
|
||||||
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/index";
|
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/index";
|
||||||
|
|
||||||
export function isPotentiallyExpression(token: TemplateToken): boolean {
|
export function isPotentiallyExpression(token: TemplateToken): boolean {
|
||||||
|
const isAlwaysExpression =
|
||||||
|
token.definition?.definitionType === DefinitionType.String && (token.definition as StringDefinition).isExpression;
|
||||||
const containsExpression = isString(token) && token.value != null && token.value.indexOf(OPEN_EXPRESSION) >= 0;
|
const containsExpression = isString(token) && token.value != null && token.value.indexOf(OPEN_EXPRESSION) >= 0;
|
||||||
// If conditions are always expressions (job-if, step-if, snapshot-if)
|
return isAlwaysExpression || containsExpression;
|
||||||
const definitionKey = token.definition?.key;
|
|
||||||
const isIfCondition = definitionKey === "job-if" || definitionKey === "step-if" || definitionKey === "snapshot-if";
|
|
||||||
return containsExpression || isIfCondition;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,12 @@
|
|||||||
import { isMapping } from "@actions/workflow-parser";
|
import {isMapping} from "@actions/workflow-parser";
|
||||||
import { isActionStep } from "@actions/workflow-parser/model/type-guards";
|
import {isActionStep} from "@actions/workflow-parser/model/type-guards";
|
||||||
import { Step } from "@actions/workflow-parser/model/workflow-template";
|
import {Step} from "@actions/workflow-parser/model/workflow-template";
|
||||||
import { ScalarToken } from "@actions/workflow-parser/templates/tokens/scalar-token";
|
import {ScalarToken} from "@actions/workflow-parser/templates/tokens/scalar-token";
|
||||||
import { TemplateToken } from "@actions/workflow-parser/templates/tokens/template-token";
|
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
||||||
import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver-types";
|
import {Diagnostic, DiagnosticSeverity} from "vscode-languageserver-types";
|
||||||
import { ActionReference, parseActionReference } from "./action";
|
import {parseActionReference} from "./action";
|
||||||
import { mapRange } from "./utils/range";
|
import {mapRange} from "./utils/range";
|
||||||
import { ValidationConfig } from "./validate";
|
import {ValidationConfig} from "./validate";
|
||||||
|
|
||||||
export const DiagnosticCode = {
|
|
||||||
MissingRequiredInputs: "missing-required-inputs"
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export interface MissingInputsDiagnosticData {
|
|
||||||
action: ActionReference;
|
|
||||||
missingInputs: Array<{
|
|
||||||
name: string;
|
|
||||||
default?: string;
|
|
||||||
}>;
|
|
||||||
hasWithKey: boolean;
|
|
||||||
// Indentation of the `with:` key if present, or the step's base indentation
|
|
||||||
withIndent?: number;
|
|
||||||
stepIndent: number;
|
|
||||||
// Position where new content should be inserted
|
|
||||||
insertPosition: { line: number; character: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function validateAction(
|
export async function validateAction(
|
||||||
diagnostics: Diagnostic[],
|
diagnostics: Diagnostic[],
|
||||||
@@ -53,7 +35,7 @@ export async function validateAction(
|
|||||||
|
|
||||||
let withKey: ScalarToken | undefined;
|
let withKey: ScalarToken | undefined;
|
||||||
let withToken: TemplateToken | undefined;
|
let withToken: TemplateToken | undefined;
|
||||||
for (const { key, value } of stepToken) {
|
for (const {key, value} of stepToken) {
|
||||||
if (key.toString() === "with") {
|
if (key.toString() === "with") {
|
||||||
withKey = key;
|
withKey = key;
|
||||||
withToken = value;
|
withToken = value;
|
||||||
@@ -63,7 +45,7 @@ export async function validateAction(
|
|||||||
|
|
||||||
const stepInputs = new Map<string, ScalarToken>();
|
const stepInputs = new Map<string, ScalarToken>();
|
||||||
if (withToken && isMapping(withToken)) {
|
if (withToken && isMapping(withToken)) {
|
||||||
for (const { key } of withToken) {
|
for (const {key} of withToken) {
|
||||||
stepInputs.set(key.toString(), key);
|
stepInputs.set(key.toString(), key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,47 +83,10 @@ export async function validateAction(
|
|||||||
missingRequiredInputs.length === 1
|
missingRequiredInputs.length === 1
|
||||||
? `Missing required input \`${missingRequiredInputs[0][0]}\``
|
? `Missing required input \`${missingRequiredInputs[0][0]}\``
|
||||||
: `Missing required inputs: ${missingRequiredInputs.map(input => `\`${input[0]}\``).join(", ")}`;
|
: `Missing required inputs: ${missingRequiredInputs.map(input => `\`${input[0]}\``).join(", ")}`;
|
||||||
|
|
||||||
const stepIndent = stepToken.range ? stepToken.range.start.column - 1 : 0; // 0-indexed
|
|
||||||
const withIndent = withKey?.range ? withKey.range.start.column - 1 : undefined;
|
|
||||||
|
|
||||||
// Calculate insert position
|
|
||||||
// For withToken, we need to handle empty mappings specially - insert after the with: line
|
|
||||||
let insertPosition: { line: number; character: number };
|
|
||||||
if (withToken?.range) {
|
|
||||||
// Check if with: has any children by comparing start and end lines
|
|
||||||
const hasChildren = stepInputs.size > 0;
|
|
||||||
if (hasChildren) {
|
|
||||||
// Insert after the last child
|
|
||||||
insertPosition = { line: withToken.range.end.line - 1, character: 0 };
|
|
||||||
} else {
|
|
||||||
// Empty with: block - insert on the next line after with:
|
|
||||||
insertPosition = { line: withKey!.range!.end.line, character: 0 };
|
|
||||||
}
|
|
||||||
} else if (stepToken.range) {
|
|
||||||
insertPosition = { line: stepToken.range.end.line - 1, character: 0 };
|
|
||||||
} else {
|
|
||||||
insertPosition = { line: 0, character: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const diagnosticData: MissingInputsDiagnosticData = {
|
|
||||||
action,
|
|
||||||
missingInputs: missingRequiredInputs.map(([name, input]) => ({
|
|
||||||
name,
|
|
||||||
default: input.default
|
|
||||||
})),
|
|
||||||
hasWithKey: withKey !== undefined,
|
|
||||||
withIndent,
|
|
||||||
stepIndent,
|
|
||||||
insertPosition
|
|
||||||
};
|
|
||||||
|
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: DiagnosticSeverity.Error,
|
severity: DiagnosticSeverity.Error,
|
||||||
range: mapRange((withKey || stepToken).range), // Highlight the whole step if we don't have a with key
|
range: mapRange((withKey || stepToken).range), // Highlight the whole step if we don't have a with key
|
||||||
message: message,
|
message: message
|
||||||
code: DiagnosticCode.MissingRequiredInputs,
|
|
||||||
data: diagnosticData
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,28 +249,7 @@ jobs:
|
|||||||
line: 7
|
line: 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
severity: DiagnosticSeverity.Error,
|
severity: DiagnosticSeverity.Error
|
||||||
code: "missing-required-inputs",
|
|
||||||
data: {
|
|
||||||
action: {
|
|
||||||
name: "cache",
|
|
||||||
owner: "actions",
|
|
||||||
ref: "v1"
|
|
||||||
},
|
|
||||||
hasWithKey: true,
|
|
||||||
insertPosition: {
|
|
||||||
character: 0,
|
|
||||||
line: 9
|
|
||||||
},
|
|
||||||
missingInputs: [
|
|
||||||
{
|
|
||||||
default: undefined,
|
|
||||||
name: "path"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
stepIndent: 6,
|
|
||||||
withIndent: 6
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -315,32 +294,7 @@ jobs:
|
|||||||
line: 7
|
line: 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
severity: DiagnosticSeverity.Error,
|
severity: DiagnosticSeverity.Error
|
||||||
code: "missing-required-inputs",
|
|
||||||
data: {
|
|
||||||
action: {
|
|
||||||
name: "cache",
|
|
||||||
owner: "actions",
|
|
||||||
ref: "v1"
|
|
||||||
},
|
|
||||||
hasWithKey: true,
|
|
||||||
insertPosition: {
|
|
||||||
character: 0,
|
|
||||||
line: 9
|
|
||||||
},
|
|
||||||
missingInputs: [
|
|
||||||
{
|
|
||||||
default: undefined,
|
|
||||||
name: "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: undefined,
|
|
||||||
name: "key"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
stepIndent: 6,
|
|
||||||
withIndent: 6
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -369,32 +323,7 @@ jobs:
|
|||||||
line: 6
|
line: 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
severity: DiagnosticSeverity.Error,
|
severity: DiagnosticSeverity.Error
|
||||||
code: "missing-required-inputs",
|
|
||||||
data: {
|
|
||||||
action: {
|
|
||||||
name: "cache",
|
|
||||||
owner: "actions",
|
|
||||||
ref: "v1"
|
|
||||||
},
|
|
||||||
hasWithKey: false,
|
|
||||||
insertPosition: {
|
|
||||||
character: 0,
|
|
||||||
line: 7
|
|
||||||
},
|
|
||||||
missingInputs: [
|
|
||||||
{
|
|
||||||
default: undefined,
|
|
||||||
name: "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: undefined,
|
|
||||||
name: "key"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
stepIndent: 6,
|
|
||||||
withIndent: undefined
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
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 literal text in conditions", () => {
|
|
||||||
describe("job-if", () => {
|
|
||||||
it("errors when literal text mixed with embedded expression", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: push == \${{ github.event_name }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo hi
|
|
||||||
`;
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
|
|
||||||
expect(result).toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
message:
|
|
||||||
"Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?",
|
|
||||||
code: "expression-literal-text-in-condition",
|
|
||||||
severity: DiagnosticSeverity.Error
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows format with only replacement tokens", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: \${{ format('{0}', github.event_name) }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo hi
|
|
||||||
`;
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
|
|
||||||
expect(result).not.toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
code: "expression-literal-text-in-condition"
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows format with only replacement tokens and whitespace", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: \${{ format('{0}{1}', github.event_name, 'test') }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo hi
|
|
||||||
`;
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
|
|
||||||
// Only replacement tokens, no literal text
|
|
||||||
expect(result).not.toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
code: "expression-literal-text-in-condition"
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("errors with literal text and replacement tokens mixed", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: \${{ format('event is {0}', github.event_name) }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo hi
|
|
||||||
`;
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
|
|
||||||
expect(result).toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
message:
|
|
||||||
"Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?",
|
|
||||||
code: "expression-literal-text-in-condition",
|
|
||||||
severity: DiagnosticSeverity.Error
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("errors with escaped left brace followed by replacement token", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: \${{ format('{{{0}', github.event_name) }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo hi
|
|
||||||
`;
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
|
|
||||||
expect(result).toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
message:
|
|
||||||
"Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?",
|
|
||||||
code: "expression-literal-text-in-condition",
|
|
||||||
severity: DiagnosticSeverity.Error
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("step-if", () => {
|
|
||||||
it("errors when literal text mixed with embedded expression", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- if: success == \${{ job.status }}
|
|
||||||
run: echo hi
|
|
||||||
`;
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
|
|
||||||
expect(result).toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
message:
|
|
||||||
"Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?",
|
|
||||||
code: "expression-literal-text-in-condition",
|
|
||||||
severity: DiagnosticSeverity.Error
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows valid expressions", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- if: \${{ success() }}
|
|
||||||
run: echo hi
|
|
||||||
`;
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
|
|
||||||
expect(result).not.toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
code: "expression-literal-text-in-condition"
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("snapshot-if", () => {
|
|
||||||
it("errors when literal text mixed with embedded expression", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest]
|
|
||||||
steps:
|
|
||||||
- run: echo hi
|
|
||||||
snapshot:
|
|
||||||
image-name: my-image
|
|
||||||
if: ubuntu == \${{ matrix.os }}
|
|
||||||
`;
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
|
|
||||||
expect(result).toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
message:
|
|
||||||
"Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?",
|
|
||||||
code: "expression-literal-text-in-condition",
|
|
||||||
severity: DiagnosticSeverity.Error
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("non-if fields", () => {
|
|
||||||
it("does not error for format in run", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo \${{ format('Event is {0}', github.event_name) }}
|
|
||||||
`;
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
|
|
||||||
// Format with literal text is OK outside of if conditions
|
|
||||||
expect(result).not.toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
code: "expression-literal-text-in-condition"
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1505,174 +1505,4 @@ jobs:
|
|||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("if condition context restrictions", () => {
|
|
||||||
describe("job-level if", () => {
|
|
||||||
it("allows github context", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo hello`;
|
|
||||||
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows needs context", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
a:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo hello
|
|
||||||
b:
|
|
||||||
needs: a
|
|
||||||
if: needs.a.result == 'success'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo hello`;
|
|
||||||
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows inputs context", async () => {
|
|
||||||
const input = `
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
environment:
|
|
||||||
type: string
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: inputs.environment == 'prod'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo hello`;
|
|
||||||
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Note: vars and matrix contexts are validated at runtime based on their existence
|
|
||||||
// vars context only exists if organization/repository variables are defined
|
|
||||||
// matrix context only exists if a strategy.matrix is defined
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("step-level if", () => {
|
|
||||||
it("allows steps context", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- id: setup
|
|
||||||
run: echo hello
|
|
||||||
- if: steps.setup.outcome == 'success'
|
|
||||||
run: echo world`;
|
|
||||||
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows job context", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- if: job.status == 'success'
|
|
||||||
run: echo hello`;
|
|
||||||
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows runner context", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- if: runner.os == 'Linux'
|
|
||||||
run: echo hello`;
|
|
||||||
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows env context", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
MY_VAR: value
|
|
||||||
steps:
|
|
||||||
- if: env.MY_VAR == 'value'
|
|
||||||
run: echo hello`;
|
|
||||||
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows matrix context in matrix job", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu, windows]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- if: matrix.os == 'ubuntu'
|
|
||||||
run: echo hello`;
|
|
||||||
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows hashFiles function", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- if: hashFiles('**/*.txt') != ''
|
|
||||||
run: echo hello`;
|
|
||||||
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows all contexts together", async () => {
|
|
||||||
const input = `
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
JOB_VAR: job-value
|
|
||||||
steps:
|
|
||||||
- id: first
|
|
||||||
run: echo hello
|
|
||||||
- if: github.event_name == 'push' && steps.first.outcome == 'success' && job.status == 'success' && runner.os == 'Linux' && env.JOB_VAR == 'job-value'
|
|
||||||
run: echo world`;
|
|
||||||
|
|
||||||
const result = await validate(createDocument("wf.yaml", input));
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
+104
-122
@@ -1,15 +1,16 @@
|
|||||||
import {Lexer, Parser, data} from "@actions/expressions";
|
import {Lexer, Parser} from "@actions/expressions";
|
||||||
import {Expr, FunctionCall, Literal, Logical} from "@actions/expressions/ast";
|
import {Expr} from "@actions/expressions/ast";
|
||||||
import {ParseWorkflowResult, WorkflowTemplate, isBasicExpression, isString} from "@actions/workflow-parser";
|
import {ParseWorkflowResult, WorkflowTemplate, isBasicExpression, isString} from "@actions/workflow-parser";
|
||||||
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
|
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
|
||||||
import {ensureStatusFunction} from "@actions/workflow-parser/model/converter/if-condition";
|
|
||||||
import {splitAllowedContext} from "@actions/workflow-parser/templates/allowed-context";
|
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 {BasicExpressionToken} from "@actions/workflow-parser/templates/tokens/basic-expression-token";
|
||||||
import {StringToken} from "@actions/workflow-parser/templates/tokens/string-token";
|
import {StringToken} from "@actions/workflow-parser/templates/tokens/string-token";
|
||||||
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token";
|
||||||
import {TokenRange} from "@actions/workflow-parser/templates/tokens/token-range";
|
import {TokenRange} from "@actions/workflow-parser/templates/tokens/token-range";
|
||||||
import {File} from "@actions/workflow-parser/workflows/file";
|
import {File} from "@actions/workflow-parser/workflows/file";
|
||||||
import {FileProvider} from "@actions/workflow-parser/workflows/file-provider";
|
import {FileProvider} from "@actions/workflow-parser/workflows/file-provider";
|
||||||
|
import {Scalar} from "yaml";
|
||||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||||
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
|
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
|
||||||
import {ActionMetadata, ActionReference} from "./action";
|
import {ActionMetadata, ActionReference} from "./action";
|
||||||
@@ -105,42 +106,10 @@ async function additionalValidations(
|
|||||||
token,
|
token,
|
||||||
validationToken.definitionInfo?.allowedContext || [],
|
validationToken.definitionInfo?.allowedContext || [],
|
||||||
config?.contextProviderConfig,
|
config?.contextProviderConfig,
|
||||||
getProviderContext(documentUri, template, root, token.range),
|
getProviderContext(documentUri, template, root, token.range)
|
||||||
key?.definition?.key
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// If this is a job-if, step-if, or snapshot-if field (which are strings that should be treated as expressions), validate it
|
validateChomp(diagnostics, token, parent, key, validationDefinition);
|
||||||
const definitionKey = token.definition?.key;
|
|
||||||
if (
|
|
||||||
isString(token) &&
|
|
||||||
token.range &&
|
|
||||||
(definitionKey === "job-if" || definitionKey === "step-if" || definitionKey === "snapshot-if")
|
|
||||||
) {
|
|
||||||
// Convert the string to an expression token for validation
|
|
||||||
const condition = token.value.trim();
|
|
||||||
if (condition) {
|
|
||||||
// Ensure the condition has a status function, wrapping if needed
|
|
||||||
const finalCondition = ensureStatusFunction(condition, token.definitionInfo);
|
|
||||||
|
|
||||||
// Create a BasicExpressionToken for validation
|
|
||||||
const expressionToken = new BasicExpressionToken(
|
|
||||||
token.file,
|
|
||||||
token.range,
|
|
||||||
finalCondition,
|
|
||||||
token.definitionInfo,
|
|
||||||
undefined,
|
|
||||||
token.source
|
|
||||||
);
|
|
||||||
|
|
||||||
await validateExpression(
|
|
||||||
diagnostics,
|
|
||||||
expressionToken,
|
|
||||||
validationToken.definitionInfo?.allowedContext || [],
|
|
||||||
config?.contextProviderConfig,
|
|
||||||
getProviderContext(documentUri, template, root, token.range)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token.definition?.key === "regular-step" && token.range) {
|
if (token.definition?.key === "regular-step" && token.range) {
|
||||||
@@ -214,99 +183,17 @@ function getProviderContext(
|
|||||||
return getWorkflowContext(documentUri, template, path);
|
return getWorkflowContext(documentUri, template, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a format function contains literal text in its format string.
|
|
||||||
* This indicates user confusion about how expressions work.
|
|
||||||
*
|
|
||||||
* Example: format('push == {0}', github.event_name)
|
|
||||||
* The literal text "push == " will always evaluate to truthy.
|
|
||||||
*
|
|
||||||
* @param expr The expression to check
|
|
||||||
* @returns true if the expression is a format() call with literal text
|
|
||||||
*/
|
|
||||||
function hasFormatWithLiteralText(expr: Expr): boolean {
|
|
||||||
// If this is a logical AND expression (from ensureStatusFunction wrapping)
|
|
||||||
// check the right side for the format call
|
|
||||||
if (expr instanceof Logical && expr.operator.lexeme === "&&" && expr.args.length === 2) {
|
|
||||||
return hasFormatWithLiteralText(expr.args[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(expr instanceof FunctionCall)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a format function
|
|
||||||
if (expr.functionName.lexeme.toLowerCase() !== "format") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the first argument is a string literal
|
|
||||||
if (expr.args.length < 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstArg = expr.args[0];
|
|
||||||
if (!(firstArg instanceof Literal) || firstArg.literal.kind !== data.Kind.String) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the format string and trim whitespace
|
|
||||||
const formatString = firstArg.literal.coerceString();
|
|
||||||
const trimmed = formatString.trim();
|
|
||||||
|
|
||||||
// Check if there's literal text (non-replacement tokens) after trimming
|
|
||||||
let inToken = false;
|
|
||||||
for (let i = 0; i < trimmed.length; i++) {
|
|
||||||
if (!inToken && trimmed[i] === "{") {
|
|
||||||
inToken = true;
|
|
||||||
} else if (inToken && trimmed[i] === "}") {
|
|
||||||
inToken = false;
|
|
||||||
} else if (inToken && trimmed[i] >= "0" && trimmed[i] <= "9") {
|
|
||||||
// OK - this is a replacement token like {0}, {1}, etc.
|
|
||||||
} else {
|
|
||||||
// Found literal text
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateExpression(
|
async function validateExpression(
|
||||||
diagnostics: Diagnostic[],
|
diagnostics: Diagnostic[],
|
||||||
token: BasicExpressionToken,
|
token: BasicExpressionToken,
|
||||||
allowedContext: string[],
|
allowedContext: string[],
|
||||||
contextProviderConfig: ContextProviderConfig | undefined,
|
contextProviderConfig: ContextProviderConfig | undefined,
|
||||||
workflowContext: WorkflowContext,
|
workflowContext: WorkflowContext
|
||||||
keyDefinitionKey?: string
|
|
||||||
) {
|
) {
|
||||||
const {namedContexts, functions} = splitAllowedContext(allowedContext);
|
|
||||||
|
|
||||||
// Check for literal text in if condition
|
|
||||||
const definitionKey = keyDefinitionKey || token.definitionInfo?.definition?.key;
|
|
||||||
if (definitionKey === "job-if" || definitionKey === "step-if" || definitionKey === "snapshot-if") {
|
|
||||||
try {
|
|
||||||
const l = new Lexer(token.expression);
|
|
||||||
const lr = l.lex();
|
|
||||||
const p = new Parser(lr.tokens, namedContexts, functions);
|
|
||||||
const expr = p.parse();
|
|
||||||
|
|
||||||
if (hasFormatWithLiteralText(expr)) {
|
|
||||||
diagnostics.push({
|
|
||||||
message:
|
|
||||||
"Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?",
|
|
||||||
range: mapRange(token.range),
|
|
||||||
severity: DiagnosticSeverity.Error,
|
|
||||||
code: "expression-literal-text-in-condition"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore parse errors here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the expression
|
// Validate the expression
|
||||||
for (const expression of token.originalExpressions || [token]) {
|
for (const expression of token.originalExpressions || [token]) {
|
||||||
|
const {namedContexts, functions} = splitAllowedContext(allowedContext);
|
||||||
|
|
||||||
let expr: Expr | undefined;
|
let expr: Expr | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -334,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"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Generated
+37
-500
@@ -406,14 +406,10 @@
|
|||||||
"vscode-languageserver-textdocument": "^1.0.7",
|
"vscode-languageserver-textdocument": "^1.0.7",
|
||||||
"yaml": "^2.1.3"
|
"yaml": "^2.1.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
|
||||||
"actions-languageserver": "bin/actions-languageserver"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.0.3",
|
"@types/jest": "^29.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||||
"@typescript-eslint/parser": "^5.56.0",
|
"@typescript-eslint/parser": "^5.56.0",
|
||||||
"esbuild": "^0.27.1",
|
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
@@ -479,7 +475,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz",
|
||||||
"integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==",
|
"integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-token": "^5.0.0",
|
"@octokit/auth-token": "^5.0.0",
|
||||||
"@octokit/graphql": "^8.2.2",
|
"@octokit/graphql": "^8.2.2",
|
||||||
@@ -1318,7 +1313,6 @@
|
|||||||
"version": "7.20.2",
|
"version": "7.20.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.1.0",
|
"@ampproject/remapping": "^2.1.0",
|
||||||
"@babel/code-frame": "^7.18.6",
|
"@babel/code-frame": "^7.18.6",
|
||||||
@@ -1896,448 +1890,6 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"aix"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-x64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==",
|
|
||||||
"cpu": [
|
|
||||||
"mips64el"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-x64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openharmony-arm64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openharmony"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"sunos"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-x64": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz",
|
||||||
@@ -3242,6 +2794,22 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@lerna/create/node_modules/typescript": {
|
||||||
|
"version": "5.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@lerna/create/node_modules/write-file-atomic": {
|
"node_modules/@lerna/create/node_modules/write-file-atomic": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
|
||||||
@@ -4155,7 +3723,6 @@
|
|||||||
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
|
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-token": "^4.0.0",
|
"@octokit/auth-token": "^4.0.0",
|
||||||
"@octokit/graphql": "^7.1.0",
|
"@octokit/graphql": "^7.1.0",
|
||||||
@@ -4683,7 +4250,6 @@
|
|||||||
"integrity": "sha512-sn1OZmBxUsgxMmR8a8U5QM/Wl+tyqlH//jTqCg8daTAmhAk26L2PFhcqPLlYBhYUJMZJK276qLXlHN3a83o2cg==",
|
"integrity": "sha512-sn1OZmBxUsgxMmR8a8U5QM/Wl+tyqlH//jTqCg8daTAmhAk26L2PFhcqPLlYBhYUJMZJK276qLXlHN3a83o2cg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "5.56.0",
|
"@typescript-eslint/scope-manager": "5.56.0",
|
||||||
"@typescript-eslint/types": "5.56.0",
|
"@typescript-eslint/types": "5.56.0",
|
||||||
@@ -4922,7 +4488,6 @@
|
|||||||
"version": "8.8.1",
|
"version": "8.8.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -5346,7 +4911,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001400",
|
"caniuse-lite": "^1.0.30001400",
|
||||||
"electron-to-chromium": "^1.4.251",
|
"electron-to-chromium": "^1.4.251",
|
||||||
@@ -6367,6 +5931,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/encoding": {
|
||||||
|
"version": "0.1.13",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "^0.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/encoding/node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/end-of-stream": {
|
"node_modules/end-of-stream": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
@@ -6475,48 +6060,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
|
||||||
"version": "0.27.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz",
|
|
||||||
"integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"esbuild": "bin/esbuild"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@esbuild/aix-ppc64": "0.27.1",
|
|
||||||
"@esbuild/android-arm": "0.27.1",
|
|
||||||
"@esbuild/android-arm64": "0.27.1",
|
|
||||||
"@esbuild/android-x64": "0.27.1",
|
|
||||||
"@esbuild/darwin-arm64": "0.27.1",
|
|
||||||
"@esbuild/darwin-x64": "0.27.1",
|
|
||||||
"@esbuild/freebsd-arm64": "0.27.1",
|
|
||||||
"@esbuild/freebsd-x64": "0.27.1",
|
|
||||||
"@esbuild/linux-arm": "0.27.1",
|
|
||||||
"@esbuild/linux-arm64": "0.27.1",
|
|
||||||
"@esbuild/linux-ia32": "0.27.1",
|
|
||||||
"@esbuild/linux-loong64": "0.27.1",
|
|
||||||
"@esbuild/linux-mips64el": "0.27.1",
|
|
||||||
"@esbuild/linux-ppc64": "0.27.1",
|
|
||||||
"@esbuild/linux-riscv64": "0.27.1",
|
|
||||||
"@esbuild/linux-s390x": "0.27.1",
|
|
||||||
"@esbuild/linux-x64": "0.27.1",
|
|
||||||
"@esbuild/netbsd-arm64": "0.27.1",
|
|
||||||
"@esbuild/netbsd-x64": "0.27.1",
|
|
||||||
"@esbuild/openbsd-arm64": "0.27.1",
|
|
||||||
"@esbuild/openbsd-x64": "0.27.1",
|
|
||||||
"@esbuild/openharmony-arm64": "0.27.1",
|
|
||||||
"@esbuild/sunos-x64": "0.27.1",
|
|
||||||
"@esbuild/win32-arm64": "0.27.1",
|
|
||||||
"@esbuild/win32-ia32": "0.27.1",
|
|
||||||
"@esbuild/win32-x64": "0.27.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -6537,7 +6080,6 @@
|
|||||||
"version": "7.32.0",
|
"version": "7.32.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "7.12.11",
|
"@babel/code-frame": "7.12.11",
|
||||||
"@eslint/eslintrc": "^0.4.3",
|
"@eslint/eslintrc": "^0.4.3",
|
||||||
@@ -8408,7 +7950,6 @@
|
|||||||
"version": "29.3.1",
|
"version": "29.3.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "^29.3.1",
|
"@jest/core": "^29.3.1",
|
||||||
"@jest/types": "^29.3.1",
|
"@jest/types": "^29.3.1",
|
||||||
@@ -9483,7 +9024,6 @@
|
|||||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -10304,7 +9844,6 @@
|
|||||||
"version": "2.6.7",
|
"version": "2.6.7",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"whatwg-url": "^5.0.0"
|
"whatwg-url": "^5.0.0"
|
||||||
},
|
},
|
||||||
@@ -10658,7 +10197,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@napi-rs/wasm-runtime": "0.2.4",
|
"@napi-rs/wasm-runtime": "0.2.4",
|
||||||
"@yarnpkg/lockfile": "^1.1.0",
|
"@yarnpkg/lockfile": "^1.1.0",
|
||||||
@@ -11295,7 +10833,6 @@
|
|||||||
"version": "2.8.3",
|
"version": "2.8.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin-prettier.js"
|
"prettier": "bin-prettier.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -194,11 +194,290 @@ jobs:
|
|||||||
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||||
const build = jobs.get(0).value.assertMapping("job");
|
const build = jobs.get(0).value.assertMapping("job");
|
||||||
const ifToken = build.get(1).value;
|
const ifToken = build.get(1).value;
|
||||||
// Without isExpression: true, the value is kept as a string until convertToIfCondition processes it
|
expect(ifToken.toString()).toEqual("${{ github.event_name == 'push' }}");
|
||||||
expect(ifToken.toString()).toEqual("github.event_name == 'push'");
|
|
||||||
|
|
||||||
if (!isString(ifToken)) {
|
if (!isBasicExpression(ifToken)) {
|
||||||
throw new Error("expected if to be a string (will be converted to expression later)");
|
throw new Error("expected if to be a basic expression");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Block scalar chomp style preservation", () => {
|
||||||
|
it("preserves clip chomping (|) for literal block scalar", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
\${{ github.event_name == 'push' }}
|
||||||
|
steps:
|
||||||
|
- run: echo hi`
|
||||||
|
},
|
||||||
|
nullTrace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||||
|
|
||||||
|
const workflowRoot = result.value!.assertMapping("root")!;
|
||||||
|
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||||
|
const build = jobs.get(0).value.assertMapping("job");
|
||||||
|
const ifToken = build.get(1).value;
|
||||||
|
|
||||||
|
if (!isBasicExpression(ifToken)) {
|
||||||
|
throw new Error("expected if to be a basic expression");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||||
|
expect(ifToken.chompStyle).toBe("clip");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves strip chomping (|-) for literal block scalar", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |-
|
||||||
|
\${{ github.event_name == 'push' }}
|
||||||
|
steps:
|
||||||
|
- run: echo hi`
|
||||||
|
},
|
||||||
|
nullTrace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||||
|
|
||||||
|
const workflowRoot = result.value!.assertMapping("root")!;
|
||||||
|
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||||
|
const build = jobs.get(0).value.assertMapping("job");
|
||||||
|
const ifToken = build.get(1).value;
|
||||||
|
|
||||||
|
if (!isBasicExpression(ifToken)) {
|
||||||
|
throw new Error("expected if to be a basic expression");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||||
|
expect(ifToken.chompStyle).toBe("strip");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves keep chomping (|+) for literal block scalar", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |+
|
||||||
|
\${{ github.event_name == 'push' }}
|
||||||
|
steps:
|
||||||
|
- run: echo hi`
|
||||||
|
},
|
||||||
|
nullTrace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||||
|
|
||||||
|
const workflowRoot = result.value!.assertMapping("root")!;
|
||||||
|
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||||
|
const build = jobs.get(0).value.assertMapping("job");
|
||||||
|
const ifToken = build.get(1).value;
|
||||||
|
|
||||||
|
if (!isBasicExpression(ifToken)) {
|
||||||
|
throw new Error("expected if to be a basic expression");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||||
|
expect(ifToken.chompStyle).toBe("keep");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves folded clip (>) chomping", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >
|
||||||
|
\${{ github.event_name == 'push' }}
|
||||||
|
steps:
|
||||||
|
- run: echo hi`
|
||||||
|
},
|
||||||
|
nullTrace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||||
|
|
||||||
|
const workflowRoot = result.value!.assertMapping("root")!;
|
||||||
|
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||||
|
const build = jobs.get(0).value.assertMapping("job");
|
||||||
|
const ifToken = build.get(1).value;
|
||||||
|
|
||||||
|
if (!isBasicExpression(ifToken)) {
|
||||||
|
throw new Error("expected if to be a basic expression");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ifToken.scalarType).toBe("BLOCK_FOLDED");
|
||||||
|
expect(ifToken.chompStyle).toBe("clip");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves folded strip (>-) chomping", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
\${{ github.event_name == 'push' }}
|
||||||
|
steps:
|
||||||
|
- run: echo hi`
|
||||||
|
},
|
||||||
|
nullTrace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||||
|
|
||||||
|
const workflowRoot = result.value!.assertMapping("root")!;
|
||||||
|
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||||
|
const build = jobs.get(0).value.assertMapping("job");
|
||||||
|
const ifToken = build.get(1).value;
|
||||||
|
|
||||||
|
if (!isBasicExpression(ifToken)) {
|
||||||
|
throw new Error("expected if to be a basic expression");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ifToken.scalarType).toBe("BLOCK_FOLDED");
|
||||||
|
expect(ifToken.chompStyle).toBe("strip");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves with explicit indent (|2)", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |2
|
||||||
|
\${{ github.event_name == 'push' }}
|
||||||
|
steps:
|
||||||
|
- run: echo hi`
|
||||||
|
},
|
||||||
|
nullTrace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||||
|
|
||||||
|
const workflowRoot = result.value!.assertMapping("root")!;
|
||||||
|
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||||
|
const build = jobs.get(0).value.assertMapping("job");
|
||||||
|
const ifToken = build.get(1).value;
|
||||||
|
|
||||||
|
if (!isBasicExpression(ifToken)) {
|
||||||
|
throw new Error("expected if to be a basic expression");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||||
|
expect(ifToken.chompStyle).toBe("clip");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves with explicit indent and strip (|-2)", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |-2
|
||||||
|
\${{ github.event_name == 'push' }}
|
||||||
|
steps:
|
||||||
|
- run: echo hi`
|
||||||
|
},
|
||||||
|
nullTrace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||||
|
|
||||||
|
const workflowRoot = result.value!.assertMapping("root")!;
|
||||||
|
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||||
|
const build = jobs.get(0).value.assertMapping("job");
|
||||||
|
const ifToken = build.get(1).value;
|
||||||
|
|
||||||
|
if (!isBasicExpression(ifToken)) {
|
||||||
|
throw new Error("expected if to be a basic expression");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||||
|
expect(ifToken.chompStyle).toBe("strip");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles flow scalars (no chomp info for inline)", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: \${{ github.event_name == 'push' }}
|
||||||
|
steps:
|
||||||
|
- run: echo hi`
|
||||||
|
},
|
||||||
|
nullTrace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||||
|
|
||||||
|
const workflowRoot = result.value!.assertMapping("root")!;
|
||||||
|
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||||
|
const build = jobs.get(0).value.assertMapping("job");
|
||||||
|
const ifToken = build.get(1).value;
|
||||||
|
|
||||||
|
if (!isBasicExpression(ifToken)) {
|
||||||
|
throw new Error("expected if to be a basic expression");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ifToken.scalarType).toBeUndefined();
|
||||||
|
expect(ifToken.chompStyle).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles job-if without ${{ }} (isExpression case)", () => {
|
||||||
|
const result = parseWorkflow(
|
||||||
|
{
|
||||||
|
name: "test.yaml",
|
||||||
|
content: `on: push
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.event_name == 'push'
|
||||||
|
steps:
|
||||||
|
- run: echo hi`
|
||||||
|
},
|
||||||
|
nullTrace
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||||
|
|
||||||
|
const workflowRoot = result.value!.assertMapping("root")!;
|
||||||
|
const jobs = workflowRoot.get(1).value.assertMapping("jobs");
|
||||||
|
const build = jobs.get(0).value.assertMapping("job");
|
||||||
|
const ifToken = build.get(1).value;
|
||||||
|
|
||||||
|
if (!isBasicExpression(ifToken)) {
|
||||||
|
throw new Error("expected if to be a basic expression");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ifToken.scalarType).toBe("BLOCK_LITERAL");
|
||||||
|
expect(ifToken.chompStyle).toBe("clip");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
{
|
{
|
||||||
id: "build",
|
id: "build",
|
||||||
if: {
|
if: {
|
||||||
expr: "success() && (true)",
|
expr: "success()",
|
||||||
type: 3
|
type: 3
|
||||||
},
|
},
|
||||||
name: "build",
|
name: "build",
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
{
|
{
|
||||||
id: "deploy",
|
id: "deploy",
|
||||||
if: {
|
if: {
|
||||||
expr: "success() && (true)",
|
expr: "success()",
|
||||||
type: 3
|
type: 3
|
||||||
},
|
},
|
||||||
name: "deploy",
|
name: "deploy",
|
||||||
@@ -382,200 +382,4 @@ jobs:
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("if condition context validation", () => {
|
|
||||||
it("validates job-level if with allowed contexts", async () => {
|
|
||||||
const result = parseWorkflow(
|
|
||||||
{
|
|
||||||
name: "wf.yaml",
|
|
||||||
content: `on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: github.event_name == 'push' && needs.test.result == 'success'
|
|
||||||
needs: test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest`
|
|
||||||
},
|
|
||||||
nullTrace
|
|
||||||
);
|
|
||||||
|
|
||||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
|
||||||
errorPolicy: ErrorPolicy.TryConversion
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should convert successfully - github and needs are allowed in job-level if
|
|
||||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
|
||||||
expect(template.jobs).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("validates job-level if rejects disallowed contexts", async () => {
|
|
||||||
const result = parseWorkflow(
|
|
||||||
{
|
|
||||||
name: "wf.yaml",
|
|
||||||
content: `on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: steps.test.outcome == 'success'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- id: test
|
|
||||||
run: echo hello`
|
|
||||||
},
|
|
||||||
nullTrace
|
|
||||||
);
|
|
||||||
|
|
||||||
await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
|
||||||
errorPolicy: ErrorPolicy.TryConversion
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should have error - steps context not allowed in job-level if
|
|
||||||
const errors = result.context.errors.getErrors();
|
|
||||||
expect(errors.length).toBeGreaterThan(0);
|
|
||||||
const errorMessages = errors.map(e => e.message).join(" ");
|
|
||||||
expect(errorMessages.toLowerCase()).toMatch(/steps|context/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("validates step-level if allows all contexts", async () => {
|
|
||||||
const result = parseWorkflow(
|
|
||||||
{
|
|
||||||
name: "wf.yaml",
|
|
||||||
content: `on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- id: first
|
|
||||||
run: echo hello
|
|
||||||
- if: steps.first.outcome == 'success' && job.status == 'success'
|
|
||||||
run: echo world`
|
|
||||||
},
|
|
||||||
nullTrace
|
|
||||||
);
|
|
||||||
|
|
||||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
|
||||||
errorPolicy: ErrorPolicy.TryConversion
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should convert successfully - steps and job contexts allowed in step-level if
|
|
||||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
|
||||||
expect(template.jobs).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles case-insensitive status functions in if conditions", async () => {
|
|
||||||
const result = parseWorkflow(
|
|
||||||
{
|
|
||||||
name: "wf.yaml",
|
|
||||||
content: `on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- if: Success()
|
|
||||||
run: echo "uppercase Success"
|
|
||||||
- if: FAILURE()
|
|
||||||
run: echo "uppercase FAILURE"
|
|
||||||
- if: Cancelled() || Always()
|
|
||||||
run: echo "mixed case"`
|
|
||||||
},
|
|
||||||
nullTrace
|
|
||||||
);
|
|
||||||
|
|
||||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
|
||||||
errorPolicy: ErrorPolicy.TryConversion
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should convert successfully - status functions are case-insensitive
|
|
||||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
|
||||||
expect(template.jobs).toHaveLength(1);
|
|
||||||
|
|
||||||
// Verify the conditions are preserved without wrapping in success() &&
|
|
||||||
const job = template.jobs[0];
|
|
||||||
expect(job.type).toBe("job");
|
|
||||||
if (job.type === "job") {
|
|
||||||
expect(job.steps[0].if?.expression).toBe("Success()");
|
|
||||||
expect(job.steps[1].if?.expression).toBe("FAILURE()");
|
|
||||||
expect(job.steps[2].if?.expression).toBe("Cancelled() || Always()");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles empty if condition", async () => {
|
|
||||||
const result = parseWorkflow(
|
|
||||||
{
|
|
||||||
name: "wf.yaml",
|
|
||||||
content: `on: push
|
|
||||||
jobs:
|
|
||||||
job1:
|
|
||||||
if: ""
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo hello
|
|
||||||
job2:
|
|
||||||
if: ''
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- if: ""
|
|
||||||
run: echo world
|
|
||||||
- if: ''
|
|
||||||
run: echo test`
|
|
||||||
},
|
|
||||||
nullTrace
|
|
||||||
);
|
|
||||||
|
|
||||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
|
||||||
errorPolicy: ErrorPolicy.TryConversion
|
|
||||||
});
|
|
||||||
|
|
||||||
// Empty conditions should default to success()
|
|
||||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
|
||||||
expect(template.jobs).toHaveLength(2);
|
|
||||||
|
|
||||||
const job1 = template.jobs[0];
|
|
||||||
expect(job1.if?.expression).toBe("success()");
|
|
||||||
|
|
||||||
const job2 = template.jobs[1];
|
|
||||||
expect(job2.if?.expression).toBe("success()");
|
|
||||||
|
|
||||||
if (job2.type === "job") {
|
|
||||||
expect(job2.steps[0].if?.expression).toBe("success()");
|
|
||||||
expect(job2.steps[1].if?.expression).toBe("success()");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles status functions with property access", async () => {
|
|
||||||
const result = parseWorkflow(
|
|
||||||
{
|
|
||||||
name: "wf.yaml",
|
|
||||||
content: `on: push
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- if: success().outputs.result
|
|
||||||
run: echo "success with property"
|
|
||||||
- if: failure().outputs.value
|
|
||||||
run: echo "failure with property"
|
|
||||||
- if: always() && steps.test.outcome
|
|
||||||
run: echo "always with &&"`
|
|
||||||
},
|
|
||||||
nullTrace
|
|
||||||
);
|
|
||||||
|
|
||||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
|
||||||
errorPolicy: ErrorPolicy.TryConversion
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should not wrap - status functions are present even with property access
|
|
||||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
|
||||||
expect(template.jobs).toHaveLength(1);
|
|
||||||
|
|
||||||
const job = template.jobs[0];
|
|
||||||
expect(job.type).toBe("job");
|
|
||||||
if (job.type === "job") {
|
|
||||||
expect(job.steps[0].if?.expression).toBe("success().outputs.result");
|
|
||||||
expect(job.steps[1].if?.expression).toBe("failure().outputs.value");
|
|
||||||
expect(job.steps[2].if?.expression).toBe("always() && steps.test.outcome");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
import {Lexer, Parser} from "@actions/expressions";
|
|
||||||
import {Binary, Expr, FunctionCall, Grouping, IndexAccess, Logical, Unary} from "@actions/expressions/ast";
|
|
||||||
import {DefinitionInfo} from "../../templates/schema/definition-info";
|
|
||||||
import {splitAllowedContext} from "../../templates/allowed-context";
|
|
||||||
import {TemplateContext} from "../../templates/template-context";
|
|
||||||
import {BasicExpressionToken, ExpressionToken, TemplateToken} from "../../templates/tokens";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures a condition expression contains a status function call.
|
|
||||||
* If the condition doesn't contain success(), failure(), cancelled(), or always(),
|
|
||||||
* wraps it in `success() && (condition)`.
|
|
||||||
*
|
|
||||||
* Parses the expression to accurately detect status functions, avoiding false positives
|
|
||||||
* from string literals or property access. If parsing fails (e.g., partially typed expression),
|
|
||||||
* returns the original condition unchanged to allow validation to report the actual error.
|
|
||||||
*
|
|
||||||
* @param condition The condition expression to check
|
|
||||||
* @param definitionInfo Schema definition containing allowed contexts for parsing
|
|
||||||
* @returns The condition with status function guaranteed, or original on parse error
|
|
||||||
*/
|
|
||||||
export function ensureStatusFunction(condition: string, definitionInfo: DefinitionInfo | undefined): string {
|
|
||||||
const allowedContext = definitionInfo?.allowedContext || [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const {namedContexts, functions} = splitAllowedContext(allowedContext);
|
|
||||||
const lexer = new Lexer(condition);
|
|
||||||
const result = lexer.lex();
|
|
||||||
const parser = new Parser(result.tokens, namedContexts, functions);
|
|
||||||
const tree = parser.parse();
|
|
||||||
|
|
||||||
// Check if tree contains status function
|
|
||||||
if (walkTreeToFindStatusFunctionCalls(tree)) {
|
|
||||||
return condition; // Already has status function
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap it
|
|
||||||
return `success() && (${condition})`;
|
|
||||||
} catch {
|
|
||||||
// Parse error - return original and let validation report the actual error
|
|
||||||
// This is important for hover/autocomplete on partially-typed expressions
|
|
||||||
return condition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an if condition token to a BasicExpressionToken.
|
|
||||||
* Treats the value as a string and parses it as an expression.
|
|
||||||
* Wraps the condition in success() && (...) if it doesn't already contain a status function.
|
|
||||||
* This allows both 'if: success()' and 'if: ${{ success() }}' to work correctly.
|
|
||||||
*
|
|
||||||
* Reads the allowed context directly from the schema definition attached to the token,
|
|
||||||
* ensuring consistency with the schema.
|
|
||||||
*
|
|
||||||
* @param context The template context for error reporting
|
|
||||||
* @param token The token containing the if condition
|
|
||||||
* @returns A BasicExpressionToken with the processed condition, or undefined on error
|
|
||||||
*/
|
|
||||||
export function convertToIfCondition(context: TemplateContext, token: TemplateToken): BasicExpressionToken | undefined {
|
|
||||||
const scalar = token.assertScalar("if condition");
|
|
||||||
|
|
||||||
// Get allowed context from the schema definition attached to the token
|
|
||||||
const allowedContext = token.definitionInfo?.allowedContext || [];
|
|
||||||
|
|
||||||
// If it's already an expression, use its value
|
|
||||||
let condition: string;
|
|
||||||
let source: string | undefined;
|
|
||||||
|
|
||||||
if (scalar instanceof BasicExpressionToken) {
|
|
||||||
condition = scalar.expression;
|
|
||||||
source = scalar.source;
|
|
||||||
} else {
|
|
||||||
// Otherwise, treat it as a string
|
|
||||||
const stringToken = scalar.assertString("if condition");
|
|
||||||
condition = stringToken.value.trim();
|
|
||||||
source = stringToken.source;
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalCondition: string;
|
|
||||||
if (!condition) {
|
|
||||||
// Empty condition defaults to success()
|
|
||||||
finalCondition = "success()";
|
|
||||||
} else {
|
|
||||||
// Ensure the condition has a status function, wrapping if needed
|
|
||||||
finalCondition = ensureStatusFunction(condition, token.definitionInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the expression before creating the token
|
|
||||||
try {
|
|
||||||
ExpressionToken.validateExpression(finalCondition, allowedContext);
|
|
||||||
} catch (err) {
|
|
||||||
context.error(token, err as Error);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a BasicExpressionToken with the final condition
|
|
||||||
return new BasicExpressionToken(token.file, token.range, finalCondition, token.definitionInfo, undefined, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Walks an expression AST to find status function calls (success, failure, cancelled, always).
|
|
||||||
* Recursively checks all nodes including function arguments and logical/binary operations.
|
|
||||||
*/
|
|
||||||
function walkTreeToFindStatusFunctionCalls(tree: Expr | undefined): boolean {
|
|
||||||
if (!tree) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tree instanceof FunctionCall) {
|
|
||||||
const funcName = tree.functionName.lexeme.toLowerCase();
|
|
||||||
if (funcName === "success" || funcName === "failure" || funcName === "cancelled" || funcName === "always") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Check arguments recursively
|
|
||||||
return tree.args.some(arg => walkTreeToFindStatusFunctionCalls(arg));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tree instanceof Binary) {
|
|
||||||
return walkTreeToFindStatusFunctionCalls(tree.left) || walkTreeToFindStatusFunctionCalls(tree.right);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tree instanceof Unary) {
|
|
||||||
return walkTreeToFindStatusFunctionCalls(tree.expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tree instanceof Logical) {
|
|
||||||
return tree.args.some(arg => walkTreeToFindStatusFunctionCalls(arg));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tree instanceof Grouping) {
|
|
||||||
return walkTreeToFindStatusFunctionCalls(tree.group);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tree instanceof IndexAccess) {
|
|
||||||
return walkTreeToFindStatusFunctionCalls(tree.expr) || walkTreeToFindStatusFunctionCalls(tree.index);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import {TemplateContext} from "../../templates/template-context";
|
|||||||
import {BasicExpressionToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "../../templates/tokens";
|
import {BasicExpressionToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "../../templates/tokens";
|
||||||
import {isSequence, isString} from "../../templates/tokens/type-guards";
|
import {isSequence, isString} from "../../templates/tokens/type-guards";
|
||||||
import {Step, WorkflowJob} from "../workflow-template";
|
import {Step, WorkflowJob} from "../workflow-template";
|
||||||
import {convertToIfCondition} from "./if-condition";
|
|
||||||
import {convertConcurrency} from "./concurrency";
|
import {convertConcurrency} from "./concurrency";
|
||||||
import {convertToJobContainer, convertToJobServices} from "./container";
|
import {convertToJobContainer, convertToJobServices} from "./container";
|
||||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||||
@@ -21,7 +20,6 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token:
|
|||||||
container,
|
container,
|
||||||
env,
|
env,
|
||||||
environment,
|
environment,
|
||||||
ifCondition,
|
|
||||||
name,
|
name,
|
||||||
outputs,
|
outputs,
|
||||||
runsOn,
|
runsOn,
|
||||||
@@ -61,10 +59,6 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token:
|
|||||||
environment = item.value;
|
environment = item.value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "if":
|
|
||||||
ifCondition = convertToIfCondition(context, item.value);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "name":
|
case "name":
|
||||||
name = item.value.assertScalar("job name");
|
name = item.value.assertScalar("job name");
|
||||||
break;
|
break;
|
||||||
@@ -140,7 +134,7 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token:
|
|||||||
id: jobKey,
|
id: jobKey,
|
||||||
name: jobName(name, jobKey),
|
name: jobName(name, jobKey),
|
||||||
needs: needs || [],
|
needs: needs || [],
|
||||||
if: ifCondition || new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
||||||
ref: workflowJobRef,
|
ref: workflowJobRef,
|
||||||
"input-definitions": undefined,
|
"input-definitions": undefined,
|
||||||
"input-values": workflowJobInputs,
|
"input-values": workflowJobInputs,
|
||||||
@@ -157,7 +151,7 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token:
|
|||||||
id: jobKey,
|
id: jobKey,
|
||||||
name: jobName(name, jobKey),
|
name: jobName(name, jobKey),
|
||||||
needs,
|
needs,
|
||||||
if: ifCondition || new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
||||||
env,
|
env,
|
||||||
concurrency,
|
concurrency,
|
||||||
environment,
|
environment,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {TemplateContext} from "../../templates/template-context";
|
|||||||
import {BasicExpressionToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "../../templates/tokens";
|
import {BasicExpressionToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "../../templates/tokens";
|
||||||
import {isSequence} from "../../templates/tokens/type-guards";
|
import {isSequence} from "../../templates/tokens/type-guards";
|
||||||
import {isActionStep} from "../type-guards";
|
import {isActionStep} from "../type-guards";
|
||||||
import {convertToIfCondition} from "./if-condition";
|
|
||||||
import {ActionStep, Step} from "../workflow-template";
|
import {ActionStep, Step} from "../workflow-template";
|
||||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||||
import {IdBuilder} from "./id-builder";
|
import {IdBuilder} from "./id-builder";
|
||||||
@@ -53,7 +52,7 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
|
|||||||
let uses: StringToken | undefined;
|
let uses: StringToken | undefined;
|
||||||
let continueOnError: boolean | ScalarToken | undefined;
|
let continueOnError: boolean | ScalarToken | undefined;
|
||||||
let env: MappingToken | undefined;
|
let env: MappingToken | undefined;
|
||||||
let ifCondition: BasicExpressionToken | undefined;
|
const ifCondition = new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined);
|
||||||
for (const item of mapping) {
|
for (const item of mapping) {
|
||||||
const key = item.key.assertString("steps item key");
|
const key = item.key.assertString("steps item key");
|
||||||
switch (key.value) {
|
switch (key.value) {
|
||||||
@@ -78,9 +77,6 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
|
|||||||
case "env":
|
case "env":
|
||||||
env = item.value.assertMapping("step env");
|
env = item.value.assertMapping("step env");
|
||||||
break;
|
break;
|
||||||
case "if":
|
|
||||||
ifCondition = convertToIfCondition(context, item.value);
|
|
||||||
break;
|
|
||||||
case "continue-on-error":
|
case "continue-on-error":
|
||||||
if (!item.value.isExpression) {
|
if (!item.value.isExpression) {
|
||||||
continueOnError = item.value.assertBoolean("steps item continue-on-error").value;
|
continueOnError = item.value.assertBoolean("steps item continue-on-error").value;
|
||||||
@@ -94,7 +90,7 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
|
|||||||
return {
|
return {
|
||||||
id: id?.value || "",
|
id: id?.value || "",
|
||||||
name,
|
name,
|
||||||
if: ifCondition || new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
if: ifCondition,
|
||||||
"continue-on-error": continueOnError,
|
"continue-on-error": continueOnError,
|
||||||
env,
|
env,
|
||||||
run
|
run
|
||||||
@@ -105,7 +101,7 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
|
|||||||
return {
|
return {
|
||||||
id: id?.value || "",
|
id: id?.value || "",
|
||||||
name,
|
name,
|
||||||
if: ifCondition || new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
if: ifCondition,
|
||||||
"continue-on-error": continueOnError,
|
"continue-on-error": continueOnError,
|
||||||
env,
|
env,
|
||||||
uses
|
uses
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// template-reader *just* does schema validation
|
// template-reader *just* does schema validation
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
|
||||||
|
import {Scalar} from "yaml";
|
||||||
import {ObjectReader} from "./object-reader";
|
import {ObjectReader} from "./object-reader";
|
||||||
import {TemplateSchema} from "./schema";
|
import {TemplateSchema} from "./schema";
|
||||||
import {DefinitionInfo} from "./schema/definition-info";
|
import {DefinitionInfo} from "./schema/definition-info";
|
||||||
@@ -8,6 +9,7 @@ import {DefinitionType} from "./schema/definition-type";
|
|||||||
import {MappingDefinition} from "./schema/mapping-definition";
|
import {MappingDefinition} from "./schema/mapping-definition";
|
||||||
import {ScalarDefinition} from "./schema/scalar-definition";
|
import {ScalarDefinition} from "./schema/scalar-definition";
|
||||||
import {SequenceDefinition} from "./schema/sequence-definition";
|
import {SequenceDefinition} from "./schema/sequence-definition";
|
||||||
|
import {StringDefinition} from "./schema/string-definition";
|
||||||
import {ANY, CLOSE_EXPRESSION, INSERT_DIRECTIVE, OPEN_EXPRESSION} from "./template-constants";
|
import {ANY, CLOSE_EXPRESSION, INSERT_DIRECTIVE, OPEN_EXPRESSION} from "./template-constants";
|
||||||
import {TemplateContext} from "./template-context";
|
import {TemplateContext} from "./template-context";
|
||||||
import {
|
import {
|
||||||
@@ -455,7 +457,27 @@ class TemplateReader {
|
|||||||
|
|
||||||
let startExpression: number = raw.indexOf(OPEN_EXPRESSION);
|
let startExpression: number = raw.indexOf(OPEN_EXPRESSION);
|
||||||
if (startExpression < 0) {
|
if (startExpression < 0) {
|
||||||
// Doesn't contain "{{"
|
// Doesn't contain "${{"
|
||||||
|
// Check if value should still be evaluated as an expression
|
||||||
|
if (definitionInfo.definition instanceof StringDefinition && definitionInfo.definition.isExpression) {
|
||||||
|
// For isExpression fields (e.g., 'if' conditions), parse token.value (not raw).
|
||||||
|
// Example YAML:
|
||||||
|
// if: |
|
||||||
|
// github.event_name == 'push'
|
||||||
|
// token.source/raw = "|\n github.event_name == 'push'\n" (includes block scalar header)
|
||||||
|
// token.value = "github.event_name == 'push'\n" (clean expression content)
|
||||||
|
// We need token.value because the '|' would interfere with expression parsing.
|
||||||
|
const expression = this.parseIntoExpressionToken(
|
||||||
|
token.range!,
|
||||||
|
token.value,
|
||||||
|
allowedContext,
|
||||||
|
token,
|
||||||
|
definitionInfo
|
||||||
|
);
|
||||||
|
if (expression) {
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
}
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,13 +620,17 @@ class TemplateReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blockScalarInfo = parseBlockScalarInfo(token);
|
||||||
return new BasicExpressionToken(
|
return new BasicExpressionToken(
|
||||||
this._fileId,
|
this._fileId,
|
||||||
token.range,
|
token.range,
|
||||||
`format('${format.join("")}'${args.join("")})`,
|
`format('${format.join("")}'${args.join("")})`,
|
||||||
definitionInfo,
|
definitionInfo,
|
||||||
expressionTokens,
|
expressionTokens,
|
||||||
raw
|
raw,
|
||||||
|
undefined,
|
||||||
|
blockScalarInfo.scalarType,
|
||||||
|
blockScalarInfo.chompStyle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,6 +704,7 @@ class TemplateReader {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Return the expression
|
// Return the expression
|
||||||
|
const blockScalarInfo = parseBlockScalarInfo(token);
|
||||||
return <ParseExpressionResult>{
|
return <ParseExpressionResult>{
|
||||||
expression: new BasicExpressionToken(
|
expression: new BasicExpressionToken(
|
||||||
this._fileId,
|
this._fileId,
|
||||||
@@ -686,7 +713,9 @@ class TemplateReader {
|
|||||||
definitionInfo,
|
definitionInfo,
|
||||||
undefined,
|
undefined,
|
||||||
token.source,
|
token.source,
|
||||||
expressionRange
|
expressionRange,
|
||||||
|
blockScalarInfo.scalarType,
|
||||||
|
blockScalarInfo.chompStyle
|
||||||
),
|
),
|
||||||
error: undefined
|
error: undefined
|
||||||
};
|
};
|
||||||
@@ -793,3 +822,53 @@ interface MatchDirectiveResult {
|
|||||||
parameters: string[];
|
parameters: string[];
|
||||||
error: Error | undefined;
|
error: Error | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BlockScalarInfo {
|
||||||
|
scalarType: Scalar.Type | undefined;
|
||||||
|
chompStyle: "clip" | "strip" | "keep" | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the block scalar info from the StringToken
|
||||||
|
* @param token The StringToken that may contain block scalar information
|
||||||
|
* @returns The scalar type and chomp style
|
||||||
|
*/
|
||||||
|
function parseBlockScalarInfo(token: StringToken): BlockScalarInfo {
|
||||||
|
const scalarType = token.scalarType;
|
||||||
|
|
||||||
|
// Only block scalars have chomp styles
|
||||||
|
if (scalarType !== Scalar.BLOCK_LITERAL && scalarType !== Scalar.BLOCK_FOLDED) {
|
||||||
|
return {scalarType: undefined, chompStyle: undefined};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse chomp style from the block scalar header
|
||||||
|
// Look for block scalar indicators at the start: | or >
|
||||||
|
// Followed by optional chomp indicator (-, +) and/or explicit indent (digit)
|
||||||
|
// Examples: |, |-, |+, |2, |-2, |+2, >, >-, >+, >2, >-2, >+2
|
||||||
|
const header = token.blockScalarHeader;
|
||||||
|
if (!header) {
|
||||||
|
// If there's no header, assume clip (default)
|
||||||
|
return {scalarType, chompStyle: "clip"};
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockScalarMatch = header.match(/^(\||>)([-+])?(\d)?/);
|
||||||
|
|
||||||
|
if (!blockScalarMatch) {
|
||||||
|
// Assume clip if we can't parse the indicator
|
||||||
|
return {scalarType, chompStyle: "clip"};
|
||||||
|
}
|
||||||
|
|
||||||
|
const chompIndicator = blockScalarMatch[2];
|
||||||
|
|
||||||
|
let chompStyle: "clip" | "strip" | "keep";
|
||||||
|
if (chompIndicator === "-") {
|
||||||
|
chompStyle = "strip";
|
||||||
|
} else if (chompIndicator === "+") {
|
||||||
|
chompStyle = "keep";
|
||||||
|
} else {
|
||||||
|
// No chomp indicator means clip (default)
|
||||||
|
chompStyle = "clip";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {scalarType, chompStyle};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {Scalar} from "yaml";
|
||||||
import {DefinitionInfo} from "../schema/definition-info";
|
import {DefinitionInfo} from "../schema/definition-info";
|
||||||
|
|
||||||
import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "../template-constants";
|
import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "../template-constants";
|
||||||
@@ -23,6 +24,16 @@ export class BasicExpressionToken extends ExpressionToken {
|
|||||||
*/
|
*/
|
||||||
public readonly expressionRange: TokenRange | undefined;
|
public readonly expressionRange: TokenRange | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The YAML scalar type (e.g., BLOCK_LITERAL, BLOCK_FOLDED, PLAIN, etc.) if the expression was parsed from a block scalar.
|
||||||
|
*/
|
||||||
|
public readonly scalarType: Scalar.Type | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The chomp style of the block scalar: 'clip' (default, keeps one newline), 'strip' (removes all), or 'keep' (keeps all).
|
||||||
|
*/
|
||||||
|
public readonly chompStyle: "clip" | "strip" | "keep" | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param originalExpressions If the basic expression was transformed from individual expressions, these will be the original ones
|
* @param originalExpressions If the basic expression was transformed from individual expressions, these will be the original ones
|
||||||
*/
|
*/
|
||||||
@@ -33,13 +44,17 @@ export class BasicExpressionToken extends ExpressionToken {
|
|||||||
definitionInfo: DefinitionInfo | undefined,
|
definitionInfo: DefinitionInfo | undefined,
|
||||||
originalExpressions: BasicExpressionToken[] | undefined,
|
originalExpressions: BasicExpressionToken[] | undefined,
|
||||||
source: string | undefined,
|
source: string | undefined,
|
||||||
expressionRange?: TokenRange | undefined
|
expressionRange?: TokenRange | undefined,
|
||||||
|
scalarType?: Scalar.Type | undefined,
|
||||||
|
chompStyle?: "clip" | "strip" | "keep" | undefined
|
||||||
) {
|
) {
|
||||||
super(TokenType.BasicExpression, file, range, undefined, definitionInfo);
|
super(TokenType.BasicExpression, file, range, undefined, definitionInfo);
|
||||||
this.expr = expression;
|
this.expr = expression;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.originalExpressions = originalExpressions;
|
this.originalExpressions = originalExpressions;
|
||||||
this.expressionRange = expressionRange;
|
this.expressionRange = expressionRange;
|
||||||
|
this.scalarType = scalarType;
|
||||||
|
this.chompStyle = chompStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get expression(): string {
|
public get expression(): string {
|
||||||
@@ -55,7 +70,9 @@ export class BasicExpressionToken extends ExpressionToken {
|
|||||||
this.definitionInfo,
|
this.definitionInfo,
|
||||||
this.originalExpressions,
|
this.originalExpressions,
|
||||||
this.source,
|
this.source,
|
||||||
this.expressionRange
|
this.expressionRange,
|
||||||
|
this.scalarType,
|
||||||
|
this.chompStyle
|
||||||
)
|
)
|
||||||
: new BasicExpressionToken(
|
: new BasicExpressionToken(
|
||||||
this.file,
|
this.file,
|
||||||
@@ -64,7 +81,9 @@ export class BasicExpressionToken extends ExpressionToken {
|
|||||||
this.definitionInfo,
|
this.definitionInfo,
|
||||||
this.originalExpressions,
|
this.originalExpressions,
|
||||||
this.source,
|
this.source,
|
||||||
this.expressionRange
|
this.expressionRange,
|
||||||
|
this.scalarType,
|
||||||
|
this.chompStyle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {Scalar} from "yaml";
|
||||||
import {LiteralToken, TemplateToken} from ".";
|
import {LiteralToken, TemplateToken} from ".";
|
||||||
import {DefinitionInfo} from "../schema/definition-info";
|
import {DefinitionInfo} from "../schema/definition-info";
|
||||||
import {TokenRange} from "./token-range";
|
import {TokenRange} from "./token-range";
|
||||||
@@ -6,23 +7,45 @@ import {TokenType} from "./types";
|
|||||||
export class StringToken extends LiteralToken {
|
export class StringToken extends LiteralToken {
|
||||||
public readonly value: string;
|
public readonly value: string;
|
||||||
public readonly source: string | undefined;
|
public readonly source: string | undefined;
|
||||||
|
public readonly scalarType: Scalar.Type | undefined;
|
||||||
|
public readonly blockScalarHeader: string | undefined;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
file: number | undefined,
|
file: number | undefined,
|
||||||
range: TokenRange | undefined,
|
range: TokenRange | undefined,
|
||||||
value: string,
|
value: string,
|
||||||
definitionInfo: DefinitionInfo | undefined,
|
definitionInfo: DefinitionInfo | undefined,
|
||||||
source?: string
|
source?: string,
|
||||||
|
scalarType?: Scalar.Type,
|
||||||
|
blockScalarHeader?: string
|
||||||
) {
|
) {
|
||||||
super(TokenType.String, file, range, definitionInfo);
|
super(TokenType.String, file, range, definitionInfo);
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
|
this.scalarType = scalarType;
|
||||||
|
this.blockScalarHeader = blockScalarHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override clone(omitSource?: boolean): TemplateToken {
|
public override clone(omitSource?: boolean): TemplateToken {
|
||||||
return omitSource
|
return omitSource
|
||||||
? new StringToken(undefined, undefined, this.value, this.definitionInfo, this.source)
|
? new StringToken(
|
||||||
: new StringToken(this.file, this.range, this.value, this.definitionInfo, this.source);
|
undefined,
|
||||||
|
undefined,
|
||||||
|
this.value,
|
||||||
|
this.definitionInfo,
|
||||||
|
this.source,
|
||||||
|
this.scalarType,
|
||||||
|
this.blockScalarHeader
|
||||||
|
)
|
||||||
|
: new StringToken(
|
||||||
|
this.file,
|
||||||
|
this.range,
|
||||||
|
this.value,
|
||||||
|
this.definitionInfo,
|
||||||
|
this.source,
|
||||||
|
this.scalarType,
|
||||||
|
this.blockScalarHeader
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override toString(): string {
|
public override toString(): string {
|
||||||
|
|||||||
@@ -1842,7 +1842,9 @@
|
|||||||
"cancelled(0,0)",
|
"cancelled(0,0)",
|
||||||
"success(0,MAX)"
|
"success(0,MAX)"
|
||||||
],
|
],
|
||||||
"string": {}
|
"string": {
|
||||||
|
"is-expression": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"job-if-result": {
|
"job-if-result": {
|
||||||
"context": [
|
"context": [
|
||||||
@@ -1947,7 +1949,9 @@
|
|||||||
"matrix"
|
"matrix"
|
||||||
],
|
],
|
||||||
"description": "Use the if conditional to prevent a snapshot from being taken unless a condition is met. Any supported context and expression can be used to create a conditional. Expressions in an `if` conditional do not require the bracketed expression syntax. When you use expressions in an `if` conditional, you may omit the expression syntax because GitHub automatically evaluates the `if` conditional as an expression.",
|
"description": "Use the if conditional to prevent a snapshot from being taken unless a condition is met. Any supported context and expression can be used to create a conditional. Expressions in an `if` conditional do not require the bracketed expression syntax. When you use expressions in an `if` conditional, you may omit the expression syntax because GitHub automatically evaluates the `if` conditional as an expression.",
|
||||||
"string": {}
|
"string": {
|
||||||
|
"is-expression": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"runs-on": {
|
"runs-on": {
|
||||||
"description": "Use `runs-on` to define the type of machine to run the job on.\n* The destination machine can be either a GitHub-hosted runner, larger runner, or a self-hosted runner.\n* You can target runners based on the labels assigned to them, or their group membership, or a combination of these.\n* You can provide `runs-on` as a single string or as an array of strings.\n* If you specify an array of strings, your workflow will execute on any runner that matches all of the specified `runs-on` values.\n* If you would like to run your workflow on multiple machines, use `jobs.<job_id>.strategy`.",
|
"description": "Use `runs-on` to define the type of machine to run the job on.\n* The destination machine can be either a GitHub-hosted runner, larger runner, or a self-hosted runner.\n* You can target runners based on the labels assigned to them, or their group membership, or a combination of these.\n* You can provide `runs-on` as a single string or as an array of strings.\n* If you specify an array of strings, your workflow will execute on any runner that matches all of the specified `runs-on` values.\n* If you would like to run your workflow on multiple machines, use `jobs.<job_id>.strategy`.",
|
||||||
@@ -2212,7 +2216,9 @@
|
|||||||
"hashFiles(1,255)"
|
"hashFiles(1,255)"
|
||||||
],
|
],
|
||||||
"description": "Use the `if` conditional to prevent a step from running unless a condition is met. Any supported context and expression can be used to create a conditional. Expressions in an `if` conditional do not require the bracketed expression syntax. When you use expressions in an `if` conditional, you may omit the expression syntax because GitHub automatically evaluates the `if` conditional as an expression.",
|
"description": "Use the `if` conditional to prevent a step from running unless a condition is met. Any supported context and expression can be used to create a conditional. Expressions in an `if` conditional do not require the bracketed expression syntax. When you use expressions in an `if` conditional, you may omit the expression syntax because GitHub automatically evaluates the `if` conditional as an expression.",
|
||||||
"string": {}
|
"string": {
|
||||||
|
"is-expression": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"step-if-result": {
|
"step-if-result": {
|
||||||
"context": [
|
"context": [
|
||||||
|
|||||||
@@ -117,11 +117,28 @@ export class YamlObjectReader implements ObjectReader {
|
|||||||
return new BooleanToken(fileId, range, value, undefined);
|
return new BooleanToken(fileId, range, value, undefined);
|
||||||
case "string": {
|
case "string": {
|
||||||
let source: string | undefined;
|
let source: string | undefined;
|
||||||
|
const scalarType = token.type;
|
||||||
|
let blockScalarHeader: string | undefined;
|
||||||
|
|
||||||
if (token.srcToken && "source" in token.srcToken) {
|
if (token.srcToken && "source" in token.srcToken) {
|
||||||
source = token.srcToken.source;
|
source = token.srcToken.source;
|
||||||
|
// 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
|
||||||
|
if (token.srcToken.type === "block-scalar" && "props" in token.srcToken) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const props = token.srcToken.props as any[];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||||
|
const headerProp = props.find((p: any) => p.type === "block-scalar-header");
|
||||||
|
if (headerProp && "source" in headerProp) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||||
|
blockScalarHeader = headerProp.source;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new StringToken(fileId, range, value, undefined, source);
|
return new StringToken(fileId, range, value, undefined, source, scalarType, blockScalarHeader);
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unexpected value type '${typeof value}' when reading object`);
|
throw new Error(`Unexpected value type '${typeof value}' when reading object`);
|
||||||
|
|||||||
+3
@@ -50,6 +50,7 @@ errors-step-uses-syntax.yml
|
|||||||
errors-unclosed-tokens.yml
|
errors-unclosed-tokens.yml
|
||||||
errors-yaml-invalid-style.yml
|
errors-yaml-invalid-style.yml
|
||||||
errors-yaml-tags-explicit-unsupported.yml
|
errors-yaml-tags-explicit-unsupported.yml
|
||||||
|
escape-html-values.yml
|
||||||
float-folded-style.yml
|
float-folded-style.yml
|
||||||
insert.yml
|
insert.yml
|
||||||
is-partial-rerun.yml
|
is-partial-rerun.yml
|
||||||
@@ -58,6 +59,7 @@ job-cancel-timeout-minutes.yml
|
|||||||
job-concurrency.yml
|
job-concurrency.yml
|
||||||
job-continue-on-error.yml
|
job-continue-on-error.yml
|
||||||
job-defaults.yml
|
job-defaults.yml
|
||||||
|
job-if.yml
|
||||||
job-permissions.yml
|
job-permissions.yml
|
||||||
job-timeout-minutes.yml
|
job-timeout-minutes.yml
|
||||||
matrix-basic.yml
|
matrix-basic.yml
|
||||||
@@ -83,6 +85,7 @@ reusable-workflow-job-permissions-overrides-default-write.yml
|
|||||||
reusable-workflow-job-permissions-overrides-workflow-level.yml
|
reusable-workflow-job-permissions-overrides-workflow-level.yml
|
||||||
root-env-defaults.yml
|
root-env-defaults.yml
|
||||||
round-to-infinity.yml
|
round-to-infinity.yml
|
||||||
|
step-if.yml
|
||||||
scientific-notation-number.yml
|
scientific-notation-number.yml
|
||||||
skip-reusable-workflows.yml
|
skip-reusable-workflows.yml
|
||||||
workflow-defaults.yml
|
workflow-defaults.yml
|
||||||
|
|||||||
Reference in New Issue
Block a user