run402-mcp 3.7.5 → 3.7.7
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 +4 -5
- package/package.json +1 -1
- package/sdk/README.md +4 -5
- package/sdk/dist/actions.d.ts +1 -1
- package/sdk/dist/actions.d.ts.map +1 -1
- package/sdk/dist/index.d.ts +1 -0
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +2 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/apps.d.ts +34 -0
- package/sdk/dist/namespaces/apps.d.ts.map +1 -1
- package/sdk/dist/namespaces/apps.js +19 -0
- package/sdk/dist/namespaces/apps.js.map +1 -1
- package/sdk/dist/node/actions-node.d.ts.map +1 -1
- package/sdk/dist/node/actions-node.js +617 -25
- package/sdk/dist/node/actions-node.js.map +1 -1
- package/sdk/dist/scoped.d.ts +5 -1
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +6 -0
- package/sdk/dist/scoped.js.map +1 -1
- package/README.zh-CN.md +0 -169
|
@@ -3,11 +3,12 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve as resolvePath } from "node:path";
|
|
6
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
7
|
import { promisify } from "node:util";
|
|
7
8
|
import { Run402Action } from "../actions.js";
|
|
8
9
|
import { RUN402_APP_MANIFEST_FILENAME, compileRun402AppInstallGraph, createRun402AppUpResult, } from "../app-up.js";
|
|
9
10
|
import { LocalError } from "../errors.js";
|
|
10
|
-
import { loadDeployManifest } from "./deploy-manifest.js";
|
|
11
|
+
import { loadDeployManifest, normalizeDeployManifest } from "./deploy-manifest.js";
|
|
11
12
|
const DEFAULT_BOOTSTRAP_TIER = "prototype";
|
|
12
13
|
const MANIFEST_CANDIDATES = [
|
|
13
14
|
RUN402_APP_MANIFEST_FILENAME,
|
|
@@ -154,13 +155,7 @@ export class NodeActions {
|
|
|
154
155
|
app_result: appResult,
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
|
-
|
|
158
|
-
return run.result({
|
|
159
|
-
project_id: input.projectId ?? "prj_planned",
|
|
160
|
-
manifest_path: manifest.manifestPath,
|
|
161
|
-
app_graph: manifest.appGraph,
|
|
162
|
-
app_result: blocked,
|
|
163
|
-
});
|
|
158
|
+
return this.#applyAppManifest(input, manifest, workspaceDir, run, startedAt);
|
|
164
159
|
}
|
|
165
160
|
if (run.executionMode === "check" || run.executionMode === "printSpec") {
|
|
166
161
|
if (!manifest.releaseSpec) {
|
|
@@ -336,6 +331,7 @@ export class NodeActions {
|
|
|
336
331
|
releaseSpec: null,
|
|
337
332
|
appGraph,
|
|
338
333
|
appSpec,
|
|
334
|
+
source,
|
|
339
335
|
};
|
|
340
336
|
}
|
|
341
337
|
const loaded = await loadDeployManifest(manifestPath, {
|
|
@@ -448,6 +444,9 @@ export class NodeActions {
|
|
|
448
444
|
}
|
|
449
445
|
#blockedAppUpResult(input, manifest, run, startedAt) {
|
|
450
446
|
const block = this.#firstAppUpBlock(input, manifest, run);
|
|
447
|
+
if (!block) {
|
|
448
|
+
throw run.error("Internal error: app manifest was marked blocked without a blocking condition.", "RUN402_ACTION_INTERNAL");
|
|
449
|
+
}
|
|
451
450
|
return this.#planAppUpResult(input, manifest, run, {
|
|
452
451
|
startedAt,
|
|
453
452
|
status: "blocked",
|
|
@@ -500,22 +499,483 @@ export class NodeActions {
|
|
|
500
499
|
const buildBlock = appBuildBlock(input, manifest);
|
|
501
500
|
if (buildBlock)
|
|
502
501
|
return buildBlock;
|
|
503
|
-
return
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
async #applyAppManifest(input, manifest, workspaceDir, run, startedAt) {
|
|
505
|
+
if (!manifest.appSpec || !manifest.appGraph) {
|
|
506
|
+
throw run.error("Internal error: app manifest did not produce an install graph.", "RUN402_ACTION_INTERNAL");
|
|
507
|
+
}
|
|
508
|
+
const block = this.#firstAppUpBlock(input, manifest, run);
|
|
509
|
+
if (block) {
|
|
510
|
+
const blocked = this.#planAppUpResult(input, manifest, run, {
|
|
511
|
+
startedAt,
|
|
512
|
+
status: "blocked",
|
|
513
|
+
dryRun: false,
|
|
514
|
+
projectId: input.projectId ?? null,
|
|
515
|
+
diagnostics: block.diagnostics,
|
|
516
|
+
nextActions: block.nextActions,
|
|
517
|
+
blockedNodeId: block.nodeId,
|
|
518
|
+
});
|
|
519
|
+
return run.result({
|
|
520
|
+
project_id: input.projectId ?? "prj_planned",
|
|
521
|
+
manifest_path: manifest.manifestPath,
|
|
522
|
+
app_graph: manifest.appGraph,
|
|
523
|
+
app_result: blocked,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (this.#targetKind() === "core") {
|
|
527
|
+
const blocked = this.#planAppUpResult(input, manifest, run, {
|
|
528
|
+
startedAt,
|
|
529
|
+
status: "blocked",
|
|
530
|
+
dryRun: false,
|
|
531
|
+
projectId: input.projectId ?? null,
|
|
532
|
+
diagnostics: [{
|
|
533
|
+
code: "UNSUPPORTED_RESOURCE_KIND",
|
|
534
|
+
severity: "error",
|
|
535
|
+
node_id: "project.ensure",
|
|
536
|
+
message: "App-aware up currently targets Run402 Cloud because it provisions Cloud projects, mailboxes, secrets, and managed subdomains.",
|
|
537
|
+
}],
|
|
538
|
+
nextActions: [{
|
|
539
|
+
type: "choose_cloud_target",
|
|
540
|
+
code: "UNSUPPORTED_RESOURCE_KIND",
|
|
541
|
+
node_id: "project.ensure",
|
|
542
|
+
message: "Select the Run402 Cloud target, then retry.",
|
|
543
|
+
argv: ["run402", "init", "--api-base", "https://api.run402.com"],
|
|
544
|
+
}],
|
|
545
|
+
blockedNodeId: "project.ensure",
|
|
546
|
+
});
|
|
547
|
+
return run.result({
|
|
548
|
+
project_id: input.projectId ?? "prj_planned",
|
|
549
|
+
manifest_path: manifest.manifestPath,
|
|
550
|
+
app_graph: manifest.appGraph,
|
|
551
|
+
app_result: blocked,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
await this.#ensureCloudTier(run, input.tier ?? DEFAULT_BOOTSTRAP_TIER);
|
|
555
|
+
const resolved = await this.#resolveProject(input, manifest, workspaceDir, run);
|
|
556
|
+
await this.#recordAppInstallState({
|
|
557
|
+
projectId: resolved.projectId,
|
|
558
|
+
manifest,
|
|
559
|
+
status: "applying",
|
|
560
|
+
run,
|
|
561
|
+
});
|
|
562
|
+
if (resolved.shouldWriteLink && run.executionMode !== "plan") {
|
|
563
|
+
await this.#writeWorkspaceProjectLink(resolved.linkPath, {
|
|
564
|
+
schema_version: "run402.workspace-project.v1",
|
|
565
|
+
project_id: resolved.projectId,
|
|
566
|
+
...(input.name ? { name: input.name } : {}),
|
|
567
|
+
target: { kind: this.#targetKind() },
|
|
568
|
+
created_at: resolved.link?.created_at ?? new Date().toISOString(),
|
|
569
|
+
updated_at: new Date().toISOString(),
|
|
570
|
+
}, resolved.link, run);
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const projectKeys = await this.sdk.projects.keys(resolved.projectId);
|
|
574
|
+
const publicOrigin = appPublicOrigin(input, manifest.appSpec) ?? projectKeys.site_url ?? null;
|
|
575
|
+
const resources = await this.#ensureAppMailboxes(resolved.projectId, manifest.appSpec, run);
|
|
576
|
+
const env = {
|
|
577
|
+
...this.#generatedAppEnv(resolved.projectId, projectKeys, publicOrigin, resources),
|
|
578
|
+
...this.#userSecretEnv(manifest.appSpec),
|
|
579
|
+
};
|
|
580
|
+
resources.env = env;
|
|
581
|
+
await this.#ensureAppSecrets(resolved.projectId, manifest.appSpec, env, run);
|
|
582
|
+
await this.#runAppBuild(workspaceDir, manifest.appSpec, env, run);
|
|
583
|
+
const releaseInput = materializeTemplates(manifest.appSpec.release, {
|
|
584
|
+
...env,
|
|
585
|
+
"input.name": input.name ?? manifest.appSpec.project.name ?? "",
|
|
586
|
+
});
|
|
587
|
+
const normalized = await normalizeDeployManifest(releaseInput, {
|
|
588
|
+
project: resolved.projectId,
|
|
589
|
+
baseDir: dirname(manifest.manifestPath),
|
|
590
|
+
});
|
|
591
|
+
if (!hasDeployableContent(normalized.spec)) {
|
|
592
|
+
throw run.error("App release contains no deployable sections.", "MANIFEST_EMPTY", { manifest_path: manifest.manifestPath });
|
|
593
|
+
}
|
|
594
|
+
const deployStep = run.addStep({
|
|
595
|
+
action: "deploy.apply",
|
|
596
|
+
description: "Apply app release",
|
|
597
|
+
mutation: true,
|
|
598
|
+
auto: false,
|
|
599
|
+
details: {
|
|
600
|
+
project_id: resolved.projectId,
|
|
601
|
+
manifest_path: manifest.manifestPath,
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
run.setState(deployStep, "running");
|
|
605
|
+
const scoped = await this.sdk.project(resolved.projectId);
|
|
606
|
+
const deploy = await scoped.apply(normalized.spec, {
|
|
607
|
+
idempotencyKey: input.idempotencyKey ?? normalized.idempotencyKey,
|
|
608
|
+
allowWarnings: input.allowWarnings,
|
|
609
|
+
allowWarningCodes: input.allowWarningCodes,
|
|
610
|
+
target: "cloud",
|
|
611
|
+
onEvent: (event) => {
|
|
612
|
+
run.options.onEvent?.({
|
|
613
|
+
type: "action.step",
|
|
614
|
+
action: Run402Action.Up,
|
|
615
|
+
step: {
|
|
616
|
+
...deployStep,
|
|
617
|
+
details: { ...deployStep.details, deploy_event: event.type },
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
run.setState(deployStep, "succeeded", {
|
|
623
|
+
release_id: deploy.release_id,
|
|
624
|
+
operation_id: deploy.operation_id,
|
|
625
|
+
});
|
|
626
|
+
const webhooks = await this.#ensureAppWebhooks(resolved.projectId, manifest.appSpec, resources, run);
|
|
627
|
+
resources.webhooks = webhooks;
|
|
628
|
+
const verification = await this.#verifyAppHttp(manifest.appSpec, publicOrigin, run);
|
|
629
|
+
markAppGraphNodes(manifest.appGraph, verification.ok ? "succeeded" : "failed");
|
|
630
|
+
const appResult = createRun402AppUpResult({
|
|
631
|
+
graph: manifest.appGraph,
|
|
632
|
+
manifest_path: manifest.manifestPath,
|
|
633
|
+
status: verification.ok ? "succeeded" : "deployed_unverified",
|
|
634
|
+
started_at: startedAt,
|
|
635
|
+
dry_run: false,
|
|
636
|
+
project_id: resolved.projectId,
|
|
637
|
+
project_name: input.name ?? manifest.appSpec.project.name ?? null,
|
|
638
|
+
public_origin: publicOrigin,
|
|
639
|
+
operation_id: deploy.operation_id ?? null,
|
|
640
|
+
diagnostics: verification.diagnostics,
|
|
641
|
+
approval_policy: {
|
|
642
|
+
yes: run.approval === "yes",
|
|
643
|
+
allow_prune: input.allowPrune === true,
|
|
644
|
+
max_spend_usd: input.maxSpendUsd ?? null,
|
|
645
|
+
build_mode: input.buildMode ?? manifest.appSpec.build?.mode ?? null,
|
|
646
|
+
shell_build_approved: input.allowShellBuild === true,
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
applyResourceStateToAppResult(appResult, resources);
|
|
650
|
+
appResult.release.operation_id = deploy.operation_id ?? null;
|
|
651
|
+
appResult.release.release_id = deploy.release_id ?? null;
|
|
652
|
+
appResult.release.spec = normalized.manifest;
|
|
653
|
+
for (const check of appResult.verification.http) {
|
|
654
|
+
const actual = verification.results.get(check.id);
|
|
655
|
+
if (actual !== undefined) {
|
|
656
|
+
check.actual_status = actual;
|
|
657
|
+
check.status = actual === check.expected_status ? "succeeded" : "failed";
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
await this.#recordAppInstallState({
|
|
661
|
+
projectId: resolved.projectId,
|
|
662
|
+
manifest,
|
|
663
|
+
status: verification.ok ? "active" : "failed",
|
|
664
|
+
run,
|
|
665
|
+
resources,
|
|
666
|
+
lastOperationId: deploy.operation_id ?? null,
|
|
667
|
+
error: verification.ok ? null : { code: "VERIFY_FAILED", diagnostics: verification.diagnostics },
|
|
668
|
+
});
|
|
669
|
+
return run.result({
|
|
670
|
+
project_id: resolved.projectId,
|
|
671
|
+
manifest_path: manifest.manifestPath,
|
|
672
|
+
...(resolved.shouldWriteLink ? { workspace_link_path: resolved.linkPath } : {}),
|
|
673
|
+
app_graph: manifest.appGraph,
|
|
674
|
+
deploy,
|
|
675
|
+
app_result: appResult,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
catch (err) {
|
|
679
|
+
await this.#recordAppInstallState({
|
|
680
|
+
projectId: resolved.projectId,
|
|
681
|
+
manifest,
|
|
682
|
+
status: "failed",
|
|
683
|
+
run,
|
|
684
|
+
error: actionErrorDetails(err),
|
|
685
|
+
});
|
|
686
|
+
throw err;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async #recordAppInstallState(input) {
|
|
690
|
+
if (!input.manifest.appSpec || !input.manifest.appGraph)
|
|
691
|
+
return;
|
|
692
|
+
const step = input.run.addStep({
|
|
693
|
+
action: "app.install",
|
|
694
|
+
description: input.status === "applying"
|
|
695
|
+
? "Record app install state"
|
|
696
|
+
: "Record app install result",
|
|
697
|
+
mutation: true,
|
|
698
|
+
auto: true,
|
|
699
|
+
details: {
|
|
700
|
+
project_id: input.projectId,
|
|
701
|
+
app_key: input.manifest.appSpec.app.id,
|
|
702
|
+
status: input.status,
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
input.run.setState(step, "running");
|
|
706
|
+
try {
|
|
707
|
+
await this.sdk.apps.upsertInstallState({
|
|
708
|
+
project_id: input.projectId,
|
|
709
|
+
app_key: input.manifest.appSpec.app.id,
|
|
710
|
+
status: input.status,
|
|
711
|
+
manifest_digest: `sha256:${sha256Hex(stableJson(input.manifest.appSpec))}`,
|
|
712
|
+
graph_digest: input.manifest.appGraph.graph_digest,
|
|
713
|
+
source: input.manifest.source,
|
|
714
|
+
manifest: input.manifest.appSpec,
|
|
715
|
+
resources: input.resources
|
|
716
|
+
? {
|
|
717
|
+
mailboxes: input.resources.mailboxes,
|
|
718
|
+
webhooks: input.resources.webhooks,
|
|
719
|
+
}
|
|
720
|
+
: {},
|
|
721
|
+
bindings: input.resources
|
|
722
|
+
? { env: Object.keys(input.resources.env).sort() }
|
|
723
|
+
: {},
|
|
724
|
+
last_operation_id: input.lastOperationId ?? null,
|
|
725
|
+
error: input.error ?? null,
|
|
726
|
+
});
|
|
727
|
+
input.run.setState(step, "succeeded");
|
|
728
|
+
}
|
|
729
|
+
catch (err) {
|
|
730
|
+
input.run.setState(step, "skipped", {
|
|
731
|
+
reason: "app_install_state_unavailable",
|
|
732
|
+
error: err instanceof Error ? err.message : String(err),
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async #ensureAppMailboxes(projectId, spec, run) {
|
|
737
|
+
const state = { mailboxes: {}, webhooks: {}, env: {} };
|
|
738
|
+
const entries = Object.entries(spec.resources?.mailboxes ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
739
|
+
if (entries.length === 0)
|
|
740
|
+
return state;
|
|
741
|
+
const step = run.addStep({
|
|
742
|
+
action: "app.mailbox.ensure",
|
|
743
|
+
description: "Ensure app mailboxes",
|
|
744
|
+
mutation: true,
|
|
745
|
+
auto: true,
|
|
746
|
+
details: { project_id: projectId, count: entries.length },
|
|
747
|
+
});
|
|
748
|
+
await run.approve(step, ["app.mailbox.ensure"], "Create or reuse app mailboxes.");
|
|
749
|
+
run.setState(step, "running");
|
|
750
|
+
let envelope = await this.sdk.email.listMailboxes(projectId);
|
|
751
|
+
for (const [logicalName, mailboxSpec] of entries) {
|
|
752
|
+
const slug = appMailboxSlug(logicalName, mailboxSpec);
|
|
753
|
+
let mailbox = envelope.mailboxes.find((candidate) => candidate.slug === slug);
|
|
754
|
+
if (!mailbox) {
|
|
755
|
+
mailbox = await this.sdk.email.createMailbox(projectId, slug);
|
|
756
|
+
envelope = await this.sdk.email.listMailboxes(projectId);
|
|
757
|
+
}
|
|
758
|
+
state.mailboxes[logicalName] = {
|
|
759
|
+
id: mailbox.mailbox_id,
|
|
760
|
+
slug: mailbox.slug,
|
|
761
|
+
address: mailbox.address,
|
|
762
|
+
...(mailbox.managed_address ? { managed_address: mailbox.managed_address } : {}),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const defaults = {};
|
|
766
|
+
for (const [logicalName, mailboxSpec] of entries) {
|
|
767
|
+
const mailbox = state.mailboxes[logicalName];
|
|
768
|
+
if (!mailbox)
|
|
769
|
+
continue;
|
|
770
|
+
if (mailboxSpec.roles?.includes("default_outbound"))
|
|
771
|
+
defaults.default_outbound_mailbox_id = mailbox.id;
|
|
772
|
+
if (mailboxSpec.roles?.includes("auth_sender"))
|
|
773
|
+
defaults.auth_sender_mailbox_id = mailbox.id;
|
|
774
|
+
}
|
|
775
|
+
if ((defaults.default_outbound_mailbox_id && envelope.mailbox_settings?.default_outbound_mailbox_id !== defaults.default_outbound_mailbox_id) ||
|
|
776
|
+
(defaults.auth_sender_mailbox_id && envelope.mailbox_settings?.auth_sender_mailbox_id !== defaults.auth_sender_mailbox_id)) {
|
|
777
|
+
await this.sdk.email.setMailboxDefaults(projectId, defaults);
|
|
778
|
+
}
|
|
779
|
+
run.setState(step, "succeeded", {
|
|
780
|
+
mailboxes: Object.fromEntries(Object.entries(state.mailboxes).map(([name, mailbox]) => [
|
|
781
|
+
name,
|
|
782
|
+
{ mailbox_id: mailbox.id, slug: mailbox.slug, address: mailbox.address },
|
|
783
|
+
])),
|
|
784
|
+
});
|
|
785
|
+
return state;
|
|
786
|
+
}
|
|
787
|
+
#generatedAppEnv(projectId, projectKeys, publicOrigin, resources) {
|
|
788
|
+
const env = {
|
|
789
|
+
RUN402_PROJECT_ID: projectId,
|
|
790
|
+
RUN402_ANON_KEY: projectKeys.anon_key,
|
|
791
|
+
RUN402_SERVICE_KEY: projectKeys.service_key,
|
|
792
|
+
RUN402_API_BASE: this.sdk.apiBase,
|
|
793
|
+
RUN402_API_BASE_URL: this.sdk.apiBase,
|
|
518
794
|
};
|
|
795
|
+
if (publicOrigin)
|
|
796
|
+
env.RUN402_PUBLIC_ORIGIN = publicOrigin;
|
|
797
|
+
for (const [logicalName, mailbox] of Object.entries(resources.mailboxes)) {
|
|
798
|
+
const suffix = logicalName.toUpperCase();
|
|
799
|
+
env[`RUN402_MAILBOX_${suffix}_ID`] = mailbox.id;
|
|
800
|
+
env[`RUN402_MAILBOX_${suffix}_ADDRESS`] = mailbox.address;
|
|
801
|
+
if (mailbox.managed_address)
|
|
802
|
+
env[`RUN402_MAILBOX_${suffix}_MANAGED_ADDRESS`] = mailbox.managed_address;
|
|
803
|
+
}
|
|
804
|
+
return env;
|
|
805
|
+
}
|
|
806
|
+
#userSecretEnv(spec) {
|
|
807
|
+
const env = {};
|
|
808
|
+
for (const [name, secret] of Object.entries(spec.secrets ?? {})) {
|
|
809
|
+
const sourceEnv = secret.source_env ?? name;
|
|
810
|
+
const value = process.env[sourceEnv];
|
|
811
|
+
if (value !== undefined)
|
|
812
|
+
env[name] = value;
|
|
813
|
+
}
|
|
814
|
+
return env;
|
|
815
|
+
}
|
|
816
|
+
async #ensureAppSecrets(projectId, spec, env, run) {
|
|
817
|
+
const required = new Set();
|
|
818
|
+
for (const [name, secret] of Object.entries(spec.secrets ?? {})) {
|
|
819
|
+
if (secret.required === true || process.env[secret.source_env ?? name] !== undefined)
|
|
820
|
+
required.add(name);
|
|
821
|
+
}
|
|
822
|
+
for (const name of releaseRequiredSecretNames(spec.release))
|
|
823
|
+
required.add(name);
|
|
824
|
+
const names = [...required].sort();
|
|
825
|
+
if (names.length === 0)
|
|
826
|
+
return;
|
|
827
|
+
const step = run.addStep({
|
|
828
|
+
action: "app.secret.ensure",
|
|
829
|
+
description: "Ensure app runtime secrets",
|
|
830
|
+
mutation: true,
|
|
831
|
+
auto: true,
|
|
832
|
+
details: { project_id: projectId, keys: names },
|
|
833
|
+
});
|
|
834
|
+
await run.approve(step, ["app.secret.ensure"], "Set generated and user-provided app runtime secrets.");
|
|
835
|
+
run.setState(step, "running");
|
|
836
|
+
for (const name of names) {
|
|
837
|
+
const value = env[name];
|
|
838
|
+
if (value === undefined) {
|
|
839
|
+
throw run.error(`App secret ${name} is required by the release but no generated or user-provided value is available.`, "MISSING_SECRET", { key: name });
|
|
840
|
+
}
|
|
841
|
+
await this.sdk.secrets.set(projectId, name, { value });
|
|
842
|
+
}
|
|
843
|
+
run.setState(step, "succeeded", { keys: names });
|
|
844
|
+
}
|
|
845
|
+
async #runAppBuild(workspaceDir, spec, env, run) {
|
|
846
|
+
const commands = spec.build?.commands ?? [];
|
|
847
|
+
if (commands.length === 0)
|
|
848
|
+
return;
|
|
849
|
+
const mode = spec.build?.mode ?? "local";
|
|
850
|
+
if (mode !== "local") {
|
|
851
|
+
throw run.error(`${mode} build mode is not executable by this SDK build.`, "REMOTE_BUILD_UNSUPPORTED", { mode });
|
|
852
|
+
}
|
|
853
|
+
const step = run.addStep({
|
|
854
|
+
action: "app.build",
|
|
855
|
+
description: "Run app build commands",
|
|
856
|
+
mutation: true,
|
|
857
|
+
auto: false,
|
|
858
|
+
details: { mode, commands: commands.map((command) => command.id) },
|
|
859
|
+
});
|
|
860
|
+
run.setState(step, "running");
|
|
861
|
+
for (const command of commands) {
|
|
862
|
+
const cwd = resolveMaybe(workspaceDir, command.cwd ?? ".");
|
|
863
|
+
if (command.argv && command.argv.length > 0) {
|
|
864
|
+
const [file, ...args] = command.argv;
|
|
865
|
+
await execFileAsync(file, args, {
|
|
866
|
+
cwd,
|
|
867
|
+
env: { ...process.env, ...env },
|
|
868
|
+
maxBuffer: 1024 * 1024 * 16,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
else if (command.shell) {
|
|
872
|
+
await execFileAsync("/bin/sh", ["-c", command.shell], {
|
|
873
|
+
cwd,
|
|
874
|
+
env: { ...process.env, ...env },
|
|
875
|
+
maxBuffer: 1024 * 1024 * 16,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
throw run.error(`Build command ${command.id} must define argv or shell.`, "APP_SPEC_INVALID", { command_id: command.id });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
run.setState(step, "succeeded");
|
|
883
|
+
}
|
|
884
|
+
async #ensureAppWebhooks(projectId, spec, resources, run) {
|
|
885
|
+
const entries = Object.entries(spec.resources?.webhooks ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
886
|
+
const webhooks = {};
|
|
887
|
+
if (entries.length === 0)
|
|
888
|
+
return webhooks;
|
|
889
|
+
const step = run.addStep({
|
|
890
|
+
action: "app.webhook.ensure",
|
|
891
|
+
description: "Ensure app mailbox webhooks",
|
|
892
|
+
mutation: true,
|
|
893
|
+
auto: true,
|
|
894
|
+
details: { project_id: projectId, count: entries.length },
|
|
895
|
+
});
|
|
896
|
+
await run.approve(step, ["app.webhook.ensure"], "Create or reuse app mailbox webhooks.");
|
|
897
|
+
run.setState(step, "running");
|
|
898
|
+
for (const [logicalName, webhook] of entries) {
|
|
899
|
+
const mailbox = resources.mailboxes[webhook.mailbox];
|
|
900
|
+
if (!mailbox) {
|
|
901
|
+
throw run.error(`Webhook ${logicalName} references unknown mailbox ${webhook.mailbox}.`, "APP_SPEC_INVALID", { webhook: logicalName, mailbox: webhook.mailbox });
|
|
902
|
+
}
|
|
903
|
+
const url = materializeTemplates(webhook.url, resources.env);
|
|
904
|
+
const events = [...webhook.events].sort();
|
|
905
|
+
const current = await this.sdk.email.webhooks.list(projectId, { mailbox: mailbox.id });
|
|
906
|
+
const existing = current.webhooks.find((candidate) => candidate.url === url && sameStringSet(candidate.events ?? [], events));
|
|
907
|
+
const summary = existing ?? await this.sdk.email.webhooks.register(projectId, {
|
|
908
|
+
mailbox: mailbox.id,
|
|
909
|
+
url,
|
|
910
|
+
events,
|
|
911
|
+
});
|
|
912
|
+
webhooks[logicalName] = {
|
|
913
|
+
id: summary.webhook_id,
|
|
914
|
+
mailbox: webhook.mailbox,
|
|
915
|
+
url,
|
|
916
|
+
events,
|
|
917
|
+
created: existing === undefined,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
run.setState(step, "succeeded", { webhooks });
|
|
921
|
+
return webhooks;
|
|
922
|
+
}
|
|
923
|
+
async #verifyAppHttp(spec, publicOrigin, run) {
|
|
924
|
+
const checks = spec.verify?.http ?? [];
|
|
925
|
+
const results = new Map();
|
|
926
|
+
const diagnostics = [];
|
|
927
|
+
if (checks.length === 0)
|
|
928
|
+
return { ok: true, results, diagnostics };
|
|
929
|
+
const step = run.addStep({
|
|
930
|
+
action: "app.verify",
|
|
931
|
+
description: "Verify app HTTP checks",
|
|
932
|
+
mutation: false,
|
|
933
|
+
auto: true,
|
|
934
|
+
details: { count: checks.length },
|
|
935
|
+
});
|
|
936
|
+
run.setState(step, "running");
|
|
937
|
+
for (const check of checks) {
|
|
938
|
+
const url = check.url ?? (publicOrigin && check.path ? new URL(check.path, publicOrigin).toString() : null);
|
|
939
|
+
if (!url) {
|
|
940
|
+
results.set(check.id, null);
|
|
941
|
+
diagnostics.push({
|
|
942
|
+
code: "VERIFY_FAILED",
|
|
943
|
+
severity: "error",
|
|
944
|
+
node_id: `verify.http.${check.id}`,
|
|
945
|
+
message: `HTTP verification ${check.id} needs either url or project public origin + path.`,
|
|
946
|
+
});
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const retries = Math.max(1, check.retries ?? 1);
|
|
950
|
+
let status = null;
|
|
951
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
952
|
+
try {
|
|
953
|
+
const res = await fetch(url, { method: "GET" });
|
|
954
|
+
status = res.status;
|
|
955
|
+
if (status === check.expect.status)
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
catch {
|
|
959
|
+
status = null;
|
|
960
|
+
}
|
|
961
|
+
if (attempt + 1 < retries)
|
|
962
|
+
await delay(1_000);
|
|
963
|
+
}
|
|
964
|
+
results.set(check.id, status);
|
|
965
|
+
if (status !== check.expect.status) {
|
|
966
|
+
diagnostics.push({
|
|
967
|
+
code: "VERIFY_FAILED",
|
|
968
|
+
severity: "error",
|
|
969
|
+
node_id: `verify.http.${check.id}`,
|
|
970
|
+
message: `HTTP verification ${check.id} expected ${check.expect.status}, got ${status ?? "network_error"}.`,
|
|
971
|
+
details: { url, expected_status: check.expect.status, actual_status: status },
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
run.setState(step, diagnostics.length === 0 ? "succeeded" : "failed", {
|
|
976
|
+
checks: Object.fromEntries(results),
|
|
977
|
+
});
|
|
978
|
+
return { ok: diagnostics.length === 0, results, diagnostics };
|
|
519
979
|
}
|
|
520
980
|
async #ensureAllowance(run, opts = { fund: false }) {
|
|
521
981
|
const status = await this.sdk.allowance.status();
|
|
@@ -671,8 +1131,9 @@ export class NodeActions {
|
|
|
671
1131
|
},
|
|
672
1132
|
});
|
|
673
1133
|
run.setState(step, "running");
|
|
674
|
-
|
|
675
|
-
|
|
1134
|
+
const linkConflict = workspaceLinkConflict(link, input, this.#targetKind(), linkPath);
|
|
1135
|
+
if (linkConflict && !input.projectId) {
|
|
1136
|
+
throw run.error(linkConflict.message, "RUN402_WORKSPACE_LINK_CONFLICT", linkConflict.details);
|
|
676
1137
|
}
|
|
677
1138
|
let projectId = null;
|
|
678
1139
|
let source = null;
|
|
@@ -978,6 +1439,71 @@ function appPublicOrigin(input, spec) {
|
|
|
978
1439
|
return null;
|
|
979
1440
|
return `https://${subdomain}.run402.com`;
|
|
980
1441
|
}
|
|
1442
|
+
function materializeTemplates(value, values) {
|
|
1443
|
+
if (typeof value === "string") {
|
|
1444
|
+
return value.replace(/\$\{([^}]+)\}/g, (_match, key) => values[key] ?? "");
|
|
1445
|
+
}
|
|
1446
|
+
if (Array.isArray(value))
|
|
1447
|
+
return value.map((item) => materializeTemplates(item, values));
|
|
1448
|
+
if (value && typeof value === "object") {
|
|
1449
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [
|
|
1450
|
+
key,
|
|
1451
|
+
materializeTemplates(nested, values),
|
|
1452
|
+
]));
|
|
1453
|
+
}
|
|
1454
|
+
return value;
|
|
1455
|
+
}
|
|
1456
|
+
function appMailboxSlug(logicalName, spec) {
|
|
1457
|
+
return spec.slug ?? logicalName.replace(/_/g, "-");
|
|
1458
|
+
}
|
|
1459
|
+
function releaseRequiredSecretNames(release) {
|
|
1460
|
+
const secrets = release.secrets;
|
|
1461
|
+
if (!secrets || typeof secrets !== "object" || Array.isArray(secrets))
|
|
1462
|
+
return [];
|
|
1463
|
+
const required = secrets.require;
|
|
1464
|
+
return Array.isArray(required)
|
|
1465
|
+
? required.filter((value) => typeof value === "string")
|
|
1466
|
+
: [];
|
|
1467
|
+
}
|
|
1468
|
+
function sameStringSet(a, b) {
|
|
1469
|
+
if (a.length !== b.length)
|
|
1470
|
+
return false;
|
|
1471
|
+
const left = [...a].sort();
|
|
1472
|
+
const right = [...b].sort();
|
|
1473
|
+
return left.every((value, index) => value === right[index]);
|
|
1474
|
+
}
|
|
1475
|
+
function markAppGraphNodes(graph, status) {
|
|
1476
|
+
for (const node of graph.nodes) {
|
|
1477
|
+
node.status = status;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function applyResourceStateToAppResult(appResult, resources) {
|
|
1481
|
+
for (const [name, mailbox] of Object.entries(resources.mailboxes)) {
|
|
1482
|
+
if (!appResult.resources.mailboxes[name])
|
|
1483
|
+
continue;
|
|
1484
|
+
appResult.resources.mailboxes[name].id = mailbox.id;
|
|
1485
|
+
appResult.resources.mailboxes[name].address = mailbox.address;
|
|
1486
|
+
}
|
|
1487
|
+
for (const [name, webhook] of Object.entries(resources.webhooks)) {
|
|
1488
|
+
appResult.resources.webhooks[name] = {
|
|
1489
|
+
id: webhook.id,
|
|
1490
|
+
mailbox: webhook.mailbox,
|
|
1491
|
+
url: webhook.url,
|
|
1492
|
+
events: webhook.events,
|
|
1493
|
+
enabled: true,
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
appResult.resources.bindings = appResult.resources.bindings.map((binding) => {
|
|
1497
|
+
const value = resources.env[binding.env];
|
|
1498
|
+
return value === undefined
|
|
1499
|
+
? binding
|
|
1500
|
+
: { ...binding, value, redacted: /KEY|SECRET|TOKEN/.test(binding.env) };
|
|
1501
|
+
});
|
|
1502
|
+
appResult.resources.user_secrets = Object.fromEntries(Object.entries(appResult.resources.user_secrets).map(([name, secret]) => [
|
|
1503
|
+
name,
|
|
1504
|
+
{ ...secret, satisfied: resources.env[name] !== undefined },
|
|
1505
|
+
]));
|
|
1506
|
+
}
|
|
981
1507
|
function spendApprovalBlock(input, run) {
|
|
982
1508
|
const expectedPrototypeSpendUsd = 0.10;
|
|
983
1509
|
if (input.maxSpendUsd !== undefined && input.maxSpendUsd < expectedPrototypeSpendUsd) {
|
|
@@ -1276,6 +1802,49 @@ function workspaceProjectLinksEqual(a, b) {
|
|
|
1276
1802
|
(a.target?.kind ?? null) === (b.target?.kind ?? null) &&
|
|
1277
1803
|
(a.target?.api_base ?? null) === (b.target?.api_base ?? null));
|
|
1278
1804
|
}
|
|
1805
|
+
function workspaceLinkConflict(link, input, targetKind, linkPath) {
|
|
1806
|
+
if (!link)
|
|
1807
|
+
return null;
|
|
1808
|
+
const linkedTarget = link.target?.kind;
|
|
1809
|
+
if (linkedTarget && targetKind !== "unknown" && linkedTarget !== targetKind) {
|
|
1810
|
+
return {
|
|
1811
|
+
message: "Workspace project link was created for a different Run402 target.",
|
|
1812
|
+
details: {
|
|
1813
|
+
reason: "target_mismatch",
|
|
1814
|
+
linked_project_id: link.project_id,
|
|
1815
|
+
linked_target: linkedTarget,
|
|
1816
|
+
target: targetKind,
|
|
1817
|
+
link_path: linkPath,
|
|
1818
|
+
},
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
if (!input.name)
|
|
1822
|
+
return null;
|
|
1823
|
+
if (!link.name) {
|
|
1824
|
+
return {
|
|
1825
|
+
message: "--name was supplied, but this workspace is already linked to a project without a recorded name.",
|
|
1826
|
+
details: {
|
|
1827
|
+
reason: "name_missing",
|
|
1828
|
+
linked_project_id: link.project_id,
|
|
1829
|
+
name: input.name,
|
|
1830
|
+
link_path: linkPath,
|
|
1831
|
+
},
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
if (input.name !== link.name) {
|
|
1835
|
+
return {
|
|
1836
|
+
message: "--name conflicts with the existing workspace project link.",
|
|
1837
|
+
details: {
|
|
1838
|
+
reason: "name_mismatch",
|
|
1839
|
+
linked_project_id: link.project_id,
|
|
1840
|
+
linked_name: link.name,
|
|
1841
|
+
name: input.name,
|
|
1842
|
+
link_path: linkPath,
|
|
1843
|
+
},
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1279
1848
|
function deriveStableRootKey(input) {
|
|
1280
1849
|
if (input.type === Run402Action.ProjectsProvision && input.name) {
|
|
1281
1850
|
return `projects.provision:${input.name}`;
|
|
@@ -1312,6 +1881,29 @@ function hasContent(value) {
|
|
|
1312
1881
|
return value.length > 0;
|
|
1313
1882
|
return true;
|
|
1314
1883
|
}
|
|
1884
|
+
function stableJson(value) {
|
|
1885
|
+
if (value === undefined)
|
|
1886
|
+
return "null";
|
|
1887
|
+
if (value === null || typeof value !== "object")
|
|
1888
|
+
return JSON.stringify(value) ?? "null";
|
|
1889
|
+
if (Array.isArray(value))
|
|
1890
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
1891
|
+
const entries = Object.entries(value)
|
|
1892
|
+
.filter(([, v]) => v !== undefined)
|
|
1893
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
1894
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableJson(v)}`).join(",")}}`;
|
|
1895
|
+
}
|
|
1896
|
+
function sha256Hex(value) {
|
|
1897
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
1898
|
+
}
|
|
1899
|
+
function actionErrorDetails(err) {
|
|
1900
|
+
const maybe = err;
|
|
1901
|
+
return {
|
|
1902
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1903
|
+
...(typeof maybe.code === "string" ? { code: maybe.code } : {}),
|
|
1904
|
+
...(maybe.details && typeof maybe.details === "object" ? { details: maybe.details } : {}),
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1315
1907
|
function assertNever(value) {
|
|
1316
1908
|
throw new LocalError(`Unknown Run402 action ${value.type}`, "running Run402 action", { code: "RUN402_ACTION_UNKNOWN" });
|
|
1317
1909
|
}
|