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.
7 files+215−200/7 viewed
| 122 | 122 | ||
| 123 | 123 | Then, once: | |
| 124 | 124 | ||
| 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`). | |
| 128 | 128 | 2. Put your own `account_id`, database ids and hostnames in each | |
| 129 | 129 | `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`. | |
| 132 | 133 | ||
| 133 | − | Deploy everything in dependency order: | |
| 134 | + | Deploy everything, migrations first, in dependency order: | |
| 134 | 135 | ||
| 135 | 136 | ```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 | |
| 144 | 138 | ``` | |
| 145 | 139 | ||
| 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 | + | ||
| 146 | 143 | Create the first account by registering on your site, or with | |
| 147 | 144 | `node services/identity/scripts/create-user.mjs <username>`. | |
| 148 | 145 |
| 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") |
| 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." |
| 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" |
| 281 | 281 | .iter() | |
| 282 | 282 | .find(|row| row.kind == kind && existing.is_none_or(|e| e.id != row.id) && row.overlaps(&environments)) | |
| 283 | 283 | { | |
| 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" }; | |
| 285 | 286 | return Ok(fail( | |
| 286 | 287 | 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."), | |
| 288 | 289 | )); | |
| 289 | 290 | } | |
| 290 | 291 | if existing.is_none() && rows.len() as u32 >= MAX_PER_OWNER { |
| 11 | 11 | { | |
| 12 | 12 | "binding": "DB", | |
| 13 | 13 | "database_name": "g1t-deployments", | |
| 14 | − | "database_id": "TO_BE_CREATED", | |
| 14 | + | "database_id": "aa935a8f-845b-4132-8fa3-98f1afba1db5", | |
| 15 | 15 | "migrations_dir": "migrations" | |
| 16 | 16 | } | |
| 17 | 17 | ], |
| 5 | 5 | "compatibility_date": "2026-09-26", | |
| 6 | 6 | "main": "./src/index.ts", | |
| 7 | 7 | "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. | |
| 10 | 11 | "routes": [ | |
| 11 | 12 | { "pattern": "*.g1t.page/*", "zone_name": "g1t.page" }, | |
| 12 | − | { "pattern": "g1t.page/*", "zone_name": "g1t.page" } | |
| 13 | + | { "pattern": "g1t.page", "custom_domain": true } | |
| 13 | 14 | ], | |
| 14 | 15 | // The apps, uploaded by the deployments service. | |
| 15 | 16 | "dispatch_namespaces": [{ "binding": "APPS", "namespace": "g1t-deployments" }], |