Compare commits

...

42 Commits

Author SHA1 Message Date
Matthew Stanciu
a2c5e97eee Oops 2022-06-27 20:28:36 -07:00
Matthew Stanciu
7938936963 Remove unnecessary fixtures 2022-06-27 20:15:00 -07:00
Matthew Stanciu
77f1baac81 Track specify-project 2022-06-27 20:07:12 -07:00
Matthew Stanciu
348a22b0da Make tests better, kind of 2022-06-27 20:02:50 -07:00
Matthew Stanciu
5301d3c3ae Cleanup & comments 2022-06-27 17:54:08 -07:00
Matthew Stanciu
018337a21c Remove "not linked" test for now
Will re-add once I figure out how to get the mock client to accept input
2022-06-27 15:33:21 -07:00
Matthew Stanciu
bcc5b0253c Clean up a bit & handle linking better
This will break the tests again though :noooooooo:
2022-06-27 15:30:38 -07:00
Matthew Stanciu
cbf00962f2 Fix test maybe? 2022-06-27 14:25:08 -07:00
Matthew Stanciu
f6d89df59c Fix tests I think I hope
I removed one of them because it tested for a no longer existing "username" field, and I think the unit tests are a better place to test for this kind of stuff anyway.
2022-06-21 17:58:01 -07:00
Matthew Stanciu
dc31ec04a6 plz......................... 2022-06-21 16:26:31 -07:00
Matthew Stanciu
da08b8b2f6 uhhhhh 2022-06-21 16:17:21 -07:00
Matthew Stanciu
ff7b6bd353 Revert "Does this passs??????????????????"
This reverts commit d0cc5ad0f4.
2022-06-21 16:08:07 -07:00
Matthew Stanciu
d0cc5ad0f4 Does this passs?????????????????? 2022-06-21 16:04:54 -07:00
Nathan Rajlich
6dea61dda3 print output 2022-06-21 15:32:11 -07:00
Matthew Stanciu
70eceb14cf Negate .vercel gitignore in fixture 2022-06-21 15:23:36 -07:00
Nathan Rajlich
2c04e27052 Use output.log() so that it outputs a newline 2022-06-21 15:17:33 -07:00
Matthew Stanciu
c63b0ebb3a Move new line to page message 2022-06-20 11:56:29 -07:00
Matthew Stanciu
7bd8f70ea4 Add newline between table & page message 2022-06-20 10:05:43 -07:00
Matthew Stanciu
26ec73c976 Add test 2022-06-17 17:52:28 -07:00
Matthew Stanciu
42c40e2dce Small import thing 2022-06-17 17:38:56 -07:00
Matthew Stanciu
9c4ff585d7 Export stateString 2022-06-17 17:38:27 -07:00
Matthew Stanciu
a39d574416 Add test 2022-06-17 17:37:28 -07:00
Matthew Stanciu
2bb101b4bc Fix local tests 2022-06-17 16:14:56 -07:00
Matthew Stanciu
697db0f453 Hopefully fix failing test 2022-06-17 13:35:17 -07:00
Matthew Stanciu
fcb7b62be3 Remove includeScheme from type definition 2022-06-17 12:20:36 -07:00
Matthew Stanciu
cc8ea4a473 Begin add tests 2022-06-17 11:53:18 -07:00
Matthew Stanciu
8d1dfff36b Fix docs 2022-06-17 11:29:11 -07:00
Matthew Stanciu
dbd4d318d6 Add --inspect and --prod flags 2022-06-17 11:04:42 -07:00
Matthew Stanciu
d72d649151 Fix small error 2022-06-17 01:38:33 -07:00
Matthew Stanciu
33fc2251fb Add instructions for seeing all deployments 2022-06-17 01:33:05 -07:00
Matthew Stanciu
0f74dda8a9 Make it work when you specify a project 2022-06-17 01:29:10 -07:00
Matthew Stanciu
e5695dafca Fix --scope behavior 2022-06-17 01:09:58 -07:00
Matthew Stanciu
ed3be7fa02 Add auto project scope and --all
I still want it to work the way it used to, with specifying a project name, and that doesn't currently work.
2022-06-16 18:58:59 -07:00
Matthew Stanciu
bd330a6c94 Better handle duration while building 2022-06-16 17:52:39 -07:00
Matthew Stanciu
cf764df522 make circle a variable 2022-06-16 15:25:09 -07:00
Matthew Stanciu
ea22346aec Add circles to state strings 2022-06-16 15:23:50 -07:00
Matthew Stanciu
b61da261f6 Some smaller changes 2022-06-16 15:02:16 -07:00
Matthew Stanciu
c490fea61f Chalk blue –> cyan 2022-06-16 10:52:52 -07:00
Matthew Stanciu
76898ccdf6 Show username conditionally 2022-06-16 10:47:52 -07:00
Matthew Stanciu
50f47a6e32 Fix double backticks 2022-06-15 21:32:55 -07:00
Matthew Stanciu
42eb8e4971 Add some changes
- project page now shows build times
- show project name on the project page
2022-06-15 18:13:34 -07:00
Matthew Stanciu
3b8427b7c2 Begin 2022-06-14 18:06:41 -07:00
13 changed files with 534 additions and 87 deletions

