├── expressions ├── script │ ├── test │ └── build ├── .npmrc ├── .prettierignore ├── testdata │ ├── coerce_boolean.json │ ├── op_gte.json │ ├── op_lte.json │ └── op_or.json ├── src │ ├── filtered_array.ts │ ├── data │ │ ├── null.ts │ │ ├── index.ts │ │ ├── string.ts │ │ ├── number.test.ts │ │ ├── boolean.ts │ │ ├── dictionary.test.ts │ │ ├── number.ts │ │ ├── array.ts │ │ ├── replacer.test.ts │ │ ├── expressiondata.ts │ │ ├── replacer.ts │ │ └── reviver.ts │ ├── funcs │ │ ├── info.ts │ │ ├── format.test.ts │ │ ├── tojson.ts │ │ ├── endswith.ts │ │ ├── startswith.ts │ │ ├── fromjson.ts │ │ └── join.ts │ ├── index.ts │ ├── idxHelper.ts │ ├── completion │ │ └── descriptionDictionary.test.ts │ └── evaluator.test.ts ├── .eslintrc.cjs ├── tsconfig.build.json ├── jest.config.js └── tsconfig.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug-report.md └── dependabot.yml ├── workflow-parser ├── .gitignore ├── .npmrc ├── .prettierignore ├── src │ ├── templates │ │ ├── schema │ │ │ ├── index.ts │ │ │ ├── definition-type.ts │ │ │ └── scalar-definition.ts │ │ ├── tokens │ │ │ ├── token-range.ts │ │ │ ├── key-value-pair.ts │ │ │ ├── index.ts │ │ │ ├── serialization.ts │ │ │ ├── null-token.ts │ │ │ ├── types.ts │ │ │ ├── literal-token.ts │ │ │ ├── number-token.ts │ │ │ ├── boolean-token.ts │ │ │ ├── string-token.ts │ │ │ └── expression-token.ts │ │ ├── trace-writer.ts │ │ ├── parse-event.ts │ │ ├── object-reader.ts │ │ ├── template-validation-error.ts │ │ └── allowed-context.ts │ ├── workflows │ │ ├── workflow-constants.ts │ │ ├── file.ts │ │ ├── file-provider.ts │ │ ├── workflow-schema.ts │ │ ├── workflow-parser.test.ts │ │ └── file-reference.ts │ ├── test-utils │ │ └── null-trace.ts │ ├── index.ts │ └── model │ │ ├── converter │ │ ├── string-list.ts │ │ ├── cron-constants.ts │ │ ├── handle-errors.ts │ │ └── job │ │ │ └── environment.ts │ │ └── type-guards.ts ├── testdata │ ├── sync-tests.sh │ └── reader │ │ ├── errors-jobs-missing.yml │ │ ├── errors-jobs-at-least-one.yml │ │ ├── errors-empty-expression.yml │ │ ├── errors-job-id-empty.yml │ │ ├── errors-job-runs-on-and-uses.yml │ │ ├── errors-step-run-missing.yml │ │ ├── errors-unexpected-mapping.yml │ │ ├── errors-job-runs-on-missing.yml │ │ ├── errors-invalid-mapping-key.yml │ │ ├── errors-step-uses-missing.yml │ │ ├── errors-anchor-tag.yml │ │ ├── errors-value-already-defined.yml │ │ ├── errors-matrix-bad-key.yml │ │ ├── errors-unexpected-sequence.yml │ │ ├── errors-max-file-size.yml │ │ ├── errors-parse-integer.yml │ │ ├── errors-parse-boolean.yml │ │ ├── errors-parse-float.yml │ │ ├── errors-max-result-size.yml │ │ ├── errors-job-concurrency.yml │ │ ├── errors-job-id-unique.yml │ │ ├── job-container-invalid-credentials.yml │ │ ├── errors-expression-not-closed.yml │ │ ├── errors-matrix-empty-vector.yml │ │ ├── errors-invalid-permissions.yml │ │ ├── errors-unclosed-tokens.yml │ │ ├── errors-yaml-invalid-style.yml │ │ ├── errors-on-workflow_call-output.yml │ │ ├── errors-max-depth.yml │ │ ├── errors-yaml-tags-explicit-unsupported.yml │ │ ├── events-single-invalid-config.yml │ │ ├── errors-job-needs-no-start-node.yml │ │ ├── error.yml │ │ ├── errors-on-workflow_call-input-type-missing.yml │ │ ├── errors-step-if.yml │ │ ├── errors-reusable-workflow-job-inputs-undefined.yml │ │ ├── errors-on-workflow_call-input-unexpected-property.yml │ │ ├── errors-reusable-workflow-job-secrets-undefined.yml │ │ ├── on-workflow_call.yml │ │ ├── workflow-name.yml │ │ ├── on-workflow_dispatch-string.yml │ │ ├── job-container-missing-image.yml │ │ ├── on-workflow_dispatch-null.yml │ │ ├── errors-reusable-workflow-job-secrets-required.yml │ │ ├── on-workflow_dispatch-mapping.yml │ │ ├── workflow-description.yml │ │ ├── on-workflow_call-sequence.yml │ │ ├── errors-reusable-workflow-job-inputs-required.yml │ │ ├── on-workflow_dispatch-inputs-null.yml │ │ ├── events-single-invalid.yml │ │ ├── on-workflow_dispatch-unknown-keys-ignored.yml │ │ ├── on-event-config-string-image-version.yml │ │ ├── errors-reusable-workflow-max-result-size.yml │ │ ├── errors-expression-not-allowed.yml │ │ ├── errors-max-job-limit-with-reusable-workflow.yml │ │ ├── workflow-concurrency-expression.yml │ │ ├── errors-job-needs-cycle.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-pages-write.yml │ │ ├── job-snapshot-simple.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-checks-write.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-issues-write.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-job-level-from-caller-default.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-actions-write.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-contents-write.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-packages-write.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-statuses-write.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-job-level-from-caller-job-level.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-job-level-from-caller-workflow-level.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-deployments-write.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-discussions-write.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-pull-requests-write.yml │ │ ├── on-event-config-image-version-name-version.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-security-events-write.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-workflow-level-from-caller-job-level.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-workflow-level-from-caller-workflow-level.yml │ │ ├── errors-job-needs-unknown-job.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-repository-projects-write.yml │ │ ├── step-id.yml │ │ ├── on-event-config-image-version-names.yml │ │ ├── basic.yml │ │ ├── errors-job-id-leading-underscores.yml │ │ ├── errors-reusable-workflow-job-inputs-type-mismatch.yml │ │ ├── on-event-config-image-version-versions.yml │ │ ├── errors-reusable-workflow-job-secrets.yml │ │ ├── errors-insert.yml │ │ ├── job-with-outputs-context.yml │ │ ├── errors-reusable-workflow-job-nested-depth-exceeded.yml │ │ ├── job-outputs.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-workflow-level-from-caller-default.yml │ │ ├── errors-step-uses-syntax.yml │ │ ├── events-single.yml │ │ ├── errors-required-property-missing.yml │ │ ├── on-workflow_call-mapping.yml │ │ ├── workflow-concurrency-mapping.yml │ │ ├── errors-job-runs-on-group-invalid-prefix.yml │ │ ├── events-sequence.yml │ │ ├── float-folded-style.yml │ │ ├── events-mapping-repo-dispatch.yml │ │ ├── errors-reusable-workflow-job-nested-permissions-not-allowed-job-level-from-caller-job-level.yml │ │ ├── errors-reusable-workflow-job-nested-permissions-not-allowed-job-level-from-caller-workflow-level.yml │ │ ├── id-to-long.yml │ │ ├── workflow-defaults.yml │ │ ├── errors-reusable-workflow-job-nested-permissions-not-allowed-workflow-level-from-caller-workflow-level.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-read-all-allowed-none.yml │ │ ├── max-result-size.yml │ │ ├── errors-reusable-workflow-job-nested-permissions-not-allowed-workflow-level-from-caller-job-level.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-write-all-allowed-none.yml │ │ ├── events-single-with-types.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-write-all-allowed-read-all.yml │ │ ├── errors-reusable-workflow-permissions-not-allowed-request-id-token-write.yml │ │ ├── job-container-invalid.yml │ │ ├── job-basic.yml │ │ ├── step-continue-on-error.yml │ │ ├── reusable-workflow-no-inputs.yml │ │ ├── job-snapshot-mapping.yml │ │ └── reusable-workflow-job-mvp.yml ├── tsconfig.build.json ├── jest.config.js ├── .eslintrc.cjs ├── .vscode │ └── launch.json └── tsconfig.json ├── .prettierignore ├── languageserver ├── .prettierignore ├── src │ ├── commands.ts │ ├── request.ts │ ├── client.ts │ ├── index.test.ts │ ├── utils │ │ ├── timer.ts │ │ ├── username.ts │ │ ├── error.ts │ │ └── cache.ts │ ├── context-providers │ │ └── action-outputs.ts │ ├── index.ts │ ├── description-providers │ │ └── action-description.ts │ ├── description-provider.ts │ ├── value-providers │ │ └── job-environment.ts │ ├── test-utils │ │ └── workflow-context.ts │ └── on-completion.ts ├── bin │ └── actions-languageserver ├── .eslintrc.cjs ├── tsconfig.build.json ├── jest.config.js └── tsconfig.json ├── languageservice ├── .prettierignore ├── src │ ├── nulltrace.ts │ ├── value-providers │ │ ├── strings-to-values.ts │ │ ├── needs.ts │ │ ├── config.ts │ │ ├── reusable-job-inputs.ts │ │ └── default.ts │ ├── test-utils │ │ ├── document.ts │ │ ├── logger.ts │ │ ├── test-workflow-context.ts │ │ └── cursor-position.ts │ ├── expression-hover │ │ └── pos-range.ts │ ├── index.ts │ ├── context-providers │ │ ├── config.ts │ │ ├── descriptionsSchema.json │ │ ├── secrets.ts │ │ ├── descriptions.ts │ │ └── env.ts │ ├── expression-validation │ │ ├── functions.ts │ │ └── error-dictionary.ts │ ├── utils │ │ ├── expression-detection.ts │ │ ├── range.ts │ │ └── scalar-to-data.ts │ └── e2e.test.ts ├── .eslintrc.cjs ├── tsconfig.build.json ├── jest.config.js └── tsconfig.json ├── browser-playground ├── .prettierignore ├── src │ ├── service-worker │ │ ├── service-worker.ts │ │ └── tsconfig.json │ └── client │ │ └── tsconfig.json ├── .eslintrc.cjs ├── public │ └── index.html ├── tsconfig.json ├── README.md └── package.json ├── .prettierrc.json ├── lerna.json ├── .vscode ├── settings.json └── launch.json ├── .gitignore ├── package.json ├── .eslintrc.json ├── script └── workflows │ ├── increment-version.sh │ └── generate-release-notes.sh └── LICENSE /expressions/script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm test -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @actions/actions-vscode-reviewers 2 | -------------------------------------------------------------------------------- /expressions/script/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run build -------------------------------------------------------------------------------- /workflow-parser/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | -------------------------------------------------------------------------------- /expressions/.npmrc: -------------------------------------------------------------------------------- 1 | @github:registry=https://npm.pkg.github.com 2 | 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.md 4 | *.js 5 | *.json 6 | *.d.ts -------------------------------------------------------------------------------- /workflow-parser/.npmrc: -------------------------------------------------------------------------------- 1 | @github:registry=https://npm.pkg.github.com 2 | 3 | -------------------------------------------------------------------------------- /expressions/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.md 4 | *.js 5 | *.json 6 | -------------------------------------------------------------------------------- /languageserver/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.md 4 | *.js 5 | *.json 6 | -------------------------------------------------------------------------------- /languageservice/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.md 4 | *.js 5 | *.json 6 | -------------------------------------------------------------------------------- /workflow-parser/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.md 4 | *.js 5 | *.json 6 | -------------------------------------------------------------------------------- /browser-playground/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.md 4 | *.js 5 | *.json 6 | -------------------------------------------------------------------------------- /browser-playground/src/service-worker/service-worker.ts: -------------------------------------------------------------------------------- 1 | import "@actions/languageserver"; 2 | -------------------------------------------------------------------------------- /expressions/testdata/coerce_boolean.json: -------------------------------------------------------------------------------- 1 | { 2 | "refer ./operator_not.json": [ 3 | ] 4 | } -------------------------------------------------------------------------------- /languageserver/src/commands.ts: -------------------------------------------------------------------------------- 1 | export enum Commands { 2 | ClearCache = "cacheClear" 3 | } 4 | -------------------------------------------------------------------------------- /languageserver/bin/actions-languageserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "../dist/cli.bundle.cjs"; 3 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/schema/index.ts: -------------------------------------------------------------------------------- 1 | export {TemplateSchema} from "./template-schema.js"; 2 | -------------------------------------------------------------------------------- /workflow-parser/src/workflows/workflow-constants.ts: -------------------------------------------------------------------------------- 1 | export const WORKFLOW_ROOT = "workflow-root-strict"; 2 | -------------------------------------------------------------------------------- /workflow-parser/src/workflows/file.ts: -------------------------------------------------------------------------------- 1 | export interface File { 2 | name: string; 3 | content: string; 4 | } 5 | -------------------------------------------------------------------------------- /expressions/src/filtered_array.ts: -------------------------------------------------------------------------------- 1 | import * as data from "./data/index.js"; 2 | 3 | export class FilteredArray extends data.Array {} 4 | -------------------------------------------------------------------------------- /browser-playground/src/service-worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["ES2022", "WebWorker"], 5 | } 6 | } -------------------------------------------------------------------------------- /languageserver/src/request.ts: -------------------------------------------------------------------------------- 1 | export const Requests = { 2 | ReadFile: "actions/readFile" 3 | } as const; 4 | 5 | export type ReadFileRequest = { 6 | path: string; 7 | }; 8 | -------------------------------------------------------------------------------- /workflow-parser/src/test-utils/null-trace.ts: -------------------------------------------------------------------------------- 1 | import {NoOperationTraceWriter} from "../templates/trace-writer.js"; 2 | 3 | export const nullTrace = new NoOperationTraceWriter(); 4 | -------------------------------------------------------------------------------- /languageservice/src/nulltrace.ts: -------------------------------------------------------------------------------- 1 | import {NoOperationTraceWriter} from "@actions/workflow-parser/templates/trace-writer"; 2 | 3 | export const nullTrace = new NoOperationTraceWriter(); 4 | -------------------------------------------------------------------------------- /languageserver/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "../.eslintrc.json", 3 | parserOptions: { 4 | project: './tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | }, 7 | } -------------------------------------------------------------------------------- /languageservice/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "../.eslintrc.json", 3 | parserOptions: { 4 | project: './tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | }, 7 | } -------------------------------------------------------------------------------- /browser-playground/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "../.eslintrc.json", 3 | parserOptions: { 4 | project: './tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | }, 7 | } -------------------------------------------------------------------------------- /languageservice/src/value-providers/strings-to-values.ts: -------------------------------------------------------------------------------- 1 | import {Value} from "./config.js"; 2 | 3 | export function stringsToValues(labels: string[]): Value[] { 4 | return labels.map(x => ({label: x})); 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "singleQuote": false, 6 | "bracketSpacing": false, 7 | "trailingComma": "none", 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/schema/definition-type.ts: -------------------------------------------------------------------------------- 1 | export enum DefinitionType { 2 | Null, 3 | Boolean, 4 | Number, 5 | String, 6 | Sequence, 7 | Mapping, 8 | OneOf, 9 | AllowedValues 10 | } 11 | -------------------------------------------------------------------------------- /workflow-parser/src/workflows/file-provider.ts: -------------------------------------------------------------------------------- 1 | import {File} from "./file.js"; 2 | import {FileReference} from "./file-reference.js"; 3 | 4 | export interface FileProvider { 5 | getFileContent(ref: FileReference): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /browser-playground/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["ES2022", "DOM"] 5 | }, 6 | "references": [ 7 | { "path": "../service-worker" } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "packages": [ 4 | "expressions", 5 | "workflow-parser", 6 | "languageservice", 7 | "languageserver" 8 | ], 9 | "version": "0.3.27" 10 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/Thumbs.db": true, 9 | // "dist": true, 10 | } 11 | } -------------------------------------------------------------------------------- /languageservice/src/test-utils/document.ts: -------------------------------------------------------------------------------- 1 | import {TextDocument} from "vscode-languageserver-textdocument"; 2 | 3 | export function createDocument(fileName: string, content: string): TextDocument { 4 | return TextDocument.create("test://test/" + fileName, "yaml", 0, content); 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | */dist 3 | lerna-debug.log 4 | node_modules 5 | .DS_Store 6 | 7 | # Minified JSON (generated at build time) 8 | *.min.json 9 | 10 | # Intermediate JSON for size comparison (generated by update-webhooks --all) 11 | *.all.json 12 | *.drop.json 13 | *.strip.json -------------------------------------------------------------------------------- /languageserver/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["./src/**/*.test.ts", "./src/test-utils"], 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "noEmit": false, 8 | "outDir": "./dist" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /workflow-parser/testdata/sync-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TEST_DATA_DIR="$HOME/github/actions-workflow-parser/testdata/reader" 4 | 5 | REPO_ROOT="$(git rev-parse --show-toplevel)" 6 | 7 | DEST_DIR="$REPO_ROOT/actions-workflow-parser/testdata/reader" 8 | 9 | cp -f "$TEST_DATA_DIR"/* "$DEST_DIR"/ 10 | -------------------------------------------------------------------------------- /languageserver/src/client.ts: -------------------------------------------------------------------------------- 1 | import {Octokit} from "@octokit/rest"; 2 | 3 | export function getClient(token: string, userAgent?: string, apiUrl?: string): Octokit { 4 | return new Octokit({ 5 | auth: token, 6 | userAgent: userAgent || `GitHub Actions Language Server`, 7 | baseUrl: apiUrl 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-jobs-missing.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | --- 5 | { 6 | "errors": [ 7 | { 8 | "Message": ".github/workflows/errors-jobs-missing.yml (Line: 1, Col: 1): Required property is missing: jobs" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /expressions/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "../.eslintrc.json", 3 | parserOptions: { 4 | project: './tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | }, 7 | rules: { 8 | // Conflicts with the expression data Array class 9 | "@typescript-eslint/no-array-constructor": "off", 10 | }, 11 | } -------------------------------------------------------------------------------- /workflow-parser/src/templates/tokens/token-range.ts: -------------------------------------------------------------------------------- 1 | /** Represents the position within a template object */ 2 | export type Position = { 3 | /** The one-based line value */ 4 | line: number; 5 | 6 | /** The one-based column value */ 7 | column: number; 8 | }; 9 | 10 | export type TokenRange = { 11 | start: Position; 12 | end: Position; 13 | }; 14 | -------------------------------------------------------------------------------- /expressions/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["./src/**/*.test.ts"], 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "module": "node16", 6 | "moduleResolution": "node16", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "noEmit": false, 10 | "outDir": "./dist", 11 | "skipLibCheck": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /expressions/src/data/null.ts: -------------------------------------------------------------------------------- 1 | import {ExpressionDataInterface, Kind} from "./expressiondata.js"; 2 | 3 | export class Null implements ExpressionDataInterface { 4 | public readonly kind = Kind.Null; 5 | 6 | public primitive = true; 7 | 8 | coerceString(): string { 9 | return ""; 10 | } 11 | 12 | number(): number { 13 | return 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /expressions/src/funcs/info.ts: -------------------------------------------------------------------------------- 1 | import {ExpressionData} from "../data/index.js"; 2 | 3 | export interface FunctionInfo { 4 | name: string; 5 | 6 | description?: string; 7 | 8 | minArgs: number; 9 | maxArgs: number; 10 | } 11 | 12 | export interface FunctionDefinition extends FunctionInfo { 13 | call: (...args: ExpressionData[]) => ExpressionData; 14 | } 15 | -------------------------------------------------------------------------------- /workflow-parser/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["./src/**/*.test.ts"], 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "module": "node16", 6 | "moduleResolution": "node16", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "noEmit": false, 10 | "outDir": "./dist", 11 | "skipLibCheck": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /languageservice/src/expression-hover/pos-range.ts: -------------------------------------------------------------------------------- 1 | import {Pos, Range} from "@actions/expressions/lexer"; 2 | 3 | export function posWithinRange(pos: Pos, range: Range): boolean { 4 | return ( 5 | pos.line >= range.start.line && 6 | pos.line <= range.end.line && 7 | pos.column >= range.start.column && 8 | pos.column <= range.end.column 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /workflow-parser/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: "ts-jest/presets/default-esm", 4 | moduleNameMapper: { 5 | "^(\\.{1,2}/.*)\\.js$": "$1", 6 | }, 7 | transform: { 8 | "^.+\\.tsx?$": [ 9 | "ts-jest", 10 | { 11 | useESM: true, 12 | }, 13 | ], 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /workflow-parser/src/index.ts: -------------------------------------------------------------------------------- 1 | export {convertWorkflowTemplate} from "./model/convert.js"; 2 | export {WorkflowTemplate} from "./model/workflow-template.js"; 3 | export * from "./templates/tokens/type-guards.js"; 4 | export {NoOperationTraceWriter, TraceWriter} from "./templates/trace-writer.js"; 5 | export {parseWorkflow, ParseWorkflowResult} from "./workflows/workflow-parser.js"; 6 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-jobs-at-least-one.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: {} 5 | --- 6 | { 7 | "errors": [ 8 | { 9 | "Message": ".github/workflows/errors-jobs-at-least-one.yml (Line: 2, Col: 7): The workflow must contain at least one job with no dependencies." 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /workflow-parser/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "../.eslintrc.json", 3 | parserOptions: { 4 | project: './tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | }, 7 | rules: { 8 | // TypeScript doesn't correctly detect toString() implementations on the token classes 9 | "@typescript-eslint/restrict-template-expressions": "off", 10 | } 11 | } -------------------------------------------------------------------------------- /languageservice/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["./src/**/*.test.ts", "./src/test-utils"], 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "module": "node16", 6 | "moduleResolution": "node16", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "noEmit": false, 10 | "outDir": "./dist", 11 | "skipLibCheck": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /workflow-parser/src/model/converter/string-list.ts: -------------------------------------------------------------------------------- 1 | import {SequenceToken} from "../../templates/tokens/sequence-token.js"; 2 | 3 | export function convertStringList(name: string, token: SequenceToken): string[] { 4 | const result = [] as string[]; 5 | 6 | for (const item of token) { 7 | result.push(item.assertString(`${name} item`).value); 8 | } 9 | 10 | return result; 11 | } 12 | -------------------------------------------------------------------------------- /languageserver/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import {validate} from "@actions/languageservice"; 2 | import {TextDocument} from "vscode-languageserver-textdocument"; 3 | 4 | describe("simple test", () => { 5 | it("should work", async () => { 6 | const doc = TextDocument.create("uri", "workflow", 1, "on: push"); 7 | 8 | const r = await validate(doc); 9 | expect(r).not.toBeNull(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /expressions/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: "ts-jest/presets/default-esm", 4 | moduleNameMapper: { 5 | "^(\\.{1,2}/.*)\\.js$": "$1", 6 | }, 7 | transform: { 8 | "^.+\\.tsx?$": [ 9 | "ts-jest", 10 | { 11 | useESM: true, 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ["ts", "js"], 16 | }; 17 | -------------------------------------------------------------------------------- /languageserver/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: "ts-jest/presets/default-esm", 4 | moduleNameMapper: { 5 | "^(\\.{1,2}/.*)\\.js$": "$1", 6 | }, 7 | transform: { 8 | "^.+\\.tsx?$": [ 9 | "ts-jest", 10 | { 11 | useESM: true, 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ["ts", "js"], 16 | }; 17 | -------------------------------------------------------------------------------- /languageserver/src/utils/timer.ts: -------------------------------------------------------------------------------- 1 | import {log} from "@actions/languageservice/log"; 2 | 3 | export async function timeOperation(name: string, f: () => T): Promise { 4 | const start = Date.now(); 5 | const result = f(); 6 | if (result instanceof Promise) { 7 | await result; 8 | } 9 | 10 | const end = Date.now(); 11 | 12 | log(`${name} took ${end - start}ms`); 13 | return result; 14 | } 15 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/tokens/key-value-pair.ts: -------------------------------------------------------------------------------- 1 | import {ScalarToken} from "./scalar-token.js"; 2 | import {TemplateToken} from "./template-token.js"; 3 | 4 | export class KeyValuePair { 5 | public readonly key: ScalarToken; 6 | public readonly value: TemplateToken; 7 | public constructor(key: ScalarToken, value: TemplateToken) { 8 | this.key = key; 9 | this.value = value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actions-languageservices", 3 | "private": true, 4 | "type": "module", 5 | "workspaces": [ 6 | "./expressions", 7 | "./workflow-parser", 8 | "./languageservice", 9 | "./languageserver" 10 | ], 11 | "devDependencies": { 12 | "lerna": "^8.2.2", 13 | "typescript": "5.8.3" 14 | }, 15 | "overrides": { 16 | "typescript": "$typescript" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/schema/scalar-definition.ts: -------------------------------------------------------------------------------- 1 | import {LiteralToken, MappingToken} from "../tokens/index.js"; 2 | import {Definition} from "./definition.js"; 3 | 4 | export abstract class ScalarDefinition extends Definition { 5 | public constructor(key: string, definition?: MappingToken) { 6 | super(key, definition); 7 | } 8 | 9 | public abstract isMatch(literal: LiteralToken): boolean; 10 | } 11 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-empty-expression.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo ${{ }} 9 | --- 10 | { 11 | "errors": [ 12 | { 13 | "Message": ".github/workflows/errors-empty-expression.yml (Line: 6, Col: 14): An expression was expected" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-job-id-empty.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | name: CI 4 | on: 5 | push: 6 | jobs: 7 | "": 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo hi 11 | --- 12 | { 13 | "errors": [ 14 | { 15 | "Message": ".github/workflows/errors-job-id-empty.yml (Line: 5, Col: 3): Unexpected value ''" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-job-runs-on-and-uses.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | uses: ./.github/workflows/foo.yml 8 | --- 9 | { 10 | "errors": [ 11 | { 12 | "Message": ".github/workflows/errors-job-runs-on-and-uses.yml (Line: 5, Col: 5): Unexpected value 'uses'" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-step-run-missing.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - shell: foo 9 | --- 10 | { 11 | "errors": [ 12 | { 13 | "Message": ".github/workflows/errors-step-run-missing.yml (Line: 6, Col: 9): Required property is missing: run" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-unexpected-mapping.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | not: a-sequence 9 | --- 10 | { 11 | "errors": [ 12 | { 13 | "Message": ".github/workflows/errors-unexpected-mapping.yml (Line: 6, Col: 7): A mapping was not expected" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /browser-playground/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GitHub Actions Language Services 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /expressions/src/data/index.ts: -------------------------------------------------------------------------------- 1 | export {Array} from "./array.js"; 2 | export {BooleanData} from "./boolean.js"; 3 | export {Dictionary} from "./dictionary.js"; 4 | export {ExpressionData, Kind} from "./expressiondata.js"; 5 | export {Null} from "./null.js"; 6 | export {NumberData} from "./number.js"; 7 | export {replacer} from "./replacer.js"; 8 | export {reviver} from "./reviver.js"; 9 | export {StringData} from "./string.js"; 10 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-job-runs-on-missing.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: # missing runs-on 6 | steps: 7 | - run: echo hi 8 | --- 9 | { 10 | "errors": [ 11 | { 12 | "Message": ".github/workflows/errors-job-runs-on-missing.yml (Line: 4, Col: 5): Required property is missing: runs-on" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-invalid-mapping-key.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - null : ' ' 9 | run: echo {{ 😀 }} 10 | --- 11 | { 12 | "errors": [ 13 | { 14 | "Message": ".github/workflows/errors-invalid-mapping-key.yml (Line: 6, Col: 9): Unexpected value ''" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-step-uses-missing.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - with: 9 | foo: bar 10 | --- 11 | { 12 | "errors": [ 13 | { 14 | "Message": ".github/workflows/errors-step-uses-missing.yml (Line: 6, Col: 9): Required property is missing: uses" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /expressions/src/funcs/format.test.ts: -------------------------------------------------------------------------------- 1 | import {Null, NumberData, StringData} from "../data/index.js"; 2 | import {format} from "./format.js"; 3 | 4 | describe("format", () => { 5 | it("null", () => { 6 | expect(format.call(new StringData("{0}"), new Null())).toEqual(new StringData("")); 7 | }); 8 | it("number", () => { 9 | expect(format.call(new StringData("{0}"), new NumberData(42))).toEqual(new StringData("42")); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-anchor-tag.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - TypeScript 4 | --- 5 | on: push 6 | jobs: 7 | bad-anchor-tag: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: &echo hi 11 | --- 12 | { 13 | "errors": [ 14 | { 15 | "Message": ".github/workflows/errors-anchor-tag.yml: Anchors are not currently supported. Remove the anchor 'echo'" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /expressions/src/data/string.ts: -------------------------------------------------------------------------------- 1 | import {ExpressionDataInterface, Kind} from "./expressiondata.js"; 2 | 3 | export class StringData implements ExpressionDataInterface { 4 | constructor(public readonly value: string) {} 5 | 6 | public readonly kind = Kind.String; 7 | 8 | public primitive = true; 9 | 10 | coerceString(): string { 11 | return this.value; 12 | } 13 | 14 | number(): number { 15 | return Number(this.value); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /languageservice/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | preset: "ts-jest/presets/default-esm", 4 | moduleNameMapper: { 5 | "^(\\.{1,2}/.*)\\.js$": "$1", 6 | }, 7 | transform: { 8 | "^.+\\.tsx?$": [ 9 | "ts-jest", 10 | { 11 | useESM: true, 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ["ts", "js"], 16 | reporters: ["default", "github-actions"] 17 | }; 18 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-value-already-defined.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | runs-on: duplicate-key 8 | steps: 9 | - run: echo hi 10 | --- 11 | { 12 | "errors": [ 13 | { 14 | "Message": ".github/workflows/errors-value-already-defined.yml (Line: 5, Col: 5): 'runs-on' is already defined" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /languageservice/src/test-utils/logger.ts: -------------------------------------------------------------------------------- 1 | import {Logger} from "../log.js"; 2 | 3 | export class TestLogger implements Logger { 4 | error(message: string): void { 5 | throw new Error(`Error: ${message}`); 6 | } 7 | 8 | warn(message: string): void { 9 | console.warn(message); 10 | } 11 | 12 | info(message: string): void { 13 | console.info(message); 14 | } 15 | 16 | log(message: string): void { 17 | console.warn(message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /languageservice/src/index.ts: -------------------------------------------------------------------------------- 1 | export {complete} from "./complete.js"; 2 | export {ContextProviderConfig} from "./context-providers/config.js"; 3 | export {documentLinks} from "./document-links.js"; 4 | export {hover} from "./hover.js"; 5 | export {Logger, LogLevel, registerLogger, setLogLevel} from "./log.js"; 6 | export {validate, ValidationConfig, ActionsMetadataProvider} from "./validate.js"; 7 | export {ValueProviderConfig, ValueProviderKind} from "./value-providers/config.js"; 8 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/trace-writer.ts: -------------------------------------------------------------------------------- 1 | export interface TraceWriter { 2 | error(message: string): void; 3 | 4 | info(message: string): void; 5 | 6 | verbose(message: string): void; 7 | } 8 | 9 | export class NoOperationTraceWriter implements TraceWriter { 10 | public error(): void { 11 | // do nothing 12 | } 13 | 14 | public info(): void { 15 | // do nothing 16 | } 17 | 18 | public verbose(): void { 19 | // do nothing 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-matrix-bad-key.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | bad-strategy-key: 6 | strategy: 7 | bad-key: 8 | os: [10] 9 | runs-on: macos-latest 10 | steps: 11 | - run: echo hi 12 | --- 13 | { 14 | "errors": [ 15 | { 16 | "Message": ".github/workflows/errors-matrix-bad-key.yml (Line: 5, Col: 7): Unexpected value 'bad-key'" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-unexpected-sequence.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | name: 4 | - no 5 | - sequence 6 | - here 7 | on: push 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - run: echo hi 13 | --- 14 | { 15 | "errors": [ 16 | { 17 | "Message": ".github/workflows/errors-unexpected-sequence.yml (Line: 2, Col: 3): A sequence was not expected" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /languageservice/src/context-providers/config.ts: -------------------------------------------------------------------------------- 1 | import {DescriptionDictionary} from "@actions/expressions"; 2 | import {WorkflowContext} from "../context/workflow-context.js"; 3 | import {Mode} from "./default.js"; 4 | 5 | export type ContextProviderConfig = { 6 | getContext: ( 7 | name: string, 8 | defaultContext: DescriptionDictionary | undefined, 9 | workflowContext: WorkflowContext, 10 | mode: Mode 11 | ) => Promise; 12 | }; 13 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-max-file-size.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | max-file-size: 124 3 | --- 4 | on: push 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - run: echo Building... 11 | --- 12 | { 13 | "errors": [ 14 | { 15 | "Message": ".github/workflows/errors-max-file-size.yml: The maximum file size of 124 characters has been exceeded" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-parse-integer.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - TypeScript 4 | --- 5 | on: push 6 | jobs: 7 | parse-int-error: 8 | runs-on: !!int test 9 | steps: 10 | - uses: actions/checkout@v2 11 | --- 12 | { 13 | "errors": [ 14 | { 15 | "Message": ".github/workflows/errors-parse-integer.yml: The value 'test' on line 4 and column 14 is invalid for the type 'tag:yaml.org,2002:int'" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /languageservice/src/value-providers/needs.ts: -------------------------------------------------------------------------------- 1 | import {WorkflowContext} from "../context/workflow-context.js"; 2 | import {Value} from "./config.js"; 3 | 4 | export function needs(context: WorkflowContext): Value[] { 5 | if (!context.template) { 6 | return []; 7 | } 8 | 9 | const uniquejobIDs = new Set(context.template.jobs.map(j => j.id)).values(); 10 | return Array.from(uniquejobIDs) 11 | .filter(x => x.value !== context.job?.id.value) 12 | .map(x => ({label: x.value})); 13 | } 14 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-parse-boolean.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - TypeScript 4 | --- 5 | on: push 6 | jobs: 7 | parse-bool-error: 8 | runs-on: !!bool test 9 | steps: 10 | - uses: actions/checkout@v2 11 | --- 12 | { 13 | "errors": [ 14 | { 15 | "Message": ".github/workflows/errors-parse-boolean.yml: The value 'test' on line 4 and column 14 is invalid for the type 'tag:yaml.org,2002:bool'" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-parse-float.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - TypeScript 4 | --- 5 | on: push 6 | jobs: 7 | parse-float-error: 8 | runs-on: !!float test 9 | steps: 10 | - uses: actions/checkout@v2 11 | --- 12 | { 13 | "errors": [ 14 | { 15 | "Message": ".github/workflows/errors-parse-float.yml: The value 'test' on line 4 and column 14 is invalid for the type 'tag:yaml.org,2002:float'" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-max-result-size.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | max-result-size: 1141 3 | --- 4 | on: push 5 | jobs: 6 | job1: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo Deploying 1... 10 | - run: echo Deploying 2... 11 | - run: echo Deploying 3... 12 | --- 13 | { 14 | "errors": [ 15 | { 16 | "Message": ".github/workflows/errors-max-result-size.yml: Maximum object size exceeded" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /expressions/testdata/op_gte.json: -------------------------------------------------------------------------------- 1 | { 2 | "bool": [ 3 | { 4 | "expr": "false >= true", 5 | "result": { "kind": "Boolean", "value": false } 6 | }, 7 | { 8 | "expr": "false >= false", 9 | "result": { "kind": "Boolean", "value": true } 10 | }, 11 | { 12 | "expr": "true >= false", 13 | "result": { "kind": "Boolean", "value": true } 14 | }, 15 | { 16 | "expr": "true >= true", 17 | "result": { "kind": "Boolean", "value": true } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /expressions/testdata/op_lte.json: -------------------------------------------------------------------------------- 1 | { 2 | "bool": [ 3 | { 4 | "expr": "false <= true", 5 | "result": { "kind": "Boolean", "value": true } 6 | }, 7 | { 8 | "expr": "false <= false", 9 | "result": { "kind": "Boolean", "value": true } 10 | }, 11 | { 12 | "expr": "true <= false", 13 | "result": { "kind": "Boolean", "value": false } 14 | }, 15 | { 16 | "expr": "true <= true", 17 | "result": { "kind": "Boolean", "value": true } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-job-concurrency.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | concurrency: ${{ secrets.foo }} 8 | steps: 9 | - run: echo hi 10 | --- 11 | { 12 | "errors": [ 13 | { 14 | "Message": ".github/workflows/errors-job-concurrency.yml (Line: 5, Col: 18): Unrecognized named-value: 'secrets'. Located at position 1 within expression: secrets.foo" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-job-id-unique.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | name: CI 4 | on: 5 | push: 6 | jobs: 7 | my-job: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo hi 11 | MY-JOB: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - run: echo hi 15 | --- 16 | { 17 | "errors": [ 18 | { 19 | "Message": ".github/workflows/errors-job-id-unique.yml (Line: 9, Col: 3): 'MY-JOB' is already defined" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/job-container-invalid-credentials.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: linux 7 | container: 8 | image: node:14.16 9 | credentials: 10 | badkey: somevalue 11 | steps: 12 | - run: echo hi 13 | --- 14 | { 15 | "errors": [ 16 | { 17 | "Message": ".github/workflows/job-container-invalid-credentials.yml (Line: 8, Col: 9): Unexpected value 'badkey'" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-expression-not-closed.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo ${{ github.event_name 9 | --- 10 | { 11 | "errors": [ 12 | { 13 | "Message": ".github/workflows/errors-expression-not-closed.yml (Line: 6, Col: 14): The expression is not closed. An unescaped ${{ sequence was found, but the closing }} sequence was not found." 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-matrix-empty-vector.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | empty-vector: 6 | strategy: 7 | matrix: 8 | os: [] 9 | version: [10,12] 10 | runs-on: macos-latest 11 | steps: 12 | - run: echo hi 13 | --- 14 | { 15 | "errors": [ 16 | { 17 | "Message": ".github/workflows/errors-matrix-empty-vector.yml (Line: 6, Col: 13): Matrix vector 'os' does not contain any values" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Ask a question or provide feedback about the Actions Language Services 4 | about: For general Q&A and feedback, see the Discussions tab. 5 | url: https://github.com/actions/languageservices/discussions 6 | - name: Ask a question or provide feedback about GitHub Actions 7 | about: Please check out the GitHub community forum for discussions about GitHub Actions 8 | url: https://github.com/orgs/community/discussions/categories/actions 9 | -------------------------------------------------------------------------------- /expressions/src/index.ts: -------------------------------------------------------------------------------- 1 | export {Expr} from "./ast.js"; 2 | export {complete, CompletionItem} from "./completion.js"; 3 | export {DescriptionDictionary, DescriptionPair, isDescriptionDictionary} from "./completion/descriptionDictionary.js"; 4 | export * as data from "./data/index.js"; 5 | export {ExpressionError, ExpressionEvaluationError} from "./errors.js"; 6 | export {Evaluator} from "./evaluator.js"; 7 | export {wellKnownFunctions} from "./funcs.js"; 8 | export {Lexer, Result} from "./lexer.js"; 9 | export {Parser} from "./parser.js"; 10 | -------------------------------------------------------------------------------- /languageserver/src/context-providers/action-outputs.ts: -------------------------------------------------------------------------------- 1 | import {ActionOutputs, ActionReference} from "@actions/languageservice/action"; 2 | import {Octokit} from "@octokit/rest"; 3 | import {fetchActionMetadata} from "../utils/action-metadata"; 4 | import {TTLCache} from "../utils/cache"; 5 | 6 | export async function getActionOutputs( 7 | octokit: Octokit, 8 | cache: TTLCache, 9 | action: ActionReference 10 | ): Promise { 11 | return (await fetchActionMetadata(octokit, cache, action))?.outputs; 12 | } 13 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-invalid-permissions.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | permissions: 5 | contents: invalid 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - run: echo hi 12 | continue-on-error: true 13 | --- 14 | { 15 | "errors": [ 16 | { 17 | "Message": ".github/workflows/errors-invalid-permissions.yml (Line: 3, Col: 13): Unexpected value 'invalid'" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /workflow-parser/src/workflows/workflow-schema.ts: -------------------------------------------------------------------------------- 1 | import {JSONObjectReader} from "../templates/json-object-reader.js"; 2 | import {TemplateSchema} from "../templates/schema/index.js"; 3 | import WorkflowSchema from "../workflow-v1.0.min.json"; 4 | 5 | let schema: TemplateSchema; 6 | 7 | export function getWorkflowSchema(): TemplateSchema { 8 | if (schema === undefined) { 9 | const json = JSON.stringify(WorkflowSchema); 10 | schema = TemplateSchema.load(new JSONObjectReader(undefined, json)); 11 | } 12 | return schema; 13 | } 14 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/parse-event.ts: -------------------------------------------------------------------------------- 1 | import {TemplateToken} from "./tokens/index.js"; 2 | 3 | export class ParseEvent { 4 | public readonly type: EventType; 5 | public readonly token: TemplateToken | undefined; 6 | public constructor(type: EventType, token?: TemplateToken | undefined) { 7 | this.type = type; 8 | this.token = token; 9 | } 10 | } 11 | 12 | export enum EventType { 13 | Literal, 14 | SequenceStart, 15 | SequenceEnd, 16 | MappingStart, 17 | MappingEnd, 18 | DocumentStart, 19 | DocumentEnd 20 | } 21 | -------------------------------------------------------------------------------- /workflow-parser/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Jest Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": [ 9 | "--inspect-brk", 10 | "${workspaceRoot}/node_modules/jest/bin/jest.js", 11 | "--runInBand" 12 | ], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "env": { 16 | "NODE_OPTIONS": "--experimental-vm-modules" 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-unclosed-tokens.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - TypeScript 4 | --- 5 | on: push 6 | jobs: 7 | build: 8 | if: ${{ contains('a', 'b' }} 9 | runs-on: ubuntu-latest 10 | steps: 11 | - run: echo hi 12 | 13 | --- 14 | { 15 | "errors": [ 16 | { 17 | "Message": ".github/workflows/errors-unclosed-tokens.yml (Line: 4, Col: 9): Unexpected end of expression: ''b''. Located at position 15 within expression: contains('a', 'b'" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-yaml-invalid-style.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - TypeScript 4 | --- 5 | on: push 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | cancel-timeout-minutes: !!int "300" 10 | steps: 11 | - run: echo hi 12 | --- 13 | { 14 | "errors": [ 15 | { 16 | "Message": ".github/workflows/errors-yaml-invalid-style.yml: The scalar style 'DoubleQuoted' on line 5 and column 29 is not valid with the tag 'tag:yaml.org,2002:int'" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /expressions/src/data/number.test.ts: -------------------------------------------------------------------------------- 1 | import {NumberData} from "./number.js"; 2 | 3 | describe("number", () => { 4 | it("coerces to string", () => { 5 | expect(new NumberData(-0).coerceString()).toEqual("0"); 6 | expect(new NumberData(0).coerceString()).toEqual("0"); 7 | expect(new NumberData(1).coerceString()).toEqual("1"); 8 | expect(new NumberData(1.2).coerceString()).toEqual("1.2"); 9 | 10 | // Round to 15 digits precision 11 | expect(new NumberData(1.2345678901234567).coerceString()).toEqual("1.234567890123457"); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /expressions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "ES2020", 6 | "lib": ["ES2022"], 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "moduleResolution": "node" 12 | }, 13 | "watchOptions": { 14 | "watchFile": "useFsEvents", 15 | "watchDirectory": "useFsEvents", 16 | "synchronousWatchDirectory": true, 17 | "excludeDirectories": ["**/node_modules", "dist"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /expressions/src/data/boolean.ts: -------------------------------------------------------------------------------- 1 | import {ExpressionDataInterface, Kind} from "./expressiondata.js"; 2 | 3 | export class BooleanData implements ExpressionDataInterface { 4 | constructor(public readonly value: boolean) {} 5 | 6 | public readonly kind = Kind.Boolean; 7 | 8 | public primitive = true; 9 | 10 | coerceString(): string { 11 | if (this.value) { 12 | return "true"; 13 | } 14 | 15 | return "false"; 16 | } 17 | 18 | number(): number { 19 | if (this.value) { 20 | return 1; 21 | } 22 | 23 | return 0; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-on-workflow_call-output.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_call: 8 | outputs: 9 | output1: 10 | description: foo 11 | jobs: 12 | my-job: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - run: echo hi 16 | --- 17 | { 18 | "errors": [ 19 | { 20 | "Message": ".github/workflows/errors-on-workflow_call-output.yml (Line: 8, Col: 9): Required property is missing: value" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /workflow-parser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "ES2020", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "sourceMap": true, 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true 12 | }, 13 | "watchOptions": { 14 | "watchFile": "useFsEvents", 15 | "watchDirectory": "useFsEvents", 16 | "synchronousWatchDirectory": true, 17 | "excludeDirectories": ["**/node_modules", "dist"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /languageservice/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "ES2020", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "sourceMap": true, 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true 12 | }, 13 | "watchOptions": { 14 | "watchFile": "useFsEvents", 15 | "watchDirectory": "useFsEvents", 16 | "synchronousWatchDirectory": true, 17 | "excludeDirectories": ["**/node_modules", "dist"], 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Actions Language Services 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /languageservice/src/expression-validation/functions.ts: -------------------------------------------------------------------------------- 1 | import {data, wellKnownFunctions} from "@actions/expressions"; 2 | 3 | // Custom implementations for standard actions-expression functions used during validation and auto-completion. 4 | // For example, for fromJson we'll most likely not have a valid input. In order to not throw, we'll always 5 | // return an empty dictionary. 6 | export const validatorFunctions = new Map( 7 | Object.entries({ 8 | fromjson: { 9 | ...wellKnownFunctions.fromjson, 10 | call: () => new data.Dictionary() 11 | } 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-max-depth.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | max-depth: 5 3 | --- 4 | on: push 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: 14 # Depth = 5, equals max-depth, raises error 13 | - run: echo Building... 14 | --- 15 | { 16 | "errors": [ 17 | { 18 | "Message": ".github/workflows/errors-max-depth.yml: Maximum object depth exceeded" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-yaml-tags-explicit-unsupported.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - TypeScript 4 | --- 5 | on: push 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: github/issue-labeler@v2.5 11 | with: 12 | not-before: !!timestamp 2022-10-26T00:00:00Z # explicitly set unsupported tag 13 | --- 14 | { 15 | "errors": [ 16 | { 17 | "Message": ".github/workflows/errors-yaml-tags-explicit-unsupported.yml: Unexpected tag 'tag:yaml.org,2002:timestamp'" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/events-single-invalid-config.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | - C# 5 | --- 6 | on: 7 | push: 8 | inputs: 9 | name: 10 | type: string 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - run: echo hi 17 | continue-on-error: true 18 | --- 19 | { 20 | "errors": [ 21 | { 22 | "Message": ".github/workflows/events-single-invalid-config.yml (Line: 3, Col: 5): Unexpected value 'inputs'" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "prettier" 11 | ], 12 | "ignorePatterns": ["**/.eslintrc.cjs", "**/node_modules/**", "**/dist/**", "**/jest.config.js"], 13 | "parser": "@typescript-eslint/parser", 14 | "plugins": ["@typescript-eslint", "prettier"], 15 | "reportUnusedDisableDirectives": true, 16 | "root": true, 17 | "rules": {} 18 | } 19 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-job-needs-no-start-node.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | 6 | one: 7 | needs: two 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo hello 11 | 12 | two: 13 | needs: one 14 | runs-on: ubuntu-latest 15 | steps: 16 | - run: echo hello 17 | --- 18 | { 19 | "errors": [ 20 | { 21 | "Message": ".github/workflows/errors-job-needs-no-start-node.yml (Line: 4, Col: 3): The workflow must contain at least one job with no dependencies." 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /languageserver/src/utils/username.ts: -------------------------------------------------------------------------------- 1 | import {Octokit} from "@octokit/rest"; 2 | import {TTLCache} from "./cache"; 3 | 4 | export async function getUsername(octokit: Octokit, cache: TTLCache): Promise { 5 | return await cache.get(`/username`, undefined, () => fetchUsername(octokit)); 6 | } 7 | 8 | async function fetchUsername(octokit: Octokit): Promise { 9 | try { 10 | const username = await octokit.request("GET /user").then(res => res.data.login); 11 | return username; 12 | } catch (e) { 13 | console.log("Failure to retrieve username: ", e); 14 | throw e; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /languageserver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "ES2020", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "sourceMap": true, 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "lib": ["es6", "webworker"], 12 | "resolveJsonModule": true 13 | }, 14 | "watchOptions": { 15 | "watchFile": "useFsEvents", 16 | "watchDirectory": "useFsEvents", 17 | "synchronousWatchDirectory": true, 18 | "excludeDirectories": ["**/node_modules", "dist"], 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /expressions/src/funcs/tojson.ts: -------------------------------------------------------------------------------- 1 | import {ExpressionData, StringData} from "../data/index.js"; 2 | import {replacer} from "../data/replacer.js"; 3 | import {FunctionDefinition} from "./info.js"; 4 | 5 | export const tojson: FunctionDefinition = { 6 | name: "toJson", 7 | description: 8 | "`toJSON(value)`\n\nReturns a pretty-print JSON representation of `value`. You can use this function to debug the information provided in contexts.", 9 | minArgs: 1, 10 | maxArgs: 1, 11 | call: (...args: ExpressionData[]): ExpressionData => { 12 | return new StringData(JSON.stringify(args[0], replacer, " ")); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/error.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | # Purposeful typo in run 9 | - runn: echo hi 10 | --- 11 | { 12 | "errors": [ 13 | { 14 | "Message": ".github/workflows/error.yml (Line: 7, Col: 9): Unexpected value 'runn'" 15 | }, 16 | { 17 | "Message": ".github/workflows/error.yml (Line: 7, Col: 9): There's not enough info to determine what you meant. Add one of these properties: run, shell, uses, with, working-directory" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /script/workflows/increment-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=$(cat lerna.json | jq -r '.version') 4 | 5 | MAJOR=$(echo $VERSION | cut -d. -f1) 6 | MINOR=$(echo $VERSION | cut -d. -f2) 7 | PATCH=$(echo $VERSION | cut -d. -f3) 8 | 9 | if [ "$1" == "major" ]; then 10 | MAJOR=$((MAJOR+1)) 11 | MINOR=0 12 | PATCH=0 13 | elif [ "$1" == "minor" ]; then 14 | MINOR=$((MINOR+1)) 15 | PATCH=0 16 | elif [ "$1" == "patch" ]; then 17 | PATCH=$((PATCH+1)) 18 | else 19 | echo "Invalid version type. Use 'major', 'minor' or 'patch'" 20 | exit 1 21 | fi 22 | 23 | NEW_VERSION="$MAJOR.$MINOR.$PATCH" 24 | echo $NEW_VERSION 25 | -------------------------------------------------------------------------------- /expressions/src/idxHelper.ts: -------------------------------------------------------------------------------- 1 | import {ExpressionData} from "./data/index.js"; 2 | 3 | export class idxHelper { 4 | public readonly str: string | undefined; 5 | public readonly int: number | undefined; 6 | 7 | constructor(public readonly star: boolean, idx: ExpressionData | undefined) { 8 | if (!idx) { 9 | return; 10 | } 11 | if (!star) { 12 | if (idx.primitive) { 13 | this.str = idx.coerceString(); 14 | } 15 | 16 | let f = idx.number(); 17 | if (!isNaN(f) && isFinite(f) && f >= 0) { 18 | f = Math.floor(f); 19 | this.int = f; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /workflow-parser/src/model/type-guards.ts: -------------------------------------------------------------------------------- 1 | import {ActionStep, Job, ReusableWorkflowJob, RunStep, Step, WorkflowJob} from "./workflow-template.js"; 2 | 3 | export function isRunStep(step: Step): step is RunStep { 4 | return (step as RunStep).run !== undefined; 5 | } 6 | 7 | export function isActionStep(step: Step): step is ActionStep { 8 | return (step as ActionStep).uses !== undefined; 9 | } 10 | 11 | export function isJob(job: WorkflowJob): job is Job { 12 | return job.type === "job"; 13 | } 14 | 15 | export function isReusableWorkflowJob(job: WorkflowJob): job is ReusableWorkflowJob { 16 | return job.type === "reusableWorkflowJob"; 17 | } 18 | -------------------------------------------------------------------------------- /workflow-parser/src/workflows/workflow-parser.test.ts: -------------------------------------------------------------------------------- 1 | import {parseWorkflow} from "./workflow-parser.js"; 2 | import {nullTrace} from "../test-utils/null-trace.js"; 3 | 4 | it("The template is not read when there are YAML errors", () => { 5 | const content = ` 6 | on: push 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 'Hello \${{ fromJSON('test') == inputs.name }}' 12 | run: echo Hello, world!`; 13 | 14 | const result = parseWorkflow({name: "main.yaml", content: content}, nullTrace); 15 | 16 | expect(result.context.errors.count).toBe(1); 17 | expect(result.value).toBeUndefined(); 18 | }); 19 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-on-workflow_call-input-type-missing.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_call: 8 | inputs: 9 | app_name: 10 | required: true 11 | type: Datetime 12 | secrets: 13 | shh: 14 | required: true 15 | jobs: 16 | my-job: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - run: echo hi 20 | --- 21 | { 22 | "errors": [ 23 | { 24 | "Message": ".github/workflows/errors-on-workflow_call-input-type-missing.yml (Line: 9, Col: 15): Unexpected value 'Datetime'" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /expressions/src/data/dictionary.test.ts: -------------------------------------------------------------------------------- 1 | import {Dictionary} from "./dictionary.js"; 2 | import {StringData} from "./string.js"; 3 | 4 | describe("dictionary", () => { 5 | it("pairs contains all values", () => { 6 | const d = new Dictionary(); 7 | d.add("ABC", new StringData("val")); 8 | 9 | expect(d.pairs()).toEqual([{key: "ABC", value: new StringData("val")}]); 10 | }); 11 | 12 | it("does not add duplicate entries", () => { 13 | const d = new Dictionary(); 14 | d.add("ABC", new StringData("val1")); 15 | d.add("abc", new StringData("val2")); 16 | 17 | expect(d.pairs()).toEqual([{key: "ABC", value: new StringData("val1")}]); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directories: 10 | - "/" 11 | - "/languageservice" 12 | - "/languageserver" 13 | - "expressions" 14 | - "browser-playground" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/object-reader.ts: -------------------------------------------------------------------------------- 1 | import {LiteralToken, SequenceToken, MappingToken} from "./tokens/index.js"; 2 | 3 | /** 4 | * Interface for reading a source object (or file). 5 | * This interface is used by TemplateReader to build a TemplateToken DOM. 6 | */ 7 | export interface ObjectReader { 8 | allowLiteral(): LiteralToken | undefined; 9 | 10 | // maybe rename these since we don't have out params 11 | allowSequenceStart(): SequenceToken | undefined; 12 | 13 | allowSequenceEnd(): boolean; 14 | 15 | allowMappingStart(): MappingToken | undefined; 16 | 17 | allowMappingEnd(): boolean; 18 | 19 | validateStart(): void; 20 | 21 | validateEnd(): void; 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "name": "vscode-jest-tests.v2", 7 | "request": "launch", 8 | "args": [ 9 | "--runInBand", 10 | "--watchAll=false", 11 | "--testNamePattern", 12 | "${jest.testNamePattern}", 13 | "--runTestsByPath", 14 | "${jest.testFile}" 15 | ], 16 | "cwd": "${workspaceFolder}", 17 | "console": "integratedTerminal", 18 | "internalConsoleOptions": "neverOpen", 19 | "program": "${workspaceFolder}/node_modules/.bin/jest", 20 | "env": { 21 | "NODE_OPTIONS": "--experimental-vm-modules" 22 | } 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-step-if.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: 4 | push: 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | if: (steps.publish-pdf.outcome != 'Skipped' || steps.publish-json.outcome != 'Skipped) && success() 11 | run: echo "Hello" 12 | --- 13 | { 14 | "errors": [ 15 | { 16 | "Message": ".github/workflows/errors-step-if.yml (Line: 8, Col: 15): Unexpected symbol: ''Skipped) && success()'. Located at position 74 within expression: (steps.publish-pdf.outcome != 'Skipped' || steps.publish-json.outcome != 'Skipped) && success()" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /languageserver/src/utils/error.ts: -------------------------------------------------------------------------------- 1 | import {RequestError} from "@octokit/types"; 2 | 3 | export function errorMessage(error: unknown): string { 4 | if (error instanceof Error) { 5 | return error.message; 6 | } 7 | if (typeof error === "string") { 8 | return error; 9 | } 10 | 11 | if ("name" in (error as RequestError)) { 12 | return (error as RequestError).name; 13 | } 14 | 15 | const status = errorStatus(error); 16 | if (status) { 17 | return `HTTP ${status}`; 18 | } 19 | 20 | return "Unknown error"; 21 | } 22 | 23 | export function errorStatus(error: unknown): number | undefined { 24 | if ("status" in (error as RequestError)) { 25 | return (error as RequestError).status; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /workflow-parser/src/model/converter/cron-constants.ts: -------------------------------------------------------------------------------- 1 | // Constants for parsing and validating cron expressions 2 | 3 | const MONTHS = { 4 | jan: 1, 5 | feb: 2, 6 | mar: 3, 7 | apr: 4, 8 | may: 5, 9 | jun: 6, 10 | jul: 7, 11 | aug: 8, 12 | sep: 9, 13 | oct: 10, 14 | nov: 11, 15 | dec: 12 16 | }; 17 | 18 | const DAYS = { 19 | sun: 0, 20 | mon: 1, 21 | tue: 2, 22 | wed: 3, 23 | thu: 4, 24 | fri: 5, 25 | sat: 6 26 | }; 27 | 28 | export const MINUTE_RANGE = {min: 0, max: 59}; 29 | export const HOUR_RANGE = {min: 0, max: 23}; 30 | export const DOM_RANGE = {min: 1, max: 31}; 31 | export const MONTH_RANGE = {min: 1, max: 12, names: MONTHS}; 32 | export const DOW_RANGE = {min: 0, max: 6, names: DAYS}; 33 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-job-inputs-undefined.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | on: push 6 | jobs: 7 | deploy: 8 | uses: contoso/templates/.github/workflows/deploy.yml@v1 9 | with: 10 | foo: bar 11 | --- 12 | contoso/templates/.github/workflows/deploy.yml@v1 13 | --- 14 | on: workflow_call 15 | jobs: 16 | job1: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - run: echo hi 20 | --- 21 | { 22 | "errors": [ 23 | { 24 | "Message": ".github/workflows/errors-reusable-workflow-job-inputs-undefined.yml (Line: 6, Col: 12): Invalid input, foo is not defined in the referenced workflow." 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /workflow-parser/src/model/converter/handle-errors.ts: -------------------------------------------------------------------------------- 1 | import {TemplateContext} from "../../templates/template-context.js"; 2 | import {TemplateToken, TemplateTokenError} from "../../templates/tokens/template-token.js"; 3 | 4 | export function handleTemplateTokenErrors( 5 | root: TemplateToken, 6 | context: TemplateContext, 7 | defaultValue: TResult, 8 | f: () => TResult 9 | ): TResult { 10 | let r: TResult = defaultValue; 11 | 12 | try { 13 | r = f(); 14 | } catch (err) { 15 | if (err instanceof TemplateTokenError) { 16 | context.error(err.token, err); 17 | } else { 18 | // Report error for the root node 19 | context.error(root, err); 20 | } 21 | } 22 | 23 | return r; 24 | } 25 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-on-workflow_call-input-unexpected-property.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_call: 8 | inputs: 9 | app_name: 10 | required: true 11 | type: string 12 | deprecationMessage: blah blah 13 | secrets: 14 | shh: 15 | required: true 16 | jobs: 17 | my-job: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - run: echo hi 21 | --- 22 | { 23 | "errors": [ 24 | { 25 | "Message": ".github/workflows/errors-on-workflow_call-input-unexpected-property.yml (Line: 10, Col: 9): Unexpected value 'deprecationMessage'" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-undefined.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | on: push 6 | jobs: 7 | deploy: 8 | uses: contoso/templates/.github/workflows/deploy.yml@v1 9 | secrets: 10 | foo: bar 11 | --- 12 | contoso/templates/.github/workflows/deploy.yml@v1 13 | --- 14 | on: workflow_call 15 | jobs: 16 | job1: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - run: echo hi 20 | --- 21 | { 22 | "errors": [ 23 | { 24 | "Message": ".github/workflows/errors-reusable-workflow-job-secrets-undefined.yml (Line: 6, Col: 12): Invalid secret, foo is not defined in the referenced workflow." 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-workflow_call.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: workflow_call 4 | jobs: 5 | my-job: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo hi 9 | --- 10 | { 11 | "jobs": [ 12 | { 13 | "type": "job", 14 | "id": "my-job", 15 | "name": "my-job", 16 | "if": { 17 | "type": 3, 18 | "expr": "success()" 19 | }, 20 | "runs-on": "ubuntu-latest", 21 | "steps": [ 22 | { 23 | "id": "__run", 24 | "if": { 25 | "type": 3, 26 | "expr": "success()" 27 | }, 28 | "run": "echo hi" 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/template-validation-error.ts: -------------------------------------------------------------------------------- 1 | import {TokenRange} from "./tokens/token-range.js"; 2 | 3 | /** 4 | * Provides information about an error which occurred during validation 5 | */ 6 | export class TemplateValidationError { 7 | constructor( 8 | public readonly rawMessage: string, 9 | public readonly prefix: string | undefined, 10 | public readonly code: string | undefined, 11 | public readonly range: TokenRange | undefined 12 | ) {} 13 | 14 | public get message(): string { 15 | if (this.prefix) { 16 | return `${this.prefix}: ${this.rawMessage}`; 17 | } 18 | 19 | return this.rawMessage; 20 | } 21 | 22 | public toString(): string { 23 | return this.message; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/workflow-name.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | name: my-workflow-name 5 | jobs: 6 | one: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo hello 10 | --- 11 | { 12 | "jobs": [ 13 | { 14 | "type": "job", 15 | "id": "one", 16 | "name": "one", 17 | "if": { 18 | "type": 3, 19 | "expr": "success()" 20 | }, 21 | "runs-on": "ubuntu-latest", 22 | "steps": [ 23 | { 24 | "id": "__run", 25 | "if": { 26 | "type": 3, 27 | "expr": "success()" 28 | }, 29 | "run": "echo hello" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-workflow_dispatch-string.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: workflow_dispatch 4 | jobs: 5 | my-job: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo hi 9 | --- 10 | { 11 | "jobs": [ 12 | { 13 | "type": "job", 14 | "id": "my-job", 15 | "name": "my-job", 16 | "if": { 17 | "type": 3, 18 | "expr": "success()" 19 | }, 20 | "runs-on": "ubuntu-latest", 21 | "steps": [ 22 | { 23 | "id": "__run", 24 | "if": { 25 | "type": 3, 26 | "expr": "success()" 27 | }, 28 | "run": "echo hi" 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/tokens/index.ts: -------------------------------------------------------------------------------- 1 | export {TemplateToken} from "./template-token.js"; 2 | export {ScalarToken} from "./scalar-token.js"; 3 | export {LiteralToken} from "./literal-token.js"; 4 | export {StringToken} from "./string-token.js"; 5 | export {NumberToken} from "./number-token.js"; 6 | export {BooleanToken} from "./boolean-token.js"; 7 | export {NullToken} from "./null-token.js"; 8 | export {KeyValuePair} from "./key-value-pair.js"; 9 | export {SequenceToken} from "./sequence-token.js"; 10 | export {MappingToken} from "./mapping-token.js"; 11 | export {ExpressionToken} from "./expression-token.js"; 12 | export {BasicExpressionToken} from "./basic-expression-token.js"; 13 | export {InsertExpressionToken} from "./insert-expression-token.js"; 14 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/job-container-missing-image.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build1: 6 | runs-on: linux 7 | container: 8 | options: someoption 9 | steps: 10 | - run: echo hi 11 | build2: 12 | runs-on: linux 13 | services: 14 | nginx: 15 | options: someoption 16 | steps: 17 | - run: echo hi 18 | --- 19 | { 20 | "errors": [ 21 | { 22 | "Message": ".github/workflows/job-container-missing-image.yml (Line: 6, Col: 7): Container image cannot be empty" 23 | }, 24 | { 25 | "Message": ".github/workflows/job-container-missing-image.yml (Line: 13, Col: 9): Container image cannot be empty" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-workflow_dispatch-null.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: 4 | workflow_dispatch: 5 | jobs: 6 | my-job: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo hi 10 | --- 11 | { 12 | "jobs": [ 13 | { 14 | "type": "job", 15 | "id": "my-job", 16 | "name": "my-job", 17 | "if": { 18 | "type": 3, 19 | "expr": "success()" 20 | }, 21 | "runs-on": "ubuntu-latest", 22 | "steps": [ 23 | { 24 | "id": "__run", 25 | "if": { 26 | "type": 3, 27 | "expr": "success()" 28 | }, 29 | "run": "echo hi" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /expressions/src/data/number.ts: -------------------------------------------------------------------------------- 1 | import {ExpressionDataInterface, Kind} from "./expressiondata.js"; 2 | 3 | export class NumberData implements ExpressionDataInterface { 4 | constructor(public readonly value: number) {} 5 | 6 | public readonly kind = Kind.Number; 7 | 8 | public primitive = true; 9 | 10 | coerceString(): string { 11 | if (this.value === 0) { 12 | return "0"; 13 | } 14 | 15 | // Workaround to limit the precision to at most 15 digits. Format the number to a string, then parse 16 | // it back to a number to remove trailing zeroes to prevent numbers to be converted to 1.200000000... 17 | return (+this.value.toFixed(15)).toString(); 18 | } 19 | 20 | number(): number { 21 | return this.value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets-required.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | on: push 6 | jobs: 7 | deploy: 8 | uses: contoso/templates/.github/workflows/deploy.yml@v1 9 | --- 10 | contoso/templates/.github/workflows/deploy.yml@v1 11 | --- 12 | on: 13 | workflow_call: 14 | secrets: 15 | shh: 16 | required: true 17 | jobs: 18 | job1: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - run: echo hi 22 | --- 23 | { 24 | "errors": [ 25 | { 26 | "Message": ".github/workflows/errors-reusable-workflow-job-secrets-required.yml (Line: 4, Col: 11): Secret shh is required, but not provided while calling." 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-workflow_dispatch-mapping.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: 4 | workflow_dispatch: {} 5 | jobs: 6 | my-job: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo hi 10 | --- 11 | { 12 | "jobs": [ 13 | { 14 | "type": "job", 15 | "id": "my-job", 16 | "name": "my-job", 17 | "if": { 18 | "type": 3, 19 | "expr": "success()" 20 | }, 21 | "runs-on": "ubuntu-latest", 22 | "steps": [ 23 | { 24 | "id": "__run", 25 | "if": { 26 | "type": 3, 27 | "expr": "success()" 28 | }, 29 | "run": "echo hi" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/workflow-description.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | description: My workflow description 4 | on: push 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo hi 10 | --- 11 | { 12 | "jobs": [ 13 | { 14 | "type": "job", 15 | "id": "build", 16 | "name": "build", 17 | "if": { 18 | "type": 3, 19 | "expr": "success()" 20 | }, 21 | "runs-on": "ubuntu-latest", 22 | "steps": [ 23 | { 24 | "id": "__run", 25 | "if": { 26 | "type": 3, 27 | "expr": "success()" 28 | }, 29 | "run": "echo hi" 30 | } 31 | ] 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-workflow_call-sequence.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: 4 | - workflow_call 5 | - push 6 | jobs: 7 | my-job: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo hi 11 | --- 12 | { 13 | "jobs": [ 14 | { 15 | "type": "job", 16 | "id": "my-job", 17 | "name": "my-job", 18 | "if": { 19 | "type": 3, 20 | "expr": "success()" 21 | }, 22 | "runs-on": "ubuntu-latest", 23 | "steps": [ 24 | { 25 | "id": "__run", 26 | "if": { 27 | "type": 3, 28 | "expr": "success()" 29 | }, 30 | "run": "echo hi" 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /languageservice/src/utils/expression-detection.ts: -------------------------------------------------------------------------------- 1 | import {isString} from "@actions/workflow-parser"; 2 | import {OPEN_EXPRESSION} from "@actions/workflow-parser/templates/template-constants"; 3 | import {TemplateToken} from "@actions/workflow-parser/templates/tokens/index"; 4 | 5 | export function isPotentiallyExpression(token: TemplateToken): boolean { 6 | const containsExpression = isString(token) && token.value != null && token.value.indexOf(OPEN_EXPRESSION) >= 0; 7 | // If conditions are always expressions (job-if, step-if, snapshot-if) 8 | const definitionKey = token.definition?.key; 9 | const isIfCondition = definitionKey === "job-if" || definitionKey === "step-if" || definitionKey === "snapshot-if"; 10 | return containsExpression || isIfCondition; 11 | } 12 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-job-inputs-required.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | on: push 6 | jobs: 7 | deploy: 8 | uses: contoso/templates/.github/workflows/deploy.yml@v1 9 | --- 10 | contoso/templates/.github/workflows/deploy.yml@v1 11 | --- 12 | on: 13 | workflow_call: 14 | inputs: 15 | foo: 16 | required: true 17 | type: string 18 | jobs: 19 | job1: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - run: echo hi 23 | --- 24 | { 25 | "errors": [ 26 | { 27 | "Message": ".github/workflows/errors-reusable-workflow-job-inputs-required.yml (Line: 4, Col: 11): Input foo is required, but not provided while calling." 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-workflow_dispatch-inputs-null.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | jobs: 7 | my-job: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo hi 11 | --- 12 | { 13 | "jobs": [ 14 | { 15 | "type": "job", 16 | "id": "my-job", 17 | "name": "my-job", 18 | "if": { 19 | "type": 3, 20 | "expr": "success()" 21 | }, 22 | "runs-on": "ubuntu-latest", 23 | "steps": [ 24 | { 25 | "id": "__run", 26 | "if": { 27 | "type": 3, 28 | "expr": "success()" 29 | }, 30 | "run": "echo hi" 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /languageservice/src/context-providers/descriptionsSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "type": "object", 4 | "properties": { 5 | "$schema": { 6 | "type": "string", 7 | "$comment": "Ignore this, just to make VS Code happy" 8 | } 9 | }, 10 | "additionalProperties": { 11 | "type": "object", 12 | "additionalProperties": { 13 | "type": "object", 14 | "properties": { 15 | "description": { 16 | "type": "string" 17 | }, 18 | "versions": { 19 | "type": "object", 20 | "additionalProperties": { 21 | "type": "string" 22 | } 23 | } 24 | }, 25 | "required": [ 26 | "description" 27 | ] 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /languageservice/src/utils/range.ts: -------------------------------------------------------------------------------- 1 | import {Position as TokenPosition, TokenRange} from "@actions/workflow-parser/templates/tokens/token-range"; 2 | import {Position, Range} from "vscode-languageserver-types"; 3 | 4 | export function mapRange(range: TokenRange | undefined): Range { 5 | if (!range) { 6 | return { 7 | start: { 8 | line: 1, 9 | character: 1 10 | }, 11 | end: { 12 | line: 1, 13 | character: 1 14 | } 15 | }; 16 | } 17 | 18 | return { 19 | start: mapPosition(range.start), 20 | end: mapPosition(range.end) 21 | }; 22 | } 23 | 24 | export function mapPosition(position: TokenPosition): Position { 25 | return { 26 | line: position.line - 1, 27 | character: position.column - 1 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/events-single-invalid.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | - C# 5 | --- 6 | on: 7 | workflow_dispatch: 8 | unknown_value: test 9 | inputs: 10 | name: 11 | another_unknown_value: 123 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - run: echo hi 18 | continue-on-error: true 19 | --- 20 | { 21 | "errors": [ 22 | { 23 | "Message": ".github/workflows/events-single-invalid.yml (Line: 3, Col: 5): Unexpected value 'unknown_value'" 24 | }, 25 | { 26 | "Message": ".github/workflows/events-single-invalid.yml (Line: 6, Col: 9): Unexpected value 'another_unknown_value'" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-workflow_dispatch-unknown-keys-ignored.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: 4 | workflow_dispatch: 5 | unknown-key: asdf 6 | jobs: 7 | my-job: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo hi 11 | --- 12 | { 13 | "jobs": [ 14 | { 15 | "type": "job", 16 | "id": "my-job", 17 | "name": "my-job", 18 | "if": { 19 | "type": 3, 20 | "expr": "success()" 21 | }, 22 | "runs-on": "ubuntu-latest", 23 | "steps": [ 24 | { 25 | "id": "__run", 26 | "if": { 27 | "type": 3, 28 | "expr": "success()" 29 | }, 30 | "run": "echo hi" 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-event-config-string-image-version.yml: -------------------------------------------------------------------------------- 1 | include-source: false 2 | skip: 3 | - C# 4 | - Go 5 | --- 6 | on: image_version 7 | jobs: 8 | my-job: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - run: echo hi 12 | --- 13 | { 14 | "events": { 15 | "image_version": {} 16 | }, 17 | "jobs": [ 18 | { 19 | "type": "job", 20 | "id": "my-job", 21 | "name": "my-job", 22 | "if": { 23 | "type": 3, 24 | "expr": "success()" 25 | }, 26 | "runs-on": "ubuntu-latest", 27 | "steps": [ 28 | { 29 | "id": "__run", 30 | "if": { 31 | "type": 3, 32 | "expr": "success()" 33 | }, 34 | "run": "echo hi" 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /expressions/src/data/array.ts: -------------------------------------------------------------------------------- 1 | import {ExpressionData, ExpressionDataInterface, Kind, kindStr} from "./expressiondata.js"; 2 | 3 | export class Array implements ExpressionDataInterface { 4 | private v: ExpressionData[] = []; 5 | 6 | constructor(...data: ExpressionData[]) { 7 | for (const d of data) { 8 | this.add(d); 9 | } 10 | } 11 | 12 | public readonly kind = Kind.Array; 13 | 14 | public primitive = false; 15 | 16 | coerceString(): string { 17 | return kindStr(this.kind); 18 | } 19 | 20 | number(): number { 21 | return NaN; 22 | } 23 | 24 | add(value: ExpressionData) { 25 | this.v.push(value); 26 | } 27 | 28 | get(index: number): ExpressionData { 29 | return this.v[index]; 30 | } 31 | 32 | values(): ExpressionData[] { 33 | return this.v; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /expressions/src/data/replacer.test.ts: -------------------------------------------------------------------------------- 1 | import {Array} from "./array.js"; 2 | import {Dictionary} from "./dictionary.js"; 3 | import {Null} from "./null.js"; 4 | import {NumberData} from "./number.js"; 5 | import {replacer} from "./replacer.js"; 6 | import {StringData} from "./string.js"; 7 | 8 | describe("replacer", () => { 9 | it("null", () => { 10 | expect(JSON.stringify(new Null(), replacer, " ")).toEqual("null"); 11 | }); 12 | 13 | it("array", () => { 14 | expect(JSON.stringify(new Array(new StringData("a"), new StringData("b")), replacer, " ")).toEqual( 15 | '[\n "a",\n "b"\n]' 16 | ); 17 | }); 18 | 19 | it("dictionary", () => { 20 | expect(JSON.stringify(new Dictionary({key: "a", value: new NumberData(42)}), replacer, " ")).toEqual( 21 | '{\n "a": 42\n}' 22 | ); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-max-result-size.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | max-result-size: 2048 3 | skip: 4 | - Go 5 | --- 6 | on: push 7 | jobs: 8 | build-0: { uses: contoso/templates/.github/workflows/deploy.yml@v1 } 9 | build-1: { uses: contoso/templates/.github/workflows/deploy.yml@v1 } 10 | --- 11 | contoso/templates/.github/workflows/deploy.yml@v1 12 | --- 13 | on: workflow_call 14 | jobs: 15 | job1: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - run: echo Deploying... 19 | --- 20 | { 21 | "errors": [ 22 | { 23 | "Message": "In .github/workflows/errors-reusable-workflow-max-result-size.yml (Line: 4, Col: 20): Error from called workflow contoso/templates/.github/workflows/deploy.yml@v1: Maximum object size exceeded" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /languageservice/src/utils/scalar-to-data.ts: -------------------------------------------------------------------------------- 1 | import {data} from "@actions/expressions"; 2 | import {isBoolean, isNumber, isString} from "@actions/workflow-parser"; 3 | import {ScalarToken} from "@actions/workflow-parser/templates/tokens/scalar-token"; 4 | import {TokenType} from "@actions/workflow-parser/templates/tokens/types"; 5 | 6 | export function scalarToData(scalar: ScalarToken): data.ExpressionData { 7 | if (isNumber(scalar)) { 8 | return new data.NumberData(scalar.value); 9 | } 10 | 11 | if (isString(scalar)) { 12 | return new data.StringData(scalar.value); 13 | } 14 | 15 | if (isBoolean(scalar)) { 16 | return new data.BooleanData(scalar.value); 17 | } 18 | 19 | if (scalar.templateTokenType === TokenType.Null) { 20 | return new data.Null(); 21 | } 22 | 23 | return new data.StringData(scalar.toDisplayString()); 24 | } 25 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-expression-not-allowed.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | name: Mixed String Expr ${{ format('hello {0}', 'world') }} 4 | on: 5 | ${{ format('expression-mapping-key {0}', 'as-key') }}: 6 | jobs: 7 | ${{ format('hello {0}', 'world') }} 8 | --- 9 | { 10 | "errors": [ 11 | { 12 | "Message": ".github/workflows/errors-expression-not-allowed.yml (Line: 1, Col: 7): A template expression is not allowed in this context" 13 | }, 14 | { 15 | "Message": ".github/workflows/errors-expression-not-allowed.yml (Line: 3, Col: 3): A template expression is not allowed in this context" 16 | }, 17 | { 18 | "Message": ".github/workflows/errors-expression-not-allowed.yml (Line: 5, Col: 3): A template expression is not allowed in this context" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-max-job-limit-with-reusable-workflow.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | max-job-limit: 8 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | uses: contoso/templates/.github/workflows/deploy.yml@v1 10 | deploy1: 11 | uses: contoso/templates/.github/workflows/deploy.yml@v1 12 | deploy2: 13 | uses: contoso/templates/.github/workflows/deploy.yml@v1 14 | --- 15 | contoso/templates/.github/workflows/deploy.yml@v1 16 | --- 17 | on: workflow_call 18 | jobs: 19 | deploy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - run: echo hi 23 | deploy2: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - run: echo hi 27 | --- 28 | { 29 | "errors": [ 30 | { 31 | "Message": "The workflow may not contain more than 8 jobs" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug or unexpected behavior while using Actions Language Services 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. With this workflow '...' 16 | 2. Do this '...' 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Package/Area** 26 | - [ ] Expressions 27 | - [ ] Workflow Parser 28 | - [ ] Language Service 29 | - [ ] Language Server 30 | 31 | **Package Version** 32 | `v1.x.y` 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/workflow-concurrency-expression.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | concurrency: ci-${{ github.ref }} 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo hi 10 | --- 11 | { 12 | "concurrency": { 13 | "type": 3, 14 | "expr": "format('ci-{0}', github.ref)" 15 | }, 16 | "jobs": [ 17 | { 18 | "type": "job", 19 | "id": "build", 20 | "name": "build", 21 | "if": { 22 | "type": 3, 23 | "expr": "success()" 24 | }, 25 | "runs-on": "ubuntu-latest", 26 | "steps": [ 27 | { 28 | "id": "__run", 29 | "if": { 30 | "type": 3, 31 | "expr": "success()" 32 | }, 33 | "run": "echo hi" 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/tokens/serialization.ts: -------------------------------------------------------------------------------- 1 | import {ScalarToken} from "./scalar-token.js"; 2 | import {TemplateToken} from "./template-token.js"; 3 | import {TokenType} from "./types.js"; 4 | 5 | export type MapItem = { 6 | Key: ScalarToken; 7 | Value: TemplateToken; 8 | }; 9 | 10 | export type SerializedMappingToken = { 11 | type: TokenType.Mapping; 12 | map: MapItem[]; 13 | }; 14 | 15 | export type SerializedSequenceToken = { 16 | type: TokenType.Sequence; 17 | seq: TemplateToken[]; 18 | }; 19 | 20 | export type SerializedExpressionToken = { 21 | type: TokenType.BasicExpression | TokenType.InsertExpression; 22 | expr: string; 23 | }; 24 | 25 | export type SerializedToken = 26 | | SerializedMappingToken 27 | | SerializedSequenceToken 28 | | SerializedExpressionToken 29 | | string 30 | | number 31 | | boolean 32 | | null 33 | | undefined; 34 | -------------------------------------------------------------------------------- /browser-playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "moduleResolution": "Node16", 6 | "lib": ["ES2022", "dom"], 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "sourceMap": true, 12 | "inlineSources": false, 13 | "stripInternal": true, 14 | "strict": true, 15 | "strictPropertyInitialization": false, 16 | "importHelpers": true, 17 | "downlevelIteration": false, 18 | "noImplicitReturns": true, 19 | "noUnusedParameters": true, 20 | "noUnusedLocals": true, 21 | // Disallow inconsistently-cased references to the same file 22 | "forceConsistentCasingInFileNames": true, 23 | "noImplicitOverride": true, 24 | "rootDir": "src", 25 | "outDir": "dist", 26 | "composite": true 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-job-needs-cycle.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | 6 | one: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo hello 10 | 11 | two: 12 | needs: 13 | - one 14 | - three 15 | runs-on: ubuntu-latest 16 | steps: 17 | - run: echo hello 18 | 19 | three: 20 | needs: two 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hello 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-job-needs-cycle.yml (Line: 12, Col: 9): Job 'two' depends on job 'three' which creates a cycle in the dependency graph." 29 | }, 30 | { 31 | "Message": ".github/workflows/errors-job-needs-cycle.yml (Line: 18, Col: 12): Job 'three' depends on job 'two' which creates a cycle in the dependency graph." 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-pages-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | pages: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | pages: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-pages-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'pages: write', but is only allowed 'pages: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/tokens/null-token.ts: -------------------------------------------------------------------------------- 1 | import {LiteralToken, TemplateToken} from "./index.js"; 2 | import {DefinitionInfo} from "../schema/definition-info.js"; 3 | import {TokenRange} from "./token-range.js"; 4 | import {TokenType} from "./types.js"; 5 | 6 | export class NullToken extends LiteralToken { 7 | public constructor( 8 | file: number | undefined, 9 | range: TokenRange | undefined, 10 | definitionInfo: DefinitionInfo | undefined 11 | ) { 12 | super(TokenType.Null, file, range, definitionInfo); 13 | } 14 | 15 | public override clone(omitSource?: boolean): TemplateToken { 16 | return omitSource 17 | ? new NullToken(undefined, undefined, this.definitionInfo) 18 | : new NullToken(this.file, this.range, this.definitionInfo); 19 | } 20 | 21 | public override toString(): string { 22 | return ""; 23 | } 24 | 25 | public override toJSON() { 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/job-snapshot-simple.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | # on: push 4 | # jobs: 5 | # job1: 6 | # runs-on: windows-2019 7 | # snapshot: custom-image 8 | # steps: 9 | # - run: echo 1 10 | on: push 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - run: echo hi 16 | snapshot: custom-image 17 | --- 18 | { 19 | "jobs": [ 20 | { 21 | "type": "job", 22 | "id": "build", 23 | "name": "build", 24 | "if": { 25 | "type": 3, 26 | "expr": "success()" 27 | }, 28 | "runs-on": "ubuntu-latest", 29 | "steps": [ 30 | { 31 | "id": "__run", 32 | "if": { 33 | "type": 3, 34 | "expr": "success()" 35 | }, 36 | "run": "echo hi" 37 | } 38 | ], 39 | "snapshot": "custom-image" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-checks-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | checks: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | checks: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-checks-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'checks: write', but is only allowed 'checks: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-issues-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | issues: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | issues: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-issues-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'issues: write', but is only allowed 'issues: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-job-level-from-caller-default.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | uses: contoso/templates/.github/workflows/deploy.yml@v1 10 | --- 11 | contoso/templates/.github/workflows/deploy.yml@v1 12 | --- 13 | on: workflow_call 14 | jobs: 15 | deploy: 16 | permissions: 17 | actions: write 18 | runs-on: ubuntu-latest 19 | steps: 20 | - run: echo hi 21 | --- 22 | { 23 | "errors": [ 24 | { 25 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-job-level-from-caller-default.yml (Line: 3, Col: 3): Error calling workflow 'contoso/templates/.github/workflows/deploy.yml@v1'. The nested job 'deploy' is requesting 'actions: write', but is only allowed 'actions: none'." 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-actions-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | actions: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | actions: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-actions-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'actions: write', but is only allowed 'actions: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-contents-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | contents: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | contents: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-contents-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'contents: write', but is only allowed 'contents: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-packages-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | packages: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | packages: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-packages-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'packages: write', but is only allowed 'packages: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-statuses-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | statuses: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | statuses: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-statuses-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'statuses: write', but is only allowed 'statuses: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /languageserver/src/index.ts: -------------------------------------------------------------------------------- 1 | import {Connection} from "vscode-languageserver"; 2 | import { 3 | BrowserMessageReader, 4 | BrowserMessageWriter, 5 | createConnection as createBrowserConnection 6 | } from "vscode-languageserver/browser"; 7 | import {createConnection as createNodeConnection} from "vscode-languageserver/node"; 8 | 9 | import {initConnection} from "./connection"; 10 | 11 | /** Helper function determining whether we are executing with node runtime */ 12 | function isNode(): boolean { 13 | return typeof process !== "undefined" && process.versions?.node != null; 14 | } 15 | 16 | function getConnection(): Connection { 17 | if (isNode()) { 18 | return createNodeConnection(); 19 | } else { 20 | const messageReader = new BrowserMessageReader(self); 21 | const messageWriter = new BrowserMessageWriter(self); 22 | return createBrowserConnection(messageReader, messageWriter); 23 | } 24 | } 25 | 26 | initConnection(getConnection()); 27 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-job-level-from-caller-job-level.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | on: push 6 | jobs: 7 | deploy: 8 | permissions: 9 | actions: read 10 | uses: contoso/templates/.github/workflows/deploy.yml@v1 11 | --- 12 | contoso/templates/.github/workflows/deploy.yml@v1 13 | --- 14 | on: workflow_call 15 | jobs: 16 | deploy: 17 | permissions: 18 | actions: write 19 | runs-on: ubuntu-latest 20 | steps: 21 | - run: echo hi 22 | --- 23 | { 24 | "errors": [ 25 | { 26 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-job-level-from-caller-job-level.yml (Line: 3, Col: 3): Error calling workflow 'contoso/templates/.github/workflows/deploy.yml@v1'. The nested job 'deploy' is requesting 'actions: write', but is only allowed 'actions: read'." 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-job-level-from-caller-workflow-level.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | on: push 6 | permissions: 7 | actions: read 8 | jobs: 9 | deploy: 10 | uses: contoso/templates/.github/workflows/deploy.yml@v1 11 | --- 12 | contoso/templates/.github/workflows/deploy.yml@v1 13 | --- 14 | on: workflow_call 15 | jobs: 16 | deploy: 17 | permissions: 18 | actions: write 19 | runs-on: ubuntu-latest 20 | steps: 21 | - run: echo hi 22 | --- 23 | { 24 | "errors": [ 25 | { 26 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-job-level-from-caller-workflow-level.yml (Line: 5, Col: 3): Error calling workflow 'contoso/templates/.github/workflows/deploy.yml@v1'. The nested job 'deploy' is requesting 'actions: write', but is only allowed 'actions: read'." 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-deployments-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | deployments: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | deployments: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-deployments-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'deployments: write', but is only allowed 'deployments: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-discussions-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | discussions: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | discussions: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-discussions-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'discussions: write', but is only allowed 'discussions: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /expressions/src/funcs/endswith.ts: -------------------------------------------------------------------------------- 1 | import {BooleanData, ExpressionData} from "../data/index.js"; 2 | import {toUpperSpecial} from "../result.js"; 3 | import {FunctionDefinition} from "./info.js"; 4 | 5 | export const endswith: FunctionDefinition = { 6 | name: "endsWith", 7 | description: 8 | "`endsWith( searchString, searchValue )`\n\nReturns `true` if `searchString` ends with `searchValue`. This function is not case sensitive. Casts values to a string.", 9 | minArgs: 2, 10 | maxArgs: 2, 11 | call: (...args: ExpressionData[]): ExpressionData => { 12 | const left = args[0]; 13 | if (!left.primitive) { 14 | return new BooleanData(false); 15 | } 16 | 17 | const right = args[1]; 18 | if (!right.primitive) { 19 | return new BooleanData(false); 20 | } 21 | 22 | const ls = toUpperSpecial(left.coerceString()); 23 | const rs = toUpperSpecial(right.coerceString()); 24 | 25 | return new BooleanData(ls.endsWith(rs)); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-pull-requests-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | pull-requests: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | pull-requests: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-pull-requests-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'pull-requests: write', but is only allowed 'pull-requests: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-event-config-image-version-name-version.yml: -------------------------------------------------------------------------------- 1 | include-source: false 2 | skip: 3 | - C# 4 | - Go 5 | --- 6 | on: 7 | image_version: 8 | names: testing 9 | versions: 1.* 10 | jobs: 11 | my-job: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - run: echo hi 15 | --- 16 | { 17 | "events": { 18 | "image_version": { 19 | "versions": [ 20 | "1.*" 21 | ], 22 | "names": [ 23 | "testing" 24 | ] 25 | } 26 | }, 27 | "jobs": [ 28 | { 29 | "type": "job", 30 | "id": "my-job", 31 | "name": "my-job", 32 | "if": { 33 | "type": 3, 34 | "expr": "success()" 35 | }, 36 | "runs-on": "ubuntu-latest", 37 | "steps": [ 38 | { 39 | "id": "__run", 40 | "if": { 41 | "type": 3, 42 | "expr": "success()" 43 | }, 44 | "run": "echo hi" 45 | } 46 | ] 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /expressions/src/funcs/startswith.ts: -------------------------------------------------------------------------------- 1 | import {BooleanData, ExpressionData} from "../data/index.js"; 2 | import {toUpperSpecial} from "../result.js"; 3 | import {FunctionDefinition} from "./info.js"; 4 | 5 | export const startswith: FunctionDefinition = { 6 | name: "startsWith", 7 | description: 8 | "`startsWith( searchString, searchValue )`\n\nReturns `true` when `searchString` starts with `searchValue`. This function is not case sensitive. Casts values to a string.", 9 | minArgs: 2, 10 | maxArgs: 2, 11 | call: (...args: ExpressionData[]): ExpressionData => { 12 | const left = args[0]; 13 | if (!left.primitive) { 14 | return new BooleanData(false); 15 | } 16 | 17 | const right = args[1]; 18 | if (!right.primitive) { 19 | return new BooleanData(false); 20 | } 21 | 22 | const ls = toUpperSpecial(left.coerceString()); 23 | const rs = toUpperSpecial(right.coerceString()); 24 | 25 | return new BooleanData(ls.startsWith(rs)); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-security-events-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | security-events: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | security-events: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-security-events-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'security-events: write', but is only allowed 'security-events: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-workflow-level-from-caller-job-level.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | on: push 6 | jobs: 7 | deploy: 8 | permissions: 9 | actions: read 10 | uses: contoso/templates/.github/workflows/deploy.yml@v1 11 | --- 12 | contoso/templates/.github/workflows/deploy.yml@v1 13 | --- 14 | on: workflow_call 15 | permissions: 16 | actions: write 17 | jobs: 18 | deploy: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - run: echo hi 22 | --- 23 | { 24 | "errors": [ 25 | { 26 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-workflow-level-from-caller-job-level.yml (Line: 3, Col: 3): Error calling workflow 'contoso/templates/.github/workflows/deploy.yml@v1'. The workflow 'contoso/templates/.github/workflows/deploy.yml@v1' is requesting 'actions: write', but is only allowed 'actions: read'." 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-workflow-level-from-caller-workflow-level.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | on: push 6 | permissions: 7 | actions: read 8 | jobs: 9 | deploy: 10 | uses: contoso/templates/.github/workflows/deploy.yml@v1 11 | --- 12 | contoso/templates/.github/workflows/deploy.yml@v1 13 | --- 14 | on: workflow_call 15 | permissions: 16 | actions: write 17 | jobs: 18 | deploy: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - run: echo hi 22 | --- 23 | { 24 | "errors": [ 25 | { 26 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-workflow-level-from-caller-workflow-level.yml (Line: 5, Col: 3): Error calling workflow 'contoso/templates/.github/workflows/deploy.yml@v1'. The workflow 'contoso/templates/.github/workflows/deploy.yml@v1' is requesting 'actions: write', but is only allowed 'actions: read'." 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-job-needs-unknown-job.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | 6 | one: 7 | needs: two 8 | runs-on: ubuntu-latest 9 | steps: 10 | - run: echo hello 11 | 12 | three: 13 | needs: 14 | - four 15 | - five 16 | runs-on: ubuntu-latest 17 | steps: 18 | - run: echo hello 19 | 20 | six: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hello 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-job-needs-unknown-job.yml (Line: 5, Col: 12): Job 'one' depends on unknown job 'two'." 29 | }, 30 | { 31 | "Message": ".github/workflows/errors-job-needs-unknown-job.yml (Line: 12, Col: 9): Job 'three' depends on unknown job 'four'." 32 | }, 33 | { 34 | "Message": ".github/workflows/errors-job-needs-unknown-job.yml (Line: 13, Col: 9): Job 'three' depends on unknown job 'five'." 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-repository-projects-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | repository-projects: read 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: 20 | repository-projects: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - run: echo hi 24 | --- 25 | { 26 | "errors": [ 27 | { 28 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-repository-projects-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'repository-projects: write', but is only allowed 'repository-projects: read'." 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /languageserver/src/description-providers/action-description.ts: -------------------------------------------------------------------------------- 1 | import {actionUrl, parseActionReference} from "@actions/languageservice/action"; 2 | import {isActionStep} from "@actions/workflow-parser/model/type-guards"; 3 | import {Step} from "@actions/workflow-parser/model/workflow-template"; 4 | import {Octokit} from "@octokit/rest"; 5 | import {fetchActionMetadata} from "../utils/action-metadata"; 6 | import {TTLCache} from "../utils/cache"; 7 | 8 | export async function getActionDescription(client: Octokit, cache: TTLCache, step: Step): Promise { 9 | if (!isActionStep(step)) { 10 | return undefined; 11 | } 12 | const action = parseActionReference(step.uses.value); 13 | if (!action) { 14 | return undefined; 15 | } 16 | 17 | const metadata = await fetchActionMetadata(client, cache, action); 18 | if (!metadata?.name || !metadata?.description) { 19 | return undefined; 20 | } 21 | 22 | return `[**${metadata.name}**](${actionUrl(action)})\n\n${metadata.description}`; 23 | } 24 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/step-id.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | job1: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v1 9 | id: action-step-id 10 | - run: echo 1 11 | id: run-step-id 12 | --- 13 | { 14 | "jobs": [ 15 | { 16 | "type": "job", 17 | "id": "job1", 18 | "name": "job1", 19 | "if": { 20 | "type": 3, 21 | "expr": "success()" 22 | }, 23 | "runs-on": "ubuntu-latest", 24 | "steps": [ 25 | { 26 | "id": "action-step-id", 27 | "if": { 28 | "type": 3, 29 | "expr": "success()" 30 | }, 31 | "uses": "actions/checkout@v1" 32 | }, 33 | { 34 | "id": "run-step-id", 35 | "if": { 36 | "type": 3, 37 | "expr": "success()" 38 | }, 39 | "run": "echo 1" 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-event-config-image-version-names.yml: -------------------------------------------------------------------------------- 1 | include-source: false 2 | skip: 3 | - C# 4 | - Go 5 | --- 6 | on: 7 | image_version: 8 | types: 9 | - ready 10 | names: 11 | - one 12 | - two 13 | jobs: 14 | my-job: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - run: echo hi 18 | --- 19 | { 20 | "events": { 21 | "image_version": { 22 | "types": [ 23 | "ready" 24 | ], 25 | "names": [ 26 | "one", 27 | "two" 28 | ] 29 | } 30 | }, 31 | "jobs": [ 32 | { 33 | "type": "job", 34 | "id": "my-job", 35 | "name": "my-job", 36 | "if": { 37 | "type": 3, 38 | "expr": "success()" 39 | }, 40 | "runs-on": "ubuntu-latest", 41 | "steps": [ 42 | { 43 | "id": "__run", 44 | "if": { 45 | "type": 3, 46 | "expr": "success()" 47 | }, 48 | "run": "echo hi" 49 | } 50 | ] 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /expressions/src/funcs/fromjson.ts: -------------------------------------------------------------------------------- 1 | import {ExpressionData} from "../data/index.js"; 2 | import {reviver} from "../data/reviver.js"; 3 | import {ExpressionEvaluationError} from "../errors.js"; 4 | import {FunctionDefinition} from "./info.js"; 5 | 6 | export const fromjson: FunctionDefinition = { 7 | name: "fromJson", 8 | description: 9 | "`fromJSON(value)`\n\nReturns a JSON object or JSON data type for `value`. You can use this function to provide a JSON object as an evaluated expression or to convert environment variables from a string.", 10 | minArgs: 1, 11 | maxArgs: 1, 12 | call: (...args: ExpressionData[]): ExpressionData => { 13 | const input = args[0]; 14 | const is = input.coerceString(); 15 | 16 | if (is.trim() === "") { 17 | throw new Error("empty input"); 18 | } 19 | 20 | try { 21 | return JSON.parse(is, reviver) as ExpressionData; 22 | } catch (e) { 23 | throw new ExpressionEvaluationError("Error parsing JSON when evaluating fromJson", {cause: e}); 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/basic.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - run: echo hi 10 | continue-on-error: true 11 | --- 12 | { 13 | "jobs": [ 14 | { 15 | "type": "job", 16 | "id": "build", 17 | "name": "build", 18 | "if": { 19 | "type": 3, 20 | "expr": "success()" 21 | }, 22 | "runs-on": "ubuntu-latest", 23 | "steps": [ 24 | { 25 | "id": "__actions_checkout", 26 | "if": { 27 | "type": 3, 28 | "expr": "success()" 29 | }, 30 | "uses": "actions/checkout@v3" 31 | }, 32 | { 33 | "id": "__run", 34 | "if": { 35 | "type": 3, 36 | "expr": "success()" 37 | }, 38 | "continue-on-error": true, 39 | "run": "echo hi" 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-job-id-leading-underscores.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | name: CI 4 | on: 5 | push: 6 | jobs: 7 | # Valid 8 | _: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - run: echo hi 12 | _a: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - run: echo hi 16 | a__: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - run: echo hi 20 | 21 | # Invalid 22 | __: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - run: echo hi 26 | __a: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - run: echo hi 30 | --- 31 | { 32 | "errors": [ 33 | { 34 | "Message": ".github/workflows/errors-job-id-leading-underscores.yml (Line: 20, Col: 3): The identifier '__' is invalid. IDs starting with '__' are reserved." 35 | }, 36 | { 37 | "Message": ".github/workflows/errors-job-id-leading-underscores.yml (Line: 24, Col: 3): The identifier '__a' is invalid. IDs starting with '__' are reserved." 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-job-inputs-type-mismatch.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | on: push 6 | jobs: 7 | deploy: 8 | uses: contoso/templates/.github/workflows/deploy.yml@v1 9 | with: 10 | some-boolean: not-a-boolean 11 | some-number: not-a-number 12 | --- 13 | contoso/templates/.github/workflows/deploy.yml@v1 14 | --- 15 | on: 16 | workflow_call: 17 | inputs: 18 | some-boolean: 19 | type: boolean 20 | some-number: 21 | type: number 22 | jobs: 23 | job1: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - run: echo hi 27 | --- 28 | { 29 | "errors": [ 30 | { 31 | "Message": ".github/workflows/errors-reusable-workflow-job-inputs-type-mismatch.yml (Line: 6, Col: 21): Unexpected value 'not-a-boolean'" 32 | }, 33 | { 34 | "Message": ".github/workflows/errors-reusable-workflow-job-inputs-type-mismatch.yml (Line: 7, Col: 20): Unexpected value 'not-a-number'" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-event-config-image-version-versions.yml: -------------------------------------------------------------------------------- 1 | include-source: false 2 | skip: 3 | - C# 4 | - Go 5 | --- 6 | on: 7 | image_version: 8 | types: 9 | - ready 10 | versions: 11 | - "1.0.0" 12 | - "1.0.1" 13 | jobs: 14 | my-job: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - run: echo hi 18 | --- 19 | { 20 | "events": { 21 | "image_version": { 22 | "types": [ 23 | "ready" 24 | ], 25 | "versions": [ 26 | "1.0.0", 27 | "1.0.1" 28 | ] 29 | } 30 | }, 31 | "jobs": [ 32 | { 33 | "type": "job", 34 | "id": "my-job", 35 | "name": "my-job", 36 | "if": { 37 | "type": 3, 38 | "expr": "success()" 39 | }, 40 | "runs-on": "ubuntu-latest", 41 | "steps": [ 42 | { 43 | "id": "__run", 44 | "if": { 45 | "type": 3, 46 | "expr": "success()" 47 | }, 48 | "run": "echo hi" 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /languageserver/src/description-provider.ts: -------------------------------------------------------------------------------- 1 | import {DescriptionProvider} from "@actions/languageservice/hover"; 2 | import {Octokit} from "@octokit/rest"; 3 | import {getActionDescription} from "./description-providers/action-description"; 4 | import {getActionInputDescription} from "./description-providers/action-input"; 5 | import {TTLCache} from "./utils/cache"; 6 | 7 | export function descriptionProvider(client: Octokit | undefined, cache: TTLCache): DescriptionProvider { 8 | const getDescription: DescriptionProvider["getDescription"] = async (context, token, path) => { 9 | if (!client || !context.step) { 10 | return undefined; 11 | } 12 | 13 | const parent = path[path.length - 1]; 14 | if (parent.definition?.key === "step-with") { 15 | return await getActionInputDescription(client, cache, context.step, token); 16 | } 17 | 18 | if (parent.definition?.key === "step-uses") { 19 | return await getActionDescription(client, cache, context.step); 20 | } 21 | }; 22 | 23 | return { 24 | getDescription 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-job-secrets.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo hi 9 | deploy: 10 | needs: build 11 | uses: contoso/templates/.github/workflows/deploy.yml@v1 12 | with: 13 | app_name: my app 14 | secrets: inhrit # typo 15 | --- 16 | contoso/templates/.github/workflows/deploy.yml@v1 17 | --- 18 | on: 19 | workflow_call: 20 | inputs: 21 | app_name: 22 | required: true 23 | type: string 24 | secrets: 25 | shh: 26 | required: true 27 | jobs: 28 | job1: 29 | runs-on: ubuntu-latest 30 | outputs: 31 | output1: ${{ steps.step1.outputs.test }} 32 | steps: 33 | - run: echo \""::set-output name=test::hello\"" 34 | --- 35 | { 36 | "errors": [ 37 | { 38 | "Message": ".github/workflows/errors-reusable-workflow-job-secrets.yml (Line: 12, Col: 14): Unexpected value 'inhrit'" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/tokens/types.ts: -------------------------------------------------------------------------------- 1 | export enum TokenType { 2 | String = 0, 3 | Sequence, 4 | Mapping, 5 | BasicExpression, 6 | InsertExpression, 7 | Boolean, 8 | Number, 9 | Null 10 | } 11 | 12 | export function tokenTypeName(type: TokenType): string { 13 | switch (type) { 14 | case TokenType.String: 15 | return "StringToken"; 16 | case TokenType.Sequence: 17 | return "SequenceToken"; 18 | case TokenType.Mapping: 19 | return "MappingToken"; 20 | case TokenType.BasicExpression: 21 | return "BasicExpressionToken"; 22 | case TokenType.InsertExpression: 23 | return "InsertExpressionToken"; 24 | case TokenType.Boolean: 25 | return "BooleanToken"; 26 | case TokenType.Number: 27 | return "NumberToken"; 28 | case TokenType.Null: 29 | return "NullToken"; 30 | default: { 31 | // Use never to ensure exhaustiveness 32 | const exhaustiveCheck: never = type; 33 | throw new Error(`Unhandled token type: ${type} ${exhaustiveCheck}}`); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-insert.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - TypeScript 4 | --- 5 | on: push 6 | jobs: 7 | run: 8 | runs-on: ubuntu-latest 9 | env: 10 | bad_insert1: "This is a bad ${{ insert }}" 11 | bad_insert2: "${{ insert }} are bad at the beginning" 12 | ${{ insert }}: ${{ github.ref }} 13 | steps: 14 | - run: echo hi 15 | --- 16 | { 17 | "errors": [ 18 | { 19 | "Message": ".github/workflows/errors-insert.yml (Line: 6, Col: 20): The directive 'insert' is not allowed in this context. Directives are not supported for expressions that are embedded within a string. Directives are only supported when the entire value is an expression." 20 | }, 21 | { 22 | "Message": ".github/workflows/errors-insert.yml (Line: 7, Col: 20): The directive 'insert' is not allowed in this context. Directives are not supported for expressions that are embedded within a string. Directives are only supported when the entire value is an expression." 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/job-with-outputs-context.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | outputs: 8 | environment: ${{ steps.a }} 9 | steps: 10 | - id: a 11 | run: echo hi 12 | --- 13 | { 14 | "jobs": [ 15 | { 16 | "type": "job", 17 | "id": "build", 18 | "name": "build", 19 | "if": { 20 | "type": 3, 21 | "expr": "success()" 22 | }, 23 | "runs-on": "ubuntu-latest", 24 | "outputs": { 25 | "type": 2, 26 | "map": [ 27 | { 28 | "Key": "environment", 29 | "Value": { 30 | "type": 3, 31 | "expr": "steps.a" 32 | } 33 | } 34 | ] 35 | }, 36 | "steps": [ 37 | { 38 | "id": "a", 39 | "if": { 40 | "type": 3, 41 | "expr": "success()" 42 | }, 43 | "run": "echo hi" 44 | } 45 | ] 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-job-nested-depth-exceeded.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | max-nested-reusable-workflows-depth: 2 5 | --- 6 | on: push 7 | jobs: 8 | deploy-level-0: 9 | uses: contoso/templates/.github/workflows/deploy-level-1.yml@v1 10 | --- 11 | contoso/templates/.github/workflows/deploy-level-1.yml@v1 12 | --- 13 | on: workflow_call 14 | jobs: 15 | deploy-level-1: 16 | uses: contoso/templates/.github/workflows/deploy-level-2.yml@v1 17 | --- 18 | contoso/templates/.github/workflows/deploy-level-2.yml@v1 19 | --- 20 | on: workflow_call 21 | jobs: 22 | deploy-level-2: 23 | uses: contoso/templates/.github/workflows/deploy-level-3.yml@v1 24 | --- 25 | contoso/templates/.github/workflows/deploy-level-3.yml@v1 26 | --- 27 | on: workflow_call 28 | jobs: 29 | deploy-level-3: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - run: echo hi 33 | --- 34 | { 35 | "errors": [ 36 | { 37 | "Message": "Nested reusable workflow depth exceeded 2." 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/job-outputs.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | one: 6 | runs-on: ubuntu-latest 7 | outputs: 8 | job-out: ${{ steps.test.outputs.foo }} 9 | steps: 10 | - run: echo 1 11 | id: test 12 | --- 13 | { 14 | "jobs": [ 15 | { 16 | "type": "job", 17 | "id": "one", 18 | "name": "one", 19 | "if": { 20 | "type": 3, 21 | "expr": "success()" 22 | }, 23 | "runs-on": "ubuntu-latest", 24 | "outputs": { 25 | "type": 2, 26 | "map": [ 27 | { 28 | "Key": "job-out", 29 | "Value": { 30 | "type": 3, 31 | "expr": "steps.test.outputs.foo" 32 | } 33 | } 34 | ] 35 | }, 36 | "steps": [ 37 | { 38 | "id": "test", 39 | "if": { 40 | "type": 3, 41 | "expr": "success()" 42 | }, 43 | "run": "echo 1" 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /languageserver/src/value-providers/job-environment.ts: -------------------------------------------------------------------------------- 1 | import {Value} from "@actions/languageservice/value-providers/config"; 2 | import {Octokit} from "@octokit/rest"; 3 | import {TTLCache} from "../utils/cache"; 4 | 5 | export async function getEnvironments(client: Octokit, cache: TTLCache, owner: string, name: string): Promise { 6 | const environments = await cache.get(`${owner}/${name}/environments`, undefined, () => 7 | fetchEnvironments(client, owner, name) 8 | ); 9 | return Array.from(environments).map(env => ({label: env})); 10 | } 11 | 12 | async function fetchEnvironments(client: Octokit, owner: string, name: string): Promise { 13 | let environments: string[] = []; 14 | try { 15 | const response = await client.repos.getAllEnvironments({ 16 | owner, 17 | repo: name 18 | }); 19 | 20 | if (response.data.environments) { 21 | environments = response.data.environments.map(env => env.name); 22 | } 23 | } catch (e) { 24 | console.log("Failure to retrieve environments: ", e); 25 | } 26 | 27 | return environments; 28 | } 29 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-workflow-level-from-caller-default.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | uses: contoso/templates/.github/workflows/deploy.yml@v1 10 | --- 11 | contoso/templates/.github/workflows/deploy.yml@v1 12 | --- 13 | on: workflow_call 14 | permissions: 15 | actions: write 16 | contents: write 17 | packages: write 18 | jobs: 19 | deploy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - run: echo hi 23 | --- 24 | { 25 | "errors": [ 26 | { 27 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-workflow-level-from-caller-default.yml (Line: 3, Col: 3): Error calling workflow 'contoso/templates/.github/workflows/deploy.yml@v1'. The workflow 'contoso/templates/.github/workflows/deploy.yml@v1' is requesting 'actions: write, contents: write, packages: write', but is only allowed 'actions: none, contents: read, packages: read'." 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-step-uses-syntax.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | # Valid 9 | - uses: docker://alpine:3.8 10 | - uses: actions/aws/ec2@main 11 | - uses: ./.github/actions/my-action 12 | 13 | # Invalid 14 | - uses: $$docker://alpine:3.8 15 | - uses: ...docker://alpine:3.8 16 | - uses: badrepo@invalid 17 | - uses: docker:// 18 | --- 19 | { 20 | "errors": [ 21 | { 22 | "Message": ".github/workflows/errors-step-uses-syntax.yml (Line: 12, Col: 15): Expected format {org}/{repo}[/path]@ref. Actual '$$docker://alpine:3.8'" 23 | }, 24 | { 25 | "Message": ".github/workflows/errors-step-uses-syntax.yml (Line: 13, Col: 15): Expected format {org}/{repo}[/path]@ref. Actual '...docker://alpine:3.8'" 26 | }, 27 | { 28 | "Message": ".github/workflows/errors-step-uses-syntax.yml (Line: 14, Col: 15): Expected format {org}/{repo}[/path]@ref. Actual 'badrepo@invalid'" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /languageservice/src/context-providers/secrets.ts: -------------------------------------------------------------------------------- 1 | import {data, DescriptionDictionary} from "@actions/expressions"; 2 | import {StringData} from "@actions/expressions/data/string"; 3 | import {WorkflowContext} from "../context/workflow-context.js"; 4 | import {Mode} from "./default.js"; 5 | import {getDescription} from "./descriptions.js"; 6 | 7 | export function getSecretsContext(workflowContext: WorkflowContext, mode: Mode): DescriptionDictionary { 8 | const d = new DescriptionDictionary({ 9 | key: "GITHUB_TOKEN", 10 | value: new data.StringData("***"), 11 | description: getDescription("secrets", "GITHUB_TOKEN") 12 | }); 13 | 14 | const eventsConfig = workflowContext?.template?.events; 15 | if (eventsConfig?.workflow_call) { 16 | // Unpredictable secrets may be passed in via a workflow_call trigger 17 | d.complete = false; 18 | if (mode === Mode.Completion) { 19 | for (const [name, value] of Object.entries(eventsConfig.workflow_call.secrets || {})) { 20 | d.add(name, new StringData(""), value.description); 21 | } 22 | } 23 | } 24 | 25 | return d; 26 | } 27 | -------------------------------------------------------------------------------- /languageservice/src/value-providers/config.ts: -------------------------------------------------------------------------------- 1 | import {WorkflowContext} from "../context/workflow-context.js"; 2 | 3 | export interface Value { 4 | /** Label of this value */ 5 | label: string; 6 | 7 | /** Optional description to show when auto-completing */ 8 | description?: string; 9 | 10 | /** Whether this value is deprecated */ 11 | deprecated?: boolean; 12 | 13 | /** Alternative insert text, if not given `label` will be used */ 14 | insertText?: string; 15 | 16 | /** Alternative filter text, if not given `label` will be used for filtering */ 17 | filterText?: string; 18 | 19 | /** Sort text to control ordering, if not given `label` will be used for sorting */ 20 | sortText?: string; 21 | } 22 | 23 | export enum ValueProviderKind { 24 | AllowedValues, 25 | SuggestedValues 26 | } 27 | 28 | export type ValueProvider = { 29 | kind: ValueProviderKind; 30 | caseInsensitive?: boolean; 31 | get: (context: WorkflowContext, existingValues?: Set) => Promise; 32 | }; 33 | 34 | export interface ValueProviderConfig { 35 | [definitionKey: string]: ValueProvider; 36 | } 37 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/tokens/literal-token.ts: -------------------------------------------------------------------------------- 1 | import {DefinitionInfo} from "../schema/definition-info.js"; 2 | import {ScalarToken} from "./scalar-token.js"; 3 | import {TokenRange} from "./token-range.js"; 4 | 5 | export abstract class LiteralToken extends ScalarToken { 6 | public constructor( 7 | type: number, 8 | file: number | undefined, 9 | range: TokenRange | undefined, 10 | definitionInfo: DefinitionInfo | undefined 11 | ) { 12 | super(type, file, range, definitionInfo); 13 | } 14 | 15 | public override get isLiteral(): boolean { 16 | return true; 17 | } 18 | 19 | public override get isExpression(): boolean { 20 | return false; 21 | } 22 | 23 | public override toDisplayString(): string { 24 | return ScalarToken.trimDisplayString(this.toString()); 25 | } 26 | 27 | /** 28 | * Throws a good debug message when an unexpected literal value is encountered 29 | */ 30 | public assertUnexpectedValue(objectDescription: string): void { 31 | throw new Error(`Error while reading '${objectDescription}'. Unexpected value '${this.toString()}'`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright GitHub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /browser-playground/README.md: -------------------------------------------------------------------------------- 1 | # browser-playground 2 | 3 | This is a web-based playground hosting the [language server](../actions-languageserver/) in a web worker connected to an instance of the [Monaco](https://microsoft.github.io/monaco-editor/) editor. You can try it at https://actions.github.com/languageservices. 4 | 5 | ## Contributing 6 | 7 | ### Build and run 8 | 9 | Even though the package is part of the `npm` workspace, it needs its dependencies to be installed locally in order to run `webpack-dev-server`. To do so, run: 10 | 11 | ```bash 12 | npm i --workspaces=false 13 | ``` 14 | 15 | then 16 | 17 | ```bash 18 | npm start 19 | ``` 20 | 21 | to build and serve the app at `localhost:8080`. 22 | 23 | ## Contributing 24 | 25 | See [CONTRIBUTING.md](../CONTRIBUTING.md) at the root of the repository for general guidelines and recommendations. 26 | 27 | If you do want to contribute, please run [prettier](https://prettier.io/) to format your code before submitting your PR. 28 | 29 | ## License 30 | 31 | This project is licensed under the terms of the MIT open source license. Please refer to [MIT](../LICENSE) for the full terms. -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/events-single.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | - C# 5 | --- 6 | on: push 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - run: echo hi 13 | continue-on-error: true 14 | --- 15 | { 16 | "events": { 17 | "push": {} 18 | }, 19 | "jobs": [ 20 | { 21 | "type": "job", 22 | "id": "build", 23 | "name": "build", 24 | "if": { 25 | "type": 3, 26 | "expr": "success()" 27 | }, 28 | "runs-on": "ubuntu-latest", 29 | "steps": [ 30 | { 31 | "id": "__actions_checkout", 32 | "if": { 33 | "type": 3, 34 | "expr": "success()" 35 | }, 36 | "uses": "actions/checkout@v3" 37 | }, 38 | { 39 | "id": "__run", 40 | "if": { 41 | "type": 3, 42 | "expr": "success()" 43 | }, 44 | "continue-on-error": true, 45 | "run": "echo hi" 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-required-property-missing.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: 4 | workflow_call: # missing 'type' 5 | inputs: 6 | username: 7 | description: 'A username passed from the caller workflow' 8 | default: 'john-doe' 9 | jobs: 10 | build: 11 | runs-on: self-hosted 12 | concurrency: # missing 'group' 13 | cancel-in-progress: true 14 | steps: 15 | - run: echo Hi 16 | build2: 17 | runs-on: self-hosted 18 | environment: # missing 'name' 19 | url: https://github.com 20 | steps: 21 | - run: echo Hi 22 | 23 | --- 24 | { 25 | "errors": [ 26 | { 27 | "Message": ".github/workflows/errors-required-property-missing.yml (Line: 5, Col: 9): Required property is missing: type" 28 | }, 29 | { 30 | "Message": ".github/workflows/errors-required-property-missing.yml (Line: 11, Col: 7): Required property is missing: group" 31 | }, 32 | { 33 | "Message": ".github/workflows/errors-required-property-missing.yml (Line: 17, Col: 7): Required property is missing: name" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/on-workflow_call-mapping.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_call: 8 | inputs: 9 | test_string: 10 | description: test input 11 | required: true 12 | type: string 13 | test_number: 14 | required: false 15 | type: number 16 | test_boolean: 17 | required: false 18 | type: boolean 19 | secrets: 20 | shh: 21 | required: true 22 | jobs: 23 | my-job: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - run: echo hi 27 | --- 28 | { 29 | "jobs": [ 30 | { 31 | "type": "job", 32 | "id": "my-job", 33 | "name": "my-job", 34 | "if": { 35 | "type": 3, 36 | "expr": "success()" 37 | }, 38 | "runs-on": "ubuntu-latest", 39 | "steps": [ 40 | { 41 | "id": "__run", 42 | "if": { 43 | "type": 3, 44 | "expr": "success()" 45 | }, 46 | "run": "echo hi" 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/tokens/number-token.ts: -------------------------------------------------------------------------------- 1 | import {LiteralToken, TemplateToken} from "./index.js"; 2 | import {DefinitionInfo} from "../schema/definition-info.js"; 3 | import {TokenRange} from "./token-range.js"; 4 | import {TokenType} from "./types.js"; 5 | 6 | export class NumberToken extends LiteralToken { 7 | private readonly num: number; 8 | 9 | public constructor( 10 | file: number | undefined, 11 | range: TokenRange | undefined, 12 | value: number, 13 | definitionInfo: DefinitionInfo | undefined 14 | ) { 15 | super(TokenType.Number, file, range, definitionInfo); 16 | this.num = value; 17 | } 18 | 19 | public get value(): number { 20 | return this.num; 21 | } 22 | 23 | public override clone(omitSource?: boolean): TemplateToken { 24 | return omitSource 25 | ? new NumberToken(undefined, undefined, this.num, this.definitionInfo) 26 | : new NumberToken(this.file, this.range, this.num, this.definitionInfo); 27 | } 28 | 29 | public override toString(): string { 30 | return `${this.num}`; 31 | } 32 | 33 | public override toJSON() { 34 | return this.num; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /workflow-parser/src/workflows/file-reference.ts: -------------------------------------------------------------------------------- 1 | export type FileReference = LocalFileReference | RemoteFileReference; 2 | 3 | export type LocalFileReference = { 4 | path: string; 5 | }; 6 | 7 | export type RemoteFileReference = { 8 | repository: string; 9 | owner: string; 10 | path: string; 11 | version: string; 12 | }; 13 | 14 | export function parseFileReference(ref: string): FileReference { 15 | if (ref.startsWith("./")) { 16 | return { 17 | path: ref.substring(2) 18 | }; 19 | } 20 | 21 | const [remotePath, version] = ref.split("@"); 22 | const [owner, repository, ...pathSegments] = remotePath.split("/").filter(s => s.length > 0); 23 | 24 | if (!owner || !repository || !version) { 25 | throw new Error(`Invalid file reference: ${ref}`); 26 | } 27 | 28 | return { 29 | repository, 30 | owner, 31 | path: pathSegments.join("/"), 32 | version 33 | }; 34 | } 35 | 36 | export function fileIdentifier(ref: FileReference): string { 37 | if (!("repository" in ref)) { 38 | return "./" + ref.path; 39 | } 40 | 41 | return `${ref.owner}/${ref.repository}/${ref.path}@${ref.version}`; 42 | } 43 | -------------------------------------------------------------------------------- /languageservice/src/test-utils/test-workflow-context.ts: -------------------------------------------------------------------------------- 1 | import {convertWorkflowTemplate, parseWorkflow, WorkflowTemplate} from "@actions/workflow-parser"; 2 | import {getWorkflowContext, WorkflowContext} from "../context/workflow-context.js"; 3 | import {nullTrace} from "../nulltrace.js"; 4 | import {findToken} from "../utils/find-token.js"; 5 | import {getPositionFromCursor} from "./cursor-position.js"; 6 | import {testFileProvider} from "./test-file-provider.js"; 7 | 8 | export async function testGetWorkflowContext(input: string): Promise { 9 | const [textDocument, pos] = getPositionFromCursor(input); 10 | const result = parseWorkflow( 11 | { 12 | content: textDocument.getText(), 13 | name: "wf.yaml" 14 | }, 15 | nullTrace 16 | ); 17 | 18 | let template: WorkflowTemplate | undefined; 19 | 20 | if (result.value) { 21 | template = await convertWorkflowTemplate(result.context, result.value, testFileProvider, { 22 | fetchReusableWorkflowDepth: 1 23 | }); 24 | } 25 | 26 | const {path} = findToken(pos, result.value); 27 | 28 | return getWorkflowContext(textDocument.uri, template, path); 29 | } 30 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/workflow-concurrency-mapping.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | concurrency: 5 | group: ci-${{ github.ref }} 6 | cancel-in-progress: true 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - run: echo hi 12 | --- 13 | { 14 | "concurrency": { 15 | "type": 2, 16 | "map": [ 17 | { 18 | "Key": "group", 19 | "Value": { 20 | "type": 3, 21 | "expr": "format('ci-{0}', github.ref)" 22 | } 23 | }, 24 | { 25 | "Key": "cancel-in-progress", 26 | "Value": true 27 | } 28 | ] 29 | }, 30 | "jobs": [ 31 | { 32 | "type": "job", 33 | "id": "build", 34 | "name": "build", 35 | "if": { 36 | "type": 3, 37 | "expr": "success()" 38 | }, 39 | "runs-on": "ubuntu-latest", 40 | "steps": [ 41 | { 42 | "id": "__run", 43 | "if": { 44 | "type": 3, 45 | "expr": "success()" 46 | }, 47 | "run": "echo hi" 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /script/workflows/generate-release-notes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # this script is used to generate release notes for a given release 3 | # first argument is the pull request id for the last release 4 | # the second is the new release number 5 | 6 | # the script then grabs every pull request merged since that pull request 7 | # and outputs a string of release notes 8 | 9 | # get the new release number 10 | NEW_RELEASE=$2 11 | 12 | echo "Generating release notes for $NEW_RELEASE" 13 | 14 | # get the last release pull request id 15 | LAST_RELEASE_PR=$1 16 | 17 | 18 | 19 | #get when the last release was merged 20 | LAST_RELEASE_MERGED_AT=$(gh pr view $LAST_RELEASE_PR --repo actions/languageservices --json mergedAt | jq -r '.mergedAt') 21 | 22 | CHANGELIST=$(gh pr list --repo actions/languageservices --base main --state merged --json title --search "merged:>$LAST_RELEASE_MERGED_AT -label:no-release") 23 | 24 | # store the release notes in a variable so we can use it later 25 | 26 | echo "Release $NEW_RELEASE" >> releasenotes.md 27 | 28 | echo $CHANGELIST | jq -r '.[].title' | while read line; do 29 | echo " - $line" >> releasenotes.md 30 | done 31 | 32 | echo " " 33 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-job-runs-on-group-invalid-prefix.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: 7 | group: ent/org/my-group 8 | steps: 9 | - run: echo hi 10 | build2: 11 | runs-on: 12 | group: asdf/my-group 13 | steps: 14 | - run: echo hi 15 | build3: 16 | runs-on: 17 | group: ent/ 18 | steps: 19 | - run: echo hi 20 | --- 21 | { 22 | "errors": [ 23 | { 24 | "Message": ".github/workflows/errors-job-runs-on-group-invalid-prefix.yml (Line: 5, Col: 14): Invalid runs-on group name 'ent/org/my-group'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group." 25 | }, 26 | { 27 | "Message": ".github/workflows/errors-job-runs-on-group-invalid-prefix.yml (Line: 10, Col: 14): Invalid runs-on group name 'asdf/my-group'. Please use 'organization/' or 'enterprise/' prefix to target a single runner group." 28 | }, 29 | { 30 | "Message": ".github/workflows/errors-job-runs-on-group-invalid-prefix.yml (Line: 15, Col: 14): Invalid runs-on group name 'ent/'." 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /workflow-parser/src/templates/tokens/boolean-token.ts: -------------------------------------------------------------------------------- 1 | import {LiteralToken, TemplateToken} from "./index.js"; 2 | import {DefinitionInfo} from "../schema/definition-info.js"; 3 | import {TokenRange} from "./token-range.js"; 4 | import {TokenType} from "./types.js"; 5 | 6 | export class BooleanToken extends LiteralToken { 7 | private readonly bool: boolean; 8 | 9 | public constructor( 10 | file: number | undefined, 11 | range: TokenRange | undefined, 12 | value: boolean, 13 | definitionInfo: DefinitionInfo | undefined 14 | ) { 15 | super(TokenType.Boolean, file, range, definitionInfo); 16 | this.bool = value; 17 | } 18 | 19 | public get value(): boolean { 20 | return this.bool; 21 | } 22 | 23 | public override clone(omitSource?: boolean): TemplateToken { 24 | return omitSource 25 | ? new BooleanToken(undefined, undefined, this.bool, this.definitionInfo) 26 | : new BooleanToken(this.file, this.range, this.bool, this.definitionInfo); 27 | } 28 | 29 | public override toString(): string { 30 | return this.bool ? "true" : "false"; 31 | } 32 | 33 | public override toJSON() { 34 | return this.bool; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/events-sequence.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | - C# 5 | --- 6 | on: [push, pull_request] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - run: echo hi 13 | continue-on-error: true 14 | --- 15 | { 16 | "events": { 17 | "push": {}, 18 | "pull_request": {} 19 | }, 20 | "jobs": [ 21 | { 22 | "type": "job", 23 | "id": "build", 24 | "name": "build", 25 | "if": { 26 | "type": 3, 27 | "expr": "success()" 28 | }, 29 | "runs-on": "ubuntu-latest", 30 | "steps": [ 31 | { 32 | "id": "__actions_checkout", 33 | "if": { 34 | "type": 3, 35 | "expr": "success()" 36 | }, 37 | "uses": "actions/checkout@v3" 38 | }, 39 | { 40 | "id": "__run", 41 | "if": { 42 | "type": 3, 43 | "expr": "success()" 44 | }, 45 | "continue-on-error": true, 46 | "run": "echo hi" 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/float-folded-style.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Switch to using Python 3.10 by default 9 | uses: actions/setup-python@v4 10 | with: 11 | python-version: >- 12 | 3.10 13 | --- 14 | { 15 | "jobs": [ 16 | { 17 | "type": "job", 18 | "id": "build", 19 | "name": "build", 20 | "if": { 21 | "type": 3, 22 | "expr": "success()" 23 | }, 24 | "runs-on": "ubuntu-latest", 25 | "steps": [ 26 | { 27 | "id": "__actions_setup-python", 28 | "name": "Switch to using Python 3.10 by default", 29 | "if": { 30 | "type": 3, 31 | "expr": "success()" 32 | }, 33 | "uses": "actions/setup-python@v4", 34 | "with": { 35 | "type": 2, 36 | "map": [ 37 | { 38 | "Key": "python-version", 39 | "Value": "3.10" 40 | } 41 | ] 42 | } 43 | } 44 | ] 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /expressions/src/completion/descriptionDictionary.test.ts: -------------------------------------------------------------------------------- 1 | import {StringData} from "../data/index.js"; 2 | import {DescriptionDictionary} from "./descriptionDictionary.js"; 3 | 4 | describe("description dictionary", () => { 5 | it("pairs contains all values", () => { 6 | const d = new DescriptionDictionary(); 7 | d.add("ABC", new StringData("val")); 8 | 9 | expect(d.pairs()).toEqual([{key: "ABC", value: new StringData("val")}]); 10 | }); 11 | 12 | it("does not add duplicate entries", () => { 13 | const d = new DescriptionDictionary(); 14 | d.add("ABC", new StringData("val1")); 15 | d.add("ABC", new StringData("val2")); 16 | d.add("abc", new StringData("val3")); 17 | 18 | expect(d.pairs()).toEqual([{key: "ABC", value: new StringData("val1")}]); 19 | }); 20 | 21 | it("can set optional descriptions", () => { 22 | const d = new DescriptionDictionary(); 23 | d.add("ABC", new StringData("val"), "desc"); 24 | d.add("DEF", new StringData("val")); 25 | 26 | expect(d.pairs()).toEqual([ 27 | {key: "ABC", value: new StringData("val"), description: "desc"}, 28 | {key: "DEF", value: new StringData("val")} 29 | ]); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/events-mapping-repo-dispatch.yml: -------------------------------------------------------------------------------- 1 | skip: 2 | - C# 3 | - Go 4 | --- 5 | on: 6 | repository_dispatch: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - run: echo hi 15 | continue-on-error: true 16 | --- 17 | { 18 | "events": { 19 | "repository_dispatch": {}, 20 | "workflow_dispatch": {} 21 | }, 22 | "jobs": [ 23 | { 24 | "type": "job", 25 | "id": "build", 26 | "name": "build", 27 | "if": { 28 | "type": 3, 29 | "expr": "success()" 30 | }, 31 | "runs-on": "ubuntu-latest", 32 | "steps": [ 33 | { 34 | "id": "__actions_checkout", 35 | "if": { 36 | "type": 3, 37 | "expr": "success()" 38 | }, 39 | "uses": "actions/checkout@v3" 40 | }, 41 | { 42 | "id": "__run", 43 | "if": { 44 | "type": 3, 45 | "expr": "success()" 46 | }, 47 | "continue-on-error": true, 48 | "run": "echo hi" 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/tokens/string-token.ts: -------------------------------------------------------------------------------- 1 | import {LiteralToken, TemplateToken} from "./index.js"; 2 | import {DefinitionInfo} from "../schema/definition-info.js"; 3 | import {TokenRange} from "./token-range.js"; 4 | import {TokenType} from "./types.js"; 5 | 6 | export class StringToken extends LiteralToken { 7 | public readonly value: string; 8 | public readonly source: string | undefined; 9 | 10 | public constructor( 11 | file: number | undefined, 12 | range: TokenRange | undefined, 13 | value: string, 14 | definitionInfo: DefinitionInfo | undefined, 15 | source?: string 16 | ) { 17 | super(TokenType.String, file, range, definitionInfo); 18 | this.value = value; 19 | this.source = source; 20 | } 21 | 22 | public override clone(omitSource?: boolean): TemplateToken { 23 | return omitSource 24 | ? new StringToken(undefined, undefined, this.value, this.definitionInfo, this.source) 25 | : new StringToken(this.file, this.range, this.value, this.definitionInfo, this.source); 26 | } 27 | 28 | public override toString(): string { 29 | return this.value; 30 | } 31 | 32 | public override toJSON() { 33 | return this.value; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /languageservice/src/context-providers/descriptions.ts: -------------------------------------------------------------------------------- 1 | import descriptions from "./descriptions.min.json"; 2 | 3 | export const RootContext = "root"; 4 | const FunctionContext = "functions"; 5 | 6 | /** 7 | * Get a description for a built-in context 8 | * @param context Name of the context, for example `github` 9 | * @param key Key of the context, for example `actor` 10 | * @returns Description if one is found, otherwise undefined 11 | */ 12 | export function getDescription(context: string, key: string): string | undefined { 13 | // The inferred type doesn't quite match the actual type, use any to work around that 14 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any 15 | return (descriptions as any)[context]?.[key]?.description; 16 | } 17 | 18 | /** 19 | * Get a description for a context function 20 | * This will not include functions defined by the expressions library (e.g. `contains`, `fromJSON`, etc.) 21 | * @param name Name of the function, for example `hashFiles` 22 | */ 23 | export function getFunctionDescription(name: string): string | undefined { 24 | return getDescription(FunctionContext, name); 25 | } 26 | -------------------------------------------------------------------------------- /languageservice/src/test-utils/cursor-position.ts: -------------------------------------------------------------------------------- 1 | import {Position, TextDocument} from "vscode-languageserver-textdocument"; 2 | import {createDocument} from "./document.js"; 3 | 4 | /** 5 | * Calculates the position of the cursor and the document without that cursor 6 | * Cursor is represented by a `|` character 7 | * @param input Input string 8 | * @param skip Instances of `|` to skip 9 | */ 10 | export function getPositionFromCursor(input: string, skip = 0): [TextDocument, Position] { 11 | const doc = createDocument("test.yaml", input); 12 | 13 | let cursorIndex = doc.getText().indexOf("|"); 14 | for (let i = 0; i < skip && cursorIndex !== -1; i++) { 15 | cursorIndex = doc.getText().indexOf("|", cursorIndex + 1); 16 | } 17 | 18 | if (cursorIndex === -1) { 19 | throw new Error("No cursor found in document"); 20 | } 21 | 22 | // Replace only the last occurence of | in string 23 | let newText = doc.getText(); 24 | newText = newText.substring(0, cursorIndex) + newText.substring(cursorIndex + 1); 25 | 26 | const position = doc.positionAt(cursorIndex); 27 | const newDoc = TextDocument.create(doc.uri, doc.languageId, doc.version, newText); 28 | 29 | return [newDoc, position]; 30 | } 31 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-job-nested-permissions-not-allowed-job-level-from-caller-job-level.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | permissions: 6 | actions: write 7 | on: push 8 | jobs: 9 | deploy-level-0: 10 | uses: contoso/templates/.github/workflows/deploy-level-1.yml@v1 11 | --- 12 | contoso/templates/.github/workflows/deploy-level-1.yml@v1 13 | --- 14 | on: workflow_call 15 | jobs: 16 | deploy-level-1: 17 | permissions: 18 | actions: read 19 | uses: contoso/templates/.github/workflows/deploy-level-2.yml@v1 20 | --- 21 | contoso/templates/.github/workflows/deploy-level-2.yml@v1 22 | --- 23 | on: workflow_call 24 | jobs: 25 | deploy-level-2: 26 | permissions: 27 | actions: write 28 | runs-on: ubuntu-latest 29 | steps: 30 | - run: echo hi 31 | --- 32 | { 33 | "errors": [ 34 | { 35 | "Message": "contoso/templates/.github/workflows/deploy-level-1.yml@v1 (Line: 3, Col: 3): Error calling workflow 'contoso/templates/.github/workflows/deploy-level-2.yml@v1'. The nested job 'deploy-level-2' is requesting 'actions: write', but is only allowed 'actions: read'." 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-job-nested-permissions-not-allowed-job-level-from-caller-workflow-level.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | permissions: 6 | actions: write 7 | on: push 8 | jobs: 9 | deploy-level-0: 10 | uses: contoso/templates/.github/workflows/deploy-level-1.yml@v1 11 | --- 12 | contoso/templates/.github/workflows/deploy-level-1.yml@v1 13 | --- 14 | permissions: 15 | actions: read 16 | on: workflow_call 17 | jobs: 18 | deploy-level-1: 19 | uses: contoso/templates/.github/workflows/deploy-level-2.yml@v1 20 | --- 21 | contoso/templates/.github/workflows/deploy-level-2.yml@v1 22 | --- 23 | on: workflow_call 24 | jobs: 25 | deploy-level-2: 26 | permissions: 27 | actions: write 28 | runs-on: ubuntu-latest 29 | steps: 30 | - run: echo hi 31 | --- 32 | { 33 | "errors": [ 34 | { 35 | "Message": "contoso/templates/.github/workflows/deploy-level-1.yml@v1 (Line: 5, Col: 3): Error calling workflow 'contoso/templates/.github/workflows/deploy-level-2.yml@v1'. The nested job 'deploy-level-2' is requesting 'actions: write', but is only allowed 'actions: read'." 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /expressions/testdata/op_or.json: -------------------------------------------------------------------------------- 1 | { 2 | "or": [ 3 | { 4 | "expr": "true || true || true", 5 | "result": { "kind": "Boolean", "value": true } 6 | }, 7 | { "expr": "true || true", "result": { "kind": "Boolean", "value": true } }, 8 | { 9 | "expr": "true || true || false", 10 | "result": { "kind": "Boolean", "value": true } 11 | }, 12 | { "expr": "true || false", "result": { "kind": "Boolean", "value": true } }, 13 | { "expr": "false || true", "result": { "kind": "Boolean", "value": true } }, 14 | { 15 | "expr": "false || false", 16 | "result": { "kind": "Boolean", "value": false } 17 | }, 18 | { 19 | "expr": "0 || 0 || 2 || 3", 20 | "result": { "kind": "Number", "value": 2.0 } 21 | }, 22 | { "expr": "false || 0", "result": { "kind": "Number", "value": 0.0 } }, 23 | { 24 | "expr": "false || '' || 'a' || 'b'", 25 | "result": { "kind": "String", "value": "a" } 26 | }, 27 | { "expr": "false || ''", "result": { "kind": "String", "value": "" } }, 28 | { "expr": "null || true", "result": { "kind": "Boolean", "value": true } }, 29 | { "expr": "false || null", "result": { "kind": "Null", "value": null } } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/id-to-long.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: docker://chinthakagodawita/autoupdate-action@sha256:a3e234f9fce69dd9b3a205acfd55bf9d5c94f0f7cf119f0267a5ab54220e8f56 # v1 9 | - run: echo hi 10 | --- 11 | { 12 | "jobs": [ 13 | { 14 | "type": "job", 15 | "id": "build", 16 | "name": "build", 17 | "if": { 18 | "type": 3, 19 | "expr": "success()" 20 | }, 21 | "runs-on": "ubuntu-latest", 22 | "steps": [ 23 | { 24 | "id": "__chinthakagodawita_autoupdate-action_sha256_a3e234f9fce69dd9b3a205acfd55bf9d5c94f0f7cf119f0267a5ab5", 25 | "if": { 26 | "type": 3, 27 | "expr": "success()" 28 | }, 29 | "uses": "docker://chinthakagodawita/autoupdate-action@sha256:a3e234f9fce69dd9b3a205acfd55bf9d5c94f0f7cf119f0267a5ab54220e8f56" 30 | }, 31 | { 32 | "id": "__run", 33 | "if": { 34 | "type": 3, 35 | "expr": "success()" 36 | }, 37 | "run": "echo hi" 38 | } 39 | ] 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /expressions/src/data/expressiondata.ts: -------------------------------------------------------------------------------- 1 | import {Dictionary} from "./dictionary.js"; 2 | import {Null} from "./null.js"; 3 | import {Array} from "./array.js"; 4 | import {StringData} from "./string.js"; 5 | import {NumberData} from "./number.js"; 6 | import {BooleanData} from "./boolean.js"; 7 | 8 | export enum Kind { 9 | String = 0, 10 | Array, 11 | Dictionary, 12 | Boolean, 13 | Number, 14 | CaseSensitiveDictionary, 15 | Null 16 | } 17 | 18 | export function kindStr(k: Kind): string { 19 | switch (k) { 20 | case Kind.Array: 21 | return "Array"; 22 | case Kind.Boolean: 23 | return "Boolean"; 24 | case Kind.Null: 25 | return "Null"; 26 | case Kind.Number: 27 | return "Number"; 28 | case Kind.Dictionary: 29 | return "Object"; 30 | case Kind.String: 31 | return "String"; 32 | } 33 | 34 | return "unknown"; 35 | } 36 | 37 | export interface ExpressionDataInterface { 38 | kind: Kind; 39 | primitive: boolean; 40 | 41 | coerceString(): string; 42 | 43 | number(): number; 44 | } 45 | 46 | export type ExpressionData = Array | Dictionary | StringData | BooleanData | NumberData | Null; 47 | 48 | export type Pair = { 49 | key: string; 50 | value: ExpressionData; 51 | }; 52 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/workflow-defaults.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | defaults: 5 | run: 6 | shell: cmd 7 | working-directory: foo 8 | jobs: 9 | one: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - run: echo hi 13 | --- 14 | { 15 | "defaults": { 16 | "type": 2, 17 | "map": [ 18 | { 19 | "Key": "run", 20 | "Value": { 21 | "type": 2, 22 | "map": [ 23 | { 24 | "Key": "shell", 25 | "Value": "cmd" 26 | }, 27 | { 28 | "Key": "working-directory", 29 | "Value": "foo" 30 | } 31 | ] 32 | } 33 | } 34 | ] 35 | }, 36 | "jobs": [ 37 | { 38 | "type": "job", 39 | "id": "one", 40 | "name": "one", 41 | "if": { 42 | "type": 3, 43 | "expr": "success()" 44 | }, 45 | "runs-on": "ubuntu-latest", 46 | "steps": [ 47 | { 48 | "id": "__run", 49 | "if": { 50 | "type": 3, 51 | "expr": "success()" 52 | }, 53 | "run": "echo hi" 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /languageserver/src/test-utils/workflow-context.ts: -------------------------------------------------------------------------------- 1 | import {WorkflowContext} from "@actions/languageservice/context/workflow-context"; 2 | import {convertWorkflowTemplate, NoOperationTraceWriter, parseWorkflow} from "@actions/workflow-parser"; 3 | import {isJob} from "@actions/workflow-parser/model/type-guards"; 4 | 5 | export async function createWorkflowContext( 6 | workflow: string, 7 | job?: string, 8 | stepIndex?: number 9 | ): Promise { 10 | const parsed = parseWorkflow({name: "test.yaml", content: workflow}, new NoOperationTraceWriter()); 11 | if (!parsed.value) { 12 | throw new Error("Failed to parse workflow"); 13 | } 14 | const template = await convertWorkflowTemplate(parsed.context, parsed.value); 15 | const context: WorkflowContext = {uri: "test.yaml", template}; 16 | 17 | if (job) { 18 | const workflowJob = template.jobs.find(j => j.id.value === job); 19 | if (workflowJob) { 20 | if (isJob(workflowJob)) { 21 | context.job = workflowJob; 22 | } else { 23 | context.reusableWorkflowJob = workflowJob; 24 | } 25 | } 26 | } 27 | 28 | if (stepIndex !== undefined) { 29 | context.step = context.job?.steps[stepIndex]; 30 | } 31 | 32 | return context; 33 | } 34 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-job-nested-permissions-not-allowed-workflow-level-from-caller-workflow-level.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | permissions: 6 | actions: write 7 | on: push 8 | jobs: 9 | deploy-level-0: 10 | uses: contoso/templates/.github/workflows/deploy-level-1.yml@v1 11 | --- 12 | contoso/templates/.github/workflows/deploy-level-1.yml@v1 13 | --- 14 | permissions: 15 | actions: read 16 | on: workflow_call 17 | jobs: 18 | deploy-level-1: 19 | uses: contoso/templates/.github/workflows/deploy-level-2.yml@v1 20 | --- 21 | contoso/templates/.github/workflows/deploy-level-2.yml@v1 22 | --- 23 | on: workflow_call 24 | permissions: 25 | actions: write 26 | jobs: 27 | deploy-level-2: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - run: echo hi 31 | --- 32 | { 33 | "errors": [ 34 | { 35 | "Message": "contoso/templates/.github/workflows/deploy-level-1.yml@v1 (Line: 5, Col: 3): Error calling workflow 'contoso/templates/.github/workflows/deploy-level-2.yml@v1'. The workflow 'contoso/templates/.github/workflows/deploy-level-2.yml@v1' is requesting 'actions: write', but is only allowed 'actions: read'." 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-read-all-allowed-none.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | # Note, the workflow names are intentionally short so the error message doesn't get truncated too much 6 | --- 7 | on: push 8 | jobs: 9 | deploy: 10 | permissions: {} 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: read-all 20 | runs-on: ubuntu-latest 21 | steps: 22 | - run: echo hi 23 | --- 24 | { 25 | "errors": [ 26 | { 27 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-read-all-allowed-none.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'actions: read, checks: read, contents: read, deployments: read, discussions: read, issues: read, packages: read, pages: read, pull-requests: read, repository-projects: read, statuses: read, security-events: read, id-token: read', but is only allowed 'actions: none, checks: none, co[...]" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/max-result-size.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | max-result-size: 1128 3 | --- 4 | on: push 5 | jobs: 6 | job1: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo Deploying 1... 10 | - run: echo Deploying 2... 11 | - run: echo Deploying 3... 12 | --- 13 | { 14 | "jobs": [ 15 | { 16 | "type": "job", 17 | "id": "job1", 18 | "name": "job1", 19 | "if": { 20 | "type": 3, 21 | "expr": "success()" 22 | }, 23 | "runs-on": "ubuntu-latest", 24 | "steps": [ 25 | { 26 | "id": "__run", 27 | "if": { 28 | "type": 3, 29 | "expr": "success()" 30 | }, 31 | "run": "echo Deploying 1..." 32 | }, 33 | { 34 | "id": "__run_2", 35 | "if": { 36 | "type": 3, 37 | "expr": "success()" 38 | }, 39 | "run": "echo Deploying 2..." 40 | }, 41 | { 42 | "id": "__run_3", 43 | "if": { 44 | "type": 3, 45 | "expr": "success()" 46 | }, 47 | "run": "echo Deploying 3..." 48 | } 49 | ] 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-job-nested-permissions-not-allowed-workflow-level-from-caller-job-level.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | --- 5 | permissions: 6 | actions: write 7 | on: push 8 | jobs: 9 | deploy-level-0: 10 | uses: contoso/templates/.github/workflows/deploy-level-1.yml@v1 11 | --- 12 | contoso/templates/.github/workflows/deploy-level-1.yml@v1 13 | --- 14 | on: workflow_call 15 | jobs: 16 | deploy-level-1: 17 | permissions: 18 | actions: read 19 | uses: contoso/templates/.github/workflows/deploy-level-2.yml@v1 20 | --- 21 | contoso/templates/.github/workflows/deploy-level-2.yml@v1 22 | --- 23 | on: workflow_call 24 | permissions: 25 | actions: write 26 | jobs: 27 | deploy-level-2: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - run: echo hi 31 | --- 32 | { 33 | "errors": [ 34 | { 35 | "Message": "contoso/templates/.github/workflows/deploy-level-1.yml@v1 (Line: 3, Col: 3): Error calling workflow 'contoso/templates/.github/workflows/deploy-level-2.yml@v1'. The workflow 'contoso/templates/.github/workflows/deploy-level-2.yml@v1' is requesting 'actions: write', but is only allowed 'actions: read'." 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-write-all-allowed-none.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | # Note, the workflow names are intentionally short so the error message doesn't get truncated too much 6 | --- 7 | on: push 8 | jobs: 9 | deploy: 10 | permissions: {} 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: write-all 20 | runs-on: ubuntu-latest 21 | steps: 22 | - run: echo hi 23 | --- 24 | { 25 | "errors": [ 26 | { 27 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-write-all-allowed-none.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'actions: write, checks: write, contents: write, deployments: write, discussions: write, issues: write, packages: write, pages: write, pull-requests: write, repository-projects: write, statuses: write, security-events: write, id-token: write', but is only allowed 'actions: none, ch[...]" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /languageservice/src/value-providers/reusable-job-inputs.ts: -------------------------------------------------------------------------------- 1 | import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token"; 2 | import {isMapping, isString} from "@actions/workflow-parser/templates/tokens/type-guards"; 3 | import {WorkflowContext} from "../context/workflow-context.js"; 4 | import {Value} from "./config.js"; 5 | 6 | export function reusableJobInputs(context: WorkflowContext): Value[] { 7 | if (!context.reusableWorkflowJob?.["input-definitions"]) { 8 | return []; 9 | } 10 | 11 | const values: Value[] = []; 12 | 13 | for (const {key, value} of context.reusableWorkflowJob["input-definitions"]) { 14 | if (!isString(key)) { 15 | continue; 16 | } 17 | 18 | values.push({ 19 | label: key.value, 20 | description: inputDescription(value), 21 | insertText: `${key.value}: ` 22 | }); 23 | } 24 | 25 | return values; 26 | } 27 | 28 | function inputDescription(inputDef: TemplateToken): string | undefined { 29 | if (!isMapping(inputDef)) { 30 | return undefined; 31 | } 32 | 33 | const descriptionToken = inputDef.find("description"); 34 | if (!descriptionToken || !isString(descriptionToken)) { 35 | return undefined; 36 | } 37 | 38 | return descriptionToken.value; 39 | } 40 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/events-single-with-types.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | - C# 5 | --- 6 | on: 7 | pull_request: 8 | types: synchronize 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - run: echo hi 15 | continue-on-error: true 16 | --- 17 | { 18 | "events": { 19 | "pull_request": { 20 | "types": [ 21 | "synchronize" 22 | ] 23 | } 24 | }, 25 | "jobs": [ 26 | { 27 | "type": "job", 28 | "id": "build", 29 | "name": "build", 30 | "if": { 31 | "type": 3, 32 | "expr": "success()" 33 | }, 34 | "runs-on": "ubuntu-latest", 35 | "steps": [ 36 | { 37 | "id": "__actions_checkout", 38 | "if": { 39 | "type": 3, 40 | "expr": "success()" 41 | }, 42 | "uses": "actions/checkout@v3" 43 | }, 44 | { 45 | "id": "__run", 46 | "if": { 47 | "type": 3, 48 | "expr": "success()" 49 | }, 50 | "continue-on-error": true, 51 | "run": "echo hi" 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-write-all-allowed-read-all.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | # Note, the workflow names are intentionally short so the error message doesn't get truncated too much 6 | --- 7 | on: push 8 | jobs: 9 | deploy: 10 | permissions: read-all 11 | uses: a/b/.github/workflows/c.yml@v1 12 | --- 13 | a/b/.github/workflows/c.yml@v1 14 | --- 15 | on: workflow_call 16 | jobs: 17 | deploy: 18 | name: Deploy 1 19 | permissions: write-all 20 | runs-on: ubuntu-latest 21 | steps: 22 | - run: echo hi 23 | --- 24 | { 25 | "errors": [ 26 | { 27 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-write-all-allowed-read-all.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'actions: write, checks: write, contents: write, deployments: write, discussions: write, issues: write, packages: write, pages: write, pull-requests: write, repository-projects: write, statuses: write, security-events: write, id-token: write', but is only allowed 'actions: read[...]" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /expressions/src/funcs/join.ts: -------------------------------------------------------------------------------- 1 | import {ExpressionData, Kind, StringData} from "../data/index.js"; 2 | import {FunctionDefinition} from "./info.js"; 3 | 4 | export const join: FunctionDefinition = { 5 | name: "join", 6 | description: 7 | "`join( array, optionalSeparator )`\n\nThe value for `array` can be an array or a string. All values in `array` are concatenated into a string. If you provide `optionalSeparator`, it is inserted between the concatenated values. Otherwise, the default separator `,` is used. Casts values to a string.", 8 | minArgs: 1, 9 | maxArgs: 2, 10 | call: (...args: ExpressionData[]): ExpressionData => { 11 | // Primitive 12 | if (args[0].primitive) { 13 | return new StringData(args[0].coerceString()); 14 | } 15 | 16 | // Array 17 | if (args[0].kind === Kind.Array) { 18 | // Separator 19 | let separator = ","; 20 | if (args.length > 1 && args[1].primitive) { 21 | separator = args[1].coerceString(); 22 | } 23 | 24 | // Convert items to strings 25 | return new StringData( 26 | args[0] 27 | .values() 28 | .map(item => item.coerceString()) 29 | .join(separator) 30 | ); 31 | } 32 | 33 | return new StringData(""); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /expressions/src/data/replacer.ts: -------------------------------------------------------------------------------- 1 | import {Array} from "./array.js"; 2 | import {BooleanData} from "./boolean.js"; 3 | import {Dictionary} from "./dictionary.js"; 4 | import {Null} from "./null.js"; 5 | import {NumberData} from "./number.js"; 6 | import {StringData} from "./string.js"; 7 | 8 | /** 9 | * Replacer can be passed to JSON.stringify to convert an ExpressionData object into plain JSON 10 | * 11 | * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#replacer 12 | */ 13 | export function replacer(_key: string, value: unknown): unknown { 14 | if (value instanceof Null) { 15 | return null; 16 | } 17 | 18 | if (value instanceof BooleanData) { 19 | return value.value; 20 | } 21 | 22 | if (value instanceof NumberData) { 23 | return value.number(); 24 | } 25 | 26 | if (value instanceof StringData) { 27 | return value.coerceString(); 28 | } 29 | 30 | if (value instanceof Array) { 31 | return value.values(); 32 | } 33 | 34 | if (value instanceof Dictionary) { 35 | const pairs = value.pairs(); 36 | 37 | const r: Record = {}; 38 | for (const p of pairs) { 39 | r[p.key] = p.value; 40 | } 41 | 42 | return r; 43 | } 44 | 45 | return value; 46 | } 47 | -------------------------------------------------------------------------------- /languageservice/src/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import {complete} from "./complete.js"; 2 | import {hover} from "./hover.js"; 3 | import {registerLogger} from "./log.js"; 4 | import {getPositionFromCursor} from "./test-utils/cursor-position.js"; 5 | import {TestLogger} from "./test-utils/logger.js"; 6 | import {clearCache} from "./utils/workflow-cache.js"; 7 | 8 | registerLogger(new TestLogger()); 9 | 10 | beforeEach(() => { 11 | clearCache(); 12 | }); 13 | 14 | describe("end-to-end", () => { 15 | it("empty workflow completion after hover", async () => { 16 | const input = "|"; 17 | 18 | // Issue hover first to fill the cache 19 | await hover(...getPositionFromCursor(input)); 20 | 21 | const result = await complete(...getPositionFromCursor(input)); 22 | 23 | expect(result).not.toBeUndefined(); 24 | expect(result.length).toEqual(13); 25 | const labels = result.map(x => x.label); 26 | expect(labels).toEqual([ 27 | "concurrency", 28 | "concurrency (full syntax)", 29 | "defaults", 30 | "description", 31 | "env", 32 | "jobs", 33 | "name", 34 | "on", 35 | "on (list)", 36 | "on (full syntax)", 37 | "permissions", 38 | "permissions (full syntax)", 39 | "run-name" 40 | ]); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /languageserver/src/on-completion.ts: -------------------------------------------------------------------------------- 1 | import {complete} from "@actions/languageservice/complete"; 2 | import {Octokit} from "@octokit/rest"; 3 | import {CompletionItem, Connection, Position} from "vscode-languageserver"; 4 | import {TextDocument} from "vscode-languageserver-textdocument"; 5 | import {contextProviders} from "./context-providers"; 6 | import {getFileProvider} from "./file-provider"; 7 | import {RepositoryContext} from "./initializationOptions"; 8 | import {Requests} from "./request"; 9 | import {TTLCache} from "./utils/cache"; 10 | import {valueProviders} from "./value-providers"; 11 | 12 | export async function onCompletion( 13 | connection: Connection, 14 | position: Position, 15 | document: TextDocument, 16 | client: Octokit | undefined, 17 | repoContext: RepositoryContext | undefined, 18 | cache: TTLCache 19 | ): Promise { 20 | return await complete(document, position, { 21 | valueProviderConfig: repoContext && valueProviders(client, repoContext, cache), 22 | contextProviderConfig: repoContext && contextProviders(client, repoContext, cache), 23 | fileProvider: getFileProvider(client, cache, repoContext?.workspaceUri, async path => { 24 | return await connection.sendRequest(Requests.ReadFile, {path}); 25 | }) 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/errors-reusable-workflow-permissions-not-allowed-request-id-token-write.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | skip: 3 | - Go 4 | permissions-policy: LimitedRead 5 | --- 6 | on: push 7 | jobs: 8 | deploy: 9 | permissions: 10 | actions: write 11 | checks: write 12 | contents: write 13 | deployments: write 14 | discussions: write 15 | issues: write 16 | packages: write 17 | pages: write 18 | pull-requests: write 19 | repository-projects: write 20 | security-events: write 21 | statuses: write 22 | id-token: none 23 | uses: a/b/.github/workflows/c.yml@v1 24 | --- 25 | a/b/.github/workflows/c.yml@v1 26 | --- 27 | on: workflow_call 28 | jobs: 29 | deploy: 30 | name: Deploy 1 31 | permissions: 32 | id-token: write 33 | runs-on: ubuntu-latest 34 | steps: 35 | - run: echo hi 36 | --- 37 | { 38 | "errors": [ 39 | { 40 | "Message": ".github/workflows/errors-reusable-workflow-permissions-not-allowed-request-id-token-write.yml (Line: 3, Col: 3): Error calling workflow 'a/b/.github/workflows/c.yml@v1'. The nested job 'Deploy 1' is requesting 'id-token: write', but is only allowed 'id-token: none'." 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/job-container-invalid.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build1: 6 | runs-on: linux 7 | container: 8 | image: node:14.16 9 | credentials: 10 | badkey: somevalue 11 | steps: 12 | - run: echo hi 13 | build2: 14 | runs-on: linux 15 | container: 16 | image: 17 | steps: 18 | - run: echo hi 19 | build3: 20 | runs-on: linux 21 | services: 22 | servicename: 23 | badkey: somevalue 24 | steps: 25 | - run: echo hi 26 | build4: 27 | runs-on: linux 28 | services: 29 | servicename: 30 | image: 31 | steps: 32 | - run: echo hi 33 | --- 34 | { 35 | "errors": [ 36 | { 37 | "Message": ".github/workflows/job-container-invalid.yml (Line: 8, Col: 9): Unexpected value 'badkey'" 38 | }, 39 | { 40 | "Message": ".github/workflows/job-container-invalid.yml (Line: 14, Col: 13): Unexpected value ''" 41 | }, 42 | { 43 | "Message": ".github/workflows/job-container-invalid.yml (Line: 21, Col: 9): Unexpected value 'badkey'" 44 | }, 45 | { 46 | "Message": ".github/workflows/job-container-invalid.yml (Line: 28, Col: 15): Unexpected value ''" 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/job-basic.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | job1: 6 | runs-on: windows-2019 7 | steps: 8 | - run: echo 1 9 | job2: 10 | runs-on: windows-2019 11 | cancel-timeout-minutes: 5 12 | steps: 13 | - run: echo 2 14 | --- 15 | { 16 | "jobs": [ 17 | { 18 | "type": "job", 19 | "id": "job1", 20 | "name": "job1", 21 | "if": { 22 | "type": 3, 23 | "expr": "success()" 24 | }, 25 | "runs-on": "windows-2019", 26 | "steps": [ 27 | { 28 | "id": "__run", 29 | "if": { 30 | "type": 3, 31 | "expr": "success()" 32 | }, 33 | "run": "echo 1" 34 | } 35 | ] 36 | }, 37 | { 38 | "type": "job", 39 | "id": "job2", 40 | "name": "job2", 41 | "if": { 42 | "type": 3, 43 | "expr": "success()" 44 | }, 45 | "cancel-timeout-minutes": 5, 46 | "runs-on": "windows-2019", 47 | "steps": [ 48 | { 49 | "id": "__run", 50 | "if": { 51 | "type": 3, 52 | "expr": "success()" 53 | }, 54 | "run": "echo 2" 55 | } 56 | ] 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /languageservice/src/context-providers/env.ts: -------------------------------------------------------------------------------- 1 | import {data, DescriptionDictionary} from "@actions/expressions"; 2 | import {isScalar, isString} from "@actions/workflow-parser"; 3 | import {MappingToken} from "@actions/workflow-parser/templates/tokens/mapping-token"; 4 | import {WorkflowContext} from "../context/workflow-context.js"; 5 | 6 | export function getEnvContext(workflowContext: WorkflowContext): DescriptionDictionary { 7 | const d = new DescriptionDictionary(); 8 | 9 | //step env 10 | if (workflowContext.step?.env) { 11 | envContext(workflowContext.step.env, d); 12 | } 13 | 14 | //job env 15 | if (workflowContext.job && workflowContext.job.env) { 16 | envContext(workflowContext.job.env, d); 17 | } 18 | 19 | //workflow env 20 | if (workflowContext.template && workflowContext.template.env) { 21 | const wfEnv = workflowContext.template.env.assertMapping("workflow env"); 22 | envContext(wfEnv, d); 23 | } 24 | 25 | return d; 26 | } 27 | 28 | function envContext(envMap: MappingToken, d: data.Dictionary) { 29 | for (const env of envMap) { 30 | if (!isString(env.key)) { 31 | continue; 32 | } 33 | 34 | const value = isScalar(env.value) ? new data.StringData(env.value.toDisplayString()) : new data.Null(); 35 | d.add(env.key.value, value); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /languageservice/src/expression-validation/error-dictionary.ts: -------------------------------------------------------------------------------- 1 | import {data, isDescriptionDictionary} from "@actions/expressions"; 2 | import {isDictionary} from "@actions/expressions/data/dictionary"; 3 | import {ExpressionData, Pair} from "@actions/expressions/data/expressiondata"; 4 | 5 | export class AccessError extends Error { 6 | constructor(message: string, public readonly keyName: string) { 7 | super(message); 8 | } 9 | } 10 | 11 | export class ErrorDictionary extends data.Dictionary { 12 | constructor(...pairs: Pair[]) { 13 | super(...pairs); 14 | } 15 | public complete = true; 16 | 17 | get(key: string): ExpressionData | undefined { 18 | const value = super.get(key); 19 | if (value) { 20 | return value; 21 | } 22 | 23 | if (this.complete) { 24 | throw new AccessError(`Invalid context access: ${key}`, key); 25 | } 26 | } 27 | } 28 | 29 | export function wrapDictionary(d: data.Dictionary): ErrorDictionary { 30 | const e = new ErrorDictionary(); 31 | if (isDescriptionDictionary(d)) { 32 | e.complete = d.complete; 33 | } 34 | 35 | for (const {key, value} of d.pairs()) { 36 | if (isDictionary(value)) { 37 | e.add(key, wrapDictionary(value)); 38 | } else { 39 | e.add(key, value); 40 | } 41 | } 42 | 43 | return e; 44 | } 45 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/allowed-context.ts: -------------------------------------------------------------------------------- 1 | import {FunctionInfo} from "@actions/expressions/funcs/info"; 2 | import {MAX_CONSTANT} from "./template-constants.js"; 3 | 4 | export function splitAllowedContext(allowedContext: string[]): { 5 | namedContexts: string[]; 6 | functions: FunctionInfo[]; 7 | } { 8 | const FUNCTION_REGEXP = /^([a-zA-Z0-9_]+)\(([0-9]+),([0-9]+|MAX)\)$/; 9 | 10 | const namedContexts: string[] = []; 11 | const functions: FunctionInfo[] = []; 12 | if (allowedContext.length > 0) { 13 | for (const contextItem of allowedContext) { 14 | const match = contextItem.match(FUNCTION_REGEXP); 15 | if (match) { 16 | const functionName = match[1]; 17 | const minParameters = Number.parseInt(match[2]); 18 | const maxParametersRaw = match[3]; 19 | const maxParameters = 20 | maxParametersRaw === MAX_CONSTANT ? Number.MAX_SAFE_INTEGER : Number.parseInt(maxParametersRaw); 21 | functions.push({ 22 | name: functionName, 23 | minArgs: minParameters, 24 | maxArgs: maxParameters 25 | }); 26 | } else { 27 | namedContexts.push(contextItem); 28 | } 29 | } 30 | } 31 | 32 | return { 33 | namedContexts: namedContexts, 34 | functions: functions 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/step-continue-on-error.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: exit 2 9 | continue-on-error: true 10 | - run: exit 1 11 | continue-on-error: false 12 | - run: exit 1 13 | --- 14 | { 15 | "jobs": [ 16 | { 17 | "type": "job", 18 | "id": "build", 19 | "name": "build", 20 | "if": { 21 | "type": 3, 22 | "expr": "success()" 23 | }, 24 | "runs-on": "ubuntu-latest", 25 | "steps": [ 26 | { 27 | "id": "__run", 28 | "if": { 29 | "type": 3, 30 | "expr": "success()" 31 | }, 32 | "continue-on-error": true, 33 | "run": "exit 2" 34 | }, 35 | { 36 | "id": "__run_2", 37 | "if": { 38 | "type": 3, 39 | "expr": "success()" 40 | }, 41 | "continue-on-error": false, 42 | "run": "exit 1" 43 | }, 44 | { 45 | "id": "__run_3", 46 | "if": { 47 | "type": 3, 48 | "expr": "success()" 49 | }, 50 | "run": "exit 1" 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /browser-playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-playground", 3 | "version": "0.2.0", 4 | "description": "", 5 | "private": true, 6 | "main": "index.js", 7 | "type": "module", 8 | "dependencies": { 9 | "@actions/languageserver": "^0.2.0", 10 | "monaco-editor-webpack-plugin": "^7.0.1", 11 | "monaco-editor-workers": "^0.34.2", 12 | "monaco-languageclient": "^4.0.3", 13 | "path-browserify": "^1.0.1", 14 | "vscode-languageserver": "^8.0.2", 15 | "vscode-languageserver-protocol": "^3.17.2" 16 | }, 17 | "scripts": { 18 | "build": "webpack --mode production", 19 | "clean": "rimraf dist", 20 | "format": "prettier --write '**/*.ts'", 21 | "format-check": "prettier --check '**/*.ts'", 22 | "lint": "eslint 'src/**/*.ts'", 23 | "lint-fix": "eslint --fix 'src/**/*.ts'", 24 | "start": "webpack-dev-server --mode development --open", 25 | "test": "exit 0", 26 | "watch": "webpack --watch --mode development --env esbuild" 27 | }, 28 | "license": "MIT", 29 | "devDependencies": { 30 | "css-loader": "^6.7.2", 31 | "rimraf": "^3.0.2", 32 | "style-loader": "^3.3.1", 33 | "ts-loader": "^9.4.2", 34 | "typescript": "^4.9.4", 35 | "webpack": "^5.75.0", 36 | "webpack-cli": "^5.0.1", 37 | "webpack-dev-server": ">=5.2.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/reusable-workflow-no-inputs.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | uses: contoso/templates/.github/workflows/build.yml@v1 7 | --- 8 | contoso/templates/.github/workflows/build.yml@v1 9 | --- 10 | on: 11 | workflow_call: 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - run: echo 1 17 | --- 18 | { 19 | "jobs": [ 20 | { 21 | "type": "reusableWorkflowJob", 22 | "id": "build", 23 | "name": "build", 24 | "needs": [], 25 | "if": { 26 | "type": 3, 27 | "expr": "success()" 28 | }, 29 | "ref": "contoso/templates/.github/workflows/build.yml@v1", 30 | "jobs": [ 31 | { 32 | "type": "job", 33 | "id": "deploy", 34 | "name": "deploy", 35 | "if": { 36 | "type": 3, 37 | "expr": "success()" 38 | }, 39 | "runs-on": "ubuntu-latest", 40 | "steps": [ 41 | { 42 | "id": "__run", 43 | "if": { 44 | "type": 3, 45 | "expr": "success()" 46 | }, 47 | "run": "echo 1" 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /workflow-parser/src/templates/tokens/expression-token.ts: -------------------------------------------------------------------------------- 1 | import {Lexer, Parser} from "@actions/expressions"; 2 | import {splitAllowedContext} from "../allowed-context.js"; 3 | import {DefinitionInfo} from "../schema/definition-info.js"; 4 | import {ScalarToken} from "./scalar-token.js"; 5 | import {TokenRange} from "./token-range.js"; 6 | 7 | export abstract class ExpressionToken extends ScalarToken { 8 | public readonly directive: string | undefined; 9 | 10 | public constructor( 11 | type: number, 12 | file: number | undefined, 13 | range: TokenRange | undefined, 14 | directive: string | undefined, 15 | definitionInfo: DefinitionInfo | undefined 16 | ) { 17 | super(type, file, range, definitionInfo); 18 | this.directive = directive; 19 | } 20 | 21 | public override get isLiteral(): boolean { 22 | return false; 23 | } 24 | 25 | public override get isExpression(): boolean { 26 | return true; 27 | } 28 | 29 | public static validateExpression(expression: string, allowedContext: string[]): void { 30 | const {namedContexts, functions} = splitAllowedContext(allowedContext); 31 | 32 | // Parse 33 | const lexer = new Lexer(expression); 34 | const result = lexer.lex(); 35 | const p = new Parser(result.tokens, namedContexts, functions); 36 | p.parse(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /workflow-parser/src/model/converter/job/environment.ts: -------------------------------------------------------------------------------- 1 | import {TemplateContext} from "../../../templates/template-context.js"; 2 | import {TemplateToken} from "../../../templates/tokens/template-token.js"; 3 | import {isScalar} from "../../../templates/tokens/type-guards.js"; 4 | import {ActionsEnvironmentReference} from "../../workflow-template.js"; 5 | 6 | export function convertToActionsEnvironmentRef( 7 | context: TemplateContext, 8 | token: TemplateToken 9 | ): ActionsEnvironmentReference { 10 | const result: ActionsEnvironmentReference = {}; 11 | 12 | if (token.isExpression) { 13 | return result; 14 | } 15 | 16 | if (isScalar(token)) { 17 | result.name = token; 18 | return result; 19 | } 20 | 21 | const environmentMapping = token.assertMapping("job environment"); 22 | 23 | for (const property of environmentMapping) { 24 | const propertyName = property.key.assertString("job environment key"); 25 | if (property.key.isExpression || property.value.isExpression) { 26 | continue; 27 | } 28 | 29 | switch (propertyName.value) { 30 | case "name": 31 | result.name = property.value.assertScalar("job environment name key"); 32 | break; 33 | 34 | case "url": 35 | result.url = property.value; 36 | break; 37 | } 38 | } 39 | 40 | return result; 41 | } 42 | -------------------------------------------------------------------------------- /languageservice/src/value-providers/default.ts: -------------------------------------------------------------------------------- 1 | import {ValueProviderConfig, ValueProviderKind} from "./config.js"; 2 | import {needs} from "./needs.js"; 3 | import {reusableJobInputs} from "./reusable-job-inputs.js"; 4 | import {reusableJobSecrets} from "./reusable-job-secrets.js"; 5 | import {stringsToValues} from "./strings-to-values.js"; 6 | 7 | export const DEFAULT_RUNNER_LABELS = [ 8 | "ubuntu-latest", 9 | "ubuntu-24.04", 10 | "ubuntu-22.04", 11 | "ubuntu-20.04", 12 | "ubuntu-slim", 13 | "windows-latest", 14 | "windows-2022", 15 | "windows-2019", 16 | "macos-latest", 17 | "macos-15", 18 | "macos-14", 19 | "self-hosted" 20 | ]; 21 | 22 | export const defaultValueProviders: ValueProviderConfig = { 23 | needs: { 24 | kind: ValueProviderKind.AllowedValues, 25 | get: context => Promise.resolve(needs(context)) 26 | }, 27 | "workflow-job-with": { 28 | kind: ValueProviderKind.AllowedValues, 29 | get: context => Promise.resolve(reusableJobInputs(context)) 30 | }, 31 | "workflow-job-secrets": { 32 | kind: ValueProviderKind.SuggestedValues, 33 | get: (context, existingValues) => Promise.resolve(reusableJobSecrets(context, existingValues)) 34 | }, 35 | "runs-on": { 36 | kind: ValueProviderKind.SuggestedValues, 37 | get: () => Promise.resolve(stringsToValues(DEFAULT_RUNNER_LABELS)) 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/job-snapshot-mapping.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo hi 9 | snapshot: 10 | image-name: custom-image 11 | version: 1.* 12 | if: ${{ github.event_name == 'something' }} 13 | 14 | --- 15 | { 16 | "jobs": [ 17 | { 18 | "type": "job", 19 | "id": "build", 20 | "name": "build", 21 | "if": { 22 | "type": 3, 23 | "expr": "success()" 24 | }, 25 | "runs-on": "ubuntu-latest", 26 | "steps": [ 27 | { 28 | "id": "__run", 29 | "if": { 30 | "type": 3, 31 | "expr": "success()" 32 | }, 33 | "run": "echo hi" 34 | } 35 | ], 36 | "snapshot": { 37 | "type": 2, 38 | "map": [ 39 | { 40 | "Key": "image-name", 41 | "Value": "custom-image" 42 | }, 43 | { 44 | "Key": "version", 45 | "Value": "1.*" 46 | }, 47 | { 48 | "Key": "if", 49 | "Value": { 50 | "type": 3, 51 | "expr": "github.event_name == 'something'" 52 | } 53 | } 54 | ] 55 | } 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /languageserver/src/utils/cache.ts: -------------------------------------------------------------------------------- 1 | // From https://github.com/cschleiden/github-actions-parser/blob/a81dec9b7462dbcff08fbad0792f5ad549d9de7d/src/lib/workflowschema/workflowSchema.ts 2 | interface CacheEntry { 3 | cachedAt: number; 4 | content: T; 5 | } 6 | 7 | export class TTLCache { 8 | private cache = new Map>(); 9 | 10 | constructor(private defaultTTLinMS: number = 10 * 60 * 1000) {} 11 | 12 | /** 13 | * 14 | * @param key Key to cache value under 15 | * @param ttlInMS How long is the content valid. If optional, default value will be used 16 | * @param getter Function to retrieve content if not in cache 17 | */ 18 | async get(key: string, ttlInMS: number | undefined, getter: () => Promise): Promise { 19 | const hasEntry = this.cache.has(key); 20 | const e = hasEntry && this.cache.get(key); 21 | if (hasEntry && e && e.cachedAt > Date.now() - (ttlInMS || this.defaultTTLinMS)) { 22 | return e.content as T; 23 | } 24 | 25 | try { 26 | const content = await getter(); 27 | 28 | this.cache.set(key, { 29 | cachedAt: Date.now(), 30 | content 31 | }); 32 | 33 | return content; 34 | } catch (e) { 35 | this.cache.delete(key); 36 | throw e; 37 | } 38 | } 39 | 40 | clear(): void { 41 | this.cache.clear(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /workflow-parser/testdata/reader/reusable-workflow-job-mvp.yml: -------------------------------------------------------------------------------- 1 | include-source: false # Drop file/line/col from output 2 | --- 3 | on: push 4 | jobs: 5 | call_reusable: 6 | uses: contoso/templates/.github/workflows/deploy.yml@v1 7 | --- 8 | contoso/templates/.github/workflows/deploy.yml@v1 9 | --- 10 | on: 11 | workflow_call: 12 | jobs: 13 | simple_job: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - run: echo Hello 17 | --- 18 | { 19 | "jobs": [ 20 | { 21 | "type": "reusableWorkflowJob", 22 | "id": "call_reusable", 23 | "name": "call_reusable", 24 | "needs": [], 25 | "if": { 26 | "type": 3, 27 | "expr": "success()" 28 | }, 29 | "ref": "contoso/templates/.github/workflows/deploy.yml@v1", 30 | "jobs": [ 31 | { 32 | "type": "job", 33 | "id": "simple_job", 34 | "name": "simple_job", 35 | "if": { 36 | "type": 3, 37 | "expr": "success()" 38 | }, 39 | "runs-on": "ubuntu-latest", 40 | "steps": [ 41 | { 42 | "id": "__run", 43 | "if": { 44 | "type": 3, 45 | "expr": "success()" 46 | }, 47 | "run": "echo Hello" 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /expressions/src/data/reviver.ts: -------------------------------------------------------------------------------- 1 | import {Array as dArray} from "./array.js"; 2 | import {BooleanData} from "./boolean.js"; 3 | import {Dictionary} from "./dictionary.js"; 4 | import {ExpressionData} from "./expressiondata.js"; 5 | import {Null} from "./null.js"; 6 | import {NumberData} from "./number.js"; 7 | import {StringData} from "./string.js"; 8 | 9 | /** 10 | * Reviver can be passed to `JSON.parse` to convert plain JSON into an `ExpressionData` object. 11 | * 12 | * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#reviver 13 | */ 14 | export function reviver(_key: string, val: unknown): ExpressionData { 15 | if (val === null) { 16 | return new Null(); 17 | } 18 | 19 | if (typeof val === "string") { 20 | return new StringData(val); 21 | } 22 | 23 | if (typeof val === "number") { 24 | return new NumberData(val); 25 | } 26 | 27 | if (typeof val === "boolean") { 28 | return new BooleanData(val); 29 | } 30 | 31 | if (Array.isArray(val)) { 32 | return new dArray(...(val as ExpressionData[])); 33 | } 34 | 35 | if (typeof val === "object") { 36 | return new Dictionary( 37 | ...Object.keys(val).map(k => ({ 38 | key: k, 39 | value: val[k as keyof typeof val] 40 | })) 41 | ); 42 | } 43 | 44 | // Pass through value 45 | return val as ExpressionData; 46 | } 47 | -------------------------------------------------------------------------------- /expressions/src/evaluator.test.ts: -------------------------------------------------------------------------------- 1 | import * as data from "./data/index.js"; 2 | import {ExpressionEvaluationError} from "./errors.js"; 3 | import {Evaluator} from "./evaluator.js"; 4 | import {Lexer} from "./lexer.js"; 5 | import {Parser} from "./parser.js"; 6 | 7 | describe("evaluator", () => { 8 | const lexAndParse = (input: string) => { 9 | const lexer = new Lexer(input); 10 | const result = lexer.lex(); 11 | 12 | // Parse 13 | const parser = new Parser(result.tokens, ["foo"], []); 14 | const expr = parser.parse(); 15 | return expr; 16 | }; 17 | 18 | it("basic evaluation", () => { 19 | const expr = lexAndParse("foo['']"); 20 | 21 | // Evaluate expression 22 | const evaluator = new Evaluator( 23 | expr, 24 | new data.Dictionary({ 25 | key: "foo", 26 | value: new data.Dictionary({key: "bar", value: new data.NumberData(42)}) 27 | }) 28 | ); 29 | const eresult = evaluator.evaluate(); 30 | 31 | expect(eresult.kind).toBe(data.Kind.Null); 32 | }); 33 | 34 | it("handle runtime errors", () => { 35 | const expr = lexAndParse("fromJson('test') == 123"); 36 | const evaluator = new Evaluator(expr, new data.Dictionary()); 37 | 38 | expect(() => evaluator.evaluate()).toThrowError( 39 | new ExpressionEvaluationError("Error parsing JSON when evaluating fromJson") 40 | ); 41 | }); 42 | }); 43 | --------------------------------------------------------------------------------