From e57c0e65ddc1f222809299415886044068668890 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 9 Oct 2026 14:14:48 +0100 Subject: [PATCH] Fix type inference for `ActionState` --- lib/entry-points.js | 14 ++++++-- src/action-common.ts | 71 +++++++++++++++++++++++++++++++++++++- src/init-action.ts | 11 ++++-- src/setup-codeql-action.ts | 11 ++++-- src/start-proxy-action.ts | 10 ++++-- 5 files changed, 107 insertions(+), 10 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 1b31b0648e..9fe963a27a 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -171686,6 +171686,9 @@ async function sendUnhandledErrorStatusReport(actionName, actionStartedAt, error } // src/action-common.ts +function extendActionState(state, extra) { + return { ...state, ...extra }; +} async function runInActions(action) { const startedAt = /* @__PURE__ */ new Date(); const logger2 = getActionsLogger(); @@ -186339,7 +186342,9 @@ async function run3(actionState) { `Failed to parse analysis kinds for 'starting' status report: ${getErrorMessage(err)}` ); } - const actionStateWithFeatures = { ...actionState, features }; + const actionStateWithFeatures = extendActionState(actionState, { + features + }); configFile = await getConfigFileInput( actionStateWithFeatures, repositoryProperties, @@ -187358,7 +187363,9 @@ async function run6(actionState) { logger2 ); const repositoryProperties = repositoryPropertiesResult.orElse({}); - const actionStateWithFeatures = { ...actionState, features }; + const actionStateWithFeatures = extendActionState(actionState, { + features + }); const statusReportBase = await createStatusReportBase( "setup-codeql" /* SetupCodeQL */, "starting", @@ -188153,7 +188160,8 @@ async function run7(action) { all_credentials: credentials, ca }; - const proxyBin = await getProxyBinaryPath({ ...action, features }); + const actionWithFeatures = extendActionState(action, { features }); + const proxyBin = await getProxyBinaryPath(actionWithFeatures); const proxyInfo = await startProxy( proxyBin, proxyConfig, diff --git a/src/action-common.ts b/src/action-common.ts index 17d4344cce..79f1901f22 100644 --- a/src/action-common.ts +++ b/src/action-common.ts @@ -56,6 +56,29 @@ export interface FeatureState { /** Identifies a type of state an Action may have. */ export type StateFeature = keyof FeatureState; +/** + * The `Env` feature implies the availability of the `ReadOnlyEnv` feature. + * + * If `T` is `Env`, this returns `Env | ReadOnlyEnv`. + * Otherwise, it is the identity and returns T. + */ +type ImpliedFeatures = T extends "Env" + ? "Env" | "ReadOnlyEnv" + : T; + +/** + * Given an object type `Obj`, this tries to lookup a corresponding `StateFeature` + * to which the object type belongs in `FeatureState`. Resolves to `never` if there + * is no match. + */ +type FeatureNameFor = { + [K in StateFeature]: [Obj] extends [FeatureState[K]] + ? [FeatureState[K]] extends [Obj] + ? K + : never + : never; +}[StateFeature]; + /** Constructs the intersection of all state types identifies by `Fs`. */ export type FieldsOf = Fs extends [] ? Record @@ -66,8 +89,54 @@ export type FieldsOf = Fs extends [] ? FeatureState[Head] & FieldsOf : never; +/** + * Symbol used for a field in `ActionState` that carries the type array of state features. + * This is a Symbol so that it doesn't clash with any property names we might want to have. + */ +const stateFeatures = Symbol(); + /** Describes the state of an Action that has access to the state corresponding to `Fs`. */ -export type ActionState = FieldsOf; +export type ActionState = FieldsOf & { + /** + * When given a chance, TypeScript will simplify an `ActionState` type as much as possible, + * which results in a concrete object type that doesn't mention `Fs`. + * + * That causes problems for functions which accept `ActionState` values, but need to know the + * feature keys `Fs`. This property here explicitly captures `Fs` in the concrete object type + * that results from simplifying `ActionState`. + * + * This is a function rather than a field, because we want to be able to provide values of type + * `ActionState` to functions expecting `ActionState` where `As` is a subset of `Fs`. + * + * Since function types are contravariant in the types of their parameters, using a function + * type here allows that to happen. + * + * Because the field is optional, we don't have to explicitly provide a value + * for it anywhere while the type is still inferred. + * + * `Fs[number]` returns the union of all features in `Fs`. We wrap it in `ImpliedFeatures` + * so that `Env` is expanded into `Env | ReadOnlyEnv`, allowing functions that expect the + * `ReadOnlyEnv` feature to be provided with an `ActionState` that has the `Env` feature + * without requiring this to be made explicit. + */ + readonly [stateFeatures]?: (ts: ImpliedFeatures) => void; +}; + +/** Extends `state` with an `extra` feature. */ +export function extendActionState< + // In first position, so that it can be explicitly provided if `FeatureNameFor` + // should not work on `extra`. + F extends StateFeature, + Fs extends readonly StateFeature[], + E extends FeatureState[F], +>( + state: ActionState, + extra: E, +): ActionState<[...Fs, FeatureNameFor & F]> { + return { ...state, ...extra } as unknown as ActionState< + [...Fs, FeatureNameFor & F] + >; +} /** The type of an Action's main entry point. This is a function that is provided * with a basic `ActionState` object with features that are always available. diff --git a/src/init-action.ts b/src/init-action.ts index 31414bd80b..ae0cb2114b 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -5,7 +5,12 @@ import * as core from "@actions/core"; import * as io from "@actions/io"; import * as semver from "semver"; -import { Action, ActionState, runInActions } from "./action-common"; +import { + Action, + ActionState, + extendActionState, + runInActions, +} from "./action-common"; import { FileCmdNotFoundError, getActionVersion, @@ -278,7 +283,9 @@ async function run( } // Compute the value of the `config-file` input. - const actionStateWithFeatures = { ...actionState, features }; + const actionStateWithFeatures = extendActionState(actionState, { + features, + }); configFile = await getConfigFileInput( actionStateWithFeatures, repositoryProperties, diff --git a/src/setup-codeql-action.ts b/src/setup-codeql-action.ts index 23a9c65ebd..e80327a81c 100644 --- a/src/setup-codeql-action.ts +++ b/src/setup-codeql-action.ts @@ -1,6 +1,11 @@ import * as core from "@actions/core"; -import { Action, ActionState, runInActions } from "./action-common"; +import { + Action, + ActionState, + extendActionState, + runInActions, +} from "./action-common"; import { getActionVersion, getOptionalInput, @@ -131,7 +136,9 @@ async function run( ); const repositoryProperties = repositoryPropertiesResult.orElse({}); - const actionStateWithFeatures = { ...actionState, features }; + const actionStateWithFeatures = extendActionState(actionState, { + features, + }); const statusReportBase = await createStatusReportBase( ActionName.SetupCodeQL, diff --git a/src/start-proxy-action.ts b/src/start-proxy-action.ts index d965a2dd6b..bc3351da53 100644 --- a/src/start-proxy-action.ts +++ b/src/start-proxy-action.ts @@ -3,7 +3,12 @@ import * as path from "path"; import * as core from "@actions/core"; -import { Action, ActionState, runInActions } from "./action-common"; +import { + Action, + ActionState, + extendActionState, + runInActions, +} from "./action-common"; import * as actionsUtil from "./actions-util"; import { getGitHubVersion } from "./api-client"; import { FeatureEnablement, initFeatures } from "./feature-flags"; @@ -98,7 +103,8 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) { }; // Start the Proxy - const proxyBin = await getProxyBinaryPath({ ...action, features }); + const actionWithFeatures = extendActionState(action, { features }); + const proxyBin = await getProxyBinaryPath(actionWithFeatures); const proxyInfo = await startProxy( proxyBin, proxyConfig,