Repository navigation
Add SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN - #8731
Merged
Merged
Conversation
This was referenced Oct 1, 2026
egaodd
force-pushed
the
eddie/org-token-2-bp-auth
branch
2 times, most recently
from
October 2, 2026 16:56
b0f0ced to
5630002
Compare
egaodd
force-pushed
the
eddie/org-token-3-org-variable
branch
from
October 2, 2026 19:07
8c0aba9 to
f2f3175
Compare
egaodd
marked this pull request as ready for review
October 2, 2026 19:20
egaodd
force-pushed
the
eddie/org-token-2-bp-auth
branch
from
October 7, 2026 14:20
5630002 to
6977478
Compare
getAppAutomationToken() now reads SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN first, then SHOPIFY_APP_AUTOMATION_TOKEN, then SHOPIFY_CLI_PARTNERS_TOKEN. Every place that already reads the automation token picks up organization tokens through it, including the store and organization commands, app commands and analytics. It returns no token when the variables can't be used: the organization variable set together with another one, or the selected variable set to an empty string. Those callers then fall back to ensureAuthenticated, which now reports the problem instead of starting a login. ensureAuthenticated also refuses to log in while the organization variable is set, so commands that only support a user login never fall back to a personal session. Co-authored-by: Eddie Gao <eddie.gao@shopify.com>
store execute, store bulk and store graphiql run on a login saved by `shopify store auth`. While SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN is set they now fail with "This command can't use SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN", the same error commands that need the user's own login already give, so nobody assumes the token gives store or Admin API access. The check is exported from cli-kit as ensureNoOrganizationAutomationToken, and ensureAuthenticated now uses it too. It runs in each command rather than in the shared saved-login loader, because `shopify store auth` reads saved logins through that loader and must keep working as if the variable weren't set. Co-authored-by: Eddie Gao <eddie.gao@shopify.com>
Co-authored-by: Eddie Gao <eddie.gao@shopify.com>
egaodd
force-pushed
the
eddie/org-token-3-org-variable
branch
from
October 7, 2026 14:20
f2f3175 to
03d54a6
Compare
Contributor
Differences in type declarationsWe 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:
New type declarationspackages/cli-kit/dist/private/node/session/automation-token.d.tsinterface AutomationTokenVariablesProblem {
message: string;
tryMessage: string;
}
/**
* Returns the name of the automation token variable the CLI authenticates with: the first one that is set, in
* the order SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN, SHOPIFY_APP_AUTOMATION_TOKEN, SHOPIFY_CLI_PARTNERS_TOKEN.
*
* A variable set to an empty string still counts as set, so `automationTokenVariablesProblem` can report it.
*
* @param env - Environment variables to read.
* @returns The variable name, or undefined when none of them is set.
*/
export declare function automationTokenVariable(env?: NodeJS.ProcessEnv): string | undefined;
/**
* Explains why the automation token variables can't be used, so a misconfigured environment fails instead of
* falling back to the logged-in user.
*
* - SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN can't be set together with SHOPIFY_APP_AUTOMATION_TOKEN or
* SHOPIFY_CLI_PARTNERS_TOKEN.
* - The selected variable can't be empty.
*
* @param env - Environment variables to check.
* @returns The problem to report, or undefined when the variables can be used.
*/
export declare function automationTokenVariablesProblem(env?: NodeJS.ProcessEnv): AutomationTokenVariablesProblem | undefined;
export {};
Existing type declarationspackages/cli-kit/dist/public/node/environment.d.ts@@ -10,10 +10,13 @@
*/
export declare function getEnvironmentVariables(): NodeJS.ProcessEnv;
/**
- * Returns the value of the SHOPIFY_APP_AUTOMATION_TOKEN environment variable,
- * falling back to the deprecated SHOPIFY_CLI_PARTNERS_TOKEN.
+ * Returns the automation token the CLI authenticates with, from the first of these variables that is set:
+ * SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN, SHOPIFY_APP_AUTOMATION_TOKEN, or the deprecated SHOPIFY_CLI_PARTNERS_TOKEN.
*
- * @returns The app automation token value, or undefined if neither env var is set.
+ * Returns undefined when the variables can't be used (an empty value, or the organization variable set alongside
+ * another one). Callers then fall back to the login flow, which reports the problem instead of logging in.
+ *
+ * @returns The automation token, or undefined if there is no usable one.
*/
export declare function getAppAutomationToken(): string | undefined;
/**
packages/cli-kit/dist/public/node/session.d.ts@@ -128,14 +128,35 @@ export declare function ensureAuthenticatedAdmin(store: string, scopes?: AdminAP
* @returns The access token and store.
*/
export declare function ensureAuthenticatedThemes(store: string, password: string | undefined, scopes?: AdminAPIScope[], options?: EnsureAuthenticatedAdditionalOptions): Promise<AdminSession>;
+/**
+ * Options for `ensureAuthenticatedBusinessPlatform`.
+ */
+export interface EnsureAuthenticatedBusinessPlatformOptions extends EnsureAuthenticatedAdditionalOptions {
+ /**
+ * Authenticate with the automation token set in the environment, when there is one, instead of the
+ * logged-in user. Only commands that support automation tokens opt in; other callers, such as
+ * Hydrogen's login, keep using the user's session.
+ */
+ allowAutomationToken?: boolean;
+}
/**
* Ensure that we have a valid session to access the Business Platform API.
*
- * @param scopes - Optional array of extra scopes to authenticate with.
+ * @param scopes - Optional array of extra scopes to authenticate with. Ignored when an automation token is used.
* @param options - Optional extra options to use.
* @returns The access token for the Business Platform API.
*/
-export declare function ensureAuthenticatedBusinessPlatform(scopes?: BusinessPlatformScope[], options?: EnsureAuthenticatedAdditionalOptions): Promise<string>;
+export declare function ensureAuthenticatedBusinessPlatform(scopes?: BusinessPlatformScope[], options?: EnsureAuthenticatedBusinessPlatformOptions): Promise<string>;
+/**
+ * Fails when SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN is set, for commands that can't run with that token.
+ *
+ * Organization automation tokens can't log in as a user or call a store's Admin API, so commands that need either
+ * refuse to run instead of quietly using the person's own login. `ensureAuthenticated` runs this check before any
+ * login. Commands that run on a login saved by `shopify store auth` call it before loading that login.
+ *
+ * @throws AbortError when SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN is set.
+ */
+export declare function ensureNoOrganizationAutomationToken(): void;
/**
* Logout from Shopify.
*
packages/cli-kit/dist/private/node/constants.d.ts@@ -8,6 +8,7 @@ export declare const environmentVariables: {
env: string;
noAnalytics: string;
optOutInstrumentation: string;
+ organizationAutomationToken: string;
appAutomationToken: string;
partnersToken: string;
runAsUser: string;
packages/cli-kit/dist/public/node/error/schema.d.ts@@ -30,24 +30,24 @@ export declare const JsonAbortErrorSchema: zod.ZodObject<{
type: "abort";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}, {
type: "abort";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}>;
export declare const JsonBugErrorSchema: zod.ZodObject<{
stack: zod.ZodOptional<zod.ZodString>;
@@ -71,6 +71,7 @@ export declare const JsonBugErrorSchema: zod.ZodObject<{
type: "bug";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
stack?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
@@ -78,11 +79,11 @@ export declare const JsonBugErrorSchema: zod.ZodObject<{
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}, {
type: "bug";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
stack?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
@@ -90,7 +91,6 @@ export declare const JsonBugErrorSchema: zod.ZodObject<{
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}>;
export declare const JsonExternalErrorSchema: zod.ZodObject<{
command: zod.ZodString;
@@ -117,26 +117,26 @@ export declare const JsonExternalErrorSchema: zod.ZodObject<{
command: string;
args: string[];
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}, {
type: "external";
message: string;
command: string;
args: string[];
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}>;
export declare const JsonErrorSchema: zod.ZodUnion<[zod.ZodObject<{
message: zod.ZodString;
@@ -159,24 +159,24 @@ export declare const JsonErrorSchema: zod.ZodUnion<[zod.ZodObject<{
type: "abort";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}, {
type: "abort";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}>, zod.ZodObject<{
stack: zod.ZodOptional<zod.ZodString>;
message: zod.ZodString;
@@ -199,6 +199,7 @@ export declare const JsonErrorSchema: zod.ZodUnion<[zod.ZodObject<{
type: "bug";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
stack?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
@@ -206,11 +207,11 @@ export declare const JsonErrorSchema: zod.ZodUnion<[zod.ZodObject<{
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}, {
type: "bug";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
stack?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
@@ -218,7 +219,6 @@ export declare const JsonErrorSchema: zod.ZodUnion<[zod.ZodObject<{
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}>, zod.ZodObject<{
command: zod.ZodString;
args: zod.ZodArray<zod.ZodString, "many">;
@@ -244,26 +244,26 @@ export declare const JsonErrorSchema: zod.ZodUnion<[zod.ZodObject<{
command: string;
args: string[];
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}, {
type: "external";
message: string;
command: string;
args: string[];
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}>]>;
export declare const jsonErrorOutputSchema: import("../json-output-schema.js").JsonOutputSchema<zod.ZodObject<{
error: zod.ZodUnion<[zod.ZodObject<{
@@ -287,24 +287,24 @@ export declare const jsonErrorOutputSchema: import("../json-output-schema.js").J
type: "abort";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}, {
type: "abort";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}>, zod.ZodObject<{
stack: zod.ZodOptional<zod.ZodString>;
message: zod.ZodString;
@@ -327,6 +327,7 @@ export declare const jsonErrorOutputSchema: import("../json-output-schema.js").J
type: "bug";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
stack?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
@@ -334,11 +335,11 @@ export declare const jsonErrorOutputSchema: import("../json-output-schema.js").J
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}, {
type: "bug";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
stack?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
@@ -346,7 +347,6 @@ export declare const jsonErrorOutputSchema: import("../json-output-schema.js").J
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}>, zod.ZodObject<{
command: zod.ZodString;
args: zod.ZodArray<zod.ZodString, "many">;
@@ -372,43 +372,44 @@ export declare const jsonErrorOutputSchema: import("../json-output-schema.js").J
command: string;
args: string[];
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}, {
type: "external";
message: string;
command: string;
args: string[];
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
}>]>;
}, "strict", zod.ZodTypeAny, {
error: {
type: "abort";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
} | {
type: "bug";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
stack?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
@@ -416,37 +417,37 @@ export declare const jsonErrorOutputSchema: import("../json-output-schema.js").J
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
} | {
type: "external";
message: string;
command: string;
args: string[];
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
};
}, {
error: {
type: "abort";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
} | {
type: "bug";
message: string;
code?: string | undefined;
+ tryMessage?: string | undefined;
stack?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
@@ -454,19 +455,18 @@ export declare const jsonErrorOutputSchema: import("../json-output-schema.js").J
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
} | {
type: "external";
message: string;
command: string;
args: string[];
code?: string | undefined;
+ tryMessage?: string | undefined;
nextSteps?: string[] | undefined;
customSections?: {
body: string | string[][];
title?: string | undefined;
}[] | undefined;
details?: unknown;
- tryMessage?: string | undefined;
};
}>>;
\ No newline at end of file
packages/cli-kit/dist/private/node/session/exchange.d.ts@@ -45,9 +45,10 @@ export declare function exchangeAppAutomationTokenForAppManagementAccessToken(to
/**
* Given a custom app automation token passed as ENV variable, request a valid Business Platform API token.
* @param token - The app automation token passed as ENV variable `SHOPIFY_APP_AUTOMATION_TOKEN`
+ * @param scopes - The scopes to request. An empty list makes Identity issue every Business Platform scope the token holds.
* @returns An instance with the application access tokens.
*/
-export declare function exchangeAppAutomationTokenForBusinessPlatformAccessToken(token: string): Promise<{
+export declare function exchangeAppAutomationTokenForBusinessPlatformAccessToken(token: string, scopes?: string[]): Promise<{
accessToken: string;
userId: string;
}>;
|
alexanderMontague
approved these changes
Oct 7, 2026
alexanderMontague
left a comment
Contributor
There was a problem hiding this comment.
This looks good. We decided to have a follow up issue to come back and update the env var to whatever it should be named. If this posts the stale inline comments, disregard as they are already addressed.
Author
Yes, the follow up issue Alex is talking about is here. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of shop/issues-develop#24004
TL;DR: Adds
SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN. Every command that already accepts an automation token picks it up. An empty or conflicting automation variable fails instead of logging in. While the organization variable is set, commands the token can't run refuse instead of using your own login, including the store commands that run on a savedshopify store authlogin.SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN← this oneWHY are these changes introduced?
Organization automation tokens need their own variable, and shop/issues-develop#24004 sets the rules for choosing between it and the existing ones:
SHOPIFY_APP_AUTOMATION_TOKENandSHOPIFY_CLI_PARTNERS_TOKEN.Today an empty
SHOPIFY_APP_AUTOMATION_TOKENsilently falls back to whoever is logged in, and it also hidesSHOPIFY_CLI_PARTNERS_TOKEN. For an agent running on a developer's machine, that means acting as the developer.WHAT is this pull request doing?
getAppAutomationToken()reads the organization variable first, then the app variable, then the Partner variable. It still returns a plain string and never throws. Every place that already reads the automation token picks up organization tokens through it, with no other changes: app commands, the store and organization commands from Authenticate store and organization commands with an automation token #8729, analytics, and the service-account label on app commands.getAppAutomationToken()returns nothing. That means the organization variable set together with another one, or the selected variable set to an empty string. Every caller then falls back toensureAuthenticated, which reports the problem instead of starting a login. So the rules live in one place, and analytics can never throw.ensureAuthenticatedrefuses before device auth or a cached CLI login. That covers every command that logs in with your Shopify account, such asapp dev, Hydrogen, and theme commands.store execute,store bulkandstore graphiqlrun on a login saved byshopify store auth, and the token has no store or Admin API access. While the organization variable is set, they refuse with the same "This command can't use SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN" error, so neither agents nor people assume the token works there. The check is one cli-kit function,ensureNoOrganizationAutomationToken, whichensureAuthenticateduses too.shopify store authitself ignores the variable. It never reads the token variables, so it runs as if they weren't set. That's why the check runs in each store command rather than in the shared saved-login loader, whichstore authalso uses. A test pins this.Behavior change for people who don't use the new variable: when the automation variable the CLI would use is set but empty, any command that logs in now fails with "
<variable>is set but empty" instead of using your Shopify account login. In CI, where there's no saved login, it already failed, but with a vaguer "Authorization is required to continue" error.Not in this PR:
partners.app.cli.access.partners_token, because DevTools' weekly-active-user queries exclude that value by name.theme pushandtheme pullstill use a saved store login when one exists, unless you pass--password. Themes aren't part of what organization tokens do, and without a saved login, theme commands already refuse.store info: it's a supported command, so it keeps using the token. Its fallback to a saved login when the Business Platform lookup can't find the store is removed in shop/issues-develop#24007 ("No cached Admin/preview fallback").login()replaces any authentication error with "Unable to authenticate with Shopify", so Hydrogen users won't see the new message.Decisions:
store authdoesn't. Agreed in review: the token can't reach a store's Admin API, so those commands need a person for now, and a clear error beats quietly running on someone's saved login.SHOPIFY_APP_AUTOMATION_TOKENshipped as a new name forSHOPIFY_CLI_PARTNERS_TOKEN, and #24004 accepts organization tokens through it. Only the exchange can tell whether a token works for an API: Identity refuses an organization token on the Partners API with "The custom token provided can't be used for the Partners API". And because the organization variable can't be set together with another one, there's never more than one token to pick. Retiring the Partner variable means deleting its constant and its two entries inautomation-token.ts, the only place it's read.How to manually test your changes?
Each of these fails right away:
In order, you should see the conflict error, the empty-variable error, and "This command can't use SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN" for the last two. The
store executeone fails even if you've runshopify store authfor that store.SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN=x shopify store auth --store <store> --scopes read_productsstill opens the browser and saves the login as usual.With a store-capable token,
SHOPIFY_ORGANIZATION_AUTOMATION_TOKEN=<token> shopify store list --organization-id <id>lists the organization's dev stores. Minting one needs shop/issues-develop#24000, or thealex/organization-token-full-e2e-demoWorld branch on a local rig.Checklist
patchfor bug fixes ·minorfor new features ·majorfor breaking changes) and added a changeset withpnpm changeset add