Fixes https://github.com/github/vscode-github-actions/issues/305 Step outputs are dynamic - actions can generate outputs based on their inputs, so validating output property names is not feasible. This marks step output dictionaries as incomplete so that accessing any output property won't produce a warning. Known outputs from action.yml will still be suggested for autocomplete.
156 lines
6.8 KiB
TypeScript
156 lines
6.8 KiB
TypeScript
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";
|
|
|
|
import {createWorkflowContext} from "../test-utils/workflow-context";
|
|
import {TTLCache} from "../utils/cache";
|
|
import {getStepsContext} from "./steps";
|
|
|
|
const workflow = `
|
|
name: Caching Primes
|
|
|
|
on: push
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Cache Primes
|
|
id: cache-primes
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: prime-numbers
|
|
key: \${{ runner.os }}-primes
|
|
|
|
- name: Generate Prime Numbers
|
|
if: steps.cache-primes.outputs.cache-hit != 'true'
|
|
run: /generate-primes.sh -d prime-numbers
|
|
|
|
- name: Use Prime Numbers
|
|
run: /primes.sh -d prime-numbers
|
|
`;
|
|
|
|
// https://api.github.com/repos/actions/cache/contents/action.yml?ref=v3
|
|
const actionMetadata = {
|
|
name: "action.yml",
|
|
path: "action.yml",
|
|
sha: "3e158e3ee33f7e597ccf45c369a6e830dc8d43e3",
|
|
size: 946,
|
|
url: "https://api.github.com/repos/actions/cache/contents/action.yml?ref=v3",
|
|
html_url: "https://github.com/actions/cache/blob/v3/action.yml",
|
|
git_url: "https://api.github.com/repos/actions/cache/git/blobs/3e158e3ee33f7e597ccf45c369a6e830dc8d43e3",
|
|
download_url: "https://raw.githubusercontent.com/actions/cache/v3/action.yml",
|
|
type: "file",
|
|
content:
|
|
"bmFtZTogJ0NhY2hlJwpkZXNjcmlwdGlvbjogJ0NhY2hlIGFydGlmYWN0cyBs\naWtlIGRlcGVuZGVuY2llcyBhbmQgYnVpbGQgb3V0cHV0cyB0byBpbXByb3Zl\nIHdvcmtmbG93IGV4ZWN1dGlvbiB0aW1lJwphdXRob3I6ICdHaXRIdWInCmlu\ncHV0czoKICBwYXRoOgogICAgZGVzY3JpcHRpb246ICdBIGxpc3Qgb2YgZmls\nZXMsIGRpcmVjdG9yaWVzLCBhbmQgd2lsZGNhcmQgcGF0dGVybnMgdG8gY2Fj\naGUgYW5kIHJlc3RvcmUnCiAgICByZXF1aXJlZDogdHJ1ZQogIGtleToKICAg\nIGRlc2NyaXB0aW9uOiAnQW4gZXhwbGljaXQga2V5IGZvciByZXN0b3Jpbmcg\nYW5kIHNhdmluZyB0aGUgY2FjaGUnCiAgICByZXF1aXJlZDogdHJ1ZQogIHJl\nc3RvcmUta2V5czoKICAgIGRlc2NyaXB0aW9uOiAnQW4gb3JkZXJlZCBsaXN0\nIG9mIGtleXMgdG8gdXNlIGZvciByZXN0b3Jpbmcgc3RhbGUgY2FjaGUgaWYg\nbm8gY2FjaGUgaGl0IG9jY3VycmVkIGZvciBrZXkuIE5vdGUgYGNhY2hlLWhp\ndGAgcmV0dXJucyBmYWxzZSBpbiB0aGlzIGNhc2UuJwogICAgcmVxdWlyZWQ6\nIGZhbHNlCiAgdXBsb2FkLWNodW5rLXNpemU6CiAgICBkZXNjcmlwdGlvbjog\nJ1RoZSBjaHVuayBzaXplIHVzZWQgdG8gc3BsaXQgdXAgbGFyZ2UgZmlsZXMg\nZHVyaW5nIHVwbG9hZCwgaW4gYnl0ZXMnCiAgICByZXF1aXJlZDogZmFsc2UK\nb3V0cHV0czoKICBjYWNoZS1oaXQ6CiAgICBkZXNjcmlwdGlvbjogJ0EgYm9v\nbGVhbiB2YWx1ZSB0byBpbmRpY2F0ZSBhbiBleGFjdCBtYXRjaCB3YXMgZm91\nbmQgZm9yIHRoZSBwcmltYXJ5IGtleScKcnVuczoKICB1c2luZzogJ25vZGUx\nNicKICBtYWluOiAnZGlzdC9yZXN0b3JlL2luZGV4LmpzJwogIHBvc3Q6ICdk\naXN0L3NhdmUvaW5kZXguanMnCiAgcG9zdC1pZjogJ3N1Y2Nlc3MoKScKYnJh\nbmRpbmc6CiAgaWNvbjogJ2FyY2hpdmUnCiAgY29sb3I6ICdncmF5LWRhcmsn\nCg==\n",
|
|
encoding: "base64",
|
|
_links: {
|
|
self: "https://api.github.com/repos/actions/cache/contents/action.yml?ref=v3",
|
|
git: "https://api.github.com/repos/actions/cache/git/blobs/3e158e3ee33f7e597ccf45c369a6e830dc8d43e3",
|
|
html: "https://github.com/actions/cache/blob/v3/action.yml"
|
|
}
|
|
};
|
|
|
|
it("returns default context when job is undefined", async () => {
|
|
const workflowContext = await createWorkflowContext(workflow, undefined);
|
|
const defaultContext = getDefaultStepsContext(workflowContext);
|
|
|
|
const stepsContext = await getStepsContext(new Octokit(), new TTLCache(), defaultContext, workflowContext);
|
|
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()
|
|
.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
|
|
);
|
|
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: expectedOutputs
|
|
},
|
|
{
|
|
key: "conclusion",
|
|
value: new data.Null(),
|
|
description:
|
|
"The result of a completed step after [`continue-on-error`](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepscontinue-on-error) is applied. Possible values are `success`, `failure`, `cancelled`, or `skipped`. When a `continue-on-error` step fails, the `outcome` is `failure`, but the final conclusion is `success`."
|
|
},
|
|
{
|
|
key: "outcome",
|
|
value: new data.Null(),
|
|
description:
|
|
"The result of a completed step before [`continue-on-error`](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepscontinue-on-error) is applied. Possible values are `success`, `failure`, `cancelled`, or `skipped`. When a `continue-on-error` step fails, the `outcome` is `failure`, but the final conclusion is `success`."
|
|
}
|
|
)
|
|
})
|
|
);
|
|
});
|