erge branch 'main' into thyeggman/reusable-workflow-hover
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-expressions",
|
||||
"version": "0.1.137",
|
||||
"version": "0.1.143",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
|
||||
@@ -9,6 +9,7 @@ export function isDescriptionDictionary(x: ExpressionData): x is DescriptionDict
|
||||
|
||||
export class DescriptionDictionary extends Dictionary {
|
||||
private readonly descriptions = new Map<string, string>();
|
||||
public complete: boolean = true;
|
||||
|
||||
constructor(...pairs: DescriptionPair[]) {
|
||||
super();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.137",
|
||||
"version": "0.1.143",
|
||||
"description": "Language server for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -38,8 +38,8 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.137",
|
||||
"@github/actions-workflow-parser": "^0.1.137",
|
||||
"@github/actions-languageservice": "^0.1.143",
|
||||
"@github/actions-workflow-parser": "^0.1.143",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
|
||||
@@ -24,9 +24,9 @@ import {getFileProvider} from "./file-provider";
|
||||
import {InitializationOptions, RepositoryContext} from "./initializationOptions";
|
||||
import {onCompletion} from "./on-completion";
|
||||
import {ReadFileRequest, Requests} from "./request";
|
||||
import {fetchActionMetadata} from "./utils/action-metadata";
|
||||
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);
|
||||
@@ -102,9 +102,9 @@ export function initConnection(connection: Connection) {
|
||||
const config: ValidationConfig = {
|
||||
valueProviderConfig: valueProviders(client, repoContext, cache),
|
||||
contextProviderConfig: contextProviders(client, repoContext, cache),
|
||||
getActionInputs: async action => {
|
||||
fetchActionMetadata: async action => {
|
||||
if (client) {
|
||||
return await getActionInputs(client, cache, action);
|
||||
return await fetchActionMetadata(client, cache, action);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -29,51 +29,55 @@ export async function getSecrets(
|
||||
}
|
||||
}
|
||||
|
||||
const secrets = await getRemoteSecrets(octokit, cache, repo, environmentName);
|
||||
|
||||
// Build combined map of secrets
|
||||
const secretsMap = new Map<
|
||||
string,
|
||||
{
|
||||
key: string;
|
||||
value: data.StringData;
|
||||
description?: string;
|
||||
}
|
||||
>();
|
||||
|
||||
secrets.orgSecrets.forEach(secret =>
|
||||
secretsMap.set(secret.value.toLowerCase(), {
|
||||
key: secret.value,
|
||||
value: new data.StringData("***"),
|
||||
description: "Organization secret"
|
||||
})
|
||||
);
|
||||
|
||||
// Override org secrets with repo secrets
|
||||
secrets.repoSecrets.forEach(secret =>
|
||||
secretsMap.set(secret.value.toLowerCase(), {
|
||||
key: secret.value,
|
||||
value: new data.StringData("***"),
|
||||
description: "Repository secret"
|
||||
})
|
||||
);
|
||||
|
||||
// Override repo secrets with environment secrets (if defined)
|
||||
secrets.environmentSecrets.forEach(secret =>
|
||||
secretsMap.set(secret.value.toLowerCase(), {
|
||||
key: secret.value,
|
||||
value: new data.StringData("***"),
|
||||
description: `Secret for environment \`${environmentName}\``
|
||||
})
|
||||
);
|
||||
|
||||
const secretsContext = defaultContext || new DescriptionDictionary();
|
||||
try {
|
||||
const secrets = await getRemoteSecrets(octokit, cache, repo, environmentName);
|
||||
|
||||
// Sort secrets by key and add to context
|
||||
Array.from(secretsMap.values())
|
||||
.sort((a, b) => a.key.localeCompare(b.key))
|
||||
.forEach(secret => secretsContext?.add(secret.key, secret.value, secret.description));
|
||||
// Build combined map of secrets
|
||||
const secretsMap = new Map<
|
||||
string,
|
||||
{
|
||||
key: string;
|
||||
value: data.StringData;
|
||||
description?: string;
|
||||
}
|
||||
>();
|
||||
|
||||
secrets.orgSecrets.forEach(secret =>
|
||||
secretsMap.set(secret.value.toLowerCase(), {
|
||||
key: secret.value,
|
||||
value: new data.StringData("***"),
|
||||
description: "Organization secret"
|
||||
})
|
||||
);
|
||||
|
||||
// Override org secrets with repo secrets
|
||||
secrets.repoSecrets.forEach(secret =>
|
||||
secretsMap.set(secret.value.toLowerCase(), {
|
||||
key: secret.value,
|
||||
value: new data.StringData("***"),
|
||||
description: "Repository secret"
|
||||
})
|
||||
);
|
||||
|
||||
// Override repo secrets with environment secrets (if defined)
|
||||
secrets.environmentSecrets.forEach(secret =>
|
||||
secretsMap.set(secret.value.toLowerCase(), {
|
||||
key: secret.value,
|
||||
value: new data.StringData("***"),
|
||||
description: `Secret for environment \`${environmentName}\``
|
||||
})
|
||||
);
|
||||
|
||||
// Sort secrets by key and add to context
|
||||
Array.from(secretsMap.values())
|
||||
.sort((a, b) => a.key.localeCompare(b.key))
|
||||
.forEach(secret => secretsContext?.add(secret.key, secret.value, secret.description));
|
||||
} catch (e: any) {
|
||||
if (e.status === 403 || e.status === 404) {
|
||||
secretsContext.complete = false;
|
||||
}
|
||||
}
|
||||
return secretsContext;
|
||||
}
|
||||
|
||||
@@ -88,9 +92,7 @@ async function getRemoteSecrets(
|
||||
orgSecrets: StringData[];
|
||||
}> {
|
||||
return {
|
||||
repoSecrets: await cache.get(`${repo.owner}/${repo.name}/secrets`, undefined, () =>
|
||||
fetchSecrets(octokit, repo.owner, repo.name)
|
||||
),
|
||||
repoSecrets: await fetchSecrets(octokit, repo.owner, repo.name),
|
||||
environmentSecrets:
|
||||
(environmentName &&
|
||||
(await cache.get(`${repo.owner}/${repo.name}/secrets/environment/${environmentName}`, undefined, () =>
|
||||
@@ -114,9 +116,8 @@ async function fetchSecrets(octokit: Octokit, owner: string, name: string): Prom
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("Failure to retrieve secrets: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchEnvironmentSecrets(
|
||||
@@ -136,9 +137,8 @@ async function fetchEnvironmentSecrets(
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("Failure to retrieve environment secrets: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchOrganizationSecrets(octokit: Octokit, repo: RepositoryContext): Promise<StringData[]> {
|
||||
@@ -155,7 +155,6 @@ async function fetchOrganizationSecrets(octokit: Octokit, repo: RepositoryContex
|
||||
return secrets.map(secret => new StringData(secret.name));
|
||||
} catch (e) {
|
||||
console.log("Failure to retrieve organization secrets: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -30,51 +30,55 @@ export async function getVariables(
|
||||
}
|
||||
}
|
||||
|
||||
const variables = await getRemoteVariables(octokit, cache, repo, environmentName);
|
||||
|
||||
// Build combined map of variables
|
||||
const variablesMap = new Map<
|
||||
string,
|
||||
{
|
||||
key: string;
|
||||
value: data.StringData;
|
||||
description?: string;
|
||||
}
|
||||
>();
|
||||
|
||||
variables.organizationVariables.forEach(variable =>
|
||||
variablesMap.set(variable.key.toLowerCase(), {
|
||||
key: variable.key,
|
||||
value: new data.StringData(variable.value.coerceString()),
|
||||
description: `${variable.value.coerceString()} - Organization variable`
|
||||
})
|
||||
);
|
||||
|
||||
// Override org variables with repo variables
|
||||
variables.repoVariables.forEach(variable =>
|
||||
variablesMap.set(variable.key.toLowerCase(), {
|
||||
key: variable.key,
|
||||
value: new data.StringData(variable.value.coerceString()),
|
||||
description: `${variable.value.coerceString()} - Repository variable`
|
||||
})
|
||||
);
|
||||
|
||||
// Override repo variables with environment veriables (if defined)
|
||||
variables.environmentVariables.forEach(variable =>
|
||||
variablesMap.set(variable.key.toLowerCase(), {
|
||||
key: variable.key,
|
||||
value: new data.StringData(variable.value.coerceString()),
|
||||
description: `${variable.value.coerceString()} - Variable for environment \`${environmentName}\``
|
||||
})
|
||||
);
|
||||
|
||||
const variablesContext = defaultContext || new DescriptionDictionary();
|
||||
try {
|
||||
const variables = await getRemoteVariables(octokit, cache, repo, environmentName);
|
||||
|
||||
// Sort variables by key and add to context
|
||||
Array.from(variablesMap.values())
|
||||
.sort((a, b) => a.key.localeCompare(b.key))
|
||||
.forEach(variable => variablesContext?.add(variable.key, variable.value, variable.description));
|
||||
// Build combined map of variables
|
||||
const variablesMap = new Map<
|
||||
string,
|
||||
{
|
||||
key: string;
|
||||
value: data.StringData;
|
||||
description?: string;
|
||||
}
|
||||
>();
|
||||
|
||||
variables.organizationVariables.forEach(variable =>
|
||||
variablesMap.set(variable.key.toLowerCase(), {
|
||||
key: variable.key,
|
||||
value: new data.StringData(variable.value.coerceString()),
|
||||
description: `${variable.value.coerceString()} - Organization variable`
|
||||
})
|
||||
);
|
||||
|
||||
// Override org variables with repo variables
|
||||
variables.repoVariables.forEach(variable =>
|
||||
variablesMap.set(variable.key.toLowerCase(), {
|
||||
key: variable.key,
|
||||
value: new data.StringData(variable.value.coerceString()),
|
||||
description: `${variable.value.coerceString()} - Repository variable`
|
||||
})
|
||||
);
|
||||
|
||||
// Override repo variables with environment veriables (if defined)
|
||||
variables.environmentVariables.forEach(variable =>
|
||||
variablesMap.set(variable.key.toLowerCase(), {
|
||||
key: variable.key,
|
||||
value: new data.StringData(variable.value.coerceString()),
|
||||
description: `${variable.value.coerceString()} - Variable for environment \`${environmentName}\``
|
||||
})
|
||||
);
|
||||
|
||||
// Sort variables by key and add to context
|
||||
Array.from(variablesMap.values())
|
||||
.sort((a, b) => a.key.localeCompare(b.key))
|
||||
.forEach(variable => variablesContext?.add(variable.key, variable.value, variable.description));
|
||||
} catch (e: any) {
|
||||
if (e.status === 403 || e.status === 404) {
|
||||
variablesContext.complete = false;
|
||||
}
|
||||
}
|
||||
return variablesContext;
|
||||
}
|
||||
|
||||
@@ -121,9 +125,8 @@ async function fetchVariables(octokit: Octokit, owner: string, name: string): Pr
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("Failure to retrieve variables: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchEnvironmentVariables(
|
||||
@@ -146,9 +149,8 @@ async function fetchEnvironmentVariables(
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("Failure to retrieve environment variables: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchOrganizationVariables(octokit: Octokit, repo: RepositoryContext): Promise<Pair[]> {
|
||||
@@ -170,7 +172,6 @@ async function fetchOrganizationVariables(octokit: Octokit, repo: RepositoryCont
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Failure to retrieve organization variables: ", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import {ActionReference, ActionInputs, ActionOutputs, actionIdentifier} from "@github/actions-languageservice/action";
|
||||
import {ActionReference, ActionMetadata, actionIdentifier} from "@github/actions-languageservice/action";
|
||||
import {error} from "@github/actions-languageservice/log";
|
||||
import {Octokit, RestEndpointMethodTypes} from "@octokit/rest";
|
||||
import {parse} from "yaml";
|
||||
import {TTLCache} from "./cache";
|
||||
|
||||
export type ActionMetadata = {
|
||||
inputs?: ActionInputs;
|
||||
outputs?: ActionOutputs;
|
||||
};
|
||||
|
||||
export async function fetchActionMetadata(
|
||||
client: Octokit,
|
||||
cache: TTLCache,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.137",
|
||||
"version": "0.1.143",
|
||||
"description": "Language service for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -38,8 +38,8 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.137",
|
||||
"@github/actions-workflow-parser": "^0.1.137",
|
||||
"@github/actions-expressions": "^0.1.143",
|
||||
"@github/actions-workflow-parser": "^0.1.143",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
"vscode-languageserver-types": "^3.17.2",
|
||||
"yaml": "^2.1.1"
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
|
||||
export type ActionMetadata = {
|
||||
inputs?: ActionInputs;
|
||||
outputs?: ActionOutputs;
|
||||
};
|
||||
|
||||
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs
|
||||
export type ActionInput = {
|
||||
description: string;
|
||||
|
||||
@@ -77,15 +77,10 @@ export async function complete(
|
||||
}
|
||||
|
||||
const {token, keyToken, parent, path} = findToken(newPos, result.value);
|
||||
const template = await convertWorkflowTemplate(
|
||||
result.context,
|
||||
result.value,
|
||||
ErrorPolicy.TryConversion,
|
||||
config?.fileProvider,
|
||||
{
|
||||
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0
|
||||
}
|
||||
);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, config?.fileProvider, {
|
||||
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0,
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
const workflowContext = getWorkflowContext(textDocument.uri, template, path);
|
||||
|
||||
// If we are inside an expression, take a different code-path. The workflow parser does not correctly create
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {StringData} from "@github/actions-expressions/data/string";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
import {testGetWorkflowContext} from "../test-utils/test-workflow-context";
|
||||
import {getNeedsContext} from "./needs";
|
||||
@@ -96,8 +97,13 @@ jobs:
|
||||
|
||||
const outputs = needs.get("outputs") as DescriptionDictionary;
|
||||
expect(outputs).toBeDefined();
|
||||
|
||||
expect(outputs.pairs().map(x => x.key)).toEqual(["build_id"]);
|
||||
expect(outputs.pairs()).toEqual([
|
||||
{
|
||||
key: "build_id",
|
||||
value: new StringData("my-build-id"),
|
||||
description: undefined
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("reusable job with outputs", async () => {
|
||||
@@ -123,6 +129,13 @@ jobs:
|
||||
expect(outputs).toBeDefined();
|
||||
|
||||
expect(outputs.pairs().map(x => x.key)).toEqual(["build_id"]);
|
||||
expect(outputs.pairs()).toEqual([
|
||||
{
|
||||
key: "build_id",
|
||||
value: new StringData("123"),
|
||||
description: "The resulting build ID"
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||
import {isScalar, isString} from "@github/actions-workflow-parser";
|
||||
import {isMapping, isScalar, isString} from "@github/actions-workflow-parser";
|
||||
import {isJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||
import {WorkflowJob} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/template-token";
|
||||
import {WorkflowContext} from "../context/workflow-context";
|
||||
|
||||
export function getNeedsContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
||||
@@ -44,9 +46,28 @@ function jobOutputs(job?: WorkflowJob): DescriptionDictionary {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Include the value for hover purposes
|
||||
const value = isScalar(output.value) ? new data.StringData(output.value.toDisplayString()) : new data.Null();
|
||||
d.add(output.key.value, value);
|
||||
d.add(output.key.value, ...jobOutput(job, output.value));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
function jobOutput(job: WorkflowJob, outputValue: TemplateToken): [data.ExpressionData, string | undefined] {
|
||||
if (isJob(job)) {
|
||||
// A regular workflow job won't have a description
|
||||
return isScalar(outputValue)
|
||||
? [new data.StringData(outputValue.toDisplayString()), undefined]
|
||||
: [new data.Null(), undefined];
|
||||
}
|
||||
|
||||
if (!isMapping(outputValue)) {
|
||||
return [new data.Null(), undefined];
|
||||
}
|
||||
|
||||
const description = outputValue.find("description");
|
||||
const value = outputValue.find("value");
|
||||
|
||||
return [
|
||||
value && isScalar(value) ? new data.StringData(value.toDisplayString()) : new data.Null(),
|
||||
description && isString(description) ? description.value : undefined
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ export async function documentLinks(document: TextDocument): Promise<DocumentLin
|
||||
return [];
|
||||
}
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
// Add links to referenced actions
|
||||
const actionLinks: DocumentLink[] = [];
|
||||
|
||||
@@ -110,7 +110,9 @@ async function hoverExpression(input: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
const workflowContext = getWorkflowContext(td.uri, template, []);
|
||||
const context = await getContext(allowedContext, contextProviderConfig, workflowContext, Mode.Completion);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {data} from "@github/actions-expressions";
|
||||
import {data, isDescriptionDictionary} from "@github/actions-expressions";
|
||||
import {isDictionary} from "@github/actions-expressions/data/dictionary";
|
||||
import {ExpressionData, Pair} from "@github/actions-expressions/data/expressiondata";
|
||||
|
||||
@@ -12,6 +12,7 @@ export class ErrorDictionary extends data.Dictionary {
|
||||
constructor(...pairs: Pair[]) {
|
||||
super(...pairs);
|
||||
}
|
||||
public complete: boolean = true;
|
||||
|
||||
get(key: string): ExpressionData | undefined {
|
||||
const value = super.get(key);
|
||||
@@ -19,12 +20,17 @@ export class ErrorDictionary extends data.Dictionary {
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new AccessError(`Invalid context access: ${key}`, key);
|
||||
if (this.complete) {
|
||||
throw new AccessError(`Invalid context access: ${key}`, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function wrapDictionary(d: data.Dictionary): ErrorDictionary {
|
||||
const e = new ErrorDictionary();
|
||||
if (isDescriptionDictionary(d)) {
|
||||
e.complete = d.complete;
|
||||
}
|
||||
|
||||
for (const {key, value} of d.pairs()) {
|
||||
if (isDictionary(value)) {
|
||||
|
||||
@@ -61,7 +61,9 @@ export async function hover(document: TextDocument, position: Position, config?:
|
||||
const allowedContext = tokenDefinitionInfo.allowedContext || [];
|
||||
const {namedContexts, functions} = splitAllowedContext(allowedContext);
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
const workflowContext = getWorkflowContext(document.uri, template, tokenResult.path);
|
||||
const context = await getContext(namedContexts, config?.contextProviderConfig, workflowContext, Mode.Completion);
|
||||
|
||||
@@ -116,15 +118,9 @@ async function getDescription(
|
||||
return defaultDescription;
|
||||
}
|
||||
|
||||
const template = await convertWorkflowTemplate(
|
||||
result.context,
|
||||
result.value,
|
||||
ErrorPolicy.TryConversion,
|
||||
config?.fileProvider,
|
||||
{
|
||||
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0
|
||||
}
|
||||
);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, config?.fileProvider, {
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
const workflowContext = getWorkflowContext(document.uri, template, path);
|
||||
const description = await config.descriptionProvider.getDescription(workflowContext, token, path, template);
|
||||
return description || defaultDescription;
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function testGetWorkflowContext(input: string): Promise<WorkflowCon
|
||||
let template: WorkflowTemplate | undefined;
|
||||
|
||||
if (result.value) {
|
||||
template = await convertWorkflowTemplate(result.context, result.value, undefined, testFileProvider, {
|
||||
template = await convertWorkflowTemplate(result.context, result.value, testFileProvider, {
|
||||
fetchReusableWorkflowDepth: 1
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function validateAction(
|
||||
step: Step | undefined,
|
||||
config: ValidationConfig | undefined
|
||||
): Promise<void> {
|
||||
if (!isMapping(stepToken) || !step || !isActionStep(step) || !config?.getActionInputs) {
|
||||
if (!isMapping(stepToken) || !step || !isActionStep(step) || !config?.fetchActionMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ export async function validateAction(
|
||||
return;
|
||||
}
|
||||
|
||||
const actionInputs = await config.getActionInputs(action);
|
||||
if (actionInputs === undefined) {
|
||||
const actionMetadata = await config.fetchActionMetadata(action);
|
||||
if (actionMetadata === undefined) {
|
||||
diagnostics.push({
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: mapRange(step.uses.range),
|
||||
@@ -50,6 +50,11 @@ export async function validateAction(
|
||||
}
|
||||
}
|
||||
|
||||
const actionInputs = actionMetadata.inputs;
|
||||
if (actionInputs === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [input, inputToken] of stepInputs) {
|
||||
if (!actionInputs[input]) {
|
||||
diagnostics.push({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
||||
import {ActionInput, ActionReference} from "./action";
|
||||
import {ActionMetadata, ActionReference} from "./action";
|
||||
import {registerLogger} from "./log";
|
||||
import {createDocument} from "./test-utils/document";
|
||||
import {TestLogger} from "./test-utils/logger";
|
||||
@@ -9,53 +9,63 @@ import {ValueProviderKind} from "./value-providers/config";
|
||||
registerLogger(new TestLogger());
|
||||
|
||||
const validationConfig: ValidationConfig = {
|
||||
getActionInputs: async (ref: ActionReference) => {
|
||||
let inputs: Record<string, ActionInput> | undefined = undefined;
|
||||
fetchActionMetadata: async (ref: ActionReference) => {
|
||||
let metadata: ActionMetadata | undefined = undefined;
|
||||
switch (ref.owner + "/" + ref.name + "@" + ref.ref) {
|
||||
case "actions/checkout@v3":
|
||||
inputs = {
|
||||
repository: {
|
||||
description: "Repository name with owner",
|
||||
default: "${{ github.repository }}"
|
||||
metadata = {
|
||||
inputs: {
|
||||
repository: {
|
||||
description: "Repository name with owner",
|
||||
default: "${{ github.repository }}"
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "actions/setup-node@v1":
|
||||
inputs = {
|
||||
version: {
|
||||
description: "Deprecated. Use node-version instead. Will not be supported after October 1, 2019",
|
||||
deprecationMessage:
|
||||
"The version property will not be supported after October 1, 2019. Use node-version instead"
|
||||
metadata = {
|
||||
inputs: {
|
||||
version: {
|
||||
description: "Deprecated. Use node-version instead. Will not be supported after October 1, 2019",
|
||||
deprecationMessage:
|
||||
"The version property will not be supported after October 1, 2019. Use node-version instead"
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "actions/deploy-pages@main":
|
||||
inputs = {
|
||||
token: {
|
||||
required: true,
|
||||
description: "token to use",
|
||||
default: "${{ github.token }}"
|
||||
metadata = {
|
||||
inputs: {
|
||||
token: {
|
||||
required: true,
|
||||
description: "token to use",
|
||||
default: "${{ github.token }}"
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "actions/cache@v1":
|
||||
inputs = {
|
||||
path: {
|
||||
description: "A directory to store and save the cache",
|
||||
required: true
|
||||
},
|
||||
key: {
|
||||
description: "An explicit key for restoring and saving the cache",
|
||||
required: true
|
||||
},
|
||||
"restore-keys": {
|
||||
description: "An ordered list of keys to use for restoring the cache if no cache hit occurred for key",
|
||||
required: false
|
||||
metadata = {
|
||||
inputs: {
|
||||
path: {
|
||||
description: "A directory to store and save the cache",
|
||||
required: true
|
||||
},
|
||||
key: {
|
||||
description: "An explicit key for restoring and saving the cache",
|
||||
required: true
|
||||
},
|
||||
"restore-keys": {
|
||||
description: "An ordered list of keys to use for restoring the cache if no cache hit occurred for key",
|
||||
required: false
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "actions/action-no-input@v1":
|
||||
metadata = {};
|
||||
}
|
||||
return inputs;
|
||||
return metadata;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,6 +113,20 @@ jobs:
|
||||
]);
|
||||
});
|
||||
|
||||
it("action does not define inputs", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/action-no-input@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input), validationConfig);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("invalid input", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {data, DescriptionDictionary} from "@github/actions-expressions/.";
|
||||
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
||||
import {ContextProviderConfig} from "./context-providers/config";
|
||||
import {registerLogger} from "./log";
|
||||
import {createDocument} from "./test-utils/document";
|
||||
import {TestLogger} from "./test-utils/logger";
|
||||
import {validate} from "./validate";
|
||||
import {validate, ValidationConfig} from "./validate";
|
||||
|
||||
registerLogger(new TestLogger());
|
||||
|
||||
@@ -39,6 +41,58 @@ jobs:
|
||||
]);
|
||||
});
|
||||
|
||||
it("partial skip access invalid context on incomplete", async () => {
|
||||
const contextProviderConfig: ContextProviderConfig = {
|
||||
getContext: async (context: string) => {
|
||||
switch (context) {
|
||||
case "secrets":
|
||||
const dict = new DescriptionDictionary();
|
||||
dict.complete = false;
|
||||
return dict;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const validationConfig: ValidationConfig = {
|
||||
contextProviderConfig: contextProviderConfig
|
||||
};
|
||||
|
||||
const result = await validate(
|
||||
createDocument(
|
||||
"wf.yaml",
|
||||
`on: push
|
||||
run-name: name-\${{ github.does-not-exist }}
|
||||
env:
|
||||
secret: \${{ secrets.secret-not-exist }}
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo`
|
||||
),
|
||||
validationConfig
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Context access might be invalid: does-not-exist",
|
||||
range: {
|
||||
end: {
|
||||
character: 43,
|
||||
line: 1
|
||||
},
|
||||
start: {
|
||||
character: 15,
|
||||
line: 1
|
||||
}
|
||||
},
|
||||
severity: DiagnosticSeverity.Warning
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("access invalid nested context field", async () => {
|
||||
const result = await validate(
|
||||
createDocument(
|
||||
|
||||
@@ -17,7 +17,7 @@ import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {FileProvider} from "@github/actions-workflow-parser/workflows/file-provider";
|
||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
|
||||
import {ActionInputs, ActionReference} from "./action";
|
||||
import {ActionMetadata, ActionReference} from "./action";
|
||||
|
||||
import {ContextProviderConfig} from "./context-providers/config";
|
||||
import {getContext, Mode} from "./context-providers/default";
|
||||
@@ -35,7 +35,7 @@ import {defaultValueProviders} from "./value-providers/default";
|
||||
export type ValidationConfig = {
|
||||
valueProviderConfig?: ValueProviderConfig;
|
||||
contextProviderConfig?: ContextProviderConfig;
|
||||
getActionInputs?(action: ActionReference): Promise<ActionInputs | undefined>;
|
||||
fetchActionMetadata?(action: ActionReference): Promise<ActionMetadata | undefined>;
|
||||
fileProvider?: FileProvider;
|
||||
};
|
||||
|
||||
@@ -57,15 +57,10 @@ export async function validate(textDocument: TextDocument, config?: ValidationCo
|
||||
const result: ParseWorkflowResult = parseWorkflow(file, nullTrace);
|
||||
if (result.value) {
|
||||
// Errors will be updated in the context. Attempt to do the conversion anyway in order to give the user more information
|
||||
const template = await convertWorkflowTemplate(
|
||||
result.context,
|
||||
result.value,
|
||||
ErrorPolicy.TryConversion,
|
||||
config?.fileProvider,
|
||||
{
|
||||
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0
|
||||
}
|
||||
);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value, config?.fileProvider, {
|
||||
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0,
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
// Validate expressions and value providers
|
||||
await additionalValidations(diagnostics, textDocument.uri, template, result.value, config);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-workflow-parser",
|
||||
"version": "0.1.137",
|
||||
"version": "0.1.143",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
@@ -40,7 +40,7 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.137",
|
||||
"@github/actions-expressions": "^0.1.143",
|
||||
"cronstrue": "^2.21.0",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
|
||||
@@ -19,7 +19,9 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
events: {
|
||||
@@ -59,7 +61,9 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
events: {
|
||||
@@ -105,7 +109,9 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
errors: [
|
||||
@@ -152,7 +158,9 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
errors: [
|
||||
@@ -229,7 +237,9 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
expect(template.jobs).not.toBeUndefined();
|
||||
expect(template.jobs).toHaveLength(1);
|
||||
@@ -282,7 +292,9 @@ jobs:
|
||||
nullTrace
|
||||
);
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
expect(template.jobs).not.toBeUndefined();
|
||||
expect(template.jobs).toHaveLength(0);
|
||||
|
||||
@@ -31,31 +31,36 @@ export type WorkflowTemplateConverterOptions = {
|
||||
* Default: 0
|
||||
*/
|
||||
fetchReusableWorkflowDepth?: number;
|
||||
|
||||
/**
|
||||
* The error policy to use when converting the workflow.
|
||||
* By default, conversion will be skipped if there are errors in the {@link TemplateContext}.
|
||||
*/
|
||||
errorPolicy?: ErrorPolicy;
|
||||
};
|
||||
|
||||
const defaultOptions: Required<WorkflowTemplateConverterOptions> = {
|
||||
maxReusableWorkflowDepth: 4,
|
||||
fetchReusableWorkflowDepth: 0
|
||||
fetchReusableWorkflowDepth: 0,
|
||||
errorPolicy: ErrorPolicy.ReturnErrorsOnly
|
||||
};
|
||||
|
||||
export async function convertWorkflowTemplate(
|
||||
context: TemplateContext,
|
||||
root: TemplateToken,
|
||||
errorPolicy: ErrorPolicy = ErrorPolicy.ReturnErrorsOnly,
|
||||
fileProvider?: FileProvider,
|
||||
options: WorkflowTemplateConverterOptions = defaultOptions
|
||||
): Promise<WorkflowTemplate> {
|
||||
const result = {} as WorkflowTemplate;
|
||||
const opts = getOptionsWithDefaults(options);
|
||||
|
||||
if (context.errors.getErrors().length > 0 && errorPolicy === ErrorPolicy.ReturnErrorsOnly) {
|
||||
if (context.errors.getErrors().length > 0 && opts.errorPolicy === ErrorPolicy.ReturnErrorsOnly) {
|
||||
result.errors = context.errors.getErrors().map(x => ({
|
||||
Message: x.message
|
||||
}));
|
||||
return result;
|
||||
}
|
||||
|
||||
const opts = getOptionsWithDefaults(options);
|
||||
|
||||
if (fileProvider === undefined && opts.fetchReusableWorkflowDepth > 0) {
|
||||
context.error(root, new Error("A file provider is required to fetch reusable workflows"));
|
||||
}
|
||||
@@ -136,6 +141,7 @@ function getOptionsWithDefaults(options: WorkflowTemplateConverterOptions): Requ
|
||||
fetchReusableWorkflowDepth:
|
||||
options.fetchReusableWorkflowDepth !== undefined
|
||||
? options.fetchReusableWorkflowDepth
|
||||
: defaultOptions.fetchReusableWorkflowDepth
|
||||
: defaultOptions.fetchReusableWorkflowDepth,
|
||||
errorPolicy: options.errorPolicy !== undefined ? options.errorPolicy : defaultOptions.errorPolicy
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token:
|
||||
let steps: Step[] = [];
|
||||
let workflowJobRef: StringToken | undefined;
|
||||
let workflowJobInputs: MappingToken | undefined;
|
||||
let inheritSecrets = false;
|
||||
let workflowJobSecrets: MappingToken | undefined;
|
||||
|
||||
for (const item of token) {
|
||||
const propertyName = item.key.assertString("job property name");
|
||||
@@ -95,6 +97,12 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token:
|
||||
case "with":
|
||||
workflowJobInputs = item.value.assertMapping("uses-with value");
|
||||
break;
|
||||
case "secrets":
|
||||
if (isString(item.value) && item.value.value === "inherit") {
|
||||
inheritSecrets = true;
|
||||
} else {
|
||||
workflowJobSecrets = item.value.assertMapping("uses-secrets value");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +116,9 @@ export function convertJob(context: TemplateContext, jobKey: StringToken, token:
|
||||
ref: workflowJobRef,
|
||||
"input-definitions": undefined,
|
||||
"input-values": workflowJobInputs,
|
||||
"secret-definitions": undefined,
|
||||
"secret-values": workflowJobSecrets,
|
||||
"inherit-secrets": inheritSecrets || undefined,
|
||||
outputs: undefined,
|
||||
concurrency,
|
||||
strategy
|
||||
|
||||
@@ -39,7 +39,7 @@ export function convertWorkflowJobInputs(context: TemplateContext, job: Reusable
|
||||
}
|
||||
}
|
||||
|
||||
function createTokenMap(mapping: MappingToken | undefined, description: string): TokenMap | undefined {
|
||||
export function createTokenMap(mapping: MappingToken | undefined, description: string): TokenMap | undefined {
|
||||
if (!mapping) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import {TemplateContext} from "../../../templates/template-context";
|
||||
import {NullToken} from "../../../templates/tokens";
|
||||
import {ReusableWorkflowJob} from "../../workflow-template";
|
||||
import {createTokenMap} from "./inputs";
|
||||
|
||||
export function convertWorkflowJobSecrets(context: TemplateContext, job: ReusableWorkflowJob) {
|
||||
// No validation if job passes all secrets
|
||||
if (!!job["inherit-secrets"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const secretDefinitions = createTokenMap(
|
||||
job["secret-definitions"]?.assertMapping("workflow job secret definitions"),
|
||||
"secrets"
|
||||
);
|
||||
|
||||
const secretValues = createTokenMap(job["secret-values"]?.assertMapping("workflow job secret values"), "secrets");
|
||||
|
||||
if (secretDefinitions !== undefined) {
|
||||
for (const [_, [name, value]] of secretDefinitions) {
|
||||
if (value instanceof NullToken) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const secretSpec = createTokenMap(value.assertMapping(`secret ${name}`), `secret ${name} key`)!;
|
||||
|
||||
const required = secretSpec.get("required")?.[1].assertBoolean(`secret ${name} required`).value;
|
||||
if (required) {
|
||||
if (secretValues == undefined || !secretValues.has(name.toLowerCase())) {
|
||||
context.error(job.ref, `Secret ${name} is required, but not provided while calling.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (secretValues !== undefined) {
|
||||
for (const [_, [name, value]] of secretValues) {
|
||||
if (!secretDefinitions?.has(name.toLowerCase())) {
|
||||
context.error(value, `Invalid secret, ${name} is not defined in the referenced workflow.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {TokenType} from "../../templates/tokens/types";
|
||||
import {ReusableWorkflowJob} from "../workflow-template";
|
||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||
import {convertWorkflowJobInputs} from "./job/inputs";
|
||||
import {convertWorkflowJobSecrets} from "./job/secrets";
|
||||
import {convertJobs} from "./jobs";
|
||||
|
||||
export function convertReferencedWorkflow(
|
||||
@@ -41,6 +42,7 @@ function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken
|
||||
const event = on.assertString("Reference workflow on value").value;
|
||||
if (event === "workflow_call") {
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobSecrets(context, job));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
@@ -52,6 +54,7 @@ function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken
|
||||
const event = eventToken.assertString(`Reference workflow on value ${eventToken}`).value;
|
||||
if (event === "workflow_call") {
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobSecrets(context, job));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +72,7 @@ function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken
|
||||
|
||||
if (pair.value.templateTokenType === TokenType.Null) {
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobSecrets(context, job));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,10 +87,15 @@ function convertReferencedWorkflowOn(context: TemplateContext, on: TemplateToken
|
||||
case "outputs":
|
||||
job.outputs = definition.value.assertMapping(`on-workflow_call-${definition.key}`);
|
||||
break;
|
||||
|
||||
case "secrets":
|
||||
job["secret-definitions"] = definition.value.assertMapping(`on-workflow_call-${definition.key}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobInputs(context, job));
|
||||
handleTemplateTokenErrors(tokenForErrors, context, undefined, () => convertWorkflowJobSecrets(context, job));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -60,6 +60,9 @@ export type ReusableWorkflowJob = BaseJob & {
|
||||
ref: StringToken;
|
||||
"input-definitions"?: MappingToken;
|
||||
"input-values"?: MappingToken;
|
||||
"secret-definitions"?: MappingToken;
|
||||
"secret-values"?: MappingToken;
|
||||
"inherit-secrets"?: boolean;
|
||||
jobs?: WorkflowJob[];
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,6 @@ export class NullToken extends LiteralToken {
|
||||
}
|
||||
|
||||
public override toJSON() {
|
||||
return "null";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,5 @@ export type SerializedToken =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
@@ -85,7 +85,6 @@ describe("x-lang tests", () => {
|
||||
const workflowTemplate = await convertWorkflowTemplate(
|
||||
parseResult.context,
|
||||
parseResult.value!,
|
||||
undefined,
|
||||
testFileProvider,
|
||||
{
|
||||
fetchReusableWorkflowDepth: 1
|
||||
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
-1
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
Vendored
-1
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- Go
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
include-source: false # Drop file/line/col from output
|
||||
skip:
|
||||
- TypeScript
|
||||
---
|
||||
on: push
|
||||
jobs:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "browser-playground",
|
||||
"version": "0.1.137",
|
||||
"version": "0.1.143",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.137",
|
||||
"@github/actions-languageserver": "^0.1.143",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
"monaco-languageclient": "^4.0.3",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.137"
|
||||
"version": "0.1.143"
|
||||
}
|
||||
|
||||
Generated
+17
-17
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"actions-expressions": {
|
||||
"name": "@github/actions-expressions",
|
||||
"version": "0.1.137",
|
||||
"version": "0.1.143",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.0.3",
|
||||
@@ -34,11 +34,11 @@
|
||||
},
|
||||
"actions-languageserver": {
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.137",
|
||||
"version": "0.1.143",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.137",
|
||||
"@github/actions-workflow-parser": "^0.1.137",
|
||||
"@github/actions-languageservice": "^0.1.143",
|
||||
"@github/actions-workflow-parser": "^0.1.143",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
@@ -59,11 +59,11 @@
|
||||
},
|
||||
"actions-languageservice": {
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.137",
|
||||
"version": "0.1.143",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.137",
|
||||
"@github/actions-workflow-parser": "^0.1.137",
|
||||
"@github/actions-expressions": "^0.1.143",
|
||||
"@github/actions-workflow-parser": "^0.1.143",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
"vscode-languageserver-types": "^3.17.2",
|
||||
"yaml": "^2.1.1"
|
||||
@@ -82,10 +82,10 @@
|
||||
},
|
||||
"actions-workflow-parser": {
|
||||
"name": "@github/actions-workflow-parser",
|
||||
"version": "0.1.137",
|
||||
"version": "0.1.143",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.137",
|
||||
"@github/actions-expressions": "^0.1.143",
|
||||
"cronstrue": "^2.21.0",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
@@ -105,10 +105,10 @@
|
||||
}
|
||||
},
|
||||
"browser-playground": {
|
||||
"version": "0.1.137",
|
||||
"version": "0.1.143",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.137",
|
||||
"@github/actions-languageserver": "^0.1.143",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
"monaco-languageclient": "^4.0.3",
|
||||
@@ -14570,8 +14570,8 @@
|
||||
"@github/actions-languageserver": {
|
||||
"version": "file:actions-languageserver",
|
||||
"requires": {
|
||||
"@github/actions-languageservice": "^0.1.137",
|
||||
"@github/actions-workflow-parser": "^0.1.137",
|
||||
"@github/actions-languageservice": "^0.1.143",
|
||||
"@github/actions-workflow-parser": "^0.1.143",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"@types/jest": "^29.0.3",
|
||||
"fetch-mock": "^9.11.0",
|
||||
@@ -14588,8 +14588,8 @@
|
||||
"@github/actions-languageservice": {
|
||||
"version": "file:actions-languageservice",
|
||||
"requires": {
|
||||
"@github/actions-expressions": "^0.1.137",
|
||||
"@github/actions-workflow-parser": "^0.1.137",
|
||||
"@github/actions-expressions": "^0.1.143",
|
||||
"@github/actions-workflow-parser": "^0.1.143",
|
||||
"@types/jest": "^29.0.3",
|
||||
"jest": "^29.0.3",
|
||||
"prettier": "^2.8.3",
|
||||
@@ -14604,7 +14604,7 @@
|
||||
"@github/actions-workflow-parser": {
|
||||
"version": "file:actions-workflow-parser",
|
||||
"requires": {
|
||||
"@github/actions-expressions": "^0.1.137",
|
||||
"@github/actions-expressions": "^0.1.143",
|
||||
"@types/jest": "^29.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
@@ -17562,7 +17562,7 @@
|
||||
"browser-playground": {
|
||||
"version": "file:browser-playground",
|
||||
"requires": {
|
||||
"@github/actions-languageserver": "^0.1.137",
|
||||
"@github/actions-languageserver": "^0.1.143",
|
||||
"css-loader": "^6.7.2",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
*.md
|
||||
*.js
|
||||
*.json
|
||||
@@ -0,0 +1,9 @@
|
||||
# Webhooks
|
||||
|
||||
Based on https://github.com/github/docs/blob/main/src/rest/README.md, this script pulls the updated OpenAPI webhook schemas from https://github.com/github/rest-api-description/blob/main/descriptions/api.github.com/dereferenced/api.github.com.deref.json and generates a `webhooks.json` file for consumption by the language service, preserving any descriptions and summaries as markdown.
|
||||
|
||||
## Usage
|
||||
|
||||
```shell
|
||||
npm start
|
||||
```
|
||||
Generated
+288
@@ -0,0 +1,288 @@
|
||||
{
|
||||
"name": "webhooks",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "webhooks",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"node-fetch": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz",
|
||||
"integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz",
|
||||
"integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
|
||||
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz",
|
||||
"integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
|
||||
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "18.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz",
|
||||
"integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node-fetch": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz",
|
||||
"integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true
|
||||
},
|
||||
"fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"requires": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"form-data": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
|
||||
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"requires": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz",
|
||||
"integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==",
|
||||
"requires": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
}
|
||||
},
|
||||
"web-streams-polyfill": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
|
||||
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "webhooks",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc --build tsconfig.json",
|
||||
"start": "npm run build && node dist/index.js",
|
||||
"clean": "rimraf dist",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-fetch": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "^2.6.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// Based on https://github.com/github/docs/blob/9be5ebb4855be066ac22a3ae0dafb9d615c7dfb4/src/rest/scripts/utils/get-body-params.js
|
||||
|
||||
// If there is a oneOf at the top level, then we have to present just one
|
||||
// in the docs. We don't currently have a convention for showing more than one
|
||||
// set of input parameters in the docs. Having a top-level oneOf is also very
|
||||
// uncommon.
|
||||
// Currently there aren't very many operations that require this treatment.
|
||||
// As an example, the 'Add status check contexts' and 'Set status check contexts'
|
||||
// operations have a top-level oneOf.
|
||||
|
||||
async function getTopLevelOneOfProperty(schema: any) {
|
||||
if (!schema.oneOf) {
|
||||
throw new Error("Schema does not have a requestBody oneOf property defined");
|
||||
}
|
||||
if (!(Array.isArray(schema.oneOf) && schema.oneOf.length > 0)) {
|
||||
throw new Error("Schema requestBody oneOf property is not an array");
|
||||
}
|
||||
// When a oneOf exists but the `type` differs, the case has historically
|
||||
// been that the alternate option is an array, where the first option
|
||||
// is the array as a property of the object. We need to ensure that the
|
||||
// first option listed is the most comprehensive and preferred option.
|
||||
const firstOneOfObject = schema.oneOf[0];
|
||||
const allOneOfAreObjects = schema.oneOf.every((elem: any) => elem.type === "object");
|
||||
let required = firstOneOfObject.required || [];
|
||||
let properties = firstOneOfObject.properties || {};
|
||||
|
||||
// When all of the oneOf objects have the `type: object` we
|
||||
// need to display all of the parameters.
|
||||
// This merges all of the properties and required values.
|
||||
if (allOneOfAreObjects) {
|
||||
for (const each of schema.oneOf.slice(1)) {
|
||||
Object.assign(firstOneOfObject.properties, each.properties);
|
||||
required = firstOneOfObject.required.concat(each.required);
|
||||
}
|
||||
properties = firstOneOfObject.properties;
|
||||
}
|
||||
return {properties, required};
|
||||
}
|
||||
|
||||
// Gets the body parameters for a given schema recursively.
|
||||
export async function getBodyParams(schema: any, topLevel = false): Promise<any> {
|
||||
const bodyParametersParsed = [];
|
||||
const schemaObject = schema.oneOf && topLevel ? await getTopLevelOneOfProperty(schema) : schema;
|
||||
const properties: Record<string, any> = schemaObject.properties || {};
|
||||
const required = schemaObject.required || [];
|
||||
|
||||
// Most operation requestBody schemas are objects. When the type is an array,
|
||||
// there will not be properties on the `schema` object.
|
||||
if (topLevel && schema.type === "array") {
|
||||
const childParamsGroups = [];
|
||||
const arrayType = schema.items.type;
|
||||
const paramType = [schema.type];
|
||||
if (arrayType === "object") {
|
||||
childParamsGroups.push(...(await getBodyParams(schema.items, false)));
|
||||
} else {
|
||||
paramType.splice(paramType.indexOf("array"), 1, `array of ${arrayType}s`);
|
||||
}
|
||||
const paramDecorated = await getTransformedParam(schema, paramType, {
|
||||
required,
|
||||
topLevel,
|
||||
childParamsGroups
|
||||
});
|
||||
return [paramDecorated];
|
||||
}
|
||||
|
||||
for (const [paramKey, param] of Object.entries(properties)) {
|
||||
// OpenAPI 3.0 only had a single value for `type`. OpenAPI 3.1
|
||||
// will either be a single value or an array of values.
|
||||
// This makes type an array regardless of how many values the array
|
||||
// includes. This allows us to support 3.1 while remaining backwards
|
||||
// compatible with 3.0.
|
||||
const paramType = Array.isArray(param.type) ? param.type : [param.type];
|
||||
const additionalPropertiesType = param.additionalProperties
|
||||
? Array.isArray(param.additionalProperties.type)
|
||||
? param.additionalProperties.type
|
||||
: [param.additionalProperties.type]
|
||||
: [];
|
||||
const childParamsGroups = [];
|
||||
|
||||
// If the parameter is an array or object there may be child params
|
||||
// If the parameter has oneOf or additionalProperties, they need to be
|
||||
// recursively read too.
|
||||
|
||||
// There are a couple operations with additionalProperties, which allows
|
||||
// the api to define input parameters with the type dictionary. These are the only
|
||||
// two operations (at the time of adding this code) that use additionalProperties
|
||||
// Create a snapshot of dependencies for a repository
|
||||
// Update a gist
|
||||
if (param.additionalProperties && additionalPropertiesType.includes("object")) {
|
||||
const keyParam = {
|
||||
type: "object",
|
||||
name: "key",
|
||||
description: `A user-defined key to represent an item in \`${paramKey}\`.`,
|
||||
isRequired: param.required,
|
||||
enum: param.enum,
|
||||
default: param.default,
|
||||
childParamsGroups: [] as any[]
|
||||
};
|
||||
keyParam.childParamsGroups.push(...(await getBodyParams(param.additionalProperties, false)));
|
||||
childParamsGroups.push(keyParam);
|
||||
} else if (paramType && paramType.includes("array")) {
|
||||
const arrayType = param.items.type;
|
||||
if (arrayType) {
|
||||
paramType.splice(paramType.indexOf("array"), 1, `array of ${arrayType}s`);
|
||||
}
|
||||
if (arrayType === "object") {
|
||||
childParamsGroups.push(...(await getBodyParams(param.items, false)));
|
||||
}
|
||||
} else if (paramType && paramType.includes("object")) {
|
||||
childParamsGroups.push(...(await getBodyParams(param, false)));
|
||||
} else if (param && param.oneOf) {
|
||||
// get concatenated description and type
|
||||
const descriptions = [];
|
||||
for (const childParam of param.oneOf) {
|
||||
paramType.push(childParam.type);
|
||||
// If there is no parent description, create a description from
|
||||
// each type
|
||||
if (!param.description) {
|
||||
if (childParam.type === "array") {
|
||||
if (childParam.items.description) {
|
||||
descriptions.push({
|
||||
type: childParam.type,
|
||||
description: childParam.items.description
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (childParam.description) {
|
||||
descriptions.push({type: childParam.type, description: childParam.description});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Occasionally, there is no parent description and the description
|
||||
// is in the first child parameter.
|
||||
const oneOfDescriptions = descriptions.length ? descriptions[0].description : "";
|
||||
if (!param.description) param.description = oneOfDescriptions;
|
||||
|
||||
// This is a workaround for an operation that incorrectly defines allOf for a
|
||||
// body parameter. As a workaround, we will use the first object in the list of
|
||||
// the allOf array. Otherwise, fallback to the first item in the array.
|
||||
// This isn't ideal, and in the case of an actual allOf occurrence, we should
|
||||
// handle it differently by merging all of the properties. There is currently
|
||||
// only one occurrence for the operation id repos/update-information-about-pages-site
|
||||
// See Ecosystem API issue number #3332 for future plans to fix this in the OpenAPI
|
||||
} else if (param && param.anyOf && Object.keys(param).length === 1) {
|
||||
const firstObject = Object.values(param.anyOf).find((item: any) => item.type === "object") as {
|
||||
description: string;
|
||||
required: boolean;
|
||||
};
|
||||
if (firstObject) {
|
||||
paramType.push("object");
|
||||
param.description = firstObject.description;
|
||||
param.isRequired = firstObject.required;
|
||||
childParamsGroups.push(...(await getBodyParams(firstObject, false)));
|
||||
} else {
|
||||
paramType.push(param.anyOf[0].type);
|
||||
param.description = param.anyOf[0].description;
|
||||
param.isRequired = param.anyOf[0].required;
|
||||
}
|
||||
}
|
||||
|
||||
const paramDecorated = await getTransformedParam(param, paramType, {
|
||||
paramKey,
|
||||
required,
|
||||
childParamsGroups,
|
||||
topLevel
|
||||
});
|
||||
bodyParametersParsed.push(paramDecorated);
|
||||
}
|
||||
return bodyParametersParsed;
|
||||
}
|
||||
|
||||
type Param = {
|
||||
type: "string" | "string or null" | "number" | "boolean" | "array" | "object" | "object or null" | "integer or null";
|
||||
name: string;
|
||||
in: string;
|
||||
isRequired: boolean;
|
||||
description: string;
|
||||
childParamsGroups?: Param[];
|
||||
enum?: string[];
|
||||
default: any;
|
||||
};
|
||||
|
||||
async function getTransformedParam(param: any, paramType: any, props: any) {
|
||||
const {paramKey, required, childParamsGroups, topLevel} = props;
|
||||
const paramDecorated = {} as Param;
|
||||
|
||||
// Supports backwards compatibility for OpenAPI 3.0
|
||||
// In 3.1 a nullable type is part of the param.type array and
|
||||
// the property param.nullable does not exist.
|
||||
if (param.nullable) paramType.push("null");
|
||||
paramDecorated.type = paramType.filter(Boolean).join(" or ");
|
||||
paramDecorated.name = paramKey;
|
||||
if (topLevel) {
|
||||
paramDecorated.in = "body";
|
||||
}
|
||||
paramDecorated.description = param.description;
|
||||
if (required && required.includes(paramKey)) {
|
||||
paramDecorated.isRequired = true;
|
||||
}
|
||||
if (childParamsGroups && childParamsGroups.length > 0) {
|
||||
paramDecorated.childParamsGroups = childParamsGroups;
|
||||
}
|
||||
if (param.enum) {
|
||||
paramDecorated.enum = param.enum;
|
||||
}
|
||||
|
||||
// we also want to catch default values of `false` for booleans
|
||||
if (param.default !== undefined) {
|
||||
paramDecorated.default = param.default;
|
||||
}
|
||||
return paramDecorated;
|
||||
}
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
import fetch from "node-fetch";
|
||||
import {promises as fs} from "fs";
|
||||
import Webhook from "./webhook.js";
|
||||
|
||||
const SCHEMA_URL =
|
||||
"https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/dereferenced/api.github.com.deref.json";
|
||||
const OUTPUT_PATH = "src/webhooks.json";
|
||||
|
||||
const schema: any = await fetch(SCHEMA_URL).then(res => res.json());
|
||||
|
||||
const rawWebhooks = Object.values(schema.webhooks || schema["x-webhooks"]) as any[];
|
||||
if (!rawWebhooks) {
|
||||
throw new Error("No webhooks found in schema");
|
||||
}
|
||||
|
||||
const webhooks = [];
|
||||
for (const webhook of Object.values(rawWebhooks)) {
|
||||
webhooks.push(new Webhook(webhook.post));
|
||||
}
|
||||
|
||||
await Promise.all(webhooks.map(webhook => webhook.process()));
|
||||
|
||||
// The category is the name of the webhook
|
||||
const categorizedWebhooks: Record<string, Record<string, Webhook>> = {};
|
||||
for (const webhook of webhooks) {
|
||||
if (!webhook.action) webhook.action = "default";
|
||||
|
||||
if (categorizedWebhooks[webhook.category]) {
|
||||
categorizedWebhooks[webhook.category][webhook.action] = webhook;
|
||||
} else {
|
||||
categorizedWebhooks[webhook.category] = {};
|
||||
categorizedWebhooks[webhook.category][webhook.action] = webhook;
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(OUTPUT_PATH, JSON.stringify(categorizedWebhooks, null, 2));
|
||||
@@ -0,0 +1,57 @@
|
||||
// Based on https://github.com/github/docs/blob/main/src/rest/scripts/utils/webhook.js
|
||||
|
||||
import {getBodyParams} from "./get-body-params.js";
|
||||
|
||||
const NO_CHILD_PROPERTIES = ["action", "enterprise", "installation", "organization", "repository", "sender"];
|
||||
|
||||
export default class Webhook {
|
||||
public description: string;
|
||||
public summary: string;
|
||||
public bodyParameters: any[];
|
||||
public availability: string[];
|
||||
public action: string;
|
||||
public category: string;
|
||||
#webhook: any;
|
||||
|
||||
constructor(webhook: any) {
|
||||
this.#webhook = webhook;
|
||||
this.description = webhook.description;
|
||||
this.summary = webhook.summary;
|
||||
this.bodyParameters = [];
|
||||
this.availability = webhook["x-github"]["supported-webhook-types"];
|
||||
|
||||
// for some webhook action types (like some pull-request webhook types) the
|
||||
// schema properties are under a oneOf so we try and take the action from
|
||||
// the first one (the action will be the same across oneOf items)
|
||||
this.action =
|
||||
webhook.requestBody.content["application/json"]?.schema?.properties?.action?.enum?.[0] ||
|
||||
webhook.requestBody.content["application/json"]?.schema?.oneOf?.[0]?.properties?.action?.enum?.[0] ||
|
||||
null;
|
||||
|
||||
// The OpenAPI uses hyphens for the webhook names, but the webhooks
|
||||
// are sent using underscores (e.g. `branch_protection_rule` instead
|
||||
// of `branch-protection-rule`)
|
||||
this.category = webhook["x-github"].subcategory.replaceAll("-", "_");
|
||||
return this;
|
||||
}
|
||||
|
||||
public async process() {
|
||||
await this.renderBodyParameterDescriptions();
|
||||
}
|
||||
|
||||
private async renderBodyParameterDescriptions() {
|
||||
if (!this.#webhook.requestBody) return [];
|
||||
const schema = this.#webhook.requestBody.content["application/json"]?.schema || {};
|
||||
|
||||
const isObject = schema !== null && typeof schema === "object";
|
||||
|
||||
this.bodyParameters = isObject ? await getBodyParams(schema, true) : [];
|
||||
|
||||
// Removes the children of the common properties
|
||||
this.bodyParameters.forEach(param => {
|
||||
if (NO_CHILD_PROPERTIES.includes(param.name)) {
|
||||
param.childParamsGroups = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "ES2020",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noEmit": false,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user