Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions .changeset/align-wait-deploy-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@salesforce/b2c-tooling-sdk': minor
'@salesforce/b2c-cli': minor
'@salesforce/b2c-dx-docs': patch
---

Add `--wait` flag to `mrt bundle deploy` to poll until deployment completes, and align all SDK wait functions (`waitForJob`, `waitForEnv`) to a consistent pattern with structured `onPoll` callbacks, seconds-based options, and injectable `sleep` for testing.
1 change: 1 addition & 0 deletions docs/cli/jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ In addition to [global flags](./index#global-flags):
|------|-------------|---------|
| `--wait`, `-w` | Wait for job to complete | `false` |
| `--timeout`, `-t` | Timeout in seconds when waiting | No timeout |
| `--poll-interval` | Polling interval in seconds when using `--wait` | `3` |
| `--param`, `-P` | Job parameter in format "name=value" (repeatable) | |
| `--body`, `-B` | Raw JSON request body (for system jobs with non-standard schemas) | |
| `--no-wait-running` | Do not wait for running job to finish before starting | `false` |
Expand Down
8 changes: 8 additions & 0 deletions docs/cli/mrt.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ b2c mrt env create prod -p my-storefront --name "Production" \
| `--enable-source-maps` | Enable source maps |
| `--proxy` | Proxy configuration in format `path=host` (repeatable) |
| `--wait`, `-w` | Wait for the environment to be ready before returning |
| `--poll-interval` | Polling interval in seconds when using `--wait` | `10` |
| `--timeout` | Maximum time to wait in seconds when using `--wait` (`0` for no timeout) | `600` |

### b2c mrt env get

Expand Down Expand Up @@ -454,6 +456,9 @@ b2c mrt bundle deploy -p my-storefront --build-dir ./dist

# Deploy existing bundle by ID
b2c mrt bundle deploy 12345 -p my-storefront -e production

# Deploy and wait for completion
b2c mrt bundle deploy -p my-storefront -e staging --wait
```

**Flags:**
Expand All @@ -465,6 +470,9 @@ b2c mrt bundle deploy 12345 -p my-storefront -e production
| `--ssr-shared` | Shared file patterns | `static/**/*,client/**/*` |
| `--node-version`, `-n` | Node.js version for SSR | `22.x` |
| `--ssr-param` | SSR parameters (key=value) | |
| `--wait`, `-w` | Wait for the deployment to complete before returning | `false` |
| `--poll-interval` | Polling interval in seconds when using `--wait` | `30` |
| `--timeout` | Maximum time to wait in seconds when using `--wait` (`0` for no timeout) | `600` |

### b2c mrt bundle list

Expand Down
2 changes: 1 addition & 1 deletion packages/b2c-cli/src/commands/content/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default class ContentExport extends JobCommand<typeof ContentExport> {
this.requireOAuthCredentials();
}

const waitOptions = flags.timeout ? {timeout: flags.timeout * 1000} : undefined;
const waitOptions = flags.timeout ? {timeoutSeconds: flags.timeout} : undefined;

if (flags['dry-run']) {
const {library} = await this.operations.fetchContentLibrary(this.instance, libraryId, {
Expand Down
2 changes: 1 addition & 1 deletion packages/b2c-cli/src/commands/content/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default class ContentList extends JobCommand<typeof ContentList> {
this.requireOAuthCredentials();
}

const waitOptions = flags.timeout ? {timeout: flags.timeout * 1000} : undefined;
const waitOptions = flags.timeout ? {timeoutSeconds: flags.timeout} : undefined;

const instance = flags['library-file'] ? (null as unknown as typeof this.instance) : this.instance;

Expand Down
9 changes: 4 additions & 5 deletions packages/b2c-cli/src/commands/job/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,13 @@ export default class JobExport extends JobCommand<typeof JobExport> {
this.log(t('commands.job.export.dataUnits', 'Data units: {{dataUnits}}', {dataUnits: JSON.stringify(dataUnits)}));

const waitOptions: WaitForJobOptions = {
timeout: timeout ? timeout * 1000 : undefined,
onProgress: (exec, elapsed) => {
timeoutSeconds: timeout,
onPoll: (info) => {
if (!this.jsonEnabled()) {
const elapsedSec = Math.floor(elapsed / 1000);
this.log(
t('commands.job.export.progress', ' Status: {{status}} ({{elapsed}}s elapsed)', {
status: exec.execution_status,
elapsed: elapsedSec.toString(),
status: info.status,
elapsed: String(info.elapsedSeconds),
}),
);
}
Expand Down
9 changes: 4 additions & 5 deletions packages/b2c-cli/src/commands/job/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,13 @@ export default class JobImport extends JobCommand<typeof JobImport> {
const result = await this.operations.siteArchiveImport(this.instance, importTarget, {
keepArchive,
waitOptions: {
timeout: timeout ? timeout * 1000 : undefined,
onProgress: (exec, elapsed) => {
timeoutSeconds: timeout,
onPoll: (info) => {
if (!this.jsonEnabled()) {
const elapsedSec = Math.floor(elapsed / 1000);
this.log(
t('commands.job.import.progress', ' Status: {{status}} ({{elapsed}}s elapsed)', {
status: exec.execution_status,
elapsed: elapsedSec.toString(),
status: info.status,
elapsed: String(info.elapsedSeconds),
}),
);
}
Expand Down
29 changes: 22 additions & 7 deletions packages/b2c-cli/src/commands/job/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export default class JobRun extends JobCommand<typeof JobRun> {
char: 't',
description: 'Timeout in seconds when waiting (default: no timeout)',
}),
'poll-interval': Flags.integer({
description: 'Polling interval in seconds when using --wait',
default: 3,
dependsOn: ['wait'],
}),
param: Flags.string({
char: 'P',
description: 'Job parameter in format "name=value" (use -P multiple times for multiple params)',
Expand Down Expand Up @@ -80,7 +85,15 @@ export default class JobRun extends JobCommand<typeof JobRun> {
this.requireOAuthCredentials();

const {jobId} = this.args;
const {wait, timeout, param, body, 'no-wait-running': noWaitRunning, 'show-log': showLog} = this.flags;
const {
wait,
timeout,
'poll-interval': pollInterval,
param,
body,
'no-wait-running': noWaitRunning,
'show-log': showLog,
} = this.flags;

// Parse parameters or body
const parameters = this.parseParameters(param || []);
Expand Down Expand Up @@ -138,6 +151,7 @@ export default class JobRun extends JobCommand<typeof JobRun> {
jobId,
executionId: execution.id!,
timeout,
pollInterval,
showLog,
context,
});
Expand Down Expand Up @@ -216,22 +230,23 @@ export default class JobRun extends JobCommand<typeof JobRun> {
jobId: string;
executionId: string;
timeout: number | undefined;
pollInterval: number | undefined;
showLog: boolean;
context: B2COperationContext;
}): Promise<JobExecution> {
const {jobId, executionId, timeout, showLog, context} = options;
const {jobId, executionId, timeout, pollInterval, showLog, context} = options;
this.log(t('commands.job.run.waiting', 'Waiting for job to complete...'));

try {
const execution = await this.operations.waitForJob(this.instance, jobId, executionId, {
timeout: timeout ? timeout * 1000 : undefined,
onProgress: (exec, elapsed) => {
timeoutSeconds: timeout,
pollIntervalSeconds: pollInterval,
onPoll: (info) => {
if (!this.jsonEnabled()) {
const elapsedSec = Math.floor(elapsed / 1000);
this.log(
t('commands.job.run.progress', ' Status: {{status}} ({{elapsed}}s elapsed)', {
status: exec.execution_status,
elapsed: elapsedSec.toString(),
status: info.status,
elapsed: String(info.elapsedSeconds),
}),
);
}
Expand Down
11 changes: 5 additions & 6 deletions packages/b2c-cli/src/commands/job/wait.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,14 @@ export default class JobWait extends JobCommand<typeof JobWait> {

try {
const execution = await this.operations.waitForJob(this.instance, jobId, executionId, {
timeout: timeout ? timeout * 1000 : undefined,
pollInterval: pollInterval * 1000,
onProgress: (exec, elapsed) => {
timeoutSeconds: timeout,
pollIntervalSeconds: pollInterval,
onPoll: (info) => {
if (!this.jsonEnabled()) {
const elapsedSec = Math.floor(elapsed / 1000);
this.log(
t('commands.job.wait.progress', ' Status: {{status}} ({{elapsed}}s elapsed)', {
status: exec.execution_status,
elapsed: elapsedSec.toString(),
status: info.status,
elapsed: String(info.elapsedSeconds),
}),
);
}
Expand Down
102 changes: 91 additions & 11 deletions packages/b2c-cli/src/commands/mrt/bundle/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {
pushBundle,
createDeployment,
waitForEnv,
DEFAULT_SSR_PARAMETERS,
type PushResult,
type CreateDeploymentResult,
type MrtEnvironment,
} from '@salesforce/b2c-tooling-sdk/operations/mrt';
import {t, withDocs} from '../../../i18n/index.js';

Expand Down Expand Up @@ -53,7 +55,7 @@ function parseSsrParams(params: string[]): Record<string, string> {
return result;
}

type DeployResult = CreateDeploymentResult | PushResult;
type DeployResult = CreateDeploymentResult | MrtEnvironment | PushResult;

/**
* Deploy a bundle to Managed Runtime.
Expand Down Expand Up @@ -86,6 +88,7 @@ export default class MrtBundleDeploy extends MrtCommand<typeof MrtBundleDeploy>
'<%= config.bin %> <%= command.id %> --project my-storefront --node-version 20.x',
'<%= config.bin %> <%= command.id %> --project my-storefront --ssr-param SSRProxyPath=/api',
'<%= config.bin %> <%= command.id %> 12345 --project my-storefront --environment staging',
'<%= config.bin %> <%= command.id %> 12345 --project my-storefront --environment staging --wait',
];

static flags = {
Expand Down Expand Up @@ -116,6 +119,27 @@ export default class MrtBundleDeploy extends MrtCommand<typeof MrtBundleDeploy>
multiple: true,
default: [],
}),
wait: Flags.boolean({
char: 'w',
description: 'Wait for the deployment to complete before returning',
default: false,
}),
'poll-interval': Flags.integer({
description: 'Polling interval in seconds when using --wait',
default: 30,
dependsOn: ['wait'],
}),
timeout: Flags.integer({
description: 'Maximum time to wait in seconds when using --wait (0 for no timeout)',
default: 600,
dependsOn: ['wait'],
}),
};

protected operations = {
pushBundle,
createDeployment,
waitForEnv,
};

async run(): Promise<DeployResult> {
Expand All @@ -132,7 +156,7 @@ export default class MrtBundleDeploy extends MrtCommand<typeof MrtBundleDeploy>
/**
* Deploy an existing bundle to an environment.
*/
private async deployExistingBundle(bundleId: number): Promise<CreateDeploymentResult> {
private async deployExistingBundle(bundleId: number): Promise<CreateDeploymentResult | MrtEnvironment> {
const {mrtProject: project, mrtEnvironment: environment} = this.resolvedConfig.values;

if (!project) {
Expand All @@ -153,7 +177,7 @@ export default class MrtBundleDeploy extends MrtCommand<typeof MrtBundleDeploy>
);

try {
const result = await createDeployment(
const result = await this.operations.createDeployment(
{
projectSlug: project,
targetSlug: environment,
Expand All @@ -174,12 +198,18 @@ export default class MrtBundleDeploy extends MrtCommand<typeof MrtBundleDeploy>
},
),
);
this.log(
t(
'commands.mrt.bundle.deploy.note',
'Note: Deployments are asynchronous. Use "b2c mrt env get" or the Runtime Admin dashboard to check status.',
),
);
if (!this.flags.wait) {
this.log(
t(
'commands.mrt.bundle.deploy.note',
'Note: Deployments are asynchronous. Use "b2c mrt env get" or the Runtime Admin dashboard to check status.',
),
);
}
}

if (this.flags.wait) {
return this.waitForDeployment(project, environment);
}

return result;
Expand All @@ -198,7 +228,7 @@ export default class MrtBundleDeploy extends MrtCommand<typeof MrtBundleDeploy>
/**
* Push a local build to create a new bundle.
*/
private async pushLocalBuild(): Promise<PushResult> {
private async pushLocalBuild(): Promise<MrtEnvironment | PushResult> {
const {mrtProject: project, mrtEnvironment: target} = this.resolvedConfig.values;
const {message} = this.flags;

Expand Down Expand Up @@ -227,7 +257,7 @@ export default class MrtBundleDeploy extends MrtCommand<typeof MrtBundleDeploy>
}

try {
const result = await pushBundle(
const result = await this.operations.pushBundle(
{
projectSlug: project,
target,
Expand Down Expand Up @@ -256,6 +286,14 @@ export default class MrtBundleDeploy extends MrtCommand<typeof MrtBundleDeploy>
),
);

if (this.flags.wait) {
if (!target) {
this.warn('--wait was specified but no environment target was provided. Skipping wait.');
return result;
}
return this.waitForDeployment(project, target);
}

return result;
} catch (error) {
if (error instanceof Error) {
Expand All @@ -264,4 +302,46 @@ export default class MrtBundleDeploy extends MrtCommand<typeof MrtBundleDeploy>
throw error;
}
}

/**
* Wait for a deployment to complete by polling the environment state.
*/
private async waitForDeployment(project: string, environment: string): Promise<MrtEnvironment> {
this.log(
t('commands.mrt.bundle.deploy.waiting', 'Waiting for deployment to complete on {{environment}}...', {
environment,
}),
);

const envResult = await this.operations.waitForEnv(
{
projectSlug: project,
slug: environment,
origin: this.resolvedConfig.values.mrtOrigin,
pollIntervalSeconds: this.flags['poll-interval'],
timeoutSeconds: this.flags.timeout,
onPoll: (info) => {
if (!this.jsonEnabled()) {
this.log(
t('commands.mrt.bundle.deploy.state', '[{{elapsed}}s] State: {{state}}', {
elapsed: String(info.elapsedSeconds),
state: info.state,
}),
);
}
},
},
this.getMrtAuth(),
);

if (!this.jsonEnabled()) {
this.log(
t('commands.mrt.bundle.deploy.deployComplete', 'Deployment complete. Environment is {{state}}.', {
state: envResult.state ?? 'unknown',
}),
);
}

return envResult;
}
}
Loading
Loading