Skip to content

Conversation

@ericsciple
Copy link
Collaborator

@ericsciple ericsciple commented Dec 30, 2025

Overview

This PR adds full language service support for GitHub Actions action.yml manifest files, bringing the same IntelliSense experience that workflow files already have.

Related issues:

What's New

🎯 Core Features

  • Validation - Schema validation for action.yml files with helpful error messages
  • Auto-completion - IntelliSense for keys, values, and expressions
  • Hover information - Descriptions for all action properties
  • Document links - Clickable links for action references in composite steps

🔧 Key Changes

  1. Document Type Detection - Files named action.yml or action.yaml are now routed to action-specific handlers instead of workflow handlers

  2. Expression Context - Composite action steps get proper expression completion for:

    • inputs.* - Action input parameters
    • steps.* - Prior steps
    • github.*, runner.*, job.*, etc.
  3. Schema Validation - Full validation for:

    • Required fields (name, description, runs)
    • Branding icons and colors
    • Composite step requirements (e.g., shell required for run steps)
    • Node/Docker action configuration

Supported Action Types

Type runs.using Key Properties
Composite composite steps
JavaScript node20, node24 main, pre, post
Docker docker image, entrypoint, args

@ericsciple ericsciple force-pushed the users/ericsciple/25-12-action.yml branch 4 times, most recently from fcdbe45 to 8d3fa55 Compare December 31, 2025 20:07
*/
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 {
Copy link
Collaborator Author

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

@ericsciple ericsciple force-pushed the users/ericsciple/25-12-action.yml branch 11 times, most recently from 0e120ec to c7254f2 Compare December 31, 2025 20:45
"format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'",
"lint": "eslint 'src/**/*.ts'",
"lint": "eslint --max-warnings 0 'src/**/*.ts'",
Copy link
Collaborator Author

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"));
Copy link
Collaborator Author

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")
Copy link
Collaborator Author

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) {
Copy link
Collaborator Author

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());
Copy link
Collaborator Author

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 {
Copy link
Collaborator Author

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";

Copy link
Collaborator Author

@ericsciple ericsciple Dec 31, 2025

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;

Copy link
Collaborator Author

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";
Copy link
Collaborator Author

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

Copy link
Collaborator Author

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";
/**
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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 @@
{
Copy link
Collaborator Author

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 -strict definitions where we diverge and are more strict than the runner

This file is safe to copy back to the runner

@ericsciple ericsciple force-pushed the users/ericsciple/25-12-action.yml branch from d148629 to adfe4a6 Compare December 31, 2025 22:45
content: newDoc.getText()
};

const parsedWorkflow = fetchOrParseWorkflow(file, textDocument.uri, true);
Copy link
Collaborator Author

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);
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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,
Copy link
Collaborator Author

@ericsciple ericsciple Jan 1, 2026

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,
Copy link
Collaborator Author

@ericsciple ericsciple Jan 1, 2026

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,
Copy link
Collaborator Author

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 {
Copy link
Collaborator Author

@ericsciple ericsciple Jan 1, 2026

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(
Copy link
Collaborator Author

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 {
Copy link
Collaborator Author

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 {
Copy link
Collaborator Author

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": {
Copy link
Collaborator Author

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[] {
Copy link
Collaborator Author

@ericsciple ericsciple Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is about making the action clickable

image

/**
* Generates clickable links for action references and reusable workflows in workflow files.
*/
async function workflowDocumentLinks(file: File, uri: string, workspace: string | undefined): Promise<DocumentLink[]> {
Copy link
Collaborator Author

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);
Copy link
Collaborator Author

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

Copy link
Collaborator Author

@ericsciple ericsciple Jan 1, 2026

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) {
Copy link
Collaborator Author

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,
Copy link
Collaborator Author

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)) {
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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

* @returns Array of diagnostics
*/
export async function validate(textDocument: TextDocument, config?: ValidationConfig): Promise<Diagnostic[]> {
return isActionDocument(textDocument.uri) ? validateAction(textDocument, config) : validateWorkflow(textDocument, config);
Copy link
Collaborator Author

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 {
Copy link
Collaborator Author

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
@ericsciple ericsciple force-pushed the users/ericsciple/25-12-action.yml branch from 23760d1 to 9e3a0f5 Compare January 1, 2026 19:56
@ericsciple ericsciple marked this pull request as ready for review January 1, 2026 20:25
@ericsciple ericsciple requested a review from a team as a code owner January 1, 2026 20:25
Copilot AI review requested due to automatic review settings January 1, 2026 20:25
},
"using": {
"description": "The runtime used to execute the action.",
"allowed-values": ["docker", "node12", "node16", "node20", "node24", "composite"]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Copilot AI left a 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.

Comment on lines +69 to +71
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];
Copy link

Copilot AI Jan 1, 2026

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").

Suggested change
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();

Copilot uses AI. Check for mistakes.
Comment on lines +402 to +403
// Default fallback
return {using: "composite", steps: []};
Copy link

Copilot AI Jan 1, 2026

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants