From 0ec6c793bbed0a2efbb4cc30b1e8230c928f9f9b Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Mon, 5 Jan 2026 14:46:35 +0100 Subject: [PATCH 1/4] Add experimentalFeatures to initialization options Introduce a feature flagging system for opt-in experimental features. Clients can enable features via initializationOptions.experimentalFeatures with granular per-feature control or an 'all' flag to enable everything. First experimental feature: missingInputsQuickfix (for upcoming code actions) --- languageserver/README.md | 31 +++++++++++ languageserver/src/connection.ts | 9 ++++ languageserver/src/features.test.ts | 57 +++++++++++++++++++++ languageserver/src/features.ts | 43 ++++++++++++++++ languageserver/src/initializationOptions.ts | 31 +++++++++++ 5 files changed, 171 insertions(+) create mode 100644 languageserver/src/features.test.ts create mode 100644 languageserver/src/features.ts diff --git a/languageserver/README.md b/languageserver/README.md index 4aaade8d..e8633e38 100644 --- a/languageserver/README.md +++ b/languageserver/README.md @@ -84,6 +84,11 @@ export interface InitializationOptions { * Desired log level */ logLevel?: LogLevel; + + /** + * Experimental features that are opt-in + */ + experimentalFeatures?: ExperimentalFeatures; } ``` @@ -100,6 +105,32 @@ const clientOptions: LanguageClientOptions = { const client = new LanguageClient("actions-language", "GitHub Actions Language Server", serverOptions, clientOptions); ``` +### Experimental Features + +The language server supports opt-in experimental features via the `experimentalFeatures` initialization option. These features may change or be removed in between releases. + +```typescript +initializationOptions: { + experimentalFeatures: { + // Enable all experimental features + all: true, + + // Or enable specific features + missingInputsQuickfix: true, + } +} +``` + +**Available experimental features:** + +| Feature | Description | +|---------|-------------| +| `missingInputsQuickfix` | Code action to add missing required inputs for actions | + +Individual feature flags take precedence over `all`. For example, `{ all: true, missingInputsQuickfix: false }` enables all experimental features except `missingInputsQuickfix`. + +When a feature graduates to stable, its flag becomes a no-op and the feature will be enabled regardless of the configuration value. + ### Standalone CLI After installing globally, you can run the language server directly: diff --git a/languageserver/src/connection.ts b/languageserver/src/connection.ts index 8424980c..bd8f4919 100644 --- a/languageserver/src/connection.ts +++ b/languageserver/src/connection.ts @@ -24,6 +24,7 @@ import {getClient} from "./client.js"; import {Commands} from "./commands.js"; import {contextProviders} from "./context-providers.js"; import {descriptionProvider} from "./description-provider.js"; +import {FeatureFlags} from "./features.js"; import {getFileProvider} from "./file-provider.js"; import {InitializationOptions, RepositoryContext} from "./initializationOptions.js"; import {onCompletion} from "./on-completion.js"; @@ -41,6 +42,7 @@ export function initConnection(connection: Connection) { const cache = new TTLCache(); let hasWorkspaceFolderCapability = false; + let featureFlags = new FeatureFlags(); // Register remote console logger with language service registerLogger(connection.console); @@ -64,6 +66,8 @@ export function initConnection(connection: Connection) { setLogLevel(options.logLevel); } + featureFlags = new FeatureFlags(options.experimentalFeatures); + const result: InitializeResult = { capabilities: { textDocumentSync: TextDocumentSyncKind.Full, @@ -91,6 +95,11 @@ export function initConnection(connection: Connection) { }); connection.onInitialized(() => { + const enabledFeatures = featureFlags.getEnabledFeatures(); + if (enabledFeatures.length > 0) { + connection.console.info(`Experimental features enabled: ${enabledFeatures.join(", ")}`); + } + if (hasWorkspaceFolderCapability) { connection.workspace.onDidChangeWorkspaceFolders(() => { clearCache(); diff --git a/languageserver/src/features.test.ts b/languageserver/src/features.test.ts new file mode 100644 index 00000000..f44fa5ab --- /dev/null +++ b/languageserver/src/features.test.ts @@ -0,0 +1,57 @@ +import {FeatureFlags} from "./features"; + +describe("FeatureFlags", () => { + describe("isEnabled", () => { + it("returns false by default when no options provided", () => { + const flags = new FeatureFlags(); + expect(flags.isEnabled("missingInputsQuickfix")).toBe(false); + }); + + it("returns false by default when empty options provided", () => { + const flags = new FeatureFlags({}); + expect(flags.isEnabled("missingInputsQuickfix")).toBe(false); + }); + + it("returns true when feature is explicitly enabled", () => { + const flags = new FeatureFlags({missingInputsQuickfix: true}); + expect(flags.isEnabled("missingInputsQuickfix")).toBe(true); + }); + + it("returns false when feature is explicitly disabled", () => { + const flags = new FeatureFlags({missingInputsQuickfix: false}); + expect(flags.isEnabled("missingInputsQuickfix")).toBe(false); + }); + + it("returns true when all is enabled", () => { + const flags = new FeatureFlags({all: true}); + expect(flags.isEnabled("missingInputsQuickfix")).toBe(true); + }); + + it("explicit feature flag takes precedence over all:true", () => { + const flags = new FeatureFlags({all: true, missingInputsQuickfix: false}); + expect(flags.isEnabled("missingInputsQuickfix")).toBe(false); + }); + + it("explicit feature flag takes precedence over all:false", () => { + const flags = new FeatureFlags({all: false, missingInputsQuickfix: true}); + expect(flags.isEnabled("missingInputsQuickfix")).toBe(true); + }); + }); + + describe("getEnabledFeatures", () => { + it("returns empty array when no features enabled", () => { + const flags = new FeatureFlags(); + expect(flags.getEnabledFeatures()).toEqual([]); + }); + + it("returns enabled features", () => { + const flags = new FeatureFlags({missingInputsQuickfix: true}); + expect(flags.getEnabledFeatures()).toEqual(["missingInputsQuickfix"]); + }); + + it("returns all features when all is enabled", () => { + const flags = new FeatureFlags({all: true}); + expect(flags.getEnabledFeatures()).toContain("missingInputsQuickfix"); + }); + }); +}); diff --git a/languageserver/src/features.ts b/languageserver/src/features.ts new file mode 100644 index 00000000..a60872ca --- /dev/null +++ b/languageserver/src/features.ts @@ -0,0 +1,43 @@ +import {ExperimentalFeatures} from "./initializationOptions.js"; + +/** + * Keys of ExperimentalFeatures that represent actual features (excludes 'all') + */ +export type ExperimentalFeatureKey = Exclude; + +/** + * All known experimental feature keys. + * This list must be kept in sync with the ExperimentalFeatures interface. + */ +const allFeatureKeys: ExperimentalFeatureKey[] = ["missingInputsQuickfix"]; + +export class FeatureFlags { + private readonly features: ExperimentalFeatures; + + constructor(features?: ExperimentalFeatures) { + this.features = features ?? {}; + } + + /** + * Check if an experimental feature is enabled. + * + * Resolution order: + * 1. Explicit feature flag (if set) + * 2. `all` flag (if set) + * 3. false (default) + */ + isEnabled(feature: ExperimentalFeatureKey): boolean { + const explicit = this.features[feature]; + if (explicit !== undefined) { + return explicit; + } + return this.features.all ?? false; + } + + /** + * Returns list of all enabled experimental features. + */ + getEnabledFeatures(): ExperimentalFeatureKey[] { + return allFeatureKeys.filter(key => this.isEnabled(key)); + } +} diff --git a/languageserver/src/initializationOptions.ts b/languageserver/src/initializationOptions.ts index 59ef4623..b408e9d2 100644 --- a/languageserver/src/initializationOptions.ts +++ b/languageserver/src/initializationOptions.ts @@ -1,6 +1,31 @@ import {LogLevel} from "@actions/languageservice/log"; export {LogLevel} from "@actions/languageservice/log"; +/** + * Experimental feature flags. + * + * Individual feature flags take precedence over `all`. + * Example: { all: true, missingInputsQuickfix: false } enables all + * experimental features EXCEPT missingInputsQuickfix. + * + * When a feature graduates to stable, its flag becomes a no-op + * (the feature will be enabled regardless of the configuration value). + */ +export interface ExperimentalFeatures { + /** + * Enable all experimental features. + * Individual feature flags take precedence over this setting. + * @default false + */ + all?: boolean; + + /** + * Enable quickfix code action for missing required action inputs. + * @default false + */ + missingInputsQuickfix?: boolean; +} + export interface InitializationOptions { /** * GitHub token that will be used to retrieve additional information from github.com @@ -28,6 +53,12 @@ export interface InitializationOptions { * If a GitHub Enterprise Server should be used, the URL of the API endpoint, eg "https://ghe.my-company.com/api/v3" */ gitHubApiUrl?: string; + + /** + * Experimental features that are opt-in. + * Features listed here may change or be removed without notice. + */ + experimentalFeatures?: ExperimentalFeatures; } export interface RepositoryContext { From 7b1a806734a49a3afa266905dd8fffc66da75ce6 Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Mon, 5 Jan 2026 13:55:55 +0000 Subject: [PATCH 2/4] Update languageserver/src/features.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- languageserver/src/features.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/languageserver/src/features.test.ts b/languageserver/src/features.test.ts index f44fa5ab..97243419 100644 --- a/languageserver/src/features.test.ts +++ b/languageserver/src/features.test.ts @@ -1,4 +1,3 @@ -import {FeatureFlags} from "./features"; describe("FeatureFlags", () => { describe("isEnabled", () => { From 6c41e995abc6e9582fdbb4c410c11e77c2b1e45d Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Mon, 5 Jan 2026 16:20:47 +0100 Subject: [PATCH 3/4] Fix PR feedback --- languageserver/src/features.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/languageserver/src/features.test.ts b/languageserver/src/features.test.ts index 97243419..eb690997 100644 --- a/languageserver/src/features.test.ts +++ b/languageserver/src/features.test.ts @@ -1,3 +1,4 @@ +import {FeatureFlags} from "./features.js"; describe("FeatureFlags", () => { describe("isEnabled", () => { @@ -50,7 +51,7 @@ describe("FeatureFlags", () => { it("returns all features when all is enabled", () => { const flags = new FeatureFlags({all: true}); - expect(flags.getEnabledFeatures()).toContain("missingInputsQuickfix"); + expect(flags.getEnabledFeatures()).toEqual(["missingInputsQuickfix"]); }); }); }); From eb6a2d946444f6844a32752cfe8e0e8e4fcc9845 Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Mon, 5 Jan 2026 18:37:18 +0100 Subject: [PATCH 4/4] Move to @actions/expressions --- .../src/features.test.ts | 0 .../src/features.ts | 25 +++++++++++++++++- expressions/src/index.ts | 1 + languageserver/src/connection.ts | 2 +- languageserver/src/initializationOptions.ts | 26 +------------------ 5 files changed, 27 insertions(+), 27 deletions(-) rename {languageserver => expressions}/src/features.test.ts (100%) rename {languageserver => expressions}/src/features.ts (61%) diff --git a/languageserver/src/features.test.ts b/expressions/src/features.test.ts similarity index 100% rename from languageserver/src/features.test.ts rename to expressions/src/features.test.ts diff --git a/languageserver/src/features.ts b/expressions/src/features.ts similarity index 61% rename from languageserver/src/features.ts rename to expressions/src/features.ts index a60872ca..ab1b7fdf 100644 --- a/languageserver/src/features.ts +++ b/expressions/src/features.ts @@ -1,4 +1,27 @@ -import {ExperimentalFeatures} from "./initializationOptions.js"; +/** + * Experimental feature flags. + * + * Individual feature flags take precedence over `all`. + * Example: { all: true, missingInputsQuickfix: false } enables all + * experimental features EXCEPT missingInputsQuickfix. + * + * When a feature graduates to stable, its flag becomes a no-op + * (the feature will be enabled regardless of the configuration value). + */ +export interface ExperimentalFeatures { + /** + * Enable all experimental features. + * Individual feature flags take precedence over this setting. + * @default false + */ + all?: boolean; + + /** + * Enable quickfix code action for missing required action inputs. + * @default false + */ + missingInputsQuickfix?: boolean; +} /** * Keys of ExperimentalFeatures that represent actual features (excludes 'all') diff --git a/expressions/src/index.ts b/expressions/src/index.ts index effcbf48..4b91e1db 100644 --- a/expressions/src/index.ts +++ b/expressions/src/index.ts @@ -4,6 +4,7 @@ export {DescriptionDictionary, DescriptionPair, isDescriptionDictionary} from ". export * as data from "./data/index.js"; export {ExpressionError, ExpressionEvaluationError} from "./errors.js"; export {Evaluator} from "./evaluator.js"; +export {ExperimentalFeatureKey, ExperimentalFeatures, FeatureFlags} from "./features.js"; export {wellKnownFunctions} from "./funcs.js"; export {Lexer, Result} from "./lexer.js"; export {Parser} from "./parser.js"; diff --git a/languageserver/src/connection.ts b/languageserver/src/connection.ts index bd8f4919..d0a7ede3 100644 --- a/languageserver/src/connection.ts +++ b/languageserver/src/connection.ts @@ -24,7 +24,7 @@ import {getClient} from "./client.js"; import {Commands} from "./commands.js"; import {contextProviders} from "./context-providers.js"; import {descriptionProvider} from "./description-provider.js"; -import {FeatureFlags} from "./features.js"; +import {FeatureFlags} from "@actions/expressions"; import {getFileProvider} from "./file-provider.js"; import {InitializationOptions, RepositoryContext} from "./initializationOptions.js"; import {onCompletion} from "./on-completion.js"; diff --git a/languageserver/src/initializationOptions.ts b/languageserver/src/initializationOptions.ts index b408e9d2..d3059714 100644 --- a/languageserver/src/initializationOptions.ts +++ b/languageserver/src/initializationOptions.ts @@ -1,31 +1,7 @@ +import {ExperimentalFeatures} from "@actions/expressions"; import {LogLevel} from "@actions/languageservice/log"; export {LogLevel} from "@actions/languageservice/log"; -/** - * Experimental feature flags. - * - * Individual feature flags take precedence over `all`. - * Example: { all: true, missingInputsQuickfix: false } enables all - * experimental features EXCEPT missingInputsQuickfix. - * - * When a feature graduates to stable, its flag becomes a no-op - * (the feature will be enabled regardless of the configuration value). - */ -export interface ExperimentalFeatures { - /** - * Enable all experimental features. - * Individual feature flags take precedence over this setting. - * @default false - */ - all?: boolean; - - /** - * Enable quickfix code action for missing required action inputs. - * @default false - */ - missingInputsQuickfix?: boolean; -} - export interface InitializationOptions { /** * GitHub token that will be used to retrieve additional information from github.com