@percepta/create 3.1.3 → 3.1.5
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 +8 -8
- package/dist/git-ops-C2CIjuce.js +51 -0
- package/dist/git-ops-C2CIjuce.js.map +1 -0
- package/dist/index.js +1073 -1067
- package/dist/index.js.map +1 -0
- package/dist/init-OeK4Yk6_.js +52 -0
- package/dist/init-OeK4Yk6_.js.map +1 -0
- package/dist/status-DC8mvHZj.js +48 -0
- package/dist/status-DC8mvHZj.js.map +1 -0
- package/dist/sync-C5Pd32VM.js +101 -0
- package/dist/sync-C5Pd32VM.js.map +1 -0
- package/dist/upstream-F6m8zRBQ.js +85 -0
- package/dist/upstream-F6m8zRBQ.js.map +1 -0
- package/package.json +23 -24
- package/templates/webapp/AGENTS.md +1 -1
- package/templates/webapp/README.md +1 -1
- package/templates/webapp/agent-skills/database.md +5 -1
- package/templates/webapp/agent-skills/deploy.md +5 -3
- package/templates/webapp/agent-skills/inngest.md +13 -8
- package/templates/webapp/agent-skills/oneshot.md +1 -1
- package/templates/webapp/deploy/README.md +2 -2
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +3 -3
- package/templates/webapp/package.json.template +2 -2
- package/templates/webapp/scripts/deploy-percepta-test.ts +311 -36
- package/templates/webapp/scripts/generate-migrations.ts +28 -0
- package/templates/webapp/src/drizzle/__tests__/migrationSql.test.ts +24 -0
- package/templates/webapp/src/drizzle/migrationSql.ts +8 -0
- package/templates/webapp/src/services/inngest/AppWorkflowService.ts +19 -0
- package/templates/webapp/src/services/inngest/__tests__/AppWorkflowService.test.ts +19 -0
- package/templates/webapp/src/services/inngest/events/AppEvents.ts +7 -13
- package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +1 -3
- package/dist/chunk-CO3YWUD6.js +0 -139
- package/dist/chunk-DCM7JOSC.js +0 -49
- package/dist/chunk-V5EJIUBJ.js +0 -60
- package/dist/index.d.ts +0 -1
- package/dist/init-EQZ2TCSJ.js +0 -96
- package/dist/status-QW5TQDYY.js +0 -76
- package/dist/sync-RLBZDOFB.js +0 -136
- package/dist/upstream-TQFVPMEG.js +0 -144
|
@@ -81,6 +81,11 @@ interface Installation {
|
|
|
81
81
|
name?: string;
|
|
82
82
|
status?: string;
|
|
83
83
|
health?: string;
|
|
84
|
+
release?: {
|
|
85
|
+
commitSha?: string;
|
|
86
|
+
currentVersion?: string;
|
|
87
|
+
targetVersion?: string;
|
|
88
|
+
};
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
interface VariableGroup {
|
|
@@ -91,6 +96,17 @@ interface VariableGroup {
|
|
|
91
96
|
}>;
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
interface Release {
|
|
100
|
+
version?: string;
|
|
101
|
+
commit?: {
|
|
102
|
+
sha?: string;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface ReleaseList {
|
|
107
|
+
releases?: Release[];
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
function parseArgs(argv: string[]): Options {
|
|
95
111
|
const options: Options = {
|
|
96
112
|
yes: false,
|
|
@@ -151,6 +167,22 @@ function run(
|
|
|
151
167
|
return typeof result === "string" ? result.trim() : "";
|
|
152
168
|
}
|
|
153
169
|
|
|
170
|
+
function runWithInput(
|
|
171
|
+
command: string,
|
|
172
|
+
args: string[],
|
|
173
|
+
cwd: string,
|
|
174
|
+
inputText: string,
|
|
175
|
+
): string {
|
|
176
|
+
const result = execFileSync(command, args, {
|
|
177
|
+
cwd,
|
|
178
|
+
encoding: "utf8",
|
|
179
|
+
input: inputText,
|
|
180
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return typeof result === "string" ? result.trim() : "";
|
|
184
|
+
}
|
|
185
|
+
|
|
154
186
|
function runJson<T>(command: string, args: string[], cwd: string): T {
|
|
155
187
|
const outputText = run(command, args, cwd, "pipe");
|
|
156
188
|
return JSON.parse(outputText) as T;
|
|
@@ -248,7 +280,7 @@ async function ensureGitHubRepo(
|
|
|
248
280
|
"repo",
|
|
249
281
|
"create",
|
|
250
282
|
repoSlug,
|
|
251
|
-
"--
|
|
283
|
+
"--internal",
|
|
252
284
|
"--source",
|
|
253
285
|
monorepoRoot,
|
|
254
286
|
"--remote",
|
|
@@ -301,6 +333,10 @@ function upsertService(manifestPath: string, monorepoRoot: string): void {
|
|
|
301
333
|
run("ryvn", ["replace", "-f", manifestPath], monorepoRoot);
|
|
302
334
|
}
|
|
303
335
|
|
|
336
|
+
function getCurrentCommitSha(monorepoRoot: string): string {
|
|
337
|
+
return run("git", ["rev-parse", "HEAD"], monorepoRoot, "pipe");
|
|
338
|
+
}
|
|
339
|
+
|
|
304
340
|
function getInstallation(
|
|
305
341
|
name: string,
|
|
306
342
|
monorepoRoot: string,
|
|
@@ -335,6 +371,39 @@ function installationExists(name: string, monorepoRoot: string): boolean {
|
|
|
335
371
|
return getInstallation(name, monorepoRoot) !== null;
|
|
336
372
|
}
|
|
337
373
|
|
|
374
|
+
function isHealthyAtCommit(
|
|
375
|
+
installation: Installation | null,
|
|
376
|
+
commitSha: string,
|
|
377
|
+
): boolean {
|
|
378
|
+
return (
|
|
379
|
+
installation?.status === "UP_TO_DATE" &&
|
|
380
|
+
installation.health === "HEALTHY" &&
|
|
381
|
+
installation.release?.commitSha === commitSha
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getReleaseForCommit(
|
|
386
|
+
serviceName: string,
|
|
387
|
+
commitSha: string,
|
|
388
|
+
monorepoRoot: string,
|
|
389
|
+
): Release | null {
|
|
390
|
+
try {
|
|
391
|
+
const releaseList = runJson<ReleaseList>(
|
|
392
|
+
"ryvn",
|
|
393
|
+
["get", "release", "--service", serviceName, "-o", "json"],
|
|
394
|
+
monorepoRoot,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
return (
|
|
398
|
+
releaseList.releases?.find(
|
|
399
|
+
(release) => release.commit?.sha === commitSha,
|
|
400
|
+
) ?? null
|
|
401
|
+
);
|
|
402
|
+
} catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
338
407
|
function assertHealthyPlatformInstallation(
|
|
339
408
|
installation: Installation | null,
|
|
340
409
|
name: string,
|
|
@@ -429,19 +498,170 @@ function upsertInstallation(
|
|
|
429
498
|
name: string,
|
|
430
499
|
manifestPath: string,
|
|
431
500
|
monorepoRoot: string,
|
|
501
|
+
expectedCommitSha: string,
|
|
502
|
+
manifestContents?: string,
|
|
503
|
+
): void {
|
|
504
|
+
const existingInstallation = getInstallation(name, monorepoRoot);
|
|
505
|
+
if (isHealthyAtCommit(existingInstallation, expectedCommitSha)) {
|
|
506
|
+
console.log(
|
|
507
|
+
`${name} is already healthy at ${expectedCommitSha.slice(0, 7)}; skipping installation replace.`,
|
|
508
|
+
);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const action = existingInstallation == null ? "create" : "replace";
|
|
513
|
+
try {
|
|
514
|
+
if (existingInstallation == null) {
|
|
515
|
+
if (manifestContents == null) {
|
|
516
|
+
run("ryvn", ["create", "-f", manifestPath], monorepoRoot);
|
|
517
|
+
} else {
|
|
518
|
+
runWithInput(
|
|
519
|
+
"ryvn",
|
|
520
|
+
["create", "-f", "-"],
|
|
521
|
+
monorepoRoot,
|
|
522
|
+
manifestContents,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
} else if (manifestContents == null) {
|
|
526
|
+
run("ryvn", ["replace", "-f", manifestPath], monorepoRoot);
|
|
527
|
+
} else {
|
|
528
|
+
runWithInput(
|
|
529
|
+
"ryvn",
|
|
530
|
+
["replace", "-f", "-"],
|
|
531
|
+
monorepoRoot,
|
|
532
|
+
manifestContents,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
} catch (error) {
|
|
536
|
+
const currentInstallation = getInstallation(name, monorepoRoot);
|
|
537
|
+
if (isHealthyAtCommit(currentInstallation, expectedCommitSha)) {
|
|
538
|
+
console.log(
|
|
539
|
+
`${name} ${action} returned an error, but Ryvn now reports it healthy at ${expectedCommitSha.slice(0, 7)}; continuing.`,
|
|
540
|
+
);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function printFailedWorkflowLog(
|
|
549
|
+
repoSlug: string,
|
|
550
|
+
runId: number,
|
|
551
|
+
monorepoRoot: string,
|
|
432
552
|
): void {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
run(
|
|
553
|
+
try {
|
|
554
|
+
console.log("");
|
|
555
|
+
console.log(`Failed workflow log for ${runId}:`);
|
|
556
|
+
run(
|
|
557
|
+
"gh",
|
|
558
|
+
["run", "view", String(runId), "--repo", repoSlug, "--log-failed"],
|
|
559
|
+
monorepoRoot,
|
|
560
|
+
);
|
|
561
|
+
} catch {
|
|
562
|
+
console.log(`Could not fetch failed workflow logs for ${runId}.`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function ensureReleaseForCurrentCommit(
|
|
567
|
+
serviceName: string,
|
|
568
|
+
repoSlug: string,
|
|
569
|
+
workflowFile: string,
|
|
570
|
+
monorepoRoot: string,
|
|
571
|
+
options: Options,
|
|
572
|
+
commitSha: string,
|
|
573
|
+
): Promise<Release> {
|
|
574
|
+
const existingRelease = getReleaseForCommit(
|
|
575
|
+
serviceName,
|
|
576
|
+
commitSha,
|
|
577
|
+
monorepoRoot,
|
|
578
|
+
);
|
|
579
|
+
if (existingRelease != null) {
|
|
580
|
+
console.log(
|
|
581
|
+
`${serviceName} release ${existingRelease.version ?? "unknown"} already exists for ${commitSha.slice(0, 7)}.`,
|
|
582
|
+
);
|
|
583
|
+
return existingRelease;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
await triggerWorkflow(
|
|
587
|
+
serviceName,
|
|
588
|
+
repoSlug,
|
|
589
|
+
workflowFile,
|
|
590
|
+
monorepoRoot,
|
|
591
|
+
options,
|
|
592
|
+
commitSha,
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const release = getReleaseForCommit(serviceName, commitSha, monorepoRoot);
|
|
596
|
+
if (release == null) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`No Ryvn release for ${serviceName} at ${commitSha} after ${workflowFile}.`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return release;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function assertReleaseExistsForSkippedWorkflow(
|
|
606
|
+
serviceName: string,
|
|
607
|
+
commitSha: string,
|
|
608
|
+
monorepoRoot: string,
|
|
609
|
+
): Release {
|
|
610
|
+
const release = getReleaseForCommit(serviceName, commitSha, monorepoRoot);
|
|
611
|
+
if (release == null) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`--skip-workflows was set, but no ${serviceName} Ryvn release exists for commit ${commitSha}. Rerun without --skip-workflows or confirm the release was created.`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return release;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function logReleaseSummary(serviceName: string, release: Release): void {
|
|
621
|
+
console.log(
|
|
622
|
+
`${serviceName} release ready: ${release.version ?? "unknown version"}.`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function requireReleaseForCurrentCommit(
|
|
627
|
+
serviceName: string,
|
|
628
|
+
repoSlug: string,
|
|
629
|
+
workflowFile: string,
|
|
630
|
+
monorepoRoot: string,
|
|
631
|
+
options: Options,
|
|
632
|
+
commitSha: string,
|
|
633
|
+
): Promise<Release> {
|
|
634
|
+
if (options.skipWorkflows) {
|
|
635
|
+
return Promise.resolve(
|
|
636
|
+
assertReleaseExistsForSkippedWorkflow(
|
|
637
|
+
serviceName,
|
|
638
|
+
commitSha,
|
|
639
|
+
monorepoRoot,
|
|
640
|
+
),
|
|
641
|
+
);
|
|
437
642
|
}
|
|
643
|
+
|
|
644
|
+
return ensureReleaseForCurrentCommit(
|
|
645
|
+
serviceName,
|
|
646
|
+
repoSlug,
|
|
647
|
+
workflowFile,
|
|
648
|
+
monorepoRoot,
|
|
649
|
+
options,
|
|
650
|
+
commitSha,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function isRetryableReleaseFailure(error: unknown): boolean {
|
|
655
|
+
return error instanceof Error;
|
|
438
656
|
}
|
|
439
657
|
|
|
440
658
|
async function triggerWorkflow(
|
|
659
|
+
serviceName: string,
|
|
441
660
|
repoSlug: string,
|
|
442
661
|
workflowFile: string,
|
|
443
662
|
monorepoRoot: string,
|
|
444
663
|
options: Options,
|
|
664
|
+
commitSha: string,
|
|
445
665
|
): Promise<void> {
|
|
446
666
|
if (options.skipWorkflows) return;
|
|
447
667
|
|
|
@@ -501,18 +721,32 @@ async function triggerWorkflow(
|
|
|
501
721
|
}
|
|
502
722
|
|
|
503
723
|
console.log(`Watching ${runInfo.url}`);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
724
|
+
try {
|
|
725
|
+
run(
|
|
726
|
+
"gh",
|
|
727
|
+
[
|
|
728
|
+
"run",
|
|
729
|
+
"watch",
|
|
730
|
+
String(runInfo.databaseId),
|
|
731
|
+
"--repo",
|
|
732
|
+
repoSlug,
|
|
733
|
+
"--exit-status",
|
|
734
|
+
],
|
|
735
|
+
monorepoRoot,
|
|
736
|
+
);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
printFailedWorkflowLog(repoSlug, runInfo.databaseId, monorepoRoot);
|
|
739
|
+
|
|
740
|
+
const release = getReleaseForCommit(serviceName, commitSha, monorepoRoot);
|
|
741
|
+
if (release != null && isRetryableReleaseFailure(error)) {
|
|
742
|
+
console.log(
|
|
743
|
+
`${workflowFile} reported a failure, but ${serviceName} release ${release.version ?? "unknown"} exists for ${commitSha.slice(0, 7)}; continuing.`,
|
|
744
|
+
);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
516
750
|
}
|
|
517
751
|
|
|
518
752
|
function latestTask(tasks: InstallationTask[]): InstallationTask | null {
|
|
@@ -619,12 +853,11 @@ function parseEnvFile(
|
|
|
619
853
|
);
|
|
620
854
|
}
|
|
621
855
|
|
|
622
|
-
async function
|
|
856
|
+
async function readInstallationSecrets(
|
|
623
857
|
packageDir: string,
|
|
624
|
-
monorepoRoot: string,
|
|
625
858
|
options: Options,
|
|
626
|
-
): Promise<
|
|
627
|
-
if (options.skipSecrets) return;
|
|
859
|
+
): Promise<Array<{ name: string; value: string }>> {
|
|
860
|
+
if (options.skipSecrets) return [];
|
|
628
861
|
|
|
629
862
|
const secretsPath = path.join(
|
|
630
863
|
packageDir,
|
|
@@ -636,20 +869,42 @@ async function patchInstallationSecrets(
|
|
|
636
869
|
console.log(
|
|
637
870
|
`No secrets file found at ${secretsPath}; skipping secret patch.`,
|
|
638
871
|
);
|
|
639
|
-
return;
|
|
872
|
+
return [];
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return parseEnvFile(await readFile(secretsPath, "utf8"));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function appendInstallationSecrets(
|
|
879
|
+
manifestContents: string,
|
|
880
|
+
secrets: Array<{ name: string; value: string }>,
|
|
881
|
+
): string {
|
|
882
|
+
if (secrets.length === 0) return manifestContents;
|
|
883
|
+
|
|
884
|
+
const marker = " variableGroups:\n";
|
|
885
|
+
if (!manifestContents.includes(marker)) {
|
|
886
|
+
throw new Error("Installation manifest is missing variableGroups marker.");
|
|
640
887
|
}
|
|
641
888
|
|
|
642
|
-
const
|
|
889
|
+
const secretYaml = [
|
|
890
|
+
" secrets:",
|
|
891
|
+
...secrets.flatMap(({ name, value }) => [
|
|
892
|
+
` - name: ${name}`,
|
|
893
|
+
` value: ${JSON.stringify(value)}`,
|
|
894
|
+
]),
|
|
895
|
+
"",
|
|
896
|
+
].join("\n");
|
|
897
|
+
|
|
898
|
+
return manifestContents.replace(marker, `${secretYaml}${marker}`);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function patchExistingInstallationSecrets(
|
|
902
|
+
secrets: Array<{ name: string; value: string }>,
|
|
903
|
+
monorepoRoot: string,
|
|
904
|
+
): void {
|
|
643
905
|
if (secrets.length === 0) return;
|
|
644
906
|
|
|
645
|
-
const
|
|
646
|
-
key: name,
|
|
647
|
-
value,
|
|
648
|
-
isSecret: true,
|
|
649
|
-
}));
|
|
650
|
-
// Strategic merge preserves existing non-secret env entries while upserting
|
|
651
|
-
// these generated secrets by key.
|
|
652
|
-
const patch = JSON.stringify({ spec: { env: secretEnv, secrets } });
|
|
907
|
+
const patch = JSON.stringify({ spec: { secrets } });
|
|
653
908
|
const result = spawnSync(
|
|
654
909
|
"ryvn",
|
|
655
910
|
[
|
|
@@ -672,7 +927,7 @@ async function patchInstallationSecrets(
|
|
|
672
927
|
);
|
|
673
928
|
|
|
674
929
|
if (result.status !== 0) {
|
|
675
|
-
throw new Error(`Failed to patch secrets for ${APP_NAME}.`);
|
|
930
|
+
throw new Error(`Failed to patch generated app secrets for ${APP_NAME}.`);
|
|
676
931
|
}
|
|
677
932
|
}
|
|
678
933
|
|
|
@@ -776,6 +1031,7 @@ async function main(): Promise<void> {
|
|
|
776
1031
|
|
|
777
1032
|
const repoSlug = await ensureGitHubRepo(monorepoRoot, options);
|
|
778
1033
|
ensureCleanAndPushed(monorepoRoot, options);
|
|
1034
|
+
const commitSha = getCurrentCommitSha(monorepoRoot);
|
|
779
1035
|
|
|
780
1036
|
const ryvnDir = path.join(packageDir, "deploy", "ryvn");
|
|
781
1037
|
const serviceManifest = path.join(ryvnDir, `${APP_NAME}.service.yaml`);
|
|
@@ -802,27 +1058,46 @@ async function main(): Promise<void> {
|
|
|
802
1058
|
upsertService(terraformServiceManifest, monorepoRoot);
|
|
803
1059
|
upsertService(serviceManifest, monorepoRoot);
|
|
804
1060
|
|
|
805
|
-
await
|
|
1061
|
+
const terraformRelease = await requireReleaseForCurrentCommit(
|
|
1062
|
+
TERRAFORM_SERVICE_NAME,
|
|
806
1063
|
repoSlug,
|
|
807
1064
|
`${TERRAFORM_SERVICE_NAME}-ryvn-release.yaml`,
|
|
808
1065
|
monorepoRoot,
|
|
809
1066
|
options,
|
|
1067
|
+
commitSha,
|
|
810
1068
|
);
|
|
1069
|
+
logReleaseSummary(TERRAFORM_SERVICE_NAME, terraformRelease);
|
|
811
1070
|
upsertInstallation(
|
|
812
1071
|
TERRAFORM_SERVICE_NAME,
|
|
813
1072
|
terraformInstallationManifest,
|
|
814
1073
|
monorepoRoot,
|
|
1074
|
+
commitSha,
|
|
815
1075
|
);
|
|
816
1076
|
await waitForTerraformApply(monorepoRoot, options);
|
|
817
1077
|
|
|
818
|
-
await
|
|
1078
|
+
const appRelease = await requireReleaseForCurrentCommit(
|
|
1079
|
+
APP_NAME,
|
|
819
1080
|
repoSlug,
|
|
820
1081
|
`${APP_NAME}-ryvn-release.yaml`,
|
|
821
1082
|
monorepoRoot,
|
|
822
1083
|
options,
|
|
1084
|
+
commitSha,
|
|
1085
|
+
);
|
|
1086
|
+
logReleaseSummary(APP_NAME, appRelease);
|
|
1087
|
+
const secrets = await readInstallationSecrets(packageDir, options);
|
|
1088
|
+
if (installationExists(APP_NAME, monorepoRoot)) {
|
|
1089
|
+
patchExistingInstallationSecrets(secrets, monorepoRoot);
|
|
1090
|
+
}
|
|
1091
|
+
upsertInstallation(
|
|
1092
|
+
APP_NAME,
|
|
1093
|
+
installationManifest,
|
|
1094
|
+
monorepoRoot,
|
|
1095
|
+
commitSha,
|
|
1096
|
+
appendInstallationSecrets(
|
|
1097
|
+
await readFile(installationManifest, "utf8"),
|
|
1098
|
+
secrets,
|
|
1099
|
+
),
|
|
823
1100
|
);
|
|
824
|
-
upsertInstallation(APP_NAME, installationManifest, monorepoRoot);
|
|
825
|
-
await patchInstallationSecrets(packageDir, monorepoRoot, options);
|
|
826
1101
|
await waitForHealthy(monorepoRoot, options);
|
|
827
1102
|
|
|
828
1103
|
await verifyDeployment();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { normalizeSchemaRelativeReferences } from "../src/drizzle/migrationSql";
|
|
7
|
+
|
|
8
|
+
const migrationsDir = path.resolve("src", "drizzle", "migrations");
|
|
9
|
+
|
|
10
|
+
function main(): void {
|
|
11
|
+
execFileSync("drizzle-kit", ["generate"], {
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
stdio: "inherit",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
for (const fileName of readdirSync(migrationsDir)) {
|
|
17
|
+
if (!fileName.endsWith(".sql")) continue;
|
|
18
|
+
|
|
19
|
+
const filePath = path.join(migrationsDir, fileName);
|
|
20
|
+
const original = readFileSync(filePath, "utf8");
|
|
21
|
+
const normalized = normalizeSchemaRelativeReferences(original);
|
|
22
|
+
if (normalized !== original) {
|
|
23
|
+
writeFileSync(filePath, normalized);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
main();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { normalizeSchemaRelativeReferences } from "../migrationSql";
|
|
3
|
+
|
|
4
|
+
describe("normalizeSchemaRelativeReferences", () => {
|
|
5
|
+
it("keeps generated foreign keys schema-relative for DATABASE_SCHEMA deploys", () => {
|
|
6
|
+
expect(
|
|
7
|
+
normalizeSchemaRelativeReferences(
|
|
8
|
+
'ALTER TABLE "child" ADD CONSTRAINT "child_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."parent"("id") ON DELETE cascade;',
|
|
9
|
+
),
|
|
10
|
+
).toBe(
|
|
11
|
+
'ALTER TABLE "child" ADD CONSTRAINT "child_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "parent"("id") ON DELETE cascade;',
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("leaves already schema-relative references unchanged", () => {
|
|
16
|
+
expect(
|
|
17
|
+
normalizeSchemaRelativeReferences(
|
|
18
|
+
'ALTER TABLE "child" ADD CONSTRAINT "child_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "parent"("id");',
|
|
19
|
+
),
|
|
20
|
+
).toBe(
|
|
21
|
+
'ALTER TABLE "child" ADD CONSTRAINT "child_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "parent"("id");',
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const PUBLIC_SCHEMA_REFERENCE_PATTERN = /REFERENCES\s+"public"\."([^"]+)"/g;
|
|
2
|
+
|
|
3
|
+
export function normalizeSchemaRelativeReferences(sql: string): string {
|
|
4
|
+
return sql.replace(
|
|
5
|
+
PUBLIC_SCHEMA_REFERENCE_PATTERN,
|
|
6
|
+
(_match, tableName: string) => `REFERENCES "${tableName}"`,
|
|
7
|
+
);
|
|
8
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AppInngest, InngestService } from "./InngestService";
|
|
2
|
+
import { type ExampleEventPayload } from "./events/payloads/ExampleEventPayload";
|
|
3
|
+
|
|
4
|
+
type AppWorkflowClient = Pick<AppInngest, "send">;
|
|
5
|
+
|
|
6
|
+
export class AppWorkflowService {
|
|
7
|
+
public static create(): AppWorkflowService {
|
|
8
|
+
return new AppWorkflowService(InngestService.create().client);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public constructor(private inngestClient: AppWorkflowClient) {}
|
|
12
|
+
|
|
13
|
+
public async sendExampleEvent(payload: ExampleEventPayload): Promise<void> {
|
|
14
|
+
await this.inngestClient.send({
|
|
15
|
+
name: "app/example.event",
|
|
16
|
+
data: payload,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { AppWorkflowService } from "../AppWorkflowService";
|
|
3
|
+
import { AppEvents } from "../events/AppEvents";
|
|
4
|
+
|
|
5
|
+
describe("AppWorkflowService", () => {
|
|
6
|
+
it("sends event data in the shape validated by AppEvents", async () => {
|
|
7
|
+
const payload = { exampleId: "example-1" };
|
|
8
|
+
const send = vi.fn().mockResolvedValue(undefined);
|
|
9
|
+
const service = new AppWorkflowService({ send } as never);
|
|
10
|
+
|
|
11
|
+
await service.sendExampleEvent(payload);
|
|
12
|
+
|
|
13
|
+
expect(AppEvents["app/example.event"].parse(payload)).toEqual(payload);
|
|
14
|
+
expect(send).toHaveBeenCalledWith({
|
|
15
|
+
name: "app/example.event",
|
|
16
|
+
data: payload,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import z from "zod";
|
|
2
1
|
import { ExampleEventPayload } from "./payloads/ExampleEventPayload";
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -6,29 +5,24 @@ import { ExampleEventPayload } from "./payloads/ExampleEventPayload";
|
|
|
6
5
|
*
|
|
7
6
|
* Each event should have:
|
|
8
7
|
* - A unique name (conventionally "app/event.name")
|
|
9
|
-
* - A Zod schema for the
|
|
8
|
+
* - A Zod schema for event.data. Do not wrap the payload in another
|
|
9
|
+
* `{ data: ... }` object here.
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```ts
|
|
13
13
|
* export const AppEvents = {
|
|
14
14
|
* "app/user.created": z.object({
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* email: z.string(),
|
|
18
|
-
* }),
|
|
15
|
+
* userId: z.string(),
|
|
16
|
+
* email: z.string(),
|
|
19
17
|
* }),
|
|
20
18
|
* "app/order.completed": z.object({
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* total: z.number(),
|
|
24
|
-
* }),
|
|
19
|
+
* orderId: z.string(),
|
|
20
|
+
* total: z.number(),
|
|
25
21
|
* }),
|
|
26
22
|
* };
|
|
27
23
|
* ```
|
|
28
24
|
*/
|
|
29
25
|
export const AppEvents = {
|
|
30
26
|
// Example event - replace with your actual events
|
|
31
|
-
"app/example.event":
|
|
32
|
-
data: ExampleEventPayload.SCHEMA,
|
|
33
|
-
}),
|
|
27
|
+
"app/example.event": ExampleEventPayload.SCHEMA,
|
|
34
28
|
};
|
|
@@ -7,8 +7,6 @@ import z from "zod";
|
|
|
7
7
|
export type ExampleEventPayload = z.infer<typeof ExampleEventPayload.SCHEMA>;
|
|
8
8
|
export namespace ExampleEventPayload {
|
|
9
9
|
export const SCHEMA = z.object({
|
|
10
|
-
|
|
11
|
-
// exampleId: z.string(),
|
|
12
|
-
// data: z.record(z.unknown()),
|
|
10
|
+
exampleId: z.string(),
|
|
13
11
|
});
|
|
14
12
|
}
|