Compare commits

...

1 Commits

Author SHA1 Message Date
tknickman
d44ca0d7c5 update 2023-06-08 14:25:12 -04:00
5 changed files with 282 additions and 1 deletions

View File

@@ -134,7 +134,7 @@ const help = () => {
export default async function main(client: Client): Promise<number> {
let { cwd } = client;
const { output } = client;
const { output, spaces } = client;
// Ensure that `vc build` is not being invoked recursively
if (process.env.__VERCEL_BUILD_RUNNING) {
@@ -231,6 +231,8 @@ export default async function main(client: Client): Promise<number> {
project = await readProjectSettings(vercelDir);
}
await spaces.createRun({ project });
// Delete output directory from potential previous build
const defaultOutputDir = join(cwd, projectRootDirectory, OUTPUT_DIR);
const outputDir = argv['--output']
@@ -566,6 +568,12 @@ async function doBuild(
// Store the build result to generate the final `config.json` after
// all builds have completed
buildResults.set(build, buildResult);
await client.spaces.createTask({
key: `${builderPkg.name}:${builder.version}`,
name: `${builderPkg.name}:build`,
workspace: pkg?.name ?? 'unknown',
log: 'Coming Soon',
});
// Start flushing the file outputs to the filesystem asynchronously
ops.push(

View File

@@ -624,6 +624,9 @@ const main = async () => {
.send();
}
} catch (err: unknown) {
// close any inprogress runs
await client.spaces.finishRun({ exitCode: 1 });
if (isErrnoException(err) && err.code === 'ENOTFOUND') {
// Error message will look like the following:
// "request to https://api.vercel.com/v2/user failed, reason: getaddrinfo ENOTFOUND api.vercel.com"
@@ -704,6 +707,9 @@ const main = async () => {
metric.event(eventCategory, `${exitCode}`, pkg.version).send();
}
client.stopLogCapture();
await client.spaces.finishRun({ exitCode });
return exitCode;
};

View File

@@ -26,6 +26,8 @@ import { APIError } from './errors-ts';
import { normalizeError } from '@vercel/error-utils';
import type { Agent } from 'http';
import sleep from './sleep';
import { interceptor } from './interceptor';
import Spaces from './spaces';
const isSAMLError = (v: any): v is SAMLError => {
return v && v.saml;
@@ -48,6 +50,7 @@ export interface ClientOptions extends Stdio {
localConfig?: VercelConfig;
localConfigPath?: string;
agent?: Agent;
spaceId?: string;
}
export const isJSONObject = (v: any): v is JSONObject => {
@@ -68,6 +71,12 @@ export default class Client extends EventEmitter implements Stdio {
localConfigPath?: string;
prompt!: inquirer.PromptModule;
requestIdCounter: number;
spaces: Spaces;
logs: {
restore?: () => void;
stdout: string[];
stderr: string[];
};
constructor(opts: ClientOptions) {
super();
@@ -83,7 +92,14 @@ export default class Client extends EventEmitter implements Stdio {
this.localConfig = opts.localConfig;
this.localConfigPath = opts.localConfigPath;
this.requestIdCounter = 1;
this.spaces = new Spaces({ client: this, spaceId: opts.spaceId });
this.logs = {
stdout: [],
stderr: [],
};
this._createPromptModule();
this._startLogCapture();
}
retry<T>(fn: RetryFunction<T>, { retries = 3, maxTimeout = Infinity } = {}) {
@@ -242,4 +258,19 @@ export default class Client extends EventEmitter implements Stdio {
set cwd(v: string) {
process.chdir(v);
}
_startLogCapture() {
this.logs.restore = interceptor({
stdout: log => this.logs.stdout.push(log.toString()),
stderr: log => this.logs.stderr.push(log.toString()),
});
}
stopLogCapture() {
this.logs.restore?.();
this.logs = {
stdout: [],
stderr: [],
};
}
}

View File

@@ -0,0 +1,48 @@
import { WriteStream } from 'tty';
type InterceptReceiver = (data: Buffer) => void;
export function interceptor({
stdout,
stderr,
opts = {
passthrough: true,
},
}: {
stdout: InterceptReceiver;
stderr: InterceptReceiver;
opts?: {
passthrough?: boolean;
};
}): () => void {
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
const originalStderrWrite = process.stderr.write.bind(process.stderr);
function injectedWrite(
stream: WriteStream,
callback: (data: Buffer) => void
) {
return (
buffer: Buffer | string,
encoding?: any,
cb?: () => void
): boolean => {
const data =
typeof buffer === 'string' ? Buffer.from(buffer, encoding) : buffer;
callback(data);
if (opts.passthrough) {
return originalStdoutWrite.call(stream, buffer, encoding, cb);
}
return true;
};
}
process.stdout.write = injectedWrite(process.stdout, stdout);
process.stderr.write = injectedWrite(process.stderr, stderr);
return () => {
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
};
}

View File

@@ -0,0 +1,188 @@
import fetch, { Headers } from 'node-fetch';
import pkg from '../../../package.json';
import Client from '../client';
import ua from '../ua';
import { ProjectLinkAndSettings } from '../projects/project-settings';
const HOST = 'https://vercel.com/api';
class Run {
id: string;
url: string;
startTime = Date.now();
constructor({ id, url }: { id: string; url: string }) {
this.id = id;
this.url = url;
}
}
/*
Ordering of requests:
1. Create run (POST)
2. Create tasks (POST)
3. Finish run (PATCH)
*/
export default class Spaces {
version: string = 'v0';
type: string = 'VERCEL-CLI';
spaceId: string;
client: Client;
run: Run | undefined;
constructor({ spaceId, client }: { spaceId?: string; client: Client }) {
// TODO: remove default spaceId (vercel-site)
this.spaceId = spaceId || 'space_FwOT2idkmZ5PIDvgK5xY3YpD';
this.client = client;
}
private runsEndpoint({ runId }: { runId?: string } = {}) {
if (runId) {
return `spaces/${this.spaceId}/runs/${runId}`;
}
return `spaces/${this.spaceId}/runs`;
}
private tasksEndpoint({ runId }: { runId: string }) {
return `spaces/${this.spaceId}/runs/${runId}/tasks`;
}
private async gitMetaData() {
return {
gitBranch: 'TODO',
gitSha: 'TODO',
};
}
private async fetch({
method,
body,
endpoint,
}: {
method: 'POST' | 'PATCH';
body: string;
endpoint: string;
}) {
const url = new URL(`${HOST}/${this.version}/${endpoint}`);
if (this.client.config.currentTeam) {
url.searchParams.set('teamId', this.client.config.currentTeam);
}
const headers = new Headers();
headers.set('user-agent', ua);
if (this.client.authConfig.token) {
headers.set('authorization', `Bearer ${this.client.authConfig.token}`);
}
return fetch(url, {
method,
body,
headers,
});
}
async finishRun({ exitCode }: { exitCode: number }) {
if (!this.spaceId) {
return;
}
if (!this.run) {
return;
}
try {
// patch with node-fetch
await this.fetch({
endpoint: this.runsEndpoint({ runId: this.run.id }),
method: 'PATCH',
body: JSON.stringify({
status: 'completed',
endTime: Date.now(),
exitCode,
}),
});
console.log(`✅ Run saved to ${this.run.url}`);
} catch (err) {
console.error(err);
} finally {
// reset the run
this.run = undefined;
}
}
async createRun({ project }: { project: ProjectLinkAndSettings }) {
if (!this.spaceId) {
return;
}
if (this.run) {
console.error(
`Existing run in progress, cannot start a new run before finishing the current run.`
);
return;
}
const data = {
status: 'running',
startTime: Date.now(),
type: this.type,
context: 'LOCAL',
client: {
id: 'vercel-cli',
name: 'Vercel CLI',
version: pkg.version,
},
meta: JSON.stringify({ project }),
repositoryPath: 'TODO',
command: project.settings.buildCommand ?? '',
...this.gitMetaData(),
};
try {
const res = await this.fetch({
endpoint: this.runsEndpoint(),
method: 'POST',
body: JSON.stringify(data),
});
const json = await res.json();
console.log(`Created run: ${json.url}`);
// track the run
this.run = new Run({ id: json.id, url: json.url });
} catch (err) {
console.error(err);
}
}
async createTask(data: {
key: string;
name: string;
workspace: string;
log: string;
}) {
if (!this.spaceId) {
return;
}
if (!this.run) {
console.error(
`No run in progress, must start a new run before creating tasks.`
);
return;
}
try {
const res = await this.fetch({
endpoint: this.tasksEndpoint({ runId: this.run.id }),
method: 'POST',
body: JSON.stringify(data),
});
const json = await res.json();
console.log(`Added task: ${json}`);
} catch (err) {
console.error(err);
}
}
}