Skip to content

Abstract build steps to externalize the build configuration#6866

Open
alfonso-noriega wants to merge 1 commit intomainfrom
01-build-steps-infrastructure
Open

Abstract build steps to externalize the build configuration#6866
alfonso-noriega wants to merge 1 commit intomainfrom
01-build-steps-infrastructure

Conversation

@alfonso-noriega
Copy link
Contributor

@alfonso-noriega alfonso-noriega commented Feb 19, 2026

Abstract build steps to externalize the build configuration

Summary

This PR introduces a client steps build pipeline — a declarative, composable replacement for the existing mode-based BuildConfig dispatch. Extension specifications now declare an explicit list of typed build steps, each with its own configuration and executor, instead of relying on a hard-coded switch over buildConfig.mode.

The main additions are:

  • ClientStep / ClientSteps types and the executeStep runner
  • A router (executeStepByType) that dispatches to type-specific executors
  • Executors for all existing build modes: bundle_ui, build_theme, bundle_theme, build_function, create_tax_stub, copy_static_assets, include_assets
  • A new include_assets step with a flexible inclusions contract (pattern, static, configKey)
  • clientSteps field added to ExtensionSpecification with full factory support
  • BuildConfig simplified from a discriminated union to a flat interface (transitional — to be removed once all consumers migrate)

Architecture

spec.clientSteps
  └─ ExtensionInstance.build(options)
       └─ executeStep(step, context)       // timing + error handling
            └─ executeStepByType(step, context)  // switch router
                 └─ executeXxxStep(step, context) // type-specific executor

Core types

interface ClientStep {
  readonly id: string                        // unique — key in stepResults map
  readonly name: string                      // human-readable label for logging
  readonly type: StepType                    // discriminant for routing
  readonly config: {[key: string]: unknown}  // step-specific config (Zod-validated)
  readonly continueOnError?: boolean
}

type ClientSteps = ReadonlyArray<{
  readonly lifecycle: 'deploy'
  readonly steps: ReadonlyArray<ClientStep>
}>

interface BuildContext {
  readonly extension: ExtensionInstance
  readonly options: ExtensionBuildOptions
  readonly stepResults: Map<string, StepResult>  // prior step outputs
}

interface StepResult {
  readonly id: string
  readonly success: boolean
  readonly duration: number
  readonly output?: unknown
  readonly error?: Error
}

executeStep wraps execution with timing and error handling. Results are stored in context.stepResults keyed by step.id, making them available to downstream steps.


Step types

bundle_ui

Delegates to the existing buildUIExtension().

{id: 'bundle-ui', name: 'Bundle UI Extension', type: 'bundle_ui', config: {}}

build_theme

Runs theme-check validation via runThemeCheck().

{id: 'build-theme', name: 'Build Theme Extension', type: 'build_theme', config: {}}

bundle_theme

Copies theme files to output preserving relative paths, using themeExtensionFiles() (respects .shopifyignore).

{id: 'bundle-theme', name: 'Bundle Theme Extension', type: 'bundle_theme', config: {}}

build_function

Delegates to buildFunctionExtension() (wasm-opt, trampoline).

{id: 'build-function', name: 'Build Function', type: 'build_function', config: {}}

create_tax_stub

Writes a minimal (()=>{})(); stub to extension.outputPath.

{id: 'create-tax-stub', name: 'Create Tax Stub', type: 'create_tax_stub', config: {}}

copy_static_assets

Calls extension.copyStaticAssets().

{id: 'copy-static-assets', name: 'Copy Static Assets', type: 'copy_static_assets', config: {}}

include_assets

Copies files from various sources into the output directory. All inclusions run in parallel. Returns {filesCopied: number}.

Config schema

{
  inclusions: Array<PatternEntry | StaticEntry | ConfigKeyEntry>
}

type: 'pattern' — glob-based

{
  type: 'pattern',
  baseDir?: string             // source subdirectory within extension root (default: root)
  include?: string[]           // glob patterns (default: ['**/*'])
  ignore?: string[]
  destination?: string         // output subdirectory
  preserveStructure?: boolean  // default: true — maintains relative paths
}

Example — copy only spec files from a subdirectory, preserving structure:

{
  type: 'pattern',
  baseDir: 'specifications',
  destination: 'specifications',
  include: ['**/*.json', '**/*.toml', '**/*.yaml'],
}

type: 'static' — explicit source path

