Skip to content

Validate extensions.targeting assets value in include-assets-step#7504

Closed
JoshuaWhite1 wants to merge 1 commit into
mainfrom
05-08-validate_extensions.targeting_assets_value_in_include-assets-step
Closed

Validate extensions.targeting assets value in include-assets-step#7504
JoshuaWhite1 wants to merge 1 commit into
mainfrom
05-08-validate_extensions.targeting_assets_value_in_include-assets-step

Conversation

@JoshuaWhite1
Copy link
Copy Markdown
Contributor

@JoshuaWhite1 JoshuaWhite1 commented May 8, 2026

WHY are these changes introduced?

closes https://github.com/shop/issues-admin-extensibility/issues/2521

When a developer sets an invalid assets value in [[extensions.targeting]] (empty string, ., ./, ../foo, etc.), the CLI currently builds the extension, compresses the bundle, and uploads it to GCS before the server rejects the deployment. When we pivoted to the extensions approach for hosted apps, we lost the contract validations from the admin module. That included: empty static_root (now called assets), length limit of 80 char, root path ./, ., and parent directories like ../.

The app will still currently fail, but with an incorrect error message, and further downstream. i.e. defining assets = ./, it is attempting to upload the entire project directory which contains node modules and fails the file limit validation:

image.png

WHAT is this pull request doing?

Adds a build-time guard in the include_assets step that validates assets values before bundling. Specifically:

  • In include-assets-step.ts: when a configKey inclusion targets extension_points[].assets, walk extension_points[] and validate each assets value against the extension directory.
  • Rejection cases:
    • Empty / whitespace-only → 'assets' for target '<target>' can't be empty.
    • ., ./, ../foo, ../../../foo, or any value that resolves to or outside the extension root → 'assets' for target '<target>' is not a valid path: '<value>'
  • Allowed: interior .. segments that resolve back inside the extension (e.g. public/../public) — validated via resolvePath + isSubpath rather than string-matching, so any chain length of ../ is handled.
  • Tests: 10 new cases in include-assets-step.test.ts covering each rejection shape plus the round-trip allowed case.

The guard fires before copyConfigKeyEntry runs, so a misconfigured value short-circuits the build instead of producing a bundle that the server later rejects.

How to test your changes?

  • configure a hosted app: shopify app init --template https://github.com/Shopify/shopify-app-template-extension-only
  • modify the assets value under extensions.targeting in extensions/app-home/shopify.extension.toml
    • set it to empty
    • set it to .
    • set it to ./
    • set it to ../
  • run shopify app dev + shopify app deploy in your local CLI, pointing to your app template:
    • SHOPIFY_CLI_NEVER_USE_PARTNERS_API=1 SHOPIFY_SERVICE_ENV=local pnpm run shopify app dev --path="<PATH_TO_APP>"
  • assert dev/deploy fails on empty and edge cases
  • modify the assets value in your toml back to valid cases
    • set it to assets
    • set it to ./assets
  • assert that correct cases do not cause errors

Considerations

Considered applying this in two alternative layers:

  • Along side file-size/file count validations in core
    • This formats the messages more consistently but there's a major flaw architecturally that I overlooked. By the time this stage has been reached, the CLI has already uploaded the compressed bundle to GCS. If the assets path is the root of the folder, it's uploading thousands of files
  • Schema level validation in cli/models/extensions/schemas.ts
    • currently: assets: zod.string().optional()
    • proposed:
