Merge branch 'main' into elbrenn/secret-complete

This commit is contained in:
Beth Brennan
2023-03-07 15:23:50 -05:00
committed by GitHub
25 changed files with 388 additions and 119 deletions
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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.
+3 -3
View File
@@ -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",
+7 -2
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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"
+9
View File
@@ -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 || ""}`;
}
+31
View File
@@ -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|
+13 -1
View File
@@ -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 => {
+2 -7
View File
@@ -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),
+1 -1
View File
@@ -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;
}
+13 -1
View File
@@ -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
View File
@@ -1,5 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true,
"version": "0.1.153"
"version": "0.1.157"
}
+17 -17
View File
@@ -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",
+87
View File
@@ -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.
+2 -2
View File
@@ -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;
}
+7 -2
View File
@@ -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",