Skip to content

Commit

Permalink
Mekk typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
rebeccahum committed Dec 11, 2023
1 parent 57fd366 commit 0ecf9c8
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 144 deletions.
199 changes: 58 additions & 141 deletions src/bin/vip-deploy-app.js → src/bin/vip-deploy-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,25 @@
import chalk from 'chalk';
import debugLib from 'debug';
import { prompt } from 'enquirer';
import fs from 'fs';
import gql from 'graphql-tag';
import { mkdtemp } from 'node:fs/promises';
import os from 'os';
import path from 'path';

/**
* Internal dependencies
*/
import { App, AppEnvironment, AppEnvironmentDeployInput } from '../graphqlTypes';
import API from '../lib/api';
import command from '../lib/cli/command';
import * as exit from '../lib/cli/exit';
import { formatEnvironment, getGlyphForStatus } from '../lib/cli/format';
import { ProgressTracker } from '../lib/cli/progress';
import {
checkFileAccess,
getFileSize,
getFileMeta,
isFile,
uploadImportSqlFileToS3,
WithId,
UploadArguments,
} from '../lib/client-file-uploader';
import { GB_IN_BYTES } from '../lib/constants/file-size';
import { currentUserCanDeployForApp, isSupportedApp } from '../lib/manual-deploy/manual-deploy';
import { gates, renameFile } from '../lib/manual-deploy/manual-deploy';
import { trackEventWithEnv } from '../lib/tracker';
import { validateDeployFileExt, validateFilename } from '../lib/validations/manual-deploy';

const appQuery = `
id,
Expand Down Expand Up @@ -76,152 +70,70 @@ const DEPLOY_PREFLIGHT_PROGRESS_STEPS = [
{ id: 'deploy', name: 'Deploying' },
];

/**
* @param {App} app
* @param {Env} env
* @param {FileMeta} fileMeta
*/
export async function gates( app, env, fileMeta ) {
const { id: envId, appId } = env;
const track = trackEventWithEnv.bind( null, appId, envId );
const { fileName, basename } = fileMeta;

if ( ! fs.existsSync( fileName ) ) {
await track( 'deploy_app_command_error', { error_type: 'invalid-file' } );
exit.withError( `Unable to access file ${ fileMeta.fileName }` );
}

try {
validateFilename( basename );
} catch ( error ) {
await track( 'deploy_app_command_error', { error_type: 'invalid-filename' } );
exit.withError( error );
}

try {
validateDeployFileExt( fileName );
} catch ( error ) {
await track( 'deploy_app_command_error', { error_type: 'invalid-extension' } );
exit.withError( error );
}

if ( ! currentUserCanDeployForApp( app ) ) {
await track( 'deploy_app_command_error', { error_type: 'unauthorized' } );
exit.withError(
'The currently authenticated account does not have permission to deploy to an application.'
);
}

if ( ! isSupportedApp( app ) ) {
await track( 'deploy_app_command_error', { error_type: 'unsupported-app' } );
exit.withError( 'The type of application you specified does not currently support deploys.' );
}

try {
await checkFileAccess( fileName );
} catch ( err ) {
await track( 'deploy_app_command_error', { error_type: 'appfile-unreadable' } );
exit.withError( `File '${ fileName }' does not exist or is not readable.` );
}

if ( ! ( await isFile( fileName ) ) ) {
await track( 'deploy_app_command_error', { error_type: 'appfile-notfile' } );
exit.withError( `Path '${ fileName }' is not a file.` );
}

const fileSize = await getFileSize( fileName );
if ( ! fileSize ) {
await track( 'deploy_app_command_error', { error_type: 'appfile-empty' } );
exit.withError( `File '${ fileName }' is empty.` );
}
interface PromptToContinueParams {
launched: boolean;
formattedEnvironment: string;
track: ( eventName: string ) => Promise< false | unknown[] >;
domain: string;
}

const maxFileSize = 4 * GB_IN_BYTES;
if ( fileSize > maxFileSize ) {
await track( 'deploy_app_command_error', {
error_type: 'appfile-toobig',
file_size: fileSize,
} );
exit.withError(
`The deploy file size (${ fileSize } bytes) exceeds the limit (${ maxFileSize } bytes).`
);
}
interface StartDeployVariables {
input: AppEnvironmentDeployInput;
}

const promptToContinue = async ( { launched, formattedEnvironment, track, domain } ) => {
const promptToMatch = domain.toUpperCase();
/**
* Prompt the user to confirm the environment they are deploying to.
* @param {PromptToContinueParams} PromptToContinueParams
*/
export async function promptToContinue( params: PromptToContinueParams ) {
const promptToMatch = params.domain.toUpperCase();
const promptResponse = await prompt( {
type: 'input',
name: 'confirmedDomain',
message: `You are about to deploy to a ${
launched ? 'launched' : 'un-launched'
} ${ formattedEnvironment } site ${ chalk.yellow( domain ) }.\nType '${ chalk.yellow(
message: `You are about to deploy to a ${ params.launched ? 'launched' : 'un-launched' } ${
params.formattedEnvironment
} site ${ chalk.yellow( params.domain ) }.\nType '${ chalk.yellow(
promptToMatch
) }' (without the quotes) to continue:\n`,
} );

