Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86888cf4c8 | |||
| ed4c2ce44c | |||
| 9bb4c76612 | |||
| 8b86b48961 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/expressions",
|
||||
"version": "0.3.26",
|
||||
"version": "0.3.27",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/languageserver",
|
||||
"version": "0.3.26",
|
||||
"version": "0.3.27",
|
||||
"description": "Language server for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -48,8 +48,8 @@
|
||||
"actions-languageserver": "./bin/actions-languageserver"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/languageservice": "^0.3.26",
|
||||
"@actions/workflow-parser": "^0.3.26",
|
||||
"@actions/languageservice": "^0.3.27",
|
||||
"@actions/workflow-parser": "^0.3.27",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@octokit/types": "^9.0.0",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/languageservice",
|
||||
"version": "0.3.26",
|
||||
"version": "0.3.27",
|
||||
"description": "Language service for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -47,8 +47,8 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/expressions": "^0.3.26",
|
||||
"@actions/workflow-parser": "^0.3.26",
|
||||
"@actions/expressions": "^0.3.27",
|
||||
"@actions/workflow-parser": "^0.3.27",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
"vscode-languageserver-types": "^3.17.2",
|
||||
"vscode-uri": "^3.0.8",
|
||||
|
||||
@@ -1110,7 +1110,7 @@ jobs:
|
||||
`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
|
||||
expect(result.map(x => x.label)).toEqual(["container", "services", "status"]);
|
||||
expect(result.map(x => x.label)).toEqual(["check_run_id", "container", "services", "status"]);
|
||||
});
|
||||
|
||||
it("job context is suggested within a job output", async () => {
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(9);
|
||||
expect(result.length).toEqual(13);
|
||||
expect(result[0].label).toEqual("concurrency");
|
||||
});
|
||||
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
|`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(21);
|
||||
expect(result.length).toEqual(30);
|
||||
});
|
||||
|
||||
it("string definition completion in sequence", async () => {
|
||||
@@ -243,7 +243,7 @@ jobs:
|
||||
runs-|`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result).toHaveLength(21);
|
||||
expect(result).toHaveLength(30);
|
||||
});
|
||||
|
||||
it("job key with comment afterwards", async () => {
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
#`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result).toHaveLength(21);
|
||||
expect(result).toHaveLength(30);
|
||||
});
|
||||
|
||||
it("job key with other values afterwards", async () => {
|
||||
@@ -266,7 +266,7 @@ jobs:
|
||||
concurrency: 'group-name'`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result).toHaveLength(20);
|
||||
expect(result).toHaveLength(29);
|
||||
});
|
||||
|
||||
it("step key without space after colon", async () => {
|
||||
@@ -335,7 +335,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).toHaveLength(17);
|
||||
expect(result).toHaveLength(25);
|
||||
});
|
||||
|
||||
it("complete from behind a colon will replace it", async () => {
|
||||
@@ -348,7 +348,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).toHaveLength(17);
|
||||
expect(result).toHaveLength(25);
|
||||
const textEdit = result[0].textEdit as TextEdit;
|
||||
expect(textEdit.range).toEqual({
|
||||
start: {line: 5, character: 4},
|
||||
@@ -533,4 +533,81 @@ jobs:
|
||||
expect(result.filter(x => x.label === "actions").map(x => x.textEdit?.newText)).toEqual(["\n actions: "]);
|
||||
expect(result.filter(x => x.label === "contents").map(x => x.textEdit?.newText)).toEqual(["\n contents: "]);
|
||||
});
|
||||
|
||||
it("shows both simple and full syntax for null+mapping one-of", async () => {
|
||||
// check_run is a one-of: [null, mapping]. Show both:
|
||||
// - check_run (simple, just the key with colon)
|
||||
// - check_run (full syntax) (ready to add mapping keys)
|
||||
const input = "on:\n |";
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// Should have both check_run and check_run (full syntax)
|
||||
expect(result.some(x => x.label === "check_run")).toBe(true);
|
||||
expect(result.some(x => x.label === "check_run (full syntax)")).toBe(true);
|
||||
});
|
||||
|
||||
it("shows all three variants for scalar+sequence+mapping one-of", async () => {
|
||||
// runs-on is a one-of: [string, sequence, mapping]
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
|`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// Should have runs-on, runs-on (list), and runs-on (full syntax)
|
||||
expect(result.some(x => x.label === "runs-on")).toBe(true);
|
||||
expect(result.some(x => x.label === "runs-on (list)")).toBe(true);
|
||||
expect(result.some(x => x.label === "runs-on (full syntax)")).toBe(true);
|
||||
});
|
||||
|
||||
it("generates correct insertText for one-of variants in parent mode", async () => {
|
||||
// runs-on is a one-of: [string, sequence, mapping]
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
|`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// Scalar: just key with colon and space
|
||||
expect(result.find(x => x.label === "runs-on")?.textEdit?.newText).toEqual("runs-on: ");
|
||||
|
||||
// Sequence: key with colon, newline, and list item
|
||||
expect(result.find(x => x.label === "runs-on (list)")?.textEdit?.newText).toEqual("runs-on:\n - ");
|
||||
|
||||
// Mapping: key with colon, newline, and indentation for nested keys
|
||||
expect(result.find(x => x.label === "runs-on (full syntax)")?.textEdit?.newText).toEqual("runs-on:\n ");
|
||||
});
|
||||
|
||||
it("generates correct insertText for one-of variants in key mode", async () => {
|
||||
// concurrency is a one-of: [string, mapping] - testing key mode (after colon on same line)
|
||||
const input = "concurrency: |";
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// Scalar in key mode: newline + indented key + colon + space
|
||||
expect(result.find(x => x.label === "group")?.textEdit?.newText).toEqual("\n group: ");
|
||||
|
||||
// Boolean in key mode (cancel-in-progress): newline + indented key + colon + space
|
||||
expect(result.find(x => x.label === "cancel-in-progress")?.textEdit?.newText).toEqual("\n cancel-in-progress: ");
|
||||
});
|
||||
|
||||
it("uses base key as filterText for qualified one-of variants", async () => {
|
||||
// runs-on has multiple structural types, so variants get qualifiers
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
|`;
|
||||
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
// Scalar: no qualifier, so no filterText needed
|
||||
expect(result.find(x => x.label === "runs-on")?.filterText).toBeUndefined();
|
||||
|
||||
// Sequence and mapping: qualified labels should filter on base key
|
||||
expect(result.find(x => x.label === "runs-on (list)")?.filterText).toEqual("runs-on");
|
||||
expect(result.find(x => x.label === "runs-on (full syntax)")?.filterText).toEqual("runs-on");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,6 +129,8 @@ export async function complete(
|
||||
|
||||
const item: CompletionItem = {
|
||||
label: value.label,
|
||||
filterText: value.filterText,
|
||||
sortText: value.sortText,
|
||||
documentation: value.description && {
|
||||
kind: "markdown",
|
||||
value: value.description
|
||||
@@ -253,7 +255,7 @@ function getExpressionCompletionItems(
|
||||
|
||||
function filterAndSortCompletionOptions(options: Value[], existingValues?: Set<string>) {
|
||||
options = options.filter(x => !existingValues?.has(x.label));
|
||||
options.sort((a, b) => a.label.localeCompare(b.label));
|
||||
options.sort((a, b) => (a.sortText ?? a.label).localeCompare(b.sortText ?? b.label));
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ export function getJobContext(workflowContext: WorkflowContext): DescriptionDict
|
||||
// Status
|
||||
jobContext.add("status", new data.Null());
|
||||
|
||||
// Check run ID
|
||||
jobContext.add("check_run_id", new data.Null());
|
||||
|
||||
return jobContext;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,17 +21,21 @@ describe("end-to-end", () => {
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result.length).toEqual(9);
|
||||
expect(result.length).toEqual(13);
|
||||
const labels = result.map(x => x.label);
|
||||
expect(labels).toEqual([
|
||||
"concurrency",
|
||||
"concurrency (full syntax)",
|
||||
"defaults",
|
||||
"description",
|
||||
"env",
|
||||
"jobs",
|
||||
"name",
|
||||
"on",
|
||||
"on (list)",
|
||||
"on (full syntax)",
|
||||
"permissions",
|
||||
"permissions (full syntax)",
|
||||
"run-name"
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -417,6 +417,21 @@ jobs:
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("job.check_run_id", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo \${{ job.check_run_id }}
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("job.services.<service_id>", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
@@ -272,6 +272,31 @@ function validateCronExpression(diagnostics: Diagnostic[], token: StringToken):
|
||||
}
|
||||
}
|
||||
|
||||
// Matches a short SHA (7-8 hex characters) that looks like it should be a full SHA
|
||||
const SHORT_SHA_PATTERN = /^[0-9a-f]{7,8}$/i;
|
||||
const SHORT_SHA_DOCS_URL =
|
||||
"https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions";
|
||||
|
||||
/**
|
||||
* Checks if a ref looks like a short SHA and adds a warning if so.
|
||||
* Returns true if a warning was added.
|
||||
*/
|
||||
function warnIfShortSha(diagnostics: Diagnostic[], token: StringToken, ref: string): boolean {
|
||||
if (SHORT_SHA_PATTERN.test(ref)) {
|
||||
diagnostics.push({
|
||||
message: `The provided ref '${ref}' may be a shortened commit SHA. If so, please use the full 40-character commit SHA instead, as short SHAs are not supported.`,
|
||||
severity: DiagnosticSeverity.Warning,
|
||||
range: mapRange(token.range),
|
||||
code: "short-sha-ref",
|
||||
codeDescription: {
|
||||
href: SHORT_SHA_DOCS_URL
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the format of a step's `uses` field.
|
||||
*
|
||||
@@ -343,6 +368,9 @@ function validateStepUsesFormat(diagnostics: Diagnostic[], token: StringToken):
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Warn if ref looks like a short SHA
|
||||
warnIfShortSha(diagnostics, token, gitRef);
|
||||
}
|
||||
|
||||
function addStepUsesFormatError(diagnostics: Diagnostic[], token: StringToken): void {
|
||||
@@ -501,6 +529,9 @@ function validateWorkflowUsesFormat(diagnostics: Diagnostic[], token: StringToke
|
||||
addWorkflowUsesFormatError(diagnostics, token, "invalid workflow file name");
|
||||
return;
|
||||
}
|
||||
|
||||
// Warn if version looks like a short SHA
|
||||
warnIfShortSha(diagnostics, token, version);
|
||||
}
|
||||
|
||||
function addWorkflowUsesFormatError(diagnostics: Diagnostic[], token: StringToken, reason: string): void {
|
||||
|
||||
@@ -891,4 +891,200 @@ jobs:
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("short SHA warnings", () => {
|
||||
describe("step uses", () => {
|
||||
it("warns on 7-char short SHA", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@a1b2c3d
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"The provided ref 'a1b2c3d' may be a shortened commit SHA. If so, please use the full 40-character commit SHA instead, as short SHAs are not supported.",
|
||||
severity: DiagnosticSeverity.Warning,
|
||||
range: {
|
||||
start: {line: 5, character: 12},
|
||||
end: {line: 5, character: 36}
|
||||
},
|
||||
code: "short-sha-ref",
|
||||
codeDescription: {
|
||||
href: "https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions"
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("warns on 8-char short SHA", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@a1b2c3d4
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"The provided ref 'a1b2c3d4' may be a shortened commit SHA. If so, please use the full 40-character commit SHA instead, as short SHAs are not supported.",
|
||||
severity: DiagnosticSeverity.Warning,
|
||||
range: {
|
||||
start: {line: 5, character: 12},
|
||||
end: {line: 5, character: 37}
|
||||
},
|
||||
code: "short-sha-ref",
|
||||
codeDescription: {
|
||||
href: "https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions"
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not warn on full SHA (40 chars)", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn on tag ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn on branch ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn on Docker action", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: docker://alpine:3.8
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn on local action", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ./my-action
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("workflow uses", () => {
|
||||
it("warns on 7-char short SHA in reusable workflow", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/ci.yml@a1b2c3d
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"The provided ref 'a1b2c3d' may be a shortened commit SHA. If so, please use the full 40-character commit SHA instead, as short SHAs are not supported.",
|
||||
severity: DiagnosticSeverity.Warning,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 53}
|
||||
},
|
||||
code: "short-sha-ref",
|
||||
codeDescription: {
|
||||
href: "https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions"
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("warns on 8-char short SHA in reusable workflow", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/ci.yml@a1b2c3d4
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"The provided ref 'a1b2c3d4' may be a shortened commit SHA. If so, please use the full 40-character commit SHA instead, as short SHAs are not supported.",
|
||||
severity: DiagnosticSeverity.Warning,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 54}
|
||||
},
|
||||
code: "short-sha-ref",
|
||||
codeDescription: {
|
||||
href: "https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions"
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not warn on full SHA in reusable workflow", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/ci.yml@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn on tag ref in reusable workflow", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/ci.yml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn on local workflow", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/ci.yml
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,12 @@ export interface Value {
|
||||
|
||||
/** Alternative insert text, if not given `label` will be used */
|
||||
insertText?: string;
|
||||
|
||||
/** Alternative filter text, if not given `label` will be used for filtering */
|
||||
filterText?: string;
|
||||
|
||||
/** Sort text to control ordering, if not given `label` will be used for sorting */
|
||||
sortText?: string;
|
||||
}
|
||||
|
||||
export enum ValueProviderKind {
|
||||
|
||||
@@ -91,13 +91,13 @@ function mappingValues(
|
||||
}
|
||||
break;
|
||||
|
||||
case DefinitionType.OneOf:
|
||||
if (mode == DefinitionValueMode.Key) {
|
||||
insertText = `\n${indentation}${key}: `;
|
||||
} else {
|
||||
insertText = `${key}: `;
|
||||
}
|
||||
break;
|
||||
case DefinitionType.OneOf: {
|
||||
// Expand one-of into multiple completions based on structural type
|
||||
const oneOfDef = typeDef as OneOfDefinition;
|
||||
const expanded = expandOneOfToCompletions(oneOfDef, definitions, key, description, indentation, mode);
|
||||
properties.push(...expanded);
|
||||
continue; // Skip the default push below
|
||||
}
|
||||
|
||||
case DefinitionType.String:
|
||||
case DefinitionType.Boolean:
|
||||
@@ -143,3 +143,101 @@ function distinctValues(values: Value[]): Value[] {
|
||||
}
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucket type for one-of expansion
|
||||
*/
|
||||
type StructuralBucket = "scalar" | "sequence" | "mapping";
|
||||
|
||||
/**
|
||||
* Get the structural bucket for a definition type.
|
||||
* Nested one-of is treated as scalar.
|
||||
*/
|
||||
function getStructuralBucket(defType: DefinitionType): StructuralBucket {
|
||||
switch (defType) {
|
||||
case DefinitionType.Sequence:
|
||||
return "sequence";
|
||||
case DefinitionType.Mapping:
|
||||
return "mapping";
|
||||
default:
|
||||
// String, Boolean, Number, Null, OneOf (nested), AllowedValues -> scalar
|
||||
// Note, nested OneOf is assumed to be all scalar values, which is true in practice.
|
||||
return "scalar";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a one-of definition into multiple completion items based on structural types.
|
||||
* Returns one completion per unique structural type (scalar, sequence, mapping).
|
||||
*/
|
||||
function expandOneOfToCompletions(
|
||||
oneOfDef: OneOfDefinition,
|
||||
definitions: {[key: string]: Definition},
|
||||
key: string,
|
||||
description: string | undefined,
|
||||
indentation: string,
|
||||
mode: DefinitionValueMode
|
||||
): Value[] {
|
||||
// Bucket variants by structural type
|
||||
const buckets: Record<StructuralBucket, boolean> = {
|
||||
scalar: false,
|
||||
sequence: false,
|
||||
mapping: false
|
||||
};
|
||||
|
||||
for (const variantKey of oneOfDef.oneOf) {
|
||||
const variantDef = definitions[variantKey];
|
||||
if (variantDef) {
|
||||
const bucket = getStructuralBucket(variantDef.definitionType);
|
||||
buckets[bucket] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const results: Value[] = [];
|
||||
|
||||
// Count how many structural types are present
|
||||
const bucketCount = [buckets.scalar, buckets.sequence, buckets.mapping].filter(Boolean).length;
|
||||
const needsQualifier = bucketCount > 1;
|
||||
|
||||
// Emit completions in order: scalar, sequence, mapping
|
||||
// Use sortText to preserve this order (scalar sorts first, then 1=sequence, 2=mapping)
|
||||
if (buckets.scalar) {
|
||||
// In Key mode, insert newline and indentation to produce valid YAML structure
|
||||
const insertText = mode === DefinitionValueMode.Key ? `\n${indentation}${key}: ` : `${key}: `;
|
||||
results.push({
|
||||
label: key,
|
||||
description,
|
||||
insertText
|
||||
});
|
||||
}
|
||||
|
||||
if (buckets.sequence) {
|
||||
const insertText =
|
||||
mode === DefinitionValueMode.Key
|
||||
? `\n${indentation}${key}:\n${indentation}${indentation}- `
|
||||
: `${key}:\n${indentation}- `;
|
||||
results.push({
|
||||
label: needsQualifier ? `${key} (list)` : key,
|
||||
description,
|
||||
insertText,
|
||||
filterText: needsQualifier ? key : undefined,
|
||||
sortText: needsQualifier ? `${key} 1` : undefined
|
||||
});
|
||||
}
|
||||
|
||||
if (buckets.mapping) {
|
||||
const insertText =
|
||||
mode === DefinitionValueMode.Key
|
||||
? `\n${indentation}${key}:\n${indentation}${indentation}`
|
||||
: `${key}:\n${indentation}`;
|
||||
results.push({
|
||||
label: needsQualifier ? `${key} (full syntax)` : key,
|
||||
description,
|
||||
insertText,
|
||||
filterText: needsQualifier ? key : undefined,
|
||||
sortText: needsQualifier ? `${key} 2` : undefined
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,5 +6,5 @@
|
||||
"languageservice",
|
||||
"languageserver"
|
||||
],
|
||||
"version": "0.3.26"
|
||||
"version": "0.3.27"
|
||||
}
|
||||
Generated
+9
-9
@@ -136,7 +136,7 @@
|
||||
},
|
||||
"expressions": {
|
||||
"name": "@actions/expressions",
|
||||
"version": "0.3.26",
|
||||
"version": "0.3.27",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.0.3",
|
||||
@@ -396,11 +396,11 @@
|
||||
},
|
||||
"languageserver": {
|
||||
"name": "@actions/languageserver",
|
||||
"version": "0.3.26",
|
||||
"version": "0.3.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/languageservice": "^0.3.26",
|
||||
"@actions/workflow-parser": "^0.3.26",
|
||||
"@actions/languageservice": "^0.3.27",
|
||||
"@actions/workflow-parser": "^0.3.27",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@octokit/types": "^9.0.0",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
@@ -940,11 +940,11 @@
|
||||
},
|
||||
"languageservice": {
|
||||
"name": "@actions/languageservice",
|
||||
"version": "0.3.26",
|
||||
"version": "0.3.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/expressions": "^0.3.26",
|
||||
"@actions/workflow-parser": "^0.3.26",
|
||||
"@actions/expressions": "^0.3.27",
|
||||
"@actions/workflow-parser": "^0.3.27",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
"vscode-languageserver-types": "^3.17.2",
|
||||
"vscode-uri": "^3.0.8",
|
||||
@@ -13345,10 +13345,10 @@
|
||||
},
|
||||
"workflow-parser": {
|
||||
"name": "@actions/workflow-parser",
|
||||
"version": "0.3.26",
|
||||
"version": "0.3.27",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/expressions": "^0.3.26",
|
||||
"@actions/expressions": "^0.3.27",
|
||||
"cronstrue": "^2.21.0",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/workflow-parser",
|
||||
"version": "0.3.26",
|
||||
"version": "0.3.27",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
@@ -48,7 +48,7 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/expressions": "^0.3.26",
|
||||
"@actions/expressions": "^0.3.27",
|
||||
"cronstrue": "^2.21.0",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user