{
  type: 'static',
  source: string               // relative path within extension root
  destination?: string         // exact output path
  preserveStructure?: boolean  // default: false — merges directory into output root
}
destination preserveStructure Result
absent false (default) directory contents merged into output root
absent true directory placed under its own name in output
set directory copied to exact destination path

type: 'configKey' — config-driven source

{
  type: 'configKey',
  configKey: string            // dot-notation key into extension config; resolves string or string[]
  destination?: string
  preserveStructure?: boolean  // default: false
}

Resolves a path (or array of paths) from the extension's TOML config. Supports TOML array-of-tables traversal. Silently skipped if the key is absent or the path does not exist.


Changes to specification.ts

New field on ExtensionSpecification

clientSteps: ClientSteps

All three factory functions now accept and default clientSteps:

createExtensionSpecification({
  ...,
  clientSteps: [...],   // optional — defaults to []
  buildConfig: {...},   // optional — defaults to {mode: 'none'}
})

BuildConfig simplified

Old (discriminated union — filePatterns only valid on copy_files):

type BuildConfig =
  | {mode: 'ui' | 'theme' | 'function' | 'tax_calculation' | 'none'}
  | {mode: 'copy_files'; filePatterns: string[]; ignoredFilePatterns?: string[]}

New (flat interface — transitional, to be removed once clientSteps fully replaces it):

interface BuildConfig {
  mode: BuildMode
  filePatterns?: string[]
  ignoredFilePatterns?: string[]
}

Wiring a spec

const mySpec = createExtensionSpecification({
  identifier: 'my_extension',
  buildConfig: {mode: 'ui'},
  clientSteps: [
    {
      lifecycle: 'deploy',
      steps: [
        {id: 'bundle-ui',           name: 'Bundle UI Extension',  type: 'bundle_ui',           config: {}},
        {id: 'copy-static-assets',  name: 'Copy Static Assets',   type: 'copy_static_assets',  config: {}},
      ],
    },
  ],
})

Copy-files pattern (e.g. channel, flow_template):

clientSteps: [{
  lifecycle: 'deploy',
  steps: [{
    id: 'copy-files', name: 'Copy Files', type: 'include_assets',
    config: {
      inclusions: [{
        type: 'pattern',
        baseDir: 'specifications',
        destination: 'specifications',
        include: ['**/*.json', '**/*.toml', '**/*.yaml', '**/*.yml', '**/*.svg'],
      }],
    },
  }],
}]

Not yet implemented

Declared in ClientStep.type for future use, throw "not yet implemented" if called:

  • esbuild
  • validate
  • transform
  • custom

Files changed

File Change
services/build/client-steps.ts NewClientStep, ClientSteps, BuildContext, StepResult, executeStep
services/build/steps/index.ts NewexecuteStepByType router
services/build/steps/include_assets_step.ts New — pattern/static/configKey inclusions
services/build/steps/bundle-ui-step.ts New — delegates to buildUIExtension
services/build/steps/build-theme-step.ts New — delegates to runThemeCheck
services/build/steps/bundle-theme-step.ts New — copies theme files via themeExtensionFiles
services/build/steps/build-function-step.ts New — delegates to buildFunctionExtension
services/build/steps/create-tax-stub-step.ts New — writes IIFE stub
services/build/steps/copy-static-assets-step.ts New — calls extension.copyStaticAssets()
models/extensions/specification.ts Added clientSteps: ClientSteps; simplified BuildConfig; updated all three factory functions
models/extensions/extension-instance.ts Minor: filePatterns ?? [] fallbacks for optional BuildConfig fields
Test files client-steps.test.ts, client-steps.integration.test.ts, include-assets-step.test.ts

Measuring impact

How do we know this change was effective? Please choose one:

  • n/a - this doesn't need measurement, e.g. a linting rule or a bug-fix
  • Existing analytics will cater for this addition
  • PR includes analytics changes to measure impact

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes

Copy link
Contributor Author

alfonso-noriega commented Feb 19, 2026

@github-actions
Copy link
Contributor

github-actions bot commented Feb 19, 2026

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements 78.76% 14567/18495
🟡 Branches 73.05% 7255/9931
🟡 Functions 78.95% 3705/4693
🟡 Lines 79.11% 13765/17400

Test suite run success

3817 tests passing in 1460 suites.

Report generated by 🧪jest coverage report action from ce4a75e

