Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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":"<GIT_SHA>"}` を返す。`version` で稼働中のコミットを確認できる(未設定時は `dev`) |
| `GET /readyz` | Readiness。`SELECT 1` でD1への到達性まで確認し、失敗時は `503` を返す |

外部監視サービス(UptimeRobotなど)は `/healthz` に向けてください。`/readyz` を1分間隔で叩いても
約1,440読み取り/日で、D1の500万読み取り/日に対して無視できる量です。

**`GET /` を監視先にしないこと。** ここはQRコードのリダイレクト用で、`?id=` が無いと404を返すため、
監視に使うとアラートが鳴り続けます。

APIの設定・認証情報は以下の2種類に分かれています。

Expand Down
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
86 changes: 86 additions & 0 deletions packages/api/scripts/deploy.mjs
Original file line number Diff line number Diff line change
@@ -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);
Loading