@samebase/alchemy-convex 0.6.1 → 0.8.0
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 +72 -13
- package/lib/index.d.ts +97 -11
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +260 -62
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Deployment.ts +97 -18
- package/src/Providers.ts +2 -2
- package/src/Push.ts +58 -48
- package/src/RunRecord.ts +208 -0
- package/src/State.ts +50 -6
- package/src/index.ts +9 -0
- package/src/store/convex/state.ts +48 -2
- package/src/store/convex/version.ts +8 -4
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
5
|
import * as Config from "effect/Config";
|
|
6
6
|
import * as Context from "effect/Context";
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
@@ -20,11 +20,12 @@ import { Stack } from "alchemy/Stack";
|
|
|
20
20
|
import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
|
|
21
21
|
import * as Schedule from "effect/Schedule";
|
|
22
22
|
import { createHash } from "node:crypto";
|
|
23
|
-
import { stripVTControlCharacters } from "node:util";
|
|
23
|
+
import { promisify, stripVTControlCharacters } from "node:util";
|
|
24
24
|
import { hashDirectory } from "alchemy/Command/Memo";
|
|
25
25
|
import * as Clock from "effect/Clock";
|
|
26
26
|
import * as ChildProcess from "effect/process/ChildProcess";
|
|
27
27
|
import * as Stream from "effect/Stream";
|
|
28
|
+
import { execFile } from "node:child_process";
|
|
28
29
|
import { fileURLToPath } from "node:url";
|
|
29
30
|
import * as Scope from "effect/Scope";
|
|
30
31
|
import { STATE_STORE_VERSION, State, StateStoreError, encodeState, reviveState } from "alchemy/State";
|
|
@@ -281,13 +282,15 @@ const EnvironmentVariableList = Schema.Struct({ environmentVariables: Schema.Rec
|
|
|
281
282
|
/** No `convex` package resolves from the app directory. */
|
|
282
283
|
var ConvexCliNotFound = class extends Schema.TaggedError()("ConvexCliNotFound", { cwd: Schema.String }) {
|
|
283
284
|
get message() {
|
|
284
|
-
return `Convex.Deployment found no installed convex package from ${this.cwd}, and it does not download one. Install convex in the app (npm install convex), or set
|
|
285
|
+
return `Convex.Deployment found no installed convex package from ${this.cwd}, and it does not download one. Install convex in the app (npm install convex), or set rootDir to the directory of the app.`;
|
|
285
286
|
}
|
|
286
287
|
};
|
|
287
|
-
/**
|
|
288
|
-
var ConvexAppNotFound = class extends Schema.TaggedError()("ConvexAppNotFound", {
|
|
288
|
+
/** The app directory (`rootDir`) has no package.json. */
|
|
289
|
+
var ConvexAppNotFound = class extends Schema.TaggedError()("ConvexAppNotFound", {
|
|
290
|
+
/** The app directory, resolved against the working directory of the deploy. */
|
|
291
|
+
rootDir: Schema.String }) {
|
|
289
292
|
get message() {
|
|
290
|
-
return `Convex.Deployment found no package.json
|
|
293
|
+
return `Convex.Deployment found no package.json in ${this.rootDir}. Set rootDir to the directory of the Convex app, relative to the directory where the deploy runs, such as "apps/web".`;
|
|
291
294
|
}
|
|
292
295
|
};
|
|
293
296
|
/** The Convex CLI failed, or exited 0 without the deployment URL. The output tail never contains the key. */
|
|
@@ -371,28 +374,26 @@ const parseDeployOutput = (text) => {
|
|
|
371
374
|
/** The last non-empty lines of `text`, without escape codes and with every secret replaced. */
|
|
372
375
|
const outputTail = (text, secrets) => secrets.reduce((redacted, secret) => redacted.split(secret).join("[REDACTED]"), stripVTControlCharacters(text)).split(/\r?\n|\r/).filter((line) => line.trim() !== "").slice(-20).join("\n");
|
|
373
376
|
const valueOf$1 = (value) => Redacted.isRedacted(value) ? Redacted.value(value) : value;
|
|
374
|
-
/**
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
},
|
|
387
|
-
catch: () => new ConvexAppNotFound({ source })
|
|
388
|
-
});
|
|
389
|
-
/** `source` is not the functions directory that the Convex CLI pushes from this app. */
|
|
377
|
+
/**
|
|
378
|
+
* The app directory: `rootDir` resolved against the working directory of the
|
|
379
|
+
* deploy. It must have a package.json. There is no search above it.
|
|
380
|
+
*/
|
|
381
|
+
const appDirectoryOf = (rootDir) => {
|
|
382
|
+
const directory = resolve(rootDir);
|
|
383
|
+
return Effect.tryPromise({
|
|
384
|
+
try: () => access(join(directory, "package.json")).then(() => directory),
|
|
385
|
+
catch: () => new ConvexAppNotFound({ rootDir: directory })
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
/** `source` is not the functions directory that the Convex CLI pushes from the app directory. */
|
|
390
389
|
var SourceMismatch = class extends Schema.TaggedError()("SourceMismatch", {
|
|
390
|
+
/** The app directory, resolved. */
|
|
391
|
+
rootDir: Schema.String,
|
|
391
392
|
source: Schema.String,
|
|
392
393
|
functions: Schema.String
|
|
393
394
|
}) {
|
|
394
395
|
get message() {
|
|
395
|
-
return `Convex.Deployment source is ${this.source}, but the Convex CLI pushes ${this.functions}
|
|
396
|
+
return `Convex.Deployment source is ${this.source}, but the Convex CLI pushes ${this.functions} from the app directory ${this.rootDir} (the "functions" field of convex.json, or convex/ by default). Set rootDir to the directory of the Convex app, and source to its functions directory relative to rootDir, such as "./convex".`;
|
|
396
397
|
}
|
|
397
398
|
};
|
|
398
399
|
const ConvexJson = Schema.Struct({ functions: Schema.optionalKey(Schema.String) });
|
|
@@ -401,8 +402,9 @@ const checkFunctionsDirectory = (cwd, path) => Effect.gen(function* () {
|
|
|
401
402
|
const text = yield* Effect.promise(() => readFile(join(cwd, "convex.json"), "utf8").catch(() => void 0));
|
|
402
403
|
const configured = text === void 0 ? void 0 : Schema.decodeUnknownSync(Schema.fromJsonString(ConvexJson))(text).functions;
|
|
403
404
|
const functions = resolve(cwd, configured ?? "convex");
|
|
404
|
-
if (functions !==
|
|
405
|
-
|
|
405
|
+
if (functions !== path) return yield* new SourceMismatch({
|
|
406
|
+
rootDir: cwd,
|
|
407
|
+
source: path,
|
|
406
408
|
functions
|
|
407
409
|
});
|
|
408
410
|
});
|
|
@@ -426,18 +428,24 @@ const installedCli = (cwd) => Effect.tryPromise({
|
|
|
426
428
|
*/
|
|
427
429
|
const pushHash = (options) => Effect.gen(function* () {
|
|
428
430
|
const source = options.source === void 0 ? void 0 : sourceOf(options.source);
|
|
429
|
-
const files = source === void 0 ? void 0 : yield* appDirectoryOf(
|
|
430
|
-
cwd,
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
431
|
+
const files = source === void 0 ? void 0 : yield* appDirectoryOf(options.rootDir).pipe(Effect.flatMap((cwd) => {
|
|
432
|
+
const functions = relative(cwd, resolve(cwd, source.path)).split("\\").join("/");
|
|
433
|
+
return hashDirectory({
|
|
434
|
+
cwd,
|
|
435
|
+
memo: source.include !== void 0 ? {
|
|
436
|
+
include: [
|
|
437
|
+
`${functions}/**`,
|
|
438
|
+
"package.json",
|
|
439
|
+
"convex.json",
|
|
440
|
+
...source.include
|
|
441
|
+
],
|
|
442
|
+
lockfile: true
|
|
443
|
+
} : functions === ".." || functions.startsWith("../") ? {
|
|
444
|
+
include: ["**/*", `${functions}/**`],
|
|
445
|
+
lockfile: true
|
|
446
|
+
} : {}
|
|
447
|
+
});
|
|
448
|
+
}));
|
|
441
449
|
const env = Object.entries(options.env).map(([name, value]) => [name, valueOf$1(value)]).sort(([a], [b]) => a < b ? -1 : 1);
|
|
442
450
|
return createHash("sha256").update(JSON.stringify({
|
|
443
451
|
files,
|
|
@@ -509,13 +517,13 @@ const writeVariables = (url, key, changes) => settleVoid("update environment var
|
|
|
509
517
|
})) } }));
|
|
510
518
|
/**
|
|
511
519
|
* The app directory and the program of a push. Fails before any change when
|
|
512
|
-
* `source` is not the functions directory
|
|
513
|
-
* installed Convex CLI and no push command.
|
|
520
|
+
* `rootDir` has no package.json, when `source` is not the functions directory
|
|
521
|
+
* of the app, or when the app has no installed Convex CLI and no push command.
|
|
514
522
|
*/
|
|
515
|
-
const checkSource = (input) => Effect.gen(function* () {
|
|
523
|
+
const checkSource = (rootDir, input) => Effect.gen(function* () {
|
|
516
524
|
const source = sourceOf(input);
|
|
517
|
-
const cwd = yield* appDirectoryOf(
|
|
518
|
-
yield* checkFunctionsDirectory(cwd, source.path);
|
|
525
|
+
const cwd = yield* appDirectoryOf(rootDir);
|
|
526
|
+
yield* checkFunctionsDirectory(cwd, resolve(cwd, source.path));
|
|
519
527
|
return {
|
|
520
528
|
cwd,
|
|
521
529
|
program: source.command ?? (yield* installedCli(cwd))
|
|
@@ -529,7 +537,7 @@ const checkSource = (input) => Effect.gen(function* () {
|
|
|
529
537
|
*/
|
|
530
538
|
const push = (spawner, key, options) => Effect.gen(function* () {
|
|
531
539
|
const source = options.source === void 0 ? void 0 : sourceOf(options.source);
|
|
532
|
-
const resolved = options.source === void 0 ? void 0 : yield* checkSource(options.source);
|
|
540
|
+
const resolved = options.source === void 0 ? void 0 : yield* checkSource(options.rootDir, options.source);
|
|
533
541
|
const cwd = resolved?.cwd;
|
|
534
542
|
const program = resolved?.program;
|
|
535
543
|
const env = Object.entries(options.env).sort(([a], [b]) => a < b ? -1 : 1);
|
|
@@ -627,7 +635,43 @@ const runPush = (spawner, key, options) => Effect.gen(function* () {
|
|
|
627
635
|
});
|
|
628
636
|
//#endregion
|
|
629
637
|
//#region src/Deployment.ts
|
|
630
|
-
|
|
638
|
+
/** The resource class. Stacks call {@link Deployment}, which writes `rootDir` into the props. */
|
|
639
|
+
const DeploymentResource = Resource("Convex.Deployment", { defaultRemovalPolicy: "retain" });
|
|
640
|
+
/** `rootDir` in POSIX form, without "./" and a trailing slash, such as `apps/web` or ".". */
|
|
641
|
+
const normalizeRootDir = (rootDir) => {
|
|
642
|
+
const normalized = posix.normalize(rootDir.replaceAll("\\", "/"));
|
|
643
|
+
return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
644
|
+
};
|
|
645
|
+
/** `rootDir` is an absolute path. The declaration stops with it as a defect, before the plan. */
|
|
646
|
+
var AbsoluteRootDir = class extends Schema.TaggedError()("AbsoluteRootDir", { rootDir: Schema.String }) {
|
|
647
|
+
get message() {
|
|
648
|
+
return `Convex.Deployment rootDir ${this.rootDir} is an absolute path. State keeps rootDir for every machine that runs the stack, so set it relative to the directory where the deploy runs, such as "apps/web".`;
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
/**
|
|
652
|
+
* `Convex.Deployment("id", props)`. The props go to state with `rootDir` in
|
|
653
|
+
* POSIX form, and with "." when it is not set, so that every row names the
|
|
654
|
+
* app directory of its deployment. An absolute `rootDir` stops the
|
|
655
|
+
* declaration, so it never reaches the plan or state. The check dies,
|
|
656
|
+
* because the program of `Alchemy.Stack` can fail only with a ConfigError.
|
|
657
|
+
* `win32.isAbsolute` also answers true for a POSIX absolute path. The other
|
|
658
|
+
* fields keep it usable where Alchemy takes a resource class, such as
|
|
659
|
+
* `Provider.collection`.
|
|
660
|
+
*/
|
|
661
|
+
const Deployment = Object.assign((id, props) => {
|
|
662
|
+
const rootDir = props.rootDir ?? ".";
|
|
663
|
+
return win32.isAbsolute(rootDir) ? Effect.die(new AbsoluteRootDir({ rootDir })) : DeploymentResource(id, {
|
|
664
|
+
...props,
|
|
665
|
+
rootDir: normalizeRootDir(rootDir)
|
|
666
|
+
});
|
|
667
|
+
}, {
|
|
668
|
+
Type: DeploymentResource.Type,
|
|
669
|
+
Props: DeploymentResource.Props,
|
|
670
|
+
Self: DeploymentResource.Self,
|
|
671
|
+
Provider: DeploymentResource.Provider,
|
|
672
|
+
Aliases: DeploymentResource.Aliases,
|
|
673
|
+
ref: (id, options) => DeploymentResource.ref(id, options)
|
|
674
|
+
});
|
|
631
675
|
/** The props name another deployment than the one in state. */
|
|
632
676
|
var DeploymentIdentityChange = class extends Schema.TaggedError()("DeploymentIdentityChange", {
|
|
633
677
|
/** Name of the deployment in state, such as `happy-animal-123`. */
|
|
@@ -749,7 +793,7 @@ const ProjectSlugs = Schema.Struct({
|
|
|
749
793
|
});
|
|
750
794
|
/** True for an answer that says this credential cannot read the object. */
|
|
751
795
|
const isRefused = (error) => error.status === 400 || error.status === 401 || error.status === 403 || error.status === 404;
|
|
752
|
-
const DeploymentProvider = () => Provider.effect(
|
|
796
|
+
const DeploymentProvider = () => Provider.effect(DeploymentResource, Effect.gen(function* () {
|
|
753
797
|
const api = yield* ManagementApi;
|
|
754
798
|
const credentials = yield* Credentials;
|
|
755
799
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -842,7 +886,8 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
842
886
|
return row === void 0 ? void 0 : targetOf(row, output.type, output.previewName);
|
|
843
887
|
});
|
|
844
888
|
/** The content identity of `source` and `env` on a deployment, or undefined when there is nothing to push. */
|
|
845
|
-
const wantedHash = (news, deployment) => news.source === void 0 && Object.keys(news.env ?? {}).length === 0 ? Effect.succeed(void 0) : pushHash({
|
|
889
|
+
const wantedHash = (news, rootDir, deployment) => news.source === void 0 && Object.keys(news.env ?? {}).length === 0 ? Effect.succeed(void 0) : pushHash({
|
|
890
|
+
rootDir,
|
|
846
891
|
source: news.source,
|
|
847
892
|
deployment,
|
|
848
893
|
env: news.env ?? {}
|
|
@@ -853,12 +898,15 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
853
898
|
"type",
|
|
854
899
|
"previewName"
|
|
855
900
|
],
|
|
856
|
-
diff: Effect.fn(function* ({ news, output }) {
|
|
901
|
+
diff: Effect.fn(function* ({ news, olds, output }) {
|
|
857
902
|
if (!isResolved(news) || output === void 0) return void 0;
|
|
858
903
|
yield* checkIdentity(output, news);
|
|
904
|
+
const rootDir = news.rootDir ?? ".";
|
|
905
|
+
if (news.source !== void 0) yield* checkSource(rootDir, news.source);
|
|
906
|
+
if (olds.rootDir !== rootDir) return { action: "update" };
|
|
859
907
|
const access = yield* accessFor(addressOf(news), changesEnv(news, output));
|
|
860
908
|
if ((yield* live(access, output))?.name !== output.name) return { action: "update" };
|
|
861
|
-
return (yield* wantedHash(news, output.name)) === output.hash ? { action: "noop" } : { action: "update" };
|
|
909
|
+
return (yield* wantedHash(news, rootDir, output.name)) === output.hash ? { action: "noop" } : { action: "update" };
|
|
862
910
|
}),
|
|
863
911
|
read: Effect.fn(function* ({ olds, output }) {
|
|
864
912
|
if (output !== void 0) {
|
|
@@ -896,7 +944,8 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
896
944
|
}),
|
|
897
945
|
reconcile: Effect.fn(function* ({ fqn, news, output }) {
|
|
898
946
|
if (output !== void 0) yield* checkIdentity(output, news);
|
|
899
|
-
|
|
947
|
+
const rootDir = news.rootDir ?? ".";
|
|
948
|
+
if (news.source !== void 0) yield* checkSource(rootDir, news.source);
|
|
900
949
|
const address = addressOf(news);
|
|
901
950
|
const access = yield* accessFor(address, changesEnv(news, output));
|
|
902
951
|
const env = news.env ?? {};
|
|
@@ -904,7 +953,7 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
904
953
|
const adopt = yield* shouldAdopt(fqn);
|
|
905
954
|
const refuse = (name) => new OwnedBySomeoneElse({
|
|
906
955
|
message: `Project ${address.projectId} already has ${news.type} deployment ${name}${news.type === "prod" ? " with code" : ` named "${news.name}"`}. Re-run with --adopt to take it over${news.type === "prod" ? "" : ", or use another name"}.`,
|
|
907
|
-
resourceType:
|
|
956
|
+
resourceType: DeploymentResource.Type,
|
|
908
957
|
physicalName: name
|
|
909
958
|
});
|
|
910
959
|
const { target, created } = yield* Effect.gen(function* () {
|
|
@@ -962,7 +1011,7 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
962
1011
|
};
|
|
963
1012
|
});
|
|
964
1013
|
const previousEnvNames = output?.name === target.name ? envNamesOf(output) : [];
|
|
965
|
-
const before = yield* wantedHash(news, target.name);
|
|
1014
|
+
const before = yield* wantedHash(news, rootDir, target.name);
|
|
966
1015
|
if (before === void 0 && previousEnvNames.length === 0) return {
|
|
967
1016
|
...target,
|
|
968
1017
|
hash: void 0,
|
|
@@ -976,6 +1025,7 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
976
1025
|
const options = {
|
|
977
1026
|
deployment: target.name,
|
|
978
1027
|
url: target.url,
|
|
1028
|
+
rootDir,
|
|
979
1029
|
source: news.source,
|
|
980
1030
|
env,
|
|
981
1031
|
previousEnvNames,
|
|
@@ -993,7 +1043,7 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
993
1043
|
}))).pipe(Effect.onError(() => created && access.kind !== "deployKey" ? absentAsUndefined(retryIdempotentWrite(access.api.requestVoid("delete deployment", (client) => client.POST("/deployments/{deployment_name}/delete", { params: { path: { deployment_name: target.name } } })))).pipe(Effect.catch((error) => Effect.logWarning(`Convex.Deployment could not delete ${target.name} after the failed push (${error.message}). The next run takes it over only with --adopt.`))) : Effect.void));
|
|
994
1044
|
return {
|
|
995
1045
|
...target,
|
|
996
|
-
hash: yield* wantedHash(news, target.name),
|
|
1046
|
+
hash: yield* wantedHash(news, rootDir, target.name),
|
|
997
1047
|
envNames: Object.keys(env).sort()
|
|
998
1048
|
};
|
|
999
1049
|
}),
|
|
@@ -1327,7 +1377,126 @@ var Providers = class extends Provider.ProviderCollection()("Convex") {};
|
|
|
1327
1377
|
* default lookup (`CONVEX_ACCESS_TOKEN`, then the Convex CLI login, and
|
|
1328
1378
|
* `CONVEX_DEPLOY_KEY`).
|
|
1329
1379
|
*/
|
|
1330
|
-
const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([Project,
|
|
1380
|
+
const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([Project, DeploymentResource])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeploymentProvider())), Layer.provide(ManagementApiLive()), Layer.provide(credentials), Layer.orDie);
|
|
1381
|
+
//#endregion
|
|
1382
|
+
//#region src/RunRecord.ts
|
|
1383
|
+
/** The key of the record in the stage output document. */
|
|
1384
|
+
const RUN_RECORD_KEY = "samebase.run";
|
|
1385
|
+
/** The name of the stack file. Alchemy resolves `--config` against the working directory and does not expose it to a state layer, so the record assumes the default name. */
|
|
1386
|
+
const STACK_FILE_NAME = "alchemy.run.ts";
|
|
1387
|
+
var RunRecordError = class extends Schema.TaggedError()("RunRecordError", { reason: Schema.String }) {
|
|
1388
|
+
get message() {
|
|
1389
|
+
return `The run record of the stage output cannot be made: ${this.reason}`;
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
/** The part of the GitHub Actions event payload that the record reads. A push event has no `pull_request`. */
|
|
1393
|
+
const EventPayload = Schema.Struct({ pull_request: Schema.optionalKey(Schema.Struct({
|
|
1394
|
+
number: Schema.Number,
|
|
1395
|
+
head: Schema.Struct({ sha: Schema.String })
|
|
1396
|
+
})) });
|
|
1397
|
+
/** `owner/name` of an https, ssh, or scp-like git URL with a host. A local path is not one. */
|
|
1398
|
+
const REMOTE_URL = /^(?:[a-z+]+:\/\/[^/]+\/|[^/@:]+@[^:/]+:)([^/]+)\/([^/]+?)(?:\.git)?\/?$/;
|
|
1399
|
+
const execFileAsync = promisify(execFile);
|
|
1400
|
+
const now = Clock.currentTimeMillis.pipe(Effect.map((millis) => new Date(millis).toISOString()));
|
|
1401
|
+
const git = (cwd, args) => Effect.tryPromise({
|
|
1402
|
+
try: async () => (await execFileAsync("git", [
|
|
1403
|
+
"-C",
|
|
1404
|
+
cwd,
|
|
1405
|
+
...args
|
|
1406
|
+
], { encoding: "utf8" })).stdout.trim(),
|
|
1407
|
+
catch: (error) => new RunRecordError({ reason: `git ${args.join(" ")} failed in ${cwd} (${error instanceof Error ? error.message.split("\n")[0] : String(error)})` })
|
|
1408
|
+
});
|
|
1409
|
+
/** The stack file from the repository root: the working directory below the git top level, plus {@link STACK_FILE_NAME}. */
|
|
1410
|
+
const stackFileOf = (cwd) => Effect.gen(function* () {
|
|
1411
|
+
const top = yield* git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1412
|
+
const [realTop, realCwd] = yield* Effect.tryPromise({
|
|
1413
|
+
try: () => Promise.all([realpath(top), realpath(cwd)]),
|
|
1414
|
+
catch: () => new RunRecordError({ reason: `${cwd} is gone` })
|
|
1415
|
+
});
|
|
1416
|
+
return [...relative(realTop, realCwd).split(sep).filter((part) => part !== ""), STACK_FILE_NAME].join("/");
|
|
1417
|
+
});
|
|
1418
|
+
const variable = (env, name) => Effect.suspend(() => {
|
|
1419
|
+
const value = env[name];
|
|
1420
|
+
return value === void 0 || value === "" ? Effect.fail(new RunRecordError({ reason: `GITHUB_ACTIONS is true, and ${name} is not set` })) : Effect.succeed(value);
|
|
1421
|
+
});
|
|
1422
|
+
const fromGitHubActions = (where) => Effect.gen(function* () {
|
|
1423
|
+
const { env } = where;
|
|
1424
|
+
const repository = yield* variable(env, "GITHUB_REPOSITORY");
|
|
1425
|
+
const refName = yield* variable(env, "GITHUB_REF_NAME");
|
|
1426
|
+
const sha = yield* variable(env, "GITHUB_SHA");
|
|
1427
|
+
const serverUrl = yield* variable(env, "GITHUB_SERVER_URL");
|
|
1428
|
+
const runId = yield* variable(env, "GITHUB_RUN_ID");
|
|
1429
|
+
const runAttempt = yield* variable(env, "GITHUB_RUN_ATTEMPT");
|
|
1430
|
+
const eventPath = env["GITHUB_EVENT_PATH"];
|
|
1431
|
+
const pullRequest = (eventPath === void 0 || eventPath === "" ? void 0 : yield* Effect.tryPromise({
|
|
1432
|
+
try: () => readFile(eventPath, "utf8"),
|
|
1433
|
+
catch: () => new RunRecordError({ reason: `GITHUB_EVENT_PATH ${eventPath} cannot be read` })
|
|
1434
|
+
}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(EventPayload))), Effect.mapError((error) => error instanceof RunRecordError ? error : new RunRecordError({ reason: `GITHUB_EVENT_PATH ${eventPath} is not an event payload` }))))?.pull_request;
|
|
1435
|
+
const headCommit = pullRequest?.head.sha ?? sha;
|
|
1436
|
+
const headRef = env["GITHUB_HEAD_REF"];
|
|
1437
|
+
const mergeRef = /^refs\/pull\/(\d+)\/merge$/.exec(env["GITHUB_REF"] ?? "");
|
|
1438
|
+
return {
|
|
1439
|
+
version: 1,
|
|
1440
|
+
repository,
|
|
1441
|
+
stackFile: yield* stackFileOf(where.cwd),
|
|
1442
|
+
branch: headRef === void 0 || headRef === "" ? refName : headRef,
|
|
1443
|
+
pullRequest: pullRequest?.number ?? (mergeRef === null ? null : Number(mergeRef[1])),
|
|
1444
|
+
headCommit,
|
|
1445
|
+
runUrl: `${serverUrl}/${repository}/actions/runs/${runId}/attempts/${runAttempt}`,
|
|
1446
|
+
source: "github-actions",
|
|
1447
|
+
recordedAt: yield* now
|
|
1448
|
+
};
|
|
1449
|
+
});
|
|
1450
|
+
const fromGit = (where) => Effect.gen(function* () {
|
|
1451
|
+
const { cwd } = where;
|
|
1452
|
+
const origin = yield* git(cwd, [
|
|
1453
|
+
"remote",
|
|
1454
|
+
"get-url",
|
|
1455
|
+
"origin"
|
|
1456
|
+
]);
|
|
1457
|
+
const match = REMOTE_URL.exec(origin);
|
|
1458
|
+
if (match === null) return yield* new RunRecordError({ reason: `the origin remote of ${cwd} is not a git URL with a host and owner/name: ${origin}` });
|
|
1459
|
+
const branch = yield* git(cwd, [
|
|
1460
|
+
"rev-parse",
|
|
1461
|
+
"--abbrev-ref",
|
|
1462
|
+
"HEAD"
|
|
1463
|
+
]);
|
|
1464
|
+
if (branch === "HEAD") return yield* new RunRecordError({ reason: `the checkout at ${cwd} is on no branch (detached HEAD)` });
|
|
1465
|
+
return {
|
|
1466
|
+
version: 1,
|
|
1467
|
+
repository: `${match[1]}/${match[2]}`,
|
|
1468
|
+
stackFile: yield* stackFileOf(cwd),
|
|
1469
|
+
branch,
|
|
1470
|
+
pullRequest: null,
|
|
1471
|
+
headCommit: yield* git(cwd, ["rev-parse", "HEAD"]),
|
|
1472
|
+
runUrl: null,
|
|
1473
|
+
source: "laptop",
|
|
1474
|
+
recordedAt: yield* now
|
|
1475
|
+
};
|
|
1476
|
+
});
|
|
1477
|
+
/**
|
|
1478
|
+
* The record of this run: from the GitHub Actions variables and the event
|
|
1479
|
+
* payload when GITHUB_ACTIONS is `true`, else from git in `cwd`, which needs
|
|
1480
|
+
* a branch and an `origin` remote with `owner/name`. Fails when a value is
|
|
1481
|
+
* missing; nothing is guessed.
|
|
1482
|
+
*/
|
|
1483
|
+
const runRecord = (where) => where.env["GITHUB_ACTIONS"] === "true" ? fromGitHubActions(where) : fromGit(where);
|
|
1484
|
+
/** A plain object, which a spread keeps whole. A Date, a Duration, or a Redacted value is an object too, and a spread loses it. */
|
|
1485
|
+
const isPlainObject = (value) => {
|
|
1486
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1487
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1488
|
+
return prototype === Object.prototype || prototype === null;
|
|
1489
|
+
};
|
|
1490
|
+
/**
|
|
1491
|
+
* The output document of a stage: the stack's output plus the record under
|
|
1492
|
+
* {@link RUN_RECORD_KEY}, or undefined for an output with no place for the
|
|
1493
|
+
* key: a string, an array, null, or an object that is not plain, such as a
|
|
1494
|
+
* Date. A value that the stack itself put under the key stays.
|
|
1495
|
+
*/
|
|
1496
|
+
const withRunRecord = (output, record) => isPlainObject(output) ? "samebase.run" in output ? output : {
|
|
1497
|
+
...output,
|
|
1498
|
+
[RUN_RECORD_KEY]: record
|
|
1499
|
+
} : void 0;
|
|
1331
1500
|
//#endregion
|
|
1332
1501
|
//#region src/State.ts
|
|
1333
1502
|
/** The name of the state project in each team. */
|
|
@@ -1449,10 +1618,11 @@ const storeDirectory = () => join(dirname(fileURLToPath(import.meta.url)), "..",
|
|
|
1449
1618
|
*/
|
|
1450
1619
|
const pushStore = (options, deployment) => Effect.gen(function* () {
|
|
1451
1620
|
const cli = yield* installedCli(join(options.store, ".."));
|
|
1452
|
-
yield* Effect.logInfo(`Convex state: pushing the store version
|
|
1621
|
+
yield* Effect.logInfo(`Convex state: pushing the store version 2 to ${deployment.name}`);
|
|
1453
1622
|
yield* withPushKey(options.api, deployment.name, PUSH_KEY_ACTIONS, (key) => push(options.spawner, key, {
|
|
1454
1623
|
deployment: deployment.name,
|
|
1455
1624
|
url: deployment.deploymentUrl,
|
|
1625
|
+
rootDir: join(options.store, ".."),
|
|
1456
1626
|
source: {
|
|
1457
1627
|
path: options.store,
|
|
1458
1628
|
command: [
|
|
@@ -1512,16 +1682,31 @@ const makeConvexState = (options) => Effect.gen(function* () {
|
|
|
1512
1682
|
return next.call(path, args);
|
|
1513
1683
|
})))) };
|
|
1514
1684
|
const version = yield* storeVersion(functions);
|
|
1515
|
-
if (version === void 0 || version <
|
|
1685
|
+
if (version === void 0 || version < 2) {
|
|
1516
1686
|
if (version === void 0 && deployment.lastDeployTime) return yield* failure(`project ${STORE_PROJECT_NAME} (${project.id}) of team ${team} has code that is not the state store, and Convex.state does not push over it. Rename that project in the Convex dashboard.`);
|
|
1517
1687
|
const now = yield* storeVersion(functions);
|
|
1518
|
-
if (now === void 0 || now <
|
|
1688
|
+
if (now === void 0 || now < 2) {
|
|
1519
1689
|
yield* pushStore(options, deployment);
|
|
1520
1690
|
const pushed = yield* storeVersion(functions);
|
|
1521
|
-
if (pushed === void 0 || pushed <
|
|
1691
|
+
if (pushed === void 0 || pushed < 2) return yield* failure(`${deployment.name} answers store version ${pushed ?? "none"} after the push of version 2.`);
|
|
1522
1692
|
}
|
|
1523
|
-
} else if (version >
|
|
1693
|
+
} else if (version > 2) yield* Effect.logInfo(`Convex state: the store of team ${team} is version ${version}, newer than version 2 of this package, and it stays.`);
|
|
1524
1694
|
const remote = remoteOf(functions);
|
|
1695
|
+
/** The stack's output plus the record, when the run has one and the output has a place for it. */
|
|
1696
|
+
const documentOf = (request) => Effect.gen(function* () {
|
|
1697
|
+
const { run } = options;
|
|
1698
|
+
if (run === void 0) return request.value;
|
|
1699
|
+
const recordedAt = new Date(yield* Clock.currentTimeMillis).toISOString();
|
|
1700
|
+
const document = withRunRecord(request.value, {
|
|
1701
|
+
...run,
|
|
1702
|
+
recordedAt
|
|
1703
|
+
});
|
|
1704
|
+
if (document === void 0) {
|
|
1705
|
+
yield* Effect.logWarning(`Convex state: the output of ${request.stack}/${request.stage} is not a plain object, so it carries no run record.`);
|
|
1706
|
+
return request.value;
|
|
1707
|
+
}
|
|
1708
|
+
return document;
|
|
1709
|
+
});
|
|
1525
1710
|
return {
|
|
1526
1711
|
id: "convex",
|
|
1527
1712
|
getVersion: () => Effect.succeed(STATE_STORE_VERSION),
|
|
@@ -1534,7 +1719,15 @@ const makeConvexState = (options) => Effect.gen(function* () {
|
|
|
1534
1719
|
set: (request) => remote.set(request).pipe(Effect.as(request.value)),
|
|
1535
1720
|
delete: (request) => remote.delete(request).pipe(Effect.asVoid),
|
|
1536
1721
|
deleteStack: (request) => remote.deleteStack(request).pipe(Effect.asVoid),
|
|
1537
|
-
setOutput: (request) =>
|
|
1722
|
+
setOutput: (request) => Effect.gen(function* () {
|
|
1723
|
+
const value = yield* documentOf(request);
|
|
1724
|
+
yield* remote.setOutput({
|
|
1725
|
+
stack: request.stack,
|
|
1726
|
+
stage: request.stage,
|
|
1727
|
+
value
|
|
1728
|
+
});
|
|
1729
|
+
return value;
|
|
1730
|
+
})
|
|
1538
1731
|
};
|
|
1539
1732
|
});
|
|
1540
1733
|
/**
|
|
@@ -1551,16 +1744,21 @@ const state = (options = {}) => Layer.effect(State, Effect.gen(function* () {
|
|
|
1551
1744
|
const context = yield* Effect.context();
|
|
1552
1745
|
return yield* Effect.cached(Effect.gen(function* () {
|
|
1553
1746
|
const api = yield* ManagementApi.pipe(Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))));
|
|
1747
|
+
const run = yield* runRecord({
|
|
1748
|
+
cwd: process.cwd(),
|
|
1749
|
+
env: process.env
|
|
1750
|
+
}).pipe(Effect.catch((error) => Effect.logWarning(`Convex state: the stage outputs of this run carry no record. ${error.message}`).pipe(Effect.as(void 0))));
|
|
1554
1751
|
return yield* makeConvexState({
|
|
1555
1752
|
team: options.team,
|
|
1556
1753
|
api,
|
|
1557
1754
|
spawner,
|
|
1558
1755
|
scope,
|
|
1559
|
-
store: storeDirectory()
|
|
1756
|
+
store: storeDirectory(),
|
|
1757
|
+
run
|
|
1560
1758
|
});
|
|
1561
1759
|
}).pipe(Effect.provideContext(context), Effect.orDie));
|
|
1562
1760
|
}));
|
|
1563
1761
|
//#endregion
|
|
1564
|
-
export { ACCESS_TOKEN_ENV, AmbiguousDeployment, AmbiguousProject, CodePushFailed, ConvexApiError, ConvexAppNotFound, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DEPLOY_KEY_ENV, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, ManagementApi, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectNotFound, ProjectTeamChange, Providers, PushTargetMismatch, STORE_PROJECT_NAME, VariablesOwnedBySomeoneElse, fromConfig, fromValues, parseDeployOutput, providers, state };
|
|
1762
|
+
export { ACCESS_TOKEN_ENV, AbsoluteRootDir, AmbiguousDeployment, AmbiguousProject, CodePushFailed, ConvexApiError, ConvexAppNotFound, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DEPLOY_KEY_ENV, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, ManagementApi, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectNotFound, ProjectTeamChange, Providers, PushTargetMismatch, RUN_RECORD_KEY, RunRecordError, STORE_PROJECT_NAME, VariablesOwnedBySomeoneElse, fromConfig, fromValues, parseDeployOutput, providers, runRecord, state, withRunRecord };
|
|
1565
1763
|
|
|
1566
1764
|
//# sourceMappingURL=index.js.map
|