if ( promptResponse.confirmedDomain !== promptToMatch ) {
await track( 'deploy_app_unexpected_input' );
await params.track( 'deploy_app_unexpected_input' );
exit.withError( 'The input did not match the expected environment label. Deploy aborted.' );
}
};
}

export async function deployAppCmd( arg: string[] = [], opts: Record< string, unknown > = {} ) {
const app = opts.app as App;
const env = opts.env as AppEnvironment;

/**
* Rename file so it doesn't get overwritten.
* @param {FileMeta} fileMeta - The metadata of the file to be renamed.
* @returns {FileMeta} The updated file metadata after renaming.
*/
export async function renameFile( fileMeta ) {
const tmpDir = await mkdtemp( path.join( os.tmpdir(), 'vip-manual-deploys' ) );

const datePrefix = new Date()
.toISOString()
// eslint-disable-next-line no-useless-escape
.replace( /[\-T:\.Z]/g, '' )
.slice( 0, 14 );
const newFileBasename = `${ datePrefix }-${ fileMeta.basename }`;
debug(
`Renaming the file to ${ chalk.cyan( newFileBasename ) } from ${
fileMeta.basename
} prior to transfer...`
);
const newFileName = `${ tmpDir }/${ newFileBasename }`;

fs.copyFileSync( fileMeta.fileName, newFileName );
fileMeta.fileName = newFileName;
fileMeta.basename = newFileBasename;

return fileMeta;
};

export async function deployAppCmd( arg = [], opts = {} ) {
const { app, env } = opts;
const { id: envId, appId } = env;
const [ fileName ] = arg;
let fileMeta = await getFileMeta( fileName );

debug( 'Options: ', opts );
debug( 'Args: ', arg );

const appId = env.appId as number;
const envId = env.id as number;
const track = trackEventWithEnv.bind( null, appId, envId );

await gates( app, env, fileMeta );

await track( 'deploy_app_command_execute' );

// Log summary of deploy details
const domain = env?.primaryDomain?.name ? env.primaryDomain.name : `#${ env.id }`;
const formattedEnvironment = formatEnvironment( opts.env.type );
const launched = opts.env.launched;
const deployMessage = opts.message ?? '';
const deployMessage = ( opts.message as string ) ?? '';
const forceDeploy = opts.force;

const domain = env?.primaryDomain?.name ? env.primaryDomain.name : `#${ env.id }`;
if ( ! forceDeploy ) {
await promptToContinue( {
launched,
formattedEnvironment,
const promptParams: PromptToContinueParams = {
launched: Boolean( env.launched ),
formattedEnvironment: formatEnvironment( env.type as string ),
track,
domain,
} );
};

await promptToContinue( promptParams );
}

/**
Expand All @@ -247,7 +159,7 @@ Processing the file for deployment to your environment...
}`;
};

const failWithError = failureError => {
const failWithError = ( failureError: Error | string ) => {
status = 'failed';
setProgressTrackerPrefixAndSuffix();
progressTracker.stopPrinting();
Expand All @@ -263,34 +175,39 @@ Processing the file for deployment to your environment...
// Call the Public API
const api = await API();

const startDeployVariables = {};

const progressCallback = percentage => {
const progressCallback = ( percentage: string ) => {
progressTracker.setUploadPercentage( percentage );
};

try {
fileMeta = await renameFile( fileMeta );
} catch ( err ) {
throw new Error( `Unable to copy file to temporary working directory: ${ err.message }` );
throw new Error(
`Unable to copy file to temporary working directory: ${ ( err as Error ).message }`
);
}

const appInput = { id: appId } as WithId;
const envInput = { id: envId } as WithId;
const uploadParams: UploadArguments = {
app: appInput,
env: envInput,
fileMeta,
progressCallback,
};
const startDeployVariables: StartDeployVariables = { input: {} };

try {
const {
fileMeta: { basename },
md5,
result,
} = await uploadImportSqlFileToS3( {
app,
env,
fileMeta,
progressCallback,
} );
} = await uploadImportSqlFileToS3( uploadParams );

startDeployVariables.input = {
id: app.id,
environmentId: env.id,
basename,
basename: fileMeta.basename,
md5,
deployMessage,
};
Expand All @@ -302,11 +219,11 @@ Processing the file for deployment to your environment...
} catch ( uploadError ) {
await track( 'deploy_app_command_error', {
error_type: 'upload_failed',
upload_error: uploadError.message,
upload_error: ( uploadError as Error ).message,
} );

progressTracker.stepFailed( 'upload' );
return failWithError( uploadError );
return failWithError( uploadError as Error );
}

// Start the deploy
Expand All @@ -322,11 +239,11 @@ Processing the file for deployment to your environment...

await track( 'deploy_app_command_error', {
error_type: 'StartDeploy-failed',
gql_err: gqlErr,
gql_err: gqlErr as Error,
} );

progressTracker.stepFailed( 'deploy' );
return failWithError( `StartDeploy call failed: ${ gqlErr }` );
return failWithError( `StartDeploy call failed: ${ ( gqlErr as Error ).message }` );
}

progressTracker.stepSuccess( 'deploy' );
Expand Down
1 change: 1 addition & 0 deletions src/graphqlTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ export type AppEnvironmentDeployInput = {
environmentId?: InputMaybe< Scalars[ 'Int' ][ 'input' ] >;
id?: InputMaybe< Scalars[ 'Int' ][ 'input' ] >;
md5?: InputMaybe< Scalars[ 'String' ][ 'input' ] >;
deployMessage?: InputMaybe< Scalars[ 'String' ][ 'input' ] >;
};

export type AppEnvironmentDeployPayload = {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/client-file-uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const UPLOAD_PART_SIZE = 16 * MB_IN_BYTES;
const MAX_CONCURRENT_PART_UPLOADS = 5;

// TODO: Replace with a proper definitions once we convert lib/cli/command.js to TypeScript
interface WithId {
export interface WithId {
id: number;
}

Expand Down Expand Up @@ -72,7 +72,7 @@ export interface GetSignedUploadRequestDataArgs {
const getWorkingTempDir = (): Promise< string > =>
mkdtemp( path.join( os.tmpdir(), 'vip-client-file-uploader' ) );

interface UploadArguments {
export interface UploadArguments {
app: WithId;
env: WithId;
fileMeta: FileMeta;
Expand Down
Loading

0 comments on commit 0ecf9c8

Please sign in to comment.