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
5 changes: 4 additions & 1 deletion .docs/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions .docs/core/delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ Commit verified responsibilities with Conventional Commits. Before an authorized
`vp run release:plan -- <major.minor.patch>` 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]].
8 changes: 8 additions & 0 deletions .docs/production-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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保護・本番応答は別々に確認する。
2 changes: 1 addition & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Verify
on:
push:
branches: [main]
branches: [main, release]
pull_request:
permissions:
contents: read
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
63 changes: 63 additions & 0 deletions scripts/deploy-production.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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,
CLOUDFLARE_ACCOUNT_ID: "9acefc4e7cfd25ff42cf1213f469f775",
...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`)
Loading