From 708e10653366421552971c6d675904adccdcc2f4 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Tue, 30 Jun 2026 17:19:20 +0100 Subject: [PATCH] Index `ActionState` over the types of state used --- src/action-common.ts | 41 ++++++++++++++++++++++++++++++--------- src/config-utils.ts | 4 ++-- src/config/file.ts | 2 +- src/config/remote-file.ts | 2 +- src/testing-utils.ts | 37 ++++++++++++++++++++++------------- 5 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/action-common.ts b/src/action-common.ts index 3b2238672..5af58f08b 100644 --- a/src/action-common.ts +++ b/src/action-common.ts @@ -2,13 +2,36 @@ import { Env } from "./environment"; import { FeatureEnablement } from "./feature-flags"; import { Logger } from "./logging"; -export interface ActionState { - /** The logger that is in use. */ - logger: Logger; - - /** Information about environment variables. */ - env: Env; - - /** Information about enabled feature flags. */ - features: FeatureEnablement; +/** Describes different state features that an Action may have. */ +export interface FeatureState { + Logger: { + /** The logger that is in use. */ + logger: Logger; + }; + Env: { + /** Information about environment variables. */ + env: Env; + }; + FeatureFlags: { + /** Information about enabled feature flags. */ + features: FeatureEnablement; + }; } + +/** Identifies a type of state an Action may have. */ +export type StateFeature = keyof FeatureState; + +/** Constructs the union of all state types identifies by `Fs`. */ +export type FieldsOf = Fs extends [ + infer Head extends StateFeature, +] + ? FeatureState[Head] + : Fs extends [ + infer Head extends StateFeature, + ...infer Tail extends StateFeature[], + ] + ? FeatureState[Head] & FieldsOf + : never; + +/** Describes the state of an Action that has access to the state corresponding to `Fs`. */ +export type ActionState = FieldsOf; diff --git a/src/config-utils.ts b/src/config-utils.ts index e246339a8..ec47d0f94 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -602,7 +602,7 @@ async function downloadCacheWithTime( } async function loadUserConfig( - actionState: ActionState, + actionState: ActionState<["Logger", "Env", "FeatureFlags"]>, configFile: string, workspacePath: string, apiDetails: api.GitHubApiCombinedDetails, @@ -1160,7 +1160,7 @@ export async function initConfig( logger.debug("No configuration file was provided"); } else { logger.debug(`Using configuration file: ${inputs.configFile}`); - const actionState: ActionState = { logger, features, env: getEnv() }; + const actionState = { logger, features, env: getEnv() }; userConfig = await loadUserConfig( actionState, inputs.configFile, diff --git a/src/config/file.ts b/src/config/file.ts index 59b667ddd..66257a5bf 100644 --- a/src/config/file.ts +++ b/src/config/file.ts @@ -51,7 +51,7 @@ export function getConfigFileInput( * @returns The `UserConfig`, if it could be fetched and parsed successfully. */ export async function getRemoteConfig( - actionState: ActionState, + actionState: ActionState<["Logger", "Env", "FeatureFlags"]>, configFile: string, apiDetails: api.GitHubApiCombinedDetails, ): Promise { diff --git a/src/config/remote-file.ts b/src/config/remote-file.ts index bbcc96db0..15990ed23 100644 --- a/src/config/remote-file.ts +++ b/src/config/remote-file.ts @@ -80,7 +80,7 @@ function parseOldRemoteFileAddress( * @throws `ConfigurationError` if the format of `configFile` is not valid. */ export async function parseRemoteFileAddress( - actionState: ActionState, + actionState: ActionState<["FeatureFlags", "Env"]>, configFile: string, ): Promise { // Try to parse the input using the old format. If successful, return the diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 1f962d490..80ea55723 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -10,7 +10,7 @@ import test, { import nock from "nock"; import * as sinon from "sinon"; -import { ActionState } from "./action-common"; +import { ActionState, StateFeature } from "./action-common"; import { ActionsEnv, ActionsEnvVars, getActionVersion } from "./actions-util"; import { AnalysisKind } from "./analyses"; import * as apiClient from "./api-client"; @@ -189,18 +189,25 @@ export function getTestActionsEnv(): ActionsEnv { }; } +/** For testing purposes, we make all available state features accessible in `TestEnv`. */ +type AllState = ["Logger", "Env", "FeatureFlags"]; + /** * Wraps a function that accepts an `ActionEnv` for testing in different environments. */ -export class TestEnv { - private readonly fn: (state: ActionState, ...args: Args) => R; +export class TestEnv< + Args extends readonly any[], + R, + Fs extends ReadonlyArray, +> { + private readonly fn: (state: ActionState, ...args: Args) => R; private args?: Args; - private state: ActionState; + private state: ActionState; constructor( - fn: (state: ActionState, ...args: Args) => R, + fn: (state: ActionState, ...args: Args) => R, args?: Args, - initialState?: ActionState, + initialState?: ActionState, ) { this.fn = fn; this.args = args; @@ -211,11 +218,11 @@ export class TestEnv { }; } - private clone(): TestEnv { + private clone(): TestEnv { return new TestEnv(this.fn, this.args, { ...this.state }); } - public getState(): ActionState { + public getState(): ActionState { return this.state; } @@ -229,13 +236,13 @@ export class TestEnv { return result; } - public withFeatures(enabled: Feature[]): TestEnv { + public withFeatures(enabled: Feature[]): TestEnv { const result = this.clone(); result.state.features = createFeatures(enabled); return result; } - public withEnv(env: Env): TestEnv { + public withEnv(env: Env): TestEnv { const result = this.clone(); result.state.env = env; return result; @@ -245,7 +252,7 @@ export class TestEnv { if (!this.args) { throw new Error("Trying to call function in TestEnv without arguments."); } - return this.fn(this.state, ...this.args); + return this.fn(this.state as ActionState, ...this.args); } public passes( @@ -259,9 +266,11 @@ export class TestEnv { } /** Utility function to construct a `TestEnv`. */ -export function callee( - fn: (state: ActionState, ...args: Args) => R, -): TestEnv { +export function callee< + Args extends readonly any[], + R, + Fs extends readonly StateFeature[], +>(fn: (state: ActionState, ...args: Args) => R): TestEnv { return new TestEnv(fn); }