Commit

Deploy scripts live in the repository

scripts/deploy.sh deploys every part, or the ones named, in dependency order, applying each one's D1 migrations before its code. scripts/setup-deployments.sh makes what Deployments needs once (database, dispatch namespace, queue, and with a Global API Key the wildcard DNS record and the service's token, via scripts/cloudflare-setup.py). The README's run-your-own steps use them; they listed five of the services.

Also:
the deployments database's id; g1t.page's apex as a custom domain, so its DNS record is made with it; and a clearer message when a key already has a row for an environment.
syntaqxcommitted Parent0295aacBrowse files
7 files+215−200/7 viewed
+11−14
122122
123123 Then, once:
124124
125−1. Create the D1 databases (`g1t`, `g1t-repos`, `g1t-work`, `g1t-events`) and
126− the queues (`g1t-events`, `g1t-events-work`) with `wrangler d1 create` and
127− `wrangler queues create`.
125+1. Create each service's D1 database and the event queues with
126+ `npx wrangler d1 create <name>` and `npx wrangler queues create <name>`
127+ (the names are in each `wrangler.jsonc`).
128128 2. Put your own `account_id`, database ids and hostnames in each
129129 `wrangler.jsonc`.
130−3. Apply the migrations: `npx wrangler d1 migrations apply DB --remote` in
131− each service directory.
130+3. For [Deployments](https://docs.g1t.sh/guides/deployments/), which needs
131+ the Workers for Platforms add-on and a zone for apps:
132+ `scripts/setup-deployments.sh`.
132133
133−Deploy everything in dependency order:
134+Deploy everything, migrations first, in dependency order:
134135
135136 ```sh
136−(cd services/events && npx wrangler deploy)
137−(cd services/identity && npx wrangler deploy)
138−(cd services/repos && npx wrangler deploy)
139−(cd services/work && npx wrangler deploy)
140−(cd apps/api && npx wrangler deploy)
141−npm run deploy
142−(cd services/runner && npx wrangler deploy) # optional: g1t agents
143−(cd apps/docs && npm run deploy)
137+scripts/deploy.sh
144138 ```
145139
140+Or only what changed, still in order: `scripts/deploy.sh billing web`.
141+Both use your `wrangler login`, not a token in `.env`.
142+
146143 Create the first account by registering on your site, or with
147144 `node services/identity/scripts/create-user.mjs <username>`.
148145
+82−0
1+"""Makes what Deployments needs that `wrangler login` cannot: a proxied
2+wildcard DNS record on the apps' zone, and an API token for the deployments
3+service (Workers Scripts: Edit, Account Analytics: Read).
4+
5+Run by scripts/setup-deployments.sh with CLOUDFLARE_EMAIL and
6+CLOUDFLARE_API_KEY (a Global API Key) set. Prints only ids and outcomes,
7+never the key or the new token. Skips what exists.
8+"""
9+import json
10+import os
11+import sys
12+import urllib.error
13+import urllib.request
14+
15+ACCOUNT = os.environ.get("CLOUDFLARE_ACCOUNT_ID") or "1e6f2cffa3f445920836e8ebe446bb58"
16+EMAIL = os.environ.get("CLOUDFLARE_EMAIL", "")
17+KEY = os.environ.get("CLOUDFLARE_API_KEY", "")
18+ZONE = os.environ.get("G1T_APPS_ZONE", "g1t.page")
19+TOKEN_FILE = os.environ["TOKEN_FILE"]
20+API = "https://api.cloudflare.com/client/v4"
21+
22+if not KEY or not EMAIL:
23+ sys.exit("CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY must be set")
24+
25+
26+def call(method, path, body=None):
27+ request = urllib.request.Request(
28+ API + path,
29+ method=method,
30+ data=json.dumps(body).encode() if body is not None else None,
31+ headers={
32+ "X-Auth-Email": EMAIL,
33+ "X-Auth-Key": KEY,
34+ "Content-Type": "application/json",
35+ # Cloudflare refuses Python's default user agent.
36+ "User-Agent": "g1t-setup",
37+ },
38+ )
39+ try:
40+ return json.load(urllib.request.urlopen(request))
41+ except urllib.error.HTTPError as error:
42+ return json.loads(error.read() or b"{}") | {"http": error.code}
43+
44+
45+zones = call("GET", f"/zones?name={ZONE}")
46+if not zones.get("result"):
47+ sys.exit(f"zone {ZONE} not found: {zones.get('errors')}")
48+zone = zones["result"][0]["id"]
49+
50+records = call("GET", f"/zones/{zone}/dns_records?name=*.{ZONE}")
51+if records.get("result"):
52+ print(f"*.{ZONE}: record exists")
53+else:
54+ made = call("POST", f"/zones/{zone}/dns_records", {
55+ "type": "AAAA", "name": "*", "content": "100::", "proxied": True, "ttl": 1,
56+ "comment": "g1t deployments: every app goes to the dispatch Worker",
57+ })
58+ print(f"*.{ZONE}:", "record created" if made.get("success") else made.get("errors"))
59+
60+if os.path.exists(TOKEN_FILE):
61+ print("token: exists in .credentials; not making another")
62+ sys.exit(0)
63+
64+groups = call("GET", "/user/tokens/permission_groups")
65+wanted = {"Workers Scripts Write", "Account Analytics Read"}
66+ids = [g["id"] for g in groups.get("result", []) if g["name"] in wanted]
67+if len(ids) != len(wanted):
68+ sys.exit("could not find the permission groups for the token")
69+created = call("POST", "/user/tokens", {
70+ "name": "g1t deployments service (Workers for Platforms uploads, analytics)",
71+ "policies": [{
72+ "effect": "allow",
73+ "resources": {f"com.cloudflare.api.account.{ACCOUNT}": "*"},
74+ "permission_groups": [{"id": i} for i in ids],
75+ }],
76+})
77+if not created.get("success"):
78+ sys.exit(f"token not created: {created.get('errors')}")
79+os.makedirs(os.path.dirname(TOKEN_FILE), exist_ok=True)
80+with open(TOKEN_FILE, "w", encoding="utf-8") as f:
81+ f.write(created["result"]["value"] + "\n")
82+print("token: created and saved to .credentials")
+59−0
1+#!/usr/bin/env bash
2+# Deploys g1t: every part, or the ones named, in the order they depend on
3+# each other. Each part's D1 migrations are applied before its code goes
4+# out, so new code never meets an old database.
5+#
6+# scripts/deploy.sh # everything
7+# scripts/deploy.sh billing web # just these, still in order
8+#
9+# Uses your `wrangler login`. The repository's .env may hold a token for
10+# other tools; it is ignored here unless you set CLOUDFLARE_DEPLOY_TOKEN.
11+set -euo pipefail
12+
13+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
14+export CLOUDFLARE_API_TOKEN="${CLOUDFLARE_DEPLOY_TOKEN:-}"
15+
16+# Dependency order: what others bind to goes first.
17+ORDER=(
18+ services/events
19+ services/identity
20+ services/repos
21+ services/work
22+ services/billing
23+ services/integrations
24+ services/webhooks
25+ services/actions
26+ services/models
27+ services/deployments
28+ services/runner
29+ apps/api
30+ services/pages
31+ apps/web
32+ apps/docs
33+)
34+
35+wanted() {
36+ [ $# -eq 0 ] && return 0
37+ local dir="$1"; shift
38+ for name in "${PICK[@]}"; do
39+ [ "$dir" = "$name" ] || [ "$(basename "$dir")" = "$name" ] && return 0
40+ done
41+ return 1
42+}
43+
44+PICK=("$@")
45+for dir in "${ORDER[@]}"; do
46+ [ ${#PICK[@]} -eq 0 ] || wanted "$dir" || continue
47+ echo "== $dir"
48+ cd "$ROOT/$dir"
49+ db=$(grep -o '"database_name": *"[^"]*"' wrangler.jsonc 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)"$/\1/' || true)
50+ if [ -n "$db" ] && [ -d migrations ]; then
51+ npx wrangler d1 migrations apply "$db" --remote
52+ fi
53+ if grep -q '"deploy": "[^"]*build' package.json 2>/dev/null; then
54+ npm run deploy
55+ else
56+ npx wrangler deploy
57+ fi
58+done
59+echo "== Deployed."
+55−0
1+#!/usr/bin/env bash
2+# Sets up what Deployments needs on a Cloudflare account, once. Safe to run
3+# again: each step skips what exists. Then deploy with scripts/deploy.sh.
4+#
5+# Needs the Workers for Platforms add-on on the account, and a zone for
6+# apps (g1t uses g1t.page) named in services/pages/wrangler.jsonc.
7+#
8+# Two steps need more than `wrangler login` can do: a wildcard DNS record
9+# on the apps' zone, and an API token for the deployments service. Set
10+# CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY (a Global API Key) and this
11+# script makes both; otherwise it says what to make by hand.
12+set -euo pipefail
13+
14+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
15+export CLOUDFLARE_API_TOKEN="${CLOUDFLARE_DEPLOY_TOKEN:-}"
16+ZONE="${G1T_APPS_ZONE:-g1t.page}"
17+NAMESPACE="g1t-deployments"
18+TOKEN_FILE="$ROOT/.credentials/deployments-cloudflare-token.txt"
19+w() { npx wrangler "$@"; }
20+
21+cd "$ROOT/services/deployments"
22+
23+echo "== D1 database"
24+if grep -q TO_BE_CREATED wrangler.jsonc; then
25+ id=$(w d1 create g1t-deployments </dev/null 2>&1 | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1 || true)
26+ [ -n "$id" ] || { echo "Could not create the database; is it there already? (npx wrangler d1 list)"; exit 1; }
27+ sed -i "s/TO_BE_CREATED/$id/" wrangler.jsonc
28+ echo "Created $id and wrote it into services/deployments/wrangler.jsonc. Commit that."
29+else
30+ echo "Already set."
31+fi
32+
33+echo "== Dispatch namespace and event queue"
34+w dispatch-namespace list 2>/dev/null | grep -q "$NAMESPACE" || w dispatch-namespace create "$NAMESPACE"
35+w queues list 2>/dev/null | grep -q g1t-events-deployments || w queues create g1t-events-deployments
36+
37+echo "== DNS record and the service's token"
38+if [ -n "${CLOUDFLARE_API_KEY:-}" ] && [ -n "${CLOUDFLARE_EMAIL:-}" ]; then
39+ G1T_APPS_ZONE="$ZONE" TOKEN_FILE="$TOKEN_FILE" python "$ROOT/scripts/cloudflare-setup.py"
40+else
41+ cat <<EOF
42+Set CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY to do this for you, or by hand:
43+ 1. On $ZONE, add a proxied DNS record: type AAAA, name *, content 100::
44+ 2. Create an API token with Workers Scripts: Edit and Account Analytics:
45+ Read on the account, and save it as one line in
46+ $TOKEN_FILE
47+EOF
48+fi
49+
50+if [ -f "$TOKEN_FILE" ]; then
51+ echo "== Storing the token as the deployments service's secret"
52+ w deploy
53+ tr -d '\r\n' <"$TOKEN_FILE" | w secret put CLOUDFLARE_API_TOKEN
54+fi
55+echo "== Done. Now: scripts/deploy.sh"
+3−2
281281 .iter()
282282 .find(|row| row.kind == kind && existing.is_none_or(|e| e.id != row.id) && row.overlaps(&environments))
283283 {
284− let at = if environments.is_empty() { "every environment".to_owned() } else { environments.join(", ") };
284+ let at = if clash.environments.is_empty() { "all environments".to_owned() } else { clash.environments.replace(',', ", ") };
285+ let what = if kind == "secret" { "secret" } else { "config" };
285286 return Ok(fail(
286287 FailureCode::Conflict,
287− format!("{name} already has a row for {at} ({}). Edit that row, or choose other environments.", if clash.environments.is_empty() { "every environment" } else { &clash.environments }),
288+ format!("{name} already has a {what} row for {at}. Edit that row, or choose other environments."),
288289 ));
289290 }
290291 if existing.is_none() && rows.len() as u32 >= MAX_PER_OWNER {
+1−1
1111 {
1212 "binding": "DB",
1313 "database_name": "g1t-deployments",
14− "database_id": "TO_BE_CREATED",
14+ "database_id": "aa935a8f-845b-4132-8fa3-98f1afba1db5",
1515 "migrations_dir": "migrations"
1616 }
1717 ],
+4−3
55 "compatibility_date": "2026-09-26",
66 "main": "./src/index.ts",
77 "workers_dev": false,
8− // Every app on g1t.page. Needs a proxied wildcard DNS record (`*`,
9− // AAAA 100::) on the zone; the apex says what g1t.page is.
8+ // Every app on g1t.page. The wildcard needs a proxied DNS record (`*`,
9+ // AAAA 100::) on the zone; the apex, which says what g1t.page is, is a
10+ // custom domain, whose record Cloudflare makes.
1011 "routes": [
1112 { "pattern": "*.g1t.page/*", "zone_name": "g1t.page" },
12− { "pattern": "g1t.page/*", "zone_name": "g1t.page" }
13+ { "pattern": "g1t.page", "custom_domain": true }
1314 ],
1415 // The apps, uploaded by the deployments service.
1516 "dispatch_namespaces": [{ "binding": "APPS", "namespace": "g1t-deployments" }],