Validate action inputs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { hover } from "@github/actions-languageservice/hover";
|
||||
import { registerLogger, setLogLevel } from "@github/actions-languageservice/log";
|
||||
import { validate } from "@github/actions-languageservice/validate";
|
||||
import {hover} from "@github/actions-languageservice/hover";
|
||||
import {registerLogger, setLogLevel} from "@github/actions-languageservice/log";
|
||||
import {validate, ValidationConfig} from "@github/actions-languageservice/validate";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {
|
||||
CompletionItem,
|
||||
Connection,
|
||||
@@ -12,12 +13,13 @@ import {
|
||||
TextDocuments,
|
||||
TextDocumentSyncKind
|
||||
} from "vscode-languageserver";
|
||||
import { TextDocument } from "vscode-languageserver-textdocument";
|
||||
import { contextProviders } from "./context-providers";
|
||||
import { InitializationOptions, RepositoryContext } from "./initializationOptions";
|
||||
import { onCompletion } from "./on-completion";
|
||||
import { TTLCache } from "./utils/cache";
|
||||
import { valueProviders } from "./value-providers";
|
||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {contextProviders} from "./context-providers";
|
||||
import {InitializationOptions, RepositoryContext} from "./initializationOptions";
|
||||
import {onCompletion} from "./on-completion";
|
||||
import {TTLCache} from "./utils/cache";
|
||||
import {valueProviders} from "./value-providers";
|
||||
import {getActionInputs} from "./value-providers/action-inputs";
|
||||
|
||||
export function initConnection(connection: Connection) {
|
||||
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
|
||||
@@ -82,11 +84,21 @@ export function initConnection(connection: Connection) {
|
||||
|
||||
async function validateTextDocument(textDocument: TextDocument): Promise<void> {
|
||||
const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri));
|
||||
const result = await validate(
|
||||
textDocument,
|
||||
valueProviders(sessionToken, repoContext, cache),
|
||||
contextProviders(sessionToken, repoContext, cache)
|
||||
);
|
||||
|
||||
const config: ValidationConfig = {
|
||||
valueProviderConfig: valueProviders(sessionToken, repoContext, cache),
|
||||
contextProviderConfig: contextProviders(sessionToken, repoContext, cache),
|
||||
getActionInputs: async action => {
|
||||
if (sessionToken) {
|
||||
const octokit = new Octokit({
|
||||
auth: sessionToken
|
||||
});
|
||||
return await getActionInputs(octokit, cache, action);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const result = await validate(textDocument, config);
|
||||
|
||||
connection.sendDiagnostics({uri: textDocument.uri, diagnostics: result});
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import {actionIdentifier, parseActionReference as parse} from "./action-reference";
|
||||
|
||||
describe("parseActionReference", () => {
|
||||
it("basic action", () => {
|
||||
expect(parse("actions/checkout@v2")).toEqual({
|
||||
owner: "actions",
|
||||
name: "checkout",
|
||||
ref: "v2"
|
||||
});
|
||||
});
|
||||
|
||||
it("action with reference to branch", () => {
|
||||
expect(parse("actions/checkout@main")).toEqual({
|
||||
owner: "actions",
|
||||
name: "checkout",
|
||||
ref: "main"
|
||||
});
|
||||
});
|
||||
|
||||
it("action with reference to branch with slashes", () => {
|
||||
expect(parse("actions/checkout@features/a")).toEqual({
|
||||
owner: "actions",
|
||||
name: "checkout",
|
||||
ref: "features/a"
|
||||
});
|
||||
});
|
||||
|
||||
it("action with reference to commit sha", () => {
|
||||
expect(parse("actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b")).toEqual({
|
||||
owner: "actions",
|
||||
name: "checkout",
|
||||
ref: "755da8c3cf115ac066823e79a1e1788f8940201b"
|
||||
});
|
||||
});
|
||||
|
||||
it("valid action with path", () => {
|
||||
expect(parse("actions/checkout/path/to/action@v2")).toEqual({
|
||||
owner: "actions",
|
||||
name: "checkout",
|
||||
ref: "v2",
|
||||
path: "path/to/action"
|
||||
});
|
||||
});
|
||||
|
||||
it("valid action with path and trailing slash", () => {
|
||||
expect(parse("actions/checkout/path/to/action/@v2")).toEqual({
|
||||
owner: "actions",
|
||||
name: "checkout",
|
||||
ref: "v2",
|
||||
path: "path/to/action"
|
||||
});
|
||||
});
|
||||
|
||||
it("local action", () => {
|
||||
expect(parse("./")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("local action with path", () => {
|
||||
expect(parse("./directory/")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Docker Hub action", () => {
|
||||
expect(parse("docker://alpine:3.8")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("GitHub Packages Container action", () => {
|
||||
expect(parse("docker://ghcr.io/OWNER/IMAGE_NAME")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("action with backslashes", () => {
|
||||
expect(parse("actions\\checkout\\path\\to\\action@v2")).toEqual({
|
||||
owner: "actions",
|
||||
name: "checkout",
|
||||
ref: "v2",
|
||||
path: "path/to/action"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("actionIdentifier", () => {
|
||||
it("basic action", () => {
|
||||
expect(
|
||||
actionIdentifier({
|
||||
owner: "actions",
|
||||
name: "checkout",
|
||||
ref: "v2"
|
||||
})
|
||||
).toEqual("actions/checkout/v2");
|
||||
});
|
||||
|
||||
it("action with path", () => {
|
||||
expect(
|
||||
actionIdentifier({
|
||||
owner: "actions",
|
||||
name: "checkout",
|
||||
ref: "v2",
|
||||
path: "path/to/action"
|
||||
})
|
||||
).toEqual("actions/checkout/v2/path/to/action");
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
export type ActionReference = {
|
||||
owner: string;
|
||||
name: string;
|
||||
ref: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses
|
||||
export function parseActionReference(uses: string): ActionReference | undefined {
|
||||
if (!uses || uses.startsWith("docker://") || uses.startsWith("./") || uses.startsWith(".\\")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [action, ref] = uses.split("@");
|
||||
const [owner, name, ...pathSegments] = action.split(/[\\/]/).filter(s => s.length > 0);
|
||||
if (!owner || !name) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (pathSegments.length === 0) {
|
||||
return {
|
||||
owner,
|
||||
name,
|
||||
ref
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
owner,
|
||||
name,
|
||||
ref,
|
||||
path: pathSegments.join("/")
|
||||
};
|
||||
}
|
||||
|
||||
export function actionIdentifier(ref: ActionReference): string {
|
||||
if (ref.path) {
|
||||
return `${ref.owner}/${ref.name}/${ref.ref}/${ref.path}`;
|
||||
}
|
||||
return `${ref.owner}/${ref.name}/${ref.ref}`;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {ValueProviderKind} from "@github/actions-languageservice/value-providers
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {RepositoryContext} from "./initializationOptions";
|
||||
import {TTLCache} from "./utils/cache";
|
||||
import {getActionInputs} from "./value-providers/action-inputs";
|
||||
import {getActionInputValues} from "./value-providers/action-inputs";
|
||||
import {getEnvironments} from "./value-providers/job-environment";
|
||||
import {getRunnerLabels} from "./value-providers/runs-on";
|
||||
|
||||
@@ -36,7 +36,7 @@ export function valueProviders(
|
||||
},
|
||||
"step-with": {
|
||||
kind: ValueProviderKind.AllowedValues,
|
||||
get: (context: WorkflowContext) => getActionInputs(octokit, cache, context)
|
||||
get: (context: WorkflowContext) => getActionInputValues(octokit, cache, context)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
import {
|
||||
actionIdentifier,
|
||||
ActionInputs,
|
||||
ActionReference,
|
||||
parseActionReference
|
||||
} from "@github/actions-languageservice/action";
|
||||
import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context";
|
||||
import {Value} from "@github/actions-languageservice/value-providers/config";
|
||||
import {isActionStep} from "@github/actions-workflow-parser/model/type-guards";
|
||||
import {Octokit, RestEndpointMethodTypes} from "@octokit/rest";
|
||||
import {parse} from "yaml";
|
||||
import {actionIdentifier, ActionReference, parseActionReference} from "../utils/action-reference";
|
||||
import {TTLCache} from "../utils/cache";
|
||||
|
||||
export async function getActionInputs(client: Octokit, cache: TTLCache, context: WorkflowContext): Promise<Value[]> {
|
||||
export async function getActionInputs(
|
||||
client: Octokit,
|
||||
cache: TTLCache,
|
||||
action: ActionReference
|
||||
): Promise<ActionInputs | undefined> {
|
||||
const inputs = await cache.get(`${actionIdentifier(action)}/action-inputs`, undefined, () =>
|
||||
fetchActionInputs(client, action)
|
||||
);
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
export async function getActionInputValues(
|
||||
client: Octokit,
|
||||
cache: TTLCache,
|
||||
context: WorkflowContext
|
||||
): Promise<Value[]> {
|
||||
if (!context.step || !isActionStep(context.step)) {
|
||||
return [];
|
||||
}
|
||||
@@ -15,18 +36,23 @@ export async function getActionInputs(client: Octokit, cache: TTLCache, context:
|
||||
if (!action) {
|
||||
return [];
|
||||
}
|
||||
const inputs = await getActionInputs(client, cache, action);
|
||||
if (!inputs) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const inputs = await cache.get(`${actionIdentifier(action)}/action-inputs`, undefined, () =>
|
||||
fetchActionInputs(client, action)
|
||||
);
|
||||
|
||||
return inputs;
|
||||
return Object.entries(inputs).map(([inputName, input]) => {
|
||||
return {
|
||||
label: inputName,
|
||||
description: input.description
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchActionInputs(client: Octokit, action: ActionReference): Promise<Value[]> {
|
||||
async function fetchActionInputs(client: Octokit, action: ActionReference): Promise<ActionInputs | undefined> {
|
||||
const metadata = await getActionMetadata(client, action);
|
||||
if (!metadata) {
|
||||
return [];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parseActionMetadata(metadata);
|
||||
@@ -71,7 +97,7 @@ async function getActionMetadata(client: Octokit, action: ActionReference): Prom
|
||||
}
|
||||
|
||||
type ActionMetadata = {
|
||||
inputs?: Record<string, ActionInput>;
|
||||
inputs?: ActionInputs;
|
||||
};
|
||||
|
||||
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs
|
||||
@@ -83,20 +109,7 @@ type ActionInput = {
|
||||
};
|
||||
|
||||
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
|
||||
async function parseActionMetadata(content: string): Promise<Value[]> {
|
||||
const inputs = new Array<Value>();
|
||||
|
||||
async function parseActionMetadata(content: string): Promise<ActionInputs> {
|
||||
const metadata: ActionMetadata = parse(content);
|
||||
if (metadata.inputs === undefined) {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
for (const [name, input] of Object.entries(metadata.inputs)) {
|
||||
inputs.push({
|
||||
label: name,
|
||||
description: input.description
|
||||
});
|
||||
}
|
||||
|
||||
return inputs;
|
||||
return metadata.inputs ?? {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user