Merge branch 'main' into elbrenn/secret-complete
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "browser-playground",
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.157",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.153",
|
||||
"@github/actions-languageserver": "^0.1.157",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
"monaco-languageclient": "^4.0.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# actions-expressions
|
||||
# actions/expressions
|
||||
|
||||
`actions-expressions` is a library to parse and evaluate GitHub Actions [expressions](https://docs.github.com/actions/learn-github-actions/expressions).
|
||||
`@actions/expressions` is a library to parse and evaluate GitHub Actions [expressions](https://docs.github.com/actions/learn-github-actions/expressions).
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-expressions",
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.157",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# actions-languageserver
|
||||
# actions/languageserver
|
||||
|
||||
`actions-languageserver` hosts the `actions-languageservice` and makes it available via the [language server protocol](https://microsoft.github.io/language-server-protocol/) (LSP) as a standalone language server.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.157",
|
||||
"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.153",
|
||||
"@github/actions-workflow-parser": "^0.1.153",
|
||||
"@github/actions-languageservice": "^0.1.157",
|
||||
"@github/actions-workflow-parser": "^0.1.157",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
|
||||
@@ -2,17 +2,22 @@ import {DescriptionProvider} from "@github/actions-languageservice/hover";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {getActionInputDescription} from "./description-providers/action-input";
|
||||
import {TTLCache} from "./utils/cache";
|
||||
import {getActionDescription} from "./description-providers/action-description";
|
||||
|
||||
export function descriptionProvider(client: Octokit | undefined, cache: TTLCache): DescriptionProvider {
|
||||
const getDescription: DescriptionProvider["getDescription"] = async (context, token, path) => {
|
||||
if (!client) {
|
||||
if (!client || !context.step) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parent = path[path.length - 1];
|
||||
if (context.step && parent.definition?.key === "step-with") {
|
||||
if (parent.definition?.key === "step-with") {
|
||||
return await getActionInputDescription(client, cache, context.step, token);
|
||||
}
|
||||
|
||||
if (parent.definition?.key === "step-uses") {
|
||||
return await getActionDescription(client, cache, context.step);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import fetchMock from "fetch-mock";
|
||||
import {createWorkflowContext} from "../test-utils/workflow-context";
|
||||
import {TTLCache} from "../utils/cache";
|
||||
import {getActionDescription} from "./action-description";
|
||||
import {actionsCheckoutMetadata} from "../test-utils/action-metadata";
|
||||
|
||||
const workflow = `
|
||||
name: Hello World
|
||||
on: workflow_dispatch
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
`;
|
||||
|
||||
async function getDescription(mock: fetchMock.FetchMockSandbox) {
|
||||
const workflowContext = await createWorkflowContext(workflow, "build", 0);
|
||||
|
||||
return await getActionDescription(
|
||||
new Octokit({
|
||||
request: {
|
||||
fetch: mock
|
||||
}
|
||||
}),
|
||||
new TTLCache(),
|
||||
workflowContext.step!
|
||||
);
|
||||
}
|
||||
|
||||
describe("action descriptions", () => {
|
||||
it("actions/checkout description", async () => {
|
||||
const mock = fetchMock
|
||||
.sandbox()
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3", actionsCheckoutMetadata);
|
||||
|
||||
expect(await getDescription(mock)).toEqual(
|
||||
"[**Checkout**](https://www.github.com/actions/checkout/tree/v3/)\n\nCheckout a Git repository at a particular version"
|
||||
);
|
||||
});
|
||||
|
||||
it("action does not exist", async () => {
|
||||
const mock = fetchMock
|
||||
.sandbox()
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3", 404)
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yaml?ref=v3", 404);
|
||||
|
||||
expect(await getDescription(mock)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("invalid metadata", async () => {
|
||||
const mock = fetchMock
|
||||
.sandbox()
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3", {});
|
||||
|
||||
expect(await getDescription(mock)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import {actionUrl, parseActionReference} from "@github/actions-languageservice/action";
|
||||
import {isActionStep} from "@github/actions-workflow-parser/model/type-guards";
|
||||
import {Step} from "@github/actions-workflow-parser/model/workflow-template";
|
||||
import {Octokit} from "@octokit/rest";
|
||||
import {fetchActionMetadata} from "../utils/action-metadata";
|
||||
import {TTLCache} from "../utils/cache";
|
||||
|
||||
export async function getActionDescription(client: Octokit, cache: TTLCache, step: Step): Promise<string | undefined> {
|
||||
if (!isActionStep(step)) {
|
||||
return undefined;
|
||||
}
|
||||
const action = parseActionReference(step.uses.value);
|
||||
if (!action) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const metadata = await fetchActionMetadata(client, cache, action);
|
||||
if (!metadata?.name || !metadata?.description) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `[**${metadata.name}**](${actionUrl(action)})\n\n${metadata.description}`;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import fetchMock from "fetch-mock";
|
||||
import {createWorkflowContext} from "../test-utils/workflow-context";
|
||||
import {TTLCache} from "../utils/cache";
|
||||
import {getActionInputDescription} from "./action-input";
|
||||
import {actionsCheckoutMetadata} from "../test-utils/action-metadata";
|
||||
|
||||
const workflow = `
|
||||
name: Hello World
|
||||
@@ -16,49 +17,6 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
`;
|
||||
|
||||
// A simplified version of the action.yml file from actions/checkout
|
||||
const actionMetadataContent = `
|
||||
name: 'Checkout'
|
||||
description: 'Checkout a Git repository at a particular version'
|
||||
inputs:
|
||||
repository:
|
||||
description: Repository name with owner. For example, actions/checkout
|
||||
default: \${{ github.repository }}
|
||||
ref:
|
||||
description: The branch, tag or SHA to checkout.
|
||||
required: true
|
||||
token:
|
||||
description: Personal access token (PAT) used to fetch the repository.
|
||||
default: \${{ github.token }}
|
||||
repo:
|
||||
description: 'Repository name with owner. For example, actions/checkout'
|
||||
deprecationMessage: 'Use repository instead'
|
||||
runs:
|
||||
using: node16
|
||||
main: dist/index.js
|
||||
post: dist/index.js
|
||||
`;
|
||||
|
||||
// Based on https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3
|
||||
const actionMetadata = {
|
||||
name: "action.yml",
|
||||
path: "action.yml",
|
||||
sha: "cab09ebd3a964aba67b57f9727f5f6fff1372b04",
|
||||
size: 3649,
|
||||
url: "https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3",
|
||||
html_url: "https://github.com/actions/checkout/blob/v3/action.yml",
|
||||
git_url: "https://api.github.com/repos/actions/checkout/git/blobs/cab09ebd3a964aba67b57f9727f5f6fff1372b04",
|
||||
download_url: "https://raw.githubusercontent.com/actions/checkout/v3/action.yml",
|
||||
type: "file",
|
||||
content: Buffer.from(actionMetadataContent).toString("base64"),
|
||||
encoding: "base64",
|
||||
_links: {
|
||||
self: "https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3",
|
||||
git: "https://api.github.com/repos/actions/checkout/git/blobs/cab09ebd3a964aba67b57f9727f5f6fff1372b04",
|
||||
html: "https://github.com/actions/checkout/blob/v3/action.yml"
|
||||
}
|
||||
};
|
||||
|
||||
async function getDescription(input: string, mock: fetchMock.FetchMockSandbox) {
|
||||
const workflowContext = await createWorkflowContext(workflow, "build", 0);
|
||||
|
||||
@@ -74,11 +32,11 @@ async function getDescription(input: string, mock: fetchMock.FetchMockSandbox) {
|
||||
);
|
||||
}
|
||||
|
||||
describe("action descriptions", () => {
|
||||
describe("action input descriptions", () => {
|
||||
it("optional input", async () => {
|
||||
const mock = fetchMock
|
||||
.sandbox()
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3", actionMetadata);
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3", actionsCheckoutMetadata);
|
||||
|
||||
expect(await getDescription("repository", mock)).toEqual(
|
||||
"Repository name with owner. For example, actions/checkout"
|
||||
@@ -88,7 +46,7 @@ describe("action descriptions", () => {
|
||||
it("required input", async () => {
|
||||
const mock = fetchMock
|
||||
.sandbox()
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3", actionMetadata);
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3", actionsCheckoutMetadata);
|
||||
|
||||
expect(await getDescription("ref", mock)).toEqual("The branch, tag or SHA to checkout.\n\n**Required**");
|
||||
});
|
||||
@@ -96,7 +54,7 @@ describe("action descriptions", () => {
|
||||
it("deprecated input", async () => {
|
||||
const mock = fetchMock
|
||||
.sandbox()
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3", actionMetadata);
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3", actionsCheckoutMetadata);
|
||||
|
||||
expect(await getDescription("repo", mock)).toEqual(
|
||||
"Repository name with owner. For example, actions/checkout\n\n**Deprecated**"
|
||||
@@ -106,7 +64,7 @@ describe("action descriptions", () => {
|
||||
it("invalid input", async () => {
|
||||
const mock = fetchMock
|
||||
.sandbox()
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3", actionMetadata);
|
||||
.getOnce("https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3", actionsCheckoutMetadata);
|
||||
|
||||
expect(await getDescription("typo", mock)).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// A simplified version of the action.yml file from actions/checkout
|
||||
const checkoutMetadataContent = `
|
||||
name: 'Checkout'
|
||||
description: 'Checkout a Git repository at a particular version'
|
||||
inputs:
|
||||
repository:
|
||||
description: Repository name with owner. For example, actions/checkout
|
||||
default: \${{ github.repository }}
|
||||
ref:
|
||||
description: The branch, tag or SHA to checkout.
|
||||
required: true
|
||||
token:
|
||||
description: Personal access token (PAT) used to fetch the repository.
|
||||
default: \${{ github.token }}
|
||||
repo:
|
||||
description: 'Repository name with owner. For example, actions/checkout'
|
||||
deprecationMessage: 'Use repository instead'
|
||||
runs:
|
||||
using: node16
|
||||
main: dist/index.js
|
||||
post: dist/index.js
|
||||
`;
|
||||
|
||||
// Based on https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3
|
||||
export const actionsCheckoutMetadata = {
|
||||
name: "action.yml",
|
||||
path: "action.yml",
|
||||
sha: "cab09ebd3a964aba67b57f9727f5f6fff1372b04",
|
||||
size: 3649,
|
||||
url: "https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3",
|
||||
html_url: "https://github.com/actions/checkout/blob/v3/action.yml",
|
||||
git_url: "https://api.github.com/repos/actions/checkout/git/blobs/cab09ebd3a964aba67b57f9727f5f6fff1372b04",
|
||||
download_url: "https://raw.githubusercontent.com/actions/checkout/v3/action.yml",
|
||||
type: "file",
|
||||
content: Buffer.from(checkoutMetadataContent).toString("base64"),
|
||||
encoding: "base64",
|
||||
_links: {
|
||||
self: "https://api.github.com/repos/actions/checkout/contents/action.yml?ref=v3",
|
||||
git: "https://api.github.com/repos/actions/checkout/git/blobs/cab09ebd3a964aba67b57f9727f5f6fff1372b04",
|
||||
html: "https://github.com/actions/checkout/blob/v3/action.yml"
|
||||
}
|
||||
};
|
||||
+38
-10
@@ -1,4 +1,4 @@
|
||||
# actions-languageservice
|
||||
# actions/languageservice
|
||||
|
||||
This package contains the logic for the GitHub Actions workflows language server.
|
||||
|
||||
@@ -22,14 +22,6 @@ The language service features use three sources of information:
|
||||
* _value providers_ which can dynamically add values to the schema, for example, the list of available labels for a repository when validating `runs-on`.
|
||||
* _context providers_ which can dynamically provide available contexts used in [expressions](https://docs.github.com/actions/reference/context-and-expression-syntax-for-github-actions#about-contexts-and-expressions). For example, the contents of the `github.event` context for a given workflow file.
|
||||
|
||||
##### Value Providers
|
||||
|
||||
TODO
|
||||
|
||||
##### Context Providers
|
||||
|
||||
TODO
|
||||
|
||||
#### Validation
|
||||
|
||||
Validate a workflow file, returns an array of [`Diagnostic`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic) objects.
|
||||
@@ -60,7 +52,43 @@ const hover = await hover(document, {line: 0, character: 1}); // { contents: { k
|
||||
|
||||
#### Auto-completion
|
||||
|
||||
TODO
|
||||
```typescript
|
||||
import {complete} from "@actions/languageservice";
|
||||
|
||||
const document = {
|
||||
uri: "file:///path/to/file",
|
||||
getText: () => `on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hello`
|
||||
};
|
||||
|
||||
// Trigger completion for `on: |`
|
||||
const suggestions = await complete(document, {line: 0, character: 4});
|
||||
```
|
||||
|
||||
will return
|
||||
|
||||
```jsonc
|
||||
[{
|
||||
"documentation": {
|
||||
"kind": "markdown",
|
||||
"value": "Runs your workflow when branch protection rules in the workflow repository are changed.",
|
||||
},
|
||||
"label": "branch_protection_rule",
|
||||
"textEdit": {
|
||||
"newText": "branch_protection_rule",
|
||||
"range": {
|
||||
"end": {"character": 4, "line": 0,},
|
||||
"start": {"character": 4, "line": 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
//... other events
|
||||
]
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.157",
|
||||
"description": "Language service for GitHub Actions",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -39,8 +39,8 @@
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.153",
|
||||
"@github/actions-workflow-parser": "^0.1.153",
|
||||
"@github/actions-expressions": "^0.1.157",
|
||||
"@github/actions-workflow-parser": "^0.1.157",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
"vscode-languageserver-types": "^3.17.2",
|
||||
"yaml": "^2.1.1"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions
|
||||
export type ActionMetadata = {
|
||||
name: string;
|
||||
description: string;
|
||||
inputs?: ActionInputs;
|
||||
outputs?: ActionOutputs;
|
||||
};
|
||||
@@ -63,3 +65,10 @@ export function actionIdentifier(ref: ActionReference): string {
|
||||
}
|
||||
return `${ref.owner}/${ref.name}/${ref.ref}`;
|
||||
}
|
||||
|
||||
export function actionUrl(actionRef: ActionReference): string {
|
||||
// TODO: Support base uri for GHES
|
||||
const gitHubBaseUri = "https://www.github.com/";
|
||||
|
||||
return `${gitHubBaseUri}${actionRef.owner}/${actionRef.name}/tree/${actionRef.ref}/${actionRef.path || ""}`;
|
||||
}
|
||||
|
||||
@@ -320,6 +320,37 @@ on:
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("null strings still give suggestions", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
one:
|
||||
runs-on: ubuntu-latest
|
||||
|:
|
||||
- uses: actions/checkout@v2
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).toHaveLength(16);
|
||||
});
|
||||
|
||||
it("complete from behind a colon will replace it", async () => {
|
||||
const input = `
|
||||
on: push
|
||||
jobs:
|
||||
one:
|
||||
runs-on: ubuntu-latest
|
||||
|:
|
||||
- uses: actions/checkout@v2
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input));
|
||||
expect(result).toHaveLength(16);
|
||||
let textEdit = result[0].textEdit as TextEdit;
|
||||
expect(textEdit.range).toEqual({
|
||||
start: {line: 5, character: 4},
|
||||
end: {line: 5, character: 5}
|
||||
});
|
||||
});
|
||||
|
||||
it("well known mapping keys have descriptions", async () => {
|
||||
const input = `
|
||||
o|
|
||||
|
||||
@@ -108,7 +108,19 @@ export async function complete(
|
||||
|
||||
// Get the length of the current word
|
||||
const val = line.match(/[\w_-]*$/)?.[0].length || 0;
|
||||
replaceRange = Range.create({line: position.line, character: position.character - val}, position);
|
||||
// Check if we need to remove a trailing colon
|
||||
const charAfterPos = textDocument.getText({
|
||||
start: {line: position.line, character: position.character},
|
||||
end: {line: position.line, character: position.character + 1}
|
||||
});
|
||||
if (charAfterPos === ":") {
|
||||
replaceRange = Range.create(
|
||||
{line: position.line, character: position.character - val},
|
||||
{line: position.line, character: position.character + 1}
|
||||
);
|
||||
} else {
|
||||
replaceRange = Range.create({line: position.line, character: position.character - val}, position);
|
||||
}
|
||||
}
|
||||
|
||||
return values.map(value => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {isJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||
import {DocumentLink} from "vscode-languageserver-types";
|
||||
import {parseActionReference} from "./action";
|
||||
import {actionUrl, parseActionReference} from "./action";
|
||||
import {nullTrace} from "./nulltrace";
|
||||
import {mapRange} from "./utils/range";
|
||||
|
||||
@@ -26,9 +26,6 @@ export async function documentLinks(document: TextDocument): Promise<DocumentLin
|
||||
// Add links to referenced actions
|
||||
const actionLinks: DocumentLink[] = [];
|
||||
|
||||
// TODO: Support base uri for GHES
|
||||
const gitHubBaseUri = "https://www.github.com/";
|
||||
|
||||
for (const job of template?.jobs || []) {
|
||||
if (!job || !isJob(job)) {
|
||||
continue;
|
||||
@@ -40,9 +37,7 @@ export async function documentLinks(document: TextDocument): Promise<DocumentLin
|
||||
continue;
|
||||
}
|
||||
|
||||
const url = `${gitHubBaseUri}${actionRef.owner}/${actionRef.name}/tree/${actionRef.ref}/${
|
||||
actionRef.path || ""
|
||||
}`;
|
||||
const url = actionUrl(actionRef);
|
||||
|
||||
actionLinks.push({
|
||||
range: mapRange(step.uses.range),
|
||||
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
- uses|: actions/checkout@v2
|
||||
`;
|
||||
|
||||
const result = await hover(...getPositionFromCursor(input), testHoverConfig("uses", "non-empty-string", undefined));
|
||||
const result = await hover(...getPositionFromCursor(input), testHoverConfig("uses", "step-uses", undefined));
|
||||
expect(result).not.toBeUndefined();
|
||||
expect(result?.contents).toEqual(
|
||||
"Selects an action to run as part of a step in your job. An action is a reusable unit of code. You can use an action defined in the same repository as the workflow, a public repository, or in a published Docker container image."
|
||||
|
||||
@@ -7,6 +7,6 @@ import {TemplateToken} from "@github/actions-workflow-parser/templates/tokens/in
|
||||
export function isPotentiallyExpression(token: TemplateToken): boolean {
|
||||
const isAlwaysExpression =
|
||||
token.definition?.definitionType === DefinitionType.String && (token.definition as StringDefinition).isExpression;
|
||||
const containsExpression = isString(token) && token.value.indexOf(OPEN_EXPRESSION) >= 0;
|
||||
const containsExpression = isString(token) && token.value != null && token.value.indexOf(OPEN_EXPRESSION) >= 0;
|
||||
return isAlwaysExpression || containsExpression;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ const validationConfig: ValidationConfig = {
|
||||
switch (ref.owner + "/" + ref.name + "@" + ref.ref) {
|
||||
case "actions/checkout@v3":
|
||||
metadata = {
|
||||
name: "Checkout",
|
||||
description: "Checkout a Git repository at a particular version",
|
||||
inputs: {
|
||||
repository: {
|
||||
description: "Repository name with owner",
|
||||
@@ -24,6 +26,9 @@ const validationConfig: ValidationConfig = {
|
||||
break;
|
||||
case "actions/setup-node@v1":
|
||||
metadata = {
|
||||
name: "Setup Node.js environment",
|
||||
description:
|
||||
"Setup a Node.js environment by adding problem matchers and optionally downloading and adding it to the PATH.",
|
||||
inputs: {
|
||||
version: {
|
||||
description: "Deprecated. Use node-version instead. Will not be supported after October 1, 2019",
|
||||
@@ -35,6 +40,8 @@ const validationConfig: ValidationConfig = {
|
||||
break;
|
||||
case "actions/deploy-pages@main":
|
||||
metadata = {
|
||||
name: "Deploy GitHub Pages site",
|
||||
description: "A GitHub Action to deploy an artifact as a GitHub Pages site",
|
||||
inputs: {
|
||||
token: {
|
||||
required: true,
|
||||
@@ -46,6 +53,8 @@ const validationConfig: ValidationConfig = {
|
||||
break;
|
||||
case "actions/cache@v1":
|
||||
metadata = {
|
||||
name: "Cache",
|
||||
description: "Cache artifacts like dependencies and build outputs to improve workflow execution time",
|
||||
inputs: {
|
||||
path: {
|
||||
description: "A directory to store and save the cache",
|
||||
@@ -63,7 +72,10 @@ const validationConfig: ValidationConfig = {
|
||||
};
|
||||
break;
|
||||
case "actions/action-no-input@v1":
|
||||
metadata = {};
|
||||
metadata = {
|
||||
name: "Action with no inputs",
|
||||
description: "An action with no inputs"
|
||||
};
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.1.153"
|
||||
"version": "0.1.157"
|
||||
}
|
||||
|
||||
Generated
+17
-17
@@ -111,10 +111,10 @@
|
||||
}
|
||||
},
|
||||
"browser-playground": {
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.157",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageserver": "^0.1.153",
|
||||
"@github/actions-languageserver": "^0.1.157",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
"monaco-languageclient": "^4.0.3",
|
||||
@@ -135,7 +135,7 @@
|
||||
},
|
||||
"expressions": {
|
||||
"name": "@github/actions-expressions",
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.157",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.0.3",
|
||||
@@ -151,11 +151,11 @@
|
||||
},
|
||||
"languageserver": {
|
||||
"name": "@github/actions-languageserver",
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.157",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-languageservice": "^0.1.153",
|
||||
"@github/actions-workflow-parser": "^0.1.153",
|
||||
"@github/actions-languageservice": "^0.1.157",
|
||||
"@github/actions-workflow-parser": "^0.1.157",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"vscode-languageserver": "^8.0.2",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
@@ -176,11 +176,11 @@
|
||||
},
|
||||
"languageservice": {
|
||||
"name": "@github/actions-languageservice",
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.157",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.153",
|
||||
"@github/actions-workflow-parser": "^0.1.153",
|
||||
"@github/actions-expressions": "^0.1.157",
|
||||
"@github/actions-workflow-parser": "^0.1.157",
|
||||
"vscode-languageserver-textdocument": "^1.0.7",
|
||||
"vscode-languageserver-types": "^3.17.2",
|
||||
"yaml": "^2.1.1"
|
||||
@@ -14265,10 +14265,10 @@
|
||||
},
|
||||
"workflow-parser": {
|
||||
"name": "@github/actions-workflow-parser",
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.157",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "^0.1.153",
|
||||
"@github/actions-expressions": "^0.1.157",
|
||||
"cronstrue": "^2.21.0",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
@@ -14827,8 +14827,8 @@
|
||||
"@github/actions-languageserver": {
|
||||
"version": "file:languageserver",
|
||||
"requires": {
|
||||
"@github/actions-languageservice": "^0.1.153",
|
||||
"@github/actions-workflow-parser": "^0.1.153",
|
||||
"@github/actions-languageservice": "^0.1.157",
|
||||
"@github/actions-workflow-parser": "^0.1.157",
|
||||
"@octokit/rest": "^19.0.7",
|
||||
"@types/jest": "^29.0.3",
|
||||
"fetch-mock": "^9.11.0",
|
||||
@@ -14845,8 +14845,8 @@
|
||||
"@github/actions-languageservice": {
|
||||
"version": "file:languageservice",
|
||||
"requires": {
|
||||
"@github/actions-expressions": "^0.1.153",
|
||||
"@github/actions-workflow-parser": "^0.1.153",
|
||||
"@github/actions-expressions": "^0.1.157",
|
||||
"@github/actions-workflow-parser": "^0.1.157",
|
||||
"@types/jest": "^29.0.3",
|
||||
"jest": "^29.0.3",
|
||||
"prettier": "^2.8.3",
|
||||
@@ -14863,7 +14863,7 @@
|
||||
"@github/actions-workflow-parser": {
|
||||
"version": "file:workflow-parser",
|
||||
"requires": {
|
||||
"@github/actions-expressions": "^0.1.153",
|
||||
"@github/actions-expressions": "^0.1.157",
|
||||
"@types/jest": "^29.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
@@ -17857,7 +17857,7 @@
|
||||
"browser-playground": {
|
||||
"version": "file:browser-playground",
|
||||
"requires": {
|
||||
"@github/actions-languageserver": "^0.1.153",
|
||||
"@github/actions-languageserver": "^0.1.157",
|
||||
"css-loader": "^6.7.2",
|
||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||
"monaco-editor-workers": "^0.34.2",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# actions/workflow-parser
|
||||
|
||||
`@actions/workflow-parser` is a library to parse GitHub Actions [workflows](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions).
|
||||
|
||||
## Installation
|
||||
|
||||
The [package](https://www.npmjs.com/package/@actions/workflow-parser) contains TypeScript types and compiled ECMAScript modules.
|
||||
|
||||
```bash
|
||||
npm install @actions/workflow-parser
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The parser is driven by a custom schema defined in [`worflows-v1.0.json`](./src/workflow-v1.0.json).
|
||||
|
||||
### Simple example
|
||||
|
||||
`parseWorkflow` parses the workflow YAML into an intermediate representation and validates that it conforms to the schema. Any parsing errors are returned in the `errors` property of the result context.
|
||||
|
||||
```typescript
|
||||
var trace: TraceWriter;
|
||||
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'hello'`
|
||||
},
|
||||
trace
|
||||
);
|
||||
```
|
||||
|
||||
`convertWorkflowTemplate` then takes that intermediate representation and converts it to a [`WorkflowTemplate`](./src/workflow-template.ts) object, which is a more convenient representation for working with workflows.
|
||||
|
||||
```typescript
|
||||
const workflowTemplate = await convertWorkflowTemplate(result.context, result.value);
|
||||
|
||||
// workflowTemplate.jobs[0].id === "build"
|
||||
// workflowTemplate.jobs[0].steps[0].run === "echo 'hello'"
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](../CONTRIBUTING.md) at the root of the repository for general guidelines and recommendations.
|
||||
|
||||
Similar to [`actions/expressions](../expressions/), this project is just one of multiple implementations of the GitHub Actions workflow parser. We therefore cannot accept contributions that significantly modify the schema or the overall behavior of the parser. If you would like to propose changes to Actions itself, please use our [Community Forum](https://github.com/community/community/discussions/categories/actions-and-packages).
|
||||
|
||||
If you do want to contribute, please run [prettier](https://prettier.io/) to format your code and add unit tests as appropriate before submitting your PR. [./testdata](./testdata) contains test cases that all implementations should pass, please also make sure those tests are still passing.
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
or to watch for changes
|
||||
|
||||
```bash
|
||||
npm run watch
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
or to watch for changes and run tests:
|
||||
|
||||
```bash
|
||||
npm run test-watch
|
||||
```
|
||||
|
||||
### Lint
|
||||
|
||||
```bash
|
||||
npm run format-check
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the terms of the MIT open source license. Please refer to [MIT](../LICENSE) for the full terms.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@github/actions-workflow-parser",
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.157",
|
||||
"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.153",
|
||||
"@github/actions-expressions": "^0.1.157",
|
||||
"cronstrue": "^2.21.0",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
|
||||
@@ -203,14 +203,15 @@ class TemplateReader {
|
||||
);
|
||||
|
||||
// Duplicate
|
||||
const upperKey = nextKey.value.toUpperCase();
|
||||
if (upperKeys[upperKey]) {
|
||||
this._context.error(nextKey, `'${nextKey.value}' is already defined`);
|
||||
this.skipValue();
|
||||
continue;
|
||||
if (nextKey.value) {
|
||||
const upperKey = nextKey.value.toUpperCase();
|
||||
if (upperKeys[upperKey]) {
|
||||
this._context.error(nextKey, `'${nextKey.value}' is already defined`);
|
||||
this.skipValue();
|
||||
continue;
|
||||
}
|
||||
upperKeys[upperKey] = true;
|
||||
}
|
||||
upperKeys[upperKey] = true;
|
||||
|
||||
// Well known
|
||||
const nextPropertyDef = this._schema.matchPropertyAndFilter(mappingDefinitions, nextKey.value);
|
||||
if (nextPropertyDef) {
|
||||
@@ -339,13 +340,15 @@ class TemplateReader {
|
||||
);
|
||||
|
||||
// Duplicate
|
||||
const upperKey = nextKey.value.toUpperCase();
|
||||
if (upperKeys[upperKey]) {
|
||||
this._context.error(nextKey, `'${nextKey.value}' is already defined`);
|
||||
this.skipValue();
|
||||
continue;
|
||||
if (nextKey.value) {
|
||||
const upperKey = nextKey.value.toUpperCase();
|
||||
if (upperKeys[upperKey]) {
|
||||
this._context.error(nextKey, `'${nextKey.value}' is already defined`);
|
||||
this.skipValue();
|
||||
continue;
|
||||
}
|
||||
upperKeys[upperKey] = true;
|
||||
}
|
||||
upperKeys[upperKey] = true;
|
||||
|
||||
// Validate
|
||||
this.validate(nextKey, keyDefinition);
|
||||
@@ -443,7 +446,7 @@ class TemplateReader {
|
||||
|
||||
private parseScalar(token: LiteralToken, definitionInfo: DefinitionInfo): ScalarToken {
|
||||
// Not a string
|
||||
if (!isString(token)) {
|
||||
if (!isString(token) || !token.value) {
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
@@ -2044,8 +2044,7 @@
|
||||
"continue-on-error": "step-continue-on-error",
|
||||
"timeout-minutes": "step-timeout-minutes",
|
||||
"uses": {
|
||||
"description": "Selects an action to run as part of a step in your job. An action is a reusable unit of code. You can use an action defined in the same repository as the workflow, a public repository, or in a published Docker container image.",
|
||||
"type": "non-empty-string",
|
||||
"type": "step-uses",
|
||||
"required": true
|
||||
},
|
||||
"with": "step-with",
|
||||
@@ -2053,6 +2052,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"step-uses": {
|
||||
"description": "Selects an action to run as part of a step in your job. An action is a reusable unit of code. You can use an action defined in the same repository as the workflow, a public repository, or in a published Docker container image.",
|
||||
"string": {
|
||||
"require-non-empty": true
|
||||
}
|
||||
},
|
||||
"step-continue-on-error": {
|
||||
"context": [
|
||||
"github",
|
||||
|
||||
Reference in New Issue
Block a user