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
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

# Cancel an in-progress run when a newer commit is pushed to the same ref.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

# pnpm version comes from the root package.json "packageManager" field (single source).
# Must run before setup-node so its `cache: pnpm` can find pnpm on PATH.
- uses: pnpm/action-setup@v6

- uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm -r typecheck

# The worker test suite needs no secrets: it sets its own dummy DISCORD_BOT_TOKEN,
# so it passes here without a .dev.vars (which is gitignored and never in CI).
- run: pnpm -r test

- run: pnpm -r build
10 changes: 9 additions & 1 deletion docs/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,19 @@ cd packages/worker
wrangler secret put DISCORD_BOT_TOKEN # 貼上第 3 步的 token
```

再建一個 `packages/worker/.dev.vars`(已被 gitignore)給「註冊 slash 指令」腳本與本地開發用:
再建一個 `packages/worker/.dev.vars`(已被 gitignore)給「註冊 slash 指令」腳本與本地開發用。直接從範本複製再填值:
```bash
cd packages/worker
cp .dev.vars.example .dev.vars # 然後填入真實值
```
內容(鍵見 `.dev.vars.example`):
```
CLOUDFLARE_API_TOKEN=你的-cloudflare-api-token
DISCORD_BOT_TOKEN=你的-bot-token
DISCORD_APPLICATION_ID=你的-application-id
DISCORD_GUILD_ID=你的-guild-id
```
> 註:跑測試(`pnpm test`)**不需要**這個檔——測試會自帶假 token,乾淨 clone/CI 沒有 `.dev.vars` 也能全綠。

---

Expand Down
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
"packageManager": "pnpm@10.33.0",
"scripts": {
"test": "pnpm -r test",
"typecheck": "pnpm -r typecheck"
},
"pnpm": {
"onlyBuiltDependencies": ["esbuild", "workerd", "sharp"]
"typecheck": "pnpm -r typecheck",
"build": "pnpm -r build"
}
}
15 changes: 15 additions & 0 deletions packages/worker/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Local dev secrets template. Copy to `.dev.vars` and fill in real values:
# cp .dev.vars.example .dev.vars
#
# `.dev.vars` is gitignored — never commit real secrets (this repo is public).
# NOTE: the test suite does NOT need this file — tests set their own dummy token,
# so `vitest run` passes on a clean checkout / in CI without any `.dev.vars`.
#
# These are only needed to actually run things locally:
# - `wrangler dev` -> CLOUDFLARE_API_TOKEN, DISCORD_BOT_TOKEN
# - `node scripts/register-commands.mjs` -> DISCORD_BOT_TOKEN, DISCORD_APPLICATION_ID, DISCORD_GUILD_ID

CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
DISCORD_BOT_TOKEN=your-discord-bot-token
DISCORD_APPLICATION_ID=your-discord-application-id
DISCORD_GUILD_ID=your-discord-guild-id
8 changes: 6 additions & 2 deletions packages/worker/test/adapters/discord-initiate.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { env } from "cloudflare:test";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { routeInteraction, type DiscordInteraction } from "../../src/adapters/discord/handler";
import { taipeiPeriod } from "../../src/core/time";
import { nextBillingPeriod } from "../../src/core/time";

const TS = "2026-05-01T00:00:00.000Z";
const WS = 9025;
Expand All @@ -11,7 +11,11 @@ const NONADMIN = "rando-9025";
const PLAN = 9025;
const SUB = 9025;
const CHAN = "chan-9025";
const PERIOD = taipeiPeriod();
// The 發起繳費 modal opens the *next* billing period: the handler computes it with
// nextBillingPeriod(workspace.billing_day), which rolls to next month once today is past the
// billing day. Match that here (workspace billing_day = 5, seeded below) instead of assuming the
// current calendar month — that assumption only held on days 1–5 and broke after the 5th.
const PERIOD = nextBillingPeriod(5);

const tasks: Promise<unknown>[] = [];
const CTX = { waitUntil: (p: Promise<unknown>) => tasks.push(p) } as unknown as ExecutionContext;
Expand Down
3 changes: 3 additions & 0 deletions packages/worker/test/core/billing-initiate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const notifier: Notifier = {
};

beforeAll(async () => {
// Tests must not depend on .dev.vars (CI / a fresh clone has none). The notifier here is a
// fake, so this token only flips the env gate that lets initiateBillingOpened actually "send".
(env as any).DISCORD_BOT_TOKEN = "test-bot-token";
const settings = JSON.stringify({ discord_billing_channel_id: CHAN });
await env.DB.batch([
env.DB.prepare(`INSERT INTO workspaces (id,name,owner_id,channel_type,billing_day,settings,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)`).bind(WS, "W", "o", "discord", 5, settings, TS, TS),
Expand Down
3 changes: 3 additions & 0 deletions packages/worker/test/core/scheduled.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const notifier: Notifier = {
};

beforeAll(async () => {
// Tests must not depend on .dev.vars (CI / a fresh clone has none). The notifier here is a
// fake, so this token only flips the env gate that lets runDailyTasks actually "send".
(env as any).DISCORD_BOT_TOKEN = "test-bot-token";
const settings = JSON.stringify({ discord_billing_channel_id: CHAN, overdue_days: 3, proof_retention_months: 24 });
await env.DB.batch([
env.DB.prepare(`INSERT INTO workspaces (id,name,owner_id,channel_type,billing_day,settings,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)`).bind(WS, "W", "o", "discord", 5, settings, TS, TS),
Expand Down
8 changes: 8 additions & 0 deletions packages/worker/test/routes/admin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,14 @@ describe("admin API", () => {

it("creates/rebuilds the persistent Discord payment message", async () => {
await call("PATCH", "/admin/workspace", { settings: { discord_billing_channel_id: "chan-1" } });
// Supply the bot token locally (CI has no .dev.vars), then restore it — the later
// billing/initiate test doesn't stub fetch, so it must keep its no-real-send behavior.
const prevToken = (env as any).DISCORD_BOT_TOKEN;
(env as any).DISCORD_BOT_TOKEN = "test-bot-token";
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ id: "msg-123" }), { status: 200 })));
const res = await call("POST", "/admin/discord/payment-message");
vi.unstubAllGlobals();
(env as any).DISCORD_BOT_TOKEN = prevToken;
expect(res!.status).toBe(200);
expect(((await res!.json()) as any).message_id).toBe("msg-123");
});
Expand Down Expand Up @@ -164,9 +169,12 @@ describe("admin notifications", () => {
expect(st.billing_opened).toBeNull();
expect(st.overdue).toBeNull();

const prevToken = (env as any).DISCORD_BOT_TOKEN;
(env as any).DISCORD_BOT_TOKEN = "test-bot-token";
vi.stubGlobal("fetch", vi.fn(async () => new Response("{}", { status: 200 })));
const r = await call("POST", "/admin/notifications/resend", { type: "overdue", period: "2028-03" });
vi.unstubAllGlobals();
(env as any).DISCORD_BOT_TOKEN = prevToken;
expect(r!.status).toBe(200);
expect(((await r!.json()) as any).count).toBeGreaterThanOrEqual(1);
st = (await (await call("GET", "/admin/notifications?period=2028-03"))!.json()) as any;
Expand Down
9 changes: 9 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
packages:
- "packages/*"

# pnpm 10 only runs build scripts for packages on this allowlist. Required so a fresh
# `pnpm install` (e.g. in CI) compiles these native deps — workerd powers the worker test
# pool, esbuild/sharp power the vite builds. (Moved here from package.json's "pnpm" field,
# which pnpm 10 no longer reads.)
onlyBuiltDependencies:
- esbuild
- workerd
- sharp