diff --git a/README.md b/README.md index 896afef..76d44d8 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,32 @@ QRコード/リンクのアクセスを記録し、リダイレクトするた | Worker URL | `https://trackinglink.nutfes-nutmeg9488.workers.dev` | | Cloudflare Workerプロジェクト名 | `trackinglink` | | D1データベース名 | `trackinglink-db` | -| デプロイ方法 | Cloudflareダッシュボード連携(Workers Builds)。`main`ブランチにpushすると自動デプロイ | +| デプロイ方法 | ローカルから `pnpm deploy:api` を実行して手動デプロイ | + +### デプロイ + +リポジトリのルートで以下を実行します。 + +```bash +pnpm deploy:api +``` + +これは [`packages/api/scripts/deploy.mjs`](packages/api/scripts/deploy.mjs) を経由して `wrangler deploy --minify` を実行し、 +あわせて**現在のコミットを `GIT_SHA` としてWorkerに埋め込みます**。作業ツリーに未コミットの変更がある場合は +`c7d34cd-dirty` のように `-dirty` が付き、稼働中のコードがそのコミットと同一でないことが分かるようになっています。 + +### ヘルスチェック + +| エンドポイント | 内容 | +| --- | --- | +| `GET /healthz` | Liveness。DBには触れず `{"ok":true,"version":""}` を返す。`version` で稼働中のコミットを確認できる(未設定時は `dev`) | +| `GET /readyz` | Readiness。`SELECT 1` でD1への到達性まで確認し、失敗時は `503` を返す | + +外部監視サービス(UptimeRobotなど)は `/healthz` に向けてください。`/readyz` を1分間隔で叩いても +約1,440読み取り/日で、D1の500万読み取り/日に対して無視できる量です。 + +**`GET /` を監視先にしないこと。** ここはQRコードのリダイレクト用で、`?id=` が無いと404を返すため、 +監視に使うとアラートが鳴り続けます。 APIの設定・認証情報は以下の2種類に分かれています。 diff --git a/packages/api/package.json b/packages/api/package.json index 304aee1..6c32eeb 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -10,7 +10,7 @@ }, "scripts": { "dev": "wrangler dev", - "deploy": "wrangler deploy --minify", + "deploy": "node scripts/deploy.mjs", "typecheck": "tsc --noEmit", "cf-typegen": "wrangler types --env-interface CloudflareBindings", "db:apply:local": "wrangler d1 execute trackinglink-db --local --file=./schema.sql", diff --git a/packages/api/scripts/deploy.mjs b/packages/api/scripts/deploy.mjs new file mode 100644 index 0000000..b6334f1 --- /dev/null +++ b/packages/api/scripts/deploy.mjs @@ -0,0 +1,86 @@ +// Deploys the Worker with the current commit baked in as GIT_SHA. +// +// Exists because `/healthz` reports `version: c.env.GIT_SHA ?? 'dev'`, and until +// this script there was nothing setting GIT_SHA — so production answered +// `{"ok":true,"version":"dev"}` forever and the health check could not tell you +// which commit was actually running. Knowing that is the entire point of putting +// a version in a liveness probe; see docs/load-test-report-2026-07-27.md §5.4. +// +// Why a Node script rather than `wrangler deploy --var GIT_SHA:$(git rev-parse ...)` +// in package.json: pnpm runs scripts through cmd.exe on Windows, where `$(...)` +// is not command substitution but a literal string. The var would have been set +// to the text "$(git rev-parse --short HEAD)" on exactly the machine this project +// is deployed from. +// +// Usage: +// pnpm deploy (from packages/api) +// pnpm deploy:api (from the repository root) +// pnpm deploy -- --dry-run (extra arguments are passed through to wrangler) +import { spawnSync } from 'node:child_process'; +import { join } from 'node:path'; + +/** + * Windows needs `shell: true` to launch wrangler.CMD at all. Copied from + * scripts/backfill-short-codes.mjs so both scripts fail the same way. + */ +const useShell = process.platform === 'win32'; + +// join() rather than a slash-separated literal: with shell:true the command runs +// through cmd.exe, which does not accept forward slashes as path separators and +// answers `'node_modules' is not recognized`. +const wranglerBin = join( + 'node_modules', + '.bin', + useShell ? 'wrangler.CMD' : 'wrangler', +); + +function git(args) { + const result = spawnSync('git', args, { encoding: 'utf8', shell: useShell }); + if (result.status !== 0) return null; + return (result.stdout ?? '').trim(); +} + +/** + * The identifier `/healthz` will report. + * + * `-dirty` is not decoration. This deploys from a laptop rather than from CI, so + * shipping uncommitted work is normal and a bare SHA would be a claim the running + * code equals that commit — which is the one thing you must not get wrong while + * reading a health check during an incident. + * + * Returns null when git cannot answer (no git, no repository, no commits). The + * deploy still goes ahead without the variable and `/healthz` falls back to + * "dev", because refusing to deploy over a missing version label would be worse + * than deploying with the label this project already had. + */ +function resolveVersion() { + const sha = git(['rev-parse', '--short', 'HEAD']); + if (!sha) return null; + const status = git(['status', '--porcelain']); + return status ? `${sha}-dirty` : sha; +} + +const passthrough = process.argv.slice(2); +const version = resolveVersion(); +const argv = ['deploy', '--minify']; + +if (version) { + argv.push('--var', `GIT_SHA:${version}`); + console.log(`Deploying as GIT_SHA=${version}`); +} else { + console.warn( + 'Could not determine the commit; deploying without GIT_SHA. /healthz will report "dev".', + ); +} + +// `--var` merges with the `vars` block in wrangler.jsonc rather than replacing +// it, so FALLBACK_DESTINATIONS and friends survive. Verified with +// `wrangler deploy --dry-run`, which lists every binding it would upload. +argv.push(...passthrough); + +const result = spawnSync(wranglerBin, argv, { + stdio: 'inherit', + shell: useShell, +}); + +process.exit(result.status ?? 1);