assets: zod
    .string()
    .trim()
    .min(1, "'assets' can't be empty") // catches all empty cases like "", " ", etc 
    .refine( //logic for handling ./, . etc
    ...
    .optional
  • these validations are more than just structural validations, deemed less appropriate

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • The change is user-facing — I've identified the correct bump type (patch for bug fixes · minor for new features · major for breaking changes) and added a changeset with pnpm changeset add

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions github-actions Bot added the no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users. label May 8, 2026
@JoshuaWhite1 JoshuaWhite1 temporarily deployed to breaking-change-approval May 8, 2026 22:35 — with GitHub Actions Inactive
@JoshuaWhite1 JoshuaWhite1 force-pushed the 05-08-validate_extensions.targeting_assets_value_in_include-assets-step branch from 64c1e3a to 6ef723c Compare May 8, 2026 22:41
@JoshuaWhite1 JoshuaWhite1 temporarily deployed to breaking-change-approval May 8, 2026 22:43 — with GitHub Actions Inactive
@JoshuaWhite1 JoshuaWhite1 force-pushed the 05-08-validate_extensions.targeting_assets_value_in_include-assets-step branch from 6ef723c to 6feec59 Compare May 8, 2026 22:45
@JoshuaWhite1 JoshuaWhite1 marked this pull request as ready for review May 9, 2026 01:57
@JoshuaWhite1 JoshuaWhite1 requested a review from a team as a code owner May 9, 2026 01:57
@JoshuaWhite1 JoshuaWhite1 requested review from MitchLillie, Copilot, elanalynn, melissaluu and vividviolet and removed request for Copilot May 9, 2026 01:57
for (const entry of config.inclusions) {
if (entry.type !== 'configKey') continue

if (entry.key === 'extension_points[].assets') {
Copy link
Copy Markdown
Member

@vividviolet vividviolet May 11, 2026

Choose a reason for hiding this comment

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

We can't hard code checks like this. These steps are generic. Isn't there a better place for this inside copyConfigKeyEntry? If the trimmed value configured is an empty string we should fail early with an error.

@JoshuaWhite1 JoshuaWhite1 force-pushed the 05-08-validate_extensions.targeting_assets_value_in_include-assets-step branch from 6feec59 to 92326e8 Compare May 11, 2026 20:07
Copilot AI review requested due to automatic review settings May 11, 2026 20:07
@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Potential Breaking Changes Detected

This PR contains changes that may break the existing contract.

@shopify/dev_experience — this PR contains breaking changes that require coordination for the next major release. This check will remain failed until a member of the team approves the workflow run.

💬 Head to #help-dev-platform to discuss timing and plan the release.

📦 Major Version Changesets

The following changesets request a major version bump:

Changeset Package
thin-webs-notice.md '@shopify/plugin-did-you-mean': major
thin-webs-notice.md '@shopify/plugin-cloudflare': major
thin-webs-notice.md '@shopify/create-app': major
thin-webs-notice.md '@shopify/cli-kit': major
thin-webs-notice.md '@shopify/store': major
thin-webs-notice.md '@shopify/theme': major
thin-webs-notice.md '@shopify/app': major
thin-webs-notice.md '@shopify/cli': major
thin-webs-notice.md '@shopify/e2e': major

@JoshuaWhite1 JoshuaWhite1 temporarily deployed to breaking-change-approval May 11, 2026 20:09 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds early validation for configKey-resolved include-assets paths (notably extension_points[].assets) to fail builds before copying/bundling when an extension config points to an empty, root, or out-of-root assets path. This prevents expensive/incorrect bundling attempts and surfaces a more direct error to the developer.

Changes:

  • Add an up-front guard in copyConfigKeyEntry to reject empty/whitespace values and paths that resolve to the extension root or outside it.
  • Add a focused test suite covering the new guard behavior (empty, whitespace, root-ish values, parent traversal, and allowed .. round-trip).
  • Minor formatting change in include-assets-step.ts (no behavior change).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
packages/app/src/cli/services/build/steps/include-assets/copy-config-key-entry.ts Adds validation for configKey-derived paths using resolvePath + isSubpath before copying.
packages/app/src/cli/services/build/steps/include-assets/copy-config-key-entry.test.ts Adds tests covering rejection/acceptance cases for the new value guard.
packages/app/src/cli/services/build/steps/include-assets-step.ts Minor whitespace-only change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +68 to +75
const trimmed = sourcePath.trim()
if (trimmed === '') {
throw new Error(`'${fieldName}' can't be empty.`)
}
const resolved = resolvePath(baseDir, trimmed)
if (resolved === baseDir || !isSubpath(baseDir, resolved)) {
throw new Error(`'${fieldName}' is not a valid path: '${sourcePath}'`)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog This PR doesn't include a changeset entry. Is an internal only change not relevant to end users.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants