@astralarium/repo-deploy-release 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +61 -2
- package/dist/bin/deploy-release.d.ts +2 -0
- package/dist/bin/deploy-release.js +7 -0
- package/dist/src/app.d.ts +143 -0
- package/dist/src/app.js +102 -0
- package/dist/src/bundle.d.ts +18 -0
- package/dist/src/bundle.js +41 -0
- package/dist/src/check.d.ts +44 -0
- package/dist/src/check.js +128 -0
- package/dist/src/cloudflare.d.ts +70 -0
- package/dist/src/cloudflare.js +170 -0
- package/dist/src/commands.d.ts +55 -0
- package/dist/src/commands.js +651 -0
- package/dist/src/cutover-at.d.ts +23 -0
- package/dist/src/cutover-at.js +152 -0
- package/dist/src/cutovers.d.ts +62 -0
- package/dist/src/cutovers.js +112 -0
- package/dist/src/deployer.d.ts +36 -0
- package/dist/src/deployer.js +63 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/lease.d.ts +36 -0
- package/dist/src/lease.js +112 -0
- package/dist/src/main.d.ts +6 -0
- package/dist/src/main.js +43 -0
- package/dist/src/process.d.ts +34 -0
- package/dist/src/process.js +68 -0
- package/dist/src/prompt.d.ts +5 -0
- package/dist/src/prompt.js +69 -0
- package/dist/src/provision.d.ts +36 -0
- package/dist/src/provision.js +230 -0
- package/dist/src/schema.d.ts +2 -0
- package/dist/src/schema.js +12 -0
- package/dist/src/session.d.ts +81 -0
- package/dist/src/session.js +275 -0
- package/dist/src/state.d.ts +55 -0
- package/dist/src/state.js +115 -0
- package/dist/src/status.d.ts +65 -0
- package/dist/src/status.js +224 -0
- package/dist/src/steps.d.ts +80 -0
- package/dist/src/steps.js +389 -0
- package/dist/src/target.d.ts +94 -0
- package/dist/src/target.js +246 -0
- package/dist/src/web.d.ts +28 -0
- package/dist/src/web.js +70 -0
- package/dist/src/workspace.d.ts +11 -0
- package/dist/src/workspace.js +40 -0
- package/dist/testkit/index.d.ts +4 -0
- package/dist/testkit/index.js +36 -0
- package/dist/testkit/release-bundles.d.ts +16 -0
- package/dist/testkit/release-bundles.js +56 -0
- package/dist/testkit/release-checks.d.ts +19 -0
- package/dist/testkit/release-checks.js +71 -0
- package/dist/testkit/release.d.ts +44 -0
- package/dist/testkit/release.js +61 -0
- package/package.json +64 -3
- package/sql/before-reset.sql +22 -0
- package/sql/schema.sql +164 -0
- package/sql/scrub.sql +5 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Test suite of an app's release declaration against the wrangler
|
|
2
|
+
* configs deploying it, and the released targets it reads from them. */
|
|
3
|
+
import { describe, it } from "vitest";
|
|
4
|
+
import { envNames } from "../src/app.js";
|
|
5
|
+
import { assertReleaseConfig, isBootstrapping } from "../src/check.js";
|
|
6
|
+
import { readEnvBlock, resolveTarget, } from "../src/target.js";
|
|
7
|
+
import { checkDatabase, checkQueueRetries, checkScripts, checkSite, checkSlotBindings, } from "./release-checks.js";
|
|
8
|
+
/** Packages of `target`: the web and slotted workers, or an in-place
|
|
9
|
+
* env's workers. */
|
|
10
|
+
const packagesOf = (target) => target.kind === "blue-green"
|
|
11
|
+
? [target.packages.web, target.packages.worker]
|
|
12
|
+
: target.workers.map((w) => w.package);
|
|
13
|
+
/** Every target of `app` its configs declare, with their env blocks;
|
|
14
|
+
* an env whose target is undeclared is left out. */
|
|
15
|
+
export function releasedTargets(app) {
|
|
16
|
+
return envNames(app).flatMap((env) => {
|
|
17
|
+
const { target } = resolveTarget(app, env);
|
|
18
|
+
if (!target)
|
|
19
|
+
return [];
|
|
20
|
+
const packages = packagesOf(target);
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
env,
|
|
24
|
+
target,
|
|
25
|
+
bootstrapping: isBootstrapping(app, env),
|
|
26
|
+
blocks: Object.fromEntries(packages.map((pkg) => [pkg, readEnvBlock(app, pkg, env)])),
|
|
27
|
+
packages,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Tests of `app`'s declaration against the repo's wrangler configs and
|
|
33
|
+
* secrets templates, every declared target's. */
|
|
34
|
+
export function testRelease(app) {
|
|
35
|
+
const targets = releasedTargets(app);
|
|
36
|
+
const { queueMaxRetries } = app;
|
|
37
|
+
it("declares the release its wrangler configs and secrets templates commit: bootstrapping exactly the envs with a config gap", () => {
|
|
38
|
+
assertReleaseConfig(app);
|
|
39
|
+
});
|
|
40
|
+
it("names each script in one target", () => {
|
|
41
|
+
checkScripts(targets);
|
|
42
|
+
});
|
|
43
|
+
describe.each(targets)("the $env target", (released) => {
|
|
44
|
+
it("serves the site every worker names, routed through the web worker", () => {
|
|
45
|
+
checkSite(released);
|
|
46
|
+
});
|
|
47
|
+
it("shares one Hyperdrive config between its workers, a real one of its own once provisioned, or none without a database", () => {
|
|
48
|
+
checkDatabase(released, targets.filter((t) => t.env !== released.env));
|
|
49
|
+
});
|
|
50
|
+
it(queueMaxRetries === undefined
|
|
51
|
+
? "declares no queue consumers, the app lacking queue retries"
|
|
52
|
+
: "gives each queue consumer the app's queue retries", () => {
|
|
53
|
+
checkQueueRetries(released, queueMaxRetries);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe.each(targets.filter((t) => t.target.kind === "blue-green"))("the $env blue/green target", (released) => {
|
|
57
|
+
it("binds both slot scripts in the web worker", () => {
|
|
58
|
+
checkSlotBindings(released);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astralarium/repo-deploy-release",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"homepage": "https://github.com/astralarium/repo/tree/main/packages/repo-deploy-release#readme",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/astralarium/repo.git",
|
|
9
|
+
"directory": "packages/repo-deploy-release"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"sql"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./dist/src/index.js",
|
|
18
|
+
"./schema": "./dist/src/schema.js",
|
|
19
|
+
"./state": "./dist/src/state.js",
|
|
20
|
+
"./testkit": "./dist/testkit/index.js",
|
|
21
|
+
"./testkit/release": "./dist/testkit/release.js",
|
|
22
|
+
"./testkit/release-bundles": "./dist/testkit/release-bundles.js"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"deploy-release": "./dist/bin/deploy-release.js"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@stricli/core": "^1.3.0",
|
|
32
|
+
"json5": "^2.2.3",
|
|
33
|
+
"postgres": "^3.4.9",
|
|
34
|
+
"zod": "^4.6.7",
|
|
35
|
+
"@astralarium/repo-deploy-info": "^0.1.1",
|
|
36
|
+
"@astralarium/repo-postgres": "^0.2.0",
|
|
37
|
+
"@astralarium/repo-telemetry": "^0.2.0",
|
|
38
|
+
"@astralarium/repo-package-files": "^0.1.0",
|
|
39
|
+
"@astralarium/repo-deploy-workflows": "^0.1.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^24.2.0",
|
|
43
|
+
"eslint": "^10.2.1",
|
|
44
|
+
"typescript": "^6.0.3",
|
|
45
|
+
"vitest": "^5.0.3",
|
|
46
|
+
"wrangler": "^4.132.0",
|
|
47
|
+
"@astralarium/repo-config-typescript": "^0.1.0",
|
|
48
|
+
"@astralarium/repo-config-vitest": "^0.2.0",
|
|
49
|
+
"@astralarium/repo-config-eslint": "^0.3.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"vitest": "^5.0.3"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"vitest": {
|
|
56
|
+
"optional": true
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=24.2"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"check-types": "tsc --noEmit && tsc -p tsconfig.build.json --noEmit",
|
|
64
|
+
"lint": "eslint",
|
|
65
|
+
"test": "vitest"
|
|
66
|
+
}
|
|
6
67
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Deploy lease guard, ahead of reset.sql's drop: raises while a deploy
|
|
2
|
+
-- command holds the lease; proceeds while the deploy schema is absent. The
|
|
3
|
+
-- table lock holds through the drop: a lease taken meanwhile waits, then
|
|
4
|
+
-- fails.
|
|
5
|
+
DO $$
|
|
6
|
+
DECLARE
|
|
7
|
+
holder text;
|
|
8
|
+
operation text;
|
|
9
|
+
expires_at timestamptz;
|
|
10
|
+
BEGIN
|
|
11
|
+
IF to_regclass('deploy.state') IS NULL THEN
|
|
12
|
+
RETURN;
|
|
13
|
+
END IF;
|
|
14
|
+
LOCK TABLE deploy.state IN ACCESS EXCLUSIVE MODE;
|
|
15
|
+
SELECT d.lease_holder, d.lease_operation, d.lease_expires_at
|
|
16
|
+
INTO holder, operation, expires_at
|
|
17
|
+
FROM deploy.state d;
|
|
18
|
+
IF expires_at > now() THEN
|
|
19
|
+
RAISE EXCEPTION 'A deploy command holds the lease: % (%). Wait for it, or run deploy-release unlock for the env.',
|
|
20
|
+
holder, operation;
|
|
21
|
+
END IF;
|
|
22
|
+
END $$;
|
package/sql/schema.sql
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
-- Deploy state in the `deploy` schema: the live build, the scheduled
|
|
2
|
+
-- update and the deploy lease. Loads under `@astralarium/repo-postgres`'s roles:
|
|
3
|
+
-- app_admin owns the schema, app_service is the workers' login.
|
|
4
|
+
SET ROLE app_admin;
|
|
5
|
+
|
|
6
|
+
CREATE SCHEMA deploy AUTHORIZATION app_admin;
|
|
7
|
+
|
|
8
|
+
GRANT USAGE ON SCHEMA deploy TO app_service;
|
|
9
|
+
|
|
10
|
+
-- A `deploy-release` command that changes the deployment.
|
|
11
|
+
CREATE TYPE deploy.operation AS ENUM
|
|
12
|
+
('init', 'stage', 'deploy', 'cutover', 'rollback', 'settle', 'cancel',
|
|
13
|
+
'secrets');
|
|
14
|
+
|
|
15
|
+
-- Deployment state, one row: live build, scheduled update, and the lease
|
|
16
|
+
-- of the command changing the deployment. `version` rises with each build
|
|
17
|
+
-- or update write, in commit order (writers hold the row lock).
|
|
18
|
+
|
|
19
|
+
CREATE TABLE deploy.state
|
|
20
|
+
(
|
|
21
|
+
id boolean PRIMARY KEY DEFAULT true CHECK (id),
|
|
22
|
+
version bigint NOT NULL DEFAULT 0,
|
|
23
|
+
-- The live build; null before the first release.
|
|
24
|
+
live_build text,
|
|
25
|
+
-- The scheduled update; the app closes to new work ahead of it.
|
|
26
|
+
update_at timestamptz,
|
|
27
|
+
-- The cutover run owning the update: its writes fence on it.
|
|
28
|
+
update_run text CHECK ((update_run IS NULL) = (update_at IS NULL)),
|
|
29
|
+
-- The lease: who changes the deployment, doing what, until when.
|
|
30
|
+
lease_holder text,
|
|
31
|
+
lease_operation deploy.operation,
|
|
32
|
+
lease_expires_at timestamptz,
|
|
33
|
+
CHECK ((lease_holder IS NULL) = (lease_operation IS NULL)
|
|
34
|
+
AND (lease_holder IS NULL) = (lease_expires_at IS NULL))
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
INSERT INTO deploy.state DEFAULT VALUES;
|
|
38
|
+
|
|
39
|
+
GRANT SELECT, UPDATE ON deploy.state TO app_service;
|
|
40
|
+
|
|
41
|
+
-- Takes the lease for `p_holder` doing `p_operation` for `p_ttl` when
|
|
42
|
+
-- free, expired, or its own (a renewal keeps its operation). One row:
|
|
43
|
+
-- whether taken, and the lease as it stands.
|
|
44
|
+
CREATE FUNCTION deploy.take_lease(
|
|
45
|
+
p_holder text,
|
|
46
|
+
p_operation deploy.operation,
|
|
47
|
+
p_ttl interval
|
|
48
|
+
)
|
|
49
|
+
RETURNS TABLE (
|
|
50
|
+
taken boolean,
|
|
51
|
+
holder text,
|
|
52
|
+
operation deploy.operation,
|
|
53
|
+
expires_at timestamptz
|
|
54
|
+
)
|
|
55
|
+
LANGUAGE plpgsql
|
|
56
|
+
SET search_path = pg_catalog
|
|
57
|
+
AS $$
|
|
58
|
+
BEGIN
|
|
59
|
+
UPDATE deploy.state d
|
|
60
|
+
SET lease_holder = p_holder,
|
|
61
|
+
lease_operation = CASE
|
|
62
|
+
WHEN d.lease_holder = p_holder AND d.lease_expires_at > now()
|
|
63
|
+
THEN d.lease_operation
|
|
64
|
+
ELSE p_operation
|
|
65
|
+
END,
|
|
66
|
+
lease_expires_at = now() + p_ttl
|
|
67
|
+
WHERE d.lease_holder IS NULL
|
|
68
|
+
OR d.lease_holder = p_holder
|
|
69
|
+
OR d.lease_expires_at < now()
|
|
70
|
+
RETURNING true, d.lease_holder, d.lease_operation, d.lease_expires_at
|
|
71
|
+
INTO taken, holder, operation, expires_at;
|
|
72
|
+
IF NOT FOUND THEN
|
|
73
|
+
-- The lease as the race winner left it.
|
|
74
|
+
SELECT false, d.lease_holder, d.lease_operation, d.lease_expires_at
|
|
75
|
+
INTO taken, holder, operation, expires_at
|
|
76
|
+
FROM deploy.state d;
|
|
77
|
+
END IF;
|
|
78
|
+
RETURN NEXT;
|
|
79
|
+
END;
|
|
80
|
+
$$;
|
|
81
|
+
|
|
82
|
+
COMMENT ON FUNCTION deploy.take_lease(text, deploy.operation, interval) IS
|
|
83
|
+
'Takes or renews the deploy lease; reports the lease as it stands.';
|
|
84
|
+
REVOKE ALL ON FUNCTION deploy.take_lease(text, deploy.operation, interval) FROM PUBLIC;
|
|
85
|
+
GRANT EXECUTE ON FUNCTION deploy.take_lease(text, deploy.operation, interval) TO app_service;
|
|
86
|
+
|
|
87
|
+
-- Extends `p_holder`'s unexpired lease to `p_ttl` from now. One row:
|
|
88
|
+
-- whether renewed, and the lease as it stands.
|
|
89
|
+
CREATE FUNCTION deploy.renew_lease(p_holder text, p_ttl interval)
|
|
90
|
+
RETURNS TABLE (
|
|
91
|
+
renewed boolean,
|
|
92
|
+
holder text,
|
|
93
|
+
operation deploy.operation,
|
|
94
|
+
expires_at timestamptz
|
|
95
|
+
)
|
|
96
|
+
LANGUAGE plpgsql
|
|
97
|
+
SET search_path = pg_catalog
|
|
98
|
+
AS $$
|
|
99
|
+
BEGIN
|
|
100
|
+
UPDATE deploy.state d
|
|
101
|
+
SET lease_expires_at = now() + p_ttl
|
|
102
|
+
WHERE d.lease_holder = p_holder AND d.lease_expires_at > now()
|
|
103
|
+
RETURNING true, d.lease_holder, d.lease_operation, d.lease_expires_at
|
|
104
|
+
INTO renewed, holder, operation, expires_at;
|
|
105
|
+
IF NOT FOUND THEN
|
|
106
|
+
SELECT false, d.lease_holder, d.lease_operation, d.lease_expires_at
|
|
107
|
+
INTO renewed, holder, operation, expires_at
|
|
108
|
+
FROM deploy.state d;
|
|
109
|
+
END IF;
|
|
110
|
+
RETURN NEXT;
|
|
111
|
+
END;
|
|
112
|
+
$$;
|
|
113
|
+
|
|
114
|
+
COMMENT ON FUNCTION deploy.renew_lease(text, interval) IS
|
|
115
|
+
'Extends the holder''s unexpired deploy lease; reports the lease as it stands.';
|
|
116
|
+
REVOKE ALL ON FUNCTION deploy.renew_lease(text, interval) FROM PUBLIC;
|
|
117
|
+
GRANT EXECUTE ON FUNCTION deploy.renew_lease(text, interval) TO app_service;
|
|
118
|
+
|
|
119
|
+
-- Row-locks the deploy state for the calling transaction while `p_holder`
|
|
120
|
+
-- holds an unexpired lease; raises otherwise. Command writes follow in the
|
|
121
|
+
-- same transaction.
|
|
122
|
+
CREATE FUNCTION deploy.fence_lease(p_holder text)
|
|
123
|
+
RETURNS void
|
|
124
|
+
LANGUAGE plpgsql
|
|
125
|
+
SET search_path = pg_catalog
|
|
126
|
+
AS $$
|
|
127
|
+
DECLARE
|
|
128
|
+
holder text;
|
|
129
|
+
expires_at timestamptz;
|
|
130
|
+
BEGIN
|
|
131
|
+
SELECT d.lease_holder, d.lease_expires_at INTO holder, expires_at
|
|
132
|
+
FROM deploy.state d FOR UPDATE;
|
|
133
|
+
IF NOT coalesce(holder = p_holder AND expires_at > now(), false) THEN
|
|
134
|
+
RAISE EXCEPTION 'deploy lease lost: % holds it until %',
|
|
135
|
+
coalesce(holder, 'no one'), coalesce(expires_at::text, 'released');
|
|
136
|
+
END IF;
|
|
137
|
+
END;
|
|
138
|
+
$$;
|
|
139
|
+
|
|
140
|
+
COMMENT ON FUNCTION deploy.fence_lease(text) IS
|
|
141
|
+
'Locks the deploy state while the holder''s lease stands; raises otherwise.';
|
|
142
|
+
REVOKE ALL ON FUNCTION deploy.fence_lease(text) FROM PUBLIC;
|
|
143
|
+
GRANT EXECUTE ON FUNCTION deploy.fence_lease(text) TO app_service;
|
|
144
|
+
|
|
145
|
+
-- Drops the lease held by `p_holder`; a null holder breaks any. Returns
|
|
146
|
+
-- the holder dropped; null when none matched.
|
|
147
|
+
CREATE FUNCTION deploy.drop_lease(p_holder text)
|
|
148
|
+
RETURNS text
|
|
149
|
+
LANGUAGE sql
|
|
150
|
+
SET search_path = pg_catalog
|
|
151
|
+
AS $$
|
|
152
|
+
UPDATE deploy.state
|
|
153
|
+
SET lease_holder = NULL, lease_operation = NULL, lease_expires_at = NULL
|
|
154
|
+
WHERE lease_holder IS NOT NULL
|
|
155
|
+
AND (p_holder IS NULL OR lease_holder = p_holder)
|
|
156
|
+
RETURNING old.lease_holder;
|
|
157
|
+
$$;
|
|
158
|
+
|
|
159
|
+
COMMENT ON FUNCTION deploy.drop_lease(text) IS
|
|
160
|
+
'Drops the deploy lease of a holder, or any with a null holder.';
|
|
161
|
+
REVOKE ALL ON FUNCTION deploy.drop_lease(text) FROM PUBLIC;
|
|
162
|
+
GRANT EXECUTE ON FUNCTION deploy.drop_lease(text) TO app_service;
|
|
163
|
+
|
|
164
|
+
SET ROLE NONE;
|
package/sql/scrub.sql
ADDED