import path from 'path' ; import shell from 'shelljs' ; import fs from 'fs-extra' ; import { mergeNewAndExistingBuildAssets , getAllFilesRecursive } from './helper' ; import { ChildProcess } from 'child_process' ; interface IDeployEnvironments < T > { master : T ; beta : T ; production : T ; } const { HOME , BRANCH , PULL_REQUEST , COMMIT_MESSAGE , SITE_NAME , PACKAGE_LOCATION , } = process . env ; process . on ( 'unhandledRejection' , error => { throw error ; } ) ; if ( ! BRANCH ) { exit ( 'Expecting to run the deploy script from within Azure-Pipelines ' + '(or at least, with all environmental variables set up). Exiting.' , true ) ; } if ( PULL_REQUEST !== 'False' ) { exit ( 'Skipping deploy for pull requests' ) ; } const DEPLOYMENT_SLOTS_DICTIONARY = { master : 'alpha' , beta : 'beta' , production : 'staging' , } ; if ( ! DEPLOYMENT_SLOTS_DICTIONARY [ BRANCH ] ) { exit ( 'Invalid branch name. Skipping deploy.' ) ; } const BUILD_DIRECTORY = path . join ( PACKAGE_LOCATION , 'build' ) ; if ( ! shell . test ( '-d' , BUILD_DIRECTORY ) ) { exit ( 'ERROR: No build directory found!' , true ) ; } ( async ( ) => { const PREVIOUS_BUILD_DIRECTORIES = await fetchPreviousBuildsFromLiveSite ( ) ; const FINAL_OUTPUT_DIRECTORY = path . join ( HOME , 'final_output' ) ; if ( fs . existsSync ( FINAL_OUTPUT_DIRECTORY ) ) { fs . removeSync ( FINAL_OUTPUT_DIRECTORY ) ; } const DEPLOYMENT_LOG_FILENAME = new Date ( ) . toISOString ( ) . replace ( /\:/g , '_' ) + '.txt' ; mergeNewAndExistingBuildAssets ( { BUILD_DIRECTORY , PREVIOUS_BUILD_DIRECTORIES , FINAL_OUTPUT_DIRECTORY , DEPLOYMENT_LOG_FILENAME , } ) ; deploy ( FINAL_OUTPUT_DIRECTORY , DEPLOYMENT_SLOTS_DICTIONARY [ BRANCH ] ) ; exit ( `Deployment to ${ SITE_NAME }-${ DEPLOYMENT_SLOTS_DICTIONARY [ BRANCH ] } completed!` ) ; } ) ( ) ; async function fetchPreviousBuildsFromLiveSite ( ) { const spec = [ { friendlyName : 'current_slot' , urlWithUsernameAndPassword : getGitUrlWithUsernameAndPassword ( DEPLOYMENT_SLOTS_DICTIONARY [ BRANCH ] ) , } , BRANCH === 'production' ? { friendlyName : 'production' , urlWithUsernameAndPassword : getGitUrlWithUsernameAndPassword ( null ) , } : null , ] ; return Promise . all ( spec . filter ( item => item ) . map ( item => cloneExistingRepo ( item ) ) ) ; } async function cloneExistingRepo ( source ) { const sanitizedFriendlyName = superSanitize ( source . friendlyName ) ; const allPreviousBuildsFolder = path . join ( HOME , 'previous_builds' ) ; const fullFolderPath = path . join ( allPreviousBuildsFolder , sanitizedFriendlyName ) ; if ( ! fs . existsSync ( allPreviousBuildsFolder ) ) { fs . mkdirSync ( allPreviousBuildsFolder ) ; } console . log ( `Fetching existing assets from "${ source . friendlyName }" and copying them into "${ fullFolderPath }"` ) ; console . log ( 'Start: ' + new Date ( ) . toString ( ) ) ; shell . pushd ( allPreviousBuildsFolder ) ; await new Promise ( ( resolve , reject ) => { const process = shell . exec ( `git clone ${ source . urlWithUsernameAndPassword } ${ sanitizedFriendlyName }` , { async : true , } ) as ChildProcess ; process . on ( 'error' , error => reject ( error ) ) ; process . on ( 'message' , message => console . log ( message ) ) ; process . on ( 'exit' , ( code , signal ) => { console . log ( 'Done: ' + new Date ( ) . toString ( ) ) ; if ( code === 0 ) { resolve ( ) ; } else { reject ( new Error ( 'Unexpected error: ' + signal ) ) ; } } ) ; } ) ; shell . popd ( ) ; console . log ( `The following files were cloned into "${ fullFolderPath }":` ) ; getAllFilesRecursive ( fullFolderPath ) . forEach ( item => console . log ( ' - ' + item ) ) ; return fullFolderPath ; } function deploy ( path , deploymentSlot ) { const commitMessageSanitized = superSanitize ( COMMIT_MESSAGE ) ; shell . pushd ( path ) ; shell . exec ( 'git init' ) ; shell . exec ( 'git config --add user.name "Azure Pipelines"' ) ; shell . exec ( 'git config --add user.email "offMakerTeam@microsoft.com"' ) ; shell . exec ( 'git add -A' ) ; shell . exec ( `git commit -m "${ commitMessageSanitized }"` ) ; shell . exec ( `git push ${ getGitUrlWithUsernameAndPassword ( deploymentSlot ) } -f -u HEAD:refs/heads/master` ) ; shell . popd ( ) ; } function getGitUrlWithUsernameAndPassword ( deploymentSlotIfAny ) { return [ `https://` , `${ fetchUsername ( ) }:${ fetchPassword ( ) }` , `@` , getSiteNameWithPossibleSlotSuffix ( '-' ) , `.scm.azurewebsites.net:443/${ SITE_NAME }.git` , ] . join ( '' ) ; function fetchUsername ( ) { return '\\$' + getSiteNameWithPossibleSlotSuffix ( '__' ) ; } function fetchPassword ( ) { return process . env [ determineKey ( ) ] ; function determineKey ( ) { return ( `DEPLOYMENT_PASSWORD_` + getSiteNameWithPossibleSlotSuffix ( '_' ) . toUpperCase ( ) . replace ( /\-/g , '_' ) ) ; } } function getSiteNameWithPossibleSlotSuffix ( separator ) { return SITE_NAME + ( deploymentSlotIfAny ? separator + deploymentSlotIfAny : '' ) ; } } function superSanitize ( text ) { return text . replace ( /\W/g , '_' ) ; } function exit ( reason , abort ? ) { if ( reason ) { abort ? console . error ( reason ) : console . log ( reason ) ; } return abort ? process . exit ( 1 ) : process . exit ( 0 ) ; }