@wtfalch/foundry 0.73.0 → 0.74.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/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/migrations/0071_release_request_schedule.sql +12 -0
- package/dist/plan/plan-apply.js +3 -0
- package/dist/plan/release-host.d.ts +5 -1
- package/dist/plan/release-host.js +99 -50
- package/dist/release/decide.d.ts +9 -0
- package/dist/release/decide.js +1 -1
- package/dist/release/golive.d.ts +77 -0
- package/dist/release/golive.js +151 -0
- package/dist/release/run.d.ts +7 -1
- package/dist/release/run.js +205 -14
- package/dist/release/state.d.ts +82 -4
- package/dist/release/state.js +114 -2
- package/dist/release/types.d.ts +48 -2
- package/dist/store/postgres.js +97 -6
- package/dist/store/schema.d.ts +34 -0
- package/dist/store/schema.js +11 -2
- package/package.json +1 -1
package/dist/store/postgres.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, asc, desc, eq, gte, inArray, isNull, ne, sql } from 'drizzle-orm';
|
|
1
|
+
import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, lte, ne, or, sql, } from 'drizzle-orm';
|
|
2
2
|
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, normalizeHostingRepos, refuseBuiltinBlueGreen, refuseCustomers, refuseHosting, validateBlueGreen, validateBuyerIdentity, validateCalls, validateDomain, validateDomainClaim, validateMail, validateOwners, validateServices, validateSignIn, validateSite, validateVersionPin, withNewOrgAppsDefault, } from '../org.js';
|
|
3
3
|
import { hostingNames } from '../plan/locked-hostnames.js';
|
|
4
4
|
import { findNameClashes, nameClashMessage } from '../plan/org-names.js';
|
|
@@ -1066,6 +1066,46 @@ export function createPostgresAppliedVersionsStore(db) {
|
|
|
1066
1066
|
},
|
|
1067
1067
|
};
|
|
1068
1068
|
}
|
|
1069
|
+
/** A request row, and for a go-live with a time its `scheduled` line, in the caller's transaction. */
|
|
1070
|
+
async function insertScheduledRequest(tx, input) {
|
|
1071
|
+
const { orgId, entryKey, action, actor, notBefore } = input;
|
|
1072
|
+
await tx.insert(foundryReleaseRequests).values({
|
|
1073
|
+
orgId,
|
|
1074
|
+
entryKey,
|
|
1075
|
+
action,
|
|
1076
|
+
actor,
|
|
1077
|
+
...(notBefore ? { notBefore: new Date(notBefore) } : {}),
|
|
1078
|
+
});
|
|
1079
|
+
if (notBefore && action.kind === 'go_live') {
|
|
1080
|
+
await tx.insert(foundryReleaseEvents).values({
|
|
1081
|
+
orgId,
|
|
1082
|
+
entryKey,
|
|
1083
|
+
at: new Date(),
|
|
1084
|
+
kind: 'scheduled',
|
|
1085
|
+
payload: { actor, reason: `${action.tag} at ${new Date(notBefore).toISOString()}` },
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
/** Consumes a waiting request in one statement and writes `schedule_cancelled` with it. False when it was not open, was claimed, or is already due. */
|
|
1090
|
+
async function cancelScheduledRequest(tx, input) {
|
|
1091
|
+
const rows = await tx
|
|
1092
|
+
.update(foundryReleaseRequests)
|
|
1093
|
+
.set({ consumedAt: sql `now()` })
|
|
1094
|
+
.where(and(eq(foundryReleaseRequests.id, input.id), eq(foundryReleaseRequests.orgId, input.orgId), eq(foundryReleaseRequests.entryKey, input.entryKey), isNull(foundryReleaseRequests.consumedAt), isNull(foundryReleaseRequests.claimedAt), gt(foundryReleaseRequests.notBefore, input.now ?? sql `now()`)))
|
|
1095
|
+
.returning({ action: foundryReleaseRequests.action });
|
|
1096
|
+
const row = rows[0];
|
|
1097
|
+
if (!row)
|
|
1098
|
+
return false;
|
|
1099
|
+
const tag = row.action.tag;
|
|
1100
|
+
await tx.insert(foundryReleaseEvents).values({
|
|
1101
|
+
orgId: input.orgId,
|
|
1102
|
+
entryKey: input.entryKey,
|
|
1103
|
+
at: new Date(),
|
|
1104
|
+
kind: 'schedule_cancelled',
|
|
1105
|
+
payload: { actor: input.actor, ...(typeof tag === 'string' ? { reason: tag } : {}) },
|
|
1106
|
+
});
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1069
1109
|
/**
|
|
1070
1110
|
* `ReleaseStore` (`../release/state.ts`) on Postgres: the recorded `bg.*`
|
|
1071
1111
|
* keys come from an org's applied state, or the platform's for the orgId
|
|
@@ -1089,18 +1129,69 @@ export function createPostgresReleaseStore(db) {
|
|
|
1089
1129
|
.limit(1);
|
|
1090
1130
|
return row?.ids ?? {};
|
|
1091
1131
|
},
|
|
1092
|
-
async addRequest({ orgId, entryKey, action, actor }) {
|
|
1093
|
-
await db.
|
|
1132
|
+
async addRequest({ orgId, entryKey, action, actor, notBefore }) {
|
|
1133
|
+
await db.transaction(async (tx) => {
|
|
1134
|
+
await insertScheduledRequest(tx, { orgId, entryKey, action, actor, notBefore });
|
|
1135
|
+
});
|
|
1094
1136
|
},
|
|
1095
|
-
async openRequests({ orgId, entryKey }) {
|
|
1137
|
+
async openRequests({ orgId, entryKey, now }) {
|
|
1096
1138
|
const rows = await db
|
|
1097
1139
|
.select()
|
|
1098
1140
|
.from(foundryReleaseRequests)
|
|
1099
|
-
.where(and(eq(foundryReleaseRequests.orgId, orgId), eq(foundryReleaseRequests.entryKey, entryKey), isNull(foundryReleaseRequests.consumedAt)))
|
|
1141
|
+
.where(and(eq(foundryReleaseRequests.orgId, orgId), eq(foundryReleaseRequests.entryKey, entryKey), isNull(foundryReleaseRequests.consumedAt), or(isNull(foundryReleaseRequests.notBefore), lte(foundryReleaseRequests.notBefore, now ?? sql `now()`))))
|
|
1100
1142
|
.orderBy(asc(foundryReleaseRequests.id));
|
|
1101
1143
|
return rows.flatMap((row) => {
|
|
1102
1144
|
const action = releaseActionSchema.safeParse(row.action);
|
|
1103
|
-
return action.success
|
|
1145
|
+
return action.success
|
|
1146
|
+
? [
|
|
1147
|
+
{
|
|
1148
|
+
id: row.id,
|
|
1149
|
+
action: action.data,
|
|
1150
|
+
actor: row.actor,
|
|
1151
|
+
notBefore: row.notBefore ? row.notBefore.toISOString() : null,
|
|
1152
|
+
},
|
|
1153
|
+
]
|
|
1154
|
+
: [];
|
|
1155
|
+
});
|
|
1156
|
+
},
|
|
1157
|
+
async claimRequests(ids) {
|
|
1158
|
+
if (ids.length === 0)
|
|
1159
|
+
return;
|
|
1160
|
+
await db
|
|
1161
|
+
.update(foundryReleaseRequests)
|
|
1162
|
+
.set({ claimedAt: sql `now()` })
|
|
1163
|
+
.where(and(inArray(foundryReleaseRequests.id, [...ids]), isNull(foundryReleaseRequests.claimedAt)));
|
|
1164
|
+
},
|
|
1165
|
+
async cancelRequest(input) {
|
|
1166
|
+
return db.transaction((tx) => cancelScheduledRequest(tx, input));
|
|
1167
|
+
},
|
|
1168
|
+
async readScheduled({ orgId, entryKey }) {
|
|
1169
|
+
const rows = await db
|
|
1170
|
+
.select()
|
|
1171
|
+
.from(foundryReleaseRequests)
|
|
1172
|
+
.where(and(eq(foundryReleaseRequests.orgId, orgId), eq(foundryReleaseRequests.entryKey, entryKey), isNull(foundryReleaseRequests.consumedAt), isNotNull(foundryReleaseRequests.notBefore)))
|
|
1173
|
+
.orderBy(asc(foundryReleaseRequests.notBefore), asc(foundryReleaseRequests.id));
|
|
1174
|
+
return rows.flatMap((row) => {
|
|
1175
|
+
const action = releaseActionSchema.safeParse(row.action);
|
|
1176
|
+
return action.success && row.notBefore
|
|
1177
|
+
? [
|
|
1178
|
+
{
|
|
1179
|
+
id: row.id,
|
|
1180
|
+
action: action.data,
|
|
1181
|
+
notBefore: row.notBefore.toISOString(),
|
|
1182
|
+
actor: row.actor,
|
|
1183
|
+
claimed: row.claimedAt !== null,
|
|
1184
|
+
},
|
|
1185
|
+
]
|
|
1186
|
+
: [];
|
|
1187
|
+
});
|
|
1188
|
+
},
|
|
1189
|
+
async rescheduleRequest({ id, orgId, entryKey, action, notBefore, actor, now }) {
|
|
1190
|
+
return db.transaction(async (tx) => {
|
|
1191
|
+
if (!(await cancelScheduledRequest(tx, { id, orgId, entryKey, actor, now })))
|
|
1192
|
+
return false;
|
|
1193
|
+
await insertScheduledRequest(tx, { orgId, entryKey, action, actor, notBefore });
|
|
1194
|
+
return true;
|
|
1104
1195
|
});
|
|
1105
1196
|
},
|
|
1106
1197
|
async consumeRequests(ids) {
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -2906,6 +2906,40 @@ export declare const foundryReleaseRequests: import("drizzle-orm/pg-core").PgTab
|
|
|
2906
2906
|
identity: undefined;
|
|
2907
2907
|
generated: undefined;
|
|
2908
2908
|
}, {}, {}>;
|
|
2909
|
+
notBefore: import("drizzle-orm/pg-core").PgColumn<{
|
|
2910
|
+
name: "not_before";
|
|
2911
|
+
tableName: "foundry_release_requests";
|
|
2912
|
+
dataType: "date";
|
|
2913
|
+
columnType: "PgTimestamp";
|
|
2914
|
+
data: Date;
|
|
2915
|
+
driverParam: string;
|
|
2916
|
+
notNull: false;
|
|
2917
|
+
hasDefault: false;
|
|
2918
|
+
isPrimaryKey: false;
|
|
2919
|
+
isAutoincrement: false;
|
|
2920
|
+
hasRuntimeDefault: false;
|
|
2921
|
+
enumValues: undefined;
|
|
2922
|
+
baseColumn: never;
|
|
2923
|
+
identity: undefined;
|
|
2924
|
+
generated: undefined;
|
|
2925
|
+
}, {}, {}>;
|
|
2926
|
+
claimedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
2927
|
+
name: "claimed_at";
|
|
2928
|
+
tableName: "foundry_release_requests";
|
|
2929
|
+
dataType: "date";
|
|
2930
|
+
columnType: "PgTimestamp";
|
|
2931
|
+
data: Date;
|
|
2932
|
+
driverParam: string;
|
|
2933
|
+
notNull: false;
|
|
2934
|
+
hasDefault: false;
|
|
2935
|
+
isPrimaryKey: false;
|
|
2936
|
+
isAutoincrement: false;
|
|
2937
|
+
hasRuntimeDefault: false;
|
|
2938
|
+
enumValues: undefined;
|
|
2939
|
+
baseColumn: never;
|
|
2940
|
+
identity: undefined;
|
|
2941
|
+
generated: undefined;
|
|
2942
|
+
}, {}, {}>;
|
|
2909
2943
|
};
|
|
2910
2944
|
dialect: "pg";
|
|
2911
2945
|
}>;
|
package/dist/store/schema.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { bigint, bigserial, boolean, index, integer, jsonb, pgTable, text, timestamp, } from 'drizzle-orm/pg-core';
|
|
2
3
|
/**
|
|
3
4
|
* Mirrors `migrations/0001_declared_org.sql`, which is the source of truth:
|
|
4
5
|
* the SQL carries the CHECKs drizzle-kit does not generate here (this
|
|
@@ -271,7 +272,15 @@ export const foundryReleaseRequests = pgTable('foundry_release_requests', {
|
|
|
271
272
|
actor: text('actor').notNull(),
|
|
272
273
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
273
274
|
consumedAt: timestamp('consumed_at', { withTimezone: true }),
|
|
274
|
-
|
|
275
|
+
/** `migrations/0071_release_request_schedule.sql`: null is due at once. */
|
|
276
|
+
notBefore: timestamp('not_before', { withTimezone: true }),
|
|
277
|
+
/** Set when the runner starts a due go-live; a claimed request can no longer be cancelled or moved. */
|
|
278
|
+
claimedAt: timestamp('claimed_at', { withTimezone: true }),
|
|
279
|
+
}, (t) => [
|
|
280
|
+
index('foundry_release_requests_scheduled_idx')
|
|
281
|
+
.on(t.orgId, t.entryKey, t.notBefore)
|
|
282
|
+
.where(sql `consumed_at is null and not_before is not null`),
|
|
283
|
+
]);
|
|
275
284
|
/** Mirrors `migrations/0069_release_events.sql`: a release's history, appended by the release run and never changed. */
|
|
276
285
|
export const foundryReleaseEvents = pgTable('foundry_release_events', {
|
|
277
286
|
id: bigserial('id', { mode: 'number' }).primaryKey(),
|