Skip to content

Commit 1b0b4ce

Browse files
committed
feat: add --wait/--no-wait flag to job import command
Add --wait/--no-wait and --poll-interval flags to `b2c job import`. Import waits for completion by default (preserving existing behavior); use --no-wait to return immediately after the job starts. Also fixes inaccurate flag documentation in b2c-job and b2c-site-import-export skills.
1 parent 69244e7 commit 1b0b4ce

7 files changed

Lines changed: 178 additions & 47 deletions

File tree

.changeset/job-import-wait-flag.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@salesforce/b2c-cli': patch
3+
'@salesforce/b2c-tooling-sdk': patch
4+
---
5+
6+
Add `--wait` / `--no-wait` flag to `b2c job import` command. Import waits for completion by default (preserving existing behavior); use `--no-wait` to return immediately after the job starts. Also adds `--poll-interval` flag for controlling poll frequency.

packages/b2c-cli/src/commands/job/import.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ export default class JobImport extends JobCommand<typeof JobImport> {
3939

4040
static flags = {
4141
...JobCommand.baseFlags,
42+
wait: Flags.boolean({
43+
char: 'w',
44+
description: 'Wait for import job to complete',
45+
default: true,
46+
allowNo: true,
47+
}),
4248
'keep-archive': Flags.boolean({
4349
char: 'k',
4450
description: 'Keep archive on instance after import',
@@ -53,6 +59,11 @@ export default class JobImport extends JobCommand<typeof JobImport> {
5359
char: 't',
5460
description: 'Timeout in seconds (default: no timeout)',
5561
}),
62+
'poll-interval': Flags.integer({
63+
description: 'Polling interval in seconds when using --wait',
64+
default: 3,
65+
dependsOn: ['wait'],
66+
}),
5667
'show-log': Flags.boolean({
5768
description: 'Show job log on failure',
5869
default: true,
@@ -68,7 +79,14 @@ export default class JobImport extends JobCommand<typeof JobImport> {
6879
this.requireWebDavCredentials();
6980

7081
const {target} = this.args;
71-
const {'keep-archive': keepArchive, remote, timeout, 'show-log': showLog = true} = this.flags;
82+
const {
83+
wait,
84+
'keep-archive': keepArchive,
85+
remote,
86+
timeout,
87+
'poll-interval': pollInterval,
88+
'show-log': showLog = true,
89+
} = this.flags;
7290

7391
const hostname = this.resolvedConfig.values.hostname!;
7492

@@ -134,8 +152,10 @@ export default class JobImport extends JobCommand<typeof JobImport> {
134152

135153
const result = await this.operations.siteArchiveImport(this.instance, importTarget, {
136154
keepArchive,
155+
wait,
137156
waitOptions: {
138157
timeoutSeconds: timeout,
158+
pollIntervalSeconds: pollInterval,
139159
onPoll: (info) => {
140160
if (!this.jsonEnabled()) {
141161
this.log(
@@ -149,13 +169,22 @@ export default class JobImport extends JobCommand<typeof JobImport> {
149169
},
150170
});
151171

152-
const durationSec = result.execution.duration ? (result.execution.duration / 1000).toFixed(1) : 'N/A';
153-
this.log(
154-
t('commands.job.import.completed', 'Import completed: {{status}} (duration: {{duration}}s)', {
155-
status: result.execution.exit_status?.code || result.execution.execution_status,
156-
duration: durationSec,
157-
}),
158-
);
172+
if (wait) {
173+
const durationSec = result.execution.duration ? (result.execution.duration / 1000).toFixed(1) : 'N/A';
174+
this.log(
175+
t('commands.job.import.completed', 'Import completed: {{status}} (duration: {{duration}}s)', {
176+
status: result.execution.exit_status?.code || result.execution.execution_status,
177+
duration: durationSec,
178+
}),
179+
);
180+
} else {
181+
this.log(
182+
t('commands.job.import.started', 'Import job started: {{executionId}} (status: {{status}})', {
183+
executionId: result.execution.id,
184+
status: result.execution.execution_status,
185+
}),
186+
);
187+
}
159188

160189
if (result.archiveKept) {
161190
this.log(

packages/b2c-cli/test/commands/job/import.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,48 @@ describe('job import', () => {
9393
expect(result.execution.exit_status.code).to.equal('skipped');
9494
});
9595

96+
it('passes wait:false to siteArchiveImport when --no-wait is set', async () => {
97+
const command: any = await createCommand({wait: false, json: true}, {target: './dir'});
98+
stubCommon(command);
99+
100+
sinon.stub(command, 'runBeforeHooks').resolves({skip: false});
101+
sinon.stub(command, 'runAfterHooks').resolves(void 0);
102+
103+
const importStub = sinon.stub().resolves({
104+
execution: {id: 'exec-1', execution_status: 'running'} as any,
105+
archiveFilename: 'import-123.zip',
106+
archiveKept: true,
107+
});
108+
command.operations = {...command.operations, siteArchiveImport: importStub};
109+
110+
const result = await command.run();
111+
112+
expect(importStub.calledOnce).to.equal(true);
113+
const options = importStub.getCall(0).args[2];
114+
expect(options.wait).to.equal(false);
115+
expect(result.execution.execution_status).to.equal('running');
116+
});
117+
118+
it('passes wait:true to siteArchiveImport by default', async () => {
119+
const command: any = await createCommand({wait: true, json: true}, {target: './dir'});
120+
stubCommon(command);
121+
122+
sinon.stub(command, 'runBeforeHooks').resolves({skip: false});
123+
sinon.stub(command, 'runAfterHooks').resolves(void 0);
124+
125+
const importStub = sinon.stub().resolves({
126+
execution: {execution_status: 'finished', exit_status: {code: 'OK'}} as any,
127+
archiveFilename: 'a.zip',
128+
archiveKept: false,
129+
});
130+
command.operations = {...command.operations, siteArchiveImport: importStub};
131+
132+
await command.run();
133+
134+
const options = importStub.getCall(0).args[2];
135+
expect(options.wait).to.equal(true);
136+
});
137+
96138
it('shows job log and errors on JobExecutionError when show-log is true', async () => {
97139
const command: any = await createCommand({json: true}, {target: './dir'});
98140
stubCommon(command);

packages/b2c-tooling-sdk/src/operations/jobs/site-archive.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const EXPORT_JOB_ID = 'sfcc-site-archive-export';
2525
export interface SiteArchiveImportOptions {
2626
/** Keep archive on instance after import (default: false) */
2727
keepArchive?: boolean;
28+
/** Whether to wait for job completion (default: true) */
29+
wait?: boolean;
2830
/** Wait options for job completion */
2931
waitOptions?: WaitForJobOptions;
3032
}
@@ -98,7 +100,7 @@ export async function siteArchiveImport(
98100
options: SiteArchiveImportOptions & {archiveName?: string} = {},
99101
): Promise<SiteArchiveImportResult> {
100102
const logger = getLogger();
101-
const {keepArchive = false, waitOptions, archiveName} = options;
103+
const {keepArchive = false, wait = true, waitOptions, archiveName} = options;
102104

103105
let zipFilename: string;
104106
let needsUpload = true;
@@ -196,32 +198,34 @@ export async function siteArchiveImport(
196198

197199
logger.debug({jobId: IMPORT_JOB_ID, executionId: execution.id}, `Import job started: ${execution.id}`);
198200

199-
// Wait for completion
200-
try {
201-
execution = await waitForJob(instance, IMPORT_JOB_ID, execution.id!, waitOptions);
202-
} catch (error) {
203-
if (error instanceof JobExecutionError) {
204-
// Try to get log file
205-
try {
206-
const log = await getJobLog(instance, error.execution);
207-
logger.error({jobId: IMPORT_JOB_ID, logFile: error.execution.log_file_path, log}, `Job log:\n${log}`);
208-
} catch {
209-
logger.error({jobId: IMPORT_JOB_ID}, 'Could not retrieve job log');
201+
if (wait) {
202+
// Wait for completion
203+
try {
204+
execution = await waitForJob(instance, IMPORT_JOB_ID, execution.id!, waitOptions);
205+
} catch (error) {
206+
if (error instanceof JobExecutionError) {
207+
// Try to get log file
208+
try {
209+
const log = await getJobLog(instance, error.execution);
210+
logger.error({jobId: IMPORT_JOB_ID, logFile: error.execution.log_file_path, log}, `Job log:\n${log}`);
211+
} catch {
212+
logger.error({jobId: IMPORT_JOB_ID}, 'Could not retrieve job log');
213+
}
210214
}
215+
throw error;
211216
}
212-
throw error;
213-
}
214217

215-
// Clean up archive if not keeping
216-
if (!keepArchive && needsUpload) {
217-
await instance.webdav.delete(uploadPath);
218-
logger.debug({path: uploadPath}, `Archive deleted: ${uploadPath}`);
218+
// Clean up archive if not keeping
219+
if (!keepArchive && needsUpload) {
220+
await instance.webdav.delete(uploadPath);
221+
logger.debug({path: uploadPath}, `Archive deleted: ${uploadPath}`);
222+
}
219223
}
220224

221225
return {
222226
execution,
223227
archiveFilename: zipFilename,
224-
archiveKept: keepArchive,
228+
archiveKept: wait ? keepArchive : true,
225229
};
226230
}
227231

packages/b2c-tooling-sdk/test/operations/jobs/site-archive.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,53 @@ describe('operations/jobs/site-archive', () => {
339339
expect(paths).to.include(`${archiveRoot}/libraries/mylib/library.xml`);
340340
});
341341

342+
it('should return immediately without polling when wait is false', async () => {
343+
const zipPath = path.join(tempDir, 'test.zip');
344+
fs.writeFileSync(zipPath, Buffer.from('PK\x03\x04'));
345+
346+
let polled = false;
347+
let deleteRequested = false;
348+
349+
server.use(
350+
http.all(`${WEBDAV_BASE}/*`, async ({request}) => {
351+
if (request.method === 'PUT') {
352+
return new HttpResponse(null, {status: 201});
353+
}
354+
if (request.method === 'DELETE') {
355+
deleteRequested = true;
356+
return new HttpResponse(null, {status: 204});
357+
}
358+
return new HttpResponse(null, {status: 404});
359+
}),
360+
http.post(`${OCAPI_BASE}/jobs/sfcc-site-archive-import/executions`, () => {
361+
return HttpResponse.json({
362+
id: 'exec-nowait',
363+
execution_status: 'running',
364+
});
365+
}),
366+
http.get(`${OCAPI_BASE}/jobs/sfcc-site-archive-import/executions/exec-nowait`, () => {
367+
polled = true;
368+
return HttpResponse.json({
369+
id: 'exec-nowait',
370+
execution_status: 'finished',
371+
exit_status: {code: 'OK'},
372+
is_log_file_existing: false,
373+
});
374+
}),
375+
);
376+
377+
const result = await siteArchiveImport(mockInstance, zipPath, {
378+
wait: false,
379+
waitOptions: FAST_WAIT_OPTIONS,
380+
});
381+
382+
expect(result.execution.id).to.equal('exec-nowait');
383+
expect(result.execution.execution_status).to.equal('running');
384+
expect(result.archiveKept).to.be.true;
385+
expect(polled).to.be.false;
386+
expect(deleteRequested).to.be.false;
387+
});
388+
342389
it('should throw JobExecutionError when import fails', async () => {
343390
const zipPath = path.join(tempDir, 'test.zip');
344391
fs.writeFileSync(zipPath, Buffer.from('PK\x03\x04'));

skills/b2c-cli/skills/b2c-job/SKILL.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,28 @@ Some system jobs (like search indexing) use non-standard request schemas. Use `-
3838

3939
```bash
4040
# run search index job for specific sites
41-
b2c job run sfcc-search-index-product-full-update --wait --body '{"site_scope":["RefArch","SiteGenesis"]}'
41+
b2c job run sfcc-search-index-product-full-update --wait --body '{"site_scope":{"named_sites":["RefArch","SiteGenesis"]}}'
4242

4343
# run search index job for a single site
44-
b2c job run sfcc-search-index-product-full-update --wait --body '{"site_scope":["RefArch"]}'
44+
b2c job run sfcc-search-index-product-full-update --wait --body '{"site_scope":{"named_sites":["RefArch"]}}'
4545
```
4646

4747
Note: `--body` and `-P` are mutually exclusive.
4848

4949
### Import Site Archives
5050

51-
The `job import` command automatically waits for the import job to complete before returning. It does not use the `--wait` option.
51+
The `job import` command waits for the import job to complete by default.
5252

5353
```bash
54-
# import a local directory as a site archive
54+
# import a local directory as a site archive (waits for completion by default)
5555
b2c job import ./my-site-data
5656

5757
# import a local zip file
5858
b2c job import ./export.zip
5959

60+
# import and return immediately without waiting for completion
61+
b2c job import ./my-site-data --no-wait
62+
6063
# keep the archive on the instance after import
6164
b2c job import ./my-site-data --keep-archive
6265

@@ -158,8 +161,8 @@ b2c job search --json
158161
### Wait for Job Completion
159162

160163
```bash
161-
# wait for a specific job execution to complete
162-
b2c job wait <execution-id>
164+
# wait for a specific job execution to complete (requires both job ID and execution ID)
165+
b2c job wait <job-id> <execution-id>
163166
```
164167

165168
### More Commands

skills/b2c-cli/skills/b2c-site-import-export/SKILL.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ Use the `b2c` CLI plugin to import and export site archives on Salesforce B2C Co
1414
### Import Local Directory
1515

1616
```bash
17-
# Import a local directory as a site archive
17+
# Import a local directory as a site archive (waits for completion by default)
1818
b2c job import ./my-site-data
1919

20-
# Import and wait for completion
21-
b2c job import ./my-site-data --wait
20+
# Import and return immediately without waiting
21+
b2c job import ./my-site-data --no-wait
2222

2323
# Import a local zip file
2424
b2c job import ./export.zip
@@ -27,7 +27,7 @@ b2c job import ./export.zip
2727
b2c job import ./my-site-data --keep-archive
2828

2929
# Show job log if the import fails
30-
b2c job import ./my-site-data --wait --show-log
30+
b2c job import ./my-site-data --show-log
3131
```
3232

3333
### Import Remote Archive
@@ -40,11 +40,11 @@ b2c job import existing-archive.zip --remote
4040
## Export Commands
4141

4242
```bash
43-
# Export site data
44-
b2c job export
43+
# Export global metadata (waits for completion by default)
44+
b2c job export --global-data meta_data
4545

46-
# Export with specific configuration
47-
b2c job export --wait
46+
# Export a site with specific data units
47+
b2c job export --site RefArch --site-data content,site_preferences
4848
```
4949

5050
## Common Workflows
@@ -85,7 +85,7 @@ my-import/
8585

8686
3. Import:
8787
```bash
88-
b2c job import ./my-import --wait
88+
b2c job import ./my-import
8989
```
9090

9191
### Adding Site Preferences
@@ -134,7 +134,7 @@ my-import/
134134

135135
4. Import:
136136
```bash
137-
b2c job import ./my-import --wait
137+
b2c job import ./my-import
138138
```
139139

140140
### Creating a Custom Object Type
@@ -175,7 +175,7 @@ b2c job import ./my-import --wait
175175

176176
2. Import:
177177
```bash
178-
b2c job import ./my-import --wait
178+
b2c job import ./my-import
179179
```
180180

181181
### Importing Custom Object Data
@@ -227,16 +227,16 @@ site-archive/
227227
b2c job search
228228

229229
# Wait for a specific job execution
230-
b2c job wait <execution-id>
230+
b2c job wait <job-id> <execution-id>
231231

232232
# View job logs on failure
233-
b2c job import ./my-data --wait --show-log
233+
b2c job import ./my-data --show-log
234234
```
235235

236236
### Best Practices
237237

238238
1. **Test imports on sandbox first** before importing to staging/production
239-
2. **Use `--wait`** to ensure import completes before continuing
239+
2. Import waits for completion by default — use `--no-wait` only when you want to return immediately
240240
3. **Use `--show-log`** to debug failed imports
241241
4. **Keep archives organized** by feature or change type
242242
5. **Version control your metadata** XML files

0 commit comments

Comments
 (0)