Compare commits

..

5 Commits

Author SHA1 Message Date
Chris Barber
a036b03398 Publish Stable
- vercel@28.7.0
 - @vercel/frameworks@1.1.14
 - @vercel/fs-detectors@3.5.4
 - @vercel/next@3.3.1
 - @vercel/static-build@1.0.41
2022-11-30 15:52:38 -06:00
Chris Barber
1a6a030df5 [cli] Add vc rollback command (#8942)
The `vc rollback` command provides the ability to redeploy a previous
deployment and check the status of a rollback request.

#### Requesting a rollback

    vc rollback <id | url>

Upon requesting a rollback, the command will being a status polling loop
for 3 minutes. This timeout can be adjusted via the `--timeout <value>`
option which accepts time formats such as `30s` or `2m`. A timeout of
`0` (zero) will skip the status polling and immediately exit after
successfully requesting a rollback.

#### Querying rollback status 

    vc rollback
    vc rollback status

The `status` action will return the most recent rollback info within the
last 3 minutes.

### Related Issues

>
https://linear.app/vercel/issue/HIT-117/cli-add-support-for-vc-rollback-deployid
>
https://linear.app/vercel/issue/HIT-118/cli-add-support-for-vc-rollback-[status]

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [ ] The code changed/added as part of this PR has been covered with
tests
- [ ] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a
reviewer
- [ ] Issue from task tracker has a link to this PR
2022-11-30 14:08:06 -06:00
Kiko Beats
fc8b68eda2 edge: format MB with sepace separator (#8990)
The Edge Function size limit is declared in KiB, not in KB:


d7654e2252/packages/next/src/edge-function-source/constants.ts (L8)

And `pretty-bytes` doesn't support base-2 as input:

```js
require('pretty-bytes')(1024 * 1024* 4) // => '4.19 MB' :(
```

Instead, `bytes` is used:

```js
require('bytes')(1024 * 1024* 4, { unitSeparator: ' ' }) // => '4 MB' 🙂
```
2022-11-30 17:38:29 +01:00
Kiko Beats
9ecc89a3c7 [next] increase edge functions limit to 4MB (#8989)
Roll-up x4 more user code size for Edge Functions 🙂
2022-11-30 00:32:11 +00:00
Luc Leray
2a4e066163 [frameworks] Fix astro default routes since it is not SPA (#8200)
The default routes for Astro are redirecting all non-existing files to
`index.html`, which means that the `404.html` page is not used, and
instead `index.html` is shown for all not found pages.

Astro outputs files for each page (ie. `about.html`, `blog.html`, ...)
so the `{handle: 'filesystem'}` route should be enough to route all
existing pages correctly.

The missing part to ship this fix is to answer the following question:
can we safely assume that Astro will always output a `404.html` file?

Internal ref: https://github.com/vercel/customer-issues/issues/638

Co-authored-by: Okiki Ojo <okikio.dev@gmail.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Steven <steven@ceriously.com>
2022-11-29 15:05:31 -05:00
38 changed files with 1103 additions and 77 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "28.6.0",
"version": "28.7.0",
"preferGlobal": true,
"license": "Apache-2.0",
"description": "The command-line interface for Vercel",
@@ -44,13 +44,13 @@
"@vercel/build-utils": "5.6.0",
"@vercel/go": "2.2.18",
"@vercel/hydrogen": "0.0.32",
"@vercel/next": "3.3.0",
"@vercel/next": "3.3.1",
"@vercel/node": "2.7.0",
"@vercel/python": "3.1.28",
"@vercel/redwood": "1.0.38",
"@vercel/remix": "1.1.0",
"@vercel/ruby": "1.3.44",
"@vercel/static-build": "1.0.40",
"@vercel/static-build": "1.0.41",
"update-notifier": "5.1.0"
},
"devDependencies": {
@@ -97,8 +97,8 @@
"@types/yauzl-promise": "2.1.0",
"@vercel/client": "12.2.20",
"@vercel/error-utils": "1.0.3",
"@vercel/frameworks": "1.1.13",
"@vercel/fs-detectors": "3.5.3",
"@vercel/frameworks": "1.1.14",
"@vercel/fs-detectors": "3.5.4",
"@vercel/fun": "1.0.4",
"@vercel/ncc": "0.24.0",
"@zeit/source-map-support": "0.6.2",

View File

@@ -23,6 +23,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
rollback [url|id] Quickly revert back to a previous deployment [beta]
switch [scope] Switches between teams and your personal account
${chalk.dim('Advanced')}

View File

@@ -33,6 +33,7 @@ export default async function dev(
if (link.status === 'not_linked' && !process.env.__VERCEL_SKIP_DEV_CMD) {
link = await setupAndLink(client, cwd, {
autoConfirm: opts['--yes'],
link,
successEmoji: 'link',
setupMsg: 'Set up and develop',
});

View File

@@ -29,6 +29,7 @@ export default new Map([
['pull', 'pull'],
['remove', 'remove'],
['rm', 'remove'],
['rollback', 'rollback'],
['secret', 'secrets'],
['secrets', 'secrets'],
['switch', 'teams'],

View File

@@ -147,6 +147,7 @@ export default async function main(client: Client) {
if (status === 'not_linked' && !app) {
const linkedProject = await ensureLink('list', client, path, {
autoConfirm,
link,
});
if (typeof linkedProject === 'number') {
return linkedProject;

View File

@@ -18,7 +18,7 @@ import getDeploymentsByProjectId, {
import { getPkgName, getCommandName } from '../util/pkg-name';
import getArgs from '../util/get-args';
import handleError from '../util/handle-error';
import Client from '../util/client';
import type Client from '../util/client';
import { Output } from '../util/output';
import { Alias, Project } from '../types';
import { NowError } from '../util/now-error';

View File

@@ -0,0 +1,122 @@
import chalk from 'chalk';
import type Client from '../util/client';
import { ensureLink } from '../util/link/ensure-link';
import getArgs from '../util/get-args';
import { getPkgName } from '../util/pkg-name';
import handleError from '../util/handle-error';
import logo from '../util/output/logo';
import ms from 'ms';
import requestRollback from '../util/rollback/request-rollback';
import rollbackStatus from '../util/rollback/status';
import validatePaths from '../util/validate-paths';
const help = () => {
console.log(`
${chalk.bold(
`${logo} ${getPkgName()} rollback`
)} [deploymentId|deploymentName]
Quickly revert back to 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]
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
--timeout=${chalk.bold.underline(
'TIME'
)} Time to wait for rollback completion [3m]
-y, --yes Skip questions when setting up new project using default scope and settings
${chalk.dim('Examples:')}
${chalk.gray('')} Show the status of any current pending rollbacks
${chalk.cyan(`$ ${getPkgName()} rollback`)}
${chalk.cyan(`$ ${getPkgName()} rollback status`)}
${chalk.cyan(`$ ${getPkgName()} rollback status --timeout 30s`)}
${chalk.gray('')} Rollback a deployment using id or url
${chalk.cyan(`$ ${getPkgName()} rollback <deployment id/url>`)}
`);
};
/**
* `vc rollback` 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), {
'--debug': Boolean,
'-d': '--debug',
'--timeout': String,
'--yes': Boolean,
'-y': '--yes',
});
} catch (err) {
handleError(err);
return 1;
}
if (argv['--help'] || argv._[0] === 'help') {
help();
return 2;
}
// ensure the current directory is good
const cwd = argv['--cwd'] || process.cwd();
const pathValidation = await validatePaths(client, [cwd]);
if (!pathValidation.valid) {
return pathValidation.exitCode;
}
// ensure the current directory is a linked project
const linkedProject = await ensureLink(
'rollback',
client,
pathValidation.path,
{
autoConfirm: Boolean(argv['--yes']),
}
);
if (typeof linkedProject === 'number') {
return linkedProject;
}
// validate the timeout
let timeout = argv['--timeout'];
if (timeout && ms(timeout) === undefined) {
client.output.error(`Invalid timeout "${timeout}"`);
return 1;
}
const { project } = linkedProject;
const actionOrDeployId = argv._[1] || 'status';
if (actionOrDeployId === 'status') {
return await rollbackStatus({
client,
project,
timeout,
});
}
return await requestRollback({
client,
deployId: actionOrDeployId,
project,
timeout,
});
};

View File

@@ -174,7 +174,7 @@ const main = async () => {
const targetOrSubcommand = argv._[2];
// Currently no beta commands - add here as needed
const betaCommands: string[] = [];
const betaCommands: string[] = ['rollback'];
if (betaCommands.includes(targetOrSubcommand)) {
console.log(
`${chalk.grey(
@@ -555,6 +555,9 @@ const main = async () => {
case 'remove':
func = require('./commands/remove').default;
break;
case 'rollback':
func = require('./commands/rollback').default;
break;
case 'secrets':
func = require('./commands/secrets').default;
break;

View File

@@ -287,6 +287,7 @@ export interface Project extends ProjectSettings {
link?: ProjectLinkData;
alias?: ProjectAliasTarget[];
latestDeployments?: Partial<Deployment>[];
lastRollbackTarget: RollbackTarget | null;
}
export interface Org {
@@ -321,6 +322,20 @@ export type ProjectLinkResult =
| 'MISSING_PROJECT_SETTINGS';
};
export type RollbackJobStatus =
| 'pending'
| 'in-progress'
| 'succeeded'
| 'failed'
| 'skipped';
export interface RollbackTarget {
fromDeploymentId: string;
jobStatus: RollbackJobStatus;
requestedAt: number;
toDeploymentId: string;
}
export interface Token {
id: string;
name: string;

View File

@@ -1,4 +1,4 @@
import Client from '../client';
import type Client from '../client';
import toHost from '../to-host';
import { Deployment } from '../../types';
import {

View File

@@ -571,7 +571,7 @@ export class DeploymentNotFound extends NowError<
super({
code: 'DEPLOYMENT_NOT_FOUND',
meta: { id, context },
message: `Can't find the deployment ${id} under the context ${context}`,
message: `Can't find the deployment "${id}" under the context "${context}"`,
});
}
}

View File

@@ -32,7 +32,11 @@ export async function ensureLink(
cwd: string,
opts: SetupAndLinkOptions
): Promise<LinkResult | number> {
let link = await getLinkedProject(client, cwd);
let { link } = opts;
if (!link) {
link = await getLinkedProject(client, cwd);
opts.link = link;
}
if (
(link.status === 'linked' && opts.forceDelete) ||

View File

@@ -30,8 +30,9 @@ import Now, { CreateOptions } from '../index';
import { isAPIError } from '../errors-ts';
export interface SetupAndLinkOptions {
forceDelete?: boolean;
autoConfirm?: boolean;
forceDelete?: boolean;
link?: ProjectLinkResult;
successEmoji?: EmojiLabel;
setupMsg?: string;
projectName?: string;
@@ -41,8 +42,9 @@ export default async function setupAndLink(
client: Client,
path: string,
{
forceDelete = false,
autoConfirm = false,
forceDelete = false,
link,
successEmoji = 'link',
setupMsg = 'Set up',
projectName,
@@ -56,7 +58,9 @@ export default async function setupAndLink(
output.error(`Expected directory but found file: ${path}`);
return { status: 'error', exitCode: 1, reason: 'PATH_IS_FILE' };
}
const link = await getLinkedProject(client, path);
if (!link) {
link = await getLinkedProject(client, path);
}
const isTTY = client.stdin.isTTY;
const quiet = !isTTY;
let rootDirectory: string | null = null;

View File

@@ -186,15 +186,14 @@ export async function getLinkedProject(
})})\n`
);
return { status: 'error', exitCode: 1 };
} else {
output.print(
prependEmoji(
'Your Project was either deleted, transferred to a new Team, or you dont have access to it anymore.\n',
emoji('warning')
)
);
}
output.print(
prependEmoji(
'Your Project was either deleted, transferred to a new Team, or you dont have access to it anymore.\n',
emoji('warning')
)
);
return { status: 'not_linked', org: null, project: null };
}

View File

@@ -0,0 +1,38 @@
import type Client from '../client';
import type { Deployment } from '../../types';
import getDeploymentByIdOrHost from '../deploy/get-deployment-by-id-or-host';
import handleCertError from '../certs/handle-cert-error';
/**
* Attempts to find the deployment by name or id.
* @param {Client} client - The Vercel client instance
* @param {string} contextName - The scope name
* @param {string} deployId - The deployment name or id to rollback
* @returns {Promise<Deployment>} Resolves an exit code or deployment info
*/
export default async function getDeploymentInfo(
client: Client,
contextName: string,
deployId: string
): Promise<Deployment> {
const deployment = handleCertError(
client.output,
await getDeploymentByIdOrHost(client, contextName, deployId)
);
if (deployment === 1) {
throw new Error(
`Failed to get deployment "${deployId}" in scope "${contextName}"`
);
}
if (deployment instanceof Error) {
throw deployment;
}
if (!deployment) {
throw new Error(`Couldn't find the deployment "${deployId}"`);
}
return deployment;
}

View File

@@ -0,0 +1,19 @@
import chalk from 'chalk';
/**
* Stylize the alias status label.
* @param {AliasStatus} status - The status label
* @returns {string}
*/
export default function renderAliasStatus(status: string): string {
if (status === 'completed') {
return chalk.green(status);
}
if (status === 'failed') {
return chalk.red(status);
}
if (status === 'skipped') {
return chalk.gray(status);
}
return chalk.yellow(status);
}

View File

@@ -0,0 +1,85 @@
import chalk from 'chalk';
import type Client from '../client';
import { getCommandName } from '../pkg-name';
import getDeploymentInfo from './get-deployment-info';
import getScope from '../get-scope';
import { isValidName } from '../is-valid-name';
import ms from 'ms';
import type { Project } from '../../types';
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 {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,
project,
timeout,
}: {
client: Client;
deployId: string;
project: Project;
timeout?: string;
}): Promise<number> {
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;
}
output.spinner(
`Fetching deployment "${deployId}" in ${chalk.bold(contextName)}`
);
let deployment;
try {
deployment = await getDeploymentInfo(client, contextName, deployId);
} catch (err: any) {
output.error(err?.toString() || err);
return 1;
} finally {
output.stopSpinner();
// re-render the spinner text because it goes so fast
output.log(
`Fetching deployment "${deployId}" in ${chalk.bold(contextName)}`
);
}
// create the rollback
await client.fetch<any>(
`/v9/projects/${project.id}/rollback/${deployment.uid}`,
{
body: {}, // required
method: 'POST',
}
);
if (timeout !== undefined && ms(timeout) === 0) {
output.log(
`Successfully requested rollback of ${chalk.bold(project.name)} to ${
deployment.url
} (${deployment.uid})`
);
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,255 @@
import chalk from 'chalk';
import type Client from '../client';
import type {
Deployment,
PaginationOptions,
Project,
RollbackTarget,
} from '../../types';
import elapsed from '../output/elapsed';
import formatDate from '../format-date';
import getDeploymentInfo from './get-deployment-info';
import getScope from '../get-scope';
import ms from 'ms';
import renderAliasStatus from './render-alias-status';
import sleep from '../sleep';
interface RollbackAlias {
alias: {
alias: string;
deploymentId: string;
};
id: string;
status: 'completed' | 'in-progress' | 'pending' | 'failed';
}
interface RollbackAliasesResponse {
aliases: RollbackAlias[];
pagination: PaginationOptions;
}
/**
* Continuously checks a deployment status until it has succeeded, failed, or
* taken longer than the timeout (default 3 minutes).
* @param {Client} client - The Vercel client instance
* @param {string} [contextName] - The scope name; if not specified, it will be
* extracted from the `client`
* @param {Deployment} [deployment] - Info about the deployment which is used
* to display different output following a rollback request
* @param {Project} project - Project info instance
* @param {string} [timeout] - Milliseconds to poll for succeeded/failed state
* @returns {Promise<number>} Resolves an exit code; 0 on success
*/
export default async function rollbackStatus({
client,
contextName,
deployment,
project,
timeout = '3m',
}: {
client: Client;
contextName?: string;
deployment?: Deployment;
project: Project;
timeout?: string;
}): Promise<number> {
const { output } = client;
const recentThreshold = Date.now() - ms('3m');
const rollbackTimeout = Date.now() + ms(timeout);
let counter = 0;
let spinnerMessage = deployment
? 'Rollback in progress'
: `Checking rollback status of ${project.name}`;
const check = async () => {
const { lastRollbackTarget } = await client.fetch<any>(
`/v9/projects/${project.id}?rollbackInfo=true`
);
return lastRollbackTarget;
};
if (!contextName) {
({ contextName } = await getScope(client));
}
try {
output.spinner(`${spinnerMessage}`);
// continuously loop until the rollback has explicitly succeeded, failed,
// or timed out
for (;;) {
const { jobStatus, requestedAt, toDeploymentId }: RollbackTarget =
(await check()) ?? {};
if (
!jobStatus ||
(jobStatus !== 'in-progress' && jobStatus !== 'pending')
) {
output.stopSpinner();
output.log(`${spinnerMessage}`);
}
if (!jobStatus || requestedAt < recentThreshold) {
output.log('No deployment rollback in progress');
return 0;
}
if (jobStatus === 'skipped') {
output.log('Rollback was skipped');
return 0;
}
if (jobStatus === 'succeeded') {
return await renderJobSucceeded({
client,
contextName,
performingRollback: !!deployment,
requestedAt,
project,
toDeploymentId,
});
}
if (jobStatus === 'failed') {
return await renderJobFailed({
client,
contextName,
deployment,
project,
toDeploymentId,
});
}
// lastly, if we're not pending/in-progress, then we don't know what
// the status is, so bail
if (jobStatus !== 'pending' && jobStatus !== 'in-progress') {
output.log(`Unknown rollback status "${jobStatus}"`);
return 1;
}
// check if we have been running for too long
if (requestedAt < recentThreshold || Date.now() >= rollbackTimeout) {
output.log(
`The rollback exceeded its deadline - rerun ${chalk.bold(
`vercel rollback ${toDeploymentId}`
)} to try again`
);
return 1;
}
// if we've done our first poll and not rolling back, then print the
// requested at date/time
if (counter++ === 0 && !deployment) {
spinnerMessage += ` requested at ${formatDate(requestedAt)}`;
}
output.spinner(`${spinnerMessage}`);
await sleep(250);
}
} finally {
output.stopSpinner();
}
}
async function renderJobFailed({
client,
contextName,
deployment,
project,
toDeploymentId,
}: {
client: Client;
contextName: string;
deployment?: Deployment;
project: Project;
toDeploymentId: string;
}) {
const { output } = client;
try {
const name = (
deployment ||
(await getDeploymentInfo(client, contextName, toDeploymentId))
)?.url;
output.error(
`Failed to remap all aliases to the requested deployment ${name} (${toDeploymentId})`
);
} catch (e) {
output.error(
`Failed to remap all aliases to the requested deployment ${toDeploymentId}`
);
}
// aliases are paginated, so continuously loop until all of them have been
// fetched
let nextTimestamp;
for (;;) {
let url = `/v9/projects/${project.id}/rollback/aliases?failedOnly=true&limit=20`;
if (nextTimestamp) {
url += `&until=${nextTimestamp}`;
}
const { aliases, pagination } = await client.fetch<RollbackAliasesResponse>(
url
);
for (const { alias, status } of aliases) {
output.log(
` ${renderAliasStatus(status).padEnd(11)} ${alias.alias} (${
alias.deploymentId
})`
);
}
if (pagination?.next) {
nextTimestamp = pagination.next;
} else {
break;
}
}
return 1;
}
async function renderJobSucceeded({
client,
contextName,
performingRollback,
project,
requestedAt,
toDeploymentId,
}: {
client: Client;
contextName: string;
performingRollback: boolean;
project: Project;
requestedAt: number;
toDeploymentId: string;
}) {
const { output } = client;
let deploymentInfo = '';
try {
const deployment = await getDeploymentInfo(
client,
contextName,
toDeploymentId
);
deploymentInfo = `${chalk.bold(deployment.url)} (${toDeploymentId})`;
} catch (err: any) {
output.debug(
`Failed to get deployment url for ${toDeploymentId}: ${
err?.toString() || err
}`
);
deploymentInfo = chalk.bold(toDeploymentId);
}
const duration = performingRollback ? elapsed(Date.now() - requestedAt) : '';
output.log(
`Success! ${chalk.bold(
project.name
)} was rolled back to ${deploymentInfo} ${duration}`
);
return 0;
}

View File

@@ -143,6 +143,7 @@ async function runNpmInstall(fixturePath) {
await execa('yarn', ['install'], {
cwd: fixturePath,
shell: true,
stdio: 'inherit',
});
}
}

View File

@@ -0,0 +1,3 @@
.next
yarn.lock
!.vercel

View File

@@ -0,0 +1,4 @@
{
"orgId": "team_dummy",
"projectId": "vercel-rollback"
}

View File

@@ -0,0 +1,12 @@
{
"scripts": {
"build": "next build",
"dev": "next",
"now-build": "next build"
},
"dependencies": {
"next": "^8.0.0",
"react": "^16.7.0",
"react-dom": "^16.7.0"
}
}

View File

@@ -0,0 +1,11 @@
import { withRouter } from 'next/router';
function Index({ router }) {
const data = {
pathname: router.pathname,
query: router.query,
};
return <div>{JSON.stringify(data)}</div>;
}
export default withRouter(Index);

View File

@@ -0,0 +1,10 @@
{
"version": 2,
"name": "vercel-rollback",
"routes": [
{
"src": "/(.*)",
"dest": "/index?route-param=b"
}
]
}

View File

@@ -140,6 +140,51 @@ function setupDeploymentEndpoints() {
res.json(deployment);
});
client.scenario.get('/v5/now/deployments/:id', (req, res) => {
const { id } = req.params;
const { url } = req.query;
let deployment;
if (id === 'get') {
if (typeof url !== 'string') {
res.statusCode = 400;
return res.json({ error: { code: 'bad_request' } });
}
deployment = Array.from(deployments.values()).find(d => {
return d.url === url;
});
} else {
// lookup by ID
deployment = deployments.get(id);
}
if (!deployment) {
res.statusCode = 404;
return res.json({
error: { code: 'not_found', message: 'Deployment not found', id },
});
}
res.json({
uid: deployment.id,
url: deployment.url,
name: '',
type: 'LAMBDAS',
state: 'READY',
version: deployment.version,
created: deployment.createdAt,
ready: deployment.ready,
buildingAt: deployment.buildingAt,
creator: {
uid: deployment.creator?.uid,
username: deployment.creator?.username,
},
target: deployment.target,
ownerId: undefined, // ?
projectId: undefined, // ?
inspectorUrl: deployment.inspectorUrl,
meta: {},
alias: deployment.alias,
});
});
client.scenario.get('/:version/deployments/:id/builds', (req, res) => {
const { id } = req.params;
const deployment = deployments.get(id);

View File

@@ -23,36 +23,42 @@ export async function toOutput(
return new Promise(resolve => {
let output = '';
let timeoutId = setTimeout(onTimeout, timeout);
const message = () => {
const labelExpected = 'Expected output';
const labelReceived = 'Received output';
const printLabel = getLabelPrinter(labelExpected, labelReceived);
const hint =
matcherHint(matcherName, 'stream', 'test', matcherHintOptions) + '\n\n';
return (
hint +
printLabel(labelExpected) +
(isNot ? 'not ' : '') +
printExpected(test) +
'\n' +
printLabel(labelReceived) +
(isNot ? ' ' : '') +
printReceived(output)
);
};
const hint =
matcherHint(matcherName, 'stream', 'test', matcherHintOptions) + '\n\n';
function onData(data: string) {
output += data;
if (output.includes(test)) {
cleanup();
resolve({ pass: true, message });
resolve({
pass: true,
message() {
const labelExpected = 'Expected output';
const labelReceived = 'Received output';
const printLabel = getLabelPrinter(labelExpected, labelReceived);
return (
hint +
printLabel(labelExpected) +
(isNot ? 'not ' : '') +
printExpected(test) +
'\n' +
printLabel(labelReceived) +
(isNot ? ' ' : '') +
printReceived(output)
);
},
});
}
}
function onTimeout() {
cleanup();
resolve({ pass: false, message });
resolve({
pass: false,
message() {
return `${hint}Timed out waiting ${timeout} ms for output`;
},
});
}
function cleanup() {

View File

@@ -130,6 +130,7 @@ export const defaultProject = {
userId: 'K4amb7K9dAt5R2vBJWF32bmY',
},
],
lastRollbackTarget: null,
alias: [
{
domain: 'foobar.com',

View File

@@ -0,0 +1,372 @@
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 '../../../src/types';
import { setupFixture } from '../../helpers/setup-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 or project'
);
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('Fetching deployment "foo" in ');
await expect(client.stderr).toOutput(
'Error: 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 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(exitCodePromise).rejects.toThrow('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',
'2s'
);
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);
});
});
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 = setupFixture('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(`/v9/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/now/deployments/get`, (req, res) => {
const { url } = req.query;
if (url === previousDeployment.url) {
res.json({ id: previousDeployment.id });
} else {
res.statusCode = 404;
res.json({
error: { code: 'not_found', message: 'Deployment not found' },
});
}
});
client.scenario.get(
'/:version/projects/:project/rollback/aliases',
(req, res) => {
res.json({
aliases: rollbackAliases,
pagination: null,
});
}
);
return {
cwd,
project,
currentDeployment,
previousDeployment,
};
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/frameworks",
"version": "1.1.13",
"version": "1.1.14",
"main": "./dist/frameworks.js",
"types": "./dist/frameworks.d.ts",
"files": [

View File

@@ -267,17 +267,10 @@ export const frameworks = [
getOutputDirName: async () => 'dist',
defaultRoutes: [
{
src: '^/dist/(.*)$',
src: '^/assets/(.*)$',
headers: { 'cache-control': 'public, max-age=31536000, immutable' },
continue: true,
},
{
handle: 'filesystem',
},
{
src: '/(.*)',
dest: '/index.html',
},
],
},
{

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/fs-detectors",
"version": "3.5.3",
"version": "3.5.4",
"description": "Vercel filesystem detectors",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -20,7 +20,7 @@
},
"dependencies": {
"@vercel/error-utils": "1.0.3",
"@vercel/frameworks": "1.1.13",
"@vercel/frameworks": "1.1.14",
"@vercel/routing-utils": "2.1.3",
"glob": "8.0.3",
"js-yaml": "4.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/next",
"version": "3.3.0",
"version": "3.3.1",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/runtimes#official-runtimes/next-js",
@@ -34,6 +34,7 @@
"devDependencies": {
"@types/aws-lambda": "8.10.19",
"@types/buffer-crc32": "0.2.0",
"@types/bytes": "3.1.1",
"@types/convert-source-map": "1.5.2",
"@types/find-up": "4.0.0",
"@types/fs-extra": "8.0.0",
@@ -49,6 +50,7 @@
"@vercel/routing-utils": "2.1.3",
"async-sema": "3.0.1",
"buffer-crc32": "0.2.13",
"bytes": "3.1.2",
"cheerio": "1.0.0-rc.10",
"convert-source-map": "1.8.0",
"esbuild": "0.12.22",

View File

@@ -4,4 +4,4 @@ const MIB = 1024 * KIB;
/**
* The maximum size of a *compressed* edge function.
*/
export const EDGE_FUNCTION_SIZE_LIMIT = MIB;
export const EDGE_FUNCTION_SIZE_LIMIT = 4 * MIB;

View File

@@ -6,7 +6,7 @@ import { join } from 'path';
import { EDGE_FUNCTION_SIZE_LIMIT } from './constants';
import zlib from 'zlib';
import { promisify } from 'util';
import bytes from 'pretty-bytes';
import { prettyBytes } from '../utils';
// @ts-expect-error this is a prebuilt file, based on `../../scripts/build-edge-function-template.js`
import template from '../../dist/___get-nextjs-edge-function.js';
@@ -44,7 +44,9 @@ export async function getNextjsEdgeFunctionSource(
* We validate at this point because we want to verify against user code.
* It should not count the Worker wrapper nor the Next.js wrapper.
*/
const wasmFiles = (wasm ?? []).map(({ filePath }) => join(outputDir, filePath));
const wasmFiles = (wasm ?? []).map(({ filePath }) =>
join(outputDir, filePath)
);
await validateSize(text, wasmFiles);
// Wrap to fake module.exports
@@ -83,9 +85,9 @@ async function validateSize(script: string, wasmFiles: string[]) {
const gzipped = await gzip(content);
if (gzipped.length > EDGE_FUNCTION_SIZE_LIMIT) {
throw new Error(
`Exceeds maximum edge function size: ${bytes(
`Exceeds maximum edge function size: ${prettyBytes(
gzipped.length
)} / ${bytes(EDGE_FUNCTION_SIZE_LIMIT)}`
)} / ${prettyBytes(EDGE_FUNCTION_SIZE_LIMIT)}`
);
}
}

View File

@@ -39,9 +39,13 @@ import { getNextjsEdgeFunctionSource } from './edge-function-source/get-edge-fun
import type { LambdaOptionsWithFiles } from '@vercel/build-utils/dist/lambda';
import { stringifySourceMap } from './sourcemapped';
import type { RawSourceMap } from 'source-map';
import bytes from 'bytes';
type stringMap = { [key: string]: string };
const _prettyBytes = (n: number) => bytes(n, { unitSeparator: ' ' });
export { _prettyBytes as prettyBytes }
// Identify /[param]/ in route string
// eslint-disable-next-line no-useless-escape
const TEST_DYNAMIC_ROUTE = /\/\[[^\/]+?\](?=\/|$)/;

View File

@@ -14,7 +14,7 @@ it('should throw an error when exceeds the script size limit', async () => {
`
module.exports.middleware = function () {
return Response(${JSON.stringify({
text: randomBytes(1200000).toString('base64'),
text: randomBytes(4200 * 1024).toString('base64'),
})})
}
`
@@ -51,27 +51,27 @@ it('throws an error if it contains too big WASM file', async () => {
);
const wasmPath = join(dir, 'big.wasm');
await writeFile(wasmPath, randomBytes(1200 * 1024));
await writeFile(wasmPath, randomBytes(4200 * 1024));
expect(async () => {
await getNextjsEdgeFunctionSource(
[file],
{
name: 'middleware',
staticRoutes: [],
nextConfig: null,
},
dir,
[
await getNextjsEdgeFunctionSource(
[file],
{
name: 'wasm_big',
filePath: 'big.wasm',
name: 'middleware',
staticRoutes: [],
nextConfig: null,
},
]
dir,
[
{
name: 'wasm_big',
filePath: 'big.wasm',
},
]
);
}).rejects.toThrow(
/Exceeds maximum edge function size: .+[MK]B \/ .+[M|K]B/i
);
}).rejects.toThrow(
/Exceeds maximum edge function size: .+[MK]B \/ .+[M|K]B/i
);
});
it('uses the template', async () => {
@@ -107,5 +107,7 @@ it('uses the template', async () => {
);
const source = edgeFunctionSource.source();
expect(source).toMatch(/nextConfig/);
expect(source).toContain(`const wasm_small = require("/wasm/wasm_small.wasm")`);
expect(source).toContain(
`const wasm_small = require("/wasm/wasm_small.wasm")`
);
});

View File

@@ -1,6 +1,6 @@
{
"name": "@vercel/static-build",
"version": "1.0.40",
"version": "1.0.41",
"license": "MIT",
"main": "./dist/index",
"homepage": "https://vercel.com/docs/build-step",
@@ -37,7 +37,7 @@
"@types/node-fetch": "2.5.4",
"@types/promise-timeout": "1.3.0",
"@vercel/build-utils": "5.6.0",
"@vercel/frameworks": "1.1.13",
"@vercel/frameworks": "1.1.14",
"@vercel/ncc": "0.24.0",
"@vercel/routing-utils": "2.1.3",
"@vercel/static-config": "2.0.6",

View File

@@ -2626,6 +2626,11 @@
resolved "https://registry.yarnpkg.com/@types/bytes/-/bytes-3.0.0.tgz#549eeacd0a8fecfaa459334583a4edcee738e6db"
integrity sha512-ZF43+CIIlzngQe8/Zo7L1kpY9W8O6rO006VDz3c5iM21ddtXWxCEyOXyft+q4pVF2tGqvrVuVrEDH1+gJEi1fQ==
"@types/bytes@3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@types/bytes/-/bytes-3.1.1.tgz#67a876422e660dc4c10a27f3e5bcfbd5455f01d0"
integrity sha512-lOGyCnw+2JVPKU3wIV0srU0NyALwTBJlVSx5DfMQOFuuohA8y9S8orImpuIQikZ0uIQ8gehrRjxgQC1rLRi11w==
"@types/chance@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.1.3.tgz#d19fe9391288d60fdccd87632bfc9ab2b4523fea"
@@ -4395,6 +4400,11 @@ bytes@3.1.0:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
cac@^6.7.12:
version "6.7.12"
resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.12.tgz#6fb5ea2ff50bd01490dbda497f4ae75a99415193"