@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch 2 times, most recently from c4c3353 to d2a2b21 Compare February 19, 2026 13:05
@alfonso-noriega alfonso-noriega force-pushed the proj-48450/asset-pipeline-for-hosted-apps branch from 510502e to f9f6e10 Compare February 19, 2026 13:27
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch 6 times, most recently from 27b8cfc to 34005f5 Compare February 19, 2026 15:31
@alfonso-noriega alfonso-noriega marked this pull request as ready for review February 20, 2026 10:36
@alfonso-noriega alfonso-noriega requested a review from a team as a code owner February 20, 2026 10:36
@github-actions
Copy link
Contributor

We detected some changes at packages/*/src and there are no updates in the .changeset.
If the changes are user-facing, run pnpm changeset add to track your changes and include them in the next release CHANGELOG.

Caution

DO NOT create changesets for features which you do not wish to be included in the public changelog of the next CLI release.

@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from 34005f5 to c2808f7 Compare February 20, 2026 15:00
@alfonso-noriega alfonso-noriega force-pushed the proj-48450/asset-pipeline-for-hosted-apps branch from f9f6e10 to ff3f4e0 Compare February 24, 2026 12:43
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch 2 times, most recently from 445a4c4 to f14e0e0 Compare February 25, 2026 16:39
@alfonso-noriega alfonso-noriega force-pushed the proj-48450/asset-pipeline-for-hosted-apps branch from ff3f4e0 to 635fedb Compare February 25, 2026 16:39
@JoshuaWhite1 JoshuaWhite1 changed the base branch from proj-48450/asset-pipeline-for-hosted-apps to graphite-base/6866 February 26, 2026 23:43
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from f14e0e0 to ef0c29e Compare February 27, 2026 11:33
@alfonso-noriega alfonso-noriega changed the base branch from graphite-base/6866 to proj-48450/asset-pipeline-for-hosted-apps February 27, 2026 11:33
const destPath = joinPath(outputDir, destination)
await mkdir(dirname(destPath))
await copyFile(sourcePath, destPath)
options.stdout.write(`Copied ${source} to ${destination}\n`)

Choose a reason for hiding this comment

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

copy_files destination path can escape outputDir (arbitrary write)

copySourceEntry builds destPath via joinPath(outputDir, destination). If destination is absolute or contains .., the resolved path can escape outputDir, enabling a build config (notably described as serializable/externalizable) to overwrite arbitrary files on the machine.

Evidence:

const destPath = joinPath(outputDir, destination)
await mkdir(dirname(destPath))
await copyFile(sourcePath, destPath)

Impact includes overwriting repo/CI files, compromising developer/CI machines, supply-chain risk, and corrupted artifacts.

resolvedPatterns,
resolvedIgnore,
definition.preserveStructure,
options,

Choose a reason for hiding this comment

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

copy_files pattern destination directory can escape outputDir (arbitrary write)

In pattern mode, definition.destination is joined into outputDir without sanitization:

const destinationDir = definition.destination ? joinPath(outputDir, definition.destination) : outputDir

Absolute paths or .. segments can escape outputDir, after which copyByPattern will create directories and write files there—especially dangerous because it can write many files outside the build output.

const outputFile = joinPath(extension.outputPath, relativePathName)
if (filepath === outputFile) return
await copyFile(filepath, outputFile)
}),

Choose a reason for hiding this comment

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

Theme bundling writes under extension.outputPath and doesn’t ensure parent dirs exist

executeBundleThemeStep computes:

const outputFile = joinPath(extension.outputPath, relativePathName)
await copyFile(filepath, outputFile)

If extension.outputPath is a file path (e.g., .../extension.js), this produces .../extension.js/<relative> and fails. Also, it does not mkdir(dirname(outputFile)), so nested paths fail unless directories already exist.

}
return copySourceEntry(entry.source, entry.destination, baseDir, outputDir, options, preserveStructure)
}),
)

Choose a reason for hiding this comment

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

Unbounded Promise.all copies can exhaust file descriptors on large file sets

Multiple places use Promise.all over potentially large arrays (copyFilesList, copyTomlKeyEntry, copyByPattern). For thousands of files, this can trigger EMFILE: too many open files, high memory usage, and flaky builds.

@binks-code-reviewer
Copy link

binks-code-reviewer bot commented Feb 27, 2026

🤖 Code Review · #projects-dev-ai for questions
React with 👍/👎 or reply — all feedback helps improve the agent.

Complete - No issues

📋 History

✅ 4 findings → ✅ 1 findings → ✅ No issues → ✅ 2 findings → ✅ No issues

@MitchLillie MitchLillie changed the base branch from proj-48450/asset-pipeline-for-hosted-apps to graphite-base/6866 March 3, 2026 18:07
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from ef0c29e to 644b571 Compare March 4, 2026 13:13
@alfonso-noriega alfonso-noriega changed the base branch from graphite-base/6866 to proj-48450/asset-pipeline-for-hosted-apps March 4, 2026 13:13
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from 644b571 to d9664fc Compare March 4, 2026 15:14

await mkdir(dirname(destPath))
await copyFile(filepath, destPath)
}),

Choose a reason for hiding this comment

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

copyByPattern can overwrite files when preserveStructure: false due to basename collisions

When preserveStructure is false, destination paths are set to basename(filepath):

const relPath = preserveStructure ? relativePath(sourceDir, filepath) : basename(filepath)

If multiple matched files share the same basename from different subdirectories (e.g., index.js, schema.json), later copies silently overwrite earlier ones.

@binks-code-reviewer
Copy link

⚠️ Findings outside the diff

These findings are in files not modified by this PR and cannot be posted as inline comments.


packages/app/src/cli/models/extensions/extension-instance.ts:142buildConfig removal breaks ExtensionInstance.outputFileName

ExtensionInstance.outputFileName still dereferences this.specification.buildConfig.mode, but this PR removes buildConfig from ExtensionSpecification and replaces it with clientSteps. This will fail to compile, and if TS is bypassed anywhere it becomes a runtime Cannot read properties of undefined crash when outputFileName is accessed (feeds outputPath logic in constructor and build/deploy flows).

Evidence:

const mode = this.specification.buildConfig.mode

@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch 4 times, most recently from 9964055 to d835caf Compare March 4, 2026 16:22
@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/private/node/conf-store.d.ts
@@ -21,8 +21,6 @@ interface Cache {
 export interface ConfSchema {
     sessionStore: string;
     currentSessionId?: string;
-    devSessionStore?: string;
-    currentDevSessionId?: string;
     cache?: Cache;
 }
 /**
packages/cli-kit/dist/private/node/session/exchange.d.ts
@@ -6,6 +6,20 @@ export declare class InvalidGrantError extends ExtendableError {
 }
 export declare class InvalidRequestError extends ExtendableError {
 }
+export interface ExchangeScopes {
+    admin: string[];
+    partners: string[];
+    storefront: string[];
+    businessPlatform: string[];
+    appManagement: string[];
+}
+/**
+ * Given an identity token, request an application token.
+ * @param identityToken - access token obtained in a previous step
+ * @param store - the store to use, only needed for admin API
+ * @returns An array with the application access tokens.
+ */
+export declare function exchangeAccessForApplicationTokens(identityToken: IdentityToken, scopes: ExchangeScopes, store?: string): Promise<Record<string, ApplicationToken>>;
 /**
  * Given an expired access token, refresh it to get a new one.
  */
