Compare commits

..
Author SHA1 Message Date
Christopher Schleiden db3896d0aa WIP 2023-04-05 15:17:12 -07:00
37 changed files with 247 additions and 564 deletions
+1 -1
View File
@@ -1 +1 @@
* @actions/actions-workflow-development-reviewers
* @actions/actions-experience
+9 -20
View File
@@ -1,18 +1,13 @@
name: Create release PR
run-name: Create release PR for new ${{ github.event.inputs.version }} version
run-name: Create release PR for v${{ github.event.inputs.version }}
on:
workflow_dispatch:
inputs:
version:
required: true
type: choice
description: "What type of release is this"
options:
- "major"
- "minor"
- "patch"
description: "Version to bump `package.json` to (format: x.y.z)"
jobs:
create-release-pr:
@@ -36,27 +31,21 @@ jobs:
git config --global user.email "[email protected]"
git config --global user.name "GitHub Actions"
NEW_VERSION=$(./script/workflows/increment-version.sh ${{ inputs.version }})
git checkout -b release/${{ inputs.version }}
git checkout -b release/$NEW_VERSION
npx lerna version $NEW_VERSION --yes --no-push --no-git-tag-version --force-publish
npx lerna version ${{ inputs.version }} --yes --no-push --no-git-tag-version --force-publish
git add **/package.json package-lock.json lerna.json
git commit -m "Release extension version $NEW_VERSION"
git commit -m "Release extension version ${{ inputs.version }}"
git push --set-upstream origin release/$NEW_VERSION
echo "new_version=$NEW_VERSION" >> $GITHUB_ENV
git push --set-upstream origin release/${{ inputs.version }}
- name: Create PR
run: |
LAST_PR=$(gh pr list --repo ${{ github.repository }} --limit 1 --state merged --search "Release version" --json number | jq -r '.[0].number')
./script/workflows/generate-release-notes.sh $LAST_PR ${{ env.new_version }}
gh pr create \
--title "Release version ${{ env.new_version }}" \
--body-file releasenotes.md \
--title "Release version ${{ inputs.version }}" \
--body "Release version ${{ inputs.version }}" \
--base main \
--head release/${{ env.new_version }}
--head release/${{ inputs.version }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -1,5 +1,5 @@
*/node_modules
*/dist
lerna-debug.log
node_modules
.DS_Store
+2 -10
View File
@@ -8,8 +8,6 @@ Hi there! We're thrilled that you'd like to contribute to this project. Your hel
We accept pull requests for bug fixes and features where we've discussed the approach in an issue and given the go-ahead for a community member to work on it. We'd also love to hear about ideas for new features as issues.
We track issues on our project board [here](https://github.com/orgs/github/projects/9557/views/1).
Please do:
* Check existing issues to verify that the [bug][bug issues] or [feature request][feature request issues] has not already been submitted.
@@ -23,7 +21,7 @@ Please avoid:
* Opening pull requests for issues marked `needs-design`, `needs-investigation`, or `blocked`.
Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE).
Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.md).
Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
@@ -62,10 +60,4 @@ Please also look at the `README.md` files for each package for additional notes
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [GitHub Help](https://help.github.com)
[bug issues]: https://github.com/actions/languageservices/labels/bug
[feature request issues]: https://github.com/actions/languageservices/labels/enhancement
[hw]: https://github.com/actions/languageservices/labels/help%20wanted
[gfi]: https://github.com/actions/languageservices/labels/good%20first%20issue
- [GitHub Help](https://help.github.com)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/expressions",
"version": "0.3.8",
"version": "0.3.1",
"license": "MIT",
"type": "module",
"source": "./src/index.ts",
+1 -1
View File
@@ -32,7 +32,7 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
return this.eval(this.n);
}
protected eval(n: Expr): data.ExpressionData {
private eval(n: Expr): data.ExpressionData {
return n.accept(this);
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/languageserver",
"version": "0.3.8",
"version": "0.3.1",
"description": "Language server for GitHub Actions",
"license": "MIT",
"type": "module",
@@ -43,8 +43,8 @@
"watch": "tsc --build tsconfig.build.json --watch"
},
"dependencies": {
"@actions/languageservice": "^0.3.8",
"@actions/workflow-parser": "^0.3.8",
"@actions/languageservice": "^0.3.1",
"@actions/workflow-parser": "^0.3.1",
"@octokit/rest": "^19.0.7",
"@octokit/types": "^9.0.0",
"vscode-languageserver": "^8.0.2",
+2 -3
View File
@@ -1,9 +1,8 @@
import {Octokit} from "@octokit/rest";
export function getClient(token: string, userAgent?: string, apiUrl?: string): Octokit {
export function getClient(token: string, userAgent?: string): Octokit {
return new Octokit({
auth: token,
userAgent: userAgent || `GitHub Actions Language Server`,
baseUrl: apiUrl
userAgent: userAgent || `GitHub Actions Language Server`
});
}
+13 -2
View File
@@ -1,4 +1,4 @@
import {documentLinks, hover, validate, ValidationConfig} from "@actions/languageservice";
import {documentLinks, documentSymbols, hover, validate, ValidationConfig} from "@actions/languageservice";
import {registerLogger, setLogLevel} from "@actions/languageservice/log";
import {clearCache, clearCacheEntry} from "@actions/languageservice/utils/workflow-cache";
import {Octokit} from "@octokit/rest";
@@ -7,6 +7,8 @@ import {
Connection,
DocumentLink,
DocumentLinkParams,
DocumentSymbol,
DocumentSymbolParams,
ExecuteCommandParams,
Hover,
HoverParams,
@@ -51,7 +53,7 @@ export function initConnection(connection: Connection) {
const options = params.initializationOptions as InitializationOptions;
if (options.sessionToken) {
client = getClient(options.sessionToken, options.userAgent, options.gitHubApiUrl);
client = getClient(options.sessionToken, options.userAgent);
}
if (options.repos) {
@@ -72,6 +74,10 @@ export function initConnection(connection: Connection) {
hoverProvider: true,
documentLinkProvider: {
resolveProvider: false
},
documentSymbolProvider: {
label: "GitHub Actions",
workDoneProgress: false
}
}
};
@@ -158,6 +164,11 @@ export function initConnection(connection: Connection) {
return documentLinks(getDocument(documents, textDocument), repoContext?.workspaceUri);
});
connection.onDocumentSymbol(async ({textDocument}: DocumentSymbolParams): Promise<DocumentSymbol[] | null> => {
const repoContext = repos.find(repo => textDocument.uri.startsWith(repo.workspaceUri));
return documentSymbols(getDocument(documents, textDocument), repoContext?.workspaceUri);
});
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
@@ -28,7 +28,6 @@ export async function getSecrets(
}
const eventsConfig = workflowContext?.template?.events;
if (eventsConfig?.workflow_call) {
// Unpredictable secrets may be passed in via a workflow_call trigger
secretsContext.complete = false;
@@ -39,7 +38,6 @@ export async function getSecrets(
}
let environmentName: string | undefined;
if (workflowContext?.job?.environment) {
if (isString(workflowContext.job.environment)) {
environmentName = workflowContext.job.environment.value;
@@ -48,17 +46,10 @@ export async function getSecrets(
if (isString(x.key) && x.key.value === "name") {
if (isString(x.value)) {
environmentName = x.value.value;
} else {
// this means we have a dynamic enviornment, in those situations we
// want to make sure we skip doing secret validation
secretsContext.complete = false;
}
break;
}
}
} else {
// if the expression is something like environment: ${{ ... }} then we want to skip validation
secretsContext.complete = false;
}
}
@@ -2,10 +2,9 @@ import {data, DescriptionDictionary} from "@actions/expressions";
import {Pair} from "@actions/expressions/data/expressiondata";
import {StringData} from "@actions/expressions/data/index";
import {WorkflowContext} from "@actions/languageservice/context/workflow-context";
import {log, warn} from "@actions/languageservice/log";
import {warn} from "@actions/languageservice/log";
import {isMapping, isString} from "@actions/workflow-parser";
import {Octokit} from "@octokit/rest";
import {RequestError} from "@octokit/request-error";
import {RepositoryContext} from "../initializationOptions";
import {TTLCache} from "../utils/cache";
@@ -43,58 +42,50 @@ export async function getVariables(
}
const variablesContext = defaultContext || new DescriptionDictionary();
try {
const variables = await getRemoteVariables(octokit, cache, repo, environmentName);
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;
}
>();
// 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`
})
);
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 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 || ""}\``
})
);
// 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));
// 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));
return variablesContext;
} catch (e) {
if (!(e instanceof RequestError)) throw e;
if (e.name == "HttpError" && e.status == 404) {
log("Failure to request variables. Ignore if you're using GitHub Enterprise Server below version 3.8");
return variablesContext;
} else throw e;
}
return variablesContext;
}
export async function getRemoteVariables(
+2 -5
View File
@@ -2,8 +2,8 @@ import {File} from "@actions/workflow-parser/workflows/file";
import {FileProvider} from "@actions/workflow-parser/workflows/file-provider";
import {fileIdentifier} from "@actions/workflow-parser/workflows/file-reference";
import {Octokit} from "@octokit/rest";
import path from "path";
import {TTLCache} from "./utils/cache";
import vscodeURI from "vscode-uri/lib/umd";
export function getFileProvider(
client: Octokit | undefined,
@@ -31,10 +31,7 @@ export function getFileProvider(
throw new Error("Local file references are not supported with this configuration");
}
const workspaceURI = vscodeURI.URI.parse(workspace);
const refURI = vscodeURI.Utils.joinPath(workspaceURI, ref.path);
const file = await readFile(refURI.toString());
const file = await readFile(path.join(workspace, ref.path));
if (!file) {
throw new Error(`File not found: ${ref.path}`);
}
@@ -23,11 +23,6 @@ export interface InitializationOptions {
* Desired log level
*/
logLevel?: LogLevel;
/**
* If a GitHub Enterprise Server should be used, the URL of the API endpoint, eg "https://ghe.my-company.com/api/v3"
*/
gitHubApiUrl?: string;
}
export interface RepositoryContext {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/languageservice",
"version": "0.3.8",
"version": "0.3.1",
"description": "Language service for GitHub Actions",
"license": "MIT",
"type": "module",
@@ -44,8 +44,8 @@
"watch": "tsc --build tsconfig.build.json --watch"
},
"dependencies": {
"@actions/expressions": "^0.3.8",
"@actions/workflow-parser": "^0.3.8",
"@actions/expressions": "^0.3.1",
"@actions/workflow-parser": "^0.3.1",
"vscode-languageserver-textdocument": "^1.0.7",
"vscode-languageserver-types": "^3.17.2",
"vscode-uri": "^3.0.7",
+5 -48
View File
@@ -4,8 +4,8 @@ import {complete} from "./complete";
import {registerLogger} from "./log";
import {getPositionFromCursor} from "./test-utils/cursor-position";
import {TestLogger} from "./test-utils/logger";
import {clearCache} from "./utils/workflow-cache";
import {ValueProviderConfig, ValueProviderKind} from "./value-providers/config";
import {clearCache} from "./utils/workflow-cache";
registerLogger(new TestLogger());
@@ -406,7 +406,7 @@ jobs:
expect(result.map(e => e.label)).toContain("runs-on");
const textEdit = result.filter(e => e.label === "runs-on")[0].textEdit as TextEdit;
expect(textEdit.newText).toEqual("runs-on: ");
expect(textEdit.newText).toEqual("runs-on");
expect(textEdit.range).toEqual({
start: {line: 3, character: 4},
end: {line: 3, character: 10}
@@ -421,7 +421,7 @@ jobs:
expect(result.map(e => e.label)).toContain("runs-on");
const textEdit = result.filter(e => e.label === "runs-on")[0].textEdit as TextEdit;
expect(textEdit.newText).toEqual("runs-on: ");
expect(textEdit.newText).toEqual("runs-on");
expect(textEdit.range).toEqual({
start: {line: 3, character: 4},
end: {line: 3, character: 4}
@@ -448,7 +448,7 @@ jobs:
]);
// One-of
expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency: "]);
expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency"]);
});
it("custom indentation", async () => {
@@ -471,50 +471,7 @@ jobs:
]);
// One-of
expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency: "]);
expect(result.filter(x => x.label === "concurrency").map(x => x.textEdit?.newText)).toEqual(["concurrency"]);
});
});
it("adds a new line and indentation for mapping keys when the key is given", async () => {
const input = "concurrency: |";
const result = await complete(...getPositionFromCursor(input));
expect(result.filter(x => x.label === "cancel-in-progress").map(x => x.textEdit?.newText)).toEqual([
"\n cancel-in-progress: "
]);
expect(result.filter(x => x.label === "group").map(x => x.textEdit?.newText)).toEqual(["\n group: "]);
});
it("does not add new line if no key in line", async () => {
const input = "run-n|";
const result = await complete(...getPositionFromCursor(input));
expect(result.filter(x => x.label === "run-name").map(x => x.textEdit?.newText)).toEqual(["run-name: "]);
});
it("adds new line for nested mapping", async () => {
const input = "on:\n workflow_dispatch: in|";
const result = await complete(...getPositionFromCursor(input));
expect(result.filter(x => x.label === "inputs").map(x => x.textEdit?.newText)).toEqual(["\n inputs:\n "]);
});
it("adds : for one-of", async () => {
const input = "on:\n check_run:\n ty|";
const result = await complete(...getPositionFromCursor(input));
expect(result.filter(x => x.label === "types").map(x => x.textEdit?.newText)).toEqual(["types: "]);
});
it("does not add : for one-of in key mode", async () => {
const input = "on:\n check_run: ty|";
const result = await complete(...getPositionFromCursor(input));
expect(result.filter(x => x.label === "types").map(x => x.textEdit?.newText)).toEqual(["types"]);
});
});
+2 -2
View File
@@ -24,7 +24,7 @@ import {isPlaceholder, transform} from "./utils/transform";
import {fetchOrConvertWorkflowTemplate, fetchOrParseWorkflow} from "./utils/workflow-cache";
import {Value, ValueProviderConfig} from "./value-providers/config";
import {defaultValueProviders} from "./value-providers/default";
import {DefinitionValueMode, definitionValues} from "./value-providers/definition";
import {definitionValues} from "./value-providers/definition";
export function getExpressionInput(input: string, pos: number): string {
// Find start marker around the cursor position
@@ -180,7 +180,7 @@ async function getValues(
return [];
}
const values = definitionValues(def, indentation, keyToken ? DefinitionValueMode.Key : DefinitionValueMode.Parent);
const values = definitionValues(def, indentation);
return filterAndSortCompletionOptions(values, existingValues);
}
@@ -8,7 +8,7 @@ import {getEventPayload, getSupportedEventTypes} from "./events/eventPayloads";
import {getInputsContext} from "./inputs";
export function getGithubContext(workflowContext: WorkflowContext, mode: Mode): DescriptionDictionary {
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-cwontext
const keys = [
"action",
"action_path",
@@ -16,7 +16,6 @@ export function getGithubContext(workflowContext: WorkflowContext, mode: Mode):
"action_repository",
"action_status",
"actor",
"actor_id",
"api_url",
"base_ref",
"env",
@@ -26,16 +25,13 @@ export function getGithubContext(workflowContext: WorkflowContext, mode: Mode):
"graphql_url",
"head_ref",
"job",
"job_workflow_sha",
"path",
"ref",
"ref_name",
"ref_protected",
"ref_type",
"path",
"repository",
"repository_id",
"repository_owner",
"repository_owner_id",
"repositoryUrl",
"retention_days",
"run_id",
@@ -47,8 +43,6 @@ export function getGithubContext(workflowContext: WorkflowContext, mode: Mode):
"token",
"triggering_actor",
"workflow",
"workflow_ref",
"workflow_sha",
"workspace"
];
+70
View File
@@ -0,0 +1,70 @@
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
import {File} from "@actions/workflow-parser/workflows/file";
import {TextDocument} from "vscode-languageserver-textdocument";
import {DocumentSymbol, SymbolKind} from "vscode-languageserver-types";
import {mapRange} from "./utils/range";
import {fetchOrConvertWorkflowTemplate, fetchOrParseWorkflow} from "./utils/workflow-cache";
export async function documentSymbols(
document: TextDocument,
workspace: string | undefined
): Promise<DocumentSymbol[]> {
const file: File = {
name: document.uri,
content: document.getText()
};
const parsedWorkflow = fetchOrParseWorkflow(file, document.uri);
if (!parsedWorkflow?.value) {
return [];
}
const template = await fetchOrConvertWorkflowTemplate(
parsedWorkflow.context,
parsedWorkflow.value,
document.uri,
undefined,
{
errorPolicy: ErrorPolicy.TryConversion
}
);
if (!template) {
return [];
}
const symbols: DocumentSymbol[] = [];
const onSymbol = DocumentSymbol.create(
"`on` Triggers",
undefined,
SymbolKind.Key,
mapRange(template.events.range),
mapRange(template.events.range)
);
symbols.push(onSymbol);
const jobsSymbol = DocumentSymbol.create(
"Jobs",
undefined,
SymbolKind.Namespace,
mapRange(template.jobs?.[0].id.range),
mapRange(template.jobs?.[0].id.range),
[]
);
symbols.push(jobsSymbol);
for (const job of template.jobs || []) {
const jobSymbol = DocumentSymbol.create(
`Job ${job.name?.toDisplayString() || job.id?.toString()}`,
"detail",
SymbolKind.Class,
mapRange(job.id.range),
mapRange(job.id.range)
);
jobsSymbol.children!.push(jobSymbol);
}
return symbols;
}
@@ -1,61 +0,0 @@
import {Evaluator, ExpressionEvaluationError, data} from "@actions/expressions";
import {Expr, Logical} from "@actions/expressions/ast";
import {ExpressionData} from "@actions/expressions/data/expressiondata";
import {TokenType} from "@actions/expressions/lexer";
import {falsy, truthy} from "@actions/expressions/result";
import {AccessError} from "./error-dictionary";
export type ValidationError = {
message: string;
severity: "error" | "warning";
};
export class ValidationEvaluator extends Evaluator {
public readonly errors: ValidationError[] = [];
public validate() {
super.evaluate();
}
protected override eval(n: Expr): ExpressionData {
try {
return super.eval(n);
} catch (e) {
// Record error
if (e instanceof AccessError) {
this.errors.push({
message: `Context access might be invalid: ${e.keyName}`,
severity: "warning"
});
} else if (e instanceof ExpressionEvaluationError) {
this.errors.push({
message: `Expression might be invalid: ${e.message}`,
severity: "error"
});
}
}
// Return null but continue with the validation
return new data.Null();
}
override visitLogical(logical: Logical): ExpressionData {
let result: data.ExpressionData | undefined;
for (const arg of logical.args) {
const r = this.eval(arg);
// Simulate short-circuit behavior but continue to evalute all arguments for validation purposes
if (
!result &&
((logical.operator.type === TokenType.AND && falsy(r)) || (logical.operator.type === TokenType.OR && truthy(r)))
) {
result = r;
}
}
// result is always assigned before we return here
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return result!;
}
}
+3 -2
View File
@@ -1,7 +1,8 @@
export {complete} from "./complete";
export {ContextProviderConfig} from "./context-providers/config";
export {documentLinks} from "./document-links";
export {documentSymbols} from "./document-symbols";
export {hover} from "./hover";
export {Logger, LogLevel, registerLogger, setLogLevel} from "./log";
export {validate, ValidationConfig, ActionsMetadataProvider} from "./validate";
export {LogLevel, Logger, registerLogger, setLogLevel} from "./log";
export {ActionsMetadataProvider, ValidationConfig, validate} from "./validate";
export {ValueProviderConfig, ValueProviderKind} from "./value-providers/config";
@@ -46,52 +46,6 @@ jobs:
]);
});
it("access invalid context field in short-circuited expression", async () => {
const result = await validate(
createDocument(
"wf.yaml",
`on: push
run-name: name-\${{ github.does-not-exist || github.does-not-exist2 }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo`
)
);
expect(result).toEqual([
{
message: "Context access might be invalid: does-not-exist",
range: {
end: {
character: 69,
line: 1
},
start: {
character: 15,
line: 1
}
},
severity: DiagnosticSeverity.Warning
},
{
message: "Context access might be invalid: does-not-exist2",
range: {
end: {
character: 69,
line: 1
},
start: {
character: 15,
line: 1
}
},
severity: DiagnosticSeverity.Warning
}
]);
});
it("partial skip access invalid context on incomplete", async () => {
const contextProviderConfig: ContextProviderConfig = {
getContext: (context: string) => {
+26 -16
View File
@@ -1,6 +1,6 @@
import {Lexer, Parser} from "@actions/expressions";
import {Evaluator, ExpressionEvaluationError, Lexer, Parser} from "@actions/expressions";
import {Expr} from "@actions/expressions/ast";
import {ParseWorkflowResult, WorkflowTemplate, isBasicExpression, isString} from "@actions/workflow-parser";
import {isBasicExpression, isString, ParseWorkflowResult, WorkflowTemplate} from "@actions/workflow-parser";
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
import {splitAllowedContext} from "@actions/workflow-parser/templates/allowed-context";
import {BasicExpressionToken} from "@actions/workflow-parser/templates/tokens/basic-expression-token";
@@ -13,10 +13,9 @@ import {TextDocument} from "vscode-languageserver-textdocument";
import {Diagnostic, DiagnosticSeverity, URI} from "vscode-languageserver-types";
import {ActionMetadata, ActionReference} from "./action";
import {ContextProviderConfig} from "./context-providers/config";
import {Mode, getContext} from "./context-providers/default";
import {WorkflowContext, getWorkflowContext} from "./context/workflow-context";
import {wrapDictionary} from "./expression-validation/error-dictionary";
import {ValidationEvaluator} from "./expression-validation/evaluator";
import {getContext, Mode} from "./context-providers/default";
import {getWorkflowContext, WorkflowContext} from "./context/workflow-context";
import {AccessError, wrapDictionary} from "./expression-validation/error-dictionary";
import {validatorFunctions} from "./expression-validation/functions";
import {error} from "./log";
import {findToken} from "./utils/find-token";
@@ -203,17 +202,28 @@ async function validateExpression(
continue;
}
const context = await getContext(namedContexts, contextProviderConfig, workflowContext, Mode.Validation);
try {
const context = await getContext(namedContexts, contextProviderConfig, workflowContext, Mode.Validation);
const e = new ValidationEvaluator(expr, wrapDictionary(context), validatorFunctions);
e.validate();
const e = new Evaluator(expr, wrapDictionary(context), validatorFunctions);
e.evaluate();
diagnostics.push(
...e.errors.map(e => ({
message: e.message,
range: mapRange(expression.range),
severity: e.severity === "error" ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning
}))
);
// Any invalid context access would've thrown an error via the `ErrorDictionary`, for now we don't have to check the actual
// result of the evaluation.
} catch (e) {
if (e instanceof AccessError) {
diagnostics.push({
message: `Context access might be invalid: ${e.keyName}`,
severity: DiagnosticSeverity.Warning,
range: mapRange(expression.range)
});
} else if (e instanceof ExpressionEvaluationError) {
diagnostics.push({
message: `Expression might be invalid: ${e.message}`,
severity: DiagnosticSeverity.Error,
range: mapRange(expression.range)
});
}
}
}
}
@@ -9,30 +9,15 @@ import {getWorkflowSchema} from "@actions/workflow-parser/workflows/workflow-sch
import {Value} from "./config";
import {stringsToValues} from "./strings-to-values";
export enum DefinitionValueMode {
/**
* We're getting completion options for a parent token
* foo:
* ba|
*/
Parent,
/**
* We're getting completion options for a key token. For example:
* foo: |
*/
Key
}
export function definitionValues(def: Definition, indentation: string, mode: DefinitionValueMode): Value[] {
export function definitionValues(def: Definition, indentation: string): Value[] {
const schema = getWorkflowSchema();
if (def instanceof MappingDefinition) {
return mappingValues(def, schema.definitions, indentation, mode);
return mappingValues(def, schema.definitions, indentation);
}
if (def instanceof OneOfDefinition) {
return oneOfValues(def, schema.definitions, indentation, mode);
return oneOfValues(def, schema.definitions, indentation);
}
if (def instanceof BooleanDefinition) {
@@ -51,7 +36,7 @@ export function definitionValues(def: Definition, indentation: string, mode: Def
if (def instanceof SequenceDefinition) {
const itemDef = schema.getDefinition(def.itemType);
if (itemDef) {
return definitionValues(itemDef, indentation, mode);
return definitionValues(itemDef, indentation);
}
}
@@ -61,8 +46,7 @@ export function definitionValues(def: Definition, indentation: string, mode: Def
function mappingValues(
mappingDefinition: MappingDefinition,
definitions: {[key: string]: Definition},
indentation: string,
mode: DefinitionValueMode
indentation: string
): Value[] {
const properties: Value[] = [];
for (const [key, value] of Object.entries(mappingDefinition.properties)) {
@@ -76,36 +60,15 @@ function mappingValues(
if (typeDef) {
switch (typeDef.definitionType) {
case DefinitionType.Sequence:
if (mode == DefinitionValueMode.Key) {
insertText = `\n${indentation}${key}:\n${indentation}${indentation}- `;
} else {
insertText = `${key}:\n${indentation}- `;
}
insertText = `${key}:\n${indentation}- `;
break;
case DefinitionType.Mapping:
if (mode == DefinitionValueMode.Key) {
insertText = `\n${indentation}${key}:\n${indentation}${indentation}`;
} else {
insertText = `${key}:\n${indentation}`;
}
insertText = `${key}:\n${indentation}`;
break;
case DefinitionType.OneOf:
if (mode == DefinitionValueMode.Parent) {
insertText = `${key}: `;
} else {
// No special insertText in this case
}
break;
case DefinitionType.String:
case DefinitionType.Boolean:
if (mode == DefinitionValueMode.Key) {
insertText = `\n${indentation}${key}: `;
} else {
insertText = `${key}: `;
}
// No special insertText in this case
break;
default:
@@ -126,12 +89,11 @@ function mappingValues(
function oneOfValues(
oneOfDefinition: OneOfDefinition,
definitions: {[key: string]: Definition},
indentation: string,
mode: DefinitionValueMode
indentation: string
): Value[] {
const values: Value[] = [];
for (const key of oneOfDefinition.oneOf) {
values.push(...definitionValues(definitions[key], indentation, mode));
values.push(...definitionValues(definitions[key], indentation));
}
return distinctValues(values);
}
+2 -7
View File
@@ -1,10 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"packages": [
"expressions",
"workflow-parser",
"languageservice",
"languageserver"
],
"version": "0.3.8"
"useWorkspaces": true,
"version": "0.3.1"
}
+16 -19
View File
@@ -135,7 +135,7 @@
},
"expressions": {
"name": "@actions/expressions",
"version": "0.3.8",
"version": "0.3.1",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.0.3",
@@ -395,11 +395,11 @@
},
"languageserver": {
"name": "@actions/languageserver",
"version": "0.3.8",
"version": "0.3.1",
"license": "MIT",
"dependencies": {
"@actions/languageservice": "^0.3.8",
"@actions/workflow-parser": "^0.3.8",
"@actions/languageservice": "^0.3.1",
"@actions/workflow-parser": "^0.3.1",
"@octokit/rest": "^19.0.7",
"@octokit/types": "^9.0.0",
"vscode-languageserver": "^8.0.2",
@@ -678,11 +678,11 @@
},
"languageservice": {
"name": "@actions/languageservice",
"version": "0.3.8",
"version": "0.3.1",
"license": "MIT",
"dependencies": {
"@actions/expressions": "^0.3.8",
"@actions/workflow-parser": "^0.3.8",
"@actions/expressions": "^0.3.1",
"@actions/workflow-parser": "^0.3.1",
"vscode-languageserver-textdocument": "^1.0.7",
"vscode-languageserver-types": "^3.17.2",
"vscode-uri": "^3.0.7",
@@ -6652,10 +6652,9 @@
"license": "MIT"
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
"version": "4.1.0",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/http-proxy-agent": {
"version": "5.0.0",
@@ -11470,10 +11469,9 @@
}
},
"node_modules/word-wrap": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"version": "1.2.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -11669,9 +11667,8 @@
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
"integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
"version": "2.1.3",
"license": "ISC",
"engines": {
"node": ">= 14"
}
@@ -11722,10 +11719,10 @@
},
"workflow-parser": {
"name": "@actions/workflow-parser",
"version": "0.3.8",
"version": "0.3.1",
"license": "MIT",
"dependencies": {
"@actions/expressions": "^0.3.8",
"@actions/expressions": "^0.3.1",
"cronstrue": "^2.21.0",
"yaml": "^2.0.0-8"
},
-1
View File
@@ -1 +0,0 @@
Release 0.3.5
@@ -1,32 +0,0 @@
#!/bin/bash
# this script is used to generate release notes for a given release
# first argument is the pull request id for the last release
# the second is the new release number
# the script then grabs every pull request merged since that pull request
# and outputs a string of release notes
# get the new release number
NEW_RELEASE=$2
echo "Generating release notes for $NEW_RELEASE"
# get the last release pull request id
LAST_RELEASE_PR=$1
#get when the last release was merged
LAST_RELEASE_MERGED_AT=$(gh pr view $LAST_RELEASE_PR --repo actions/languageservices --json mergedAt | jq -r '.mergedAt')
CHANGELIST=$(gh pr list --repo actions/languageservices --base main --state merged --json title --search "merged:>$LAST_RELEASE_MERGED_AT -label:no-release")
# store the release notes in a variable so we can use it later
echo "Release $NEW_RELEASE" >> releasenotes.md
echo $CHANGELIST | jq -r '.[].title' | while read line; do
echo " - $line" >> releasenotes.md
done
echo " "
-24
View File
@@ -1,24 +0,0 @@
#!/bin/bash
VERSION=$(cat lerna.json | jq -r '.version')
MAJOR=$(echo $VERSION | cut -d. -f1)
MINOR=$(echo $VERSION | cut -d. -f2)
PATCH=$(echo $VERSION | cut -d. -f3)
if [ "$1" == "major" ]; then
MAJOR=$((MAJOR+1))
MINOR=0
PATCH=0
elif [ "$1" == "minor" ]; then
MINOR=$((MINOR+1))
PATCH=0
elif [ "$1" == "patch" ]; then
PATCH=$((PATCH+1))
else
echo "Invalid version type. Use 'major', 'minor' or 'patch'"
exit 1
fi
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
echo $NEW_VERSION
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
);
```
`convertWorkflowTemplate` then takes that intermediate representation and converts it to a [`WorkflowTemplate`](./src/model/workflow-template.ts) object, which is a more convenient representation for working with workflows.
`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);
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/workflow-parser",
"version": "0.3.8",
"version": "0.3.1",
"license": "MIT",
"type": "module",
"source": "./src/index.ts",
@@ -43,7 +43,7 @@
"watch": "tsc --build tsconfig.build.json --watch"
},
"dependencies": {
"@actions/expressions": "^0.3.8",
"@actions/expressions": "^0.3.1",
"cronstrue": "^2.21.0",
"yaml": "^2.0.0-8"
},
+4 -2
View File
@@ -9,7 +9,7 @@ import {handleTemplateTokenErrors} from "./converter/handle-errors";
import {convertJobs} from "./converter/jobs";
import {convertReferencedWorkflow} from "./converter/referencedWorkflow";
import {isReusableWorkflowJob} from "./type-guards";
import {WorkflowTemplate} from "./workflow-template";
import {EventsConfig, WorkflowTemplate} from "./workflow-template";
export enum ErrorPolicy {
ReturnErrorsOnly,
@@ -73,7 +73,9 @@ export async function convertWorkflowTemplate(
switch (key.value) {
case "on":
result.events = handleTemplateTokenErrors(root, context, {}, () => convertOn(context, item.value));
result.events = handleTemplateTokenErrors(root, context, {range: item.key.range} as EventsConfig, () =>
convertOn(context, item.value)
);
break;
case "jobs":
+14 -14
View File
@@ -23,12 +23,15 @@ export function convertOn(context: TemplateContext, token: TemplateToken): Event
const event = token.assertString("on");
return {
range: token.range,
[event.value]: {}
} as EventsConfig;
}
if (isSequence(token)) {
const result = {} as EventsConfig;
const result = {
range: token.range
} as EventsConfig;
for (const item of token) {
const event = item.assertString("on");
@@ -39,7 +42,9 @@ export function convertOn(context: TemplateContext, token: TemplateToken): Event
}
if (isMapping(token)) {
const result = {} as EventsConfig;
const result = {
range: token.range
} as EventsConfig;
for (const item of token) {
const eventKey = item.key.assertString("event name");
@@ -59,24 +64,17 @@ export function convertOn(context: TemplateContext, token: TemplateToken): Event
// All other events are defined as mappings. During schema validation we already ensure that events
// receive only known keys, so here we can focus on the values and whether they are valid.
const eventToken = item.value.assertMapping(`event ${eventName}`);
if (eventName === "workflow_call") {
result.workflow_call = convertEventWorkflowCall(context, eventToken);
continue;
}
if (eventName === "workflow_dispatch") {
result.workflow_dispatch = convertEventWorkflowDispatchInputs(context, eventToken);
continue;
}
result[eventName] = {
...convertPatternFilter("branches", eventToken),
...convertPatternFilter("tags", eventToken),
...convertPatternFilter("paths", eventToken),
...convertFilter("types", eventToken),
...convertFilter("workflows", eventToken)
...convertFilter("workflows", eventToken),
// workflow_call and workflow_dispatch share input parsing
...convertEventWorkflowDispatchInputs(context, eventToken),
...convertEventWorkflowCall(context, eventToken)
};
}
@@ -84,7 +82,9 @@ export function convertOn(context: TemplateContext, token: TemplateToken): Event
}
context.error(token, "Invalid format for 'on'");
return {};
return {
range: token.range
} as EventsConfig;
}
function convertPatternFilter<T extends BranchFilterConfig & TagFilterConfig & PathFilterConfig>(
+2 -6
View File
@@ -50,7 +50,7 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
let id: StringToken | undefined;
let name: ScalarToken | undefined;
let uses: StringToken | undefined;
let continueOnError: boolean | ScalarToken | undefined;
let continueOnError: boolean | undefined;
let env: MappingToken | undefined;
const ifCondition = new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined);
for (const item of mapping) {
@@ -78,11 +78,7 @@ function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: Templ
env = item.value.assertMapping("step env");
break;
case "continue-on-error":
if (!item.value.isExpression) {
continueOnError = item.value.assertBoolean("steps item continue-on-error").value;
} else {
continueOnError = item.value.assertScalar("steps item continue-on-error");
}
continueOnError = item.value.assertBoolean("steps item continue-on-error").value;
}
}
@@ -1,9 +1,7 @@
import {TemplateContext} from "../../templates/template-context";
import {MappingToken, TemplateToken} from "../../templates/tokens";
import {isMapping} from "../../templates/tokens/type-guards";
import {SecretConfig, WorkflowCallConfig, InputConfig, InputType} from "../workflow-template";
import {convertStringList} from "./string-list";
import {ScalarToken} from "../../templates/tokens/scalar-token";
import {SecretConfig, WorkflowCallConfig} from "../workflow-template";
export function convertEventWorkflowCall(context: TemplateContext, token: MappingToken): WorkflowCallConfig {
const result: WorkflowCallConfig = {};
@@ -13,7 +11,7 @@ export function convertEventWorkflowCall(context: TemplateContext, token: Mappin
switch (key.value) {
case "inputs":
result.inputs = convertWorkflowInputs(context, item.value.assertMapping("workflow dispatch inputs"));
// Ignore, these are handled by convertEventWorkflowDispatchInputs
break;
case "secrets":
@@ -29,94 +27,6 @@ export function convertEventWorkflowCall(context: TemplateContext, token: Mappin
return result;
}
export function convertWorkflowInputs(
context: TemplateContext,
token: MappingToken
): {
[inputName: string]: InputConfig;
} {
const result: {[inputName: string]: InputConfig} = {};
for (const item of token) {
const inputName = item.key.assertString("input name");
const inputMapping = item.value.assertMapping("input configuration");
result[inputName.value] = convertWorkflowInput(context, inputMapping);
}
return result;
}
export function convertWorkflowInput(context: TemplateContext, token: MappingToken): InputConfig {
const result: InputConfig = {
type: InputType.string // Default to string
};
let defaultValue: undefined | ScalarToken;
for (const item of token) {
const key = item.key.assertString("workflow dispatch input key");
switch (key.value) {
case "description":
result.description = item.value.assertString("input description").value;
break;
case "required":
result.required = item.value.assertBoolean("input required").value;
break;
case "default":
defaultValue = item.value.assertScalar("input default");
break;
case "type":
result.type = InputType[item.value.assertString("input type").value as keyof typeof InputType];
break;
case "options":
result.options = convertStringList("input options", item.value.assertSequence("input options"));
break;
default:
context.error(item.key, `Invalid key '${key.value}'`);
}
}
// Validate default value
if (defaultValue !== undefined && !defaultValue.isExpression) {
try {
switch (result.type) {
case InputType.boolean:
result.default = defaultValue.assertBoolean("input default").value;
break;
case InputType.string:
case InputType.choice:
case InputType.environment:
result.default = defaultValue.assertString("input default").value;
break;
}
} catch (e) {
context.error(defaultValue, e);
}
}
// Validate `options` for `choice` type
if (result.type === InputType.choice) {
if (result.options === undefined || result.options.length === 0) {
context.error(token, "Missing 'options' for choice input");
}
} else {
if (result.options !== undefined) {
context.error(token, "Input type is not 'choice', but 'options' is defined");
}
}
return result;
}
function convertWorkflowCallSecrets(
context: TemplateContext,
token: MappingToken
@@ -6,6 +6,7 @@ import {
StringToken,
TemplateToken
} from "../templates/tokens";
import {TokenRange} from "../templates/tokens/token-range";
export type WorkflowTemplate = {
events: EventsConfig;
@@ -86,7 +87,7 @@ type BaseStep = {
id: string;
name?: ScalarToken;
if: BasicExpressionToken;
"continue-on-error"?: boolean | ScalarToken;
"continue-on-error"?: boolean;
env?: MappingToken;
};
@@ -99,6 +100,8 @@ export type ActionStep = BaseStep & {
};
export type EventsConfig = {
range: TokenRange;
schedule?: ScheduleConfig[];
workflow_dispatch?: WorkflowDispatchConfig;
workflow_call?: WorkflowCallConfig;
@@ -158,7 +161,7 @@ export type WorkflowDispatchConfig = {
};
export type WorkflowCallConfig = {
inputs?: {[inputName: string]: InputConfig & {default?: string | boolean | number | ScalarToken}};
inputs?: {[inputName: string]: InputConfig};
secrets?: {[secretName: string]: SecretConfig};
// TODO - these are supported in C# and Go but not in TS yet
// outputs: { [outputName: string]: OutputConfig }
+3 -5
View File
@@ -576,9 +576,7 @@
"merge-group-mapping": {
"mapping": {
"properties": {
"types": "merge-group-activity",
"branches": "event-branches",
"branches-ignore": "event-branches-ignore"
"types": "merge-group-activity"
}
}
},
@@ -1183,7 +1181,7 @@
]
},
"workflow-run-activity": {
"description": "The types of workflow run activity that trigger the workflow. Supported activity types: `completed`, `requested`, `in_progress`.",
"description": "The types of workflow run activity that trigger the workflow. Suupported activity types: `completed`, `requested`, `in_progress`.",
"one-of": [
"workflow-run-activity-type",
"workflow-run-activity-types"
@@ -2489,7 +2487,7 @@
"string": {
"require-non-empty": true
},
"description": "Use `shell` to override the default shell settings in the runner's operating system. You can use built-in shell keywords, or you can define a custom set of shell options. The shell command that is run internally executes a temporary file that contains the commands specified in `run`."
"description": "Use `shell` to override the default shell settings in the runner's operating system. You can use built-in shell keywords, or you can define a custom set of shell options. The shell command that is run internally executes a temporary file that contains the comands specified in `run`."
},
"working-directory": {
"string": {
+1 -14
View File
@@ -72,13 +72,7 @@ on:
- edited
- deleted
merge_group:
branches:
- master
- main
branches-ignore:
- develop
types:
- checks_requested
types: checks_requested
milestone:
types:
- created
@@ -319,13 +313,6 @@ jobs:
]
},
"merge_group": {
"branches": [
"master",
"main"
],
"branches-ignore": [
"develop"
],
"types": [
"checks_requested"
]