[cli] New vc redeploy command (#9956)

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
This commit is contained in:
Chris Barber
2023-05-22 17:08:49 -05:00
committed by GitHub
parent 8de42e0a70
commit cdf55b3b1a
18 changed files with 627 additions and 273 deletions

View File

@@ -4,7 +4,7 @@ import { parse } from 'dotenv';
import env from '../../../src/commands/env';
import { setupUnitFixture } from '../../helpers/setup-unit-fixture';
import { client } from '../../mocks/client';
import { defaultProject, useProject } from '../../mocks/project';
import { defaultProject, envs, useProject } from '../../mocks/project';
import { useTeams } from '../../mocks/team';
import { useUser } from '../../mocks/user';
@@ -308,21 +308,26 @@ describe('env', () => {
try {
useUser();
useTeams('team_dummy');
defaultProject.env.push({
type: 'encrypted',
id: '781dt89g8r2h789g',
key: 'NEW_VAR',
value: '"testvalue"',
target: ['development'],
configurationId: null,
updatedAt: 1557241361455,
createdAt: 1557241361455,
});
useProject({
...defaultProject,
id: 'env-pull-delta-quotes',
name: 'env-pull-delta-quotes',
});
useProject(
{
...defaultProject,
id: 'env-pull-delta-quotes',
name: 'env-pull-delta-quotes',
},
[
...envs,
{
type: 'encrypted',
id: '781dt89g8r2h789g',
key: 'NEW_VAR',
value: '"testvalue"',
target: ['development'],
configurationId: null,
updatedAt: 1557241361455,
createdAt: 1557241361455,
},
]
);
client.setArgv('env', 'pull', '--yes', '--cwd', cwd);
const pullPromise = env(client);
@@ -336,7 +341,6 @@ describe('env', () => {
} finally {
client.setArgv('env', 'rm', 'NEW_VAR', '--yes', '--cwd', cwd);
await env(client);
defaultProject.env.pop();
}
});
@@ -345,21 +349,26 @@ describe('env', () => {
try {
useUser();
useTeams('team_dummy');
defaultProject.env.push({
type: 'encrypted',
id: '781dt89g8r2h789g',
key: 'NEW_VAR',
value: 'testvalue',
target: ['development'],
configurationId: null,
updatedAt: 1557241361455,
createdAt: 1557241361455,
});
useProject({
...defaultProject,
id: 'env-pull-delta-quotes',
name: 'env-pull-delta-quotes',
});
useProject(
{
...defaultProject,
id: 'env-pull-delta-quotes',
name: 'env-pull-delta-quotes',
},
[
...envs,
{
type: 'encrypted',
id: '781dt89g8r2h789g',
key: 'NEW_VAR',
value: 'testvalue',
target: ['development'],
configurationId: null,
updatedAt: 1557241361455,
createdAt: 1557241361455,
},
]
);
client.setArgv('env', 'pull', '.env.testquotes', '--yes', '--cwd', cwd);
const pullPromise = env(client);
@@ -373,7 +382,6 @@ describe('env', () => {
} finally {
client.setArgv('env', 'rm', 'NEW_VAR', '--yes', '--cwd', cwd);
await env(client);
defaultProject.env.pop();
}
});
});

View File

@@ -0,0 +1,103 @@
import { client } from '../../mocks/client';
import { defaultProject, useProject } from '../../mocks/project';
import redeploy from '../../../src/commands/redeploy';
import { setupUnitFixture } from '../../helpers/setup-unit-fixture';
import { useDeployment } from '../../mocks/deployment';
import { useTeams } from '../../mocks/team';
import { useUser } from '../../mocks/user';
describe('redeploy', () => {
it('should error if missing deployment url', async () => {
client.setArgv('redeploy');
const exitCodePromise = redeploy(client);
await expect(client.stderr).toOutput(
'Missing required deployment id or url:'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should error if deployment not found', async () => {
initRedeployTest();
client.setArgv('redeploy', 'foo');
const exitCodePromise = redeploy(client);
await expect(client.stderr).toOutput('Fetching deployment "foo" in ');
await expect(client.stderr).toOutput(
'Error: Can\'t find the deployment "foo" under the context'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should error if deployment belongs to another team', async () => {
const { fromDeployment } = initRedeployTest();
fromDeployment.team = {
id: 'abc',
name: 'abc',
slug: 'abc',
};
client.setArgv('rollback', fromDeployment.id);
const exitCodePromise = redeploy(client);
await expect(client.stderr).toOutput(
`Fetching deployment "${fromDeployment.id}" in ${fromDeployment.creator?.username}`
);
await expect(client.stderr).toOutput(
'Error: Deployment belongs to a different team'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
it('should redeploy an existing deployment', async () => {
const { fromDeployment } = initRedeployTest();
client.setArgv('rollback', fromDeployment.id);
const exitCodePromise = redeploy(client);
await expect(client.stderr).toOutput(
`Fetching deployment "${fromDeployment.id}" in ${fromDeployment.creator?.username}`
);
await expect(client.stderr).toOutput('Production');
await expect(exitCodePromise).resolves.toEqual(0);
});
it('should redeploy and not wait for completion', async () => {
const { fromDeployment, toDeployment } = initRedeployTest();
toDeployment.readyState = 'QUEUED';
client.setArgv('rollback', fromDeployment.id, '--no-wait');
const exitCodePromise = redeploy(client);
await expect(client.stderr).toOutput(
`Fetching deployment "${fromDeployment.id}" in ${fromDeployment.creator?.username}`
);
await expect(client.stderr).toOutput(
'Note: Deployment is still processing'
);
await expect(exitCodePromise).resolves.toEqual(0);
});
});
function initRedeployTest() {
setupUnitFixture('commands/redeploy/simple-static');
const user = useUser();
useTeams('team_dummy');
const { project } = useProject({
...defaultProject,
id: 'vercel-redeploy',
name: 'vercel-redeploy',
});
const fromDeployment = useDeployment({ creator: user });
const toDeployment = useDeployment({ creator: user });
client.scenario.post(`/v13/deployments`, (req, res) => {
res.json(toDeployment);
});
return {
project,
fromDeployment,
toDeployment,
};
}

View File

@@ -40,7 +40,7 @@ describe('rollback', () => {
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput(
'Error: The provided argument "????" is not a valid deployment or project'
'Error: The provided argument "????" is not a valid deployment ID or URL'
);
await expect(exitCodePromise).resolves.toEqual(1);
});
@@ -51,9 +51,8 @@ describe('rollback', () => {
const exitCodePromise = rollback(client);
await expect(client.stderr).toOutput('Retrieving project…');
await expect(client.stderr).toOutput('Fetching deployment "foo" in ');
await expect(client.stderr).toOutput(
'Error: Error: Can\'t find the deployment "foo" under the context'
'Error: Can\'t find the deployment "foo" under the context'
);
await expect(exitCodePromise).resolves.toEqual(1);
@@ -147,14 +146,14 @@ describe('rollback', () => {
});
client.setArgv('rollback', previousDeployment.id, '--yes', '--cwd', cwd);
const exitCodePromise = rollback(client);
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(exitCodePromise).rejects.toThrow('Response Error (500)');
await expect(client.stderr).toOutput('Response Error (500)');
});
it('should error if rollback fails (no aliases)', async () => {
@@ -222,7 +221,7 @@ describe('rollback', () => {
'--cwd',
cwd,
'--timeout',
'2s'
'1s'
);
const exitCodePromise = rollback(client);