mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-08 21:07:46 +00:00
This PR improves the error handling when a zero config framework has an unexpected output directory. Previously, setting a Docusaurus 2.0 build command to `docusaurus build && mv build foo` would fail with the following: ``` Error: ENOENT: no such file or directory, scandir '/vercel/514ce14b/build' ``` With this PR, the error message will show the expected: ``` Error: No Output Directory named "build" found after the Build completed. You can configure the Output Directory in your project settings. Learn more: https://vercel.com/docs/v2/platform/frequently-asked-questions#missing-public-directory ``` I also changed the usage of [`promisify(fs)`](https://nodejs.org/docs/latest-v10.x/api/util.html#util_util_promisify_original) to [`fs.promises`](https://nodejs.org/docs/latest-v10.x/api/fs.html#fs_fs_promises_api) which is available in Node 10 or newer. Lastly, I updated the test suite to check if the correct error message is returned for builds we expect to fail.
249 lines
6.4 KiB
JavaScript
249 lines
6.4 KiB
JavaScript
const assert = require('assert');
|
|
const { createHash } = require('crypto');
|
|
const path = require('path');
|
|
const _fetch = require('node-fetch');
|
|
const fetch = require('./fetch-retry.js');
|
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
async function nowDeploy(bodies, randomness) {
|
|
const files = Object.keys(bodies)
|
|
.filter(n => n !== 'vercel.json' && n !== 'now.json')
|
|
.map(n => ({
|
|
sha: digestOfFile(bodies[n]),
|
|
size: bodies[n].length,
|
|
file: n,
|
|
mode: path.extname(n) === '.sh' ? 0o100755 : 0o100644,
|
|
}));
|
|
|
|
const { FORCE_BUILD_IN_REGION, NOW_DEBUG, VERCEL_DEBUG } = process.env;
|
|
const nowJson = JSON.parse(bodies['vercel.json'] || bodies['now.json']);
|
|
|
|
const nowDeployPayload = {
|
|
version: 2,
|
|
public: true,
|
|
env: { ...nowJson.env, RANDOMNESS_ENV_VAR: randomness },
|
|
build: {
|
|
env: {
|
|
...(nowJson.build || {}).env,
|
|
RANDOMNESS_BUILD_ENV_VAR: randomness,
|
|
FORCE_BUILD_IN_REGION,
|
|
NOW_DEBUG,
|
|
VERCEL_DEBUG,
|
|
},
|
|
},
|
|
name: 'test2020',
|
|
files,
|
|
builds: nowJson.builds,
|
|
routes: nowJson.routes || [],
|
|
meta: {},
|
|
};
|
|
|
|
console.log(`posting ${files.length} files`);
|
|
|
|
for (const { file: filename } of files) {
|
|
await filePost(bodies[filename], digestOfFile(bodies[filename]));
|
|
}
|
|
|
|
let deploymentId;
|
|
let deploymentUrl;
|
|
|
|
{
|
|
const json = await deploymentPost(nowDeployPayload);
|
|
if (json.error && json.error.code === 'missing_files')
|
|
throw new Error('Missing files');
|
|
deploymentId = json.id;
|
|
deploymentUrl = json.url;
|
|
}
|
|
|
|
console.log('id', deploymentId);
|
|
console.log('deploymentUrl', `https://${deploymentUrl}`);
|
|
|
|
for (let i = 0; i < 750; i += 1) {
|
|
const deployment = await deploymentGet(deploymentId);
|
|
const { readyState } = deployment;
|
|
if (readyState === 'ERROR') {
|
|
const error = new Error(
|
|
`State of https://${deploymentUrl} is ${readyState}`
|
|
);
|
|
error.deployment = deployment;
|
|
throw error;
|
|
}
|
|
if (readyState === 'READY') {
|
|
break;
|
|
}
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
}
|
|
|
|
return { deploymentId, deploymentUrl };
|
|
}
|
|
|
|
function digestOfFile(body) {
|
|
return createHash('sha1')
|
|
.update(body)
|
|
.digest('hex');
|
|
}
|
|
|
|
async function filePost(body, digest) {
|
|
assert(Buffer.isBuffer(body));
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/octet-stream',
|
|
'Content-Length': body.length,
|
|
'x-now-digest': digest,
|
|
'x-now-size': body.length,
|
|
};
|
|
|
|
const url = '/v2/now/files';
|
|
|
|
const resp = await fetchWithAuth(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
});
|
|
|
|
const json = await resp.json();
|
|
|
|
if (json.error) {
|
|
const { status, statusText, headers } = resp;
|
|
const { message } = json.error;
|
|
console.log('Fetch Error', { url, status, statusText, headers, digest });
|
|
throw new Error(message);
|
|
}
|
|
return json;
|
|
}
|
|
|
|
async function deploymentPost(payload) {
|
|
const url = '/v6/now/deployments?forceNew=1';
|
|
const resp = await fetchWithAuth(url, {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
const json = await resp.json();
|
|
|
|
if (json.error) {
|
|
const { status, statusText, headers } = resp;
|
|
const { message } = json.error;
|
|
console.log('Fetch Error', { url, status, statusText, headers });
|
|
throw new Error(message);
|
|
}
|
|
return json;
|
|
}
|
|
|
|
async function deploymentGet(deploymentId) {
|
|
const url = `/v12/now/deployments/${deploymentId}`;
|
|
const resp = await fetchWithAuth(url);
|
|
const json = await resp.json();
|
|
if (json.error) {
|
|
const { status, statusText, headers } = resp;
|
|
const { message } = json.error;
|
|
console.log('Fetch Error', { url, status, statusText, headers });
|
|
throw new Error(message);
|
|
}
|
|
return json;
|
|
}
|
|
|
|
let token;
|
|
let currentCount = 0;
|
|
const MAX_COUNT = 10;
|
|
|
|
async function fetchWithAuth(url, opts = {}) {
|
|
if (!opts.headers) opts.headers = {};
|
|
|
|
if (!opts.headers.Authorization) {
|
|
currentCount += 1;
|
|
if (!token || currentCount === MAX_COUNT) {
|
|
currentCount = 0;
|
|
// used for health checks
|
|
token = process.env.VERCEL_TOKEN || process.env.NOW_TOKEN;
|
|
if (!token) {
|
|
// used by GH Actions
|
|
token = await fetchTokenWithRetry();
|
|
}
|
|
}
|
|
|
|
opts.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
return await fetchApi(url, opts);
|
|
}
|
|
|
|
async function fetchTokenWithRetry(retries = 5) {
|
|
const {
|
|
NOW_TOKEN,
|
|
VERCEL_TOKEN,
|
|
VERCEL_TEAM_TOKEN,
|
|
VERCEL_REGISTRATION_URL,
|
|
} = process.env;
|
|
if (VERCEL_TOKEN || NOW_TOKEN) {
|
|
console.log('Your personal token will be used to make test deployments.');
|
|
return VERCEL_TOKEN || NOW_TOKEN;
|
|
}
|
|
if (!VERCEL_TEAM_TOKEN || !VERCEL_REGISTRATION_URL) {
|
|
throw new Error(
|
|
process.env.CI
|
|
? 'Failed to create test deployment. This is expected for 3rd-party Pull Requests. Please run tests locally.'
|
|
: 'Failed to create test deployment. Please set `VERCEL_TOKEN` environment variable and run again.'
|
|
);
|
|
}
|
|
try {
|
|
const res = await _fetch(VERCEL_REGISTRATION_URL, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${VERCEL_TEAM_TOKEN}`,
|
|
},
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(
|
|
`Unexpected status (${res.status}) from registration: ${text}`
|
|
);
|
|
}
|
|
const data = await res.json();
|
|
if (!data) {
|
|
throw new Error(`Unexpected response from registration: no body`);
|
|
}
|
|
if (!data.token) {
|
|
const text = JSON.stringify(data);
|
|
throw new Error(`Unexpected response from registration: ${text}`);
|
|
}
|
|
return data.token;
|
|
} catch (error) {
|
|
console.log(`Failed to fetch token. Retries remaining: ${retries}`);
|
|
if (retries === 0) {
|
|
console.log(error);
|
|
throw error;
|
|
}
|
|
await sleep(500);
|
|
return fetchTokenWithRetry(retries - 1);
|
|
}
|
|
}
|
|
|
|
async function fetchApi(url, opts = {}) {
|
|
const apiHost = process.env.API_HOST || 'api.vercel.com';
|
|
const urlWithHost = `https://${apiHost}${url}`;
|
|
const { method = 'GET', body } = opts;
|
|
|
|
if (process.env.VERBOSE) {
|
|
console.log('fetch', method, url);
|
|
if (body) console.log(encodeURIComponent(body).slice(0, 80));
|
|
}
|
|
|
|
if (!opts.headers) opts.headers = {};
|
|
|
|
if (!opts.headers.Accept) {
|
|
opts.headers.Accept = 'application/json';
|
|
}
|
|
|
|
opts.headers['x-now-trace-priority'] = '1';
|
|
|
|
return await fetch(urlWithHost, opts);
|
|
}
|
|
|
|
module.exports = {
|
|
fetchApi,
|
|
fetchWithAuth,
|
|
nowDeploy,
|
|
fetchTokenWithRetry,
|
|
};
|