View File

@@ -1,14 +1,14 @@
import chalk from 'chalk';
import ms from 'ms';
import table from 'text-table';
import fs from 'fs-extra';
import { basename } from 'path';
import Now from '../util';
import getArgs from '../util/get-args';
import { handleError } from '../util/error';
import cmd from '../util/output/cmd';
import logo from '../util/output/logo';
import elapsed from '../util/output/elapsed';
import strlen from '../util/strlen';
import getScope from '../util/get-scope';
import toHost from '../util/to-host';
import parseMeta from '../util/parse-meta';
import { isValidName } from '../util/is-valid-name';
@@ -16,6 +16,10 @@ import getCommandFlags from '../util/get-command-flags';
import { getPkgName, getCommandName } from '../util/pkg-name';
import Client from '../util/client';
import { Deployment } from '../types';
import validatePaths from '../util/validate-paths';
import { getLinkedProject } from '../util/projects/link';
import { ensureLink } from '../util/link-project';
import getScope from '../util/get-scope';
const help = () => {
console.log(`
@@ -24,6 +28,9 @@ const help = () => {
${chalk.dim('Options:')}
-h, --help Output usage information
-a, --all Show all deployments in your scope
-i, --inspect Display the deployment inspect url instead of the deploy url
--prod Filter only for production deployments
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`vercel.json`'} file
@@ -31,6 +38,7 @@ const help = () => {
'DIR'
)} Path to the global ${'`.vercel`'} directory
-d, --debug Debug mode [off]
-y, --yes Skip the confirmation prompt
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
@@ -42,10 +50,22 @@ const help = () => {
${chalk.dim('Examples:')}
${chalk.gray('')} List all deployments
${chalk.gray(
''
)} List all deployments for the app connected to by your current directory
${chalk.cyan(`$ ${getPkgName()} ls`)}
${chalk.gray(
''
)} List all projects in the scope (team) of the project in your current directory
${chalk.cyan(`$ ${getPkgName()} ls --all`)}
${chalk.gray('')} List all projects in team ${chalk.dim('`my-team`')}
${chalk.cyan(`$ ${getPkgName()} ls --scope my-team --all`)}
${chalk.gray('')} List all deployments for the app ${chalk.dim('`my-app`')}
${chalk.cyan(`$ ${getPkgName()} ls my-app`)}
@@ -54,6 +74,20 @@ const help = () => {
${chalk.cyan(`$ ${getPkgName()} ls -m key1=value1 -m key2=value2`)}
${chalk.gray('')} Display only production deployments
${chalk.cyan(`$ ${getPkgName()} ls --prod`)}
${chalk.gray('')} Display dashboard inspect urls instead of deployment urls
${chalk.cyan(`$ ${getPkgName()} ls --inspect`)}
${chalk.gray('')} Get all deployments in team ${chalk.dim(
'`my-team`'
)}, filtering for production deployments and inspect urls
${chalk.cyan(`$ ${getPkgName()} ls --scope my-team --all --prod --inspect`)}
${chalk.gray('')} Paginate deployments for a project, where ${chalk.dim(
'`1584722256178`'
)} is the time in milliseconds since the UNIX epoch.
@@ -67,10 +101,16 @@ export default async function main(client: Client) {
try {
argv = getArgs(client.argv.slice(2), {
'--all': Boolean,
'-a': '--all',
'--inspect': Boolean,
'-i': '--inspect',
'--meta': [String],
'-m': '--meta',
'--next': Number,
'-N': '--next',
'--prod': Boolean,
'--yes': Boolean,
});
} catch (err) {
handleError(err);
@@ -86,30 +126,85 @@ export default async function main(client: Client) {
return 1;
}
let app: string | undefined = argv._[1];
let host: string | undefined = undefined;
if (argv['--help']) {
help();
return 2;
}
const all = argv['--all'];
const filterProd = argv['--prod'];
const inspect = argv['--inspect'];
const yes = argv['--yes'] || false;
const meta = parseMeta(argv['--meta']);
const { currentTeam, includeScheme } = config;
let contextName = null;
if (argv._[0] === 'list' || argv._[0] === 'ls') {
argv._.shift();
}
try {
({ contextName } = await getScope(client));
} catch (err) {
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
error(err.message);
let paths = [process.cwd()];
for (const path of paths) {
try {
await fs.stat(path);
} catch (err) {
output.error(
`The specified file or directory "${basename(path)}" does not exist.`
);
return 1;
}
throw err;
}
// check paths
const pathValidation = await validatePaths(output, paths);
if (!pathValidation.valid) {
return pathValidation.exitCode;
}
const { path } = pathValidation;
// retrieve `project` and `org` from .vercel
let link = await getLinkedProject(client, path);
if (link.status === 'error') {
return link.exitCode;
}
let { org, project, status } = link;
const appArg: string | undefined = argv._[0];
let app: string | undefined = appArg || project?.name;
let host: string | undefined = undefined;
if (app && !isValidName(app)) {
error(`The provided argument "${app}" is not a valid project name`);
return 1;
}
// If there's no linked project and user doesn't pass `app` or `all` args,
// prompt to link their current directory.
if (status === 'not_linked' && !app && !all) {
const linkedProject = await ensureLink('list', client, path, yes);
if (typeof linkedProject === 'number') {
return linkedProject;
}
link.org = linkedProject.org;
link.project = linkedProject.project;
}
let { contextName, team } = await getScope(client);
// If user passed in a custom scope, update the current team & context name
if (argv['--scope']) {
client.config.currentTeam = team?.id || undefined;
if (team?.slug) contextName = team.slug;
} else {
client.config.currentTeam = org?.type === 'team' ? org.id : undefined;
if (org?.slug) contextName = org.slug;
}
const { currentTeam } = config;
const nextTimestamp = argv['--next'];
if (typeof nextTimestamp !== undefined && Number.isNaN(nextTimestamp)) {
@@ -125,11 +220,6 @@ export default async function main(client: Client) {
});
const start = Date.now();
if (app && !isValidName(app)) {
error(`The provided argument "${app}" is not a valid project name`);
return 1;
}
// Some people are using entire domains as app names, so
// we need to account for this here
const asHost = app ? toHost(app) : '';
@@ -152,7 +242,7 @@ export default async function main(client: Client) {
}
debug('Fetching deployments');
const response = await now.list(app, {
const response = await now.list(all ? undefined : app, {
version: 6,
meta,
nextTimestamp,
@@ -166,7 +256,7 @@ export default async function main(client: Client) {
pagination: { count: number; next: number };
} = response;
if (app && !deployments.length) {
if (app && !all && !deployments.length) {
debug(
'No deployments: attempting to find deployment that matches supplied app name'
);
@@ -194,43 +284,74 @@ export default async function main(client: Client) {
deployments = deployments.filter(deployment => deployment.url === host);
}
if (filterProd) {
deployments = deployments.filter(
deployment => deployment.target === 'production'
);
}
log(
`Deployments under ${chalk.bold(contextName)} ${elapsed(
`${filterProd ? `Production deployments` : `Deployments`}${
app && !all ? ` for ${chalk.bold(chalk.magenta(app))}` : ''
} under ${chalk.bold(chalk.magenta(contextName))} ${elapsed(
Date.now() - start
)}`
);
if (app && !all) {
log(
`To see a list of all of the projects under ${chalk.bold(
contextName
)}, type ${getCommandName('ls --all')}.`
);
}
// we don't output the table headers if we have no deployments
if (!deployments.length) {
log(`No deployments found.`);
return 0;
}
// information to help the user find other deployments or instances
if (app == null) {
log(
`To list more deployments for a project run ${cmd(
`${getCommandName('ls [project]')}`
`To list more deployments for a project run ${getCommandName(
'ls [project]'
)}`
);
}
print('\n');
console.log(
`${table(
let tablePrint;
if (app && !all) {
tablePrint = `${table(
[
['project', 'latest deployment', 'state', 'age', 'username'].map(
header => chalk.dim(header)
),
(team
? [
'age',
inspect ? 'inspect url' : 'deployment url',
'state',
'duration',
]
: [
'age',
inspect ? 'inspect url' : 'deployment url',
'state',
'duration',
'username',
]
).map(header => chalk.bold(chalk.cyan(header))),
...deployments
.sort(sortRecent())
.map(dep => [
.map((dep, i) => [
[
getProjectName(dep),
chalk.bold((includeScheme ? 'https://' : '') + dep.url),
stateString(dep.state),
chalk.gray(ms(Date.now() - dep.createdAt)),
dep.creator.username,
i === 0
? chalk.bold(`${getDeployUrl(dep, inspect)}`)
: `${getDeployUrl(dep, inspect)}`,
stateString(dep.state),
chalk.gray(getDeploymentDuration(dep)),
team ? '' : chalk.gray(dep.creator.username),
],
])
// flatten since the previous step returns a nested
@@ -243,15 +364,50 @@ export default async function main(client: Client) {
),
],
{
align: ['l', 'l', 'r', 'l', 'l'],
align: team ? ['l', 'l', 'l', 'l'] : ['l', 'l', 'l', 'l', 'l'],
hsep: ' '.repeat(team ? 4 : 5),
stringLength: strlen,
}
).replace(/^/gm, ' ')}\n`;
} else {
tablePrint = `${table(
[
['project', 'latest deployment', 'state', 'age'].map(header =>
chalk.bold(chalk.cyan(header))
),
...deployments
.sort(sortRecent())
.map(dep => [
[
getProjectName(dep),
chalk.bold(getDeployUrl(dep, inspect)),
stateString(dep.state),
chalk.gray(ms(Date.now() - dep.createdAt)),
],
])
// flatten since the previous step returns a nested
// array of the deployment and (optionally) its instances
.flat()
.filter(app =>
// if an app wasn't supplied to filter by,
// we only want to render one deployment per app
app === null ? filterUniqueApps() : () => true
),
],
{
align: ['l', 'l', 'l', 'l'],
hsep: ' '.repeat(4),
stringLength: strlen,
}
).replace(/^/gm, ' ')}\n`
);
).replace(/^/gm, ' ')}\n`;
}
// print table with deployment information
client.output.print(tablePrint);
if (pagination && pagination.count === 20) {
const flags = getCommandFlags(argv, ['_', '--next']);
client.output.print('\n');
log(
`To display the next page run ${getCommandName(
`ls${app ? ' ' + app : ''}${flags} --next ${pagination.next}`
@@ -269,23 +425,53 @@ function getProjectName(d: Deployment) {
return d.name;
}
function getDeployUrl(
deployment: Deployment,
inspect: boolean | undefined
): string {
return inspect ? deployment.inspectorUrl : 'https://' + deployment.url;
}
// renders the state string
function stateString(s: string) {
switch (s) {
export function stateString(s: string) {
const CIRCLE = '● ';
// make `s` title case
s = `${s.substring(0, 1)}${s.toLowerCase().substring(1)}`;
switch (s.toUpperCase()) {
case 'INITIALIZING':
return chalk.yellow(s);
case 'BUILDING':
case 'DEPLOYING':
case 'ANALYZING':
return chalk.yellow(CIRCLE) + s;
case 'ERROR':
return chalk.red(s);
return chalk.red(CIRCLE) + s;
case 'READY':
return s;
return chalk.green(CIRCLE) + s;
case 'QUEUED':
return chalk.white(CIRCLE) + s;
case 'CANCELED':
return chalk.gray(s);
default:
return chalk.gray('UNKNOWN');
}
}
export function getDeploymentDuration(dep: Deployment): string {
if (!dep || !dep.ready || !dep.buildingAt) {
return '?';
}
const duration = ms(dep.ready - dep.buildingAt);
if (duration === '0ms') {
return '--';
}
return duration;
}
// sorts by most recent deployment
function sortRecent() {
return function recencySort(a: Deployment, b: Deployment) {

View File

@@ -26,7 +26,6 @@ export interface AuthConfig {
export interface GlobalConfig {
_?: string;
currentTeam?: string;
includeScheme?: string;
collectMetrics?: boolean;
api?: string;
@@ -128,6 +127,8 @@ export type Deployment = {
version?: number;
created: number;
createdAt: number;
ready?: number;
buildingAt?: number;
creator: { uid: string; username: string };
target: string | null;
ownerId: string;

View File

@@ -0,0 +1,44 @@
import { Org, Project } from '../types';
import Client from './client';
import setupAndLink from './link/setup-and-link';
import param from './output/param';
import { getCommandName } from './pkg-name';
import { getLinkedProject } from './projects/link';
type LinkResult = {
org: Org;
project: Project;
};
export async function ensureLink(
commandName: string,
client: Client,
cwd: string,
yes: boolean
): Promise<LinkResult | number> {
let link = await getLinkedProject(client, cwd);
if (link.status === 'not_linked') {
link = await setupAndLink(client, cwd, {
autoConfirm: yes,
successEmoji: 'link',
setupMsg: 'Set up',
});
if (link.status === 'not_linked') {
// User aborted project linking questions
return 0;
}
}
if (link.status === 'error') {
if (link.reason === 'HEADLESS') {
client.output.error(
`Command ${getCommandName(
commandName
)} requires confirmation. Use option ${param('--yes')} to confirm.`
);
}
return link.exitCode;
}
return { org: link.org, project: link.project };
}

View File

@@ -0,0 +1 @@
!.vercel

View File

@@ -0,0 +1,11 @@
> Why do I have a folder named ".vercel" in my project?
The ".vercel" folder is created when you link a directory to a Vercel project.
> What does the "project.json" file contain?
The "project.json" file contains:
- The ID of the Vercel project that you linked ("projectId")
- The ID of the user or team your Vercel project is owned by ("orgId")
> Should I commit the ".vercel" folder?
No, you should not share the ".vercel" folder with anyone.
Upon creation, it will be automatically added to your ".gitignore" file.

View File

@@ -0,0 +1,4 @@
{
"projectId": "prj_Am19DF8JBL9g89tn4RdDVD59axFi",
"orgId": "team_MtLD9hKuWAvoDd3KmiHs9zUg"
}

View File

@@ -0,0 +1 @@
<h1>hi</h1>

View File

@@ -553,8 +553,8 @@ test('default command should warn when deploying with conflicting subdirectory',
/Did you mean to deploy the subdirectory "list"\? Use `vc --cwd list` instead./
);
const listHeader = /project +latest deployment +state +age +username/;
t.regex(stdout || '', listHeader); // ensure `list` command still ran
const listHeader = /No deployments found/;
t.regex(stderr || '', listHeader, formatOutput({ stdout, stderr })); // ensure `list` command still ran
});
test('deploy command should not warn when deploying with conflicting subdirectory and using --cwd', async t => {
@@ -577,8 +577,8 @@ test('deploy command should not warn when deploying with conflicting subdirector
/Did you mean to deploy the subdirectory "list"\? Use `vc --cwd list` instead./
);
const listHeader = /project +latest deployment +state +age +username/;
t.regex(stdout || '', listHeader); // ensure `list` command still ran
const listHeader = /No deployments found/;
t.regex(stderr || '', listHeader); // ensure `list` command still ran
});
test('default command should work with --cwd option', async t => {
@@ -1813,31 +1813,6 @@ test('remove the wildcard alias', async t => {
});
*/
test('ensure username in list is right', async t => {
const { stdout, stderr, exitCode } = await execa(
binaryPath,
['ls', ...defaultArgs],
{
reject: false,
}
);
console.log(stderr);
console.log(stdout);
console.log(exitCode);
// Ensure the exit code is right
t.is(exitCode, 0);
const line = stdout
.split('\n')
.find(line => line.includes('.now.sh') || line.includes('.vercel.app'));
const columns = line.split(/\s+/);
// Ensure username column have username
t.truthy(columns.pop().includes(contextName));
});
test('ensure we render a warning for deployments with no files', async t => {
const directory = fixture('empty-directory');

View File

@@ -7,13 +7,37 @@ import { Build, User } from '../../src/types';
let deployments = new Map<string, Deployment>();
let deploymentBuilds = new Map<Deployment, Build[]>();
export function useDeployment({ creator }: { creator: Pick<User, 'id'> }) {
export function useDeployment({
creator,
}: {
creator: Pick<User, 'id' | 'email' | 'name'>;
}) {
type state =
| 'INITIALIZING'
| 'ANALYZING'
| 'BUILDING'
| 'DEPLOYING'
| 'READY'
| 'QUEUED'
| 'CANCELED'
| 'ERROR';
const createdAt = Date.now();
const url = new URL(chance().url());
const states: Array<state> = [
'READY',
'BUILDING',
'INITIALIZING',
'ANALYZING',
'DEPLOYING',
'ERROR',
'CANCELED',
'QUEUED',
];
const deployment: Deployment = {
id: `dpl_${chance().guid()}`,
url: url.hostname,
name: '',
name: `dpl_${chance().guid()}`,
meta: {},
regions: [],
routes: [],
@@ -22,8 +46,16 @@ export function useDeployment({ creator }: { creator: Pick<User, 'id'> }) {
version: 2,
createdAt,
createdIn: 'sfo1',
buildingAt: Date.now(),
ownerId: creator.id,
creator: {
uid: creator.id,
email: creator.email,
username: creator.name,
},
readyState: 'READY',
state: states[Math.floor(Math.random() * states.length)],
ready: Date.now() + Math.floor(Math.random() * 300000),
env: {},
build: { env: {} },
target: 'production',
@@ -77,4 +109,9 @@ beforeEach(() => {
const builds = deploymentBuilds.get(deployment);
res.json({ builds });
});
client.scenario.get('/:version/now/deployments', (req, res) => {
const deploymentsList = Array.from(deployments.values());
res.json({ deployments: deploymentsList });
});
});

View File

@@ -157,6 +157,12 @@ export function useProject(project: Partial<Project> = defaultProject) {
res.json({ envs });
});
client.scenario.get(`/v4/projects`, (req, res) => {
res.json({
projects: [defaultProject],
pagination: null,
});
});
return { project, envs };
}

View File

@@ -0,0 +1,163 @@
import { client } from '../../mocks/client';
import { useUser } from '../../mocks/user';
import list, {
stateString,
getDeploymentDuration,
} from '../../../src/commands/list';
import { join } from 'path';
import { useTeams } from '../../mocks/team';
import { defaultProject, useProject } from '../../mocks/project';
import { useDeployment } from '../../mocks/deployment';
import { Deployment } from '../../../src/types';
const fixture = (name: string) =>
join(__dirname, '../../fixtures/unit/commands/list', name);
describe('list', () => {
const originalCwd = process.cwd();
let teamSlug: string = '';
it('should get deployments from a project linked by a directory', async () => {
const cwd = fixture('with-team');
try {
process.chdir(cwd);
const user = useUser();
const team = useTeams('team_MtLD9hKuWAvoDd3KmiHs9zUg');
teamSlug = team[0].slug;
const project = useProject({
...defaultProject,
id: 'prj_Am19DF8JBL9g89tn4RdDVD59axFi',
name: 'prj_Am19DF8JBL9g89tn4RdDVD59axFi',
});
const deployment = useDeployment({ creator: user });
await list(client);
const { project: proj, org } = getDataFromIntro(
client.outputBuffer.split('\n')[0]
);
const header: string[] = formatTable(client.outputBuffer.split('\n')[3]);
const data: string[] = formatTable(client.outputBuffer.split('\n')[4]);
data.shift();
expect(proj).toEqual(project.project.name);
expect(org).toEqual(team[0].slug);
expect(header).toEqual([
'age',
'deployment url',
'state',
'duration',
'username',
]);
expect(data).toEqual([
`https://${deployment.url}`,
stateString(deployment.state || ''),
getDeploymentDuration(deployment as unknown as Deployment),
user.name,
]);
} finally {
process.chdir(originalCwd);
}
});
it('should get all deployments in the project scope', async () => {
const cwd = fixture('with-team');
try {
process.chdir(cwd);
const user = useUser();
useTeams('team_MtLD9hKuWAvoDd3KmiHs9zUg');
useProject({
...defaultProject,
id: 'prj_Am19DF8JBL9g89tn4RdDVD59axFi',
name: 'prj_Am19DF8JBL9g89tn4RdDVD59axFi',
});
const deployment = useDeployment({ creator: user });
client.setArgv('--all');
await list(client);
const { project, org } = getDataFromIntro(
client.outputBuffer.split('\n')[0]
);
const header: string[] = formatTable(client.outputBuffer.split('\n')[2]);
const data: string[] = formatTable(client.outputBuffer.split('\n')[3]);
data.pop();
expect(project).toBeUndefined();
expect(org).toEqual(teamSlug);
expect(header).toEqual(['project', 'latest deployment', 'state', 'age']);
expect(data).toEqual([
deployment.name,
`https://${deployment.url}`,
stateString(deployment.state || ''),
]);
} finally {
process.chdir(originalCwd);
}
});
it('should get the deployments for a specified project', async () => {
const cwd = fixture('with-team');
try {
process.chdir(cwd);
const user = useUser();
useTeams('team_MtLD9hKuWAvoDd3KmiHs9zUg');
useProject({
...defaultProject,
id: 'prj_Am19DF8JBL9g89tn4RdDVD59axFi',
name: 'prj_Am19DF8JBL9g89tn4RdDVD59axFi',
});
const deployment = useDeployment({ creator: user });
client.setArgv(deployment.name);
await list(client);
const { project, org } = getDataFromIntro(
client.outputBuffer.split('\n')[0]
);
const header: string[] = formatTable(client.outputBuffer.split('\n')[3]);
const data: string[] = formatTable(client.outputBuffer.split('\n')[4]);
data.shift();
expect(project).toEqual(deployment.name);
expect(org).toEqual(teamSlug);
expect(header).toEqual([
'age',
'deployment url',
'state',
'duration',
'username',
]);
expect(data).toEqual([
`https://${deployment.url}`,
stateString(deployment.state || ''),
getDeploymentDuration(deployment as unknown as Deployment),
user.name,
]);
} finally {
process.chdir(originalCwd);
}
});
});
function getDataFromIntro(output: string): {
project: string | undefined;
org: string | undefined;
} {
const project = output.match(/(?<=Deployments for )(.*)(?= under)/);
const org = output.match(/(?<=under )(.*)(?= \[)/);
return {
project: project?.[0],
org: org?.[0],
};
}
function formatTable(output: string): string[] {
return output
.trim()
.replace(/ {3} +/g, ',')
.split(',');
}

View File

@@ -60,9 +60,18 @@ export interface Deployment {
| 'BUILDING'
| 'DEPLOYING'
| 'READY'
| 'QUEUED'
| 'CANCELED'
| 'ERROR';
createdAt: number;
createdIn: string;
buildingAt: number;
creator?: {
uid?: string;
email?: string;
username?: string;
};
ready?: number;
env: Dictionary<string>;
build: {
env: Dictionary<string>;

View File

@@ -30,21 +30,30 @@ export interface Deployment {
public: boolean;
ownerId: string;
readyState:
| 'INITIALIZING'
| 'ANALYZING'
| 'BUILDING'
| 'DEPLOYING'
| 'READY'
| 'ERROR';
| 'INITIALIZING'
| 'ANALYZING'
| 'BUILDING'
| 'DEPLOYING'
| 'READY'
| 'ERROR';
state?:
| 'INITIALIZING'
| 'ANALYZING'
| 'BUILDING'
| 'DEPLOYING'
| 'READY'
| 'ERROR';
| 'INITIALIZING'
| 'ANALYZING'
| 'BUILDING'
| 'DEPLOYING'
| 'READY'
| 'QUEUED'
| 'CANCELED'
| 'ERROR';
createdAt: string;
createdIn: string;
buildingAt: number;
creator?: {
uid?: string;
email?: string;
username?: string;
};
ready?: number;
env: {
[key: string]: string;
};