Skip to content

Commit 1ba6489

Browse files
feat(rollback): show a plan-not-available message when instant rollback isn't entitled
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 <noreply@anthropic.com>
1 parent 3ef2991 commit 1ba6489

3 files changed

Lines changed: 94 additions & 16 deletions

File tree

‎src/commands/launch/rollback.test.ts‎

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,73 @@ describe('Rollback Command', () => {
8282
jest.clearAllMocks();
8383
});
8484

85-
it('exits when no rollback-eligible deployments are available', async () => {
86-
const noEligibleResponse = {
87-
data: {
88-
Environments: {
89-
edges: [
90-
{
91-
node: {
92-
uid: 'env-uid',
93-
name: 'Default',
94-
deployments: { edges: [{ node: liveDeployment }] },
95-
},
85+
const noEligibleResponse = {
86+
data: {
87+
Environments: {
88+
edges: [
89+
{
90+
node: {
91+
uid: 'env-uid',
92+
name: 'Default',
93+
deployments: { edges: [{ node: liveDeployment }] },
9694
},
97-
],
98-
},
95+
},
96+
],
9997
},
100-
};
101-
const query = jest.fn().mockResolvedValueOnce(noEligibleResponse);
98+
},
99+
};
100+
101+
it('exits when no rollback-eligible deployments are available', async () => {
102+
const query = jest
103+
.fn()
104+
.mockResolvedValueOnce(noEligibleResponse)
105+
.mockResolvedValueOnce({
106+
data: { RollbackSettings: { isEnabled: true, retentionCount: 3 } },
107+
});
108+
const mutate = jest.fn();
109+
const cmd = buildCommand({ environment: 'Default' }, query, mutate);
110+
jest
111+
.spyOn(cmd as any, 'fetchCurrentLiveDeployment')
112+
.mockResolvedValueOnce(liveDeployment);
113+
114+
await expect((cmd as any).rollbackDeployment()).rejects.toThrow('process.exit:1');
115+
116+
expect(mutate).not.toHaveBeenCalled();
117+
expect(exitMock).toHaveBeenCalledWith(1);
118+
expect((cmd as any).log).toHaveBeenCalledWith(
119+
'No rollback-eligible deployments are available for this environment.',
120+
'error',
121+
);
122+
});
123+
124+
it('shows a plan-not-available message when instant rollback is not entitled', async () => {
125+
const query = jest
126+
.fn()
127+
.mockResolvedValueOnce(noEligibleResponse)
128+
.mockResolvedValueOnce({
129+
data: { RollbackSettings: { isEnabled: false, retentionCount: 0 } },
130+
});
131+
const mutate = jest.fn();
132+
const cmd = buildCommand({ environment: 'Default' }, query, mutate);
133+
jest
134+
.spyOn(cmd as any, 'fetchCurrentLiveDeployment')
135+
.mockResolvedValueOnce(liveDeployment);
136+
137+
await expect((cmd as any).rollbackDeployment()).rejects.toThrow('process.exit:1');
138+
139+
expect(mutate).not.toHaveBeenCalled();
140+
expect(exitMock).toHaveBeenCalledWith(1);
141+
expect((cmd as any).log).toHaveBeenCalledWith(
142+
'Instant rollback isn\'t available on your organization\'s plan.',
143+
'error',
144+
);
145+
});
146+
147+
it('falls back to the generic message when the settings fetch fails', async () => {
148+
const query = jest
149+
.fn()
150+
.mockResolvedValueOnce(noEligibleResponse)
151+
.mockRejectedValueOnce(new Error('network error'));
102152
const mutate = jest.fn();
103153
const cmd = buildCommand({ environment: 'Default' }, query, mutate);
104154
jest

‎src/commands/launch/rollback.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
environmentsQuery,
1111
latestLiveDeploymentQuery,
1212
rollbackDeploymentMutation,
13+
rollbackSettingsQuery,
1314
} from '../../graphql';
1415
import { Logger, selectOrg, selectProject } from '../../util';
1516

@@ -88,7 +89,12 @@ export default class Rollback extends BaseCommand<typeof Rollback> {
8889
const eligibleSorted = this.getEligibleSortedDeployments(environment, currentLive?.uid);
8990

9091
if (isEmpty(eligibleSorted)) {
91-
this.log('No rollback-eligible deployments are available for this environment.', 'error');
92+
const settings = await this.fetchRollbackSettings();
93+
if (settings?.isEnabled === false) {
94+
this.log('Instant rollback isn\'t available on your organization\'s plan.', 'error');
95+
} else {
96+
this.log('No rollback-eligible deployments are available for this environment.', 'error');
97+
}
9298
process.exit(1);
9399
}
94100

@@ -194,6 +200,18 @@ export default class Rollback extends BaseCommand<typeof Rollback> {
194200
.catch(() => undefined);
195201
}
196202

203+
/**
204+
* @method fetchRollbackSettings - org-level entitlement; undefined on any fetch failure
205+
*
206+
* @memberof Rollback
207+
*/
208+
async fetchRollbackSettings(): Promise<{ isEnabled: boolean; retentionCount: number } | undefined> {
209+
return this.apolloClient
210+
.query({ query: rollbackSettingsQuery })
211+
.then(({ data }) => data?.RollbackSettings)
212+
.catch(() => undefined);
213+
}
214+
197215
/**
198216
* @method getEligibleSortedDeployments - eligible deployments excluding current live, sorted by number desc
199217
*

‎src/graphql/queries.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,15 @@ const latestLiveDeploymentQuery: DocumentNode = gql`
185185
}
186186
`;
187187

188+
const rollbackSettingsQuery: DocumentNode = gql`
189+
query RollbackSettings {
190+
RollbackSettings {
191+
isEnabled
192+
retentionCount
193+
}
194+
}
195+
`;
196+
188197
const environmentsQuery: DocumentNode = gql`
189198
query Environments($skipRollbackData: Boolean = true) {
190199
Environments {
@@ -228,4 +237,5 @@ export {
228237
serverlessLogsQuery,
229238
latestLiveDeploymentQuery,
230239
environmentsQuery,
240+
rollbackSettingsQuery,
231241
};

0 commit comments

Comments
 (0)