clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

build: establish the manual release gate

Dami committed fee4ab3
commit
fee4ab36ece5970f5f085fc308344dec8fa98b5a
parent
9dbc413e11af9e9a1249d0a13338517f267fbeb7
author
Dami <dami.etoile@gmail.com>
4 files changed, +284 -71
D.github/buildkitd.toml2-
D.github/workflows/ci.yml56-------
MREADME.md45++++--
Arelease.sh252++++++++++++++++++++++++++++++
D .github/buildkitd.toml +0 -2
D .github/workflows/ci.yml +0 -56
M README.md +32 -13
@@ -8,14 +8,15 @@
88 | --- | --- |99 | `apps/editor/` | Browser editor and local development entry point |1010 | `apps/docs/` | Docusaurus site published at [docs.diagram.zip](https://docs.diagram.zip) |11-| `workers/api/` | Cloudflare Worker for encrypted, accountless persistence |12-| `workers/shell/` | Cloudflare shell and routing layer |13-| `renderers/edge/` | Renderer catalog, shared contracts, and Worker-based renderer units |11+| `services/api/` | Cloudflare service for encrypted, accountless persistence |12+| `services/shell/` | Cloudflare application shell and routing service |13+| `renderers/catalog/` | Catalog service for renderer capabilities and health |14+| `renderers/edge/` | JavaScript and WebAssembly renderer units |1415 | `renderers/client/` | Sandboxed browser renderers |1516 | `renderers/python/` | Python renderer and translation units |16-| `packages/svg/` | Shared SVG normalization and presentation logic |17+| `renderers/shared/` | Contracts shared across renderer runtimes |18+| `shared/svg/` | Shared SVG normalization and presentation logic |1719 | `examples/diagrams/` | Canonical diagram corpus shared by the editor and renderer tests |18-| `vendor/diagramsnet/` | Pinned diagrams.net browser runtime assets |1920 | `skills/` | Diagram-specific agent skills and reference material |2021 2122 Top-level directories describe architectural roles. The production application and renderer plane run on Cloudflare; there is no JVM gateway or shared rendering server.
@@ -26,20 +27,38 @@ The JavaScript workspace uses npm with Node.js 24.
2627 2728 ```sh2829 npm install29-npm run dev:diagramzip30+npm run dev:editor3031 ```3132 3233 Useful checks:3334 3435 ```sh35-npm run test:diagramzip36-npm run build:diagramzip37-npm run build:diagramzip-docs38-npm --prefix renderers/edge test39-npm --prefix workers/api test36+npm run test:editor37+npm run build:editor38+npm run build:docs39+npm run test:edge40+npm run test:api4041 ```4142 42-Individual services have their own README and package scripts. Run deployment commands from the service directory whose `wrangler.jsonc` you intend to use.43+## Release44+45+Production is released manually through one repository entry point:46+47+```sh48+./release.sh49+```50+51+The command requires a clean `main` worktree. Before asking for confirmation,52+it installs every locked dependency set and runs all tests and builds. It also53+verifies generated types and runtime license notices, then dry-runs every54+JavaScript and static-asset Worker. It deploys the renderer plane in55+service-binding order, applies D1 migrations, and deploys the API, editor shell,56+and documentation. Public smoke tests complete the release. Run57+`./release.sh --check` to execute the same local gate without authentication or58+deployment.59+60+Individual package deployment commands remain useful for development, but they61+are not the production release procedure.4362 4463 ## Architecture4564
@@ -51,7 +70,7 @@ Saved diagrams use encrypted client-side content, mutable aliases in D1, and imm
5170 5271 DiagramZip began with an import of the MIT-licensed Kroki v0.32.1 source tree and has since replaced the application, documentation, persistence, and renderer delivery architecture. The Git repository is maintained as an independent project; Kroki and other retained dependencies remain credited in [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) and the nested license files.5372 54-DiagramZip is distributed under the [MIT License](LICENSE). Some bundled renderers and vendored components use other licenses; review `licenses/`, nested `THIRD_PARTY_NOTICES.md` files, and vendor-license directories before redistribution.73+Except where otherwise noted, original DiagramZip code is distributed under the [MIT License](LICENSE). This repository is a multi-license distribution: bundled renderers, WebAssembly artifacts, vendored source, fonts, and visual assets retain their respective licenses and terms. Review [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md), `licenses/`, and the notices beside each renderer before redistribution.5574 5675 ## Contributing and security5776
A release.sh +252 -0
@@ -0,0 +1,252 @@
1+#!/usr/bin/env bash2+3+set -Eeuo pipefail4+5+ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"6+CHECK_ONLY=false7+ALLOW_DIRTY=false8+PHASE="initialization"9+10+usage() {11+ cat <<'EOF'12+Usage: ./release.sh [--check] [--allow-dirty]13+14+ --check Run the local gate without Cloudflare authentication or deployment.15+ --allow-dirty Permit a dirty worktree in --check mode only.16+EOF17+}18+19+die() {20+ printf '\nRelease stopped during %s: %s\n' "$PHASE" "$*" >&221+ exit 122+}23+24+on_error() {25+ local status=$?26+ printf '\nRelease failed during %s (exit %s).\n' "$PHASE" "$status" >&227+ if [[ "$CHECK_ONLY" == false ]]; then28+ printf '%s\n' 'Production may be partially updated. Fix the cause and rerun this exact commit.' >&229+ printf '%s\n' 'Do not roll back D1 migrations automatically; Worker versions can be rolled back independently.' >&230+ fi31+ exit "$status"32+}33+trap on_error ERR34+35+while (($#)); do36+ case "$1" in37+ --check) CHECK_ONLY=true ;;38+ --allow-dirty) ALLOW_DIRTY=true ;;39+ -h|--help) usage; exit 0 ;;40+ *) usage >&2; die "unknown option: $1" ;;41+ esac42+ shift43+done44+45+if [[ "$ALLOW_DIRTY" == true && "$CHECK_ONLY" == false ]]; then46+ die '--allow-dirty is restricted to --check; production releases always require a clean worktree.'47+fi48+49+cd "$ROOT"50+51+section() {52+ PHASE="$1"53+ printf '\n==> %s\n' "$PHASE"54+}55+56+run_in() {57+ local directory=$158+ shift59+ local label="${directory#"$ROOT"/}"60+ [[ "$directory" == "$ROOT" ]] && label='.'61+ printf ' [%s] ' "$label"62+ printf '%q ' "$@"63+ printf '\n'64+ (cd "$directory" && "$@")65+}66+67+require_command() {68+ command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"69+}70+71+workspace_fingerprint() {72+ {73+ git diff --binary HEAD74+ while IFS= read -r -d '' file; do75+ shasum -a 256 "$file"76+ done < <(git ls-files --others --exclude-standard -z)77+ } | shasum -a 256 | awk '{print $1}'78+}79+80+retry() {81+ local attempts=$182+ local delay=$283+ shift 284+ local attempt=185+ until "$@"; do86+ if ((attempt >= attempts)); then return 1; fi87+ printf ' Attempt %s/%s failed; retrying in %ss.\n' "$attempt" "$attempts" "$delay" >&288+ sleep "$delay"89+ attempt=$((attempt + 1))90+ done91+}92+93+section 'repository checks'94+require_command git95+require_command node96+require_command npm97+require_command uv98+require_command shasum99+require_command curl100+101+[[ "$(git rev-parse --show-toplevel)" == "$ROOT" ]] || die 'release.sh must run from its own repository.'102+[[ "$(git branch --show-current)" == main ]] || die 'production releases must come from main.'103+104+if [[ "$ALLOW_DIRTY" == false && -n "$(git status --porcelain=v1)" ]]; then105+ git status --short >&2106+ die 'the worktree must be clean, including untracked files.'107+fi108+109+NODE_MAJOR="$(node -p 'Number(process.versions.node.split(".")[0])')"110+((NODE_MAJOR >= 24)) || die "Node.js 24 or newer is required; found $(node --version)."111+START_FINGERPRINT="$(workspace_fingerprint)"112+RELEASE_SHA="$(git rev-parse HEAD)"113+RELEASE_SHORT_SHA="$(git rev-parse --short=12 HEAD)"114+RELEASE_SUBJECT="$(git log -1 --pretty=%s)"115+RELEASE_MESSAGE="diagram.zip ${RELEASE_SHORT_SHA}: ${RELEASE_SUBJECT}"116+117+NPM_DIRECTORIES=("$ROOT")118+while IFS= read -r lockfile; do119+ directory="$(cd "$(dirname "$lockfile")" && pwd -P)"120+ [[ "$directory" == "$ROOT" ]] || NPM_DIRECTORIES+=("$directory")121+done < <(find apps/docs services renderers/client renderers/edge -name package-lock.json -not -path '*/node_modules/*' -print | sort)122+123+PYTHON_DIRECTORIES=()124+while IFS= read -r config; do125+ PYTHON_DIRECTORIES+=("$(cd "$(dirname "$config")" && pwd -P)")126+done < <(find renderers/python -mindepth 2 -maxdepth 2 -name wrangler.jsonc -print | sort)127+128+CLIENT_DIRECTORIES=()129+while IFS= read -r config; do130+ CLIENT_DIRECTORIES+=("$(cd "$(dirname "$config")" && pwd -P)")131+done < <(find renderers/client -mindepth 2 -maxdepth 2 -name wrangler.jsonc -print | sort)132+133+EDGE_CONFIG_COUNT="$(find renderers/edge/config/units -maxdepth 1 -name '*.jsonc' | wc -l | tr -d ' ')"134+DEPLOYMENT_COUNT=$((EDGE_CONFIG_COUNT + ${#PYTHON_DIRECTORIES[@]} + ${#CLIENT_DIRECTORIES[@]} + 4))135+WRANGLER_CONFIG_COUNT="$(find apps services renderers \136+ -path '*/node_modules' -prune -o \137+ -path 'renderers/edge/config' -prune -o \138+ -name wrangler.jsonc -print | wc -l | tr -d ' ')"139+PRODUCTION_CONFIG_COUNT=$((WRANGLER_CONFIG_COUNT + EDGE_CONFIG_COUNT))140+[[ "$PRODUCTION_CONFIG_COUNT" == "$DEPLOYMENT_COUNT" ]] || \141+ die "release ownership covers ${DEPLOYMENT_COUNT} deployments, but ${PRODUCTION_CONFIG_COUNT} production Wrangler configs exist."142+printf ' Commit: %s (%s)\n' "$RELEASE_SHORT_SHA" "$RELEASE_SUBJECT"143+printf ' Discovered: %s edge + %s Python + %s client + catalog/API/shell/docs = %s deployments\n' \144+ "$EDGE_CONFIG_COUNT" "${#PYTHON_DIRECTORIES[@]}" "${#CLIENT_DIRECTORIES[@]}" "$DEPLOYMENT_COUNT"145+146+section 'locked dependency installation'147+for directory in "${NPM_DIRECTORIES[@]}"; do148+ run_in "$directory" npm ci149+done150+for directory in "${PYTHON_DIRECTORIES[@]}"; do151+ run_in "$directory" uv lock --check152+ run_in "$directory" uv sync --locked153+done154+155+WRANGLER="$ROOT/renderers/edge/node_modules/.bin/wrangler"156+[[ -x "$WRANGLER" ]] || die 'the locked Wrangler installation is unavailable.'157+[[ "$($WRANGLER --version)" == 4.* ]] || die 'Wrangler 4.x is required.'158+159+section 'generated types and static checks'160+run_in "$ROOT/services/api" "$ROOT/services/api/node_modules/.bin/wrangler" types --check161+run_in "$ROOT/services/shell" "$ROOT/services/shell/node_modules/.bin/wrangler" types --check162+run_in "$ROOT/renderers/edge" "$WRANGLER" types ../catalog/worker-configuration.d.ts --config ../catalog/wrangler.jsonc --check163+run_in "$ROOT/services/api" npm run check164+run_in "$ROOT/services/shell" npm run check165+run_in "$ROOT/renderers/edge" npm run check166+167+section 'tests and production builds'168+run_in "$ROOT" npm test169+run_in "$ROOT" npm run build:editor170+run_in "$ROOT" npm run build:docs171+for directory in "${PYTHON_DIRECTORIES[@]}"; do172+ run_in "$directory" uv run pytest -q173+ run_in "$directory" uv run pywrangler sync --force174+done175+for directory in "${CLIENT_DIRECTORIES[@]}"; do176+ if node -e 'const fs=require("node:fs"); const p=JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.exit(p.scripts?.test ? 0 : 1)' "$directory/package.json"; then177+ run_in "$directory" npm test178+ else179+ run_in "$directory" npm run build180+ fi181+done182+183+section 'artifact and deployment dry runs'184+run_in "$ROOT/renderers/edge" npm run check:licenses185+run_in "$ROOT/renderers/catalog" "$WRANGLER" deploy --dry-run --strict --config wrangler.jsonc186+run_in "$ROOT/services/api" "$ROOT/services/api/node_modules/.bin/wrangler" deploy --dry-run --strict --config wrangler.jsonc187+run_in "$ROOT/services/shell" "$ROOT/services/shell/node_modules/.bin/wrangler" deploy --dry-run --strict --config wrangler.jsonc188+for directory in "${CLIENT_DIRECTORIES[@]}"; do189+ run_in "$directory" "$WRANGLER" deploy --dry-run --strict --config wrangler.jsonc190+done191+192+section 'generated-file drift check'193+END_FINGERPRINT="$(workspace_fingerprint)"194+if [[ "$START_FINGERPRINT" != "$END_FINGERPRINT" ]]; then195+ git status --short >&2196+ die 'the release gate changed tracked or untracked repository content; regenerate and review it before release.'197+fi198+199+if [[ "$CHECK_ONLY" == true ]]; then200+ printf '\nRelease gate passed for %s. No remote commands were run.\n' "$RELEASE_SHORT_SHA"201+ exit 0202+fi203+204+section 'Cloudflare authentication and remote preflight'205+run_in "$ROOT/renderers/edge" "$WRANGLER" whoami206+run_in "$ROOT/services/api" "$ROOT/services/api/node_modules/.bin/wrangler" d1 migrations list diagramzip --remote --config wrangler.jsonc207+208+[[ -t 0 ]] || die 'production confirmation requires an interactive terminal.'209+printf '\nDeploy %s to production across %s Cloudflare deployments? [y/N] ' "$RELEASE_SHORT_SHA" "$DEPLOYMENT_COUNT"210+read -r answer211+[[ "$answer" == y || "$answer" == Y ]] || die 'production release cancelled.'212+213+export DIAGRAMZIP_RELEASE_MESSAGE="$RELEASE_MESSAGE"214+215+section 'edge renderer deployment'216+run_in "$ROOT/renderers/edge" npm run deploy:edge217+218+section 'Python renderer deployment'219+run_in "$ROOT/renderers/edge" npm run deploy:python220+221+section 'client renderer deployment'222+for directory in "${CLIENT_DIRECTORIES[@]}"; do223+ run_in "$directory" "$WRANGLER" deploy --strict --message "$RELEASE_MESSAGE" --config wrangler.jsonc224+done225+226+section 'renderer catalog deployment'227+run_in "$ROOT/renderers/catalog" "$WRANGLER" deploy --strict --message "$RELEASE_MESSAGE" --config wrangler.jsonc228+229+section 'D1 migrations and API deployment'230+run_in "$ROOT/services/api" "$ROOT/services/api/node_modules/.bin/wrangler" d1 migrations apply diagramzip --remote --config wrangler.jsonc231+run_in "$ROOT/services/api" "$ROOT/services/api/node_modules/.bin/wrangler" deploy --strict --message "$RELEASE_MESSAGE" --config wrangler.jsonc232+233+section 'editor shell deployment'234+run_in "$ROOT/services/shell" "$ROOT/services/shell/node_modules/.bin/wrangler" deploy --strict --message "$RELEASE_MESSAGE" --config wrangler.jsonc235+236+section 'documentation deployment'237+run_in "$ROOT/apps/docs" "$ROOT/apps/docs/node_modules/.bin/wrangler" pages deploy build \238+ --project-name diagramzip-docs \239+ --branch main \240+ --commit-hash "$RELEASE_SHA" \241+ --commit-message "$RELEASE_SUBJECT" \242+ --commit-dirty=false243+244+section 'production smoke tests'245+retry 3 5 run_in "$ROOT/renderers/edge" npm run smoke:renderers246+retry 3 5 env DIAGRAMZIP_RENDER_URL=https://diagram.zip node "$ROOT/renderers/catalog/scripts/smoke.mjs"247+retry 6 5 curl --fail --silent --show-error --output /dev/null https://diagram.zip/api/v1/health248+retry 6 5 curl --fail --silent --show-error --output /dev/null https://diagram.zip/healthz249+retry 6 5 curl --fail --silent --show-error --output /dev/null https://docs.diagram.zip/250+251+printf '\nRelease %s completed: %s deployments and all public smoke tests passed.\n' \252+ "$RELEASE_SHORT_SHA" "$DEPLOYMENT_COUNT"