Files
languageservices/languageservice/src/document-links.ts
T
eric sciple d2ffb50a92 Add language service support for action.yml files (#275)
- 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
2026-01-02 10:38:52 -06:00

144 lines
4.1 KiB
TypeScript

import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
import {isJob, isReusableWorkflowJob} from "@actions/workflow-parser/model/type-guards";
import {File} from "@actions/workflow-parser/workflows/file";
import {parseFileReference} from "@actions/workflow-parser/workflows/file-reference";
import {TextDocument} from "vscode-languageserver-textdocument";
import {DocumentLink} from "vscode-languageserver-types";
import * as vscodeURI from "vscode-uri";
import {actionUrl, parseActionReference} from "./action.js";
import {isActionDocument} from "./utils/document-type.js";
import {mapRange} from "./utils/range.js";
import {
getOrConvertActionTemplate,
getOrConvertWorkflowTemplate,
getOrParseAction,
getOrParseWorkflow
} from "./utils/workflow-cache.js";
/**
* Generates clickable links for action references and reusable workflows.
*/
export async function documentLinks(document: TextDocument, workspace: string | undefined): Promise<DocumentLink[]> {
const file: File = {
name: document.uri,
content: document.getText()
};
return isActionDocument(document.uri)
? actionDocumentLinks(file, document.uri)
: workflowDocumentLinks(file, document.uri, workspace);
}
/**
* Generates clickable links for action references in action.yml files.
*/
function actionDocumentLinks(file: File, uri: string): DocumentLink[] {
const parsedAction = getOrParseAction(file, uri);
if (!parsedAction?.value) {
return [];
}
const template = getOrConvertActionTemplate(parsedAction.context, parsedAction.value, uri, {
errorPolicy: ErrorPolicy.TryConversion
});
const links: DocumentLink[] = [];
// Only composite actions have steps
if (template?.runs?.using !== "composite") {
return links;
}
const steps = template.runs.steps ?? [];
for (const step of steps) {
if ("uses" in step) {
const actionRef = parseActionReference(step.uses.value);
if (!actionRef) {
continue;
}
const url = actionUrl(actionRef);
links.push({
range: mapRange(step.uses.range),
target: url,
tooltip: `Open action on GitHub`
});
}
}
return links;
}
/**
* Generates clickable links for action references and reusable workflows in workflow files.
*/
async function workflowDocumentLinks(file: File, uri: string, workspace: string | undefined): Promise<DocumentLink[]> {
const parsedWorkflow = getOrParseWorkflow(file, uri);
if (!parsedWorkflow?.value) {
return [];
}
const template = await getOrConvertWorkflowTemplate(parsedWorkflow.context, parsedWorkflow.value, uri, undefined, {
errorPolicy: ErrorPolicy.TryConversion
});
const links: DocumentLink[] = [];
for (const job of template?.jobs || []) {
if (!job) {
continue;
}
if (isJob(job)) {
// Add links to referenced actions
for (const step of job.steps || []) {
if ("uses" in step) {
const actionRef = parseActionReference(step.uses.value);
if (!actionRef) {
continue;
}
const url = actionUrl(actionRef);
links.push({
range: mapRange(step.uses.range),
target: url,
tooltip: `Open action on GitHub`
});
}
}
} else if (isReusableWorkflowJob(job)) {
// Add links to referenced reusable workflows
const ref = parseFileReference(job.ref.value);
if ("repository" in ref) {
// Remote workflow
const url = actionUrl({
owner: ref.owner,
name: ref.repository,
path: ref.path,
ref: ref.version
});
links.push({
range: mapRange(job.ref.range),
target: url,
tooltip: `Open reusable workflow on GitHub`
});
} else if (workspace) {
// We need workspace information to generate this link
// Local workflow, generate workspace link
const workspaceURI = vscodeURI.URI.parse(workspace);
const refURI = vscodeURI.Utils.joinPath(workspaceURI, job.ref.value);
links.push({
range: mapRange(job.ref.range),
target: refURI.toString()
});
}
}
}
return [...links];
}