[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

@@ -0,0 +1,6 @@
---
'@vercel/client': minor
'vercel': minor
---
New `vc redeploy` command

View File

@@ -157,6 +157,7 @@ export type Deployment = {
errorLink?: string;
errorMessage?: string | null;
errorStep?: string;
forced?: boolean;
functions?: BuilderFunctions | null;
gitSource?: {
org?: string;
@@ -183,6 +184,7 @@ export type Deployment = {
ownerId?: string;
plan?: 'enterprise' | 'hobby' | 'oss' | 'pro';
previewCommentsEnabled?: boolean;
private?: boolean;
projectId?: string;
projectSettings?: {
buildCommand?: string | null;

View File

@@ -25,6 +25,7 @@ export const help = () => `
login [email] Logs into your account or creates a new one
logout Logs out of your account
pull [path] Pull your Project Settings from the cloud
redeploy [url|id] Rebuild and deploy a previous deployment.
rollback [url|id] Quickly revert back to a previous deployment
switch [scope] Switches between teams and your personal account

View File

@@ -21,7 +21,6 @@ import stamp from '../../util/output/stamp';
import createDeploy from '../../util/deploy/create-deploy';
import getDeployment from '../../util/get-deployment';
import parseMeta from '../../util/parse-meta';
import linkStyle from '../../util/output/link';
import param from '../../util/output/param';
import {
BuildsRateLimited,
@@ -59,7 +58,6 @@ import validatePaths, {
validateRootDirectory,
} from '../../util/validate-paths';
import { getCommandName } from '../../util/pkg-name';
import { getPreferredPreviewURL } from '../../util/deploy/get-preferred-preview-url';
import { Output } from '../../util/output';
import { help } from './args';
import { getDeploymentChecks } from '../../util/deploy/get-deployment-checks';
@@ -70,8 +68,7 @@ import { isValidArchive } from '../../util/deploy/validate-archive-format';
import { parseEnv } from '../../util/parse-env';
import { errorToString, isErrnoException, isError } from '@vercel/error-utils';
import { pickOverrides } from '../../util/projects/project-settings';
import { isDeploying } from '../../util/deploy/is-deploying';
import type { Deployment } from '@vercel-internals/types';
import { printDeploymentStatus } from '../../util/deploy/print-deployment-status';
export default async (client: Client): Promise<number> => {
const { output } = client;
@@ -728,7 +725,7 @@ export default async (client: Client): Promise<number> => {
return 1;
}
return printDeploymentStatus(output, client, deployment, deployStamp, noWait);
return printDeploymentStatus(client, deployment, deployStamp, noWait);
};
function handleCreateDeployError(
@@ -835,112 +832,3 @@ const addProcessEnv = async (
}
}
};
const printDeploymentStatus = async (
output: Output,
client: Client,
{
readyState,
alias: aliasList,
aliasError,
target,
indications,
url: deploymentUrl,
aliasWarning,
}: {
readyState: Deployment['readyState'];
alias: string[];
aliasError: Error;
target: string;
indications: any;
url: string;
aliasWarning?: {
code: string;
message: string;
link?: string;
action?: string;
};
},
deployStamp: () => string,
noWait: boolean
) => {
indications = indications || [];
const isProdDeployment = target === 'production';
let isStillBuilding = false;
if (noWait) {
if (isDeploying(readyState)) {
isStillBuilding = true;
output.print(
prependEmoji(
'Note: Deployment is still processing...',
emoji('notice')
) + '\n'
);
}
}
if (!isStillBuilding && readyState !== 'READY') {
output.error(
`Your deployment failed. Please retry later. More: https://err.sh/vercel/deployment-error`
);
return 1;
}
if (aliasError) {
output.warn(
`Failed to assign aliases${
aliasError.message ? `: ${aliasError.message}` : ''
}`
);
} else {
// print preview/production url
let previewUrl: string;
// if `noWait` is true, then use the deployment url, not an alias
if (!noWait && Array.isArray(aliasList) && aliasList.length > 0) {
const previewUrlInfo = await getPreferredPreviewURL(client, aliasList);
if (previewUrlInfo) {
previewUrl = previewUrlInfo.previewUrl;
} else {
previewUrl = `https://${deploymentUrl}`;
}
} else {
// fallback to deployment url
previewUrl = `https://${deploymentUrl}`;
}
output.print(
prependEmoji(
`${isProdDeployment ? 'Production' : 'Preview'}: ${chalk.bold(
previewUrl
)} ${deployStamp()}`,
emoji('success')
) + `\n`
);
}
if (aliasWarning?.message) {
indications.push({
type: 'warning',
payload: aliasWarning.message,
link: aliasWarning.link,
action: aliasWarning.action,
});
}
const newline = '\n';
for (let indication of indications) {
const message =
prependEmoji(chalk.dim(indication.payload), emoji(indication.type)) +
newline;
let link = '';
if (indication.link)
link =
chalk.dim(
`${indication.action || 'Learn More'}: ${linkStyle(indication.link)}`
) + newline;
output.print(message + link);
}
return 0;
};

View File

@@ -27,6 +27,7 @@ export default new Map([
['project', 'project'],
['projects', 'project'],
['pull', 'pull'],
['redeploy', 'redeploy'],
['remove', 'remove'],
['rm', 'remove'],
['rollback', 'rollback'],

View File

@@ -0,0 +1,204 @@
import chalk from 'chalk';
import { checkDeploymentStatus } from '@vercel/client';
import type Client from '../util/client';
import { emoji, prependEmoji } from '../util/emoji';
import getArgs from '../util/get-args';
import { getCommandName, getPkgName } from '../util/pkg-name';
import { getDeploymentByIdOrURL } from '../util/deploy/get-deployment-by-id-or-url';
import getScope from '../util/get-scope';
import handleError from '../util/handle-error';
import { isErrnoException } from '@vercel/error-utils';
import logo from '../util/output/logo';
import Now from '../util';
import { printDeploymentStatus } from '../util/deploy/print-deployment-status';
import stamp from '../util/output/stamp';
import ua from '../util/ua';
import type { VercelClientOptions } from '@vercel/client';
const help = () => {
console.log(`
${chalk.bold(
`${logo} ${getPkgName()} redeploy`
)} [deploymentId|deploymentName]
Rebuild and deploy a previous deployment.
${chalk.dim('Options:')}
-h, --help Output usage information
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`vercel.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off]
--no-color No color mode [off]
--no-wait Don't wait for the redeploy to finish
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
-y, --yes Skip questions when setting up new project using default scope and settings
${chalk.dim('Examples:')}
${chalk.gray('')} Rebuild and deploy an existing deployment using id or url
${chalk.cyan(`$ ${getPkgName()} redeploy my-deployment.vercel.app`)}
${chalk.gray('')} Write Deployment URL to a file
${chalk.cyan(
`$ ${getPkgName()} redeploy my-deployment.vercel.app > deployment-url.txt`
)}
`);
};
/**
* `vc redeploy` command
* @param {Client} client
* @returns {Promise<number>} Resolves an exit code; 0 on success
*/
export default async (client: Client): Promise<number> => {
let argv;
try {
argv = getArgs(client.argv.slice(2), {
'--no-wait': Boolean,
'--yes': Boolean,
'-y': '--yes',
});
} catch (err) {
handleError(err);
return 1;
}
if (argv['--help'] || argv._[0] === 'help') {
help();
return 2;
}
const { output } = client;
const deployIdOrUrl = argv._[1];
if (!deployIdOrUrl) {
output.error(
`Missing required deployment id or url: ${getCommandName(
`redeploy <deployment-id-or-url>`
)}`
);
return 1;
}
const { contextName } = await getScope(client);
const noWait = !!argv['--no-wait'];
try {
const fromDeployment = await getDeploymentByIdOrURL({
client,
contextName,
deployIdOrUrl,
});
const deployStamp = stamp();
output.spinner(`Redeploying project ${fromDeployment.id}`, 0);
let deployment = await client.fetch<any>(`/v13/deployments?forceNew=1`, {
body: {
deploymentId: fromDeployment.id,
meta: {
action: 'redeploy',
},
name: fromDeployment.name,
target: fromDeployment.target || 'production',
},
method: 'POST',
});
output.stopSpinner();
output.print(
`${prependEmoji(
`Inspect: ${chalk.bold(deployment.inspectorUrl)} ${deployStamp()}`,
emoji('inspect')
)}\n`
);
if (!client.stdout.isTTY) {
client.stdout.write(`https://${deployment.url}`);
}
if (!noWait) {
output.spinner(
deployment.readyState === 'QUEUED' ? 'Queued' : 'Building',
0
);
if (deployment.readyState === 'READY' && deployment.aliasAssigned) {
output.spinner('Completing', 0);
} else {
try {
const clientOptions: VercelClientOptions = {
agent: client.agent,
apiUrl: client.apiUrl,
debug: client.output.debugEnabled,
path: '', // unused by checkDeploymentStatus()
teamId: fromDeployment.team?.id,
token: client.authConfig.token!,
userAgent: ua,
};
for await (const event of checkDeploymentStatus(
deployment,
clientOptions
)) {
if (event.type === 'building') {
output.spinner('Building', 0);
} else if (
event.type === 'ready' &&
((event.payload as any).checksState
? (event.payload as any).checksState === 'completed'
: true)
) {
output.spinner('Completing', 0);
} else if (event.type === 'checks-running') {
output.spinner('Running Checks', 0);
} else if (
event.type === 'alias-assigned' ||
event.type === 'checks-conclusion-failed'
) {
output.stopSpinner();
deployment = event.payload;
break;
} else if (event.type === 'canceled') {
output.stopSpinner();
output.print('The deployment has been canceled.\n');
return 1;
} else if (event.type === 'error') {
output.stopSpinner();
const now = new Now({
client,
currentTeam: fromDeployment.team?.id,
});
const error = await now.handleDeploymentError(event.payload, {
env: {},
});
throw error;
}
}
} catch (err: unknown) {
output.prettyError(err);
process.exit(1);
}
}
}
return printDeploymentStatus(client, deployment, deployStamp, noWait);
} catch (err: unknown) {
output.prettyError(err);
if (isErrnoException(err) && err.code === 'ERR_INVALID_TEAM') {
output.error(
`Use ${chalk.bold('vc switch')} to change your current team`
);
}
return 1;
}
};

View File

@@ -116,7 +116,7 @@ export default async (client: Client): Promise<number> => {
return await requestRollback({
client,
deployId: actionOrDeployId,
deployIdOrUrl: actionOrDeployId,
project,
timeout,
});

View File

@@ -574,6 +574,9 @@ const main = async () => {
case 'pull':
func = require('./commands/pull').default;
break;
case 'redeploy':
func = require('./commands/redeploy').default;
break;
case 'remove':
func = require('./commands/remove').default;
break;

View File

@@ -0,0 +1,89 @@
import chalk from 'chalk';
import getDeployment from '../get-deployment';
import getTeamById from '../teams/get-team-by-id';
import { isValidName } from '../is-valid-name';
import type Client from '../client';
import type { Deployment, Team } from '@vercel-internals/types';
/**
* Renders feedback while retrieving a deployment, then validates the
* deployment belongs to the current team.
*
* @param client - The CLI client instance.
* @param contextName - The context/team name.
* @param deployIdOrUrl - The deployment id or URL.
* @returns The deployment info.
*/
export async function getDeploymentByIdOrURL({
client,
contextName,
deployIdOrUrl,
}: {
client: Client;
contextName: string;
deployIdOrUrl: string;
}): Promise<Deployment> {
const { config, output } = client;
if (!isValidName(deployIdOrUrl)) {
throw new Error(
`The provided argument "${deployIdOrUrl}" is not a valid deployment ID or URL`
);
}
let deployment: Deployment;
let team: Team | undefined;
try {
output.spinner(
`Fetching deployment "${deployIdOrUrl}" in ${chalk.bold(contextName)}`
);
const [teamResult, deploymentResult] = await Promise.allSettled([
config.currentTeam ? getTeamById(client, config.currentTeam) : undefined,
getDeployment(client, contextName, deployIdOrUrl),
]);
if (teamResult.status === 'rejected') {
throw new Error(
`Failed to retrieve team information: ${teamResult.reason}`
);
}
if (deploymentResult.status === 'rejected') {
throw new Error(deploymentResult.reason.message);
}
team = teamResult.value;
deployment = deploymentResult.value;
// re-render the spinner text because it goes so fast
output.log(
`Fetching deployment "${deployIdOrUrl}" in ${chalk.bold(contextName)}`
);
} finally {
output.stopSpinner();
}
if (deployment.team?.id) {
if (!team || deployment.team.id !== team.id) {
const err: NodeJS.ErrnoException = new Error(
team
? `Deployment doesn't belong to current team ${chalk.bold(
contextName
)}`
: `Deployment belongs to a different team`
);
err.code = 'ERR_INVALID_TEAM';
throw err;
}
} else if (team) {
const err: NodeJS.ErrnoException = new Error(
`Deployment doesn't belong to current team ${chalk.bold(contextName)}`
);
err.code = 'ERR_INVALID_TEAM';
throw err;
}
return deployment;
}

View File

@@ -0,0 +1,117 @@
import chalk from 'chalk';
import type Client from '../client';
import type { Deployment } from '@vercel-internals/types';
import { getPreferredPreviewURL } from '../../util/deploy/get-preferred-preview-url';
import { isDeploying } from '../../util/deploy/is-deploying';
import linkStyle from '../output/link';
import { prependEmoji, emoji } from '../../util/emoji';
export async function printDeploymentStatus(
client: Client,
{
readyState,
alias: aliasList,
aliasError,
target,
indications,
url: deploymentUrl,
aliasWarning,
}: {
readyState: Deployment['readyState'];
alias: string[];
aliasError: Error;
target: string;
indications: any;
url: string;
aliasWarning?: {
code: string;
message: string;
link?: string;
action?: string;
};
},
deployStamp: () => string,
noWait: boolean
): Promise<number> {
const { output } = client;
indications = indications || [];
const isProdDeployment = target === 'production';
let isStillBuilding = false;
if (noWait) {
if (isDeploying(readyState)) {
isStillBuilding = true;
output.print(
prependEmoji(
'Note: Deployment is still processing...',
emoji('notice')
) + '\n'
);
}
}
if (!isStillBuilding && readyState !== 'READY') {
output.error(
`Your deployment failed. Please retry later. More: https://err.sh/vercel/deployment-error`
);
return 1;
}
if (aliasError) {
output.warn(
`Failed to assign aliases${
aliasError.message ? `: ${aliasError.message}` : ''
}`
);
} else {
// print preview/production url
let previewUrl: string;
// if `noWait` is true, then use the deployment url, not an alias
if (!noWait && Array.isArray(aliasList) && aliasList.length > 0) {
const previewUrlInfo = await getPreferredPreviewURL(client, aliasList);
if (previewUrlInfo) {
previewUrl = previewUrlInfo.previewUrl;
} else {
previewUrl = `https://${deploymentUrl}`;
}
} else {
// fallback to deployment url
previewUrl = `https://${deploymentUrl}`;
}
output.print(
prependEmoji(
`${isProdDeployment ? 'Production' : 'Preview'}: ${chalk.bold(
previewUrl
)} ${deployStamp()}`,
emoji('success')
) + `\n`
);
}
if (aliasWarning?.message) {
indications.push({
type: 'warning',
payload: aliasWarning.message,
link: aliasWarning.link,
action: aliasWarning.action,
});
}
const newline = '\n';
for (let indication of indications) {
const message =
prependEmoji(chalk.dim(indication.payload), emoji(indication.type)) +
newline;
let link = '';
if (indication.link)
link =
chalk.dim(
`${indication.action || 'Learn More'}: ${linkStyle(indication.link)}`
) + newline;
output.print(message + link);
}
return 0;
}

View File

@@ -10,7 +10,7 @@ import mapCertError from './certs/map-cert-error';
import toHost from './to-host';
/**
* Retrieves a v13 deployment.
* Retrieves a deployment by id or URL.
*
* @param client - The Vercel CLI client instance.
* @param contextName - The scope context/team name.

View File

@@ -1,124 +1,75 @@
import chalk from 'chalk';
import type Client from '../client';
import type { Deployment, Project, Team } from '@vercel-internals/types';
import type { Project } from '@vercel-internals/types';
import { getCommandName } from '../pkg-name';
import getDeployment from '../get-deployment';
import { getDeploymentByIdOrURL } from '../deploy/get-deployment-by-id-or-url';
import getScope from '../get-scope';
import getTeamById from '../teams/get-team-by-id';
import { isValidName } from '../is-valid-name';
import { isErrnoException } from '@vercel/error-utils';
import ms from 'ms';
import rollbackStatus from './status';
/**
* Requests a rollback and waits for it complete.
* @param {Client} client - The Vercel client instance
* @param {string} deployId - The deployment name or id to rollback
* @param {string} deployIdOrUrl - The deployment name or id to rollback
* @param {Project} project - Project info instance
* @param {string} [timeout] - Time to poll for succeeded/failed state
* @returns {Promise<number>} Resolves an exit code; 0 on success
*/
export default async function requestRollback({
client,
deployId,
deployIdOrUrl,
project,
timeout,
}: {
client: Client;
deployId: string;
deployIdOrUrl: string;
project: Project;
timeout?: string;
}): Promise<number> {
const { config, output } = client;
const { output } = client;
const { contextName } = await getScope(client);
if (!isValidName(deployId)) {
output.error(
`The provided argument "${deployId}" is not a valid deployment or project`
);
return 1;
}
let deployment: Deployment;
let team: Team | undefined;
try {
output.spinner(
`Fetching deployment "${deployId}" in ${chalk.bold(contextName)}`
);
const deployment = await getDeploymentByIdOrURL({
client,
contextName,
deployIdOrUrl,
});
const [teamResult, deploymentResult] = await Promise.allSettled([
config.currentTeam ? getTeamById(client, config.currentTeam) : undefined,
getDeployment(client, contextName, deployId),
]);
// create the rollback
await client.fetch(`/v9/projects/${project.id}/rollback/${deployment.id}`, {
body: {}, // required
method: 'POST',
});
if (teamResult.status === 'rejected') {
output.error(`Failed to retrieve team information: ${teamResult.reason}`);
return 1;
}
if (deploymentResult.status === 'rejected') {
output.error(deploymentResult.reason);
return 1;
}
team = teamResult.value;
deployment = deploymentResult.value;
// re-render the spinner text because it goes so fast
output.log(
`Fetching deployment "${deployId}" in ${chalk.bold(contextName)}`
);
} finally {
output.stopSpinner();
}
if (deployment.team?.id) {
if (!team || deployment.team.id !== team.id) {
output.error(
team
? `Deployment doesn't belong to current team ${chalk.bold(
contextName
)}`
: `Deployment belongs to a different team`
if (timeout !== undefined && ms(timeout) === 0) {
output.log(
`Successfully requested rollback of ${chalk.bold(project.name)} to ${
deployment.url
} (${deployment.id})`
);
output.log(
`To check rollback status, run ${getCommandName('rollback')}.`
);
return 0;
}
// check the status
return await rollbackStatus({
client,
contextName,
deployment,
project,
timeout,
});
} catch (err: unknown) {
output.prettyError(err);
if (isErrnoException(err) && err.code === 'ERR_INVALID_TEAM') {
output.error(
`Use ${chalk.bold('vc switch')} to change your current team`
);
return 1;
}
} else if (team) {
output.error(
`Deployment doesn't belong to current team ${chalk.bold(contextName)}`
);
output.error(`Use ${chalk.bold('vc switch')} to change your current team`);
return 1;
}
// create the rollback
await client.fetch<any>(
`/v9/projects/${project.id}/rollback/${deployment.id}`,
{
body: {}, // required
method: 'POST',
}
);
if (timeout !== undefined && ms(timeout) === 0) {
output.log(
`Successfully requested rollback of ${chalk.bold(project.name)} to ${
deployment.url
} (${deployment.id})`
);
output.log(`To check rollback status, run ${getCommandName('rollback')}.`);
return 0;
}
// check the status
return await rollbackStatus({
client,
contextName,
deployment,
project,
timeout,
});
}

View File

@@ -0,0 +1 @@
test

View File

@@ -8,7 +8,7 @@ import { formatProvider } from '../../src/util/git/connect-git-provider';
import { parseEnvironment } from '../../src/commands/pull';
import type { Env } from '@vercel/build-utils';
const envs: ProjectEnvVariable[] = [
export const envs: ProjectEnvVariable[] = [
{
type: 'encrypted',
id: '781dt89g8r2h789g',
@@ -101,34 +101,12 @@ const systemEnvs = [
},
];
export const defaultProject = {
export const defaultProject: Project = {
id: 'foo',
name: 'cli',
accountId: 'K4amb7K9dAt5R2vBJWF32bmY',
createdAt: 1555413045188,
updatedAt: 1555413045188,
env: envs,
targets: {
production: {
alias: ['foobar.com'],
aliasAssigned: 1571239348998,
createdAt: 1571239348998,
createdIn: 'sfo1',
deploymentHostname: 'a-project-name-rjtr4pz3f',
forced: false,
id: 'dpl_89qyp1cskzkLrVicDaZoDbjyHuDJ',
meta: {},
plan: 'pro',
private: true,
readyState: 'READY',
requestedAt: 1571239348998,
target: 'production',
teamId: null,
type: 'LAMBDAS',
url: 'a-project-name-rjtr4pz3f.vercel.app',
userId: 'K4amb7K9dAt5R2vBJWF32bmY',
},
},
latestDeployments: [
{
alias: ['foobar.com'],
@@ -136,18 +114,18 @@ export const defaultProject = {
buildingAt: 1571239348998,
createdAt: 1571239348998,
createdIn: 'sfo1',
creator: {
uid: 'K4amb7K9dAt5R2vBJWF32bmY',
},
forced: false,
id: 'dpl_89qyp1cskzkLrVicDaZoDbjyHuDJ',
meta: {},
plan: 'pro',
private: true,
readyState: 'READY',
requestedAt: 1571239348998,
target: 'production',
teamId: null,
type: undefined,
url: 'a-project-name-rjtr4pz3f.vercel.app',
userId: 'K4amb7K9dAt5R2vBJWF32bmY',
},
],
lastRollbackTarget: null,
@@ -223,7 +201,10 @@ export function useUnknownProject() {
});
}
export function useProject(project: Partial<Project> = defaultProject) {
export function useProject(
project: Partial<Project> = defaultProject,
projectEnvs: ProjectEnvVariable[] = envs
) {
client.scenario.get(`/v8/projects/${project.name}`, (_req, res) => {
res.json(project);
});
@@ -241,7 +222,6 @@ export function useProject(project: Partial<Project> = defaultProject) {
typeof req.params.target === 'string'
? parseEnvironment(req.params.target)
: undefined;
let projectEnvs = envs;
if (target) {
projectEnvs = projectEnvs.filter(env => {
if (!env.target) return false;

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);

View File

@@ -1,5 +1,6 @@
import buildCreateDeployment from './create-deployment';
export { checkDeploymentStatus } from './check-deployment-status';
export { getVercelIgnore, buildFileTree } from './utils/index';
export const createDeployment = buildCreateDeployment();
export * from './errors';