diff --git a/actions-expressions/package.json b/actions-expressions/package.json index 643e2d5..691de77 100755 --- a/actions-expressions/package.json +++ b/actions-expressions/package.json @@ -1,6 +1,6 @@ { "name": "@github/actions-expressions", - "version": "0.1.137", + "version": "0.1.143", "license": "MIT", "type": "module", "source": "./src/index.ts", diff --git a/actions-expressions/src/completion/descriptionDictionary.ts b/actions-expressions/src/completion/descriptionDictionary.ts index 0fe6892..2aaa3dd 100644 --- a/actions-expressions/src/completion/descriptionDictionary.ts +++ b/actions-expressions/src/completion/descriptionDictionary.ts @@ -9,6 +9,7 @@ export function isDescriptionDictionary(x: ExpressionData): x is DescriptionDict export class DescriptionDictionary extends Dictionary { private readonly descriptions = new Map(); + public complete: boolean = true; constructor(...pairs: DescriptionPair[]) { super(); diff --git a/actions-languageserver/package.json b/actions-languageserver/package.json index ba927af..d5aa316 100644 --- a/actions-languageserver/package.json +++ b/actions-languageserver/package.json @@ -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", diff --git a/actions-languageserver/src/connection.ts b/actions-languageserver/src/connection.ts index 496c37d..712021e 100644 --- a/actions-languageserver/src/connection.ts +++ b/actions-languageserver/src/connection.ts @@ -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 = 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; diff --git a/actions-languageserver/src/context-providers/secrets.ts b/actions-languageserver/src/context-providers/secrets.ts index 61c6ca0..51372ec 100644 --- a/actions-languageserver/src/context-providers/secrets.ts +++ b/actions-languageserver/src/context-providers/secrets.ts @@ -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 { @@ -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 []; } diff --git a/actions-languageserver/src/context-providers/variables.ts b/actions-languageserver/src/context-providers/variables.ts index 52babbd..f78c650 100644 --- a/actions-languageserver/src/context-providers/variables.ts +++ b/actions-languageserver/src/context-providers/variables.ts @@ -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 { @@ -170,7 +172,6 @@ async function fetchOrganizationVariables(octokit: Octokit, repo: RepositoryCont }); } catch (e) { console.log("Failure to retrieve organization variables: ", e); + throw e; } - - return []; } diff --git a/actions-languageserver/src/utils/action-metadata.ts b/actions-languageserver/src/utils/action-metadata.ts index cf424f5..c23cfae 100644 --- a/actions-languageserver/src/utils/action-metadata.ts +++ b/actions-languageserver/src/utils/action-metadata.ts @@ -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, diff --git a/actions-languageservice/package.json b/actions-languageservice/package.json index b38ea88..02c2fc6 100644 --- a/actions-languageservice/package.json +++ b/actions-languageservice/package.json @@ -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" diff --git a/actions-languageservice/src/action.ts b/actions-languageservice/src/action.ts index 4c35918..b10a166 100644 --- a/actions-languageservice/src/action.ts +++ b/actions-languageservice/src/action.ts @@ -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; diff --git a/actions-languageservice/src/complete.ts b/actions-languageservice/src/complete.ts index c12bacd..8050570 100644 --- a/actions-languageservice/src/complete.ts +++ b/actions-languageservice/src/complete.ts @@ -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 diff --git a/actions-languageservice/src/context-providers/needs.test.ts b/actions-languageservice/src/context-providers/needs.test.ts index 729646c..a50fc30 100644 --- a/actions-languageservice/src/context-providers/needs.test.ts +++ b/actions-languageservice/src/context-providers/needs.test.ts @@ -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" + } + ]); }); }); }); diff --git a/actions-languageservice/src/context-providers/needs.ts b/actions-languageservice/src/context-providers/needs.ts index 3196e9f..d1baa25 100644 --- a/actions-languageservice/src/context-providers/needs.ts +++ b/actions-languageservice/src/context-providers/needs.ts @@ -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 + ]; +} diff --git a/actions-languageservice/src/document-links.ts b/actions-languageservice/src/document-links.ts index a22d358..42ec015 100644 --- a/actions-languageservice/src/document-links.ts +++ b/actions-languageservice/src/document-links.ts @@ -19,7 +19,9 @@ export async function documentLinks(document: TextDocument): Promise { - 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({ diff --git a/actions-languageservice/src/validate.actions.test.ts b/actions-languageservice/src/validate.actions.test.ts index 0b01bff..fa00c5e 100644 --- a/actions-languageservice/src/validate.actions.test.ts +++ b/actions-languageservice/src/validate.actions.test.ts @@ -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 | 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 diff --git a/actions-languageservice/src/validate.expressions.test.ts b/actions-languageservice/src/validate.expressions.test.ts index df2b245..3a5f54c 100644 --- a/actions-languageservice/src/validate.expressions.test.ts +++ b/actions-languageservice/src/validate.expressions.test.ts @@ -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( diff --git a/actions-languageservice/src/validate.ts b/actions-languageservice/src/validate.ts index 544a163..347300d 100644 --- a/actions-languageservice/src/validate.ts +++ b/actions-languageservice/src/validate.ts @@ -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; + fetchActionMetadata?(action: ActionReference): Promise; 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); diff --git a/actions-workflow-parser/package.json b/actions-workflow-parser/package.json index 1be7cbe..a24b791 100644 --- a/actions-workflow-parser/package.json +++ b/actions-workflow-parser/package.json @@ -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" }, diff --git a/actions-workflow-parser/src/model/convert.test.ts b/actions-workflow-parser/src/model/convert.test.ts index 78b1f68..5fb1ec8 100644 --- a/actions-workflow-parser/src/model/convert.test.ts +++ b/actions-workflow-parser/src/model/convert.test.ts @@ -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); diff --git a/actions-workflow-parser/src/model/convert.ts b/actions-workflow-parser/src/model/convert.ts index 944ba75..47e9373 100644 --- a/actions-workflow-parser/src/model/convert.ts +++ b/actions-workflow-parser/src/model/convert.ts @@ -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 = { 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 { 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 }; } diff --git a/actions-workflow-parser/src/model/converter/job.ts b/actions-workflow-parser/src/model/converter/job.ts index 5a7f335..a9d5163 100644 --- a/actions-workflow-parser/src/model/converter/job.ts +++ b/actions-workflow-parser/src/model/converter/job.ts @@ -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 diff --git a/actions-workflow-parser/src/model/converter/job/inputs.ts b/actions-workflow-parser/src/model/converter/job/inputs.ts index ebd9467..f8d6c57 100644 --- a/actions-workflow-parser/src/model/converter/job/inputs.ts +++ b/actions-workflow-parser/src/model/converter/job/inputs.ts @@ -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; } diff --git a/actions-workflow-parser/src/model/converter/job/secrets.ts b/actions-workflow-parser/src/model/converter/job/secrets.ts new file mode 100644 index 0000000..bc4c9a5 --- /dev/null +++ b/actions-workflow-parser/src/model/converter/job/secrets.ts @@ -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.`); + } + } + } +} diff --git a/actions-workflow-parser/src/model/converter/referencedWorkflow.ts b/actions-workflow-parser/src/model/converter/referencedWorkflow.ts index 9de9b49..00a1627 100644 --- a/actions-workflow-parser/src/model/converter/referencedWorkflow.ts +++ b/actions-workflow-parser/src/model/converter/referencedWorkflow.ts @@ -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; diff --git a/actions-workflow-parser/src/model/workflow-template.ts b/actions-workflow-parser/src/model/workflow-template.ts index 660fdea..ae0ecbb 100644 --- a/actions-workflow-parser/src/model/workflow-template.ts +++ b/actions-workflow-parser/src/model/workflow-template.ts @@ -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[]; }; diff --git a/actions-workflow-parser/src/templates/tokens/null-token.ts b/actions-workflow-parser/src/templates/tokens/null-token.ts index 5474c10..11fabda 100644 --- a/actions-workflow-parser/src/templates/tokens/null-token.ts +++ b/actions-workflow-parser/src/templates/tokens/null-token.ts @@ -23,6 +23,6 @@ export class NullToken extends LiteralToken { } public override toJSON() { - return "null"; + return null; } } diff --git a/actions-workflow-parser/src/templates/tokens/serialization.ts b/actions-workflow-parser/src/templates/tokens/serialization.ts index 5dd6f9d..3e10bbf 100644 --- a/actions-workflow-parser/src/templates/tokens/serialization.ts +++ b/actions-workflow-parser/src/templates/tokens/serialization.ts @@ -29,4 +29,5 @@ export type SerializedToken = | string | number | boolean + | null | undefined; diff --git a/actions-workflow-parser/src/xlang.test.ts b/actions-workflow-parser/src/xlang.test.ts index e9e2ab1..42a2b77 100644 --- a/actions-workflow-parser/src/xlang.test.ts +++ b/actions-workflow-parser/src/xlang.test.ts @@ -85,7 +85,6 @@ describe("x-lang tests", () => { const workflowTemplate = await convertWorkflowTemplate( parseResult.context, parseResult.value!, - undefined, testFileProvider, { fetchReusableWorkflowDepth: 1 diff --git a/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-required.yml b/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-required.yml index 6015a83..1bd2c84 100644 --- a/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-required.yml +++ b/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-required.yml @@ -1,7 +1,6 @@ include-source: false # Drop file/line/col from output skip: - Go - - TypeScript --- on: push jobs: diff --git a/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-undefined.yml b/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-undefined.yml index 97ad0e7..48d9816 100644 --- a/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-undefined.yml +++ b/actions-workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-undefined.yml @@ -1,7 +1,6 @@ include-source: false # Drop file/line/col from output skip: - Go - - TypeScript --- on: push jobs: diff --git a/actions-workflow-parser/testdata/reader/reusable-workflow-job-basic.yml b/actions-workflow-parser/testdata/reader/reusable-workflow-job-basic.yml index 9d9c070..37fd6ee 100644 --- a/actions-workflow-parser/testdata/reader/reusable-workflow-job-basic.yml +++ b/actions-workflow-parser/testdata/reader/reusable-workflow-job-basic.yml @@ -1,7 +1,6 @@ include-source: false # Drop file/line/col from output skip: - Go - - TypeScript --- on: push jobs: diff --git a/actions-workflow-parser/testdata/reader/reusable-workflow-secrets-secret-without-definition.yml b/actions-workflow-parser/testdata/reader/reusable-workflow-secrets-secret-without-definition.yml index 2c8e078..664f4fc 100644 --- a/actions-workflow-parser/testdata/reader/reusable-workflow-secrets-secret-without-definition.yml +++ b/actions-workflow-parser/testdata/reader/reusable-workflow-secrets-secret-without-definition.yml @@ -1,7 +1,6 @@ include-source: false # Drop file/line/col from output skip: - Go - - TypeScript --- on: push jobs: diff --git a/actions-workflow-parser/testdata/reader/reusable-workflow-secrets.yml b/actions-workflow-parser/testdata/reader/reusable-workflow-secrets.yml index 7514011..7714103 100644 --- a/actions-workflow-parser/testdata/reader/reusable-workflow-secrets.yml +++ b/actions-workflow-parser/testdata/reader/reusable-workflow-secrets.yml @@ -1,7 +1,6 @@ include-source: false # Drop file/line/col from output skip: - Go - - TypeScript --- on: push jobs: diff --git a/actions-workflow-parser/testdata/reader/yaml-schema-null.yml b/actions-workflow-parser/testdata/reader/yaml-schema-null.yml index 6349637..0e73a48 100644 --- a/actions-workflow-parser/testdata/reader/yaml-schema-null.yml +++ b/actions-workflow-parser/testdata/reader/yaml-schema-null.yml @@ -1,6 +1,4 @@ include-source: false # Drop file/line/col from output -skip: - - TypeScript --- on: push jobs: diff --git a/browser-playground/package.json b/browser-playground/package.json index 7a08d9e..60bbad1 100644 --- a/browser-playground/package.json +++ b/browser-playground/package.json @@ -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", diff --git a/lerna.json b/lerna.json index ed12df8..d7cd17f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useWorkspaces": true, - "version": "0.1.137" + "version": "0.1.143" } diff --git a/package-lock.json b/package-lock.json index e804b53..136d38f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/webhooks/.prettierignore b/webhooks/.prettierignore new file mode 100644 index 0000000..ce98533 --- /dev/null +++ b/webhooks/.prettierignore @@ -0,0 +1,5 @@ +node_modules +dist +*.md +*.js +*.json diff --git a/webhooks/README.md b/webhooks/README.md new file mode 100644 index 0000000..f046035 --- /dev/null +++ b/webhooks/README.md @@ -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 +``` diff --git a/webhooks/package-lock.json b/webhooks/package-lock.json new file mode 100644 index 0000000..e111cbf --- /dev/null +++ b/webhooks/package-lock.json @@ -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==" + } + } +} diff --git a/webhooks/package.json b/webhooks/package.json new file mode 100644 index 0000000..970d3e6 --- /dev/null +++ b/webhooks/package.json @@ -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" + } +} \ No newline at end of file diff --git a/webhooks/src/get-body-params.ts b/webhooks/src/get-body-params.ts new file mode 100644 index 0000000..9606d6b --- /dev/null +++ b/webhooks/src/get-body-params.ts @@ -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 { + const bodyParametersParsed = []; + const schemaObject = schema.oneOf && topLevel ? await getTopLevelOneOfProperty(schema) : schema; + const properties: Record = 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; +} diff --git a/webhooks/src/index.ts b/webhooks/src/index.ts new file mode 100755 index 0000000..24f4078 --- /dev/null +++ b/webhooks/src/index.ts @@ -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> = {}; +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)); diff --git a/webhooks/src/webhook.ts b/webhooks/src/webhook.ts new file mode 100644 index 0000000..5ee0e1a --- /dev/null +++ b/webhooks/src/webhook.ts @@ -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 = []; + } + }); + } +} diff --git a/webhooks/tsconfig.json b/webhooks/tsconfig.json new file mode 100644 index 0000000..a6a8dc8 --- /dev/null +++ b/webhooks/tsconfig.json @@ -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" + }, +}