packages/cli-kit/dist/private/node/session/schema.d.ts
@@ -12,8 +12,8 @@ declare const IdentityTokenSchema: zod.ZodObject<{
 }, "strip", zod.ZodTypeAny, {
     accessToken: string;
     refreshToken: string;
-    expiresAt: Date;
     scopes: string[];
+    expiresAt: Date;
     userId: string;
     alias?: string | undefined;
 }, {
@@ -34,8 +34,8 @@ declare const ApplicationTokenSchema: zod.ZodObject<{
     storeFqdn: zod.ZodOptional<zod.ZodString>;
 }, "strip", zod.ZodTypeAny, {
     accessToken: string;
-    expiresAt: Date;
     scopes: string[];
+    expiresAt: Date;
     storeFqdn?: string | undefined;
 }, {
     accessToken: string;
@@ -54,8 +54,8 @@ declare const SessionSchema: zod.ZodObject<{
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     }, {
@@ -73,8 +73,8 @@ declare const SessionSchema: zod.ZodObject<{
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -88,8 +88,8 @@ declare const SessionSchema: zod.ZodObject<{
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -103,8 +103,8 @@ declare const SessionSchema: zod.ZodObject<{
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -116,16 +116,16 @@ declare const SessionSchema: zod.ZodObject<{
     identity: {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     };
     applications: {} & {
         [k: string]: {
             accessToken: string;
-            expiresAt: Date;
             scopes: string[];
+            expiresAt: Date;
             storeFqdn?: string | undefined;
         };
     };
@@ -166,8 +166,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     }, {
@@ -185,8 +185,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -200,8 +200,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -215,8 +215,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -228,16 +228,16 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     identity: {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     };
     applications: {} & {
         [k: string]: {
             accessToken: string;
-            expiresAt: Date;
             scopes: string[];
+            expiresAt: Date;
             storeFqdn?: string | undefined;
         };
     };
@@ -269,8 +269,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     }, {
@@ -288,8 +288,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -303,8 +303,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -318,8 +318,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -331,16 +331,16 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     identity: {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     };
     applications: {} & {
         [k: string]: {
             accessToken: string;
-            expiresAt: Date;
             scopes: string[];
+            expiresAt: Date;
             storeFqdn?: string | undefined;
         };
     };
@@ -372,8 +372,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     }, {
@@ -391,8 +391,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -406,8 +406,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -421,8 +421,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -434,16 +434,16 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     identity: {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     };
     applications: {} & {
         [k: string]: {
             accessToken: string;
-            expiresAt: Date;
             scopes: string[];
+            expiresAt: Date;
             storeFqdn?: string | undefined;
         };
     };
@@ -475,8 +475,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     }, {
@@ -494,8 +494,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -509,8 +509,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -524,8 +524,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -537,16 +537,16 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     identity: {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     };
     applications: {} & {
         [k: string]: {
             accessToken: string;
-            expiresAt: Date;
             scopes: string[];
+            expiresAt: Date;
             storeFqdn?: string | undefined;
         };
     };
@@ -578,8 +578,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     }, {
@@ -597,8 +597,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -612,8 +612,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -627,8 +627,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -640,16 +640,16 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     identity: {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     };
     applications: {} & {
         [k: string]: {
             accessToken: string;
-            expiresAt: Date;
             scopes: string[];
+            expiresAt: Date;
             storeFqdn?: string | undefined;
         };
     };
@@ -681,8 +681,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     }, {
@@ -700,8 +700,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -715,8 +715,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -730,8 +730,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -743,16 +743,16 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     identity: {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     };
     applications: {} & {
         [k: string]: {
             accessToken: string;
-            expiresAt: Date;
             scopes: string[];
+            expiresAt: Date;
             storeFqdn?: string | undefined;
         };
     };
@@ -784,8 +784,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     }, {
@@ -803,8 +803,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -818,8 +818,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -833,8 +833,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -846,16 +846,16 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     identity: {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     };
     applications: {} & {
         [k: string]: {
             accessToken: string;
-            expiresAt: Date;
             scopes: string[];
+            expiresAt: Date;
             storeFqdn?: string | undefined;
         };
     };
@@ -887,8 +887,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     }, {
@@ -906,8 +906,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -921,8 +921,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -936,8 +936,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -949,16 +949,16 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     identity: {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     };
     applications: {} & {
         [k: string]: {
             accessToken: string;
-            expiresAt: Date;
             scopes: string[];
+            expiresAt: Date;
             storeFqdn?: string | undefined;
         };
     };
@@ -990,8 +990,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     }, {
@@ -1009,8 +1009,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -1024,8 +1024,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -1039,8 +1039,8 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
         storeFqdn: zod.ZodOptional<zod.ZodString>;
     }, "strip", zod.ZodTypeAny, {
         accessToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         storeFqdn?: string | undefined;
     }, {
         accessToken: string;
@@ -1052,16 +1052,16 @@ export declare const SessionsSchema: zod.ZodObject<{}, "strip", zod.ZodObject<{}
     identity: {
         accessToken: string;
         refreshToken: string;
-        expiresAt: Date;
         scopes: string[];
+        expiresAt: Date;
         userId: string;
         alias?: string | undefined;
     };
     applications: {} & {
         [k: string]: {
             accessToken: string;
-            expiresAt: Date;
             scopes: string[];
+            expiresAt: Date;
             storeFqdn?: string | undefined;
         };
     };
packages/cli-kit/dist/private/node/session/validate.d.ts
@@ -1,11 +1,12 @@
 import { Session } from './schema.js';
+import { OAuthApplications } from '../session.js';
 type ValidationResult = 'needs_refresh' | 'needs_full_auth' | 'ok';
 /**
- * Validate if the current session is valid or we need to refresh/re-authenticate.
- * With PCAT, only the identity token needs validation - no per-application tokens.
+ * Validate if the current session is valid or we need to refresh/re-authenticate
  * @param scopes - requested scopes to validate
- * @param session - current session with identity token
+ * @param applications - requested applications
+ * @param session - current session with identity and application tokens
  * @returns 'ok' if the session is valid, 'needs_full_auth' if we need to re-authenticate, 'needs_refresh' if we need to refresh the session
  */
-export declare function validateSession(scopes: string[], session: Session | undefined): Promise<ValidationResult>;
+export declare function validateSession(scopes: string[], applications: OAuthApplications, session: Session | undefined): Promise<ValidationResult>;
 export {};
\ No newline at end of file
packages/cli-kit/dist/private/node/testing/ui.d.ts
@@ -73,8 +73,7 @@ export declare function sendInputAndWait(renderInstance: ReturnType<typeof rende
 export declare function sendInputAndWaitForContent(renderInstance: ReturnType<typeof render>, content: string, ...inputs: string[]): Promise<void>;
 /** Function that is useful when you want to check the last frame of a component that unmounted.
  *
- * With Ink 6 / React 19, the output is no longer cleared on unmount,
- * so lastFrame() consistently returns the last rendered content.
+ * The reason this function exists is that in CI Ink will clear the last frame on unmount.
  */
 export declare function getLastFrameAfterUnmount(renderInstance: ReturnType<typeof render>): string | undefined;
 type TrackedPromise<T> = Promise<T> & {
packages/cli-kit/dist/private/node/ui/components/SelectInput.d.ts
@@ -1,5 +1,8 @@
 import React from 'react';
 import { DOMElement } from 'ink';
+declare module 'react' {
+    function forwardRef<T, TProps>(render: (props: TProps, ref: React.Ref<T>) => React.ReactElement | null): (props: TProps & React.RefAttributes<T>) => React.ReactElement | null;
+}
 export interface SelectInputProps<T> {
     items: Item<T>[];
     initialItems?: Item<T>[];
@@ -15,8 +18,7 @@ export interface SelectInputProps<T> {
     morePagesMessage?: string;
     availableLines?: number;
     onSubmit?: (item: Item<T>) => void;
-    inputFixedAreaRef?: React.Ref<DOMElement>;
-    ref?: React.Ref<DOMElement>;
+    inputFixedAreaRef?: React.RefObject<DOMElement>;
     groupOrder?: string[];
 }
 export interface Item<T> {
@@ -27,5 +29,4 @@ export interface Item<T> {
     helperText?: string;
     disabled?: boolean;
 }
-declare function SelectInput<T>({ items: rawItems, initialItems, onChange, enableShortcuts, focus, emptyMessage, defaultValue, highlightedTerm, loading, errorMessage, hasMorePages, morePagesMessage, availableLines, onSubmit, inputFixedAreaRef, ref, groupOrder, }: SelectInputProps<T>): React.ReactElement | null;
-export { SelectInput };
\ No newline at end of file
+export declare const SelectInput: <T>(props: SelectInputProps<T> & React.RefAttributes<DOMElement>) => React.ReactElement | null;
\ No newline at end of file

@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from d835caf to ef058d2 Compare March 4, 2026 16:29
@alfonso-noriega alfonso-noriega force-pushed the proj-48450/asset-pipeline-for-hosted-apps branch from ae7ed38 to b255e6d Compare March 4, 2026 16:29
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch 2 times, most recently from 57a0790 to 316c2ab Compare March 4, 2026 16:38
@alfonso-noriega alfonso-noriega changed the base branch from proj-48450/asset-pipeline-for-hosted-apps to graphite-base/6866 March 5, 2026 12:00
@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from 316c2ab to a9d3692 Compare March 5, 2026 12:01
@alfonso-noriega alfonso-noriega changed the base branch from graphite-base/6866 to main March 5, 2026 12:01
await copyFile(sourcePath, destPath)
options.stdout.write(`Copied ${source} to ${destination}\n`)
return 1
}

Choose a reason for hiding this comment

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

copySourceEntry copies directories incorrectly when destination is set

In copySourceEntry, when destination is provided, the code always uses copyFile(sourcePath, destPath), but source can be a directory (per the step semantics and schema). Copying a directory as a file will throw and fail the build for valid configs that specify a directory + destination.

duration: Date.now() - startTime,
error: stepError,
}
}

Choose a reason for hiding this comment

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

executeStep never writes results into context.stepResults (pipeline contract not met)

executeStep returns a StepResult and never stores it into context.stepResults, despite the stated intention that later steps can read outputs from earlier ones. This breaks the centralized pipeline behavior and will cause later steps to silently fail/misbehave if they rely on stepResults.

@alfonso-noriega alfonso-noriega force-pushed the 01-build-steps-infrastructure branch from a9d3692 to ce4a75e Compare March 5, 2026 12:48
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.

1 participant