Merge branch 'main' into node24
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/languageserver",
|
||||
"version": "0.3.17",
|
||||
"version": "0.3.25",
|
||||
"description": "Language server for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -43,8 +43,8 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/languageservice": "^0.3.17",
|
||||
"@actions/workflow-parser": "^0.3.17",
|
||||
"@actions/languageservice": "^0.3.25",
|
||||
"@actions/workflow-parser": "^0.3.25",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@octokit/types": "^9.0.0",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
@@ -52,7 +52,7 @@
|
||||
"yaml": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.15"
|
||||
"node": ">= 18"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import {data, DescriptionDictionary} from "@actions/expressions";
|
||||
import {WorkflowContext} from "@actions/languageservice/context/workflow-context";
|
||||
import {Mode} from "@actions/languageservice/context-providers/default";
|
||||
import {contextProviders} from "./context-providers";
|
||||
import {RepositoryContext} from "./initializationOptions";
|
||||
import {TTLCache} from "./utils/cache";
|
||||
|
||||
describe("contextProviders", () => {
|
||||
const mockCache = new TTLCache();
|
||||
const mockRepo: RepositoryContext = {
|
||||
id: 123,
|
||||
owner: "test-owner",
|
||||
name: "test-repo",
|
||||
organizationOwned: true,
|
||||
workspaceUri: "file:///workspace"
|
||||
};
|
||||
const mockWorkflowContext: WorkflowContext = {
|
||||
uri: "test.yaml",
|
||||
template: undefined
|
||||
};
|
||||
|
||||
describe("when client is undefined", () => {
|
||||
it("should return incomplete context for secrets", async () => {
|
||||
const config = contextProviders(undefined, mockRepo, mockCache);
|
||||
const result = await config.getContext("secrets", undefined, mockWorkflowContext, Mode.Validation);
|
||||
|
||||
expect(result).toBeInstanceOf(DescriptionDictionary);
|
||||
expect((result as DescriptionDictionary).complete).toBe(false);
|
||||
});
|
||||
|
||||
it("should return incomplete context for vars", async () => {
|
||||
const config = contextProviders(undefined, mockRepo, mockCache);
|
||||
const result = await config.getContext("vars", undefined, mockWorkflowContext, Mode.Validation);
|
||||
|
||||
expect(result).toBeInstanceOf(DescriptionDictionary);
|
||||
expect((result as DescriptionDictionary).complete).toBe(false);
|
||||
});
|
||||
|
||||
it("should preserve defaultContext and mark as incomplete for secrets", async () => {
|
||||
const config = contextProviders(undefined, mockRepo, mockCache);
|
||||
const defaultContext = new DescriptionDictionary();
|
||||
defaultContext.add("EXISTING_SECRET", new data.StringData("test"));
|
||||
|
||||
const result = await config.getContext("secrets", defaultContext, mockWorkflowContext, Mode.Validation);
|
||||
|
||||
expect(result).toBe(defaultContext);
|
||||
expect((result as DescriptionDictionary).complete).toBe(false);
|
||||
expect((result as DescriptionDictionary).get("EXISTING_SECRET")).toBeDefined();
|
||||
});
|
||||
|
||||
it("should return undefined for other contexts like steps", async () => {
|
||||
const config = contextProviders(undefined, mockRepo, mockCache);
|
||||
const result = await config.getContext("steps", undefined, mockWorkflowContext, Mode.Validation);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when both client and repo are undefined", () => {
|
||||
it("should return incomplete context for secrets", async () => {
|
||||
const config = contextProviders(undefined, undefined, mockCache);
|
||||
const result = await config.getContext("secrets", undefined, mockWorkflowContext, Mode.Validation);
|
||||
|
||||
expect(result).toBeInstanceOf(DescriptionDictionary);
|
||||
expect((result as DescriptionDictionary).complete).toBe(false);
|
||||
});
|
||||
|
||||
it("should return incomplete context for vars", async () => {
|
||||
const config = contextProviders(undefined, undefined, mockCache);
|
||||
const result = await config.getContext("vars", undefined, mockWorkflowContext, Mode.Validation);
|
||||
|
||||
expect(result).toBeInstanceOf(DescriptionDictionary);
|
||||
expect((result as DescriptionDictionary).complete).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,18 @@ export function contextProviders(
|
||||
cache: TTLCache
|
||||
): ContextProviderConfig {
|
||||
if (!repo || !client) {
|
||||
return {getContext: () => Promise.resolve(undefined)};
|
||||
// When GitHub client/repo is unavailable, return an incomplete dictionary
|
||||
// to avoid false "Context access might be invalid" warnings
|
||||
return {
|
||||
getContext: (name: string, defaultContext: DescriptionDictionary | undefined) => {
|
||||
if (name === "secrets" || name === "vars") {
|
||||
const context = defaultContext || new DescriptionDictionary();
|
||||
context.complete = false;
|
||||
return Promise.resolve(context);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getContext = async (
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function getSecrets(
|
||||
if (isString(x.value)) {
|
||||
environmentName = x.value.value;
|
||||
} else {
|
||||
// this means we have a dynamic enviornment, in those situations we
|
||||
// this means we have a dynamic environment, in those situations we
|
||||
// want to make sure we skip doing secret validation
|
||||
secretsContext.complete = false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {data, DescriptionDictionary} from "@actions/expressions";
|
||||
import {data, DescriptionDictionary, isDescriptionDictionary} from "@actions/expressions";
|
||||
import {getStepsContext as getDefaultStepsContext} from "@actions/languageservice/context-providers/steps";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import fetchMock from "fetch-mock";
|
||||
@@ -63,6 +63,43 @@ it("returns default context when job is undefined", async () => {
|
||||
expect(stepsContext).toEqual(defaultContext);
|
||||
});
|
||||
|
||||
it("outputs is an incomplete dictionary to allow dynamic outputs", async () => {
|
||||
const mock = fetchMock
|
||||
.sandbox()
|
||||
.getOnce("https://api.github.com/repos/actions/cache/contents/action.yml?ref=v3", actionMetadata);
|
||||
|
||||
const workflowContext = await createWorkflowContext(workflow, "build");
|
||||
const defaultContext = getDefaultStepsContext(workflowContext);
|
||||
|
||||
const stepsContext = await getStepsContext(
|
||||
new Octokit({
|
||||
request: {
|
||||
fetch: mock
|
||||
}
|
||||
}),
|
||||
new TTLCache(),
|
||||
defaultContext,
|
||||
workflowContext
|
||||
);
|
||||
|
||||
// Get the step context
|
||||
const stepContext = stepsContext?.get("cache-primes");
|
||||
expect(stepContext).toBeDefined();
|
||||
expect(isDescriptionDictionary(stepContext!)).toBe(true);
|
||||
|
||||
// Get the outputs - should be a dictionary, not null
|
||||
const outputs = (stepContext as DescriptionDictionary).get("outputs");
|
||||
expect(outputs).toBeDefined();
|
||||
expect(isDescriptionDictionary(outputs!)).toBe(true);
|
||||
|
||||
// Outputs should be marked incomplete to allow dynamic outputs
|
||||
const outputsDict = outputs as DescriptionDictionary;
|
||||
expect(outputsDict.complete).toBe(false);
|
||||
|
||||
// Known outputs from action.yml should be present
|
||||
expect(outputsDict.get("cache-hit")).toBeDefined();
|
||||
});
|
||||
|
||||
it("adds action outputs", async () => {
|
||||
const mock = fetchMock
|
||||
.sandbox()
|
||||
@@ -83,17 +120,22 @@ it("adds action outputs", async () => {
|
||||
);
|
||||
expect(stepsContext).toBeDefined();
|
||||
|
||||
// Create expected outputs dict with complete = false
|
||||
// (actions can have dynamic outputs beyond what's declared in action.yml)
|
||||
const expectedOutputs = new DescriptionDictionary({
|
||||
key: "cache-hit",
|
||||
value: new data.StringData("A boolean value to indicate an exact match was found for the primary key"),
|
||||
description: "A boolean value to indicate an exact match was found for the primary key"
|
||||
});
|
||||
expectedOutputs.complete = false;
|
||||
|
||||
expect(stepsContext).toEqual(
|
||||
new DescriptionDictionary({
|
||||
key: "cache-primes",
|
||||
value: new DescriptionDictionary(
|
||||
{
|
||||
key: "outputs",
|
||||
value: new DescriptionDictionary({
|
||||
key: "cache-hit",
|
||||
value: new data.StringData("A boolean value to indicate an exact match was found for the primary key"),
|
||||
description: "A boolean value to indicate an exact match was found for the primary key"
|
||||
})
|
||||
value: expectedOutputs
|
||||
},
|
||||
{
|
||||
key: "conclusion",
|
||||
|
||||
@@ -58,6 +58,8 @@ export async function getStepsContext(
|
||||
continue;
|
||||
}
|
||||
const outputsDict = new DescriptionDictionary();
|
||||
// Actions can have dynamic outputs beyond what's declared in action.yml
|
||||
outputsDict.complete = false;
|
||||
for (const [key, value] of Object.entries(outputs)) {
|
||||
outputsDict.add(key, new data.StringData(value.description), value.description);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ export async function getVariables(
|
||||
return secretsContext;
|
||||
}
|
||||
|
||||
const variablesContext = defaultContext || new DescriptionDictionary();
|
||||
|
||||
let environmentName: string | undefined;
|
||||
if (workflowContext?.job?.environment) {
|
||||
if (isString(workflowContext.job.environment)) {
|
||||
@@ -35,14 +37,19 @@ export async function getVariables(
|
||||
if (isString(x.key) && x.key.value === "name") {
|
||||
if (isString(x.value)) {
|
||||
environmentName = x.value.value;
|
||||
} else {
|
||||
// this means we have a dynamic environment, in those situations we want to skip validation
|
||||
variablesContext.complete = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if the expression is something like environment: ${{ ... }} then we want to skip validation
|
||||
variablesContext.complete = false;
|
||||
}
|
||||
}
|
||||
|
||||
const variablesContext = defaultContext || new DescriptionDictionary();
|
||||
try {
|
||||
const variables = await getRemoteVariables(octokit, cache, repo, environmentName);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user