From f47634c86ccfcaced59bcdb27d1c7ce4f897752e Mon Sep 17 00:00:00 2001 From: shigurenimo Date: Wed, 28 Oct 2026 14:41:58 +0900 Subject: [PATCH 1/2] ci: add protected release deployment entry point --- .docs/catalog.json | 5 ++- .docs/core/delivery.md | 2 ++ .docs/production-deployment.md | 8 +++++ .github/workflows/verify.yml | 2 +- package.json | 3 +- scripts/deploy-production.mjs | 59 ++++++++++++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 scripts/deploy-production.mjs diff --git a/.docs/catalog.json b/.docs/catalog.json index 82f44de..dbe9f5f 100644 --- a/.docs/catalog.json +++ b/.docs/catalog.json @@ -490,7 +490,10 @@ "apps/site/public/_headers", "apps/app/pages/index.tsx", "apps/app/pages/index.server.ts", - "apps/app/src/api/api.ts" + "apps/app/src/api/api.ts", + "scripts/deploy-production.mjs", + "package.json", + ".github/workflows/verify.yml" ], "verification": [ "apps/site/src/utils/setup-access.test.mjs", diff --git a/.docs/core/delivery.md b/.docs/core/delivery.md index 12646d7..fb9c784 100644 --- a/.docs/core/delivery.md +++ b/.docs/core/delivery.md @@ -5,3 +5,5 @@ Commit verified responsibilities with Conventional Commits. Before an authorized `vp run release:plan -- ` produces an ignored, local candidate containing the exact clean SHA, previous reachable tag, commit-derived draft changelog, verification evidence and rollback prerequisites. It requires a successful full verification receipt for the same SHA and fresh secret scan. It neither creates tags/releases nor deploys. A published version cannot be inferred from a local candidate. Review the draft for user-visible accuracy and migration impact. Actual tags, releases, production deployment, credentials, permission changes and database restores require explicit authorization for those actions. A release must be tied to an immutable SHA and passing checks. Rollback requires the previously deployed artifact and a verified database compatibility/restore plan; a Git revert alone cannot undo persisted writes. Never blindly auto-publish or auto-merge policy changes. + +Production uses one protected `release` branch. Promote reviewed changes through a pull request and use `vp run deploy:production` only on its clean, CI-verified commit after explicit deployment authorization. The command re-runs full verification and secret scanning before using local Cloudflare authentication. It does not add CI deployment credentials or publish automatically. See [[production-deployment]]. diff --git a/.docs/production-deployment.md b/.docs/production-deployment.md index 9568ffb..366ba86 100644 --- a/.docs/production-deployment.md +++ b/.docs/production-deployment.md @@ -26,3 +26,11 @@ PUBLIC_APP_URL=https://app.stack.open.inta.site PORTLESS_URL=https://stack.open. 2026-10-27、ユーザーの依頼で紹介サイトと会話アプリを検索インデックスの対象外とする。公開HTMLにはnoindexのメタ情報を付け、アプリのページとAPI、サイトのWorker応答と静的アセットにも対応する応答ヘッダーを付ける。クロール自体は妨げず、検索側が除外指定を読めるようにする。これはアクセス制御の代替ではなく、既存の認証・初期設定保護は維持する。ソースのmainへのプッシュと、本番の反映・検索側の処理完了は分けて報告する。検証はローカルHTTP・HTMLとビルド生成物で行う。 このnoindex追加は整形・lint・構造・文書・単体・型・全体ビルドを通過し、アプリのブラウザ試験3件でHTMLのメタ情報とページ・APIの応答ヘッダーを確認した。CMS試験1件ではサイトのメタ情報と既存の編集・公開・リンク動作を確認し、サイトのビルド生成物にも静的配信ヘッダーとWorkerの設定が残ることを確認した。公開済みドメインでの応答と検索エンジン側の削除完了は検証していない。 + +## Release branch and publication + +2026-10-28、ユーザーは公開用に `release` ブランチを1つ使う方針を選んだ。通常の開発は `main`、公開する変更はプルリクエストで `release` に取り込む。`release` は管理者にも適用する保護を設け、最新の `verify` 必須チェックとプルリクエストを要求し、強制プッシュと削除を許可しない。単独運用のため別の人の承認人数は要求しない。レビューで変更と検証結果を確認してから取り込む。 + +GitHubの検証は `main` と `release` へのpush、およびプルリクエストで実行する。公開は明示的な依頼を受け、クリーンな `release` のチェックアウトで `vp run deploy:production` を実行する。このコマンドは公開先ブランチの保護・リモートSHA・同一SHAのCI成功を確認し、全E2Eを含む検証と秘密情報検査を実行してから、既存のCloudflare認証でアプリとサイトを公開する。GitHubへCloudflareの秘密情報を追加せず、自動公開は行わない。必要な依存関係、ブラウザと検査道具は通常のセットアップ手順で準備する。[[core-verification]]、[[core-delivery]]。 + +アプリとサイトは順に公開されるため、片方だけ成功する可能性がある。失敗時は実際の公開バージョンとデータ互換性を調べ、成功した部分も含めて報告する。コマンドはDBを復元しない。移行がある場合は公開前に内容・互換性・回復手順を確認する。以前の直接公開コマンドは過去の手順として残し、通常の公開入口はこの検証付きコマンドとする。設定の存在と実際のGitHub保護・本番応答は別々に確認する。 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 5fc3aa8..6da7865 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -1,7 +1,7 @@ name: Verify on: push: - branches: [main] + branches: [main, release] pull_request: permissions: contents: read diff --git a/package.json b/package.json index 3e20e83..341a889 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "verify": "node scripts/verify.mjs", "verify:full": "node scripts/verify.mjs --full", "release:plan": "node scripts/release-plan.mjs", - "setup:scanner": "node scripts/setup-scanner.mjs" + "setup:scanner": "node scripts/setup-scanner.mjs", + "deploy:production": "node scripts/deploy-production.mjs" }, "devDependencies": { "@babel/parser": "7.29.11", diff --git a/scripts/deploy-production.mjs b/scripts/deploy-production.mjs new file mode 100644 index 0000000..dd403b6 --- /dev/null +++ b/scripts/deploy-production.mjs @@ -0,0 +1,59 @@ +import { execFileSync, spawnSync } from "node:child_process" +import { readFileSync } from "node:fs" +import { fileURLToPath } from "node:url" + +const repository = "interactive-inc/open-stack-void" +const read = (command, args) => execFileSync(command, args, { encoding: "utf8" }).trim() +const api = (path) => JSON.parse(read("gh", ["api", `repos/${repository}/${path}`])) +const sha = read("git", ["rev-parse", "HEAD"]) +function assertTarget() { + if (read("git", ["branch", "--show-current"]) !== "release") + throw new Error("Deploy only from the release branch") + if (read("git", ["status", "--porcelain"])) throw new Error("Release checkout must be clean") + if (api("git/ref/heads/release").object.sha !== sha) + throw new Error("Checkout must match the current remote release commit") + const protection = api("branches/release/protection") + if ( + !protection.enforce_admins?.enabled || + !protection.required_status_checks?.strict || + !protection.required_status_checks.contexts.includes("verify") || + !protection.required_pull_request_reviews || + protection.allow_force_pushes?.enabled || + protection.allow_deletions?.enabled + ) + throw new Error("Release branch protections are incomplete") + const checks = api(`commits/${sha}/check-runs`).check_runs + const verification = checks.find( + (check) => check.name === "verify" && check.app?.slug === "github-actions", + ) + if (verification?.status !== "completed" || verification.conclusion !== "success") + throw new Error("The exact release commit needs a successful GitHub verify check") +} +const vp = fileURLToPath(new URL("../node_modules/vite-plus/bin/vp", import.meta.url)) +function run(args, extraEnv = {}) { + const result = spawnSync(process.execPath, [vp, ...args], { + stdio: "inherit", + env: { ...process.env, ...extraEnv }, + }) + if (result.error) throw result.error + if (result.status !== 0) throw new Error(`Production release stopped: vp ${args.join(" ")}`) +} +assertTarget() +run(["run", "verify:full"]) +run(["run", "check:secrets"]) +const verification = JSON.parse(readFileSync(".local/verification.json", "utf8")) +const secrets = JSON.parse(readFileSync(".local/secret-receipt.json", "utf8")) +if (verification.sha !== sha || !verification.clean || !verification.full || secrets.sha !== sha) + throw new Error("Verification receipts must match the exact clean release commit") +// Recheck GitHub and the checkout after the lengthy verification, before any production write. +assertTarget() +console.log(`Deploying verified release ${sha}`) +run(["-C", "apps/app", "exec", "void", "deploy", "--platform", "cloudflare"], { + TSX_TSCONFIG_PATH: fileURLToPath(new URL("../apps/app/tsconfig.json", import.meta.url)), +}) +assertTarget() +run(["-C", "apps/site", "run", "deploy"], { + PUBLIC_APP_URL: "https://app.stack.open.inta.site", + PORTLESS_URL: "https://stack.open.inta.site", +}) +console.log(`Published app and site from ${sha}; verify production HTTP behavior separately`) From 1d113f77b4ce68bd1e7cf99ad3a7fa75b6c7fe19 Mon Sep 17 00:00:00 2001 From: shigurenimo Date: Wed, 28 Oct 2026 14:44:29 +0900 Subject: [PATCH 2/2] fix(ci): select the configured production account explicitly --- .docs/production-deployment.md | 2 +- scripts/deploy-production.mjs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.docs/production-deployment.md b/.docs/production-deployment.md index 366ba86..6ea0d59 100644 --- a/.docs/production-deployment.md +++ b/.docs/production-deployment.md @@ -31,6 +31,6 @@ PUBLIC_APP_URL=https://app.stack.open.inta.site PORTLESS_URL=https://stack.open. 2026-10-28、ユーザーは公開用に `release` ブランチを1つ使う方針を選んだ。通常の開発は `main`、公開する変更はプルリクエストで `release` に取り込む。`release` は管理者にも適用する保護を設け、最新の `verify` 必須チェックとプルリクエストを要求し、強制プッシュと削除を許可しない。単独運用のため別の人の承認人数は要求しない。レビューで変更と検証結果を確認してから取り込む。 -GitHubの検証は `main` と `release` へのpush、およびプルリクエストで実行する。公開は明示的な依頼を受け、クリーンな `release` のチェックアウトで `vp run deploy:production` を実行する。このコマンドは公開先ブランチの保護・リモートSHA・同一SHAのCI成功を確認し、全E2Eを含む検証と秘密情報検査を実行してから、既存のCloudflare認証でアプリとサイトを公開する。GitHubへCloudflareの秘密情報を追加せず、自動公開は行わない。必要な依存関係、ブラウザと検査道具は通常のセットアップ手順で準備する。[[core-verification]]、[[core-delivery]]。 +GitHubの検証は `main` と `release` へのpush、およびプルリクエストで実行する。公開は明示的な依頼を受け、クリーンな `release` のチェックアウトで `vp run deploy:production` を実行する。このコマンドは公開先ブランチの保護・リモートSHA・同一SHAのCI成功を確認し、全E2Eを含む検証と秘密情報検査を実行してから、既存のCloudflare認証でアプリとサイトを公開する。複数アカウントがある端末でも既存の公開先アカウントを明示して選択する。GitHubへCloudflareの秘密情報を追加せず、自動公開は行わない。必要な依存関係、ブラウザと検査道具は通常のセットアップ手順で準備する。[[core-verification]]、[[core-delivery]]。 アプリとサイトは順に公開されるため、片方だけ成功する可能性がある。失敗時は実際の公開バージョンとデータ互換性を調べ、成功した部分も含めて報告する。コマンドはDBを復元しない。移行がある場合は公開前に内容・互換性・回復手順を確認する。以前の直接公開コマンドは過去の手順として残し、通常の公開入口はこの検証付きコマンドとする。設定の存在と実際のGitHub保護・本番応答は別々に確認する。 diff --git a/scripts/deploy-production.mjs b/scripts/deploy-production.mjs index dd403b6..d706d54 100644 --- a/scripts/deploy-production.mjs +++ b/scripts/deploy-production.mjs @@ -33,7 +33,11 @@ const vp = fileURLToPath(new URL("../node_modules/vite-plus/bin/vp", import.meta function run(args, extraEnv = {}) { const result = spawnSync(process.execPath, [vp, ...args], { stdio: "inherit", - env: { ...process.env, ...extraEnv }, + env: { + ...process.env, + CLOUDFLARE_ACCOUNT_ID: "9acefc4e7cfd25ff42cf1213f469f775", + ...extraEnv, + }, }) if (result.error) throw result.error if (result.status !== 0) throw new Error(`Production release stopped: vp ${args.join(" ")}`)