Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 70 additions & 72 deletions apps/cli/commands/pull-reprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
runReprintCommandUntilComplete,
} from 'cli/lib/pull/migration-client';
import { preserveUnselectedLocalContent } from 'cli/lib/pull/preserve-local-content';
import { overallPercent, PullStep, withPercent } from 'cli/lib/pull/pull-progress';
import {
fetchReprintPullTree,
mapCliOnlyToReprint,
Expand All @@ -66,7 +67,11 @@ import {
} from 'cli/lib/pull/runtime-start-options';
import { buildAutoLoginUrl } from 'cli/lib/site-utils';
import { fetchSyncableSites } from 'cli/lib/sync-api';
import { getSyncSupportError, pickSyncSite } from 'cli/lib/sync-site-picker';
import {
findSyncSiteByIdentifier,
getSyncSupportError,
pickSyncSite,
} from 'cli/lib/sync-site-picker';
import {
startWordPressServer,
stopWordPressServer,
Expand All @@ -88,9 +93,9 @@ export const registerCommand = ( yargs: StudioArgv ) => {
),
builder: ( builderYargs ) => {
return builderYargs
.option( 'url', {
.option( 'remote-site', {
type: 'string',
describe: __( 'URL of the remote WordPress site to pull from (remote source)' ),
description: __( 'Remote site URL or ID' ),
} )
.option( 'only', {
type: 'string',
Expand Down Expand Up @@ -119,15 +124,14 @@ export const registerCommand = ( yargs: StudioArgv ) => {
const verbose = argv.verbose;

try {
await runCommand( argv.path, argv.url, verbose, {
await runCommand( argv.path, argv.remoteSite, verbose, {
only: argv.only as string[] | undefined,
skipDatabase: argv[ 'skip-database' ] as boolean,
skipUploads: argv[ 'skip-uploads' ] as boolean,
} );
} catch ( error ) {
if ( error instanceof PullError ) {
logger.spinner.fail( __( 'Pull failed' ) );
console.error( '\n' + chalk.bold.red( error.message ) );
logger.reportError( error );
if ( verbose && error.technicalDetails ) {
console.error( '\n' + chalk.dim( error.technicalDetails ) );
} else if ( error.technicalDetails ) {
Expand Down Expand Up @@ -285,15 +289,21 @@ class PullError extends LoggerError {
*/
export async function runCommand(
localPath: string,
remoteUrl?: string,
remoteSite?: string,
verbose = false,
cliSelection: CliSelectionOptions = {}
): Promise< void > {
logger.reportStart( LoggerAction.LOAD_SITES, __( 'Loading site…' ) );
logger.reportStart(
LoggerAction.LOAD_SITES,
withPercent( __( 'Loading site…' ), overallPercent( PullStep.SETUP ) )
);
const site = await getSiteByFolder( localPath );
logger.reportSuccess( __( 'Site loaded' ) );

const sourceSite = await resolveSourceSite( remoteUrl ?? site.reprintOrigin?.remoteUrl, verbose );
const sourceSite = await resolveSourceSite(
remoteSite ?? site.reprintOrigin?.remoteUrl,
verbose
);
if ( ! sourceSite ) {
return;
}
Expand Down Expand Up @@ -430,7 +440,10 @@ export async function runCommand(
// wires the generated runtime Blueprint onto it so `studio start` and
// the daemon serve the imported runtime rather than the original blank
// install. Idempotent — re-writing the same value on a resume is harmless.
logger.reportStart( LoggerAction.CREATE_SITE, `Linking pulled files to "${ site.name }"…` );
logger.reportStart(
LoggerAction.CREATE_SITE,
withPercent( `Linking pulled files to "${ site.name }"…`, overallPercent( PullStep.LINK ) )
);
site.runtimeBlueprintPath = studioMetadata.runtimeBlueprintPath;
await updateSiteRecord( site.id, ( record ) => {
record.runtimeBlueprintPath = studioMetadata.runtimeBlueprintPath;
Expand Down Expand Up @@ -472,7 +485,10 @@ export async function runCommand(
const startOptionsPath = path.join( studioMetadata.runtimeDirectory, 'start-options.json' );
fs.writeFileSync( startOptionsPath, JSON.stringify( runtimeStartOptions, null, 2 ) + '\n' );

logger.reportStart( LoggerAction.START_SITE, __( 'Starting WordPress server…' ) );
logger.reportStart(
LoggerAction.START_SITE,
withPercent( __( 'Starting WordPress server…' ), overallPercent( PullStep.START ) )
);

try {
await connectToDaemon();
Expand Down Expand Up @@ -551,6 +567,11 @@ export async function runCommand(
// again instead of silently reusing this run's choice.
clearPullSelection( studioMetadata );

// The percentage has to ride an in-progress message — `pullSite` only
// parses the token out of those, so the success below can't close the bar.
logger.reportProgress( withPercent( __( 'Pull complete' ), 100 ) );
logger.reportSuccess( __( 'Pull complete' ) );

site.importComplete = true;
site.status = 'ready';
await updateSiteRecord( site.id, ( record ) => {
Expand Down Expand Up @@ -807,7 +828,10 @@ async function runPreflight(
return JSON.parse( fs.readFileSync( preflightCachePath, 'utf-8' ) );
}

logger.reportStart( LoggerAction.PREFLIGHT, __( 'Initiating the migration…' ) );
logger.reportStart(
LoggerAction.PREFLIGHT,
withPercent( __( 'Initiating the migration…' ), overallPercent( PullStep.PREFLIGHT ) )
);

let preflightResult: ReprintProcessResult;
try {
Expand Down Expand Up @@ -1027,12 +1051,13 @@ export async function runFullPull(
const reprintRuntime = runtime === SITE_RUNTIME_NATIVE_PHP ? 'nginx-fpm' : 'playground-cli';
const onlyArgs = ( selection.fileOnlyPaths ?? [] ).map( ( onlyPath ) => `--only=${ onlyPath }` );

const runStep = ( progressLabel: string, args: string[] ) =>
const runStep = ( step: PullStep, progressLabel: string, args: string[] ) =>
runReprintCommandUntilComplete(
metadata.stateDirectory,
metadata.rawDirectory,
args,
( progress ) => logger.reportProgress( progress ),
( progress, fraction ) =>
logger.reportProgress( withPercent( progress, overallPercent( step, fraction ) ) ),
{
progressLabel,
mounts: [
Expand Down Expand Up @@ -1061,12 +1086,15 @@ export async function runFullPull(
resetEssentialFilesState( metadata.stateDirectory );
}

logger.reportStart( LoggerAction.DOWNLOAD_FILES, __( 'Pulling site…' ) );
logger.reportStart(
LoggerAction.DOWNLOAD_FILES,
withPercent( __( 'Pulling site…' ), overallPercent( PullStep.FILES ) )
);

// 1. Files. `--only` restricts the download to the selected wp-content
// folders; essential-files defers the media library to the post-start
// skipped-earlier pass.
await runStep( __( 'Pulling files' ), [
await runStep( PullStep.FILES, __( 'Pulling files' ), [
'pull-files',
apiUrl,
`--secret=${ secret }`,
Expand All @@ -1082,7 +1110,7 @@ export async function runFullPull(
// only from the target that db-apply persists — record it explicitly,
// pointing at the kept database.
if ( ! selection.skipDatabase ) {
await runStep( __( 'Pulling database' ), [
await runStep( PullStep.DATABASE, __( 'Pulling database' ), [
'pull-db',
apiUrl,
`--secret=${ secret }`,
Expand Down Expand Up @@ -1126,7 +1154,7 @@ export async function runFullPull(

// 4. Flatten the raw download into the site directory. `-` is the URL
// placeholder for local commands.
await runStep( __( 'Flattening layout' ), [
await runStep( PullStep.FLATTEN, __( 'Flattening layout' ), [
'flat-docroot',
'-',
`--flatten-to=${ metadata.sitePath }`,
Expand All @@ -1138,7 +1166,7 @@ export async function runFullPull(
// 5. Runtime config — last, so it reads the DB credentials pull-db wrote
// to state. apply-runtime takes no URL positional, and
// --flat-document-root replaces --fs-root (they are mutually exclusive).
await runStep( __( 'Preparing runtime' ), [
await runStep( PullStep.RUNTIME, __( 'Preparing runtime' ), [
'apply-runtime',
`--runtime=${ reprintRuntime }`,
`--output-dir=${ metadata.runtimeDirectory }`,
Expand Down Expand Up @@ -1168,7 +1196,10 @@ export async function downloadSkippedFiles(
verbose: boolean,
selection: PullSelection = {}
): Promise< void > {
logger.reportStart( LoggerAction.DOWNLOAD_FILES, __( 'Downloading remaining files…' ) );
logger.reportStart(
LoggerAction.DOWNLOAD_FILES,
withPercent( __( 'Downloading remaining files…' ), overallPercent( PullStep.REMAINING ) )
);

// Studio's split pipeline runs pull-db between pull-files and this
// tail, and pull-db's prepare_repull() resets the skipped_pending flag
Expand Down Expand Up @@ -1199,7 +1230,10 @@ export async function downloadSkippedFiles(
`--state-dir=${ metadata.stateDirectory }`,
`--fs-root=${ metadata.rawDirectory }`,
],
( progress ) => logger.reportProgress( progress ),
( progress, fraction ) =>
logger.reportProgress(
withPercent( progress, overallPercent( PullStep.REMAINING, fraction ) )
),
{
progressLabel: __( 'Remaining files' ),
verboseCommands: verbose,
Expand All @@ -1221,55 +1255,24 @@ export function normalizeSiteUrl( url: string ): string {
return normalized.toString();
}

/**
* Finds the WordPress.com site in the user's connected list whose
* public URL best matches `url`. Matches on the full normalized URL
* first, then falls back to host-only matching so `example.com` and
* `example.com/blog` both resolve to the same WP.com site record.
*
* No existing Studio helper does this today — `getSiteByFolder` /
* `getHostnameFromUrl` operate on local sites or return a string, and
* `findSite` variants all key on site id. Keep this one local to
* pull-reprint; if a second caller ever needs the same shape, the
* natural home would be `cli/lib/wpcom-sites`.
*/
export function findMatchingWpComSite< T extends { url: string } >(
sites: T[],
url: string
): T | undefined {
const normalizedUrl = normalizeSiteUrl( url );
const target = new URL( normalizedUrl );

return sites.find( ( site ) => {
try {
const normalizedSiteUrl = normalizeSiteUrl( site.url );
if ( normalizedSiteUrl === normalizedUrl ) {
return true;
}

return new URL( normalizedSiteUrl ).host === target.host;
} catch {
return false;
}
} );
}

/**
* Resolves the **remote source** to pull from. A valid source must be
* present in the user's WordPress.com Jetpack API site list, which also
* includes Pressable sites on the same platform. Handles two input
* patterns:
*
* 1. URL provided — resolve it against the connected WordPress.com/
* Pressable sites, enable the exporter, and rotate a fresh secret.
* 2. No URL — among pullable (`syncable`) sites only: if the user has
* 1. Identifier provided — a site URL or WordPress.com site ID, resolved
* against the connected WordPress.com/Pressable sites through the same
* helper `pull` and `push` use, then the exporter is enabled and a fresh
* secret is rotated.
* 2. No identifier — among pullable (`syncable`) sites only: if the user has
* exactly one, pick it; with several, show an interactive picker in a
* TTY (returning `null` if the user cancels) or error out when run
* non-interactively. Non-pullable sites (Simple, or missing hosting
* features) are surfaced as disabled in the picker.
*/
export async function resolveSourceSite(
url?: string,
identifier?: string,
verbose = false
): Promise< PullSource | null > {
const token = await readAuthToken();
Expand All @@ -1290,18 +1293,11 @@ export async function resolveSourceSite(
let resolvedUrl: string;
let wpComSite: SyncSite;

if ( url ) {
const matched = findMatchingWpComSite( sites, url );
if ( ! matched ) {
throw new LoggerError(
__( 'This URL is not a WordPress.com or Pressable site connected to your account.' )
);
}
if ( matched.syncSupport !== 'syncable' ) {
throw getSyncSupportError( matched );
}
resolvedUrl = matched.url;
wpComSite = matched;
if ( identifier ) {
// Throws when nothing matches, when several sites share the hostname,
// or when the match isn't syncable.
wpComSite = findSyncSiteByIdentifier( sites, identifier );
resolvedUrl = wpComSite.url;
} else {
// Only sites that can run the reprint exporter — those with hosting
// features enabled (`syncable`) — are pull candidates.
Expand All @@ -1324,11 +1320,13 @@ export async function resolveSourceSite(
if ( pullableSites.length > 1 ) {
// In a real terminal, let the user pick interactively. Outside a
// TTY (CI, or Studio driving the command) there's no way to
// prompt, so exit with guidance to pass `--url` — the realistic
// prompt, so exit with guidance to pass `--remote-site` — the realistic
// non-TTY caller already does.
if ( ! process.stdin.isTTY ) {
throw new LoggerError(
__( 'Multiple WordPress.com sites are available. Re-run with `--url <site-url>`.' )
__(
'Multiple WordPress.com sites are available. Re-run with `--remote-site <site-url-or-id>`.'
)
);
}

Expand Down
Loading