mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-09 21:07:46 +00:00
This adds a new `vc redeploy <url-or-id>` command. It fetches the requested deployment, then performs a redeploy with similar output to `vc deploy` including the ability to pipe the deployment URL into a file or program. ### Redeploy Example: <img width="650" alt="image" src="https://github.com/vercel/vercel/assets/97262/b17fc424-558b-415c-8b74-63e450f4b753"> ### Bad deployment URL: <img width="579" alt="image" src="https://github.com/vercel/vercel/assets/97262/0cb53209-396e-4490-b5d0-744d5d870aaf"> ### No args: <img width="622" alt="image" src="https://github.com/vercel/vercel/assets/97262/cb36d625-991b-41fa-bb49-d7d36c1a201b"> Linear: https://linear.app/vercel/issue/VCCLI-558/cli-new-command-to-redeploy
381 lines
12 KiB
TypeScript
381 lines
12 KiB
TypeScript
import chalk from 'chalk';
|
|
import { client } from '../../mocks/client';
|
|
import { defaultProject, useProject } from '../../mocks/project';
|
|
import { Request, Response } from 'express';
|
|
import rollback from '../../../src/commands/rollback';
|
|
import { RollbackJobStatus, RollbackTarget } from '@vercel-internals/types';
|
|
import { setupUnitFixture } from '../../helpers/setup-unit-fixture';
|
|
import { useDeployment } from '../../mocks/deployment';
|
|
import { useTeams } from '../../mocks/team';
|
|
import { useUser } from '../../mocks/user';
|
|
import sleep from '../../../src/util/sleep';
|
|
|
|
jest.setTimeout(60000);
|
|
|
|
describe('rollback', () => {
|
|
it('should error if cwd is invalid', async () => {
|
|
client.setArgv('rollback', '--cwd', __filename);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput(
|
|
'Error: Support for single file deployments has been removed.'
|
|
);
|
|
|
|
await expect(exitCodePromise).resolves.toEqual(1);
|
|
});
|
|
|
|
it('should error if timeout is invalid', async () => {
|
|
const { cwd } = initRollbackTest();
|
|
client.setArgv('rollback', '--yes', '--cwd', cwd, '--timeout', 'foo');
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Error: Invalid timeout "foo"');
|
|
await expect(exitCodePromise).resolves.toEqual(1);
|
|
});
|
|
|
|
it('should error if invalid deployment name', async () => {
|
|
const { cwd } = initRollbackTest();
|
|
client.setArgv('rollback', '????', '--yes', '--cwd', cwd);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
'Error: The provided argument "????" is not a valid deployment ID or URL'
|
|
);
|
|
await expect(exitCodePromise).resolves.toEqual(1);
|
|
});
|
|
|
|
it('should error if deployment not found', async () => {
|
|
const { cwd } = initRollbackTest();
|
|
client.setArgv('rollback', 'foo', '--yes', '--cwd', cwd);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
'Error: Can\'t find the deployment "foo" under the context'
|
|
);
|
|
|
|
await expect(exitCodePromise).resolves.toEqual(1);
|
|
});
|
|
|
|
it('should show status when not rolling back', async () => {
|
|
const { cwd } = initRollbackTest();
|
|
client.setArgv('rollback', '--yes', '--cwd', cwd);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
'Checking rollback status of vercel-rollback'
|
|
);
|
|
await expect(client.stderr).toOutput('No deployment rollback in progress');
|
|
|
|
await expect(exitCodePromise).resolves.toEqual(0);
|
|
});
|
|
|
|
it('should rollback by deployment id', async () => {
|
|
const { cwd, previousDeployment } = initRollbackTest();
|
|
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
|
|
);
|
|
await expect(client.stderr).toOutput('Rollback in progress');
|
|
await expect(client.stderr).toOutput(
|
|
`Success! ${chalk.bold('vercel-rollback')} was rolled back to ${
|
|
previousDeployment.url
|
|
} (${previousDeployment.id})`
|
|
);
|
|
|
|
await expect(exitCodePromise).resolves.toEqual(0);
|
|
});
|
|
|
|
it('should rollback by deployment url', async () => {
|
|
const { cwd, previousDeployment } = initRollbackTest();
|
|
client.setArgv('rollback', previousDeployment.url, '--yes', '--cwd', cwd);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
`Fetching deployment "${previousDeployment.url}" in ${previousDeployment.creator?.username}`
|
|
);
|
|
await expect(client.stderr).toOutput('Rollback in progress');
|
|
await expect(client.stderr).toOutput(
|
|
`Success! ${chalk.bold('vercel-rollback')} was rolled back to ${
|
|
previousDeployment.url
|
|
} (${previousDeployment.id})`
|
|
);
|
|
|
|
await expect(exitCodePromise).resolves.toEqual(0);
|
|
});
|
|
|
|
it('should get status while rolling back', async () => {
|
|
const { cwd, previousDeployment, project } = initRollbackTest({
|
|
rollbackPollCount: 10,
|
|
});
|
|
|
|
// start the rollback
|
|
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
|
|
rollback(client);
|
|
|
|
// need to wait for the rollback request to be accepted
|
|
await sleep(500);
|
|
|
|
// get the status
|
|
client.setArgv('rollback', '--yes', '--cwd', cwd);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
`Checking rollback status of ${project.name}`
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
`Success! ${chalk.bold('vercel-rollback')} was rolled back to ${
|
|
previousDeployment.url
|
|
} (${previousDeployment.id})`
|
|
);
|
|
|
|
await expect(exitCodePromise).resolves.toEqual(0);
|
|
});
|
|
|
|
it('should error if rollback request fails', async () => {
|
|
const { cwd, previousDeployment } = initRollbackTest({
|
|
rollbackPollCount: 10,
|
|
rollbackStatusCode: 500,
|
|
});
|
|
|
|
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
|
|
const exitCode = await rollback(client);
|
|
|
|
expect(exitCode).toBe(1);
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
|
|
);
|
|
await expect(client.stderr).toOutput('Response Error (500)');
|
|
});
|
|
|
|
it('should error if rollback fails (no aliases)', async () => {
|
|
const { cwd, previousDeployment } = initRollbackTest({
|
|
rollbackJobStatus: 'failed',
|
|
});
|
|
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
|
|
);
|
|
await expect(client.stderr).toOutput('Rollback in progress');
|
|
await expect(client.stderr).toOutput(
|
|
`Error: Failed to remap all aliases to the requested deployment ${previousDeployment.url} (${previousDeployment.id})`
|
|
);
|
|
|
|
await expect(exitCodePromise).resolves.toEqual(1);
|
|
});
|
|
|
|
it('should error if rollback fails (with aliases)', async () => {
|
|
const { cwd, previousDeployment } = initRollbackTest({
|
|
rollbackAliases: [
|
|
{
|
|
alias: { alias: 'foo', deploymentId: 'foo_123' },
|
|
status: 'completed',
|
|
},
|
|
{
|
|
alias: { alias: 'bar', deploymentId: 'bar_123' },
|
|
status: 'failed',
|
|
},
|
|
],
|
|
rollbackJobStatus: 'failed',
|
|
});
|
|
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
|
|
);
|
|
await expect(client.stderr).toOutput('Rollback in progress');
|
|
await expect(client.stderr).toOutput(
|
|
`Error: Failed to remap all aliases to the requested deployment ${previousDeployment.url} (${previousDeployment.id})`
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
` ${chalk.green('completed')} foo (foo_123)`
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
` ${chalk.red('failed')} bar (bar_123)`
|
|
);
|
|
|
|
await expect(exitCodePromise).resolves.toEqual(1);
|
|
});
|
|
|
|
it('should error if deployment times out', async () => {
|
|
const { cwd, previousDeployment } = initRollbackTest({
|
|
rollbackPollCount: 10,
|
|
});
|
|
client.setArgv(
|
|
'rollback',
|
|
previousDeployment.id,
|
|
'--yes',
|
|
'--cwd',
|
|
cwd,
|
|
'--timeout',
|
|
'1s'
|
|
);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
|
|
);
|
|
await expect(client.stderr).toOutput('Rollback in progress');
|
|
await expect(client.stderr).toOutput(
|
|
`The rollback exceeded its deadline - rerun ${chalk.bold(
|
|
`vercel rollback ${previousDeployment.id}`
|
|
)} to try again`
|
|
);
|
|
|
|
await expect(exitCodePromise).resolves.toEqual(1);
|
|
});
|
|
|
|
it('should immediately exit after requesting rollback', async () => {
|
|
const { cwd, previousDeployment } = initRollbackTest();
|
|
client.setArgv(
|
|
'rollback',
|
|
previousDeployment.id,
|
|
'--yes',
|
|
'--cwd',
|
|
cwd,
|
|
'--timeout',
|
|
'0'
|
|
);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
`Successfully requested rollback of ${chalk.bold('vercel-rollback')} to ${
|
|
previousDeployment.url
|
|
} (${previousDeployment.id})`
|
|
);
|
|
|
|
await expect(exitCodePromise).resolves.toEqual(0);
|
|
});
|
|
|
|
it('should error if deployment belongs to different team', async () => {
|
|
const { cwd, previousDeployment } = initRollbackTest();
|
|
previousDeployment.team = {
|
|
id: 'abc',
|
|
name: 'abc',
|
|
slug: 'abc',
|
|
};
|
|
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
|
|
const exitCodePromise = rollback(client);
|
|
|
|
await expect(client.stderr).toOutput('Retrieving project…');
|
|
await expect(client.stderr).toOutput(
|
|
`Fetching deployment "${previousDeployment.id}" in ${previousDeployment.creator?.username}`
|
|
);
|
|
await expect(client.stderr).toOutput(
|
|
'Error: Deployment belongs to a different team'
|
|
);
|
|
|
|
await expect(exitCodePromise).resolves.toEqual(1);
|
|
});
|
|
});
|
|
|
|
type RollbackAlias = {
|
|
alias: {
|
|
alias: string;
|
|
deploymentId: string;
|
|
};
|
|
status: string;
|
|
};
|
|
|
|
function initRollbackTest({
|
|
rollbackAliases = [],
|
|
rollbackJobStatus = 'succeeded',
|
|
rollbackPollCount = 2,
|
|
rollbackStatusCode,
|
|
}: {
|
|
rollbackAliases?: RollbackAlias[];
|
|
rollbackJobStatus?: RollbackJobStatus;
|
|
rollbackPollCount?: number;
|
|
rollbackStatusCode?: number;
|
|
} = {}) {
|
|
const cwd = setupUnitFixture('vercel-rollback');
|
|
const user = useUser();
|
|
useTeams('team_dummy');
|
|
const { project } = useProject({
|
|
...defaultProject,
|
|
id: 'vercel-rollback',
|
|
name: 'vercel-rollback',
|
|
});
|
|
|
|
const currentDeployment = useDeployment({ creator: user });
|
|
const previousDeployment = useDeployment({ creator: user });
|
|
let lastRollbackTarget: RollbackTarget | null = null;
|
|
|
|
client.scenario.post(
|
|
'/:version/projects/:project/rollback/:id',
|
|
(req: Request, res: Response) => {
|
|
const { id } = req.params;
|
|
if (previousDeployment.id !== id) {
|
|
res.statusCode = 404;
|
|
res.json({
|
|
error: { code: 'not_found', message: 'Deployment not found', id },
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (rollbackStatusCode === 500) {
|
|
res.statusCode = 500;
|
|
res.end('Server error');
|
|
return;
|
|
}
|
|
|
|
lastRollbackTarget = {
|
|
fromDeploymentId: currentDeployment.id,
|
|
jobStatus: 'in-progress',
|
|
requestedAt: Date.now(),
|
|
toDeploymentId: id,
|
|
};
|
|
res.statusCode = 201;
|
|
res.end();
|
|
}
|
|
);
|
|
|
|
let counter = 0;
|
|
|
|
client.scenario.get(`/:version/projects/${project.id}`, (req, res) => {
|
|
const data = { ...project };
|
|
if (req.query?.rollbackInfo === 'true') {
|
|
if (lastRollbackTarget && counter++ > rollbackPollCount) {
|
|
lastRollbackTarget.jobStatus = rollbackJobStatus;
|
|
}
|
|
data.lastRollbackTarget = lastRollbackTarget;
|
|
}
|
|
res.json(data);
|
|
});
|
|
|
|
client.scenario.get(
|
|
'/:version/projects/:project/rollback/aliases',
|
|
(req, res) => {
|
|
res.json({
|
|
aliases: rollbackAliases,
|
|
pagination: null,
|
|
});
|
|
}
|
|
);
|
|
|
|
return {
|
|
cwd,
|
|
project,
|
|
currentDeployment,
|
|
previousDeployment,
|
|
};
|
|
}
|