diff --git a/.github/actions/verify/action.yml b/.github/actions/verify/action.yml new file mode 100644 index 0000000..d6d160c --- /dev/null +++ b/.github/actions/verify/action.yml @@ -0,0 +1,67 @@ +name: Verify worker +description: >- + lockfile どおりに依存を入れて lint / typecheck / test を回し、Worker が実際に + バンドルできることを dev・production 双方の設定で確認する。検証用ワークフローと + デプロイ用ワークフローで同じ手順を踏むため composite action に切り出してある。 + +inputs: + node-version: + description: Node.js のバージョン (package.json の engines と揃えること) + required: false + default: "22" + +# ローカル action は checkout 済みでないと解決できないため、checkout は +# 呼び出し側のワークフローに置いてある。 +runs: + using: composite + steps: + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: npm + + # npm install と違い package-lock.json を書き換えないので、ローカルで + # 動かしたのと同じ wrangler / biome / TypeScript の版で検証できる。 + # wrangler もこの lockfile から入るため、デプロイに使う版の固定先は + # ワークフロー側ではなく package-lock.json 一箇所で済む。 + - name: Install dependencies + shell: bash + run: npm ci + + - name: Lint + shell: bash + run: npm run lint + + - name: Typecheck + shell: bash + run: npm run typecheck + + - name: Test + shell: bash + run: npm test + + # tsc は型しか見ないので、import の解決ミスや nodejs_compat で賄えない + # Node API はバンドルして初めて落ちる。--dry-run は Cloudflare API を + # 叩かないため認証情報なしで回せる。 + # + # dev と production を両方バンドルするのは、wrangler.jsonc の env.production + # 側だけが壊れている状態を master へ入れる前に捕まえるため。dev への push + # では production 設定に一切触れないまま緑になってしまう。 + - name: Build (dry-run) + shell: bash + env: + WRANGLER_SEND_METRICS: "false" + # wrangler の色付けが Total Upload 行に混ざると要約が読めなくなる + NO_COLOR: "1" + run: | + # dev は wrangler.jsonc の top-level 設定。wrangler 4 は環境が複数ある + # 状態で --env を省くと警告を出すため、空文字でも明示する。 + for target in "" production; do + label="${target:-dev}" + npx wrangler deploy --env="$target" --dry-run \ + --outdir "$RUNNER_TEMP/bundle-$label" 2>&1 | tee "$RUNNER_TEMP/$label.log" + # Workers の上限は gzip 後で 10 MiB。今は 1/10 にも届かないので + # 失敗にはせず、増え方が見えるよう要約に残すだけにする。 + size=$(grep -m1 'Total Upload' "$RUNNER_TEMP/$label.log" || true) + echo "- \`$label\`: ${size:-size unknown}" >> "$GITHUB_STEP_SUMMARY" + done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9e156bf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +# lint / typecheck / test が通ることと、Worker がバンドルできることを検証する +# だけのワークフロー。デプロイはしない。 +# +# デプロイは環境ごとに別ファイルへ分けてある: +# dev -> deploy_dev.yml (trainlcd-worker-dev) +# master -> deploy_production.yml (trainlcd-worker) +# デプロイ先をトリガとファイルで固定することで、他のブランチが誤って +# どこかの環境へ向くことがないようにしている。 +on: + pull_request: + # GitHub Actions は YAML のアンカー / エイリアスを解釈しないため、 + # このリストは push 側とも、デプロイ用の 2 ファイルとも二重に書く必要が + # ある。片方だけ直さないこと。 + paths: + - "src/**" + - "test/**" + - "scripts/**" + - "package.json" + - "package-lock.json" + - "tsconfig.json" + - "biome.json" + - "jest.config.js" + - "wrangler.jsonc" + - ".github/actions/verify/action.yml" + - ".github/workflows/ci.yml" + # deploy 用の 2 ファイルはどちらも pull_request で起動しないため、 + # ここに載せておかないと変更した PR がどの workflow も通らないまま + # マージされ、デプロイ時に初めて動くことになる。 + - ".github/workflows/deploy_dev.yml" + - ".github/workflows/deploy_production.yml" + push: + # dev / master は deploy_dev.yml / deploy_production.yml が同じ composite + # action で検証してからデプロイするため、ここでは走らせない。 + branches-ignore: + - dev + - master + paths: + - "src/**" + - "test/**" + - "scripts/**" + - "package.json" + - "package-lock.json" + - "tsconfig.json" + - "biome.json" + - "jest.config.js" + - "wrangler.jsonc" + - ".github/actions/verify/action.yml" + - ".github/workflows/ci.yml" + # deploy 用の 2 ファイルはどちらも pull_request で起動しないため、 + # ここに載せておかないと変更した PR がどの workflow も通らないまま + # マージされ、デプロイ時に初めて動くことになる。 + - ".github/workflows/deploy_dev.yml" + - ".github/workflows/deploy_production.yml" + workflow_dispatch: + +name: Continuous integration + +# 同じ PR / ブランチに続けて push したとき、古い方は結果が要らない。 +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + name: Lint, typecheck, test and build + runs-on: ubuntu-latest + + # ここでは environment を宣言しない。environment は if: と違って + # ジョブが走れば必ず適用されるため、宣言すると全ブランチ・全 PR が + # その環境へのデプロイとして履歴に載り、環境 Secret (デプロイ用の + # CLOUDFLARE_API_TOKEN を含む) が任意のブランチのビルドから触れる。 + # + # 検証は --dry-run で Cloudflare API を叩かないため、そもそも認証情報が要らない。 + steps: + # checkout は既定で GITHUB_TOKEN を .git/config に残す。後続の npm ci は + # 依存パッケージの install スクリプトを実行するため読み取られうる。 + # ここから先で git 認証は使わない。 + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: ./.github/actions/verify diff --git a/.github/workflows/deploy_dev.yml b/.github/workflows/deploy_dev.yml new file mode 100644 index 0000000..69b531f --- /dev/null +++ b/.github/workflows/deploy_dev.yml @@ -0,0 +1,69 @@ +# dev を dev 環境 (trainlcd-worker-dev) へデプロイする。 +# +# デプロイ先はこのファイルとトリガで固定してある。ブランチを式で判定して +# 環境を選ぶ作りにすると、environment は if: と違ってジョブが走れば必ず +# 適用されるため、意図しないブランチがこの環境の履歴と Secret に触れる。 +on: + push: + branches: + - dev + # GitHub Actions は YAML のアンカー / エイリアスを解釈しないため、 + # このリストは ci.yml / deploy_production.yml とも二重に書く必要がある。 + # 片方だけ直さないこと。 + paths: + - "src/**" + - "test/**" + - "scripts/**" + - "package.json" + - "package-lock.json" + - "tsconfig.json" + - "biome.json" + - "jest.config.js" + - "wrangler.jsonc" + - ".github/actions/verify/action.yml" + - ".github/workflows/deploy_dev.yml" + workflow_dispatch: + +name: Deploy to dev + +# 同時に流れると、先に始まった古い版が後から上書きしうる。 +concurrency: + group: deploy-dev + cancel-in-progress: false + +permissions: + contents: read + +jobs: + deploy: + name: Verify and deploy to dev + runs-on: ubuntu-latest + + # workflow_dispatch にはブランチ絞り込みが無いので、ここで塞ぐ。 + # push は on: branches で dev に限定済み。 + if: github.ref == 'refs/heads/dev' + + environment: dev + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: ./.github/actions/verify + + # 直前の検証で --dry-run 済みのものと同じ入力から同じバンドルが組み上がる。 + # wrangler は node_modules から解決されるので、検証と同じ版が走る。 + # + # dev は wrangler.jsonc の top-level 設定なので環境名は空にする。 + # wrangler 4 は環境が複数あると --env の省略を警告するため、空でも明示する。 + # + # Worker の secrets (SESSION_JWT_SECRET など) はここでは触らない。 + # deploy は既存の secrets を保持するため、投入は scripts/put-secrets.sh で + # 手元から行う運用のままでよい。 + - name: Deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + WRANGLER_SEND_METRICS: "false" + run: npx wrangler deploy --env="" diff --git a/.github/workflows/deploy_production.yml b/.github/workflows/deploy_production.yml new file mode 100644 index 0000000..0b108e8 --- /dev/null +++ b/.github/workflows/deploy_production.yml @@ -0,0 +1,68 @@ +# master を production (trainlcd-worker) へデプロイする。 +# +# デプロイ先はこのファイルとトリガで固定してある。ブランチを式で判定して +# 環境を選ぶ作りにすると、environment は if: と違ってジョブが走れば必ず +# 適用されるため、意図しないブランチがこの環境の履歴と Secret に触れる。 +on: + push: + branches: + - master + # GitHub Actions は YAML のアンカー / エイリアスを解釈しないため、 + # このリストは ci.yml / deploy_dev.yml とも二重に書く必要がある。 + # 片方だけ直さないこと。 + paths: + - "src/**" + - "test/**" + - "scripts/**" + - "package.json" + - "package-lock.json" + - "tsconfig.json" + - "biome.json" + - "jest.config.js" + - "wrangler.jsonc" + - ".github/actions/verify/action.yml" + - ".github/workflows/deploy_production.yml" + workflow_dispatch: + +name: Deploy to production + +# 同時に流れると、先に始まった古い版が後から上書きしうる。 +concurrency: + group: deploy-production + cancel-in-progress: false + +permissions: + contents: read + +jobs: + deploy: + name: Verify and deploy to production + runs-on: ubuntu-latest + + # workflow_dispatch にはブランチ絞り込みが無いので、ここで塞ぐ。 + # push は on: branches で master に限定済み。 + if: github.ref == 'refs/heads/master' + + environment: production + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: ./.github/actions/verify + + # 直前の検証で --dry-run 済みのものと同じ入力から同じバンドルが組み上がる。 + # wrangler は node_modules から解決されるので、検証と同じ版が走る。 + # + # production は wrangler.jsonc の env.production を指す。 + # + # Worker の secrets (SESSION_JWT_SECRET など) はここでは触らない。 + # deploy は既存の secrets を保持するため、投入は scripts/put-secrets.sh で + # 手元から行う運用のままでよい。 + - name: Deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + WRANGLER_SEND_METRICS: "false" + run: npx wrangler deploy --env production diff --git a/README.md b/README.md index 18042ab..e8a00e6 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,45 @@ npm run deploy:prod # wrangler deploy --env production npm run tail # follow logs ``` +### CI / CD (GitHub Actions) + +Deploys run from GitHub Actions. The target is fixed by the workflow file and +its trigger rather than chosen by an expression, so no branch can point at an +environment it was not meant to reach: + +| Workflow | Trigger | Result | +| ------------------------------------ | -------------------------------- | ----------------------------------- | +| `.github/workflows/ci.yml` | PRs, and pushes to other branches | Verify only, no deploy | +| `.github/workflows/deploy_dev.yml` | push to `dev` | Deploy `trainlcd-worker-dev` | +| `.github/workflows/deploy_production.yml` | push to `master` | Deploy `trainlcd-worker` | + +All three run the same `.github/actions/verify` composite action first — `npm +ci`, lint, typecheck, tests, and a `wrangler deploy --dry-run` of **both** the +dev and the production config. The dry run bundles the Worker for real, so +import mistakes and missing `nodejs_compat` APIs fail there rather than at +deploy time, and building the production config on every run catches an +`env.production` that only breaks after the merge to `master`. `npm ci` installs +wrangler from `package-lock.json`, so the version that deploys is the version +the lockfile pins — there is no second place to bump. + +Each deploy workflow needs two secrets on its GitHub environment (`dev` and +`production` respectively): + +- `CLOUDFLARE_API_TOKEN` — the *Edit Cloudflare Workers* template plus + **Queues: Edit**, since `wrangler deploy` also applies the queue consumer + settings from `wrangler.jsonc`. +- `CLOUDFLARE_ACCOUNT_ID` — `wrangler.jsonc` carries no `account_id`. + +Keeping them on the environment rather than on the repository is what stops an +arbitrary branch from reading the production token: `ci.yml` deliberately +declares no `environment`, and it needs no credentials because `--dry-run` never +calls the Cloudflare API. + +Worker secrets (`SESSION_JWT_SECRET`, `OCTOKIT_PAT`, …) are **not** touched by +the workflows. `wrangler deploy` preserves the secrets already on a Worker, so +they stay a manual `scripts/put-secrets.sh` step — see [Setting +secrets](#setting-secrets). + ## Client wire protocol `POST /tts` and `POST /postFeedback` keep the Firebase callable-compatible wire