@geonosis/release 1.1.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.
@@ -0,0 +1,252 @@
1
+ /** The dialects the gate can route to. A dialect is never sniffed: it is what the repo declared. */
2
+ type Dialect = 'mikro-orm-ts' | 'postgres' | 'sqlite';
3
+ type Phase = 'down' | 'up';
4
+ type MigrationDir = {
5
+ dialect: Dialect;
6
+ dir: string;
7
+ phases?: Phase[];
8
+ squawk?: {
9
+ exclude?: string[];
10
+ };
11
+ };
12
+ type ProofConfig = {
13
+ mustAssertVersion?: boolean;
14
+ };
15
+ /**
16
+ * Law 6: every directory, dialect, secret name and worker is an OPTION. The defaults are empty, and
17
+ * a command that needs one it has not got refuses with 2 rather than measuring nothing and
18
+ * reporting green.
19
+ */
20
+ type ReleaseConfig = {
21
+ migrations?: MigrationDir[];
22
+ proof?: ProofConfig;
23
+ secrets?: string[];
24
+ steps?: string[];
25
+ workers?: string[];
26
+ wrangler?: string[];
27
+ wranglerEnv?: string;
28
+ };
29
+ type Refusal = {
30
+ line: number;
31
+ path: string;
32
+ verb: string;
33
+ };
34
+ /** A marker the gate could not believe, and the sentence saying which part it could not believe. */
35
+ type MarkerRefusal = {
36
+ line: number;
37
+ path: string;
38
+ why: string;
39
+ };
40
+ type MigrationsReport = {
41
+ linted: number;
42
+ markers: MarkerRefusal[];
43
+ ok: boolean;
44
+ read: number;
45
+ refusals: Refusal[];
46
+ squawk: {
47
+ findings: string;
48
+ path: string;
49
+ }[];
50
+ };
51
+
52
+ declare const parseReleaseConfig: (raw: unknown) => ReleaseConfig;
53
+ /** geonosis.json is where a repo says what it has. Its absence is a repo that has said nothing. */
54
+ declare const readReleaseConfig: (root: string) => ReleaseConfig;
55
+
56
+ type Declared = {
57
+ bindings: string[];
58
+ crons: string[];
59
+ routes: string[];
60
+ };
61
+ /**
62
+ * JSON with comments and trailing commas — which is what `wrangler.jsonc` is, and the format both
63
+ * consumers actually use. Strings are walked over rather than regexed around, because a `//` inside
64
+ * a URL is not a comment and a repo whose route was silently truncated would read as no route.
65
+ */
66
+ declare const parseJsonc: (source: string) => unknown;
67
+ /**
68
+ * Enough TOML for the keys a deployment drift check reads: `[table]`, `[[array of tables]]`,
69
+ * `key = value` with strings, numbers, booleans, arrays (multi-line included) and inline tables.
70
+ * It is deliberately not a TOML implementation — it exists because `wrangler.toml` is wrangler's
71
+ * default and refusing it outright would make this command unusable for most repos.
72
+ */
73
+ declare const parseToml: (source: string) => Record<string, unknown>;
74
+ declare const declaredIn: (config: Record<string, unknown>) => Declared;
75
+ declare const readWrangler: (root: string, relative: string, env?: string) => Declared;
76
+
77
+ /** Where a consumer's pipeline writes what it actually put in front of a user, after promote. */
78
+ declare const DEPLOYED_FILE = ".geonosis/deployed.json";
79
+ declare const NOTHING_SAYS = "nothing here says what is deployed \u2014 .geonosis/deployed.json is written by the pipeline after promote, and its absence is not a pass";
80
+ type Deployed = {
81
+ bindings: string[];
82
+ crons: string[];
83
+ routes: string[];
84
+ secrets: string[];
85
+ };
86
+ type Drift = {
87
+ kind: string;
88
+ missing: string[];
89
+ extra: string[];
90
+ };
91
+ type DeployedReport = {
92
+ at?: string;
93
+ drift: Drift[];
94
+ ok: boolean;
95
+ };
96
+ declare const readDeployed: (root: string) => {
97
+ at?: string;
98
+ deployed: Deployed;
99
+ };
100
+ /**
101
+ * The comparison itself, over two plain shapes. Ten lines, deliberately — the doctor asks the same
102
+ * question of the same file, and it asks it by reading the file rather than by importing this
103
+ * package, so a repo can install either one alone.
104
+ */
105
+ declare const driftBetween: (declared: Declared & {
106
+ secrets: string[];
107
+ }, deployed: Deployed) => Drift[];
108
+ declare const runDeployed: (input: {
109
+ root: string;
110
+ }) => DeployedReport;
111
+ declare const formatDeployed: (report: DeployedReport) => string;
112
+
113
+ type MigrationsInput = {
114
+ dialect?: Dialect;
115
+ root: string;
116
+ since: string;
117
+ };
118
+ declare const runMigrations: (input: MigrationsInput) => MigrationsReport;
119
+ declare const formatMigrations: (report: MigrationsReport) => string;
120
+
121
+ /**
122
+ * The contract, in the only order that means anything.
123
+ *
124
+ * `upload` moves no traffic. `park` puts the new version in the deployment at 0 %, which is what
125
+ * makes it reachable by an override at all. `prove` reaches it and asks it who it is. `promote`
126
+ * swaps the pointer. Promote before prove is the whole failure this package was extracted from;
127
+ * park before upload is nothing.
128
+ */
129
+ declare const STEPS: readonly ["upload", "park", "prove", "promote"];
130
+ /**
131
+ * C1.2 of the review this was extracted from. Cloudflare documents that an override which was not
132
+ * applied — propagation, a malformed dictionary, a version not in the deployment — causes the
133
+ * request to be routed by the configured PERCENTAGES instead, silently.
134
+ */
135
+ declare const WHY_VERSION_IDENTITY = "An override that was not applied routes by the configured percentages, to the OLD version \u2014 so a smoke that does not assert which version answered can pass by testing the code it was replacing.";
136
+ type PlanReport = {
137
+ accepted: boolean;
138
+ ok: boolean;
139
+ problems: string[];
140
+ steps: string[];
141
+ };
142
+ declare const runPlan: (input: {
143
+ accept: boolean;
144
+ root: string;
145
+ }) => PlanReport;
146
+ declare const formatPlan: (report: PlanReport) => string;
147
+
148
+ type Verdict = {
149
+ ok: boolean;
150
+ steps: string[];
151
+ why?: string;
152
+ };
153
+ /**
154
+ * One release, over one runner. The gate never invokes a platform; it asks, reads the answer, and
155
+ * REFUSES — and a refusal means the promote request is never written, which is the only reason the
156
+ * park step exists.
157
+ */
158
+ declare const proveOver: (input: {
159
+ args?: readonly string[];
160
+ command: string;
161
+ cwd: string;
162
+ timeoutMs?: number;
163
+ }) => Promise<Verdict>;
164
+ /** The three plants, and the honest runner that must still get through. */
165
+ declare const PLANTS: readonly [{
166
+ readonly name: "answered";
167
+ readonly says: "a smoke that answered a version other than the one it overrode to is refused";
168
+ }, {
169
+ readonly name: "failed";
170
+ readonly says: "a smoke that did not pass is refused";
171
+ }, {
172
+ readonly name: "promoted";
173
+ readonly says: "a promote of a version nothing proved is refused";
174
+ }];
175
+ type ProveOutcome = {
176
+ lines: string[];
177
+ ok: boolean;
178
+ };
179
+ /**
180
+ * A gate whose failure has never been observed is a claim, not a gate. With no runner named, each
181
+ * refusal is planted into the package's own stub and required to be caught — and the honest runner
182
+ * is required to pass, because a gate that refused everything would be as useless as one that
183
+ * refused nothing.
184
+ */
185
+ declare const prove: (cwd: string, stub?: string) => Promise<ProveOutcome>;
186
+ declare const formatProve: (outcome: ProveOutcome) => string;
187
+ declare const formatVerdict: (verdict: Verdict) => string;
188
+
189
+ type Request = {
190
+ step: 'promote';
191
+ versionId: string;
192
+ } | {
193
+ step: 'smoke';
194
+ versionId: string;
195
+ } | {
196
+ step: 'upload';
197
+ };
198
+ type Reply = Record<string, unknown>;
199
+ /**
200
+ * A consumer executable speaking JSON on stdio, one request per line and one reply per line.
201
+ *
202
+ * This is the whole seam. Every platform call — `wrangler versions upload`, the override header,
203
+ * `versions deploy` — lives on the far side of it, in the consumer's repo, where the credentials
204
+ * and the knowledge of which workers exist already are. Nothing on this side has ever heard of
205
+ * Cloudflare.
206
+ */
207
+ declare class Runner {
208
+ private buffered;
209
+ private closed;
210
+ private readonly child;
211
+ private readonly waiting;
212
+ constructor(command: string, args: readonly string[], cwd: string);
213
+ ask(request: Request, timeoutMs?: number): Promise<Reply>;
214
+ close(): void;
215
+ }
216
+
217
+ type Extracted = {
218
+ line: number;
219
+ sql: string;
220
+ };
221
+ /**
222
+ * Every string argument of `addSql(` inside the requested phases. Handles a template literal, a
223
+ * quoted string, either of them opening on the line after the call, a literal that itself spans
224
+ * lines, and adjacent literals joined by `+`. Everything else — an identifier holding the SQL, an
225
+ * interpolation — is refused by name: resolving one is a TypeScript parser, and this package will
226
+ * not grow one.
227
+ */
228
+ declare const addSqlIn: (source: string, phases: readonly Phase[]) => Extracted[];
229
+
230
+ type Marker = {
231
+ line: number;
232
+ reason: string;
233
+ since?: string;
234
+ };
235
+ declare const markersIn: (source: string) => Marker[];
236
+
237
+ /**
238
+ * Narrowing verbs. Each removes something a version that is still serving — or one a rollback could
239
+ * put back — may still be reading or writing. The schema is live from the instant the migration
240
+ * runs, minutes before any code has been proven and for ever if the release is rolled back.
241
+ */
242
+ declare const NARROWING: readonly (readonly [string, RegExp])[];
243
+ /**
244
+ * Comments are where the marker lives, so they are not stripped; strings are, because a verb inside
245
+ * one is data, not a statement. The replacement keeps the same length AND the same newlines, so a
246
+ * line number counted afterwards is still the line number in the file somebody has open.
247
+ */
248
+ declare const statementsOf: (sql: string) => string;
249
+ /** One refusal per verb per file, at the line of that verb's first occurrence. */
250
+ declare const refusalsIn: (path: string, body: string, firstLine?: number) => Refusal[];
251
+
252
+ export { DEPLOYED_FILE, type Declared, type Deployed, type DeployedReport, type Dialect, type Drift, type MigrationDir, type MigrationsReport, NARROWING, NOTHING_SAYS, PLANTS, type Phase, type PlanReport, type ProveOutcome, type Refusal, type ReleaseConfig, Runner, STEPS, type Verdict, WHY_VERSION_IDENTITY, addSqlIn, declaredIn, driftBetween, formatDeployed, formatMigrations, formatPlan, formatProve, formatVerdict, markersIn, parseJsonc, parseReleaseConfig, parseToml, prove, proveOver, readDeployed, readReleaseConfig, readWrangler, refusalsIn, runDeployed, runMigrations, runPlan, statementsOf };
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ import {
2
+ DEPLOYED_FILE,
3
+ NARROWING,
4
+ NOTHING_SAYS,
5
+ PLANTS,
6
+ Runner,
7
+ STEPS,
8
+ WHY_VERSION_IDENTITY,
9
+ addSqlIn,
10
+ declaredIn,
11
+ driftBetween,
12
+ formatDeployed,
13
+ formatMigrations,
14
+ formatPlan,
15
+ formatProve,
16
+ formatVerdict,
17
+ markersIn,
18
+ parseJsonc,
19
+ parseReleaseConfig,
20
+ parseToml,
21
+ prove,
22
+ proveOver,
23
+ readDeployed,
24
+ readReleaseConfig,
25
+ readWrangler,
26
+ refusalsIn,
27
+ runDeployed,
28
+ runMigrations,
29
+ runPlan,
30
+ statementsOf
31
+ } from "./chunk-2JLVQK7Y.js";
32
+ export {
33
+ DEPLOYED_FILE,
34
+ NARROWING,
35
+ NOTHING_SAYS,
36
+ PLANTS,
37
+ Runner,
38
+ STEPS,
39
+ WHY_VERSION_IDENTITY,
40
+ addSqlIn,
41
+ declaredIn,
42
+ driftBetween,
43
+ formatDeployed,
44
+ formatMigrations,
45
+ formatPlan,
46
+ formatProve,
47
+ formatVerdict,
48
+ markersIn,
49
+ parseJsonc,
50
+ parseReleaseConfig,
51
+ parseToml,
52
+ prove,
53
+ proveOver,
54
+ readDeployed,
55
+ readReleaseConfig,
56
+ readWrangler,
57
+ refusalsIn,
58
+ runDeployed,
59
+ runMigrations,
60
+ runPlan,
61
+ statementsOf
62
+ };
@@ -0,0 +1,160 @@
1
+ import {
2
+ CannotRun,
3
+ formatDeployed,
4
+ formatMigrations,
5
+ formatPlan,
6
+ formatProve,
7
+ formatVerdict,
8
+ prove,
9
+ proveOver,
10
+ runDeployed,
11
+ runMigrations,
12
+ runPlan
13
+ } from "./chunk-2JLVQK7Y.js";
14
+
15
+ // src/release-cli.ts
16
+ import { resolve } from "path";
17
+ var USAGE = `geonosis-release <command> [options]
18
+
19
+ migrations --since <ref> [--dialect <name>] [--root <dir>] [--json]
20
+
21
+ Refuse a schema change the version that is still serving cannot survive. The schema
22
+ serves 100 % of traffic the instant the migration runs \u2014 before any code has been
23
+ proven, and for ever if the release is rolled back \u2014 so a release may only WIDEN.
24
+
25
+ Dialects are routed by geonosis.json \u2192 release.migrations[], never sniffed:
26
+ sqlite the narrowing-verb check (D1 is not Postgres and squawk cannot
27
+ parse it)
28
+ postgres squawk-cli, its exit code and output surfaced verbatim
29
+ mikro-orm-ts the SQL extracted from addSql(\u2026) in up(), read both ways
30
+
31
+ Exit 0 clean, 1 refusals, 2 the run could not be made.
32
+
33
+ plan [--root <dir>] [--i-accept-an-unproven-smoke] [--json]
34
+
35
+ The contract as data \u2014 geonosis.json \u2192 release.steps and release.proof \u2014 validated
36
+ and printed. upload \u2192 park \u2192 prove \u2192 promote, in that order and no other.
37
+
38
+ prove [--runner <script>] [--root <dir>] [--json]
39
+
40
+ With a runner: one release over it, refusing a smoke that answered a version other
41
+ than the one it overrode to, a smoke that failed, and a promote of a version nothing
42
+ proved. With no runner: each of those three planted into this package's own stub and
43
+ required to be caught.
44
+
45
+ The runner is a consumer executable speaking JSON on stdio, one request per line:
46
+ {"step":"upload"} -> {"versionId":"\u2026"}
47
+ {"step":"smoke","versionId":"\u2026"} -> {"ok":true,"answeredVersionId":"\u2026"}
48
+ {"step":"promote","versionId":"\u2026"} -> {"ok":true}
49
+
50
+ deployed --check [--root <dir>] [--json]
51
+
52
+ What the pipeline REPORTED deploying, in .geonosis/deployed.json, against what the
53
+ tree declares in the wrangler configs geonosis.json names and release.secrets.
54
+ \`versions upload\` applies no triggers, and a secret can be put outside the gate
55
+ entirely \u2014 so declared and deployed drift apart silently, with a green pipeline.
56
+ `;
57
+ var VALUED = /* @__PURE__ */ new Set(["--dialect", "--root", "--runner", "--since"]);
58
+ var FLAGS = /* @__PURE__ */ new Set(["--check", "--i-accept-an-unproven-smoke", "--json"]);
59
+ var DIALECTS = /* @__PURE__ */ new Set(["mikro-orm-ts", "postgres", "sqlite"]);
60
+ var parseArgs = (argv) => {
61
+ const read = {};
62
+ const flags = /* @__PURE__ */ new Set();
63
+ let json = false;
64
+ for (let index = 1; index < argv.length; index += 1) {
65
+ const flag = argv[index] ?? "";
66
+ if (FLAGS.has(flag)) {
67
+ if (flag === "--json") json = true;
68
+ flags.add(flag);
69
+ continue;
70
+ }
71
+ if (!VALUED.has(flag)) throw new CannotRun(`unknown argument "${flag}"`);
72
+ const value = argv[index + 1];
73
+ if (value === void 0 || value.startsWith("--")) throw new CannotRun(`${flag} needs a value`);
74
+ read[flag] = value;
75
+ index += 1;
76
+ }
77
+ return { command: argv[0] ?? "", flags, json, read };
78
+ };
79
+ var rootOf = (parsed, cwd) => resolve(cwd, parsed.read["--root"] ?? cwd);
80
+ var migrations = (parsed, cwd) => {
81
+ const since = parsed.read["--since"];
82
+ if (since === void 0) throw new CannotRun("migrations needs --since <ref>");
83
+ const dialect = parsed.read["--dialect"];
84
+ if (dialect !== void 0 && !DIALECTS.has(dialect)) {
85
+ throw new CannotRun(`--dialect must be one of ${[...DIALECTS].toSorted().join(", ")}`);
86
+ }
87
+ const report = runMigrations({
88
+ ...dialect === void 0 ? {} : { dialect },
89
+ root: resolve(cwd, parsed.read["--root"] ?? cwd),
90
+ since
91
+ });
92
+ process.stdout.write(
93
+ parsed.json ? `${JSON.stringify(report, void 0, 2)}
94
+ ` : formatMigrations(report)
95
+ );
96
+ return report.ok ? 0 : 1;
97
+ };
98
+ var plan = (parsed, cwd) => {
99
+ const report = runPlan({
100
+ accept: parsed.flags.has("--i-accept-an-unproven-smoke"),
101
+ root: rootOf(parsed, cwd)
102
+ });
103
+ process.stdout.write(
104
+ parsed.json ? `${JSON.stringify(report, void 0, 2)}
105
+ ` : formatPlan(report)
106
+ );
107
+ return report.ok ? 0 : 1;
108
+ };
109
+ var proving = async (parsed, cwd) => {
110
+ const root = rootOf(parsed, cwd);
111
+ const named = parsed.read["--runner"];
112
+ if (named === void 0) {
113
+ const outcome = await prove(root);
114
+ process.stdout.write(
115
+ parsed.json ? `${JSON.stringify(outcome, void 0, 2)}
116
+ ` : formatProve(outcome)
117
+ );
118
+ return outcome.ok ? 0 : 1;
119
+ }
120
+ const verdict = await proveOver({ command: resolve(cwd, named), cwd: root });
121
+ process.stdout.write(
122
+ parsed.json ? `${JSON.stringify(verdict, void 0, 2)}
123
+ ` : formatVerdict(verdict)
124
+ );
125
+ return verdict.ok ? 0 : 1;
126
+ };
127
+ var deployed = (parsed, cwd) => {
128
+ if (!parsed.flags.has("--check")) throw new CannotRun("deployed needs --check");
129
+ const report = runDeployed({ root: rootOf(parsed, cwd) });
130
+ process.stdout.write(
131
+ parsed.json ? `${JSON.stringify(report, void 0, 2)}
132
+ ` : formatDeployed(report)
133
+ );
134
+ return report.ok ? 0 : 1;
135
+ };
136
+ var main = async () => {
137
+ const argv = process.argv.slice(2);
138
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
139
+ process.stdout.write(USAGE);
140
+ return 0;
141
+ }
142
+ const parsed = parseArgs(argv);
143
+ if (parsed.command === "migrations") return migrations(parsed, process.cwd());
144
+ if (parsed.command === "plan") return plan(parsed, process.cwd());
145
+ if (parsed.command === "prove") return proving(parsed, process.cwd());
146
+ if (parsed.command === "deployed") return deployed(parsed, process.cwd());
147
+ throw new CannotRun(
148
+ `no command called "${parsed.command}" \u2014 this bin has migrations, plan, prove, deployed`
149
+ );
150
+ };
151
+ try {
152
+ process.exit(await main());
153
+ } catch (error) {
154
+ process.stderr.write(`geonosis-release: ${error.message}
155
+ `);
156
+ process.exit(2);
157
+ }
158
+ export {
159
+ parseArgs
160
+ };
@@ -0,0 +1,31 @@
1
+ // src/stub-runner.ts
2
+ import { createInterface } from "readline";
3
+ var plant = (process.argv.find((one) => one.startsWith("--plant=")) ?? "--plant=good").slice(
4
+ "--plant=".length
5
+ );
6
+ var reply = (answer) => {
7
+ process.stdout.write(`${JSON.stringify(answer)}
8
+ `);
9
+ };
10
+ createInterface({ input: process.stdin }).on("line", (line) => {
11
+ const request = JSON.parse(line);
12
+ if (request.step === "upload") {
13
+ reply({ versionId: "version-under-test" });
14
+ return;
15
+ }
16
+ if (request.step === "smoke") {
17
+ reply({
18
+ answeredVersionId: plant === "answered" ? "the-version-that-was-already-serving" : "version-under-test",
19
+ ok: plant !== "failed"
20
+ });
21
+ return;
22
+ }
23
+ if (request.step === "promote") {
24
+ reply({
25
+ ok: true,
26
+ promotedVersionId: plant === "promoted" ? "a-version-nothing-proved" : "version-under-test"
27
+ });
28
+ return;
29
+ }
30
+ reply({ ok: false });
31
+ });
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@geonosis/release",
3
+ "version": "1.1.0",
4
+ "types": "./dist/index.d.ts",
5
+ "description": "The release contract with its proofs: an expand-only migration gate over three dialects, a smoke that must name the version that answered it, and declared ≠ deployed.",
6
+ "keywords": [
7
+ "release",
8
+ "migrations",
9
+ "expand-contract",
10
+ "squawk",
11
+ "zero-downtime",
12
+ "gate",
13
+ "ci",
14
+ "drift"
15
+ ],
16
+ "homepage": "https://github.com/microcompanies/geonosis/tree/main/packages/release",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/microcompanies/geonosis.git",
20
+ "directory": "packages/release"
21
+ },
22
+ "license": "Apache-2.0",
23
+ "type": "module",
24
+ "main": "dist/index.js",
25
+ "bin": {
26
+ "geonosis-release": "bin/geonosis-release.mjs"
27
+ },
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "bin",
36
+ "dist"
37
+ ],
38
+ "dependencies": {
39
+ "squawk-cli": "2.63.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=22"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "typecheck": "tsc --noEmit"
50
+ }
51
+ }