@numa-tech/numa 1.13.2 → 1.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/dist/application-onboarding/client.d.ts +6 -1
- package/dist/application-onboarding/client.js +11 -1
- package/dist/application-onboarding/client.js.map +1 -1
- package/dist/application-onboarding/commands.d.ts +23 -1
- package/dist/application-onboarding/commands.js +277 -29
- package/dist/application-onboarding/commands.js.map +1 -1
- package/dist/application-onboarding/schemas.d.ts +109 -0
- package/dist/application-onboarding/schemas.js +17 -4
- package/dist/application-onboarding/schemas.js.map +1 -1
- package/dist/command-catalog.js +54 -11
- package/dist/command-catalog.js.map +1 -1
- package/dist/jenkins-jobs/commands.js +31 -1
- package/dist/jenkins-jobs/commands.js.map +1 -1
- package/dist/publications/commands.js +24 -1
- package/dist/publications/commands.js.map +1 -1
- package/dist/repositories/commands.js +23 -1
- package/dist/repositories/commands.js.map +1 -1
- package/package.json +3 -3
- package/skills/numa-create-application/SKILL.md +158 -0
- package/skills/numa-create-application/agents/openai.yaml +4 -0
- package/skills/numa-create-application/evals/evals.json +61 -0
- package/skills/numa-create-application/references/checklist.md +126 -0
- package/skills/numa-create-application/references/inference-rules.md +69 -0
- package/skills/numa-create-application/references/numa-cli.md +216 -0
- package/skills/numa-create-application/references/promote-workflows.md +105 -0
- package/skills/numa-create-application/scripts/inspect-project.mjs +243 -0
|
@@ -4,7 +4,7 @@ import { loadAppConfig } from "../app-config.js";
|
|
|
4
4
|
import { loadConfig } from "../config.js";
|
|
5
5
|
import { registerApplicationCandidateCommands } from "../application-candidates/commands.js";
|
|
6
6
|
import { SourceRepositoryClient } from "../repositories/client.js";
|
|
7
|
-
import {
|
|
7
|
+
import { scanProjectPublication } from "../publications/scanner.js";
|
|
8
8
|
import { OnboardingClient } from "./client.js";
|
|
9
9
|
import { OnboardingError, sanitizeOnboardingOutput } from "./errors.js";
|
|
10
10
|
import { OnboardingModeSchema, OnboardingEventOutputSchema, OnboardingFirstBuildActionRequestSchema, OnboardingJenkinsBindingActionRequestSchema, OnboardingObserverScopeActionRequestSchema, OnboardingPublicationPlanActionRequestSchema, OnboardingSessionStateSchema, OnboardingSpecSchema, ScmProviderSchema } from "./schemas.js";
|
|
@@ -189,7 +189,12 @@ export function scmCredentialUpdate(version, authType, credential) {
|
|
|
189
189
|
}
|
|
190
190
|
function scmConnectionLine(connection) {
|
|
191
191
|
const enabled = Object.entries(connection.capabilities).filter(([, value]) => value).map(([key]) => key);
|
|
192
|
-
|
|
192
|
+
const codeupCloneReadiness = connection.provider === "CODEUP"
|
|
193
|
+
? `\tcodeup-clone:${connection.codeupCloneCredentialConfigured == null
|
|
194
|
+
? "unknown"
|
|
195
|
+
: connection.codeupCloneCredentialConfigured ? "ready" : "missing"}`
|
|
196
|
+
: "";
|
|
197
|
+
return `${connection.code}\t${connection.provider}\t${connection.health}\t${connection.organizationKey}\t${enabled.join(",") || "no-capabilities"}${codeupCloneReadiness}`;
|
|
193
198
|
}
|
|
194
199
|
function repositoryLine(repository) {
|
|
195
200
|
return `${repository.externalId}\t${repository.fullName ?? repository.name}\t${repository.defaultBranch ?? "-"}\t${repository.headRevision ?? "-"}${repository.archived ? "\tARCHIVED" : ""}`;
|
|
@@ -290,6 +295,9 @@ export async function executeOnboardingSpec(client, spec, options = {}) {
|
|
|
290
295
|
if (!options.apply && options.idempotencyKey) {
|
|
291
296
|
throw new OnboardingError("--idempotency-key is only valid with --apply.", "onboarding_input_invalid");
|
|
292
297
|
}
|
|
298
|
+
if (spec.schemaVersion === 3 && spec.projectStrategy === "PUBLISH_PROJECT" && !options.sessionNo) {
|
|
299
|
+
throw new OnboardingError("schemaVersion=3 PUBLISH_PROJECT must reuse the session created by `numa app begin`; provide --session <session-no>.", "onboarding_session_required");
|
|
300
|
+
}
|
|
293
301
|
const key = options.apply ? idempotencyKey(options.idempotencyKey) : undefined;
|
|
294
302
|
const draftSession = options.sessionNo
|
|
295
303
|
? await client.getSession(options.sessionNo)
|
|
@@ -315,9 +323,17 @@ export async function executeOnboardingSpec(client, spec, options = {}) {
|
|
|
315
323
|
if (!planHash) {
|
|
316
324
|
throw new OnboardingError("The server did not return a plan hash; apply was not submitted.", "onboarding_plan_missing");
|
|
317
325
|
}
|
|
318
|
-
const
|
|
326
|
+
const attempt = await executeOnboardingSessionMutation(client, session.sessionNo, () => client.apply(session.sessionNo, { planHash, idempotencyKey: key }), (event) => hasOnboardingApplyRecoveryEvidence(event, planHash));
|
|
327
|
+
if (!attempt.result) {
|
|
328
|
+
return {
|
|
329
|
+
session: attempt.inspection.session,
|
|
330
|
+
preflight,
|
|
331
|
+
idempotencyKey: key,
|
|
332
|
+
recoveredFromStatus: true
|
|
333
|
+
};
|
|
334
|
+
}
|
|
319
335
|
session = await client.getSession(session.sessionNo);
|
|
320
|
-
return { session, preflight, task, idempotencyKey: key };
|
|
336
|
+
return { session, preflight, task: attempt.result, idempotencyKey: key, recoveredFromStatus: false };
|
|
321
337
|
}
|
|
322
338
|
function defaultSleep(milliseconds, signal) {
|
|
323
339
|
return new Promise((resolve, reject) => {
|
|
@@ -367,7 +383,7 @@ export async function inspectOnboardingSession(client, sessionNo, afterSequence
|
|
|
367
383
|
.filter((event) => event.sequenceNo > afterSequence)
|
|
368
384
|
.map(publicOnboardingEvent)
|
|
369
385
|
.sort((left, right) => left.sequenceNo - right.sequenceNo);
|
|
370
|
-
const lastSequence = events.reduce((maximum, event) => Math.max(maximum, event.sequenceNo), afterSequence);
|
|
386
|
+
const lastSequence = events.reduce((maximum, event) => Math.max(maximum, event.sequenceNo), Math.max(afterSequence, eventPage.nextSequence ?? afterSequence));
|
|
371
387
|
return {
|
|
372
388
|
session,
|
|
373
389
|
events,
|
|
@@ -375,6 +391,192 @@ export async function inspectOnboardingSession(client, sessionNo, afterSequence
|
|
|
375
391
|
execution: { ...onboardingExecutionView(session, events), lastSequence }
|
|
376
392
|
};
|
|
377
393
|
}
|
|
394
|
+
function mutationResultMayBeUnknown(error) {
|
|
395
|
+
if (error instanceof OnboardingError) {
|
|
396
|
+
return error.retryable
|
|
397
|
+
|| (error.status != null && error.status >= 500)
|
|
398
|
+
|| (error.code === "onboarding_invalid_response" && error.status != null
|
|
399
|
+
&& error.status >= 200 && error.status < 300);
|
|
400
|
+
}
|
|
401
|
+
return error instanceof TypeError;
|
|
402
|
+
}
|
|
403
|
+
export function hasOnboardingApplyRecoveryEvidence(event, planHash) {
|
|
404
|
+
return event.output?.taskNo != null && event.output.planHash === planHash;
|
|
405
|
+
}
|
|
406
|
+
export function hasJenkinsBindingRecoveryEvidence(_event, _bindingId) {
|
|
407
|
+
// jenkinsBindingId alone does not prove CONFIRMED status or the requested
|
|
408
|
+
// optimistic-lock version; unknown confirmations therefore fail closed.
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
export function hasObserverScopeRecoveryEvidence(_event) {
|
|
412
|
+
// The reviewed event projection currently lacks serviceBindingId and exact
|
|
413
|
+
// observer coordinates, so no event can prove this request's full intent.
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
export function hasFirstBuildRecoveryEvidence(_event, _input) {
|
|
417
|
+
// Aggregated task output can repeat an older decision on an unrelated new
|
|
418
|
+
// event and does not carry the action idempotency key or full build intent.
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
export async function executeOnboardingSessionMutation(client, sessionNo, mutate, hasNewEvidence) {
|
|
422
|
+
// A pre-write cursor is required: without it, a matching field or broad
|
|
423
|
+
// session state that predates this request could be misreported as recovery.
|
|
424
|
+
const baseline = await inspectOnboardingSession(client, sessionNo);
|
|
425
|
+
try {
|
|
426
|
+
return { result: await mutate(), recoveredFromStatus: false };
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
if (!mutationResultMayBeUnknown(error))
|
|
430
|
+
throw error;
|
|
431
|
+
let inspection;
|
|
432
|
+
try {
|
|
433
|
+
inspection = await inspectOnboardingSession(client, sessionNo, baseline.lastSequence);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// Preserve the mutation error below; a failed read must never trigger a write replay.
|
|
437
|
+
}
|
|
438
|
+
if (inspection && inspection.events.some((event) => event.sequenceNo > baseline.lastSequence && hasNewEvidence(event, baseline, inspection))) {
|
|
439
|
+
return { inspection, recoveredFromStatus: true };
|
|
440
|
+
}
|
|
441
|
+
const original = error instanceof OnboardingError ? error : undefined;
|
|
442
|
+
throw new OnboardingError("The mutation result is unknown. Status recovery was attempted; inspect the same session before reusing the original idempotency key.", "onboarding_result_unknown", original?.status, original?.requestId, true, {
|
|
443
|
+
sessionNo,
|
|
444
|
+
recoveryCommand: `numa app inspect ${sessionNo} --json`,
|
|
445
|
+
originalCode: original?.code ?? "network_error"
|
|
446
|
+
}, original?.retryAfterSeconds);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function assertPublicationSessionBinding(result, sessionNo, resolved) {
|
|
450
|
+
if (result.session.sessionNo !== sessionNo
|
|
451
|
+
|| result.publicationPlan.consumerType !== "APPLICATION_ONBOARDING"
|
|
452
|
+
|| result.publicationPlan.consumerRef !== sessionNo
|
|
453
|
+
|| (resolved && !result.replayed)) {
|
|
454
|
+
throw new OnboardingError("The publication response is not a replay bound to the same APPLICATION_ONBOARDING session.", "onboarding_invalid_response", 200);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function publicationResolutionPending(error) {
|
|
458
|
+
return error instanceof OnboardingError
|
|
459
|
+
&& error.status === 404
|
|
460
|
+
&& error.code === "ONBOARDING_PUBLICATION_PLAN_NOT_RESOLVED"
|
|
461
|
+
&& error.retryable;
|
|
462
|
+
}
|
|
463
|
+
export async function executePublicationAttachmentWithRecovery(client, sessionNo, clientRequestId, mutate, options = {}) {
|
|
464
|
+
try {
|
|
465
|
+
const result = await mutate();
|
|
466
|
+
assertPublicationSessionBinding(result, sessionNo, false);
|
|
467
|
+
return { result, recoveredFromStatus: false };
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
if (!mutationResultMayBeUnknown(error))
|
|
471
|
+
throw error;
|
|
472
|
+
const delaysMs = options.delaysMs ?? [0, 200, 500, 1_000];
|
|
473
|
+
const sleep = options.sleep ?? ((milliseconds) => defaultSleep(milliseconds));
|
|
474
|
+
let resolutionCode;
|
|
475
|
+
for (const delayMs of delaysMs) {
|
|
476
|
+
if (delayMs > 0)
|
|
477
|
+
await sleep(delayMs);
|
|
478
|
+
try {
|
|
479
|
+
const result = await client.resolvePublicationPlan(sessionNo, clientRequestId);
|
|
480
|
+
assertPublicationSessionBinding(result, sessionNo, true);
|
|
481
|
+
return { result, recoveredFromStatus: true };
|
|
482
|
+
}
|
|
483
|
+
catch (resolutionError) {
|
|
484
|
+
resolutionCode = resolutionError instanceof OnboardingError ? resolutionError.code : "network_error";
|
|
485
|
+
if (publicationResolutionPending(resolutionError) || mutationResultMayBeUnknown(resolutionError))
|
|
486
|
+
continue;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const original = error instanceof OnboardingError ? error : undefined;
|
|
491
|
+
throw new OnboardingError("The publication attachment result is unknown. Continue read-only resolution with the original session and client-request-id; do not replay the write based on this error.", "onboarding_publication_result_unknown", original?.status, original?.requestId, true, {
|
|
492
|
+
sessionNo,
|
|
493
|
+
clientRequestId,
|
|
494
|
+
recoveryCommand: `numa app publication resolve ${sessionNo} --client-request-id ${clientRequestId} --json`,
|
|
495
|
+
originalCode: original?.code ?? "network_error",
|
|
496
|
+
resolutionCode
|
|
497
|
+
}, original?.retryAfterSeconds);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function sessionSummary(session) {
|
|
501
|
+
return {
|
|
502
|
+
sessionNo: session.sessionNo,
|
|
503
|
+
mode: session.mode,
|
|
504
|
+
state: session.state,
|
|
505
|
+
currentStep: session.currentStep,
|
|
506
|
+
revision: session.revision,
|
|
507
|
+
planHash: session.planHash
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function recoveredMutationPayload(inspection, idempotencyKeyValue) {
|
|
511
|
+
return {
|
|
512
|
+
ok: true,
|
|
513
|
+
session: sessionSummary(inspection.session),
|
|
514
|
+
execution: inspection.execution,
|
|
515
|
+
recoveredFromStatus: true,
|
|
516
|
+
mutationResponseReceived: false,
|
|
517
|
+
idempotencyKey: idempotencyKeyValue,
|
|
518
|
+
recovery: { inspectCommand: `numa app inspect ${inspection.session.sessionNo} --json` }
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
export function onboardingPublicationAttachmentView(result, source, idempotencyKeyValue, recoveredFromStatus = false) {
|
|
522
|
+
const plan = result.publicationPlan;
|
|
523
|
+
return {
|
|
524
|
+
ok: true,
|
|
525
|
+
session: sessionSummary(result.session),
|
|
526
|
+
publicationPlan: {
|
|
527
|
+
planNo: plan.planNo,
|
|
528
|
+
state: plan.state,
|
|
529
|
+
planHash: plan.planHash,
|
|
530
|
+
repositoryKey: plan.repositoryKey,
|
|
531
|
+
consumerType: plan.consumerType,
|
|
532
|
+
consumerRef: plan.consumerRef,
|
|
533
|
+
publicationMode: plan.publicationMode,
|
|
534
|
+
branch: plan.branch,
|
|
535
|
+
expectedHead: plan.expectedHead,
|
|
536
|
+
fileCount: plan.manifest.length,
|
|
537
|
+
warningCount: plan.warnings.length,
|
|
538
|
+
blockerCount: plan.blockers.length,
|
|
539
|
+
expiresAt: plan.expiresAt
|
|
540
|
+
},
|
|
541
|
+
scan: {
|
|
542
|
+
schemaVersion: source.schemaVersion,
|
|
543
|
+
sourceRevision: source.sourceRevision,
|
|
544
|
+
sourceTreeDigest: source.sourceTreeDigest,
|
|
545
|
+
clean: source.clean,
|
|
546
|
+
fileCount: source.fileCount,
|
|
547
|
+
totalBytes: source.totalBytes,
|
|
548
|
+
executableFileCount: source.executableFileCount
|
|
549
|
+
},
|
|
550
|
+
replayed: result.replayed,
|
|
551
|
+
recoveredFromStatus,
|
|
552
|
+
idempotencyKey: idempotencyKeyValue
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
export function onboardingPublicationResolutionView(result, clientRequestId) {
|
|
556
|
+
const plan = result.publicationPlan;
|
|
557
|
+
return {
|
|
558
|
+
ok: true,
|
|
559
|
+
session: sessionSummary(result.session),
|
|
560
|
+
publicationPlan: {
|
|
561
|
+
planNo: plan.planNo,
|
|
562
|
+
state: plan.state,
|
|
563
|
+
planHash: plan.planHash,
|
|
564
|
+
repositoryKey: plan.repositoryKey,
|
|
565
|
+
consumerType: plan.consumerType,
|
|
566
|
+
consumerRef: plan.consumerRef,
|
|
567
|
+
publicationMode: plan.publicationMode,
|
|
568
|
+
branch: plan.branch,
|
|
569
|
+
expectedHead: plan.expectedHead,
|
|
570
|
+
fileCount: plan.manifest.length,
|
|
571
|
+
warningCount: plan.warnings.length,
|
|
572
|
+
blockerCount: plan.blockers.length,
|
|
573
|
+
expiresAt: plan.expiresAt
|
|
574
|
+
},
|
|
575
|
+
replayed: result.replayed,
|
|
576
|
+
recoveredFromStatus: true,
|
|
577
|
+
clientRequestId
|
|
578
|
+
};
|
|
579
|
+
}
|
|
378
580
|
export async function executeOnboardingAction(client, sessionNo, action) {
|
|
379
581
|
await client[action](sessionNo);
|
|
380
582
|
return inspectOnboardingSession(client, sessionNo);
|
|
@@ -415,6 +617,15 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
415
617
|
结果不明时用相同 idempotency-key 重试或 inspect/resume,禁止新建重复会话。
|
|
416
618
|
`);
|
|
417
619
|
registerApplicationCandidateCommands(app);
|
|
620
|
+
addExamples(app.command("capabilities")
|
|
621
|
+
.description("读取服务端 Application Onboarding 能力自述;命令存在不代表端到端能力可用")
|
|
622
|
+
.action(async (_options, command) => {
|
|
623
|
+
const capabilities = await client().getCapabilities();
|
|
624
|
+
await output(command, { ok: true, capabilities }, JSON.stringify(stableOutput(capabilities), null, 2));
|
|
625
|
+
}), ["numa app capabilities --json"], [
|
|
626
|
+
"与 `numa commands --json`、Repository capability 及各 child control plane 的只读能力共同形成运行时能力矩阵。",
|
|
627
|
+
"HTTP 501 表示服务端能力未发布;不得因为本地命令存在就声称可执行。"
|
|
628
|
+
]);
|
|
418
629
|
addExamples(app.command("options <type>")
|
|
419
630
|
.description("按名称查询应用所需的团队、系统、业务域或环境 ID")
|
|
420
631
|
.option("--search <text>", "按 code 或名称搜索")
|
|
@@ -701,11 +912,13 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
701
912
|
idempotencyKey: options.idempotencyKey
|
|
702
913
|
});
|
|
703
914
|
const execution = onboardingExecutionView(result.session);
|
|
704
|
-
const text = result.
|
|
705
|
-
?
|
|
706
|
-
: result.
|
|
707
|
-
?
|
|
708
|
-
:
|
|
915
|
+
const text = result.recoveredFromStatus
|
|
916
|
+
? `写响应未知,已从同一会话状态恢复:\n${executionText(result.session)}\nidempotency-key: ${result.idempotencyKey}`
|
|
917
|
+
: result.task
|
|
918
|
+
? `入驻执行已提交:\n${executionText(result.session)}\nidempotency-key: ${result.idempotencyKey}`
|
|
919
|
+
: result.preflight
|
|
920
|
+
? `预检完成: ${sessionLine(result.session)}\nplan: ${result.preflight.planHash ?? "-"}\nblocking: ${result.preflight.blockingErrors.length}`
|
|
921
|
+
: `草稿已保存: ${sessionLine(result.session)}`;
|
|
709
922
|
await output(command, { ok: true, ...result, execution }, text);
|
|
710
923
|
}), [
|
|
711
924
|
"numa app onboard",
|
|
@@ -719,6 +932,20 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
719
932
|
]);
|
|
720
933
|
const publicationAction = app.command("publication")
|
|
721
934
|
.description("为 schemaVersion=3 会话附加受控本地源码 Publication 计划");
|
|
935
|
+
addExamples(publicationAction.command("resolve <session-no>")
|
|
936
|
+
.description("按原 clientRequestId 只读恢复 publication attach 的确定结果")
|
|
937
|
+
.requiredOption("--client-request-id <id>", "原 attach 使用的稳定 child request ID")
|
|
938
|
+
.action(async (sessionNo, options, command) => {
|
|
939
|
+
const clientRequestId = stableActionText(options.clientRequestId, "client-request-id", 160);
|
|
940
|
+
const result = await client().resolvePublicationPlan(sessionNo, clientRequestId);
|
|
941
|
+
assertPublicationSessionBinding(result, sessionNo, true);
|
|
942
|
+
await output(command, onboardingPublicationResolutionView(result, clientRequestId), `${result.publicationPlan.planNo}\t${result.publicationPlan.state}\t${result.publicationPlan.planHash}\trecovered-from-status`);
|
|
943
|
+
}), [
|
|
944
|
+
"numa app publication resolve <session-no> --client-request-id onboarding-code-index-publication --json"
|
|
945
|
+
], [
|
|
946
|
+
"固定 GET resolve,不上传 bundle、不重放 attach;404 ONBOARDING_PUBLICATION_PLAN_NOT_RESOLVED 可安全稍后重查。",
|
|
947
|
+
"compact 输出不含 manifest path、文件内容、完整 session draft/plan 或 SCM credential。"
|
|
948
|
+
]);
|
|
722
949
|
addExamples(publicationAction.command("attach <session-no>")
|
|
723
950
|
.description("扫描 clean Git HEAD 并创建绑定当前 onboarding planHash 的 child plan;不写远端 Git")
|
|
724
951
|
.requiredOption("--source <directory>", "本地 Git worktree 根目录")
|
|
@@ -729,12 +956,15 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
729
956
|
.action(async (sessionNo, options, command) => {
|
|
730
957
|
requireConfirmation(options.yes, "Attaching a project publication plan");
|
|
731
958
|
const key = idempotencyKey(options.idempotencyKey, "publication attach");
|
|
732
|
-
const clientRequestId = stableActionText(options.clientRequestId, "client-request-id",
|
|
959
|
+
const clientRequestId = stableActionText(options.clientRequestId, "client-request-id", 160);
|
|
733
960
|
if (key !== clientRequestId) {
|
|
734
961
|
throw new OnboardingError("idempotency-key must exactly match client-request-id for publication attachment.", "onboarding_input_invalid");
|
|
735
962
|
}
|
|
736
963
|
const onboarding = client();
|
|
737
964
|
const session = await onboarding.getSession(sessionNo);
|
|
965
|
+
if (session.state !== "WAITING_EXTERNAL" || session.currentStep !== "ATTACH_PUBLICATION_PLAN") {
|
|
966
|
+
throw new OnboardingError("Publication attachment requires the same parent session to be WAITING_EXTERNAL at ATTACH_PUBLICATION_PLAN.", "onboarding_publication_session_invalid", undefined, undefined, false, { sessionNo: session.sessionNo, state: session.state, currentStep: session.currentStep ?? null });
|
|
967
|
+
}
|
|
738
968
|
const source = await scanPublication(options.source);
|
|
739
969
|
const input = parsedActionInput(OnboardingPublicationPlanActionRequestSchema, {
|
|
740
970
|
parentPlanHash: sha256(session.planHash, "session planHash"),
|
|
@@ -745,19 +975,14 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
745
975
|
commitMessage: options.commitMessage,
|
|
746
976
|
files: source.requestFiles
|
|
747
977
|
});
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
session: result.session,
|
|
752
|
-
publicationPlan: result.publicationPlan,
|
|
753
|
-
replayed: result.replayed,
|
|
754
|
-
scan: publicationScanSummary(source),
|
|
755
|
-
idempotencyKey: key
|
|
756
|
-
}, `${result.publicationPlan.planNo}\t${result.publicationPlan.state}\t${result.publicationPlan.planHash}\treplayed:${result.replayed}`);
|
|
978
|
+
const attempt = await executePublicationAttachmentWithRecovery(onboarding, session.sessionNo, clientRequestId, () => onboarding.attachPublicationPlan(session.sessionNo, input, key));
|
|
979
|
+
const result = attempt.result;
|
|
980
|
+
await output(command, onboardingPublicationAttachmentView(result, source, key, attempt.recoveredFromStatus), `${result.publicationPlan.planNo}\t${result.publicationPlan.state}\t${result.publicationPlan.planHash}\t${attempt.recoveredFromStatus ? "recovered-from-status" : `replayed:${result.replayed}`}`);
|
|
757
981
|
}), [
|
|
758
982
|
"numa app publication attach <session-no> --source ./code-index --commit-message 'feat: publish code-index source' --client-request-id onboarding-code-index-publication --idempotency-key onboarding-code-index-publication --yes --json"
|
|
759
983
|
], [
|
|
760
|
-
"只读取 clean Git HEAD
|
|
984
|
+
"只读取 clean Git HEAD;compact 输出不含 sourceRoot、manifest path、contentBase64、完整 session draft/plan 或 SCM credential。",
|
|
985
|
+
"POST 500/504、连接中断或成功响应无法解析时,CLI 以原 sessionNo/clientRequestId 轮询 GET resolve;绝不自动重放 attach。",
|
|
761
986
|
"该动作只创建 child plan;onboarding worker 仍会等待显式执行和验证。"
|
|
762
987
|
]);
|
|
763
988
|
const jenkinsBindingAction = app.command("jenkins-binding")
|
|
@@ -765,7 +990,7 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
765
990
|
addExamples(jenkinsBindingAction.command("confirm <session-no>")
|
|
766
991
|
.requiredOption("--binding-id <id>", "会话事件返回的候选 binding ID")
|
|
767
992
|
.requiredOption("--expected-version <number>", "候选 binding 当前乐观锁版本")
|
|
768
|
-
.requiredOption("--idempotency-key <key>", "
|
|
993
|
+
.requiredOption("--idempotency-key <key>", "稳定 key;写响应未知时仅用于取证,不自动重发")
|
|
769
994
|
.option("--yes", "确认该 Jenkins job 与应用/tier 的绑定关系")
|
|
770
995
|
.action(async (sessionNo, options, command) => {
|
|
771
996
|
requireConfirmation(options.yes, "Confirming a Jenkins binding");
|
|
@@ -774,7 +999,13 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
774
999
|
bindingId: positiveInteger(options.bindingId, "binding-id"),
|
|
775
1000
|
expectedVersion: nonNegativeInteger(options.expectedVersion, "expected-version", 0)
|
|
776
1001
|
});
|
|
777
|
-
const
|
|
1002
|
+
const onboarding = client();
|
|
1003
|
+
const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.confirmJenkinsBinding(sessionNo, input, key), (event) => hasJenkinsBindingRecoveryEvidence(event, input.bindingId));
|
|
1004
|
+
if (!attempt.result) {
|
|
1005
|
+
await output(command, recoveredMutationPayload(attempt.inspection, key), `${input.bindingId}\trecovered-from-status\t${attempt.inspection.session.state}`);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const result = attempt.result;
|
|
778
1009
|
await output(command, { ok: true, ...result, idempotencyKey: key }, `${result.binding.id}\t${result.binding.confirmationStatus}\tv${result.binding.version}\treplayed:${result.replayed}`);
|
|
779
1010
|
}), [
|
|
780
1011
|
"numa app jenkins-binding confirm <session-no> --binding-id 42 --expected-version 0 --idempotency-key confirm-code-index-binding --yes --json"
|
|
@@ -791,7 +1022,7 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
791
1022
|
.requiredOption("--workload-kind <kind>", "最终 workload kind")
|
|
792
1023
|
.requiredOption("--workload-name <name>", "最终 workload name")
|
|
793
1024
|
.requiredOption("--expected-version <number>", "scope 当前版本;首次创建使用服务端事件给出的版本")
|
|
794
|
-
.requiredOption("--idempotency-key <key>", "
|
|
1025
|
+
.requiredOption("--idempotency-key <key>", "稳定 key;写响应未知时仅用于取证,不自动重发")
|
|
795
1026
|
.option("--yes", "确认精确运行态坐标")
|
|
796
1027
|
.action(async (sessionNo, options, command) => {
|
|
797
1028
|
requireConfirmation(options.yes, "Configuring an observer scope");
|
|
@@ -807,7 +1038,13 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
807
1038
|
workloadName: stableActionText(options.workloadName, "workload-name"),
|
|
808
1039
|
expectedVersion: nonNegativeInteger(options.expectedVersion, "expected-version", 0)
|
|
809
1040
|
});
|
|
810
|
-
const
|
|
1041
|
+
const onboarding = client();
|
|
1042
|
+
const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.configureObserverScope(sessionNo, input, key), hasObserverScopeRecoveryEvidence);
|
|
1043
|
+
if (!attempt.result) {
|
|
1044
|
+
await output(command, recoveredMutationPayload(attempt.inspection, key), `${String(attempt.inspection.execution.eventOutput.observerScopeNo)}\trecovered-from-status\t${attempt.inspection.session.state}`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const result = attempt.result;
|
|
811
1048
|
await output(command, { ok: true, ...result, idempotencyKey: key }, `${result.scope.scopeNo}\t${result.scope.status}\t${result.scope.evidenceFreshness}\t${result.scope.lastDriftState ?? "-"}`);
|
|
812
1049
|
}), [
|
|
813
1050
|
"numa app observer-scope configure <session-no> --service-binding-id 17 --kustomization-namespace flux-system --kustomization-name prd --helm-release-namespace prd --helm-release-name code-index --workload-namespace prd --workload-kind Deployment --workload-name code-index --expected-version 0 --idempotency-key scope-code-index-prd --yes --json"
|
|
@@ -820,7 +1057,7 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
820
1057
|
.option("--parameter <KEY=VALUE>", "非敏感构建参数,可重复", collectParameter, [])
|
|
821
1058
|
.option("--approve-id <id>", "生产 TRIGGER 的审批记录 ID")
|
|
822
1059
|
.option("--production-reason <reason>", "生产 TRIGGER 的受审原因(至少10字符)")
|
|
823
|
-
.requiredOption("--idempotency-key <key>", "
|
|
1060
|
+
.requiredOption("--idempotency-key <key>", "稳定 key;写响应未知时仅用于取证,不自动重发")
|
|
824
1061
|
.option("--yes", "确认记录决定;TRIGGER 会真正提交 Jenkins build")
|
|
825
1062
|
.action(async (sessionNo, options, command) => {
|
|
826
1063
|
requireConfirmation(options.yes, "Recording the first-build decision");
|
|
@@ -833,7 +1070,13 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
833
1070
|
productionReason: options.productionReason?.trim() || undefined
|
|
834
1071
|
});
|
|
835
1072
|
const key = idempotencyKey(options.idempotencyKey, "first-build decision");
|
|
836
|
-
const
|
|
1073
|
+
const onboarding = client();
|
|
1074
|
+
const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.decideFirstBuild(sessionNo, input, key), (event) => hasFirstBuildRecoveryEvidence(event, input));
|
|
1075
|
+
if (!attempt.result) {
|
|
1076
|
+
await output(command, recoveredMutationPayload(attempt.inspection, key), `${decision}\trecovered-from-status\t${attempt.inspection.session.state}`);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const result = attempt.result;
|
|
837
1080
|
await output(command, { ok: true, ...result, idempotencyKey: key }, result.build
|
|
838
1081
|
? `${result.decision}\t${result.build.requestId}\t${result.build.state}\treplayed:${result.replayed}`
|
|
839
1082
|
: `${result.decision}\tno-build\treplayed:${result.replayed}`);
|
|
@@ -860,11 +1103,16 @@ export function registerApplicationOnboardingCommands(program, dependencies = {}
|
|
|
860
1103
|
throw new OnboardingError("Session has no server plan hash; run onboarding preflight first.", "onboarding_plan_missing");
|
|
861
1104
|
}
|
|
862
1105
|
const key = idempotencyKey(options.idempotencyKey);
|
|
863
|
-
const
|
|
1106
|
+
const attempt = await executeOnboardingSessionMutation(onboarding, sessionNo, () => onboarding.apply(sessionNo, { planHash: before.planHash, idempotencyKey: key }), (event) => hasOnboardingApplyRecoveryEvidence(event, before.planHash));
|
|
1107
|
+
if (!attempt.result) {
|
|
1108
|
+
await output(command, recoveredMutationPayload(attempt.inspection, key), `写响应未知,已从同一会话状态恢复:\n${executionText(attempt.inspection.session, attempt.inspection.events)}\nidempotency-key: ${key}`);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const task = attempt.result;
|
|
864
1112
|
const session = await onboarding.getSession(sessionNo);
|
|
865
1113
|
await output(command, { ok: true, session, task, idempotencyKey: key, execution: onboardingExecutionView(session) }, `入驻执行已提交:\n${executionText(session)}\nidempotency-key: ${key}`);
|
|
866
1114
|
}), ["numa app apply 8d15ee89-43f0-4378-a7aa-51f09c5b42fc --idempotency-key app-order-20260819 --json"], [
|
|
867
|
-
"用于先 --plan
|
|
1115
|
+
"用于先 --plan 后的首次显式 apply;响应不明时只读 inspect/status,证据不足返回 onboarding_result_unknown,不要重发 apply。"
|
|
868
1116
|
]);
|
|
869
1117
|
addExamples(app.command("inspect <session-no>")
|
|
870
1118
|
.description("查询会话;watch 从最后 event sequence 持续恢复")
|