venture backends on Cloudflare: a domain, a Worker, a database and AI per venture

With CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID, each venture gets its own
Worker (time-<slug>) with its own D1 database and Workers AI. The Worker serves
the venture's GitHub Pages folder under <slug>.<TIME_VENTURE_DOMAIN> (or
workers.dev) and adds a small API: share links stored in D1, and a
rate-limited small model. The page reaches it through time.js, which Time
writes next to index.html and the build may not edit; every call degrades to
null, so products still work on Pages alone and in the headless checks.

The backend is deployed when a venture is picked, so the plan, the signal
issue and the page all carry the venture's own domain. A Cloudflare failure
never holds a venture: it launches on Pages and the chores retry. The chores
also backfill older ventures and redeploy every venture when worker.js or its
settings change (a revision hash), a couple per tick; update.yml carries the
new files. The doctor checks the token. Offline tests cover the provisioner,
the loop with and without Cloudflare, the backfill and redeploys, and the
worker itself (node, against a fake D1, AI and Pages).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wyb3QQDESFkosKRXzrXqwr
This commit is contained in:
Claude authored and Hyperbox committed 2026-10-12 10:50:13 -07:00
1 parent e1dc4f2790
commit 134a03ce41
13 files changed
+717 -11

No files matched your search

+177
View File
@@ -0,0 +1,177 @@
"""Cloudflare backing for ventures: each venture gets a domain, a Worker, a D1 database and Workers AI.
python3 .github/time/cloudflare.py check # what the token can do (doctor)
python3 .github/time/cloudflare.py deploy <slug> <pages-folder-url>
Off unless CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID are set. The page stays on GitHub Pages; the Worker
(cloudflare/worker.js, one script `time-<slug>` per venture) serves it under the venture's own domain and adds the API
the page's time.js client calls: share links stored in D1, and a rate-limited small model. With TIME_VENTURE_DOMAIN set
(a zone on the same Cloudflare account) a venture lives at <slug>.<domain>; without it, at the script's workers.dev URL.
Everything here is idempotent: deploying again updates the script in place and reuses the database and the domain.
The deployed revision (REV) is a hash of the worker code and its settings, so the venture loop can redeploy every
venture when Time's update brings a new worker, and leave them alone otherwise.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import sys
import urllib.error
import urllib.request
import uuid
from pathlib import Path
API = "https://api.cloudflare.com/client/v4"
HERE = Path(__file__).resolve().parent
WORKER = HERE / "cloudflare" / "worker.js"
CLIENT = HERE / "cloudflare" / "time.js"
COMPAT = "2026-09-01"
AI_MODEL = "@cf/meta/llama-3.1-8b-instruct"
def env(name: str, default: str = "") -> str:
return (os.environ.get(name) or default).strip()
def enabled() -> bool:
return bool(env("CLOUDFLARE_API_TOKEN") and env("CLOUDFLARE_ACCOUNT_ID"))
def account() -> str:
return f"/accounts/{env('CLOUDFLARE_ACCOUNT_ID')}"
def cf(method: str, path: str, body=None, *, data: bytes | None = None, ctype: str = "application/json"):
"""One Cloudflare API call; returns `result`. Errors name the call and Cloudflare's own messages, never the token."""
raw = data if data is not None else (json.dumps(body).encode() if body is not None else None)
req = urllib.request.Request(API + path, method=method, data=raw,
headers={"Authorization": f"Bearer {env('CLOUDFLARE_API_TOKEN')}", "Content-Type": ctype})
try:
with urllib.request.urlopen(req, timeout=60) as r:
out = json.loads(r.read() or b"{}")
except urllib.error.HTTPError as e:
text = e.read().decode(errors="replace")
try:
msgs = "; ".join(f"{x.get('code')} {x.get('message')}" for x in json.loads(text).get("errors", []))
except ValueError:
msgs = text[:200]
raise RuntimeError(f"cloudflare {method} {path.split('?')[0].replace(env('CLOUDFLARE_ACCOUNT_ID'), '<account>')} "
f"-> {e.code}: {msgs}") from None
return out.get("result")
def script_name(slug: str) -> str:
return re.sub(r"[^a-z0-9-]", "-", f"time-{slug}".lower())[:63].strip("-")
def host(slug: str) -> str | None:
zone = env("TIME_VENTURE_DOMAIN").lower().strip(".")
return f"{slug}.{zone}" if zone else None
def settings(slug: str, origin: str, pages_origin: str) -> dict:
return {"SLUG": slug, "ORIGIN": origin.rstrip("/"), "PAGES_ORIGIN": pages_origin,
"AI_MODEL": env("TIME_VENTURE_AI_MODEL", AI_MODEL), "AI_DAILY": env("TIME_VENTURE_AI_DAILY", "200"),
"SAVE_DAILY": env("TIME_VENTURE_SAVE_DAILY", "2000")}
def rev(vars_: dict) -> str:
"""The revision a deploy would make: worker code + settings + domain. Equal revs mean nothing to do."""
h = hashlib.sha256(WORKER.read_bytes())
h.update(json.dumps({**vars_, "host": host(vars_["SLUG"]), "compat": COMPAT}, sort_keys=True).encode())
return h.hexdigest()[:12]
def database(slug: str) -> str:
"""The venture's own D1 database (created once). Returns its id."""
name = script_name(slug)
for d in cf("GET", f"{account()}/d1/database?name={name}") or []:
if d.get("name") == name:
return d["uuid"]
return cf("POST", f"{account()}/d1/database", {"name": name})["uuid"]
def multipart(parts: list[tuple[str, str, bytes]]) -> tuple[bytes, str]:
b = uuid.uuid4().hex
out = b"".join(f'--{b}\r\nContent-Disposition: form-data; name="{n}"; filename="{n}"\r\nContent-Type: {t}\r\n\r\n'.encode()
+ data + b"\r\n" for n, t, data in parts) + f"--{b}--\r\n".encode()
return out, f"multipart/form-data; boundary={b}"
def upload(name: str, db: str, vars_: dict) -> None:
meta = {"main_module": "worker.js", "compatibility_date": COMPAT,
"bindings": [{"type": "d1", "name": "DB", "id": db}, {"type": "ai", "name": "AI"}]
+ [{"type": "plain_text", "name": k, "text": str(v)} for k, v in vars_.items()]}
data, ctype = multipart([("metadata", "application/json", json.dumps(meta).encode()),
("worker.js", "application/javascript+module", WORKER.read_bytes())])
cf("PUT", f"{account()}/workers/scripts/{name}", data=data, ctype=ctype)
def attach(name: str, hostname: str) -> str:
"""Point <slug>.<domain> at the script as a Workers custom domain (Cloudflare adds the DNS record and the cert)."""
zone = hostname.split(".", 1)[1]
zones = cf("GET", f"/zones?name={zone}") or []
if not zones:
raise RuntimeError(f"TIME_VENTURE_DOMAIN {zone} is not a zone this Cloudflare token can see")
cf("PUT", f"{account()}/workers/domains",
{"hostname": hostname, "service": name, "zone_id": zones[0]["id"], "environment": "production"})
return f"https://{hostname}/"
def workers_dev(name: str) -> str:
cf("POST", f"{account()}/workers/scripts/{name}/subdomain", {"enabled": True, "previews_enabled": False})
sub = (cf("GET", f"{account()}/workers/subdomain") or {}).get("subdomain")
if not sub:
raise RuntimeError("this Cloudflare account has no workers.dev subdomain yet: set TIME_VENTURE_DOMAIN instead")
return f"https://{name}.{sub}.workers.dev/"
def pages_origin_of(url: str) -> str:
m = re.match(r"(https?://[^/]+)", url)
return m.group(1) if m else ""
def deploy(slug: str, origin: str) -> dict:
"""Database, script and address for one venture. Returns {url, worker, db, rev, api}."""
vars_ = settings(slug, origin, pages_origin_of(origin))
vars_["REV"] = r = rev(vars_)
name = script_name(slug)
db = database(slug)
upload(name, db, vars_)
hostname = host(slug)
url = attach(name, hostname) if hostname else workers_dev(name)
return {"url": url, "worker": name, "db": db, "rev": r, "api": url + "api/"}
def client(api: str) -> str:
"""The time.js a venture page loads, pointed at its backend."""
return CLIENT.read_text().replace("__TIME_API__", api)
def check() -> tuple[bool, str]:
"""Can this token do what a venture needs? For the doctor: (ok, detail), with nothing secret in the detail."""
if not enabled():
return False, "not set (optional): CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID give ventures domains, a database and AI"
try:
cf("GET", f"{account()}/workers/scripts")
cf("GET", f"{account()}/d1/database?per_page=1")
zone = env("TIME_VENTURE_DOMAIN")
if zone and not cf("GET", f"/zones?name={zone}"):
return False, f"works, but TIME_VENTURE_DOMAIN {zone} is not a zone this token can see"
return True, f"works: Workers and D1; ventures live at {'<slug>.' + zone if zone else 'workers.dev'}"
except Exception as e: # noqa: BLE001
return False, f"set but failing: {str(e)[:160]}"
if __name__ == "__main__":
if sys.argv[1:2] == ["check"]:
ok, detail = check()
print(detail)
sys.exit(0 if ok else 1)
if sys.argv[1:2] == ["deploy"] and len(sys.argv) == 4:
print(json.dumps(deploy(sys.argv[2], sys.argv[3]), indent=1))
sys.exit(0)
sys.exit(__doc__)
+107
View File
@@ -0,0 +1,107 @@
// Offline test for worker.js: a fake D1 (just the statements the worker uses), fake Workers AI and a fake Pages origin.
// Run: node .github/time/cloudflare/test_worker.mjs (test_time.py runs it when node is around)
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const here = dirname(fileURLToPath(import.meta.url));
const worker = (await import(join(here, "worker.js"))).default;
class FakeD1 {
constructor() { this.items = new Map(); this.quota = new Map(); this.created = 0; }
prepare(sql) {
const db = this;
const stmt = {
args: [],
bind(...a) { return { ...stmt, args: a }; },
async run() { return this.exec(); },
async first() { return (await this.exec()).results[0] ?? null; },
async exec() {
const a = this.args;
if (sql.startsWith("CREATE TABLE")) { db.created++; return { results: [] }; }
if (sql.startsWith("INSERT INTO quota")) {
const n = (db.quota.get(a[0]) || 0) + 1; db.quota.set(a[0], n); return { results: [{ n }] };
}
if (sql.startsWith("INSERT INTO items")) { db.items.set(a[0], a[1]); return { results: [] }; }
if (sql.startsWith("SELECT data FROM items")) { return { results: db.items.has(a[0]) ? [{ data: db.items.get(a[0]) }] : [] }; }
throw new Error("unexpected sql " + sql);
},
};
return stmt;
}
async batch(stmts) { return Promise.all(stmts.map((s) => s.exec())); }
}
const prompts = [];
const env = {
DB: new FakeD1(), SLUG: "mdecks", REV: "abc123", ORIGIN: "https://o.github.io/r/ventures/mdecks/", PAGES_ORIGIN: "https://o.github.io",
AI_MODEL: "@cf/test/model", AI_DAILY: "3", SAVE_DAILY: "100",
AI: { async run(model, input) { prompts.push({ model, input }); return { response: "a crisp title" }; } },
};
const pages = [];
globalThis.fetch = async (url, init) => {
pages.push({ url, init });
if (url.endsWith("/ventures/mdecks/shots")) return new Response(null, { status: 301, headers: { location: "https://o.github.io/r/ventures/mdecks/shots/" } });
return new Response(`<html>${url}</html>`, { status: 200, headers: { "content-type": "text/html" } });
};
const call = (path, init = {}, ip = "1.1.1.1") =>
worker.fetch(new Request("https://mdecks.yolo.lol" + path, { ...init, headers: { "cf-connecting-ip": ip, ...(init.headers || {}) } }), env);
const post = (path, body, ip, headers = {}) =>
call(path, { method: "POST", body: typeof body === "string" ? body : JSON.stringify(body), headers: { "content-type": "application/json", ...headers } }, ip);
// health, without touching the database
let r = await call("/api/health");
assert.deepEqual(await r.json(), { ok: true, slug: "mdecks", rev: "abc123", ai: true });
assert.equal(env.DB.created, 0);
// save and load a creation; the link comes back to this domain
r = await post("/api/items", { slides: ["# Hi"] });
assert.equal(r.status, 201);
const saved = await r.json();
assert.match(saved.id, /^[a-f0-9]{12}$/);
assert.equal(saved.link, `https://mdecks.yolo.lol/?s=${saved.id}`);
assert.equal(env.DB.created, 2);
r = await call(`/api/items/${saved.id}`);
assert.deepEqual(await r.json(), { slides: ["# Hi"] });
assert.equal((await call("/api/items/doesnotexist1")).status, 404);
assert.equal((await post("/api/items", "not json")).status, 400);
assert.equal((await post("/api/items", JSON.stringify({ x: "y".repeat(70000) }))).status, 413);
// AI through Workers AI with the configured model; per-person and daily limits
r = await post("/api/ai", { prompt: "title for my deck", system: "be brief" }, "2.2.2.2");
assert.deepEqual(await r.json(), { text: "a crisp title" });
assert.equal(prompts[0].model, "@cf/test/model");
assert.deepEqual(prompts[0].input.messages.map((m) => m.role), ["system", "user"]);
assert.equal((await post("/api/ai", { prompt: "" }, "2.2.2.2")).status, 400);
assert.equal((await post("/api/ai", { prompt: "two" }, "3.3.3.3")).status, 200);
assert.equal((await post("/api/ai", { prompt: "three" }, "4.4.4.4")).status, 200);
assert.equal((await post("/api/ai", { prompt: "over the day" }, "5.5.5.5")).status, 429);
assert.equal(prompts.length, 3);
// CORS: this domain and the Pages origin, nobody else
r = await call("/api/health", { headers: { origin: "https://o.github.io" } });
assert.equal(r.headers.get("access-control-allow-origin"), "https://o.github.io");
r = await call("/api/health", { headers: { origin: "https://evil.example" } });
assert.equal(r.headers.get("access-control-allow-origin"), null);
assert.equal((await call("/api/nope")).status, 404);
// a broken database answers 500 with JSON, not an exception
const broken = { ...env, DB: { prepare() { throw new Error("D1 down"); }, batch() { return Promise.reject(new Error("D1 down")); } } };
r = await worker.fetch(new Request("https://x.yolo.lol/api/items", { method: "POST", body: "{}" }), broken);
assert.equal(r.status, 500);
assert.match((await r.json()).detail, /D1 down/);
// the page: Pages under this domain, query kept, redirects kept on this host
r = await call("/?s=" + saved.id);
assert.equal(pages.at(-1).url, "https://o.github.io/r/ventures/mdecks/?s=" + saved.id);
assert.equal(r.headers.get("x-time-venture"), "mdecks");
assert.match(await r.text(), /ventures\/mdecks\//);
r = await call("/app.js");
assert.equal(pages.at(-1).url, "https://o.github.io/r/ventures/mdecks/app.js");
r = await call("");
assert.equal(pages.at(-1).url, "https://o.github.io/r/ventures/mdecks/");
r = await call("/shots");
assert.equal(r.status, 301);
assert.equal(r.headers.get("location"), "/shots/");
console.log("WORKER OK");
+35
View File
@@ -0,0 +1,35 @@
/* Time's backend client for a venture page. Written by Time next to index.html; the build never edits it.
Every call resolves to null when the backend can't help (no network, over the daily limit, or the page is being
checked locally), so a product uses these only for extras and works fully without them.
Time.save(obj) -> a share link (string) or null
Time.load() -> the object a share link (?s=...) carried, or null
Time.ai(prompt, {system}) -> text from a small model, or null
Time.ready -> a promise of true when the backend answers */
(function () {
var API = "__TIME_API__";
var local = /^(localhost|127\.|\[::1\]|0\.0\.0\.0)/.test(location.hostname) || location.protocol === "file:";
function call(path, init) {
if (local) return Promise.resolve(null);
return fetch(API + path, init).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; });
}
function post(path, body) {
return call(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
}
window.Time = {
api: API,
ready: call("health").then(function (r) { return !!(r && r.ok); }),
save: function (obj) {
return post("items", obj).then(function (r) { return r && r.link ? r.link : null; });
},
load: function () {
var id = new URLSearchParams(location.search).get("s");
return id && /^[a-z0-9]{6,32}$/.test(id) ? call("items/" + id) : Promise.resolve(null);
},
ai: function (prompt, opts) {
return post("ai", { prompt: String(prompt || ""), system: opts && opts.system })
.then(function (r) { return r && typeof r.text === "string" ? r.text : null; });
}
};
})();
+110
View File
@@ -0,0 +1,110 @@
// A venture's backend on Cloudflare: its own domain in front of its GitHub Pages folder, plus a small API.
// Time deploys one copy per venture (script time-<slug>) with its own D1 database; see .github/time/cloudflare.py.
//
// GET /api/health -> { ok, slug, rev, ai }
// POST /api/items -> { id, link } save a JSON creation (<= 64 KB), for share links
// GET /api/items/:id -> the saved JSON
// POST /api/ai -> { text } { prompt, system? } through Workers AI, rate limited
// anything else -> the venture's page on GitHub Pages (ORIGIN), served under this domain
//
// Bindings: DB (D1), AI (Workers AI). Vars: SLUG, ORIGIN, PAGES_ORIGIN, REV, AI_MODEL, AI_DAILY, SAVE_DAILY.
const SCHEMA = [
"CREATE TABLE IF NOT EXISTS items (id TEXT PRIMARY KEY, data TEXT NOT NULL, created TEXT NOT NULL)",
"CREATE TABLE IF NOT EXISTS quota (k TEXT PRIMARY KEY, n INTEGER NOT NULL)",
];
const MAX_ITEM = 65536;
const MAX_PROMPT = 4000;
let ready = null;
const json = (body, status = 200, headers = {}) =>
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8", ...headers } });
function cors(req, env) {
const origin = req.headers.get("origin");
if (!origin) return {};
const self = new URL(req.url).origin;
const ok = origin === self || (env.PAGES_ORIGIN && origin === env.PAGES_ORIGIN);
return ok ? { "access-control-allow-origin": origin, "access-control-allow-methods": "GET, POST",
"access-control-allow-headers": "content-type", "vary": "origin" } : {};
}
async function setup(env) {
ready ??= env.DB.batch(SCHEMA.map((s) => env.DB.prepare(s))).catch((e) => { ready = null; throw e; });
return ready;
}
async function who(req, env) {
const ip = req.headers.get("cf-connecting-ip") || "?";
const raw = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${env.SLUG}:${ip}`));
return [...new Uint8Array(raw).slice(0, 8)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
// One counter per (day, kind, person) and one per (day, kind): over either limit means no.
async function spend(env, kind, person, perPerson, total) {
const day = new Date().toISOString().slice(0, 10);
const bump = (k) => env.DB.prepare("INSERT INTO quota (k, n) VALUES (?, 1) ON CONFLICT(k) DO UPDATE SET n = n + 1 RETURNING n").bind(k);
const [mine, all] = await env.DB.batch([bump(`${day}:${kind}:${person}`), bump(`${day}:${kind}:*`)]);
return mine.results[0].n <= perPerson && all.results[0].n <= total;
}
async function api(req, env, url, h) {
const path = url.pathname.replace(/\/+$/, "");
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: h });
if (path === "/api/health") return json({ ok: true, slug: env.SLUG, rev: env.REV, ai: !!env.AI }, 200, h);
await setup(env);
if (path === "/api/items" && req.method === "POST") {
const text = await req.text();
if (text.length > MAX_ITEM) return json({ error: "too big (64 KB max)" }, 413, h);
try { JSON.parse(text); } catch { return json({ error: "send JSON" }, 400, h); }
if (!(await spend(env, "save", await who(req, env), 50, Number(env.SAVE_DAILY || 2000))))
return json({ error: "over today's limit" }, 429, h);
const id = crypto.randomUUID().replace(/-/g, "").slice(0, 12);
await env.DB.prepare("INSERT INTO items (id, data, created) VALUES (?, ?, ?)").bind(id, text, new Date().toISOString()).run();
return json({ id, link: `${url.origin}/?s=${id}` }, 201, h);
}
const item = path.match(/^\/api\/items\/([a-z0-9]{6,32})$/);
if (item && req.method === "GET") {
const row = await env.DB.prepare("SELECT data FROM items WHERE id = ?").bind(item[1]).first();
return row ? new Response(row.data, { headers: { "content-type": "application/json; charset=utf-8", ...h } })
: json({ error: "not found" }, 404, h);
}
if (path === "/api/ai" && req.method === "POST") {
if (!env.AI) return json({ error: "no AI on this venture" }, 501, h);
let body;
try { body = await req.json(); } catch { return json({ error: "send JSON" }, 400, h); }
const prompt = String(body.prompt || "").slice(0, MAX_PROMPT);
if (!prompt) return json({ error: "prompt is empty" }, 400, h);
if (!(await spend(env, "ai", await who(req, env), 20, Number(env.AI_DAILY || 200))))
return json({ error: "over today's limit" }, 429, h);
const messages = [...(body.system ? [{ role: "system", content: String(body.system).slice(0, MAX_PROMPT) }] : []),
{ role: "user", content: prompt }];
const out = await env.AI.run(env.AI_MODEL, { messages, max_tokens: 512 });
return json({ text: out.response ?? "" }, 200, h);
}
return json({ error: "no such endpoint" }, 404, h);
}
// The page itself: the venture's folder on GitHub Pages, under this domain. Pages redirects folder URLs without a
// trailing slash to its own host; those are rewritten to stay here.
async function page(req, env, url) {
const origin = env.ORIGIN.replace(/\/+$/, "");
const up = await fetch(origin + url.pathname + url.search, { redirect: "manual", cf: { cacheTtl: 60, cacheEverything: true } });
const headers = new Headers(up.headers);
const loc = headers.get("location");
if (loc && loc.startsWith(origin)) headers.set("location", loc.slice(origin.length) || "/");
headers.set("x-time-venture", env.SLUG);
return new Response(up.body, { status: up.status, headers });
}
export default {
async fetch(req, env) {
const url = new URL(req.url);
if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
const h = cors(req, env);
try { return await api(req, env, url, h); }
catch (e) { return json({ error: "backend error", detail: String(e).slice(0, 200) }, 500, h); }
}
return page(req, env, url);
},
};
+177
View File
@@ -856,3 +856,180 @@ os.environ.pop("TIME_VENTURE_START", None); os.environ.pop("TIME_VENTURE_HEAVY_S
c37 = next(i["number"] for i in W.issues.values() if i["title"].endswith("(2)"))
assert W.beats(c37)[:3] == ["scouted", "pitched", "picked"], W.beats(c37)
print("ALL OK 29")
# 38 Cloudflare backing: a venture gets its own Worker, database and domain; its page gets time.js; nothing is held up
cfb = vt.cfb
class FakeCF:
"""Fake Cloudflare API: D1 databases, scripts (with their metadata), custom domains, zones."""
def __init__(self, fail=None):
self.dbs, self.scripts, self.domains, self.fail, self.calls = {}, {}, {}, fail, []
def __call__(self, method, path, body=None, *, data=None, ctype="application/json"):
self.calls.append((method, path.split("?")[0]))
if self.fail and self.fail in path:
raise RuntimeError(f"cloudflare {method} {path} -> 403: 10000 Authentication error")
if path.startswith("/zones?name="):
return [{"id": "z1", "name": path.split("=")[1]}] if path.endswith("yolo.lol") else []
if "/d1/database" in path:
if method == "POST":
self.dbs[body["name"]] = f"uuid-{len(self.dbs) + 1}"
return {"uuid": self.dbs[body["name"]], "name": body["name"]}
name = dict(urllib.parse.parse_qsl(path.partition("?")[2])).get("name")
return [{"name": n, "uuid": u} for n, u in self.dbs.items() if not name or n == name]
if path.endswith("/workers/domains"):
self.domains[body["hostname"]] = body["service"]; return {}
if "/workers/scripts/" in path and method == "PUT":
assert ctype.startswith("multipart/form-data; boundary=")
meta = json.loads(re.search(rb'name="metadata".*?\r\n\r\n(.*?)\r\n--', data, re.S).group(1))
assert b"export default" in data and meta["main_module"] == "worker.js"
self.scripts[path.rsplit("/", 1)[1]] = meta; return {}
if path.endswith("/subdomain") and method == "POST":
return {}
if path.endswith("/workers/subdomain"):
return {"subdomain": "acct"}
if path.endswith("/workers/scripts"):
return []
raise AssertionError(f"unexpected cloudflare {method} {path}")
def with_cf(fake, extra=None):
cfb.cf = fake
os.environ.update({"CLOUDFLARE_API_TOKEN": "cft", "CLOUDFLARE_ACCOUNT_ID": "acct", **(extra or {})})
def without_cf():
for k in ("CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID", "TIME_VENTURE_DOMAIN"):
os.environ.pop(k, None)
# deploy: one database, one script with its bindings, a custom domain; deploying again reuses them
CF = FakeCF()
with_cf(CF, {"TIME_VENTURE_DOMAIN": "yolo.lol"})
be = cfb.deploy("mdecks", "https://o.github.io/r/ventures/mdecks/")
assert be["url"] == "https://mdecks.yolo.lol/" and be["api"] == "https://mdecks.yolo.lol/api/" and be["worker"] == "time-mdecks", be
meta = CF.scripts["time-mdecks"]
kinds = {b["name"]: b for b in meta["bindings"]}
assert kinds["DB"] == {"type": "d1", "name": "DB", "id": "uuid-1"} and kinds["AI"]["type"] == "ai", kinds
assert kinds["ORIGIN"]["text"] == "https://o.github.io/r/ventures/mdecks" and kinds["PAGES_ORIGIN"]["text"] == "https://o.github.io"
assert kinds["REV"]["text"] == be["rev"] and CF.domains == {"mdecks.yolo.lol": "time-mdecks"}
assert cfb.deploy("mdecks", "https://o.github.io/r/ventures/mdecks/")["rev"] == be["rev"] and len(CF.dbs) == 1
assert "cft" not in str(CF.calls) and "https://mdecks.yolo.lol/api/" in cfb.client(be["api"]) and "__TIME_API__" not in cfb.client(be["api"])
# no domain: the script's workers.dev address; an unknown zone is a clear error
os.environ.pop("TIME_VENTURE_DOMAIN")
assert cfb.deploy("dl", "https://o.github.io/r/ventures/dl/")["url"] == "https://time-dl.acct.workers.dev/"
os.environ["TIME_VENTURE_DOMAIN"] = "nope.dev"
try:
cfb.deploy("dl", "https://o.github.io/r/ventures/dl/"); raise AssertionError("expected an error")
except RuntimeError as e:
assert "nope.dev is not a zone" in str(e)
assert cfb.check()[0] is False and "nope.dev" in cfb.check()[1]
os.environ["TIME_VENTURE_DOMAIN"] = "yolo.lol"
assert cfb.check() == (True, "works: Workers and D1; ventures live at <slug>.yolo.lol")
without_cf()
assert cfb.check()[0] is False and "optional" in cfb.check()[1]
print("ALL OK 30")
# a new venture with Cloudflare: its own domain everywhere, time.js on Pages, the brief explains the backend
W = VWorld()
vt._build_on_pages = Worker()
CF = FakeCF()
with_cf(CF, {"TIME_VENTURE_DOMAIN": "yolo.lol"})
vtick({"TIME_VENTURE": "on"})
c38 = next(i["number"] for i in W.issues.values() if "venture" in i["labels"])
assert W.beats(c38)[-1] == "launched", W.beats(c38)
w38 = tm.beats_of(W.comments[c38])[-1]["winner"]
assert w38["url"] == "https://mdecks.yolo.lol/" and w38["pages"] == "https://o.github.io/r/ventures/mdecks/", w38
assert w38["backend"]["worker"] == "time-mdecks" and "Backend: https://mdecks.yolo.lol/" in W.text(c38)
assert "https://mdecks.yolo.lol/api/" in W.files["ventures/mdecks/time.js"]
brief = vt._build_on_pages.briefs[0]
assert "This venture has a backend at https://mdecks.yolo.lol/" in brief and "Time.save(obj)" in brief and "never write it" in brief
reg38 = json.loads(W.files["ventures/ventures.json"])["ventures"][0]
assert reg38["url"] == "https://mdecks.yolo.lol/" and reg38["pages"] == w38["pages"] and reg38["backend"]["rev"] == w38["backend"]["rev"]
assert "https://mdecks.yolo.lol/" in W.files["ventures/index.html"] and "mdecks.yolo.lol" in W.files["ventures/mdecks/LAUNCH.md"]
deploys = len(CF.scripts) and sum(1 for m, p in CF.calls if m == "PUT" and "/workers/scripts/" in p)
vtick({"TIME_VENTURE": "on"}) # nothing changed: no redeploy
assert sum(1 for m, p in CF.calls if m == "PUT" and "/workers/scripts/" in p) == deploys
print("ALL OK 31")
# Cloudflare refusing doesn't hold the venture: it launches on Pages, and the chores give it a backend once it works
W = VWorld()
vt._build_on_pages = Worker()
with_cf(FakeCF(fail="/d1/database"), {"TIME_VENTURE_DOMAIN": "yolo.lol"})
vtick({"TIME_VENTURE": "on"})
c39 = next(i["number"] for i in W.issues.values() if "venture" in i["labels"])
w39 = tm.beats_of(W.comments[c39])[-1]["winner"]
assert W.beats(c39)[-1] == "launched" and w39["url"] == "https://o.github.io/r/ventures/mdecks/" and "backend" not in w39
assert "Cloudflare didn't take it" in W.text(c39) and "ventures/mdecks/time.js" not in W.files
assert "Backend" not in vt._build_on_pages.briefs[0]
CF = FakeCF()
with_cf(CF, {"TIME_VENTURE_DOMAIN": "yolo.lol"})
vt.refresh_backends(vt.registry())
reg39 = json.loads(W.files["ventures/ventures.json"])["ventures"][0]
assert reg39["url"] == "https://mdecks.yolo.lol/" and reg39["pages"] == "https://o.github.io/r/ventures/mdecks/", reg39
assert "ventures/mdecks/time.js" in W.files and "https://mdecks.yolo.lol/" in W.files["ventures/index.html"]
print("ALL OK 32")
# the chores: old ventures get backfilled (a couple per tick), a new worker.js redeploys everyone, killed ones are left
W = VWorld()
W.files["ventures/ventures.json"] = json.dumps({"ventures": [
{"slug": s, "name": s, "status": st, "url": f"https://o.github.io/r/ventures/{s}/"}
for s, st in (("a", "live"), ("b", "parked"), ("c", "live"), ("k", "killed"))]})
vt.tm.http = W.http
CF = FakeCF()
with_cf(CF, {"TIME_VENTURE_DOMAIN": "yolo.lol"})
vt.refresh_backends(vt.registry())
got = {v["slug"]: v for v in json.loads(W.files["ventures/ventures.json"])["ventures"]}
assert sorted(CF.scripts) == ["time-a", "time-b"] and got["c"]["url"].startswith("https://o.github.io"), sorted(CF.scripts)
vt.refresh_backends(vt.registry())
got = {v["slug"]: v for v in json.loads(W.files["ventures/ventures.json"])["ventures"]}
assert sorted(CF.scripts) == ["time-a", "time-b", "time-c"] and "backend" not in got["k"] and got["c"]["url"] == "https://c.yolo.lol/"
puts = lambda: sum(1 for m, p in CF.calls if m == "PUT" and "/workers/scripts/" in p)
before = puts()
vt.refresh_backends(vt.registry())
assert puts() == before # up to date: nothing to do
real = cfb.WORKER
newer = Path(_tf.mkdtemp()) / "worker.js"
newer.write_text(real.read_text() + "\n// a newer worker from Time's update\n")
cfb.WORKER = newer
vt.refresh_backends(vt.registry()); vt.refresh_backends(vt.registry())
cfb.WORKER = real
assert puts() == before + 3, puts() - before
without_cf()
print("ALL OK 33")
# the builds never write time.js, and Time's client doesn't count as the product's code
calls = []
class FakeHands:
def __init__(self, n, branch): self.steps = 0
def call(self, name, a): calls.append(a.get("path")); return "wrote"
real_hands, real_work = tm.Hands, tm.venice_work
tm.Hands = FakeHands
tm.venice_work = lambda card, done, spec: {"refusals": [tm.Hands(1, "x").call("write_file", {"path": p, "content": "x"})
for p in ("ventures/s/time.js", "ventures/s/./time.js", "ventures/s/app.js")]}
# vt._build_on_pages is a scripted Worker here: load a fresh venture.py for the real one
vspec2 = importlib.util.spec_from_file_location("vt2", Path(__file__).resolve().parent / "venture.py")
vt2 = importlib.util.module_from_spec(vspec2); vspec2.loader.exec_module(vt2)
vt2.tm.Hands, vt2.tm.venice_work = tm.Hands, tm.venice_work
r = vt2._build_on_pages({"number": 1}, "done", "venice:qwen", root="ventures/s/")
tm.Hands, tm.venice_work = real_hands, real_work
assert r["refusals"][0].startswith("refused: time.js") and r["refusals"][1].startswith("refused: time.js"), r
assert r["refusals"][2] == "wrote" and calls == ["ventures/s/app.js"], (r, calls)
W = VWorld()
vt.tm.http = W.http
wq = {"slug": "q", "signal": 3, "join": "https://github.com/o/r/issues/new?template=early-access.yml&venture=q", "rides": []}
W.files["ventures/q/index.html"] = ('<!doctype html><html><head><meta name="viewport" content="width=device-width"></head><body>'
+ '<script src="time.js"></script>' + "<!-- x -->" * 400 + "</body></html>")
W.files["ventures/q/time.js"] = cfb.client("https://q.yolo.lol/api/") * 3
assert any("no working product" in p for p in vt.check_page(wq, {"pricing": []}))
print("ALL OK 34")
# the worker itself (worker.js), against a fake D1, Workers AI and Pages
import shutil as _sh, subprocess as _sp # noqa: E402
if _sh.which("node"):
res = _sp.run(["node", str(Path(__file__).resolve().parent / "cloudflare" / "test_worker.mjs")], capture_output=True, text=True, timeout=60)
assert res.returncode == 0 and "WORKER OK" in res.stdout, res.stdout[-2000:] + res.stderr[-2000:]
print("ALL OK 35")
else:
print("skipped the worker test: no node")
+6
View File
@@ -10,6 +10,7 @@ workflows; the heartbeat has a daily cap.
from __future__ import annotations
import datetime as dt
import importlib.util
import json
import os
import re
@@ -758,6 +759,11 @@ def cmd_doctor() -> int:
row("(optional) VENICE_API_KEY (seats)", ids is not None, f"works, {len(ids)} models" if ids else "not set")
except Exception as e: # noqa: BLE001
row("(optional) VENICE_API_KEY (seats)", False, f"set but failing: {str(e)[:120]}")
cfs = importlib.util.spec_from_file_location("cfb", Path(__file__).resolve().parent / "cloudflare.py")
cfb = importlib.util.module_from_spec(cfs)
cfs.loader.exec_module(cfb)
good, detail = cfb.check()
row("(optional) CLOUDFLARE_API_TOKEN (venture backends)", good, detail)
try:
models = cursor("GET", "/v1/models")
items = models.get("models") or models.get("data") or models.get("items") or (models if isinstance(models, list) else [])
+85 -9
View File
@@ -11,6 +11,9 @@ Plan: every board seat writes a launch plan (positioning, first users, pricing,
synthesizes them, and LAUNCH.md records which seat each idea came from.
Build: the Venice worker builds the product as the page on the Pages branch: the tool fills the first screen under a
slim header with the one signup button; pricing, limits and the why frame it below. Check: deterministic gates, then headless Chrome at phone and desktop size; two fix rounds, then andon.
Backend (optional, CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID): each venture also gets a Cloudflare Worker with its
own D1 database and Workers AI, serving the Pages folder under <slug>.<TIME_VENTURE_DOMAIN> (cloudflare.py). The page
reaches it through time.js; every call degrades to null, so the product works on Pages alone and in the checks.
Signup: a GitHub issue form (.github/ISSUE_TEMPLATE/early-access.yml) prefilled with the venture and the plan.
Measure: after 7 days, distinct people who signed up or 👍'd the signal issue, against the plan's success and kill
lines: iterate (a v2 card), kill, or watch one more week and then park.
@@ -41,6 +44,9 @@ HERE = Path(__file__).resolve().parent
_spec = importlib.util.spec_from_file_location("tm", HERE / "time.py")
tm = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(tm)
_spec = importlib.util.spec_from_file_location("cfb", HERE / "cloudflare.py")
cfb = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(cfb)
REPO = tm.REPO
PAGES_BRANCH = "gh-pages"
FORM = "early-access.yml"
@@ -382,7 +388,7 @@ For: {for_whom}
The product (free, works entirely in the page): {page}
The next version (the paid idea; do not build it): {next}
Positioning: {positioning}
{prior}
{prior}{backend}
Write these files on this branch under ventures/{slug}/ (relative links only). The launch plan is already there
in ventures/{slug}/LAUNCH.md.
- app.js: the product itself, working, in plain JavaScript (no libraries from other sites).
@@ -446,6 +452,22 @@ The signal issue is {signal}
When the fixes are written, call open_pr to finish (nothing is opened)."""
BACKEND = """
This venture has a backend at {url} (its own domain; the same page is also on GitHub Pages). ventures/{slug}/time.js is
already in your folder: load it with <script src="time.js"></script> before app.js, and never write it. It gives:
- Time.save(obj) -> a share link to this page that brings obj back, or null
- Time.load() -> the object the current share link carried, or null (call it on load and open that creation if present)
- Time.ai(prompt, {{system}}) -> text from a small language model, or null (rate limited; a few hundred calls a day)
Every call can return null (offline, over the limit, and always during the checks, which run the page locally), so the
product must work fully without them: use them for extras such as a "Share link" button that saves the user's creation,
or an AI assist, and hide or disable that extra when the call gives null. The preloaded sample never depends on them.
"""
def backend_brief(w: dict) -> str:
return BACKEND.format(url=w["url"], slug=w["slug"]) if w.get("backend") else ""
def build_body(w: dict, plan: dict) -> str:
rides_txt = "\n".join(f" - {r['name']} ({r['stars']:,} stars): https://github.com/{r['name']}" for r in w.get("rides", []))
tiers = []
@@ -457,7 +479,7 @@ def build_body(w: dict, plan: dict) -> str:
return BUILD_BODY.format(name=w["name"], one_liner=w.get("one_liner", ""), for_whom=w.get("for_whom", ""),
page=w.get("page", ""), next=w.get("next", ""), positioning=plan["positioning"], prior=prior,
slug=w["slug"], url=w["url"], cta=plan["primary_cta"], join=w["join"], rides=rides_txt or " (none)",
tiers="\n".join(tiers), repo=REPO, signal=signal_url(w))
tiers="\n".join(tiers), repo=REPO, signal=signal_url(w), backend=backend_brief(w))
def launch_md(w: dict, plan: dict, plans: dict) -> str:
@@ -648,16 +670,18 @@ def index_html(reg: dict) -> str:
rows = []
vs = sorted(reg["ventures"], key=lambda v: (v.get("planned", ""), v.get("card", 0)), reverse=True)
for k, v in enumerate(vs):
links = [f'<a href="{e(v["join"])}">Join early access</a>' if v.get("join") else "",
home = v["url"] if v.get("backend") else f'{v["slug"]}/' # its own domain once it has a backend
links = [f'<a href="{e(home)}">{e(urllib.parse.urlsplit(home).netloc)}</a>' if v.get("backend") else "",
f'<a href="{e(v["join"])}">Join early access</a>' if v.get("join") else "",
f'<a href="https://github.com/{REPO}/issues/{v["signal"]}">Signal #{v["signal"]}</a>' if v.get("signal") else "",
f'<a href="https://github.com/{REPO}/blob/{PAGES_BRANCH}/ventures/{e(v["slug"])}/LAUNCH.md">Launch plan</a>',
f'<a href="https://github.com/{REPO}/issues/{v["card"]}">Card #{v["card"]}</a>' if v.get("card") else ""]
status = v.get("status", "building")
since = f"since {v['launched']}" if v.get("launched") else f"planned {v.get('planned', '')}"
shot = (f'<a class="shot" href="{e(v["slug"])}/"><img src="{e(v["slug"])}/shots/sheet.png" alt="{e(v.get("name", v["slug"]))} '
shot = (f'<a class="shot" href="{e(home)}"><img src="{e(v["slug"])}/shots/sheet.png" alt="{e(v.get("name", v["slug"]))} '
f'at phone and desktop size" loading="lazy" onerror="this.parentNode.remove()"></a>') if v.get("launched") else ""
rows.append(f"""<article class="row sheet"><div class="no">{len(vs) - k:03d}</div>
<div><h2><a href="{e(v['slug'])}/">{e(v.get('name', v['slug']))}</a></h2><p>{e(v.get('one_liner', ''))}</p>
<div><h2><a href="{e(home)}">{e(v.get('name', v['slug']))}</a></h2><p>{e(v.get('one_liner', ''))}</p>
<p class="for">For: {e(v.get('for_whom', ''))}</p><p class="links">{' · '.join(x for x in links if x)}</p>{shot}</div>
<div><span class="tag {e(status)}">{e(status)}</span><small>{e(since)}</small></div>
<div class="count"><b>{int(v.get('signups', 0))}</b><small>signups</small></div></article>""")
@@ -722,7 +746,8 @@ def check_page(w: dict, plan: dict) -> list[str]:
files = {a: pages_file(f"ventures/{slug}/{a.lstrip('./')}") or "" for a in dict.fromkeys(local)}
assets = "".join(files.values())
low, problems = page.lower(), []
code = "".join(re.findall(r"<script\b[^>]*>(.*?)</script>", page, re.S | re.I)) + "".join(v for a, v in files.items() if a.endswith(".js"))
code = "".join(re.findall(r"<script\b[^>]*>(.*?)</script>", page, re.S | re.I)) + \
"".join(v for a, v in files.items() if a.endswith(".js") and Path(a).name != "time.js") # Time's client isn't the product
if len(re.sub(r"\s+", "", code)) < 800:
problems.append("no working product: the page has almost no JavaScript. The product must run in the page and show real "
"output from a preloaded sample")
@@ -1224,7 +1249,8 @@ def advance(card: dict, comments: list[dict], beats: list[dict], heavy_ok: bool)
w["slug"] = f"{w['slug']}-{n}" # never overwrite another venture's page
site = ensure_pages()
w["card"] = n
w["url"] = f"{site.rstrip('/')}/ventures/{w['slug']}/"
w["url"] = w["pages"] = f"{site.rstrip('/')}/ventures/{w['slug']}/"
backend_note = give_backend(w)
w["design"] = w.get("design") or PRODUCT_FIRST
w["join"] = join_url(w)
plan, plans = plan_round(w, board_seats, chair)
@@ -1234,11 +1260,13 @@ def advance(card: dict, comments: list[dict], beats: list[dict], heavy_ok: bool)
put_pages(f"ventures/{w['slug']}/LAUNCH.md", launch_md(w, plan, plans), f"venture {w['slug']}: launch plan")
version = int(w.get("version", 1))
entry = {"slug": w["slug"], "name": w["name"], "one_liner": w["one_liner"], "for_whom": w["for_whom"], "url": w["url"],
"join": w["join"], "signal": w["signal"], "card": n, "design": w["design"], "version": version}
"pages": w["pages"], "join": w["join"], "signal": w["signal"], "card": n, "design": w["design"], "version": version}
if w.get("backend"):
entry["backend"] = w["backend"]
entry.update({"planned": today(), "status": "building", "signups": 0} if version == 1 else {"status": "iterating"})
upsert(reg, entry)
save_registry(reg, f"ventures: plan {w['slug']}")
say(plan_summary(w, plan, plans, chair), {"beat": "planned", "winner": w, "plan": plan})
say(plan_summary(w, plan, plans, chair) + backend_note, {"beat": "planned", "winner": w, "plan": plan})
elif stage == "planned":
if not heavy_ok:
return "wait"
@@ -1355,6 +1383,50 @@ def measure(n: int, st: dict, say) -> None:
tm.gh("PATCH", f"/repos/{REPO}/issues/{n}", {"state": "closed"})
# ---------- backends on Cloudflare (optional) ----------
def give_backend(w: dict) -> str:
"""Deploy the venture's Worker, database and domain, and write its time.js. On success w["url"] becomes the domain
(w["pages"] keeps the Pages folder). A failure never holds the venture: it launches on Pages and the chores retry."""
if not cfb.enabled():
return ""
try:
be = cfb.deploy(w["slug"], w["pages"])
put_pages(f"ventures/{w['slug']}/time.js", cfb.client(be["api"]), f"venture {w['slug']}: backend client")
except Exception as e: # noqa: BLE001
return f"\n\n_Backend: Cloudflare didn't take it ({str(e)[:200]}); launching on Pages, and the next tick tries again._"
w["url"], w["backend"] = be["url"], {k: be[k] for k in ("worker", "db", "rev", "api")}
return f"\n\n☁️ Backend: {be['url']} (Worker `{be['worker']}`, its own D1 database, Workers AI); the page is also on {w['pages']}"
def refresh_backends(reg: dict, limit: int = 2) -> None:
"""Keep every venture's backend on the current worker: deploy where it's missing or out of date (a new worker.js
from Time's update, or new settings), a couple per tick. Killed ventures keep whatever they had."""
if not cfb.enabled():
return
done, changed = 0, False
for v in reg["ventures"]:
if done >= limit or v.get("status") == "killed":
continue
pages = v.get("pages") or v.get("url")
if not pages or not v.get("slug"):
continue
current = cfb.rev({**cfb.settings(v["slug"], pages, cfb.pages_origin_of(pages))})
if (v.get("backend") or {}).get("rev") == current:
continue
done += 1
try:
be = cfb.deploy(v["slug"], pages)
put_pages(f"ventures/{v['slug']}/time.js", cfb.client(be["api"]), f"venture {v['slug']}: backend client")
except Exception as e: # noqa: BLE001
print(f"backend {v['slug']} failed:", str(e)[:200])
continue
v.update(pages=pages, url=be["url"], backend={k: be[k] for k in ("worker", "db", "rev", "api")})
changed = True
if changed:
save_registry(reg, "ventures: backends on Cloudflare")
# ---------- the tick ----------
def tick() -> int:
@@ -1365,6 +1437,7 @@ def tick() -> int:
reg = registry()
ack_signups(reg)
refresh_daily(reg)
refresh_backends(reg)
except Exception as e: # noqa: BLE001 chores never block the cards
print("chores failed:", e)
fresh = []
@@ -1417,6 +1490,9 @@ def _build_on_pages(card: dict, done: str, spec: str, root: str = "") -> dict:
if name == "open_pr":
self.pr = -1 # truthy sentinel: nothing to open, the page is live on the branch
return "done: the page is on the Pages branch, no PR needed"
if root and name == "write_file" and os.path.normpath(str(a.get("path") or "")) == os.path.normpath(root + "time.js"):
self.steps += 1
return "refused: time.js is Time's backend client and stays as it is; load it, don't write it"
if root and name == "write_file" and not _inside(a.get("path"), root):
self.steps += 1
return f"refused: this build writes only under {root} (not {a.get('path')!r}); write the file there"
+3
View File
@@ -18,6 +18,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TIME_VENTURE_DOMAIN: ${{ vars.TIME_VENTURE_DOMAIN }}
TIME_MODE: ${{ vars.TIME_MODE }}
TIME_DAILY_CAP: ${{ vars.TIME_DAILY_CAP }}
TIME_REVIEW: ${{ vars.TIME_REVIEW }}
+3 -2
View File
@@ -24,12 +24,12 @@ jobs:
- name: copy engine, open PR if changed
run: |
set -e
for f in .github/time/time.py .github/time/venture.py .github/time/test_time.py .github/time/seats.json .github/ISSUE_TEMPLATE/early-access.yml .claude/skills/time/SKILL.md; do
for f in .github/time/time.py .github/time/venture.py .github/time/cloudflare.py .github/time/cloudflare/worker.js .github/time/cloudflare/time.js .github/time/cloudflare/test_worker.mjs .github/time/test_time.py .github/time/seats.json .github/ISSUE_TEMPLATE/early-access.yml .claude/skills/time/SKILL.md; do
mkdir -p "$(dirname "$f")"; cp "/tmp/up/$f" "$f"
done
python3 .github/time/test_time.py > /dev/null
wf=$(for f in /tmp/up/.github/workflows/*.yml; do b=.github/workflows/$(basename "$f"); cmp -s "$f" "$b" || echo "- \`$b\`"; done)
if git diff --quiet; then
if git diff --quiet && [ -z "$(git status --porcelain .github/time)" ]; then
echo "Time engine is up to date." | tee -a "$GITHUB_STEP_SUMMARY"
[ -n "$wf" ] && printf 'Workflow files differ upstream, copy by hand:\n%s\n' "$wf" | tee -a "$GITHUB_STEP_SUMMARY"
exit 0
@@ -37,6 +37,7 @@ jobs:
sha=$(git -C /tmp/up rev-parse --short HEAD)
branch="time-update-$sha"
git switch -c "$branch"
git add .github/time .github/ISSUE_TEMPLATE .claude/skills/time
git -c user.name=time -c user.email=time@users.noreply.github.com commit -qam "Update Time to $UPSTREAM@$sha"
git push -q origin "$branch"
body="Fresh Time engine from \`$UPSTREAM@$CHANNEL\` ($sha). Offline tests pass."
+4
View File
@@ -33,6 +33,10 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VENICE_API_KEY: ${{ secrets.VENICE_API_KEY }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} # optional: a backend per venture (cloudflare.py)
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TIME_VENTURE_DOMAIN: ${{ vars.TIME_VENTURE_DOMAIN }}
TIME_VENTURE_AI_MODEL: ${{ vars.TIME_VENTURE_AI_MODEL }}
TIME_VENTURE: ${{ vars.TIME_VENTURE }}
TIME_VENTURE_START: ${{ inputs.start && '1' || '' }}
TIME_VENTURE_PITCH: ${{ vars.TIME_VENTURE_PITCH }}