From 1ba648930f003bbb0ed2014ec3c1c8214e728c15 Mon Sep 17 00:00:00 2001 From: Aryan Bansal Date: Wed, 30 Sep 2026 17:27:24 +0530 Subject: [PATCH] feat(rollback): show a plan-not-available message when instant rollback isn't entitled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit launch rollback previously showed the same generic "No rollback-eligible deployments are available" message whether an org's plan simply doesn't include instant rollback, or the environment genuinely has nothing eligible yet. Now queries the same RollbackSettings.isEnabled entitlement signal the Launch UI added recently and shows a distinct message when the org's plan explicitly disables it. Fails open on any fetch error (falls back to the existing generic message), and only fires on the already- failing path (no extra request when a rollback target exists). Does not change the rollback mutation's own authorization: it is still gated solely by a deployment's isRollbackEligible flag, with no live entitlement check on the mutation itself. This is a message-clarity improvement only. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/commands/launch/rollback.test.ts | 80 ++++++++++++++++++++++------ src/commands/launch/rollback.ts | 20 ++++++- src/graphql/queries.ts | 10 ++++ 3 files changed, 94 insertions(+), 16 deletions(-) diff --git a/src/commands/launch/rollback.test.ts b/src/commands/launch/rollback.test.ts index ad5cf33..721f342 100644 --- a/src/commands/launch/rollback.test.ts +++ b/src/commands/launch/rollback.test.ts @@ -82,23 +82,73 @@ describe('Rollback Command', () => { jest.clearAllMocks(); }); - it('exits when no rollback-eligible deployments are available', async () => { - const noEligibleResponse = { - data: { - Environments: { - edges: [ - { - node: { - uid: 'env-uid', - name: 'Default', - deployments: { edges: [{ node: liveDeployment }] }, - }, + const noEligibleResponse = { + data: { + Environments: { + edges: [ + { + node: { + uid: 'env-uid', + name: 'Default', + deployments: { edges: [{ node: liveDeployment }] }, }, - ], - }, + }, + ], }, - }; - const query = jest.fn().mockResolvedValueOnce(noEligibleResponse); + }, + }; + + it('exits when no rollback-eligible deployments are available', async () => { + const query = jest + .fn() + .mockResolvedValueOnce(noEligibleResponse) + .mockResolvedValueOnce({ + data: { RollbackSettings: { isEnabled: true, retentionCount: 3 } }, + }); + const mutate = jest.fn(); + const cmd = buildCommand({ environment: 'Default' }, query, mutate); + jest + .spyOn(cmd as any, 'fetchCurrentLiveDeployment') + .mockResolvedValueOnce(liveDeployment); + + await expect((cmd as any).rollbackDeployment()).rejects.toThrow('process.exit:1'); + + expect(mutate).not.toHaveBeenCalled(); + expect(exitMock).toHaveBeenCalledWith(1); + expect((cmd as any).log).toHaveBeenCalledWith( + 'No rollback-eligible deployments are available for this environment.', + 'error', + ); + }); + + it('shows a plan-not-available message when instant rollback is not entitled', async () => { + const query = jest + .fn() + .mockResolvedValueOnce(noEligibleResponse) + .mockResolvedValueOnce({ + data: { RollbackSettings: { isEnabled: false, retentionCount: 0 } }, + }); + const mutate = jest.fn(); + const cmd = buildCommand({ environment: 'Default' }, query, mutate); + jest + .spyOn(cmd as any, 'fetchCurrentLiveDeployment') + .mockResolvedValueOnce(liveDeployment); + + await expect((cmd as any).rollbackDeployment()).rejects.toThrow('process.exit:1'); + + expect(mutate).not.toHaveBeenCalled(); + expect(exitMock).toHaveBeenCalledWith(1); + expect((cmd as any).log).toHaveBeenCalledWith( + 'Instant rollback isn\'t available on your organization\'s plan.', + 'error', + ); + }); + + it('falls back to the generic message when the settings fetch fails', async () => { + const query = jest + .fn() + .mockResolvedValueOnce(noEligibleResponse) + .mockRejectedValueOnce(new Error('network error')); const mutate = jest.fn(); const cmd = buildCommand({ environment: 'Default' }, query, mutate); jest diff --git a/src/commands/launch/rollback.ts b/src/commands/launch/rollback.ts index 8cf536a..84b3ddb 100644 --- a/src/commands/launch/rollback.ts +++ b/src/commands/launch/rollback.ts @@ -10,6 +10,7 @@ import { environmentsQuery, latestLiveDeploymentQuery, rollbackDeploymentMutation, + rollbackSettingsQuery, } from '../../graphql'; import { Logger, selectOrg, selectProject } from '../../util'; @@ -88,7 +89,12 @@ export default class Rollback extends BaseCommand { const eligibleSorted = this.getEligibleSortedDeployments(environment, currentLive?.uid); if (isEmpty(eligibleSorted)) { - this.log('No rollback-eligible deployments are available for this environment.', 'error'); + const settings = await this.fetchRollbackSettings(); + if (settings?.isEnabled === false) { + this.log('Instant rollback isn\'t available on your organization\'s plan.', 'error'); + } else { + this.log('No rollback-eligible deployments are available for this environment.', 'error'); + } process.exit(1); } @@ -194,6 +200,18 @@ export default class Rollback extends BaseCommand { .catch(() => undefined); } + /** + * @method fetchRollbackSettings - org-level entitlement; undefined on any fetch failure + * + * @memberof Rollback + */ + async fetchRollbackSettings(): Promise<{ isEnabled: boolean; retentionCount: number } | undefined> { + return this.apolloClient + .query({ query: rollbackSettingsQuery }) + .then(({ data }) => data?.RollbackSettings) + .catch(() => undefined); + } + /** * @method getEligibleSortedDeployments - eligible deployments excluding current live, sorted by number desc * diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 1cc1578..e3d9886 100755 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -185,6 +185,15 @@ const latestLiveDeploymentQuery: DocumentNode = gql` } `; +const rollbackSettingsQuery: DocumentNode = gql` + query RollbackSettings { + RollbackSettings { + isEnabled + retentionCount + } + } +`; + const environmentsQuery: DocumentNode = gql` query Environments($skipRollbackData: Boolean = true) { Environments { @@ -228,4 +237,5 @@ export { serverlessLogsQuery, latestLiveDeploymentQuery, environmentsQuery, + rollbackSettingsQuery, };