Skip secrets/vars validation when context is incomplete (#233)
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import {DescriptionDictionary} from "@actions/expressions";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {getContext, Mode} from "./default";
|
||||
|
||||
describe("getContext", () => {
|
||||
const emptyWorkflowContext: WorkflowContext = {
|
||||
uri: "test.yaml",
|
||||
template: undefined
|
||||
};
|
||||
|
||||
describe("when no contextProviderConfig is provided", () => {
|
||||
it("should mark secrets context as incomplete", async () => {
|
||||
const result = await getContext(["secrets"], undefined, emptyWorkflowContext, Mode.Validation);
|
||||
|
||||
const secretsContext = result.get("secrets") as DescriptionDictionary;
|
||||
expect(secretsContext).toBeDefined();
|
||||
expect(secretsContext.complete).toBe(false);
|
||||
});
|
||||
|
||||
it("should mark vars context as incomplete", async () => {
|
||||
const result = await getContext(["vars"], undefined, emptyWorkflowContext, Mode.Validation);
|
||||
|
||||
const varsContext = result.get("vars") as DescriptionDictionary;
|
||||
expect(varsContext).toBeDefined();
|
||||
expect(varsContext.complete).toBe(false);
|
||||
});
|
||||
|
||||
it("should not mark other contexts as incomplete", async () => {
|
||||
const result = await getContext(["env", "github"], undefined, emptyWorkflowContext, Mode.Validation);
|
||||
|
||||
const envContext = result.get("env") as DescriptionDictionary;
|
||||
const githubContext = result.get("github") as DescriptionDictionary;
|
||||
|
||||
// These contexts are derived from the workflow file, so they can be complete
|
||||
expect(envContext).toBeDefined();
|
||||
expect(envContext.complete).toBe(true);
|
||||
expect(githubContext).toBeDefined();
|
||||
expect(githubContext.complete).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when contextProviderConfig returns a value", () => {
|
||||
it("should use the provided context for secrets", async () => {
|
||||
const providedContext = new DescriptionDictionary();
|
||||
providedContext.complete = true; // Provider fetched from API, so it's complete
|
||||
|
||||
const config = {
|
||||
getContext: () => Promise.resolve(providedContext)
|
||||
};
|
||||
|
||||
const result = await getContext(["secrets"], config, emptyWorkflowContext, Mode.Validation);
|
||||
|
||||
const secretsContext = result.get("secrets");
|
||||
expect(secretsContext).toBe(providedContext);
|
||||
expect((secretsContext as DescriptionDictionary).complete).toBe(true);
|
||||
});
|
||||
|
||||
it("should use the provided context for vars", async () => {
|
||||
const providedContext = new DescriptionDictionary();
|
||||
providedContext.complete = true;
|
||||
|
||||
const config = {
|
||||
getContext: () => Promise.resolve(providedContext)
|
||||
};
|
||||
|
||||
const result = await getContext(["vars"], config, emptyWorkflowContext, Mode.Validation);
|
||||
|
||||
const varsContext = result.get("vars");
|
||||
expect(varsContext).toBe(providedContext);
|
||||
expect((varsContext as DescriptionDictionary).complete).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when contextProviderConfig returns undefined", () => {
|
||||
it("should mark secrets as incomplete", async () => {
|
||||
const config = {
|
||||
getContext: () => Promise.resolve(undefined)
|
||||
};
|
||||
|
||||
const result = await getContext(["secrets"], config, emptyWorkflowContext, Mode.Validation);
|
||||
|
||||
const secretsContext = result.get("secrets") as DescriptionDictionary;
|
||||
expect(secretsContext.complete).toBe(false);
|
||||
});
|
||||
|
||||
it("should mark vars as incomplete", async () => {
|
||||
const config = {
|
||||
getContext: () => Promise.resolve(undefined)
|
||||
};
|
||||
|
||||
const result = await getContext(["vars"], config, emptyWorkflowContext, Mode.Validation);
|
||||
|
||||
const varsContext = result.get("vars") as DescriptionDictionary;
|
||||
expect(varsContext.complete).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -40,7 +40,15 @@ export async function getContext(
|
||||
continue;
|
||||
}
|
||||
|
||||
value = (await config?.getContext(contextName, value, workflowContext, mode)) || value;
|
||||
const remoteValue = await config?.getContext(contextName, value, workflowContext, mode);
|
||||
if (remoteValue) {
|
||||
value = remoteValue;
|
||||
} else if (contextName === "secrets" || contextName === "vars") {
|
||||
// Without a context provider to fetch remote secrets/vars, we can't know
|
||||
// what values exist, so mark the context as incomplete to avoid false
|
||||
// "Context access might be invalid" warnings
|
||||
value.complete = false;
|
||||
}
|
||||
|
||||
context.add(contextName, value, getDescription(RootContext, contextName));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Test validation behavior when no context providers are configured.
|
||||
*
|
||||
* When contextProviderConfig is not provided (or returns incomplete data),
|
||||
* we should skip validation for secrets/vars rather than showing false
|
||||
* positive "Context access might be invalid" warnings.
|
||||
*
|
||||
* This is important for offline/disconnected scenarios where API calls
|
||||
* to fetch secrets/vars are not possible.
|
||||
*/
|
||||
|
||||
import {validate} from "./validate";
|
||||
import {createDocument} from "./test-utils/document";
|
||||
import {clearCache} from "./utils/workflow-cache";
|
||||
|
||||
beforeEach(() => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe("validation without context providers", () => {
|
||||
describe("secrets context", () => {
|
||||
it("should not warn on secrets.GITHUB_TOKEN", async () => {
|
||||
const doc = createDocument(
|
||||
"wf.yaml",
|
||||
`
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "test"
|
||||
env:
|
||||
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
||||
`
|
||||
);
|
||||
const result = await validate(doc);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should not warn on custom secrets when no provider configured", async () => {
|
||||
const doc = createDocument(
|
||||
"wf.yaml",
|
||||
`
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "test"
|
||||
env:
|
||||
API_KEY: \${{ secrets.MY_API_KEY }}
|
||||
`
|
||||
);
|
||||
const result = await validate(doc);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should not warn on secrets with environment", async () => {
|
||||
const doc = createDocument(
|
||||
"wf.yaml",
|
||||
`
|
||||
on: push
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- run: echo "test"
|
||||
env:
|
||||
API_KEY: \${{ secrets.API_KEY }}
|
||||
`
|
||||
);
|
||||
const result = await validate(doc);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("vars context", () => {
|
||||
it("should not warn on vars when no provider configured", async () => {
|
||||
const doc = createDocument(
|
||||
"wf.yaml",
|
||||
`
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "\${{ vars.ENVIRONMENT }}"
|
||||
`
|
||||
);
|
||||
const result = await validate(doc);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should not warn on vars with environment", async () => {
|
||||
const doc = createDocument(
|
||||
"wf.yaml",
|
||||
`
|
||||
on: push
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- run: echo "\${{ vars.API_URL }}"
|
||||
`
|
||||
);
|
||||
const result = await validate(doc);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should not warn on vars with fallback pattern", async () => {
|
||||
const doc = createDocument(
|
||||
"wf.yaml",
|
||||
`
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "\${{ vars.OPTIONAL_VAR || 'default-value' }}"
|
||||
`
|
||||
);
|
||||
const result = await validate(doc);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("combined secrets and vars", () => {
|
||||
it("should not warn on workflow using both secrets and vars", async () => {
|
||||
const doc = createDocument(
|
||||
"wf.yaml",
|
||||
`
|
||||
on: push
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- run: |
|
||||
echo "Deploying to \${{ vars.API_URL }}"
|
||||
echo "Using region \${{ vars.AWS_REGION }}"
|
||||
env:
|
||||
API_KEY: \${{ secrets.API_KEY }}
|
||||
AWS_SECRET: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
`
|
||||
);
|
||||
const result = await validate(doc);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user