From 82f29eb2167f711503e706850f45d6f24dc1e555 Mon Sep 17 00:00:00 2001 From: Christopher Schleiden Date: Tue, 15 Nov 2022 13:03:44 -0800 Subject: [PATCH 1/6] Expression auto-completion --- .../src/complete.expressions.test.ts | 106 ++++++++++++++++++ actions-languageservice/src/complete.ts | 68 ++++++++--- .../src/context-providers/config.ts | 26 +++++ actions-languageservice/src/index.ts | 4 +- .../src/utils/find-token.ts | 14 +-- 5 files changed, 191 insertions(+), 27 deletions(-) create mode 100644 actions-languageservice/src/complete.expressions.test.ts create mode 100644 actions-languageservice/src/context-providers/config.ts diff --git a/actions-languageservice/src/complete.expressions.test.ts b/actions-languageservice/src/complete.expressions.test.ts new file mode 100644 index 0000000..8e18cba --- /dev/null +++ b/actions-languageservice/src/complete.expressions.test.ts @@ -0,0 +1,106 @@ +import {data} from "@github/actions-expressions/."; +import {complete, getExpressionInput} from "./complete"; +import {ContextProviderConfig} from "./context-providers/config"; +import {getPositionFromCursor} from "./test-utils/cursor-position"; + +const contextProviderConfig: ContextProviderConfig = { + async getContext(contexts: string[]): Promise { + const context = new data.Dictionary(); + + for (const contextName of contexts) { + switch (contextName) { + case "github": + context.add( + "github", + new data.Dictionary({ + key: "event", + value: new data.StringData("push") + }) + ); + break; + + default: + context.add(contextName, new data.Dictionary()); + break; + } + } + + return context; + } +}; + +describe("expressions", () => { + it("input extraction", () => { + const test = (input: string) => { + const [doc, pos] = getPositionFromCursor(input); + return getExpressionInput(doc.getText(), pos.character); + }; + + expect(test("${{ gh |")).toBe(" gh "); + expect(test("${{ gh |}}")).toBe(" gh "); + expect(test("${{ vars }} ${{ gh |}}")).toBe(" gh "); + }); + + describe("top-level auto-complete", () => { + it("single region", async () => { + const input = "run-name: ${{ | }}"; + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + + expect(result.map(x => x.label)).toEqual([ + "github", + "inputs", + "vars", + "contains", + "endsWith", + "format", + "fromJson", + "join", + "startsWith", + "toJson" + ]); + }); + + it("single region with existing input", async () => { + const input = "run-name: ${{ g| }}"; + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + + expect(result.map(x => x.label)).toEqual([ + "github", + "inputs", + "vars", + "contains", + "endsWith", + "format", + "fromJson", + "join", + "startsWith", + "toJson" + ]); + }); + + it("multiple regions", async () => { + const input = "run-name: test-${{ github }}-${{ | }}"; + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + + expect(result.map(x => x.label)).toEqual([ + "github", + "inputs", + "vars", + "contains", + "endsWith", + "format", + "fromJson", + "join", + "startsWith", + "toJson" + ]); + }); + + it("nested auto-complete", async () => { + const input = "run-name: ${{ github.| }}"; + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + + expect(result.map(x => x.label)).toEqual(["event"]); + }); + }); +}); diff --git a/actions-languageservice/src/complete.ts b/actions-languageservice/src/complete.ts index e4c72c6..160b8a4 100644 --- a/actions-languageservice/src/complete.ts +++ b/actions-languageservice/src/complete.ts @@ -1,27 +1,42 @@ -import {parseWorkflow} from "@github/actions-workflow-parser"; -import { - SEQUENCE_TYPE, - STRING_TYPE, - MAPPING_TYPE, - TemplateToken, - NULL_TYPE -} from "@github/actions-workflow-parser/templates/tokens/index"; +import {complete as completeExpression, data} from "@github/actions-expressions"; +import {isSequence, isString, parseWorkflow} from "@github/actions-workflow-parser"; +import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants"; +import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index"; import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token"; -import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token"; +import {TokenType} from "@github/actions-workflow-parser/templates/tokens/types"; import {File} from "@github/actions-workflow-parser/workflows/file"; import {Position, TextDocument} from "vscode-languageserver-textdocument"; import {CompletionItem} from "vscode-languageserver-types"; +import {ContextProviderConfig} from "./context-providers/config"; import {nullTrace} from "./nulltrace"; import {findInnerTokenAndParent} from "./utils/find-token"; import {transform} from "./utils/transform"; import {Value, ValueProviderConfig} from "./value-providers/config"; import {defaultValueProviders} from "./value-providers/default"; +export function getExpressionInput(input: string, pos: number): string | undefined { + // Find start marker around the cursor position + const startPos = input.lastIndexOf(OPEN_EXPRESSION, pos); + if (startPos === -1) { + return undefined; + } + + // Find end marker after the cursor position + let endPos = input.indexOf(CLOSE_EXPRESSION, pos); + if (endPos === -1) { + // Assume an unfinished expression like "${{ someinput.|" + endPos = input.length; + } + + return input.substring(startPos + OPEN_EXPRESSION.length, endPos); +} + export async function complete( textDocument: TextDocument, position: Position, - valueProviderConfig?: ValueProviderConfig + valueProviderConfig?: ValueProviderConfig, + contextProviderConfig?: ContextProviderConfig ): Promise { // Fix the input to work around YAML parsing issues const [newDoc, newPos] = transform(textDocument, position); @@ -33,6 +48,25 @@ export async function complete( const result = parseWorkflow(file.name, [file], nullTrace); const [innerToken, parent] = findInnerTokenAndParent(newPos, result.value); + + // If we are inside an expression, take a different code-path. The workflow parser does not correctly create + // expression nodes for invalid expressions and during editing expressions are invalid most of the time. + if (innerToken && isString(innerToken) && innerToken.value.indexOf(OPEN_EXPRESSION) >= 0) { + // TODO: Handle expressions without markers like `if` + + const currentInput = innerToken.value; + + // Transform the overall position into a node relative position + const relCharPos = newPos.character - innerToken.range!.start[1]; + + const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim(); + + const context = + (await contextProviderConfig?.getContext(innerToken.definition!.readerContext)) || new data.Dictionary(); + + return completeExpression(expressionInput, context, []); + } + const values = await getValues(innerToken, parent, newPos, textDocument.uri, valueProviderConfig); return values.map(value => CompletionItem.create(value.label)); } @@ -48,7 +82,7 @@ async function getValues( return []; } - if (token?.templateTokenType === NULL_TYPE) { + if (token?.templateTokenType === TokenType.Null) { // Ensure there's a space after the parent key if (parent.range && position.character + 1 === parent.range.end[1]) { return []; @@ -84,7 +118,7 @@ async function getValues( function getExistingValues(token: TemplateToken | null, parent: TemplateToken) { // For incomplete YAML, we may only have a parent token if (token) { - if (token.templateTokenType !== STRING_TYPE || parent.templateTokenType !== SEQUENCE_TYPE) { + if (!isString(token) || !isSequence(parent)) { return; } @@ -92,22 +126,22 @@ function getExistingValues(token: TemplateToken | null, parent: TemplateToken) { const seqToken = parent as SequenceToken; for (let i = 0; i < seqToken.count; i++) { const t = seqToken.get(i); - if (t.isLiteral && t.templateTokenType === STRING_TYPE) { + if (t.isLiteral && isString(t)) { // Should we support other literal values here? - sequenceValues.add((t as StringToken).value); + sequenceValues.add(t.value); } } return sequenceValues; } - if (parent.templateTokenType === MAPPING_TYPE) { + if (parent.templateTokenType === TokenType.Mapping) { // No token and parent is a mapping, so we're completing a key const mapKeys = new Set(); const mapToken = parent as MappingToken; for (let i = 0; i < mapToken.count; i++) { const key = mapToken.get(i).key; - if (key.isLiteral && key.templateTokenType === STRING_TYPE) { - mapKeys.add((key as StringToken).value); + if (key.isLiteral && isString(key)) { + mapKeys.add(key.value); } } diff --git a/actions-languageservice/src/context-providers/config.ts b/actions-languageservice/src/context-providers/config.ts new file mode 100644 index 0000000..1b4e37e --- /dev/null +++ b/actions-languageservice/src/context-providers/config.ts @@ -0,0 +1,26 @@ +import {data} from "@github/actions-expressions"; +import {Dictionary} from "@github/actions-expressions/data/dictionary"; +import {ExpressionData, Pair} from "@github/actions-expressions/data/expressiondata"; + +export interface ContextProviderConfig { + getContext(contexts: string[]): Promise; +} + +/** + * DynamicDictionary is a dictionary that returns an empty DynamicDictionary for + * any key that is not present. + */ +export class DynamicDictionary extends data.Dictionary { + constructor(pairs: Pair[], private creator: () => T = () => new data.Dictionary() as T) { + super(...pairs); + } + + get(key: string): ExpressionData | undefined { + const value = super.get(key); + if (value) { + return value; + } + + return this.creator(); + } +} diff --git a/actions-languageservice/src/index.ts b/actions-languageservice/src/index.ts index db326a4..db8e8e6 100644 --- a/actions-languageservice/src/index.ts +++ b/actions-languageservice/src/index.ts @@ -1,3 +1,5 @@ +export {complete} from "./complete"; +export {ContextProviderConfig} from "./context-providers/config"; export {hover} from "./hover"; export {validate} from "./validate"; -export {complete} from "./complete"; +export {ValueProviderConfig} from "./value-providers/config"; diff --git a/actions-languageservice/src/utils/find-token.ts b/actions-languageservice/src/utils/find-token.ts index 0048f1a..48caa60 100644 --- a/actions-languageservice/src/utils/find-token.ts +++ b/actions-languageservice/src/utils/find-token.ts @@ -1,11 +1,7 @@ -import { - TemplateToken, - MAPPING_TYPE, - SEQUENCE_TYPE, - NULL_TYPE -} from "@github/actions-workflow-parser/templates/tokens/index"; +import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index"; import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token"; import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token"; +import {TokenType} from "@github/actions-workflow-parser/templates/tokens/types"; import {Position} from "vscode-languageserver-textdocument"; export function findInnerToken(pos: Position, root?: TemplateToken) { @@ -37,7 +33,7 @@ export function findInnerTokenAndParent( // Position is in token, enqueue children if there are any switch (token.templateTokenType) { - case MAPPING_TYPE: + case TokenType.Mapping: const mappingToken = token as MappingToken; parent = mappingToken; for (let i = 0; i < mappingToken.count; i++) { @@ -52,7 +48,7 @@ export function findInnerTokenAndParent( } continue; - case SEQUENCE_TYPE: + case TokenType.Sequence: const sequenceToken = token as SequenceToken; parent = sequenceToken; for (let i = 0; i < sequenceToken.count; i++) { @@ -92,7 +88,7 @@ function posInToken(pos: Position, token: TemplateToken): boolean { } function nullNodeOnLine(pos: Position, key: TemplateToken, value: TemplateToken): boolean { - if (value.templateTokenType !== NULL_TYPE) { + if (value.templateTokenType !== TokenType.Null) { return false; } From 35d706334eaa562bdf365657010a66832a7a87bc Mon Sep 17 00:00:00 2001 From: Christopher Schleiden Date: Tue, 15 Nov 2022 15:59:14 -0800 Subject: [PATCH 2/6] Add first default value provider --- .../src/complete.expressions.test.ts | 38 +++++++-------- actions-languageservice/src/complete.ts | 6 +-- .../src/context-providers/config.ts | 6 +-- .../src/context-providers/default.ts | 48 +++++++++++++++++++ .../src/context-providers/runner.ts | 0 5 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 actions-languageservice/src/context-providers/default.ts create mode 100644 actions-languageservice/src/context-providers/runner.ts diff --git a/actions-languageservice/src/complete.expressions.test.ts b/actions-languageservice/src/complete.expressions.test.ts index 8e18cba..274fb45 100644 --- a/actions-languageservice/src/complete.expressions.test.ts +++ b/actions-languageservice/src/complete.expressions.test.ts @@ -1,31 +1,19 @@ -import {data} from "@github/actions-expressions/."; +import {data} from "@github/actions-expressions"; import {complete, getExpressionInput} from "./complete"; import {ContextProviderConfig} from "./context-providers/config"; import {getPositionFromCursor} from "./test-utils/cursor-position"; const contextProviderConfig: ContextProviderConfig = { - async getContext(contexts: string[]): Promise { - const context = new data.Dictionary(); - - for (const contextName of contexts) { - switch (contextName) { - case "github": - context.add( - "github", - new data.Dictionary({ - key: "event", - value: new data.StringData("push") - }) - ); - break; - - default: - context.add(contextName, new data.Dictionary()); - break; - } + getContext: async (context: string) => { + switch (context) { + case "github": + return new data.Dictionary({ + key: "event", + value: new data.StringData("push") + }); } - return context; + return undefined; } }; @@ -102,5 +90,13 @@ describe("expressions", () => { expect(result.map(x => x.label)).toEqual(["event"]); }); + + it("using default context provider", async () => { + const input = + "on: push\njobs:\n build:\n runs-on: ubuntu-latest\n environment:\n url: ${{ runner.| }}\n steps:\n - run: echo"; + const result = await complete(...getPositionFromCursor(input), undefined, contextProviderConfig); + + expect(result.map(x => x.label)).toEqual(["arch", "name", "os", "temp", "tool_cache"]); + }); }); }); diff --git a/actions-languageservice/src/complete.ts b/actions-languageservice/src/complete.ts index 160b8a4..9722fa1 100644 --- a/actions-languageservice/src/complete.ts +++ b/actions-languageservice/src/complete.ts @@ -1,4 +1,4 @@ -import {complete as completeExpression, data} from "@github/actions-expressions"; +import {complete as completeExpression} from "@github/actions-expressions"; import {isSequence, isString, parseWorkflow} from "@github/actions-workflow-parser"; import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "@github/actions-workflow-parser/templates/template-constants"; import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/index"; @@ -9,6 +9,7 @@ import {File} from "@github/actions-workflow-parser/workflows/file"; import {Position, TextDocument} from "vscode-languageserver-textdocument"; import {CompletionItem} from "vscode-languageserver-types"; import {ContextProviderConfig} from "./context-providers/config"; +import {getContext} from "./context-providers/default"; import {nullTrace} from "./nulltrace"; import {findInnerTokenAndParent} from "./utils/find-token"; import {transform} from "./utils/transform"; @@ -61,8 +62,7 @@ export async function complete( const expressionInput = (getExpressionInput(currentInput, relCharPos) || "").trim(); - const context = - (await contextProviderConfig?.getContext(innerToken.definition!.readerContext)) || new data.Dictionary(); + const context = await getContext(innerToken.definition?.readerContext || [], contextProviderConfig); return completeExpression(expressionInput, context, []); } diff --git a/actions-languageservice/src/context-providers/config.ts b/actions-languageservice/src/context-providers/config.ts index 1b4e37e..49f61dd 100644 --- a/actions-languageservice/src/context-providers/config.ts +++ b/actions-languageservice/src/context-providers/config.ts @@ -2,9 +2,9 @@ import {data} from "@github/actions-expressions"; import {Dictionary} from "@github/actions-expressions/data/dictionary"; import {ExpressionData, Pair} from "@github/actions-expressions/data/expressiondata"; -export interface ContextProviderConfig { - getContext(contexts: string[]): Promise; -} +export type ContextProviderConfig = { + getContext: (name: string) => Promise; +}; /** * DynamicDictionary is a dictionary that returns an empty DynamicDictionary for diff --git a/actions-languageservice/src/context-providers/default.ts b/actions-languageservice/src/context-providers/default.ts new file mode 100644 index 0000000..cc964e9 --- /dev/null +++ b/actions-languageservice/src/context-providers/default.ts @@ -0,0 +1,48 @@ +import {data} from "@github/actions-expressions"; +import {ContextProviderConfig} from "./config"; + +export async function getContext(names: string[], config: ContextProviderConfig | undefined): Promise { + const context = new data.Dictionary(); + + for (const contextName of names) { + let value: data.Dictionary | undefined; + + value = await getDefaultContext(contextName); + + if (!value) { + value = await config?.getContext(contextName); + } + + if (!value) { + value = new data.Dictionary(); + } + + context.add(contextName, value); + } + + return context; +} + +async function getDefaultContext(name: string): Promise { + switch (name) { + case "runner": + return objectToDictionary({ + os: "Linux", + arch: "X64", + name: "GitHub Actions 2", + tool_cache: "/opt/hostedtoolcache", + temp: "/home/runner/work/_temp" + }); + } + + return undefined; +} + +function objectToDictionary(object: {[key: string]: string}): data.Dictionary { + const dictionary = new data.Dictionary(); + for (const key in object) { + dictionary.add(key, new data.StringData(object[key])); + } + + return dictionary; +} diff --git a/actions-languageservice/src/context-providers/runner.ts b/actions-languageservice/src/context-providers/runner.ts new file mode 100644 index 0000000..e69de29 From 4f57450d8c15843a814945968bced46157c4942d Mon Sep 17 00:00:00 2001 From: Christopher Schleiden Date: Tue, 15 Nov 2022 16:24:10 -0800 Subject: [PATCH 3/6] Use trigger characters --- actions-languageserver/src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/actions-languageserver/src/index.ts b/actions-languageserver/src/index.ts index cd7b2ec..f82d55c 100644 --- a/actions-languageserver/src/index.ts +++ b/actions-languageserver/src/index.ts @@ -57,13 +57,15 @@ connection.onInitialize((params: InitializeParams) => { const result: InitializeResult = { capabilities: { - textDocumentSync: TextDocumentSyncKind.Incremental, + textDocumentSync: TextDocumentSyncKind.Full, completionProvider: { resolveProvider: false, + triggerCharacters: [":", "."], }, hoverProvider: true, }, }; + if (hasWorkspaceFolderCapability) { result.capabilities.workspace = { workspaceFolders: { @@ -71,6 +73,7 @@ connection.onInitialize((params: InitializeParams) => { }, }; } + return result; }); From b433ce535b5ce45d3d247fdacd5982dd2ce46a93 Mon Sep 17 00:00:00 2001 From: Christopher Schleiden Date: Tue, 15 Nov 2022 16:26:20 -0800 Subject: [PATCH 4/6] Update comment --- actions-languageservice/src/context-providers/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions-languageservice/src/context-providers/config.ts b/actions-languageservice/src/context-providers/config.ts index 49f61dd..08c441f 100644 --- a/actions-languageservice/src/context-providers/config.ts +++ b/actions-languageservice/src/context-providers/config.ts @@ -7,8 +7,8 @@ export type ContextProviderConfig = { }; /** - * DynamicDictionary is a dictionary that returns an empty DynamicDictionary for - * any key that is not present. + * DynamicDictionary is a dictionary that returns an empty DynamicDictionary (or other given type) + * for any key that is not present. */ export class DynamicDictionary extends data.Dictionary { constructor(pairs: Pair[], private creator: () => T = () => new data.Dictionary() as T) { From 0b2754e6576bad4038cc2537bff954bb1c8d0dde Mon Sep 17 00:00:00 2001 From: Christopher Schleiden Date: Tue, 15 Nov 2022 16:49:21 -0800 Subject: [PATCH 5/6] Consume parser --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d1cc34..74f7c93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -665,9 +665,9 @@ "link": true }, "node_modules/@github/actions-workflow-parser": { - "version": "0.0.10", - "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.10/fca1dbab29f9fceb4c52e2486a384b410171f9d3", - "integrity": "sha512-dnDnAms7lQSU+qu6bpnzcgfRT1wIxjc8iitIyeHDkqS3bc3np+kTZ7mBQ8ZQzOxVzyWOLdd9Eyn4r0IPR2/qQg==", + "version": "0.0.12", + "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.12/b7b753fe96c1be36cc6f7952be76301ee3a10f12", + "integrity": "sha512-tPtIiRpCilAGUygpCCBv7yEamw9/Te0SF1lBN5BljxLE0atb34bLMGR3yyKjE+S3R3xUf3jmNNcTiGRLIqnAfw==", "license": "MIT", "dependencies": { "@github/actions-expressions": "*", @@ -10927,9 +10927,9 @@ } }, "@github/actions-workflow-parser": { - "version": "0.0.10", - "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.10/fca1dbab29f9fceb4c52e2486a384b410171f9d3", - "integrity": "sha512-dnDnAms7lQSU+qu6bpnzcgfRT1wIxjc8iitIyeHDkqS3bc3np+kTZ7mBQ8ZQzOxVzyWOLdd9Eyn4r0IPR2/qQg==", + "version": "0.0.12", + "resolved": "https://npm.pkg.github.com/download/@github/actions-workflow-parser/0.0.12/b7b753fe96c1be36cc6f7952be76301ee3a10f12", + "integrity": "sha512-tPtIiRpCilAGUygpCCBv7yEamw9/Te0SF1lBN5BljxLE0atb34bLMGR3yyKjE+S3R3xUf3jmNNcTiGRLIqnAfw==", "requires": { "@github/actions-expressions": "*", "yaml": "^2.0.0-8" From 3e3ab8ea53db1e9d2a38cff9c7a190d7636a7464 Mon Sep 17 00:00:00 2001 From: Christopher Schleiden Date: Tue, 15 Nov 2022 16:54:48 -0800 Subject: [PATCH 6/6] Consume expressions lib --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74f7c93..41c8b79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -648,9 +648,9 @@ "dev": true }, "node_modules/@github/actions-expressions": { - "version": "0.0.5", - "resolved": "https://npm.pkg.github.com/download/@github/actions-expressions/0.0.5/e435211d26b66939ea7329c9a817c66709eab475", - "integrity": "sha512-ehT1WMG3j0OWfFMRoK/i5I3iIGe8IUPO5sMLyldE1STYA8X07ux/1HGxyevbzbMTS6/MwXAYEPMgxvPxsrScaA==", + "version": "0.0.6", + "resolved": "https://npm.pkg.github.com/download/@github/actions-expressions/0.0.6/b98fa3f0e98aec967c22906c2a5848782d6a2ff2", + "integrity": "sha512-0W7a8PIUHcH39cvQdGIOMaj5+3JMCW0u0STKKA42V4TNEvp6R5tcs4L3w3+PEmslPX9P2j6GPxLxSEaq9armew==", "license": "MIT", "engines": { "node": ">= 16" @@ -10892,9 +10892,9 @@ "dev": true }, "@github/actions-expressions": { - "version": "0.0.5", - "resolved": "https://npm.pkg.github.com/download/@github/actions-expressions/0.0.5/e435211d26b66939ea7329c9a817c66709eab475", - "integrity": "sha512-ehT1WMG3j0OWfFMRoK/i5I3iIGe8IUPO5sMLyldE1STYA8X07ux/1HGxyevbzbMTS6/MwXAYEPMgxvPxsrScaA==" + "version": "0.0.6", + "resolved": "https://npm.pkg.github.com/download/@github/actions-expressions/0.0.6/b98fa3f0e98aec967c22906c2a5848782d6a2ff2", + "integrity": "sha512-0W7a8PIUHcH39cvQdGIOMaj5+3JMCW0u0STKKA42V4TNEvp6R5tcs4L3w3+PEmslPX9P2j6GPxLxSEaq9armew==" }, "@github/actions-languageserver": { "version": "file:actions-languageserver",