@augurworks/augur 0.15.1
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/CANVAS.md +858 -0
- package/INSTALL.md +603 -0
- package/LICENSE +27 -0
- package/README.md +150 -0
- package/TRADEMARKS.md +72 -0
- package/agents/README.md +65 -0
- package/agents/canon-extract.md +117 -0
- package/agents/canon.md +151 -0
- package/agents/canvas.md +88 -0
- package/agents/currency.md +74 -0
- package/agents/drafts.md +92 -0
- package/agents/identity.md +148 -0
- package/agents/prototype-contract.md +69 -0
- package/agents/publishing.md +324 -0
- package/agents/review-feedback.md +76 -0
- package/agents/space-json.md +132 -0
- package/agents/ui-skill.md +101 -0
- package/agents/working-marks.md +86 -0
- package/brand/augur-eye.svg +3 -0
- package/brand/augur-icon.svg +41 -0
- package/brand/augur-mark.svg +5 -0
- package/brand/lentianova-bulky.otf +0 -0
- package/brand/lentianova-median.otf +0 -0
- package/brand/lentianova.woff2 +0 -0
- package/build.js +8409 -0
- package/changelog.md +133 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/package.json +57 -0
- package/pitis/PETS.md +121 -0
- package/pitis/README.md +117 -0
- package/pitis/index.html +266 -0
- package/pitis/piti.build.js +81 -0
- package/pitis/piti.js +929 -0
- package/pitis/roast-agent.md +185 -0
- package/realtime/src/index.js +119 -0
- package/realtime/wrangler.example.toml +44 -0
- package/scripts/adopt.mjs +106 -0
- package/scripts/board-snapshot.mjs +197 -0
- package/scripts/build-seed-pack.mjs +33 -0
- package/scripts/bundle-rekey.mjs +112 -0
- package/scripts/bundle-tenancy-rehearsal.mjs +781 -0
- package/scripts/canon-extract.mjs +578 -0
- package/scripts/canon.mjs +599 -0
- package/scripts/canvas-screen.mjs +333 -0
- package/scripts/check-local.mjs +136 -0
- package/scripts/clawd-art.mjs +75 -0
- package/scripts/clawd-canvas.mjs +741 -0
- package/scripts/cli.mjs +100 -0
- package/scripts/clone.mjs +422 -0
- package/scripts/close.mjs +30 -0
- package/scripts/connect.mjs +103 -0
- package/scripts/deploy.mjs +157 -0
- package/scripts/dev.mjs +54 -0
- package/scripts/doc-lint.mjs +143 -0
- package/scripts/export.mjs +300 -0
- package/scripts/fork.mjs +64 -0
- package/scripts/freeze.mjs +74 -0
- package/scripts/frontdoor-parity.mjs +180 -0
- package/scripts/hook.mjs +67 -0
- package/scripts/identity-rekey.mjs +112 -0
- package/scripts/init.mjs +122 -0
- package/scripts/land.mjs +40 -0
- package/scripts/lib/adapters.mjs +156 -0
- package/scripts/lib/board-snapshot.mjs +511 -0
- package/scripts/lib/draft.mjs +385 -0
- package/scripts/lib/graduate.mjs +276 -0
- package/scripts/lib/instance.mjs +74 -0
- package/scripts/lib/marks.mjs +107 -0
- package/scripts/lib/materialize.mjs +138 -0
- package/scripts/lib/merge3.mjs +124 -0
- package/scripts/lib/offline-posture.mjs +120 -0
- package/scripts/lib/offline-respawn.mjs +18 -0
- package/scripts/lib/offline-wrangler.mjs +78 -0
- package/scripts/lib/publish-compose.mjs +12 -0
- package/scripts/lib/publish-conflict.mjs +78 -0
- package/scripts/lib/publish-evidence.mjs +144 -0
- package/scripts/lib/refine-compare.mjs +140 -0
- package/scripts/lib/refine-ledger.mjs +150 -0
- package/scripts/lib/refine-png.mjs +149 -0
- package/scripts/lib/seed-pack-build.mjs +141 -0
- package/scripts/lib/state-compare.mjs +68 -0
- package/scripts/lib/store.mjs +122 -0
- package/scripts/login.mjs +84 -0
- package/scripts/mark.mjs +112 -0
- package/scripts/migrate-board-keys.mjs +164 -0
- package/scripts/migrate.mjs +265 -0
- package/scripts/no-foreign-vocabulary.mjs +216 -0
- package/scripts/no-product-names.mjs +157 -0
- package/scripts/no-tenant-globals.mjs +1134 -0
- package/scripts/offline.mjs +247 -0
- package/scripts/og.mjs +170 -0
- package/scripts/one-tenant-resolver.mjs +146 -0
- package/scripts/open.mjs +63 -0
- package/scripts/publish.mjs +1041 -0
- package/scripts/read.mjs +31 -0
- package/scripts/refine.mjs +455 -0
- package/scripts/release-drift.mjs +138 -0
- package/scripts/restore.mjs +322 -0
- package/scripts/review.mjs +125 -0
- package/scripts/save.mjs +21 -0
- package/scripts/shell-lint.mjs +189 -0
- package/scripts/ship.mjs +460 -0
- package/scripts/shoot.mjs +264 -0
- package/scripts/smoke-invite.mjs +113 -0
- package/scripts/state-inventory.mjs +223 -0
- package/scripts/status.mjs +177 -0
- package/scripts/sync.mjs +27 -0
- package/scripts/tenant-do-rehearsal.mjs +1314 -0
- package/scripts/ui-copy-lint.mjs +202 -0
- package/scripts/watch.mjs +32 -0
- package/scripts/wrangler-preflight.mjs +246 -0
- package/seed/CANON.md +43 -0
- package/seed/README.md +119 -0
- package/seed/prototype-status.json +9 -0
- package/seed/registry.json +88 -0
- package/seed/skills/starter-ui/SKILL.md +52 -0
- package/seed/skills/starter-ui/skill.json +11 -0
- package/seed/skills/starter-ui/starter-tokens.css +73 -0
- package/seed/skills/starter-ui/starter-ui.css +318 -0
- package/seed/skills/starter-ui/starter-ui.js +50 -0
- package/seed/space.json +5 -0
- package/seed/start-here/prototypes/connect-your-terminal/index.html +428 -0
- package/seed/start-here/prototypes/connect-your-terminal/preview.webp +0 -0
- package/seed/start-here/prototypes/sample-with-comments/index.html +172 -0
- package/seed/start-here/prototypes/sample-with-comments/preview.webp +0 -0
- package/seed/start-here/prototypes/set-up-your-design-system/index.html +353 -0
- package/seed/start-here/prototypes/set-up-your-design-system/preview.webp +0 -0
- package/seed/threads.json +52 -0
- package/seed/worked-examples/prototypes/field-readings/index.html +273 -0
- package/seed/worked-examples/prototypes/field-readings/preview.webp +0 -0
- package/seed/worked-examples/prototypes/slide-deck/index.html +214 -0
- package/seed/worked-examples/prototypes/slide-deck/preview.webp +0 -0
- package/seed/worked-examples/prototypes/specimen-viewer/index.html +203 -0
- package/seed/worked-examples/prototypes/specimen-viewer/preview.webp +0 -0
- package/src/_worker.js +12668 -0
- package/src/board-key.mjs +57 -0
- package/src/board-room.mjs +881 -0
- package/src/bundle-keys.mjs +138 -0
- package/src/canon/collect-in-browser.js +215 -0
- package/src/canon/emit.mjs +232 -0
- package/src/canon/observe.mjs +260 -0
- package/src/canon/schema.mjs +451 -0
- package/src/canvas/DSEG-LICENSE.txt +95 -0
- package/src/canvas/DSEG7Classic-Bold.woff2 +0 -0
- package/src/canvas/canvas.css +885 -0
- package/src/canvas/canvas.js +5780 -0
- package/src/canvas/capture.js +485 -0
- package/src/chrome/appchrome.mjs +568 -0
- package/src/currency.mjs +203 -0
- package/src/delete-confirmation.mjs +254 -0
- package/src/drafts/drafts.js +326 -0
- package/src/entry.js +58 -0
- package/src/galleries.mjs +400 -0
- package/src/health-cron.mjs +173 -0
- package/src/identity.json +1 -0
- package/src/kv-codec.mjs +135 -0
- package/src/kv-identity.mjs +259 -0
- package/src/mail.mjs +599 -0
- package/src/provenance.mjs +74 -0
- package/src/publish-compose.mjs +363 -0
- package/src/publish-fork.mjs +221 -0
- package/src/publish-units.mjs +44 -0
- package/src/purge.mjs +81 -0
- package/src/review/COMMENTING-UX.md +306 -0
- package/src/review/cat.png +0 -0
- package/src/review/comment-cursor.svg +7 -0
- package/src/review/comments.js +1738 -0
- package/src/room-ticket.mjs +96 -0
- package/src/roster-chip.mjs +32 -0
- package/src/seed-pack.mjs +261 -0
- package/src/state-inventory.mjs +249 -0
- package/src/sw-logic.mjs +48 -0
- package/src/tenant-cache.mjs +141 -0
- package/src/tenant-context.mjs +386 -0
- package/src/tenant-do.js +2940 -0
- package/src/tenant-host.mjs +144 -0
- package/src/tenant-quotas.mjs +140 -0
- package/src/unit-core.mjs +103 -0
- package/src/unit-object.mjs +338 -0
- package/templates/README.md +67 -0
- package/templates/shell/deploy.yml +145 -0
- package/templates/shell/engine-bump.yml +142 -0
- package/templates/shell/health.yml +233 -0
- package/templates/shell/kv-backup.yml +201 -0
- package/templates/shell/roster-update.yml +110 -0
- package/templates/shell/space-preflight.yml +35 -0
- package/templates/shell/space-rebake.yml +79 -0
- package/templates/shell/store-backup.yml +152 -0
- package/templates/shell/wrangler.example.toml +266 -0
- package/templates/space/publish.yml +91 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// src/unit-object.mjs — one Durable Object per unit: the authority on main, drafts,
|
|
2
|
+
// landings and the landing lease. See docs/drafts-that-land.md §6.
|
|
3
|
+
//
|
|
4
|
+
// File BODIES never live here. A table row names a content hash; the bytes are in the
|
|
5
|
+
// bundle store under `blobs/<hash>`, shared and content-addressed, exactly as a publish
|
|
6
|
+
// leaves them. A table is stored as one JSON document per revision, well under the row
|
|
7
|
+
// limit for any prototype this engine has met.
|
|
8
|
+
//
|
|
9
|
+
// THE OBJECT NEVER MERGES. `save` applies a batch of changes with per-file compare-and-set
|
|
10
|
+
// or refuses the whole batch with the fresh hashes; `land` refuses when main moved since
|
|
11
|
+
// the draft's base. What to do about a refusal is the client's, on its own disk.
|
|
12
|
+
//
|
|
13
|
+
// THE LANDING LEASE. Landing writes the space manifest, and that write happens in the
|
|
14
|
+
// worker, outside this object's single thread. So `land` hands out a ten-second lease and
|
|
15
|
+
// refuses every other landing while it is held; `landed` commits under that lease. A
|
|
16
|
+
// worker that dies between the two leaves a lease that simply expires — nothing depends on
|
|
17
|
+
// it being released, and `abandon-land` is a courtesy.
|
|
18
|
+
import {
|
|
19
|
+
newDraftId, unitTable, sameTable, applyChanges, tableDelta, presenceOf, DRAFT_ID_RE,
|
|
20
|
+
} from "./unit-core.mjs";
|
|
21
|
+
|
|
22
|
+
export const LAND_LEASE_MS = 10_000;
|
|
23
|
+
export const UNIT_SCHEMA_VERSION = 1;
|
|
24
|
+
// A literal, not a constructed RegExp — its group mirrors DRAFT_ID_RE in unit-core.mjs.
|
|
25
|
+
const DRAFT_ROUTE_RE = /^\/draft\/([a-z0-9]{6})$/;
|
|
26
|
+
|
|
27
|
+
export const UNIT_SCHEMA = Object.freeze([
|
|
28
|
+
`CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL)`,
|
|
29
|
+
`CREATE TABLE IF NOT EXISTS landings (
|
|
30
|
+
revision INTEGER PRIMARY KEY,
|
|
31
|
+
tbl TEXT NOT NULL,
|
|
32
|
+
by TEXT,
|
|
33
|
+
session TEXT,
|
|
34
|
+
at TEXT NOT NULL,
|
|
35
|
+
note TEXT,
|
|
36
|
+
draft_id TEXT,
|
|
37
|
+
restored_from INTEGER
|
|
38
|
+
)`,
|
|
39
|
+
`CREATE TABLE IF NOT EXISTS drafts (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
owner TEXT NOT NULL,
|
|
42
|
+
session TEXT,
|
|
43
|
+
opened_at TEXT NOT NULL,
|
|
44
|
+
last_save_at TEXT,
|
|
45
|
+
base_revision INTEGER NOT NULL,
|
|
46
|
+
revision INTEGER NOT NULL,
|
|
47
|
+
tbl TEXT NOT NULL,
|
|
48
|
+
closed_at TEXT,
|
|
49
|
+
discarded INTEGER NOT NULL DEFAULT 0
|
|
50
|
+
)`,
|
|
51
|
+
`CREATE TABLE IF NOT EXISTS draft_saves (
|
|
52
|
+
draft_id TEXT NOT NULL,
|
|
53
|
+
revision INTEGER NOT NULL,
|
|
54
|
+
tbl TEXT NOT NULL,
|
|
55
|
+
at TEXT NOT NULL,
|
|
56
|
+
PRIMARY KEY (draft_id, revision)
|
|
57
|
+
)`,
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
export function applyUnitSchema(sql, workspace, unit) {
|
|
61
|
+
for (const stmt of UNIT_SCHEMA) sql.exec(stmt);
|
|
62
|
+
sql.exec(`INSERT INTO meta (k, v) VALUES ('schema_version', ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v`, String(UNIT_SCHEMA_VERSION));
|
|
63
|
+
if (workspace) sql.exec(`INSERT INTO meta (k, v) VALUES ('workspace', ?) ON CONFLICT(k) DO NOTHING`, String(workspace));
|
|
64
|
+
if (unit) sql.exec(`INSERT INTO meta (k, v) VALUES ('unit', ?) ON CONFLICT(k) DO NOTHING`, String(unit));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const json = (obj, status = 200) => new Response(JSON.stringify(obj), { status, headers: { "content-type": "application/json" } });
|
|
68
|
+
const rowDraft = (r) => r && ({
|
|
69
|
+
id: r.id, owner: r.owner, session: r.session || "", openedAt: r.opened_at, lastSaveAt: r.last_save_at || null,
|
|
70
|
+
baseRevision: Number(r.base_revision), revision: Number(r.revision), table: JSON.parse(r.tbl),
|
|
71
|
+
closedAt: r.closed_at || null, discarded: !!r.discarded,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export class UnitObject {
|
|
75
|
+
constructor(ctx, env) {
|
|
76
|
+
this.ctx = ctx;
|
|
77
|
+
this.env = env;
|
|
78
|
+
this.ready = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get sql() { return this.ctx.storage.sql; }
|
|
82
|
+
|
|
83
|
+
async init(workspace, unit) {
|
|
84
|
+
if (this.ready) return;
|
|
85
|
+
const run = () => { applyUnitSchema(this.sql, workspace, unit); this.ready = true; };
|
|
86
|
+
if (this.ctx.blockConcurrencyWhile) await this.ctx.blockConcurrencyWhile(async () => run());
|
|
87
|
+
else run();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── meta ──────────────────────────────────────────────────────────────────
|
|
91
|
+
metaGet(k) {
|
|
92
|
+
const rows = [...this.sql.exec(`SELECT v FROM meta WHERE k = ?`, k)];
|
|
93
|
+
return rows.length ? rows[0].v : null;
|
|
94
|
+
}
|
|
95
|
+
metaSet(k, v) {
|
|
96
|
+
if (v === null) this.sql.exec(`DELETE FROM meta WHERE k = ?`, k);
|
|
97
|
+
else this.sql.exec(`INSERT INTO meta (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v`, k, String(v));
|
|
98
|
+
}
|
|
99
|
+
mainRevision() { return Number(this.metaGet("main_revision") || 0); }
|
|
100
|
+
landing(revision) {
|
|
101
|
+
const rows = [...this.sql.exec(`SELECT * FROM landings WHERE revision = ?`, revision)];
|
|
102
|
+
return rows.length ? rows[0] : null;
|
|
103
|
+
}
|
|
104
|
+
mainTable() {
|
|
105
|
+
const l = this.landing(this.mainRevision());
|
|
106
|
+
return l ? JSON.parse(l.tbl) : {};
|
|
107
|
+
}
|
|
108
|
+
draft(id) {
|
|
109
|
+
const rows = [...this.sql.exec(`SELECT * FROM drafts WHERE id = ?`, id)];
|
|
110
|
+
return rows.length ? rowDraft(rows[0]) : null;
|
|
111
|
+
}
|
|
112
|
+
openDrafts() {
|
|
113
|
+
return [...this.sql.exec(`SELECT * FROM drafts WHERE closed_at IS NULL ORDER BY opened_at`)].map(rowDraft);
|
|
114
|
+
}
|
|
115
|
+
lease(nowMs) {
|
|
116
|
+
const raw = this.metaGet("lease");
|
|
117
|
+
if (!raw) return null;
|
|
118
|
+
const l = JSON.parse(raw);
|
|
119
|
+
if (Number(l.until) <= nowMs) { this.metaSet("lease", null); return null; }
|
|
120
|
+
return l;
|
|
121
|
+
}
|
|
122
|
+
writeLanding({ table, by, session, at, note, draftId, restoredFrom }) {
|
|
123
|
+
const revision = this.mainRevision() + 1;
|
|
124
|
+
this.sql.exec(
|
|
125
|
+
`INSERT INTO landings (revision, tbl, by, session, at, note, draft_id, restored_from) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
126
|
+
revision, JSON.stringify(table), by || null, session || null, at, note || null, draftId || null, restoredFrom == null ? null : restoredFrom,
|
|
127
|
+
);
|
|
128
|
+
this.metaSet("main_revision", revision);
|
|
129
|
+
return revision;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── verbs ─────────────────────────────────────────────────────────────────
|
|
133
|
+
syncMain({ table, at }) {
|
|
134
|
+
const cur = this.mainTable();
|
|
135
|
+
if (this.mainRevision() > 0 && sameTable(cur, table)) return { revision: this.mainRevision() };
|
|
136
|
+
return { revision: this.writeLanding({ table, by: "live", at, note: "adopted from live" }) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
open({ owner, session, at }) {
|
|
140
|
+
let id = newDraftId();
|
|
141
|
+
while (this.draft(id)) id = newDraftId();
|
|
142
|
+
const base = this.mainRevision();
|
|
143
|
+
const table = this.mainTable();
|
|
144
|
+
this.sql.exec(
|
|
145
|
+
`INSERT INTO drafts (id, owner, session, opened_at, base_revision, revision, tbl) VALUES (?, ?, ?, ?, ?, 0, ?)`,
|
|
146
|
+
id, String(owner || ""), String(session || ""), at, base, JSON.stringify(table),
|
|
147
|
+
);
|
|
148
|
+
return { draftId: id, baseRevision: base, table, presence: presenceOf(this.openDrafts(), Date.parse(at)) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
save({ draftId, draftRevision, changes, baseRevision, at }) {
|
|
152
|
+
const d = this.draft(draftId);
|
|
153
|
+
if (!d || d.closedAt) return [404, { error: "unknown-draft" }];
|
|
154
|
+
const held = this.lease(Date.parse(at));
|
|
155
|
+
if (held && held.draftId === draftId) return [409, { error: "landing-in-progress" }];
|
|
156
|
+
if (Number(draftRevision) !== d.revision) return [409, { error: "stale-draft-revision", draftRevision: d.revision }];
|
|
157
|
+
if (baseRevision !== undefined) {
|
|
158
|
+
const b = Number(baseRevision);
|
|
159
|
+
if (!Number.isInteger(b) || b < d.baseRevision || b > this.mainRevision()) return [400, { error: "bad-base", mainRevision: this.mainRevision() }];
|
|
160
|
+
}
|
|
161
|
+
const r = applyChanges(d.table, Array.isArray(changes) ? changes : []);
|
|
162
|
+
if (!r.ok) return [409, { error: "stale-draft", stale: r.stale }];
|
|
163
|
+
const revision = d.revision + 1;
|
|
164
|
+
this.sql.exec(
|
|
165
|
+
`UPDATE drafts SET revision = ?, tbl = ?, last_save_at = ?, base_revision = ? WHERE id = ?`,
|
|
166
|
+
revision, JSON.stringify(r.table), at, baseRevision === undefined ? d.baseRevision : Number(baseRevision), draftId,
|
|
167
|
+
);
|
|
168
|
+
this.sql.exec(`INSERT INTO draft_saves (draft_id, revision, tbl, at) VALUES (?, ?, ?, ?)`, draftId, revision, JSON.stringify(r.table), at);
|
|
169
|
+
return [200, { draftRevision: revision, table: r.table }];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
takeLease({ draftId, table, at, restoredFrom }) {
|
|
173
|
+
const nowMs = Date.parse(at);
|
|
174
|
+
const held = this.lease(nowMs);
|
|
175
|
+
if (held && held.draftId !== (draftId || null)) return [409, { error: "landing-in-progress" }];
|
|
176
|
+
const lease = newDraftId() + newDraftId();
|
|
177
|
+
const revision = this.mainRevision() + 1;
|
|
178
|
+
this.metaSet("lease", JSON.stringify({
|
|
179
|
+
token: lease, draftId: draftId || null, revision, until: nowMs + LAND_LEASE_MS,
|
|
180
|
+
table, restoredFrom: restoredFrom == null ? null : restoredFrom,
|
|
181
|
+
}));
|
|
182
|
+
const delta = tableDelta(this.mainTable(), table);
|
|
183
|
+
return [200, { lease, revision, table, ...delta }];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
land({ draftId, baseRevision, at }) {
|
|
187
|
+
const d = this.draft(draftId);
|
|
188
|
+
if (!d || d.closedAt) return [404, { error: "unknown-draft" }];
|
|
189
|
+
const main = this.mainRevision();
|
|
190
|
+
if (d.baseRevision !== main || Number(baseRevision) !== main) {
|
|
191
|
+
const base = this.landing(d.baseRevision);
|
|
192
|
+
const delta = tableDelta(base ? JSON.parse(base.tbl) : {}, this.mainTable());
|
|
193
|
+
return [409, { error: "main-moved", mainRevision: main, ...delta }];
|
|
194
|
+
}
|
|
195
|
+
// AN EMPTIED DRAFT IS NOT AN UNPUBLISH REQUEST. A draft whose every file is gone lands
|
|
196
|
+
// a unit with nothing behind it: the URL goes dark for everyone, the prefix is left
|
|
197
|
+
// declared, and the landing reads as a success. The one thing that reliably produces
|
|
198
|
+
// it is a folder that failed to materialise — a killed open, a disk that filled — so
|
|
199
|
+
// the answer is a refusal rather than a confirmation the object has no way to ask for.
|
|
200
|
+
// Deleting a prototype is its own verb, with its own confirmation.
|
|
201
|
+
if (!Object.keys(d.table).length && Object.keys(this.mainTable()).length) {
|
|
202
|
+
return [409, { error: "would-unpublish" }];
|
|
203
|
+
}
|
|
204
|
+
return this.takeLease({ draftId, table: d.table, at });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
restore({ revision, at }) {
|
|
208
|
+
const l = this.landing(Number(revision));
|
|
209
|
+
if (!l) return [404, { error: "unknown-revision" }];
|
|
210
|
+
return this.takeLease({ draftId: null, table: JSON.parse(l.tbl), at, restoredFrom: Number(revision) });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
landed({ lease, draftId, note, by, session, at }) {
|
|
214
|
+
const held = this.lease(Date.parse(at));
|
|
215
|
+
if (!held || held.token !== lease) return [409, { error: "bad-lease" }];
|
|
216
|
+
const table = held.table;
|
|
217
|
+
const revision = this.writeLanding({
|
|
218
|
+
table, by, session, at, note, draftId: held.draftId || draftId || null, restoredFrom: held.restoredFrom,
|
|
219
|
+
});
|
|
220
|
+
if (held.draftId) this.sql.exec(`UPDATE drafts SET closed_at = ? WHERE id = ?`, at, held.draftId);
|
|
221
|
+
this.metaSet("lease", null);
|
|
222
|
+
return [200, { revision }];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
abandonLand({ lease }) {
|
|
226
|
+
const raw = this.metaGet("lease");
|
|
227
|
+
if (raw && JSON.parse(raw).token === lease) this.metaSet("lease", null);
|
|
228
|
+
return [200, { ok: true }];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
sync({ draftId }) {
|
|
232
|
+
const d = this.draft(draftId);
|
|
233
|
+
if (!d || d.closedAt) return [404, { error: "unknown-draft" }];
|
|
234
|
+
const base = this.landing(d.baseRevision);
|
|
235
|
+
const delta = tableDelta(base ? JSON.parse(base.tbl) : {}, this.mainTable());
|
|
236
|
+
return [200, { mainRevision: this.mainRevision(), baseRevision: d.baseRevision, ...delta }];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
discard({ draftId, at }) {
|
|
240
|
+
const d = this.draft(draftId);
|
|
241
|
+
if (!d || d.closedAt) return [404, { error: "unknown-draft" }];
|
|
242
|
+
this.sql.exec(`UPDATE drafts SET closed_at = ?, discarded = 1 WHERE id = ?`, at, draftId);
|
|
243
|
+
return [200, { closed: true }];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
history() {
|
|
247
|
+
const rows = [...this.sql.exec(`SELECT * FROM landings ORDER BY revision DESC`)];
|
|
248
|
+
return {
|
|
249
|
+
revision: this.mainRevision(),
|
|
250
|
+
landings: rows.map((r) => ({
|
|
251
|
+
revision: Number(r.revision), by: r.by || null, session: r.session || "", at: r.at, note: r.note || "",
|
|
252
|
+
draftId: r.draft_id || null, restoredFrom: r.restored_from == null ? null : Number(r.restored_from),
|
|
253
|
+
files: Object.keys(JSON.parse(r.tbl)).length,
|
|
254
|
+
})),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── live tabs ─────────────────────────────────────────────────────────────
|
|
259
|
+
// One socket per open tab, accepted with the Hibernation API so an idle unit with tabs
|
|
260
|
+
// on it costs nothing. A SOCKET CARRIES NO AUTHORITY: the worker authenticated the
|
|
261
|
+
// person before forwarding the Upgrade, and every message sent here is a fact any member
|
|
262
|
+
// may read anyway — a draft saved, a landing happened. Nothing about a socket is stored
|
|
263
|
+
// beyond its attachment, nothing waits for one, and a verb's answer never depends on a
|
|
264
|
+
// send having worked: `emit` is fire-and-forget and closes what it cannot reach.
|
|
265
|
+
socket(request, url) {
|
|
266
|
+
if ((request.headers.get("Upgrade") || "").toLowerCase() !== "websocket") return json({ error: "expected-websocket" }, 426);
|
|
267
|
+
const draft = url.searchParams.get("draft") || null;
|
|
268
|
+
if (draft && !DRAFT_ID_RE.test(draft)) return json({ error: "bad-draft" }, 400);
|
|
269
|
+
if (typeof WebSocketPair !== "function" || !this.ctx.acceptWebSocket) return json({ error: "sockets-unavailable" }, 501);
|
|
270
|
+
const pair = new WebSocketPair();
|
|
271
|
+
const [client, server] = Object.values(pair);
|
|
272
|
+
this.ctx.acceptWebSocket(server);
|
|
273
|
+
server.serializeAttachment({ draft, since: Date.now() });
|
|
274
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
275
|
+
}
|
|
276
|
+
emit(msg) {
|
|
277
|
+
if (!this.ctx.getWebSockets) return;
|
|
278
|
+
const raw = JSON.stringify(msg);
|
|
279
|
+
for (const ws of this.ctx.getWebSockets()) {
|
|
280
|
+
try { ws.send(raw); } catch (e) { try { ws.close(1011, "send failed"); } catch (e2) { /* already gone */ } }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
webSocketMessage(ws, raw) {
|
|
284
|
+
if (raw === "ping") { try { ws.send("pong"); } catch (e) { /* closing */ } }
|
|
285
|
+
}
|
|
286
|
+
webSocketClose() { /* the runtime owns the socket list; nothing to clean up */ }
|
|
287
|
+
webSocketError() { /* same */ }
|
|
288
|
+
|
|
289
|
+
// ── router ────────────────────────────────────────────────────────────────
|
|
290
|
+
async fetch(request) {
|
|
291
|
+
const url = new URL(request.url);
|
|
292
|
+
const route = url.pathname;
|
|
293
|
+
let body = {};
|
|
294
|
+
if (request.method === "POST") {
|
|
295
|
+
try { body = await request.json(); } catch (e) { return json({ error: "bad-json" }, 400); }
|
|
296
|
+
}
|
|
297
|
+
// Every verb but sync-main assumes the schema is there; sync-main is what the worker
|
|
298
|
+
// calls first on every request, carrying the names the schema is stamped with.
|
|
299
|
+
if (route === "/sync-main") {
|
|
300
|
+
await this.init(body.workspace, body.unit);
|
|
301
|
+
return json(this.syncMain({ table: unitTable(body.table || {}, body.unit || ""), at: body.at || new Date().toISOString() }));
|
|
302
|
+
}
|
|
303
|
+
if (route === "/socket") return this.socket(request, url);
|
|
304
|
+
await this.init(null, null);
|
|
305
|
+
if (request.method === "GET") {
|
|
306
|
+
if (route === "/presence") return json({ drafts: presenceOf(this.openDrafts(), Date.parse(url.searchParams.get("at") || "") || Date.now()) });
|
|
307
|
+
if (route === "/history") return json(this.history());
|
|
308
|
+
if (route === "/main") return json({ revision: this.mainRevision(), table: this.mainTable() });
|
|
309
|
+
const m = DRAFT_ROUTE_RE.exec(route);
|
|
310
|
+
if (m) {
|
|
311
|
+
const d = this.draft(m[1]);
|
|
312
|
+
if (!d || d.discarded) return json({ error: "unknown-draft" }, 404);
|
|
313
|
+
return json({ draftId: d.id, table: d.table, owner: d.owner, session: d.session, openedAt: d.openedAt, lastSaveAt: d.lastSaveAt, baseRevision: d.baseRevision, revision: d.revision, closedAt: d.closedAt });
|
|
314
|
+
}
|
|
315
|
+
return json({ error: "unknown-route" }, 404);
|
|
316
|
+
}
|
|
317
|
+
const at = body.at || new Date().toISOString();
|
|
318
|
+
const verbs = {
|
|
319
|
+
"/open": () => [200, this.open({ ...body, at })],
|
|
320
|
+
"/save": () => this.save({ ...body, at }),
|
|
321
|
+
"/land": () => this.land({ ...body, at }),
|
|
322
|
+
"/restore": () => this.restore({ ...body, at }),
|
|
323
|
+
"/landed": () => this.landed({ ...body, at }),
|
|
324
|
+
"/abandon-land": () => this.abandonLand(body),
|
|
325
|
+
"/sync": () => this.sync(body),
|
|
326
|
+
"/discard": () => this.discard({ ...body, at }),
|
|
327
|
+
};
|
|
328
|
+
if (!verbs[route]) return json({ error: "unknown-route" }, 404);
|
|
329
|
+
const [status, out] = verbs[route]();
|
|
330
|
+
if (status === 200) {
|
|
331
|
+
if (route === "/open") this.emit({ t: "open", draftId: out.draftId, at });
|
|
332
|
+
else if (route === "/save") this.emit({ t: "save", draftId: body.draftId, revision: out.draftRevision, at });
|
|
333
|
+
else if (route === "/landed") this.emit({ t: "land", revision: out.revision, draftId: body.draftId || null, at });
|
|
334
|
+
else if (route === "/discard") this.emit({ t: "discard", draftId: body.draftId, at });
|
|
335
|
+
}
|
|
336
|
+
return json(out, status);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Deploy templates
|
|
2
|
+
|
|
3
|
+
Copy-paste workflows for a **deploy shell** — the private repo that pins this engine,
|
|
4
|
+
holds the user list and the secrets, and ships the site. The engine deploys nothing
|
|
5
|
+
itself.
|
|
6
|
+
|
|
7
|
+
**The recipe that uses these files is [INSTALL.md](../INSTALL.md)** — read that first;
|
|
8
|
+
this page is only the file index.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
templates/shell/ → <shell-repo>/.github/workflows/
|
|
12
|
+
├── deploy.yml # REQUIRED. Build engine chrome → Pages → publish chrome to the store.
|
|
13
|
+
├── engine-bump.yml # REQUIRED in practice. Take engine updates on your own schedule.
|
|
14
|
+
├── health.yml # Canary: pushed-but-never-published drift, stale dirty publishes.
|
|
15
|
+
├── store-backup.yml # Weekly + monthly off-Cloudflare copies of the bundle store.
|
|
16
|
+
├── kv-backup.yml # Nightly copy of KV — the half store-backup does not cover.
|
|
17
|
+
├── space-preflight.yml # Probe that CI's PAT can read a space repo before you add it.
|
|
18
|
+
└── roster-update.yml # Commit Admin-panel invites/removals back to identity.json.
|
|
19
|
+
|
|
20
|
+
templates/space/ → <space-repo>/.github/workflows/ (OPTIONAL — see below)
|
|
21
|
+
└── publish.yml # Auto-publish on push + keep baked chrome current with the engine.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Drift between a shell and these files is caught by `shell-lint` — run it from a shell
|
|
25
|
+
(`node engine/scripts/shell-lint.mjs`), where the engine submodule IS that shell's
|
|
26
|
+
pinned engine. `health.yml` runs it on every canary pass. Filled-in placeholders and
|
|
27
|
+
reworded comments stay quiet; changed behaviour fails.
|
|
28
|
+
|
|
29
|
+
Each file carries its own header explaining what it does and what it needs. Three of them
|
|
30
|
+
have an instance value to fill in before first use — the Pages project name and site
|
|
31
|
+
origin in `deploy.yml`, the site origin in `health.yml` and `store-backup.yml`.
|
|
32
|
+
|
|
33
|
+
**The two backups do not overlap, and a shell needs both.** `store-backup.yml` copies
|
|
34
|
+
published *content* out of the bundle store (R2). `kv-backup.yml` copies the mutable
|
|
35
|
+
*state* the worker keeps alongside it — comment threads, statuses, pins, renames,
|
|
36
|
+
canvases, and the identity records below. Neither store has point-in-time restore, and
|
|
37
|
+
neither backup covers the other, so running only one is being half-backed-up while
|
|
38
|
+
reading as backed-up.
|
|
39
|
+
|
|
40
|
+
⚠️ `kv-backup.yml` commits the **whole** namespace to a branch on the shell repo, and
|
|
41
|
+
that includes `users:secrets` (password hashes, and the tombstones that hold reset
|
|
42
|
+
passwords out of service) and `publish:tokens` (live bearer tokens that can overwrite
|
|
43
|
+
published content). **Do not enable it on a public shell.** Everything else here is
|
|
44
|
+
safe on one; this is not.
|
|
45
|
+
|
|
46
|
+
**Keeping baked chrome current is the SHELL's job, not the space's.** Page-level chrome
|
|
47
|
+
(rail, overlays, layout) is baked into each page at publish time (`/_build.json`
|
|
48
|
+
`builtWithEngine`), so an engine bump refreshes the *serving* engine but leaves
|
|
49
|
+
already-published pages on older chrome until the space republishes. `deploy.yml` closes
|
|
50
|
+
that with a `rebake` job: when the engine pin moves it clones each roster space and
|
|
51
|
+
re-publishes it with the shell's own `*`-scoped `AUGUR_TOKEN` (the one it already uses for
|
|
52
|
+
`--engine`). The star token therefore lives ONLY in the private shell — never in a
|
|
53
|
+
(possibly public) space repo — and **no space needs a token or CI just to stay on the
|
|
54
|
+
current chrome.** `health.yml` check (f) alarms if a space is ever left on chrome older
|
|
55
|
+
than the deployed engine.
|
|
56
|
+
|
|
57
|
+
**`templates/space/publish.yml` is optional and only for a PUBLIC auto-publish space** —
|
|
58
|
+
a demo whose last pusher is often not the person who'd remember to publish. It joins
|
|
59
|
+
push→publish so content goes live on push, running the same `augur publish` client with a
|
|
60
|
+
token scoped to that space (not `*`, since the repo may be public). A working space needs
|
|
61
|
+
none of this: it publishes deliberately from a terminal, and its chrome is re-baked by the
|
|
62
|
+
shell above. A bare space repo is just a `space.json` and some `prototypes/` folders — no
|
|
63
|
+
CI, no secret, no submodule mount.
|
|
64
|
+
|
|
65
|
+
Engine missing something your instance needs? Don't patch your copy — **open a PR
|
|
66
|
+
upstream** ([CONTRIBUTING.md](../CONTRIBUTING.md)) and take it back via pin bump. That
|
|
67
|
+
keeps your instance on the update train and ships your fix to everyone.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
name: Deploy to Cloudflare Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch: # allow manual runs from the Actions tab
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: pages-deploy
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
deploy:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
# A deploy shell composes ONLY the Augur ENGINE (submodule at engine/). No
|
|
20
|
+
# space repo is mounted here and none is checked out: space content reaches
|
|
21
|
+
# production exclusively through `augur publish` from a space clone, into the
|
|
22
|
+
# bundle store the worker serves from.
|
|
23
|
+
#
|
|
24
|
+
# If you are tempted to mount spaces here and build the whole site: that was
|
|
25
|
+
# the previous design, and it gave every page two sources of content with
|
|
26
|
+
# nothing to tell them apart. The store shadowed the built assets at serve
|
|
27
|
+
# time, so a submodule pin could sit weeks behind its space repo while the
|
|
28
|
+
# live URL served something else — and bumping the pin changed nothing,
|
|
29
|
+
# because pins were never the serving path. Keep the roster of space repos in
|
|
30
|
+
# deploy.config.json (`spaces`) for repo-side automation instead.
|
|
31
|
+
#
|
|
32
|
+
# The engine is public; SUBMODULE_PAT is only needed because actions/checkout
|
|
33
|
+
# uses one token for the host repo and its submodules, and this shell repo is
|
|
34
|
+
# private. (.gitmodules URLs must stay HTTPS — checkout cannot authenticate
|
|
35
|
+
# SSH URLs.)
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
with:
|
|
38
|
+
fetch-depth: 0 # the realtime step below diffs across the engine pin move
|
|
39
|
+
submodules: true # the engine only
|
|
40
|
+
token: ${{ secrets.SUBMODULE_PAT }}
|
|
41
|
+
|
|
42
|
+
- uses: actions/setup-node@v4
|
|
43
|
+
with:
|
|
44
|
+
node-version: 20
|
|
45
|
+
|
|
46
|
+
# GV_ENGINE_ONLY=1 builds the shared chrome and the worker with NO space on
|
|
47
|
+
# disk. That is the point: a build that never sees a space cannot emit one, so
|
|
48
|
+
# this job is structurally incapable of overwriting a direct publish however
|
|
49
|
+
# stale its checkout. build.js asserts the result really is chrome-only and
|
|
50
|
+
# fails the deploy if anything space-derived slipped in.
|
|
51
|
+
- name: Build engine chrome
|
|
52
|
+
run: node engine/build.js
|
|
53
|
+
env:
|
|
54
|
+
GV_ENGINE_ONLY: "1"
|
|
55
|
+
GV_IDENTITY_PATH: ${{ github.workspace }}/identity.json
|
|
56
|
+
GV_DEPLOY_CONFIG_PATH: ${{ github.workspace }}/deploy.config.json
|
|
57
|
+
|
|
58
|
+
# ── ONE FRONT DOOR OR THE OTHER, chosen by whether this shell has a wrangler.toml ──
|
|
59
|
+
#
|
|
60
|
+
# An instance is on Pages or on a plain Worker, and this template answers both so a
|
|
61
|
+
# shell can move on its own schedule. The `hashFiles` conditional is the pattern this
|
|
62
|
+
# very file already uses for the realtime worker below.
|
|
63
|
+
#
|
|
64
|
+
# Keeping BOTH here is deliberate. `scripts/shell-lint.mjs` compares a shell's
|
|
65
|
+
# workflows to these templates LINE BY LINE, so a template that knew only about
|
|
66
|
+
# Workers would red-line every shell still on Pages — and with them the health check
|
|
67
|
+
# that reads shell-lint — until the last one migrated. One template, both worlds,
|
|
68
|
+
# and cutover is adding one file to a shell while rollback is deleting it.
|
|
69
|
+
|
|
70
|
+
# PAGES. Carries the WORKER (_worker.js) plus the chrome as an assets-mode fallback.
|
|
71
|
+
# Content is not here.
|
|
72
|
+
- name: Deploy to Cloudflare Pages
|
|
73
|
+
if: ${{ hashFiles('wrangler.toml') == '' }}
|
|
74
|
+
uses: cloudflare/wrangler-action@v3
|
|
75
|
+
with:
|
|
76
|
+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
77
|
+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
78
|
+
command: pages deploy engine/dist --project-name=your-pages-project --branch=main
|
|
79
|
+
|
|
80
|
+
# PLAIN WORKER. The preflight runs FIRST and the deploy is gated on it, because the
|
|
81
|
+
# config it checks is the difference between a gated site and a public one: Workers
|
|
82
|
+
# serves a matching static asset BEFORE invoking the worker unless
|
|
83
|
+
# `run_worker_first = true`, and the asset directory holds __config/instance.json —
|
|
84
|
+
# the roster, with seed passwords. A deploy that skips this check and gets it wrong
|
|
85
|
+
# passes every test, answers the health canary "healthy", and serves the roster.
|
|
86
|
+
- name: Preflight the worker config
|
|
87
|
+
if: ${{ hashFiles('wrangler.toml') != '' }}
|
|
88
|
+
run: node engine/scripts/wrangler-preflight.mjs -c wrangler.toml
|
|
89
|
+
|
|
90
|
+
- name: Deploy the Worker
|
|
91
|
+
if: ${{ hashFiles('wrangler.toml') != '' }}
|
|
92
|
+
uses: cloudflare/wrangler-action@v3
|
|
93
|
+
with:
|
|
94
|
+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
95
|
+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
96
|
+
command: deploy --config wrangler.toml
|
|
97
|
+
|
|
98
|
+
# The store's copy of the chrome, plus the instance config (identity + knobs)
|
|
99
|
+
# the worker reads at runtime. --engine sets GV_ENGINE_ONLY itself, so it
|
|
100
|
+
# cannot touch space content. Skips gracefully while AUGUR_TOKEN isn't
|
|
101
|
+
# configured (assets-mode setups, where Pages is the only source).
|
|
102
|
+
- name: Publish engine chrome to the bundle store
|
|
103
|
+
env:
|
|
104
|
+
AUGUR_TOKEN: ${{ secrets.AUGUR_TOKEN }}
|
|
105
|
+
AUGUR_ORIGIN: https://your-site-origin.example
|
|
106
|
+
GV_IDENTITY_PATH: ${{ github.workspace }}/identity.json
|
|
107
|
+
GV_DEPLOY_CONFIG_PATH: ${{ github.workspace }}/deploy.config.json
|
|
108
|
+
run: |
|
|
109
|
+
if [ -z "$AUGUR_TOKEN" ]; then echo "AUGUR_TOKEN not configured — skipping store publish"; exit 0; fi
|
|
110
|
+
cd engine && node scripts/publish.mjs --engine
|
|
111
|
+
|
|
112
|
+
# The realtime (canvas multiplayer) worker deploys separately from Pages; its
|
|
113
|
+
# instance config lives in THIS repo (realtime.wrangler.toml). Third parties
|
|
114
|
+
# forget manual redeploys, so: when the engine pin moved AND the move touched
|
|
115
|
+
# engine realtime code, redeploy it here. Skips gracefully when the shell has
|
|
116
|
+
# no realtime config, the pin didn't move, or realtime/ is untouched.
|
|
117
|
+
- name: Redeploy realtime if the engine bump changed it
|
|
118
|
+
if: ${{ hashFiles('realtime.wrangler.toml') != '' }}
|
|
119
|
+
env:
|
|
120
|
+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
121
|
+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
122
|
+
run: |
|
|
123
|
+
set -euo pipefail
|
|
124
|
+
OLD="$(git rev-parse '${{ github.event.before }}:engine' 2>/dev/null \
|
|
125
|
+
|| git rev-parse 'HEAD^:engine' 2>/dev/null || true)"
|
|
126
|
+
NEW="$(git rev-parse 'HEAD:engine')"
|
|
127
|
+
if [ -z "$OLD" ] || [ "$OLD" = "$NEW" ]; then echo "engine pin unmoved — skipping realtime."; exit 0; fi
|
|
128
|
+
# BOTH paths, and src/board-room.mjs is the one that is easy to forget: the
|
|
129
|
+
# BoardRoom class MOVED out of realtime/ into the engine's own module graph, so
|
|
130
|
+
# this worker's behaviour now lives mostly in a directory this diff used to
|
|
131
|
+
# ignore. Watching only realtime/ would ship a room fix to the engine worker and
|
|
132
|
+
# silently leave the standalone realtime workers on the old code.
|
|
133
|
+
if git -C engine diff --quiet "$OLD" "$NEW" -- realtime/ src/board-room.mjs 2>/dev/null; then
|
|
134
|
+
echo "engine bump didn't touch realtime/ or src/board-room.mjs — skipping."; exit 0
|
|
135
|
+
fi
|
|
136
|
+
echo "realtime code changed in ${OLD:0:12}..${NEW:0:12} — redeploying"
|
|
137
|
+
npx wrangler deploy -c realtime.wrangler.toml
|
|
138
|
+
|
|
139
|
+
# Drift-driven re-bake after every deploy; the same workflow also answers the
|
|
140
|
+
# worker's space-rebake dispatch the moment a stale-baked publish arrives.
|
|
141
|
+
# Full rationale + the script live in space-rebake.yml.
|
|
142
|
+
rebake:
|
|
143
|
+
needs: deploy
|
|
144
|
+
uses: ./.github/workflows/space-rebake.yml
|
|
145
|
+
secrets: inherit
|