From e53d5712c91399daa6e7a085e556e69a44edc0c5 Mon Sep 17 00:00:00 2001 From: Aryan Bansal Date: Wed, 13 May 2026 14:34:43 +0530 Subject: [PATCH 1/4] Revert "CL-1753 | refactor rollback command: rename init method, enhance error handling, and update method names for clarity" This reverts commit 3b9750a47c634d123ced14aa00d8dc270b1a213a. --- src/commands/launch/rollback.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/commands/launch/rollback.ts b/src/commands/launch/rollback.ts index da447ab..29aa5f0 100644 --- a/src/commands/launch/rollback.ts +++ b/src/commands/launch/rollback.ts @@ -44,18 +44,16 @@ export default class Rollback extends BaseCommand { }), }; - async init(): Promise { - await super.init(); + async run(): Promise { this.logger = new Logger(this.sharedConfig); this.log = this.logger.log.bind(this.logger); - await this.prepareApiClients(); - } - async run(): Promise { if (!this.flags.environment) { await this.getConfig(); } + await this.prepareApiClients(); + if (!this.sharedConfig.currentConfig?.uid) { await selectOrg({ log: this.log, @@ -84,7 +82,7 @@ export default class Rollback extends BaseCommand { async rollbackDeployment(): Promise { const environment = await this.resolveEnvironment(); const currentLive = await this.fetchCurrentLiveDeployment(environment.uid); - const eligibleSorted = this.getEligibleSortedDeployments(environment, currentLive?.uid); + const eligibleSorted = this.getEligibleSorted(environment, currentLive?.uid); if (isEmpty(eligibleSorted)) { this.log('No rollback-eligible deployments are available for this environment.', 'error'); @@ -107,7 +105,7 @@ export default class Rollback extends BaseCommand { return; } - let rolledBack: { deploymentNumber: number; uid: string }; + let rolledBack: any; try { const { data } = await this.apolloClient.mutate({ mutation: rollbackDeploymentMutation, @@ -120,9 +118,8 @@ export default class Rollback extends BaseCommand { }, }); rolledBack = data?.deployment; - } catch (error: unknown) { - const err = error as { graphQLErrors?: { extensions?: { exception?: { name?: string } } }[]; message?: string }; - const code = err?.graphQLErrors?.[0]?.extensions?.exception?.name || err?.message; + } catch (error: any) { + const code = error?.graphQLErrors?.[0]?.extensions?.exception?.name || error?.message; this.log(`Rollback failed. Please try again. (${code})`, 'error'); process.exit(1); } @@ -246,11 +243,11 @@ export default class Rollback extends BaseCommand { } /** - * @method getEligibleSortedDeployments - eligible deployments excluding current live, sorted by number desc + * @method getEligibleSorted - eligible deployments excluding current live, sorted by number desc * * @memberof Rollback */ - getEligibleSortedDeployments(environment: any, currentLiveUid?: string): any[] { + getEligibleSorted(environment: any, currentLiveUid?: string): any[] { const deployments = map(environment?.deployments?.edges, 'node'); const eligible = filter( deployments, From d6e7fef6a62bbec88bd283de54c24c00c53bae9f Mon Sep 17 00:00:00 2001 From: Aryan Bansal Date: Wed, 13 May 2026 14:34:43 +0530 Subject: [PATCH 2/4] Revert "CL-1753 | add unit tests for rollback command and enhance environment resolution logic" This reverts commit 189764d6d56059b46925101d832b84fc1a4aab73. --- .talismanrc | 2 - src/commands/launch/rollback.test.ts | 233 --------------------------- src/commands/launch/rollback.ts | 67 ++++---- src/graphql/queries.ts | 15 +- 4 files changed, 35 insertions(+), 282 deletions(-) delete mode 100644 src/commands/launch/rollback.test.ts diff --git a/.talismanrc b/.talismanrc index 70638de..fb07040 100644 --- a/.talismanrc +++ b/.talismanrc @@ -6,6 +6,4 @@ fileignoreconfig: checksum: 9db6c02ad35a0367343cd753b916dd64db4a9efd24838201d2e1113ed19c9b62 - filename: package-lock.json checksum: 43c0eecc2192095c8fb5bc524b7dafa33a6141ddd3923d41ffb15ec025bea9a9 -- filename: src/commands/launch/rollback.test.ts - checksum: 561d709dfaa046af3afaf73e8570211d1b63ca8fdf23d3a6ffec0fff7587eacd version: "1.0" \ No newline at end of file diff --git a/src/commands/launch/rollback.test.ts b/src/commands/launch/rollback.test.ts deleted file mode 100644 index 0c1081d..00000000 --- a/src/commands/launch/rollback.test.ts +++ /dev/null @@ -1,233 +0,0 @@ -import Rollback from './rollback'; -import { Logger } from '../../util'; -import { cliux } from '@contentstack/cli-utilities'; - -jest.mock('../../util', () => { - const actual = jest.requireActual('../../util'); - return { - ...actual, - Logger: jest.fn(), - selectOrg: jest.fn(), - selectProject: jest.fn(), - }; -}); - -jest.mock('@contentstack/cli-utilities', () => { - const actual = jest.requireActual('@contentstack/cli-utilities'); - return { - ...actual, - configHandler: { - get: jest.fn((key) => { - if (key === 'authtoken') return 'dummy-token'; - if (key === 'authorisationType') return 'OAuth'; - if (key === 'oauthAccessToken') return 'dummy-oauth-token'; - return undefined; - }), - }, - cliux: { - ...actual.cliux, - inquire: jest.fn(), - print: jest.fn(), - }, - }; -}); - -const targetDeployment = { - uid: 'target-uid', - status: 'ARCHIVED', - gitBranch: 'main', - commitHash: 'abcdef1', - createdAt: '2026-04-29T00:00:00Z', - commitMessage: 'previous good build', - deploymentUrl: 'https://example.com', - deploymentNumber: 2, - isRollbackEligible: true, -}; - -const liveDeployment = { - ...targetDeployment, - uid: 'live-uid', - status: 'LIVE', - deploymentNumber: 3, -}; - -const environmentsResponse = { - data: { - Environments: { - edges: [ - { - node: { - uid: 'env-uid', - name: 'Default', - deployments: { - edges: [ - { node: liveDeployment }, - { node: targetDeployment }, - ], - }, - }, - }, - ], - }, - }, -}; - -const buildCommand = (flags: Record = {}, queryImpl?: jest.Mock, mutateImpl?: jest.Mock) => { - const cmd = new Rollback([], {} as any); - (cmd as any).flags = flags; - (cmd as any).log = jest.fn(); - (cmd as any).logger = { log: jest.fn() }; - (cmd as any).sharedConfig = { currentConfig: { uid: 'project-uid' } }; - (cmd as any).apolloClient = { - query: queryImpl || jest.fn(), - mutate: mutateImpl || jest.fn(), - }; - return cmd; -}; - -describe('Rollback Command', () => { - let exitMock: jest.SpyInstance; - - beforeEach(() => { - (Logger as jest.Mock).mockImplementation(() => ({ log: jest.fn() })); - exitMock = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => { - throw new Error(`process.exit:${code}`); - }) as any); - }); - - afterEach(() => { - 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 query = jest.fn().mockResolvedValueOnce(noEligibleResponse); - 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('exits when --deployment flag does not match an eligible deployment', async () => { - const query = jest.fn().mockResolvedValueOnce(environmentsResponse); - const mutate = jest.fn(); - const cmd = buildCommand( - { environment: 'Default', deployment: 'unknown-uid' }, - 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( - 'Provided deployment UID is not rollback-eligible or does not exist.', - 'error', - ); - }); - - it('skips the mutation when the user does not confirm', async () => { - const query = jest.fn().mockResolvedValueOnce(environmentsResponse); - const mutate = jest.fn(); - const cmd = buildCommand( - { environment: 'Default', deployment: 'target-uid', reason: 'audit' }, - query, - mutate, - ); - jest - .spyOn(cmd as any, 'fetchCurrentLiveDeployment') - .mockResolvedValueOnce(liveDeployment); - (cliux.inquire as jest.Mock).mockResolvedValueOnce(false); // confirm prompt - - await (cmd as any).rollbackDeployment(); - - expect(mutate).not.toHaveBeenCalled(); - }); - - it('fires the rollback mutation and polls until LIVE on success', async () => { - const query = jest.fn().mockResolvedValueOnce(environmentsResponse); - const mutate = jest.fn().mockResolvedValueOnce({ - data: { deployment: { ...targetDeployment, status: 'QUEUED' } }, - }); - const cmd = buildCommand( - { environment: 'Default', deployment: 'target-uid', reason: 'restoring' }, - query, - mutate, - ); - jest - .spyOn(cmd as any, 'fetchCurrentLiveDeployment') - .mockResolvedValueOnce(liveDeployment); - jest.spyOn(cmd as any, 'pollDeploymentStatus').mockResolvedValueOnce('LIVE'); - (cliux.inquire as jest.Mock).mockResolvedValueOnce(true); - - await (cmd as any).rollbackDeployment(); - - expect(mutate).toHaveBeenCalledTimes(1); - const variables = mutate.mock.calls[0][0].variables; - expect(variables).toEqual({ - input: { - deployment: 'target-uid', - environment: 'env-uid', - reason: 'restoring', - }, - }); - expect((cmd as any).pollDeploymentStatus).toHaveBeenCalledWith('env-uid', 'target-uid'); - expect(exitMock).not.toHaveBeenCalled(); - }); - - it('logs an error and exits when the rollback mutation fails', async () => { - const query = jest.fn().mockResolvedValueOnce(environmentsResponse); - const error = Object.assign(new Error('boom'), { - graphQLErrors: [{ extensions: { exception: { name: 'DeploymentRollbackFailed' } } }], - }); - const mutate = jest.fn().mockRejectedValueOnce(error); - const cmd = buildCommand( - { environment: 'Default', deployment: 'target-uid' }, - query, - mutate, - ); - jest - .spyOn(cmd as any, 'fetchCurrentLiveDeployment') - .mockResolvedValueOnce(liveDeployment); - (cliux.inquire as jest.Mock) - .mockResolvedValueOnce('') // reason - .mockResolvedValueOnce(true); // confirm - - await expect((cmd as any).rollbackDeployment()).rejects.toThrow('process.exit:1'); - - expect(mutate).toHaveBeenCalledTimes(1); - expect(exitMock).toHaveBeenCalledWith(1); - expect((cmd as any).log).toHaveBeenCalledWith( - 'Rollback failed. Please try again. (DeploymentRollbackFailed)', - 'error', - ); - }); -}); diff --git a/src/commands/launch/rollback.ts b/src/commands/launch/rollback.ts index 29aa5f0..1b4f7c9 100644 --- a/src/commands/launch/rollback.ts +++ b/src/commands/launch/rollback.ts @@ -194,37 +194,37 @@ export default class Rollback extends BaseCommand { */ async resolveEnvironment(): Promise { const environments = await this.apolloClient - .query({ - query: environmentsQuery, - variables: { skipRollbackData: false }, - }) + .query({ query: environmentsQuery }) .then(({ data: { Environments } }) => map(Environments.edges, 'node')) .catch((error) => { this.log(error?.message, 'error'); process.exit(1); }); - if (this.flags.environment) { - const environment = find( - environments, - ({ uid, name }) => uid === this.flags.environment || name === this.flags.environment, - ); - if (isEmpty(environment)) { - this.log('Environment(s) not found!', 'error'); - process.exit(1); - } - return environment; + let environment = find( + environments, + ({ uid, name }) => + uid === this.flags.environment || + name === this.flags.environment || + uid === this.sharedConfig.currentConfig?.environments?.[0]?.uid, + ); + + if (isEmpty(environment) && (this.flags.environment || this.sharedConfig.currentConfig?.environments?.[0]?.uid)) { + this.log('Environment(s) not found!', 'error'); + process.exit(1); + } else if (isEmpty(environment)) { + environment = await ux + .inquire({ + type: 'search-list', + name: 'Environment', + choices: map(environments, (row) => ({ ...row, value: row.name })), + message: 'Choose an environment', + }) + .then((name: any) => find(environments, { name }) as Record); } - // NOTE: rollback is destructive; never auto-select from saved config — always prompt. - return ux - .inquire({ - type: 'search-list', - name: 'Environment', - choices: map(environments, (row) => ({ ...row, value: row.name })), - message: 'Choose an environment', - }) - .then((name: any) => find(environments, { name }) as Record); + this.sharedConfig.environment = environment; + return environment; } /** @@ -271,15 +271,11 @@ export default class Rollback extends BaseCommand { return match; } - const choices = map(eligibleSorted, (d) => { - const message = (d.commitMessage || '').split('\n')[0].trim() || '—'; - const truncated = message.length > 60 ? `${message.slice(0, 57)}…` : message; - return { - ...d, - name: `#${d.deploymentNumber} | ${sourceLabel(d) || '—'} | ${truncated} | ${d.createdAt}`, - value: d.uid, - }; - }); + const choices = map(eligibleSorted, (d) => ({ + ...d, + name: `#${d.deploymentNumber} | ${sourceLabel(d) || '—'} | ${d.createdAt}`, + value: d.uid, + })); const selectedUid = await ux.inquire({ type: 'search-list', @@ -391,11 +387,8 @@ function formatDeployment(deployment?: any): string { } const number = deployment.deploymentNumber ? `#${deployment.deploymentNumber}` : deployment.uid; const source = sourceLabel(deployment); - const message = ((deployment.commitMessage || '').split('\n')[0] || '').trim(); - const truncated = message.length > 40 ? `${message.slice(0, 37)}…` : message; const createdAt = deployment.createdAt || ''; const numberCol = chalk.green(number.padEnd(6)); - const sourceCol = source ? chalk.cyan(source.padEnd(22)) : ''.padEnd(22); - const messageCol = truncated || chalk.dim('—'); - return `${numberCol} ${sourceCol} ${messageCol} ${chalk.dim(createdAt)}`; + const sourceCol = source ? chalk.cyan(source.padEnd(28)) : ''.padEnd(28); + return `${numberCol} ${sourceCol} ${chalk.dim(createdAt)}`; } diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 1cb0e81..a4a7e7c 100755 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -145,17 +145,12 @@ const latestLiveDeploymentQuery: DocumentNode = gql` environment deploymentNumber deploymentUrl - status - gitBranch - commitHash - commitMessage - createdAt } } `; const environmentsQuery: DocumentNode = gql` - query Environments($skipRollbackData: Boolean = true) { + query Environments { Environments { edges { node { @@ -166,14 +161,14 @@ const environmentsQuery: DocumentNode = gql` edges { node { uid + status + gitBranch + commitHash createdAt commitMessage deploymentUrl deploymentNumber - status @skip(if: $skipRollbackData) - gitBranch @skip(if: $skipRollbackData) - commitHash @skip(if: $skipRollbackData) - isRollbackEligible @skip(if: $skipRollbackData) + isRollbackEligible } } } From cdab0e57970d485c381b35ed275c6bc5240f998c Mon Sep 17 00:00:00 2001 From: Aryan Bansal Date: Wed, 13 May 2026 14:34:43 +0530 Subject: [PATCH 3/4] Revert "CL-1753 | enhance rollback command with deployment status polling and improved error handling" This reverts commit 99804546071bb3e6970e0082af6fe5bf0df4935a. --- src/commands/launch/rollback.ts | 85 +++++---------------------------- 1 file changed, 13 insertions(+), 72 deletions(-) diff --git a/src/commands/launch/rollback.ts b/src/commands/launch/rollback.ts index 1b4f7c9..4b6a89a 100644 --- a/src/commands/launch/rollback.ts +++ b/src/commands/launch/rollback.ts @@ -7,7 +7,6 @@ import { FlagInput, Flags, cliux as ux } from '@contentstack/cli-utilities'; import { BaseCommand } from '../../base-command'; import { - deploymentQuery, environmentsQuery, latestLiveDeploymentQuery, rollbackDeploymentMutation, @@ -105,9 +104,8 @@ export default class Rollback extends BaseCommand { return; } - let rolledBack: any; - try { - const { data } = await this.apolloClient.mutate({ + await this.apolloClient + .mutate({ mutation: rollbackDeploymentMutation, variables: { input: { @@ -116,75 +114,18 @@ export default class Rollback extends BaseCommand { ...(reason ? { reason } : {}), }, }, + }) + .then(({ data: { deployment: rolledBack } }) => { + ux.print(''); + ux.print(chalk.green('✔ Instant rollback to a previous deployment is successful.')); + ux.print(` New deployment: ${chalk.cyan(rolledBack.uid)} status: ${chalk.cyan(rolledBack.status)}`); + ux.print(''); + }) + .catch((error) => { + const code = error?.graphQLErrors?.[0]?.extensions?.exception?.name || error?.message; + this.log(`Rollback failed. Please try again. (${code})`, 'error'); + process.exit(1); }); - rolledBack = data?.deployment; - } catch (error: any) { - const code = error?.graphQLErrors?.[0]?.extensions?.exception?.name || error?.message; - this.log(`Rollback failed. Please try again. (${code})`, 'error'); - process.exit(1); - } - - ux.print(''); - ux.print( - `Promoting deployment ${chalk.cyan(`#${rolledBack.deploymentNumber}`)} ` - + chalk.dim(`(${rolledBack.uid})`) + '…', - ); - - const finalStatus = await this.pollDeploymentStatus(environment.uid, target.uid); - - ux.print(''); - if (finalStatus === 'LIVE') { - ux.print(chalk.green('✔ Instant rollback to a previous deployment is successful.')); - const label = `${chalk.cyan(`#${rolledBack.deploymentNumber}`)} ${chalk.dim(`(${rolledBack.uid})`)}`; - ux.print(` Deployment ${label} is now ${chalk.green('LIVE')}.`); - } else if (finalStatus === 'FAILED' || finalStatus === 'CANCELLED') { - ux.print(chalk.red(`✘ Rollback ended with status: ${finalStatus}.`)); - process.exit(1); - } else { - ux.print(chalk.yellow(`Rollback is still in progress (status: ${finalStatus}).`)); - ux.print(chalk.dim(' Check the Launch dashboard for the final status.')); - } - ux.print(''); - } - - /** - * @method pollDeploymentStatus - poll the target deployment until it goes LIVE or terminal/timeout - * - * @memberof Rollback - */ - async pollDeploymentStatus(environmentUid: string, deploymentUid: string): Promise { - const intervalMs = 3000; - const timeoutMs = 90000; - const start = Date.now(); - const terminal = new Set(['LIVE', 'FAILED', 'CANCELLED']); - - while (Date.now() - start < timeoutMs) { - try { - const { data } = await this.apolloClient.query({ - query: deploymentQuery, - variables: { query: { environment: environmentUid, uid: deploymentUid } }, - fetchPolicy: 'no-cache', - }); - const status = data?.Deployment?.status; - if (status && terminal.has(status)) { - return status; - } - } catch (error: any) { - this.log(`Failed to fetch deployment status: ${error?.message}`, 'warn'); - } - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } - - try { - const { data } = await this.apolloClient.query({ - query: deploymentQuery, - variables: { query: { environment: environmentUid, uid: deploymentUid } }, - fetchPolicy: 'no-cache', - }); - return data?.Deployment?.status || 'UNKNOWN'; - } catch { - return 'UNKNOWN'; - } } /** From 396aca7f12eb3aa429c5aff1fded430aad331855 Mon Sep 17 00:00:00 2001 From: Aryan Bansal Date: Wed, 13 May 2026 14:34:43 +0530 Subject: [PATCH 4/4] Revert "CL-1753 | + Anuja | + venky | feat: add rollback command for previous deployments with GraphQL integration" This reverts commit ada3a1884169a3bb2b9915eca37bcd79d4272f3f. --- src/commands/launch/rollback.ts | 335 -------------------------------- src/graphql/mutation.ts | 16 -- src/graphql/queries.ts | 4 - 3 files changed, 355 deletions(-) delete mode 100644 src/commands/launch/rollback.ts diff --git a/src/commands/launch/rollback.ts b/src/commands/launch/rollback.ts deleted file mode 100644 index 4b6a89a..00000000 --- a/src/commands/launch/rollback.ts +++ /dev/null @@ -1,335 +0,0 @@ -import chalk from 'chalk'; -import map from 'lodash/map'; -import find from 'lodash/find'; -import filter from 'lodash/filter'; -import isEmpty from 'lodash/isEmpty'; -import { FlagInput, Flags, cliux as ux } from '@contentstack/cli-utilities'; - -import { BaseCommand } from '../../base-command'; -import { - environmentsQuery, - latestLiveDeploymentQuery, - rollbackDeploymentMutation, -} from '../../graphql'; -import { Logger, selectOrg, selectProject } from '../../util'; - -export default class Rollback extends BaseCommand { - static description = 'Roll back to previous deployment'; - - static examples = [ - '$ <%= config.bin %> <%= command.id %>', - '$ <%= config.bin %> <%= command.id %> -d "current working directory"', - '$ <%= config.bin %> <%= command.id %> -c "path to the local config file"', - // eslint-disable-next-line max-len - '$ <%= config.bin %> <%= command.id %> -e "environment number or uid" --deployment= --org= --project= --reason="restoring previous build"', - ]; - - static flags: FlagInput = { - org: Flags.string({ - description: '[Optional] Provide the organization UID', - }), - project: Flags.string({ - description: '[Optional] Provide the project UID', - }), - environment: Flags.string({ - char: 'e', - description: 'Environment name or UID', - }), - deployment: Flags.string({ - description: '[Optional] Deployment UID to roll back to', - }), - reason: Flags.string({ - description: '[Optional] Reason for the rollback (saved to audit log)', - }), - }; - - async run(): Promise { - this.logger = new Logger(this.sharedConfig); - this.log = this.logger.log.bind(this.logger); - - if (!this.flags.environment) { - await this.getConfig(); - } - - await this.prepareApiClients(); - - if (!this.sharedConfig.currentConfig?.uid) { - await selectOrg({ - log: this.log, - flags: this.flags, - config: this.sharedConfig, - managementSdk: this.managementSdk, - }); - await this.prepareApiClients(); // NOTE update org-id in header - await selectProject({ - log: this.log, - flags: this.flags, - config: this.sharedConfig, - apolloClient: this.apolloClient, - }); - await this.prepareApiClients(); // NOTE update project-id in header - } - - await this.rollbackDeployment(); - } - - /** - * @method rollbackDeployment - resolve env, run select + review steps, fire mutation - * - * @memberof Rollback - */ - async rollbackDeployment(): Promise { - const environment = await this.resolveEnvironment(); - const currentLive = await this.fetchCurrentLiveDeployment(environment.uid); - const eligibleSorted = this.getEligibleSorted(environment, currentLive?.uid); - - if (isEmpty(eligibleSorted)) { - this.log('No rollback-eligible deployments are available for this environment.', 'error'); - process.exit(1); - } - - this.printSelectStep(environment, currentLive, eligibleSorted); - const target = await this.selectDeployment(eligibleSorted); - - this.printReviewStep(currentLive, target, eligibleSorted); - const reason = await this.promptReason(); - const confirmed = await ux.inquire({ - type: 'confirm', - name: 'confirm', - message: 'Confirm & Rollback?', - }); - - if (!confirmed) { - ux.print(chalk.yellow('Rollback aborted.')); - return; - } - - await this.apolloClient - .mutate({ - mutation: rollbackDeploymentMutation, - variables: { - input: { - deployment: target.uid, - environment: environment.uid, - ...(reason ? { reason } : {}), - }, - }, - }) - .then(({ data: { deployment: rolledBack } }) => { - ux.print(''); - ux.print(chalk.green('✔ Instant rollback to a previous deployment is successful.')); - ux.print(` New deployment: ${chalk.cyan(rolledBack.uid)} status: ${chalk.cyan(rolledBack.status)}`); - ux.print(''); - }) - .catch((error) => { - const code = error?.graphQLErrors?.[0]?.extensions?.exception?.name || error?.message; - this.log(`Rollback failed. Please try again. (${code})`, 'error'); - process.exit(1); - }); - } - - /** - * @method resolveEnvironment - resolve environment via flag, config, or prompt - * - * @memberof Rollback - */ - async resolveEnvironment(): Promise { - const environments = await this.apolloClient - .query({ query: environmentsQuery }) - .then(({ data: { Environments } }) => map(Environments.edges, 'node')) - .catch((error) => { - this.log(error?.message, 'error'); - process.exit(1); - }); - - let environment = find( - environments, - ({ uid, name }) => - uid === this.flags.environment || - name === this.flags.environment || - uid === this.sharedConfig.currentConfig?.environments?.[0]?.uid, - ); - - if (isEmpty(environment) && (this.flags.environment || this.sharedConfig.currentConfig?.environments?.[0]?.uid)) { - this.log('Environment(s) not found!', 'error'); - process.exit(1); - } else if (isEmpty(environment)) { - environment = await ux - .inquire({ - type: 'search-list', - name: 'Environment', - choices: map(environments, (row) => ({ ...row, value: row.name })), - message: 'Choose an environment', - }) - .then((name: any) => find(environments, { name }) as Record); - } - - this.sharedConfig.environment = environment; - return environment; - } - - /** - * @method fetchCurrentLiveDeployment - fetch the currently live deployment for the environment - * - * @memberof Rollback - */ - async fetchCurrentLiveDeployment(environmentUid: string): Promise { - return this.apolloClient - .query({ - query: latestLiveDeploymentQuery, - variables: { query: { environment: environmentUid } }, - }) - .then(({ data }) => data?.latestLiveDeployment) - .catch(() => undefined); - } - - /** - * @method getEligibleSorted - eligible deployments excluding current live, sorted by number desc - * - * @memberof Rollback - */ - getEligibleSorted(environment: any, currentLiveUid?: string): any[] { - const deployments = map(environment?.deployments?.edges, 'node'); - const eligible = filter( - deployments, - (d) => d.isRollbackEligible && d.uid !== currentLiveUid, - ); - return [...eligible].sort((a, b) => (b.deploymentNumber || 0) - (a.deploymentNumber || 0)); - } - - /** - * @method selectDeployment - resolve target via --deployment flag or interactive picker - * - * @memberof Rollback - */ - async selectDeployment(eligibleSorted: any[]): Promise { - if (this.flags.deployment) { - const match = find(eligibleSorted, ({ uid }) => uid === this.flags.deployment); - if (isEmpty(match)) { - this.log('Provided deployment UID is not rollback-eligible or does not exist.', 'error'); - process.exit(1); - } - return match; - } - - const choices = map(eligibleSorted, (d) => ({ - ...d, - name: `#${d.deploymentNumber} | ${sourceLabel(d) || '—'} | ${d.createdAt}`, - value: d.uid, - })); - - const selectedUid = await ux.inquire({ - type: 'search-list', - name: 'Deployment', - choices, - message: 'Select a version to restore', - }); - - return find(eligibleSorted, { uid: selectedUid }) as Record; - } - - /** - * @method promptReason - prompt for rollback reason unless provided via --reason flag - * - * @memberof Rollback - */ - async promptReason(): Promise { - if (this.flags.reason) { - return this.flags.reason.trim() || undefined; - } - const input = await ux.inquire({ - type: 'input', - name: 'reason', - message: 'Reason (saved to audit log) — press enter to skip:', - }); - const trimmed = (input || '').trim(); - return trimmed ? trimmed : undefined; - } - - /** - * @method printSelectStep - mirror the UI "select" step heading and table - * - * @memberof Rollback - */ - printSelectStep(environment: any, currentLive: any, eligibleSorted: any[]): void { - ux.print(''); - ux.print(chalk.bold.underline('Roll back to previous deployment')); - ux.print(`${chalk.dim('Environment:')} ${chalk.cyan(environment.name)}`); - ux.print(''); - ux.print(chalk.bold('Currently live')); - ux.print(` ${formatDeployment(currentLive)}`); - ux.print(''); - ux.print(chalk.bold('Select a version to restore')); - ux.print(chalk.dim('Choose a previously successful deployment to ensure stability.')); - const count = eligibleSorted.length; - ux.print(chalk.dim(`(${count} eligible deployment${count === 1 ? '' : 's'} available)`)); - ux.print(''); - } - - /** - * @method printReviewStep - mirror the UI "review" step warnings, skips info, and summary - * - * @memberof Rollback - */ - printReviewStep(currentLive: any, target: any, eligibleSorted: any[]): void { - ux.print(''); - ux.print(chalk.bold.underline('Review rollback')); - ux.print(''); - ux.print('You are about to replace your live site with the version below.'); - ux.print('This build will be pushed to the edge immediately.'); - ux.print(''); - ux.print( - `${chalk.yellow.bold('Note:')} The rolled back instance will use the environment variables`, - ); - ux.print(' associated with the selected deployment.'); - - const targetIndex = eligibleSorted.findIndex((d) => d.uid === target.uid); - const skipped = targetIndex > 0 ? eligibleSorted.slice(0, targetIndex) : []; - if (skipped.length > 0) { - const list = skipped.map((d) => `#${d.deploymentNumber}`).join(', '); - const noun = skipped.length === 1 ? 'good deployment' : 'good deployments'; - const verb = skipped.length === 1 ? 'stays' : 'stay'; - ux.print(''); - ux.print( - `${chalk.blue('ⓘ')} Selecting #${target.deploymentNumber} skips ${skipped.length} ${noun} — ${list}`, - ); - ux.print(` ${verb} in history and can be restored later.`); - } - - ux.print(''); - ux.print(` ${chalk.bold('Current Live')} ${formatDeployment(currentLive)}`); - ux.print(` ${chalk.bold('Roll back to')} ${formatDeployment(target)}`); - ux.print(''); - ux.print( - chalk.dim('A new deployment may be initiated if any automations/commits/webhooks are triggered.'), - ); - ux.print(''); - } -} - -function shortHash(hash?: string): string { - return hash ? hash.substring(0, 7) : ''; -} - -function sourceLabel(deployment?: any): string { - if (!deployment) { - return ''; - } - const hash = shortHash(deployment.commitHash); - if (deployment.gitBranch && hash) { - return `${deployment.gitBranch} - ${hash}`; - } - return deployment.gitBranch || hash || ''; -} - -function formatDeployment(deployment?: any): string { - if (!deployment) { - return chalk.dim('(none)'); - } - const number = deployment.deploymentNumber ? `#${deployment.deploymentNumber}` : deployment.uid; - const source = sourceLabel(deployment); - const createdAt = deployment.createdAt || ''; - const numberCol = chalk.green(number.padEnd(6)); - const sourceCol = source ? chalk.cyan(source.padEnd(28)) : ''.padEnd(28); - return `${numberCol} ${sourceCol} ${chalk.dim(createdAt)}`; -} diff --git a/src/graphql/mutation.ts b/src/graphql/mutation.ts index fac8510..97bc3f6 100755 --- a/src/graphql/mutation.ts +++ b/src/graphql/mutation.ts @@ -76,24 +76,8 @@ const importProjectMutation: DocumentNode = gql` } `; -const rollbackDeploymentMutation: DocumentNode = gql` - mutation RollbackDeployment($input: RollbackDeploymentInput!) { - deployment: rollbackDeployment(input: $input) { - uid - status - createdAt - updatedAt - commitHash - commitMessage - deploymentUrl - deploymentNumber - } - } -`; - export { importProjectMutation, createDeploymentMutation, - rollbackDeploymentMutation, createSignedUploadUrlMutation, }; diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index a4a7e7c..c27debe 100755 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -161,14 +161,10 @@ const environmentsQuery: DocumentNode = gql` edges { node { uid - status - gitBranch - commitHash createdAt commitMessage deploymentUrl deploymentNumber - isRollbackEligible } } }