-
Notifications
You must be signed in to change notification settings - Fork 52
Add language service support for action.yml files #275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
fcdbe45 to
8d3fa55
Compare
| */ | ||
| export function fetchOrParseWorkflow(file: File, uri: string, transformed = false): ParseWorkflowResult { | ||
| const key = workflowKey(uri, transformed); | ||
| export function getOrParseWorkflow(file: File, uri: string, transformed = false): TemplateParseResult { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getOrParse better communicates this is either getting it from the cache or parsing
0e120ec to
c7254f2
Compare
| "format": "prettier --write '**/*.ts'", | ||
| "format-check": "prettier --check '**/*.ts'", | ||
| "lint": "eslint 'src/**/*.ts'", | ||
| "lint": "eslint --max-warnings 0 'src/**/*.ts'", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Treat warnings as errors, so we stop accumulating new warnings (all fixed now)
| if (jobContainer && isMapping(jobContainer)) { | ||
| const containerContext = createContainerContext(jobContainer, false); | ||
| jobContext.add("container", containerContext); | ||
| jobContext.add("container", containerContext, getDescription("job", "container")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added descriptions
| containerContext.add( | ||
| "id", | ||
| new data.StringData(""), | ||
| getDescription("job", isServices ? "services.<service_id>.id" : "container.id") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note, services.<service_id>.id is the literal lookup key for the description
| ); | ||
|
|
||
| // ports are only available for service containers (not job container) | ||
| if (isServices) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
now adds ports only for services
| } | ||
| containerContext.add("ports", ports, getDescription("job", "services.<service_id>.ports")); | ||
| } | ||
| containerContext.add("id", new data.Null()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
id and network are added further above now
| "max-parallel": new data.NumberData(1) | ||
| }; | ||
|
|
||
| export function getStrategyContext(workflowContext: WorkflowContext): DescriptionDictionary { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deleted a bunch of logic. For autocomplete purposes all we need is a static config. The structure is always the same regardless whether it's a matrix job or not.
| import {MappingToken} from "@actions/workflow-parser/templates/tokens/mapping-token"; | ||
| import {SequenceToken} from "@actions/workflow-parser/templates/tokens/sequence-token"; | ||
| import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token"; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file helps provide autocomplete values for things like ${{ steps.| }}
Basically it uses information from the parsed document, like step IDs to show what step ID are available within an expression.
| const tokenResult = findToken(position, parsedTemplate.value); | ||
| const {token, keyToken, parent} = tokenResult; | ||
| const tokenDefinitionInfo = (keyToken || parent || token)?.definitionInfo; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the below 👇 early exit code to avoid duplication (previously loaded document context in two places)
| @@ -0,0 +1,92 @@ | |||
| import {isMapping} from "@actions/workflow-parser"; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually existing code
The file was renamed from validate-action.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(i added new comments throughout the file)
| import {Step} from "@actions/workflow-parser/model/workflow-template"; | ||
| import {ScalarToken} from "@actions/workflow-parser/templates/tokens/scalar-token"; | ||
| import {TemplateToken} from "@actions/workflow-parser/templates/tokens/template-token"; | ||
| /** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is actually new. The old validate-action.ts was renamed to validate-action-reference.ts
| * @param tokenStructure - If provided, filters completions to only those matching | ||
| * the YAML structure the user has already started (e.g., only mapping keys if | ||
| * they've started a mapping) | ||
| * @param schema - The schema to use for definition lookups |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i made this required since we always have it anyway and it avoided lint issues related to undefined/null
| @@ -0,0 +1,556 @@ | |||
| { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is basically a copy from the runner with the following changes:
- Added descriptions
- Added
-strictdefinitions where we diverge and are more strict than the runner
This file is safe to copy back to the runner
d148629 to
adfe4a6
Compare
| content: newDoc.getText() | ||
| }; | ||
|
|
||
| const parsedWorkflow = fetchOrParseWorkflow(file, textDocument.uri, true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetchOrParseWorkflow is now called getOrParseWorkflow. The function first attempts to "get" the parsed workflow from the cache, otherwise parses.
| const allowedContext = token.definitionInfo?.allowedContext || []; | ||
| const context = await getContext(allowedContext, config?.contextProviderConfig, workflowContext, Mode.Completion); | ||
| const schema = isAction ? getActionSchema() : getWorkflowSchema(); | ||
| const {token, keyToken, parent, path} = findToken(newPos, parsedTemplate.value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finds the current YAML token based on the cursor position
|
|
||
| return getExpressionCompletionItems(token, context, newPos); | ||
| } | ||
| // Expression completions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handles expression completions
| const indentString = " ".repeat(indentation.tabSize); | ||
|
|
||
| const values = await getValues(token, keyToken, parent, config?.valueProviderConfig, workflowContext, indentString); | ||
| // YAML key/value completions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handles YAML completions
| indentation: string | ||
| workflowContext: WorkflowContext | undefined, | ||
| indentation: string, | ||
| schema: TemplateSchema |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now we're passing the schema since it could be either workflow schema or action schema
| valueProviderConfig: ValueProviderConfig | undefined, | ||
| workflowContext: WorkflowContext, | ||
| indentation: string | ||
| workflowContext: WorkflowContext | undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note, the workflowContext is needed for YAML completions like needs (i.e. which job IDs are available in the document. The workflowContext basically contains that type of information.
Whereas actionContext isn't passed because we don't have any similar YAML completions in the action.yml file. Note, expression completions (e.g. steps.*) do use information from the document, but this function is related to YAML completions only.
| names: string[], | ||
| config: ContextProviderConfig | undefined, | ||
| workflowContext: WorkflowContext, | ||
| workflowContext: WorkflowContext | undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is unrelated to action.yml files.
This function is only used for workflow YAML files.
The change here is related to a hypothetical case where workflowContext can be undefined (basically the file is not parsable enough). In that case, we want to gracefully degrade.
This is related to fixing a lint warning where previously the caller forced the compiler to assume workflowContext was always defined. That assumption was unsafe.
| */ | ||
| function getDefaultContext( | ||
| name: string, | ||
| workflowContext: WorkflowContext | undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same deal in this function. Gracefully degrading if WorkflowContext is undefined.
| /** | ||
| * Returns the strategy context with default values (fail-fast, job-index, etc.) | ||
| */ | ||
| function getStrategyContext(): DescriptionDictionary { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The values don't matter for autocomplete. Only the keys and descriptions.
The strategy context is always available, even for non-matrix jobs.
| * Get context for expression completion in action.yml files. | ||
| * Actions have a more limited set of contexts available compared to workflows. | ||
| */ | ||
| export function getActionExpressionContext( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used for expression autocomplete within action.yml files.
| /** | ||
| * Get steps context for composite action files based on step IDs | ||
| */ | ||
| function getActionStepsContext(actionContext: ActionContext): DescriptionDictionary { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uses the parsed action.yml for step ID completions (in expressions)
| /** | ||
| * Get inputs context for action files based on defined inputs | ||
| */ | ||
| function getActionInputsContext(actionContext: ActionContext): DescriptionDictionary { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uses the parsed action.yml for inputs completions (in expressoins)
| "description": "The default working directory on the runner for steps, and the default location of your repository when using the [`checkout`](https://github.com/actions/checkout) action." | ||
| } | ||
| }, | ||
| "job": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These were missing
| /** | ||
| * Generates clickable links for action references in action.yml files. | ||
| */ | ||
| function actionDocumentLinks(file: File, uri: string): DocumentLink[] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /** | ||
| * Generates clickable links for action references and reusable workflows in workflow files. | ||
| */ | ||
| async function workflowDocumentLinks(file: File, uri: string, workspace: string | undefined): Promise<DocumentLink[]> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic for workflow YAML files moved here
| const isAction = isActionDocument(document.uri); | ||
|
|
||
| // Parse document | ||
| const parsedTemplate = isAction ? parseAction(file, nullTrace) : getOrParseWorkflow(file, document.uri); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throughout this file, basically we use ternary operators to load action-vs-workflow specific data.
This keeps the overall logic reading as one, rather than duplicating the core logic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also I chose ternary over polymorphism/callbacks for simplicity
| } | ||
|
|
||
| if (!token?.definition) { | ||
| if (!hoverToken?.definition) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Introduced hoverToken variable to use token with a fallback to keyToken, and use it consistently throughout the function. This makes the code cleaner and more maintainable.
| function expressionHover( | ||
| exprPos: ExpressionPos, | ||
| context: DescriptionDictionary, | ||
| expressionContext: DescriptionDictionary, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed for clarity - "context" is overloaded
| // Matches: .github/workflows/*.yml or .github/workflows/*.yaml | ||
| // Also matches: .github/workflows-lab/*.yml or .github/workflows-lab/*.yaml | ||
| // This ensures .github/workflows/action.yml is treated as a workflow, not an action | ||
| if (/\.github\/workflows(-lab)?\/[^/]+\.ya?ml$/i.test(normalizedUri)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.github/workflows/*.yml takes precedent over action.yml
| errorPolicy: ErrorPolicy.TryConversion | ||
| }); | ||
|
|
||
| // Only composite actions have steps to validate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional validations after parsing and converting to domain specific types
languageservice/src/validate.ts
Outdated
| * @returns Array of diagnostics | ||
| */ | ||
| export async function validate(textDocument: TextDocument, config?: ValidationConfig): Promise<Diagnostic[]> { | ||
| return isActionDocument(textDocument.uri) ? validateAction(textDocument, config) : validateWorkflow(textDocument, config); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handles both file types
| @@ -0,0 +1,550 @@ | |||
| import { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file contains the domain specific types and conversion logic
- Add validation, completion, hover, and document links for action.yml files - Implement document type detection to route action.yml to action-specific handlers - Add expression context for composite actions (inputs, steps, github, runner, etc.) - Add schema validation for required fields, branding, and composite step requirements - Support JavaScript (node20/node24), Docker, and composite action types - Validate action references in composite action uses steps - Add JSDoc comments to parser and template functions - Refactor hover to use hoverToken consistently - Fix lint errors and add return type annotations
23760d1 to
9e3a0f5
Compare
| }, | ||
| "using": { | ||
| "description": "The runtime used to execute the action.", | ||
| "allowed-values": ["docker", "node12", "node16", "node20", "node24", "composite"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related issue for future follow-up:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive language service support for GitHub Actions action.yml manifest files, enabling IntelliSense, validation, hover information, and document links for action definitions alongside the existing workflow file support.
Key Changes:
- Introduced action-specific parsing, schema validation, and template conversion
- Extended language service features (completion, hover, validation, document links) to action files
- Implemented document type detection to route action.yml and workflow files to appropriate handlers
- Added action-specific expression contexts (inputs, steps) for composite actions
Reviewed changes
Copilot reviewed 43 out of 45 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
workflow-parser/src/workflows/workflow-parser.ts |
Refactored to use shared TemplateParseResult type |
workflow-parser/src/templates/template-parse-result.ts |
New shared type for parse results (workflow and action) |
workflow-parser/src/actions/action-parser.ts |
New action.yml parser with schema validation |
workflow-parser/src/actions/action-template.ts |
Action template converter with support for composite/node/docker actions |
workflow-parser/src/actions/action-schema.ts |
Schema loader for action.yml validation |
workflow-parser/src/action-v1.0.json |
Comprehensive JSON schema for action manifest validation |
languageservice/src/utils/document-type.ts |
Document type detection based on file path patterns |
languageservice/src/validate.ts |
Routes validation to action or workflow validator based on file type |
languageservice/src/validate-action.ts |
Validates action.yml files including composite step validation |
languageservice/src/validate-action-reference.ts |
Extracted action reference validation (inputs, required fields) |
languageservice/src/context/action-context.ts |
Action-specific context for expression completion |
languageservice/src/context-providers/default.ts |
Split into workflow and action expression context builders |
languageservice/src/context-providers/job.ts |
Enhanced job context with container/services descriptions |
languageservice/src/complete.ts |
Routes completion to action or workflow based on document type |
languageservice/src/hover.ts |
Routes hover to action or workflow based on document type |
languageservice/src/document-links.ts |
Added document links for actions in composite steps |
languageservice/src/inlay-hints.ts |
Skips action files (cron hints only for workflows) |
languageservice/src/utils/workflow-cache.ts |
Added action parsing and conversion caching |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const portParts = item.toString().split(":"); | ||
| // The key is the container port (second part if host:container format) | ||
| const containerPort = portParts.length === 2 ? portParts[1] : portParts[0]; |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The port parsing logic doesn't handle all port formats. Service ports can be defined as "8080:80/tcp" or "8080/udp" with protocol specifiers, but the current implementation only splits on ":" without handling the protocol part. This could result in incorrect container port extraction (e.g., "80/tcp" instead of "80").
| const portParts = item.toString().split(":"); | |
| // The key is the container port (second part if host:container format) | |
| const containerPort = portParts.length === 2 ? portParts[1] : portParts[0]; | |
| const portString = item.toString(); | |
| const portParts = portString.split(":"); | |
| // The key is the container port (second part if host:container format) | |
| let containerPort = portParts.length === 2 ? portParts[1] : portParts[0]; | |
| // Strip optional protocol suffix (e.g., "/tcp", "/udp") and trim whitespace | |
| containerPort = containerPort.split("/")[0].trim(); |
| // Default fallback | ||
| return {using: "composite", steps: []}; |
Copilot
AI
Jan 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function returns a fallback composite action with empty steps when the 'using' field doesn't match expected values or required fields are missing. This could mask validation errors. For example, if 'using' is "node20" but 'main' is missing, it silently returns an empty composite action instead of preserving the node configuration. Consider returning the partially constructed object or adding error reporting to the context.

Overview
This PR adds full language service support for GitHub Actions
action.ymlmanifest files, bringing the same IntelliSense experience that workflow files already have.Related issues:
.githubgithub/vscode-github-actions#75Required property is missing: shellgithub/vscode-github-actions#378What's New
🎯 Core Features
🔧 Key Changes
Document Type Detection - Files named
action.ymloraction.yamlare now routed to action-specific handlers instead of workflow handlersExpression Context - Composite action steps get proper expression completion for:
inputs.*- Action input parameterssteps.*- Prior stepsgithub.*,runner.*,job.*, etc.Schema Validation - Full validation for:
name,description,runs)shellrequired forrunsteps)Supported Action Types
runs.usingcompositestepsnode20,node24main,pre,postdockerimage,entrypoint,args