Allow code-owner approval to override breaking-change check#7469
Open
alfonso-noriega wants to merge 5 commits into
Open
Allow code-owner approval to override breaking-change check#7469alfonso-noriega wants to merge 5 commits into
alfonso-noriega wants to merge 5 commits into
Conversation
4 tasks
Contributor
Author
This stack of pull requests is managed by Graphite. Learn more about stacking. |
4 tasks
isaacroldan
previously approved these changes
May 14, 2026
d04a8f9 to
d31b575
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 declarationsWe found no new type declarations in this PR Existing type declarationspackages/cli-kit/dist/private/node/conf-store.d.ts@@ -128,11 +128,10 @@ interface RunWithRateLimitOptions {
export declare function runWithRateLimit(options: RunWithRateLimitOptions, config?: LocalStorage<ConfSchema>): Promise<boolean>;
/**
* Get auto-upgrade preference.
- * Defaults to true if the preference has never been explicitly set.
*
- * @returns Whether auto-upgrade is enabled.
+ * @returns Whether auto-upgrade is enabled, or undefined if never set.
*/
-export declare function getAutoUpgradeEnabled(config?: LocalStorage<ConfSchema>): boolean;
+export declare function getAutoUpgradeEnabled(config?: LocalStorage<ConfSchema>): boolean | undefined;
/**
* Set auto-upgrade preference.
*
packages/cli-kit/dist/private/node/constants.d.ts@@ -8,7 +8,6 @@ export declare const environmentVariables: {
env: string;
firstPartyDev: string;
noAnalytics: string;
- optOutInstrumentation: string;
appAutomationToken: string;
partnersToken: string;
runAsUser: string;
packages/cli-kit/dist/public/node/metadata.d.ts@@ -34,7 +34,7 @@ export type SensitiveSchema<T> = T extends RuntimeMetadataManager<infer _TPublic
* @returns A container for the metadata.
*/
export declare function createRuntimeMetadataContainer<TPublic extends AnyJson, TSensitive extends AnyJson = Record<string, never>>(defaultPublicMetadata?: Partial<TPublic>): RuntimeMetadataManager<TPublic, TSensitive>;
-type CmdFieldsFromMonorail = PickByPrefix<MonorailEventPublic, 'cmd_all_'> & PickByPrefix<MonorailEventPublic, 'cmd_app_'> & PickByPrefix<MonorailEventPublic, 'cmd_create_app_'> & PickByPrefix<MonorailEventPublic, 'cmd_theme_'> & PickByPrefix<MonorailEventPublic, 'store_'> & PickByPrefix<MonorailEventPublic, 'env_auto_upgrade_'>;
+type CmdFieldsFromMonorail = Pick<MonorailEventPublic, 'shop_id'> & PickByPrefix<MonorailEventPublic, 'cmd_all_'> & PickByPrefix<MonorailEventPublic, 'cmd_app_'> & PickByPrefix<MonorailEventPublic, 'cmd_create_app_'> & PickByPrefix<MonorailEventPublic, 'cmd_theme_'> & PickByPrefix<MonorailEventPublic, 'store_'> & PickByPrefix<MonorailEventPublic, 'env_auto_upgrade_'>;
declare const coreData: RuntimeMetadataManager<CmdFieldsFromMonorail, {
commandStartOptions: {
startTime: number;
packages/cli-kit/dist/public/node/monorail.d.ts@@ -2,7 +2,7 @@ import { JsonMap } from '../../private/common/json.js';
import { DeepRequired } from '../common/ts/deep-required.js';
export { DeepRequired };
type Optional<T> = T | null;
-export declare const MONORAIL_COMMAND_TOPIC = "app_cli3_command/1.24";
+export declare const MONORAIL_COMMAND_TOPIC = "app_cli3_command/1.23";
export interface Schemas {
[MONORAIL_COMMAND_TOPIC]: {
sensitive: {
@@ -32,7 +32,7 @@ export interface Schemas {
node_version: string;
is_employee: boolean;
store_fqdn_hash?: Optional<string>;
- store_fqdn_validated?: Optional<boolean>;
+ shop_id?: Optional<number>;
user_id: string;
cmd_all_alias_used?: Optional<string>;
cmd_all_launcher?: Optional<string>;
packages/cli-kit/dist/public/node/node-package-manager.d.ts@@ -13,8 +13,8 @@ export declare const bunLockfile = "bun.lockb";
export declare const pnpmWorkspaceFile = "pnpm-workspace.yaml";
/** An array containing the lockfiles from all the package managers */
export declare const lockfiles: Lockfile[];
-export declare const lockfilesByManager: Record<PackageManager, Lockfile[]>;
-export type Lockfile = 'yarn.lock' | 'package-lock.json' | 'pnpm-lock.yaml' | 'bun.lockb' | 'bun.lock';
+export declare const lockfilesByManager: Record<PackageManager, Lockfile | undefined>;
+export type Lockfile = 'yarn.lock' | 'package-lock.json' | 'pnpm-lock.yaml' | 'bun.lockb';
/**
* A union type that represents the type of dependencies in the package.json
* - dev: devDependencies
packages/cli-kit/dist/public/node/upgrade.d.ts import { getAutoUpgradeEnabled, setAutoUpgradeEnabled } from '../../private/node/conf-store.js';
export { getAutoUpgradeEnabled, setAutoUpgradeEnabled };
/**
* Utility function for generating an install command for the user to run
* to install an updated version of Shopify CLI.
*
* @returns A string with the command to run, or undefined if the package manager cannot be determined.
*/
export declare function cliInstallCommand(): string | undefined;
/**
- * Options for {@link runCLIUpgrade}.
- */
-export interface RunCLIUpgradeOptions {
- /**
- * `true` when the upgrade is being triggered by the automatic postrun hook.
- * In that case we skip project-local upgrades — those should only happen when the
- * user explicitly runs `shopify upgrade` so we don't surprise them by mutating
- * their `package.json` / lockfile in the background.
- */
- autoupgrade?: boolean;
-}
-/**
* Runs the CLI upgrade using the appropriate package manager.
* Determines the install command and executes it.
*
- * @param options - See {@link RunCLIUpgradeOptions}.
* @throws AbortError if the package manager or command cannot be determined.
*/
-export declare function runCLIUpgrade(options?: RunCLIUpgradeOptions): Promise<void>;
+export declare function runCLIUpgrade(): Promise<void>;
/**
* Returns the version to auto-upgrade to, or undefined if auto-upgrade should be skipped.
- * Auto-upgrade is enabled by default and can be disabled via `setAutoUpgradeEnabled(false)`.
+ * Auto-upgrade is disabled by default and must be enabled via `shopify upgrade`.
* Also skips for CI, pre-release versions, or when no newer version is available.
*
* @returns The version string to upgrade to, or undefined if no upgrade should happen.
*/
export declare function versionToAutoUpgrade(): string | undefined;
/**
* Checks the freshly fetched notifications feed for a kill-switch notification that
* disables auto-upgrade. A blocking notification is one with `surface: "autoupgrade"`,
* `type: "error"`, and matching version/date ranges for the current CLI.
*
* Fails open: any error fetching or parsing the feed results in `false`, so a broken
* notifications endpoint never prevents users from auto-upgrading. Intentionally silent
* (no logs) — this is invoked on the auto-upgrade hot path.
*
* @returns `true` when an active blocking notification is found, `false` otherwise.
*/
export declare function hasBlockingAutoUpgradeNotification(): Promise<boolean>;
/**
* Shows a daily upgrade-available warning for users who have not enabled auto-upgrade.
* Skipped in CI and for pre-release versions. When auto-upgrade is enabled this is a no-op
* because the postrun hook will handle the upgrade directly.
*/
export declare function warnIfUpgradeAvailable(): Promise<void>;
/**
* Generates a message to remind the user to update the CLI.
* For major version bumps, appends a link to the GitHub release notes so users
* can review breaking changes before deciding to upgrade.
*
* @param version - The version to update to.
* @param isMajor - Whether the version bump is a major version change.
* @returns The message to remind the user to update the CLI.
*/
export declare function getOutputUpdateCLIReminder(version: string, isMajor?: boolean): string;
+/**
+ * Prompts the user to enable or disable automatic upgrades, then persists their choice.
+ *
+ * @returns Whether the user chose to enable auto-upgrade.
+ */
+export declare function promptAutoUpgrade(): Promise<boolean>;
packages/cli-kit/dist/public/node/context/local.d.ts@@ -53,7 +53,7 @@ export declare function isUnitTest(env?: NodeJS.ProcessEnv): boolean;
* Returns true if reporting analytics is enabled.
*
* @param env - The environment variables from the environment of the current process.
- * @returns True unless SHOPIFY_CLI_NO_ANALYTICS or OPT_OUT_INSTRUMENTATION is truthy, or debug mode is enabled.
+ * @returns True unless SHOPIFY_CLI_NO_ANALYTICS is truthy or debug mode is enabled.
*/
export declare function analyticsDisabled(env?: NodeJS.ProcessEnv): boolean;
/**
|
04411a5 to
15299d0
Compare
d31b575 to
0872730
Compare
6e6bea7 to
8fdaa1f
Compare
0872730 to
27d7554
Compare
isaacroldan
reviewed
May 14, 2026
isaacroldan
left a comment
Contributor
There was a problem hiding this comment.
Review assisted by pair-review
isaacroldan
reviewed
May 14, 2026
10ffab5 to
d2b3f35
Compare
f3ea402 to
04d5fb5
Compare
isaacroldan
reviewed
May 22, 2026
isaacroldan
left a comment
Contributor
There was a problem hiding this comment.
Review assisted by pair-review
Moving the job into its own workflow file accidentally dropped fetch-depth from 0 to 1. Without full history, `git merge-base origin/<base> HEAD` fails and the scoping logic falls back to scanning all changesets/schemas, re-flagging every pre-existing major change in the repo.
…views
- Workflow: split the post-check sticky-comment step so the override
footer ('Override: approved by @user') survives instead of being
deleted alongside the 'no breaking changes' clear-out.
- major-change-check.js: filter the latest-review-per-author map to
actionable states only (APPROVED / CHANGES_REQUESTED / DISMISSED).
Without this, an approver who later leaves an inline review comment
has their APPROVED entry overwritten by the resulting COMMENTED
review and the override is silently lost.
- Adds regression test for the COMMENTED-after-APPROVED case.
Comment #3: resolveContext only returned {baselineRef, changedFiles}, so runMain called findCodeownerApproval with undefined repo/prNumber and the override always bailed at 'no PR number in event'. The feature was a silent no-op in CI even though unit tests passed (they injected repo/prNumber directly, bypassing the integration path). - Reads GITHUB_REPOSITORY and GITHUB_EVENT_PATH inside resolveContext, with the same dependency-injection shape (`repository`, `eventPath`, `readEvent`) we use for runGit so tests can drive it deterministically. - Returns {baselineRef, changedFiles, repo, prNumber} from every code path; merge_group / local invocations cleanly degrade to null. - Adds an integration-shaped test that exercises the actual env-var path (pull_request and pull_request_review payloads), plus coverage for merge_group, missing env, malformed slugs, and unreadable event files. This is the exact regression the unit tests previously missed. - Logs the resolved PR context (or its absence) so CI runs make it obvious whether the override check is eligible. Comment #4: removed the `pnpm nx run-many --target=build` step from the workflow. major-change-check.js operates on git-tracked source files and `git diff` output only \u2014 it never imports compiled artefacts, and runMain already explicitly skips install+build for the baseline clone for the same reason. Verified locally: the script runs cleanly against a fresh checkout with no dist/. Approval-triggered re-runs now take ~30s instead of ~5\u20138min.
5 follow-ups from Isaac's review: 1. Drop `edited` from the pull_request_review trigger types. The `edited` action fires when a review's body text is mutated, which doesn't affect approval state \u2014 so it re-runs the full clone+diff for no functional effect. 2. Bump DEFAULT_NODE_VERSION 24.1.0 \u2192 26.1.0 to match tests-pr.yml, tests-main.yml, and tests-manual.yml. The 24.1.0 value was an oversight from the workflow extraction. 3. Add a module-level comment above the code-owner override section clarifying that the CODEOWNERS parsing helpers (parseCodeowners, codeownersPatternToRegExp, ownersForFiles) are informational-only: they log which teams own the changed paths but do *not* gate the override. Gating is on repo write access, per the existing doc-comment on findCodeownerApproval. This addresses the 'misleading surface area' concern without ripping out tested primitives that we may want to promote into the gating path later (e.g. once a token that can resolve org-team membership is available). 4. Delete the unused `defaultFetchCompare` helper. Verified no callers anywhere in the codebase \u2014 it was scaffolded for a compare-API path that was replaced by the git-based diff in resolveContext. 5. Add an explicit `!repo` guard to findCodeownerApproval. This is a follow-on to the previous round's resolveContext change: that fix started returning `repo: null` when GITHUB_REPOSITORY is unset or malformed (local invocation), but the downstream guard was missed. Without it, defaultFetchReviews(null, prNumber) would throw TypeError reading 'owner' \u2014 accidentally fail-safe (non-zero exit) but with a baffling error message. Adds a regression test covering the case and asserting that no GitHub API URL is constructed when repo is null.
372e473 to
0511a7e
Compare
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.

WHY are these changes introduced?
The breaking-change check fails hard (
exit 1) whenever the script detects a removed Zod field, command, flag, or major changeset. There's currently no way to override it short of force-merging or admin-bypassing branch protection — and after PR #7468 in this stack, the previous (broken) "approval" path is gone too.In practice, breaking changes are sometimes intentional: removing a deprecated field at a major-version boundary, restructuring a schema after a migration, etc. The team needs a way to acknowledge "yes, we know this is breaking, ship it" that's auditable and lightweight.
The simplest, most natural mechanism is the one already in place for everything else: a code-owner approval on the PR.
WHAT is this pull request doing?
Adds a code-owner-approval override to the breaking-change check. When a code owner of the changed files approves the PR, the check re-runs automatically (via a new
pull_request_reviewtrigger) and turns green..github/workflows/breaking-change-check.yml(new)The breaking-change job moved into its own workflow file so we can trigger it on
pull_request_reviewevents without re-running the entire test suite (unit tests, e2e, type-diff, etc.) every time someone clicks Approve. Triggers:pull_request— same as beforepull_request_review: types: [submitted, dismissed, edited]— re-runs when an approval is added or withdrawn so the check turns green/red automaticallymerge_group— same as beforeConcurrency is keyed per PR (or per merge-queue head) so the latest review event always wins.
.github/workflows/tests-pr.ymlmajor-change-checkjob.workspace/src/major-change-check.jsWhen breaking changes are detected, the script now:
GET /repos/{owner}/{repo}/pulls/{N}/reviews.CHANGES_REQUESTEDcancels an earlierAPPROVED, exactly likedismiss_stale_reviews).GET /repos/{owner}/{repo}/collaborators/{user}/permission. If the permission isadmin,maintain, orwrite, treat the approval as a code-owner override.has_breaking_changes=false, log the approver, append an "Override: approved by @user" footer to the sticky comment, and exit cleanly.exit 1.The script also reads CODEOWNERS (
.github/CODEOWNERS) and logs which teams own the changed paths for transparency in the run output. The override decision uses the repo-permission check (not direct team-membership lookup) because the defaultGITHUB_TOKENcan't list arbitrary org-team memberships across Shopify, but it can read repo-level collaborator permissions. Anyone withwriteon this repo got that access via a code-owning team grant — same security posture as branch protection's "require code-owner review", evaluated earlier so the check can flip without manual CI re-runs.The override does not auto-approve when the GitHub API fails — every degradation path returns
approved: false. We'd rather over-flag than silently grant.workspace/src/major-change-check.test.js10 new tests:
parseCodeowners— comments and blank lines stripped, owners preservedcodeownersPatternToRegExp—*,**, anchored, and unanchored patternsownersForFiles— last-matching-rule-wins (matches CODEOWNERS semantics, including theme overriding the catch-all)findCodeownerApproval:CHANGES_REQUESTEDcancels an earlierAPPROVED, sticky-as-dismiss_stale_reviews)Stickiness behaviour
Per the requirement: matches
dismiss_stale_reviews. Concretely:CHANGES_REQUESTEDorDISMISSED, the override is gone (we always look at latest review per author).git push, the PR's existing approving reviews remain (sincedismiss_stale_reviews: falseonmain's branch protection), so the override survives a force-push. If branch-protection ever flipsdismiss_stale_reviewsto true, GitHub will dismiss the reviews and our check will correctly go red again on the next event.How to test your changes?
cd workspace node --test src/major-change-check.test.jsAll 21 tests pass locally (11 pre-existing + 10 new). End-to-end testing requires a PR that introduces a real or fake breaking change; smoke-test plan:
Post-release steps
None — the workflow takes effect immediately on first run on
main.Checklist