feat: automate Cloudflare Workers Builds setup
This commit is contained in:
1 parent
e1b2597a29
commit
257b740d44
13 files changed
+457
-110
No files matched your search
+13
-2
@@ -9,6 +9,16 @@ CLOUDFLARE_ACCOUNT_ID=
|
||||
# Leave empty when authenticated locally with `wrangler login`; set it for CI/CD deployments.
|
||||
CLOUDFLARE_API_TOKEN=
|
||||
|
||||
# Optional user-scoped API token for configuring Cloudflare Workers Builds.
|
||||
# It needs Workers Builds Configuration: Edit and Workers Scripts: Read. It is
|
||||
# used only by `bun run deploy:builds:setup` and is never sent to the Worker.
|
||||
EDGE_EVER_BUILDS_API_TOKEN=
|
||||
|
||||
# Optional Workers Builds build-token UUID. Set this only when the Cloudflare
|
||||
# account has multiple build tokens and setup cannot choose one automatically.
|
||||
# The selected build token must be able to apply D1 migrations and deploy Workers.
|
||||
EDGE_EVER_BUILDS_BUILD_TOKEN_UUID=
|
||||
|
||||
# Worker service name.
|
||||
# Defaults to the `edgeever` Cloudflare Worker.
|
||||
EDGE_EVER_WORKER_NAME=edgeever
|
||||
@@ -22,7 +32,7 @@ EDGE_EVER_D1_DATABASE_NAME=edgeever
|
||||
|
||||
# D1 database ID.
|
||||
# After creating the D1 database, copy the returned database_id here.
|
||||
# Cloudflare Git deployments also require this variable in Build Variables.
|
||||
# Cloudflare Workers Builds also requires this variable in Build Variables.
|
||||
# The scripts generate a temporary .wrangler.generated.toml, so you do not need to
|
||||
# commit your personal D1 ID to the public repository.
|
||||
EDGE_EVER_D1_DATABASE_ID=00000000-0000-0000-0000-000000000000
|
||||
@@ -42,7 +52,8 @@ EDGE_EVER_AUTH_USERNAME=admin
|
||||
# Generate it with: `bun run auth:hash -- <your-password>`
|
||||
# For multi-instance deployments, use `EDGE_EVER_<INSTANCE>_AUTH_PASSWORD_HASH`
|
||||
# to configure a different password for each instance.
|
||||
# For production deployments, store the hash as a Cloudflare Worker Secret.
|
||||
# For Workers Builds, store this as a Cloudflare Build Secret. The deploy command
|
||||
# synchronizes it to the runtime Worker Secret after every successful deployment.
|
||||
EDGE_EVER_AUTH_PASSWORD_HASH=
|
||||
|
||||
# Web session lifetime in days. The server limits it to 1-400 days.
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
name: Deploy EdgeEver
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "apps/site/**"
|
||||
- "apps/mobile/**"
|
||||
- ".github/workflows/deploy-site.yml"
|
||||
- ".github/workflows/mobile-build.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: edgeever-production
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy Cloudflare Workers
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
EDGE_EVER_TIANMA_WORKER_NAME: ${{ secrets.EDGE_EVER_TIANMA_WORKER_NAME }}
|
||||
EDGE_EVER_TIANMA_CUSTOM_DOMAIN: ${{ secrets.EDGE_EVER_TIANMA_CUSTOM_DOMAIN }}
|
||||
EDGE_EVER_TIANMA_D1_DATABASE_NAME: ${{ secrets.EDGE_EVER_TIANMA_D1_DATABASE_NAME }}
|
||||
EDGE_EVER_TIANMA_D1_DATABASE_ID: ${{ secrets.EDGE_EVER_TIANMA_D1_DATABASE_ID }}
|
||||
EDGE_EVER_TIANMA_R2_BUCKET_NAME: ${{ secrets.EDGE_EVER_TIANMA_R2_BUCKET_NAME }}
|
||||
EDGE_EVER_TIANMA_R2_PREVIEW_BUCKET_NAME: ${{ secrets.EDGE_EVER_TIANMA_R2_PREVIEW_BUCKET_NAME }}
|
||||
EDGE_EVER_TIANMA_AUTH_USERNAME: ${{ secrets.EDGE_EVER_TIANMA_AUTH_USERNAME }}
|
||||
EDGE_EVER_TIANMA_AUTH_PASSWORD_HASH: ${{ secrets.EDGE_EVER_TIANMA_AUTH_PASSWORD_HASH }}
|
||||
EDGE_EVER_TIANMA_SESSION_TTL_DAYS: ${{ secrets.EDGE_EVER_TIANMA_SESSION_TTL_DAYS }}
|
||||
EDGE_EVER_DEMO_WORKER_NAME: ${{ secrets.EDGE_EVER_DEMO_WORKER_NAME }}
|
||||
EDGE_EVER_DEMO_D1_DATABASE_NAME: ${{ secrets.EDGE_EVER_DEMO_D1_DATABASE_NAME }}
|
||||
EDGE_EVER_DEMO_D1_DATABASE_ID: ${{ secrets.EDGE_EVER_DEMO_D1_DATABASE_ID }}
|
||||
EDGE_EVER_DEMO_R2_BUCKET_NAME: ${{ secrets.EDGE_EVER_DEMO_R2_BUCKET_NAME }}
|
||||
EDGE_EVER_DEMO_R2_PREVIEW_BUCKET_NAME: ${{ secrets.EDGE_EVER_DEMO_R2_PREVIEW_BUCKET_NAME }}
|
||||
EDGE_EVER_DEMO_AUTH_USERNAME: ${{ secrets.EDGE_EVER_DEMO_AUTH_USERNAME }}
|
||||
EDGE_EVER_DEMO_AUTH_PASSWORD_HASH: ${{ secrets.EDGE_EVER_DEMO_AUTH_PASSWORD_HASH }}
|
||||
EDGE_EVER_DEMO_SESSION_TTL_DAYS: ${{ secrets.EDGE_EVER_DEMO_SESSION_TTL_DAYS }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: bun run typecheck
|
||||
|
||||
- name: Build web app
|
||||
run: bun run build
|
||||
|
||||
- name: Deploy tianma instance
|
||||
run: EDGE_EVER_INSTANCE=tianma bun run deploy:worker
|
||||
|
||||
- name: Sync tianma auth secret
|
||||
run: |
|
||||
printf '%s' "$EDGE_EVER_TIANMA_AUTH_PASSWORD_HASH" \
|
||||
| EDGE_EVER_INSTANCE=tianma bun scripts/run-wrangler.mjs secret put EDGE_EVER_AUTH_PASSWORD_HASH
|
||||
|
||||
- name: Deploy demo instance
|
||||
run: EDGE_EVER_INSTANCE=demo bun run deploy:worker
|
||||
|
||||
- name: Sync demo auth secret
|
||||
run: |
|
||||
printf '%s' "$EDGE_EVER_DEMO_AUTH_PASSWORD_HASH" \
|
||||
| EDGE_EVER_INSTANCE=demo bun scripts/run-wrangler.mjs secret put EDGE_EVER_AUTH_PASSWORD_HASH
|
||||
@@ -23,6 +23,8 @@ bun run deploy:doctor
|
||||
bun run deploy
|
||||
```
|
||||
|
||||
首次部署与验证成功后,应优先执行 `bun run deploy:builds:setup`,按 `docs/cloudflare-workers-builds.md` 将该 Worker 连接到 Fork 的 `main` 分支,并将 `.env.local` 中的实例配置写入 Cloudflare Workers Builds 的 Build Variables/Secrets。只有 Cloudflare GitHub App 的浏览器授权和 Build Token 选择无法由 API 自动完成;应由 Agent 尽可能代办,并在需要用户确认时给出文档中的精确路径。之后 GitHub Sync fork 触发 Cloudflare 自动构建、执行 D1 migration 并发布;不要为此新增 GitHub Actions Worker 部署流程。
|
||||
|
||||
如果用户没有提供首次登录密码,应只询问这一个必要信息,或在用户同意后生成随机密码。Cloudflare 授权、账号、D1/R2 资源、Worker 名称、自定义域名等私有配置必须来自用户环境、Cloudflare MCP/插件、Wrangler 登录态或 `.env.local`,严禁硬编码到仓库文件。
|
||||
|
||||
部署脚本必须通过 `scripts/run-wrangler.mjs` 读取 `.env.local` 并生成临时 Wrangler 配置。不要直接修改 `wrangler.toml` 来写入个人 `database_id`、bucket 名称、Worker 名称或 route。
|
||||
|
||||
@@ -27,17 +27,19 @@ The public demo resets daily and restores sample notes. Do not store private con
|
||||
|
||||
Copy this prompt into your AI coding assistant, such as Claude Code, Codex, Antigravity, Cursor, or Trae:
|
||||
|
||||
**Recommendation:** Before deployment, configure GitHub and Cloudflare MCP servers, plugins, or other supported integrations for your AI Agent. This allows it to fork the repository, create the required Cloudflare resources, and deploy the application.
|
||||
**Recommendation:** Before deployment, configure GitHub and Cloudflare MCP servers, plugins, or other supported integrations for your AI Agent. This allows it to fork the repository, create the required Cloudflare resources, deploy the application, and connect the instance to Cloudflare Workers Builds.
|
||||
|
||||
```text
|
||||
Please follow these steps:
|
||||
1. Fork the EdgeEver upstream repository: https://github.com/tianma-if/edgeever
|
||||
2. Use the forked repository to install and deploy EdgeEver to Cloudflare.
|
||||
3. Configure a mechanism for the fork to sync updates from the upstream repository, so it can receive the latest product features periodically or on demand.
|
||||
3. Connect the deployed Worker to the fork's `main` branch through Cloudflare Workers Builds, so GitHub Sync fork automatically builds, migrates, and deploys future updates.
|
||||
```
|
||||
|
||||
Agents should follow [AI Agent Cloudflare Deployment](docs/agent-deploy-cloudflare.md).
|
||||
|
||||
After the first deployment, see [Cloudflare Workers Builds](docs/cloudflare-workers-builds.md) for automatic updates. The same deployment flow is used by official instances and forks.
|
||||
|
||||
> Common pitfall: Cloudflare R2, D1, and Workers may still require a Visa card during activation or usage, even when you stay within the free quotas.
|
||||
|
||||
### Manual Deployment
|
||||
|
||||
+3
-3
@@ -29,16 +29,16 @@ EdgeEver 想填补这个空白:保留熟悉的笔记体验,同时提供开
|
||||
|
||||
将下方提示词复制给你的 AI 助手(Claude Code、Codex、Antigravity、Cursor、Trae 等),它会自动完成安装
|
||||
|
||||
**建议:** 开始部署前,请先为 AI Agent 配置 GitHub 和 Cloudflare 的 MCP、插件或其他可用集成,以便 Agent 完成仓库 Fork、Cloudflare 资源创建与应用部署。
|
||||
**建议:** 开始部署前,请先为 AI Agent 配置 GitHub 和 Cloudflare 的 MCP、插件或其他可用集成,以便 Agent 完成仓库 Fork、Cloudflare 资源创建、应用部署和 Workers Builds 自动部署连接。
|
||||
|
||||
```text
|
||||
请按以下流程操作:
|
||||
1. Fork EdgeEver 上游仓库:https://github.com/tianma-if/edgeever
|
||||
2. 使用 Fork 后的仓库,将 EdgeEver 安装并部署到 Cloudflare。
|
||||
3. 为 Fork 仓库配置从上游仓库同步更新的机制,使其能够定期或按需获取上游仓库的最新代码与产品特性。
|
||||
3. 通过 Cloudflare Workers Builds 将已部署的 Worker 连接到 Fork 仓库的 `main` 分支,使 GitHub Sync fork 后能自动构建、迁移数据库并发布新版。
|
||||
```
|
||||
|
||||
Agent 应优先按 [AI Agent Cloudflare Deployment](docs/agent-deploy-cloudflare.md) 执行
|
||||
Agent 应优先按 [AI Agent Cloudflare Deployment](docs/agent-deploy-cloudflare.md) 执行。首次部署后,请参阅 [Cloudflare Workers Builds 自动部署](docs/cloudflare-workers-builds.zh-CN.md);官方实例和 Fork 使用同一套发布流程。
|
||||
|
||||
> 常见踩坑:Cloudflare 的 R2、D1 和 Worker 即使使用免费额度,在开通或使用过程中也可能要求绑定一张 Visa 卡。国内用户可以考虑办理招商银行多币种卡,拿到 Visa 卡后绑定到 Cloudflare 账号即可。
|
||||
|
||||
|
||||
@@ -25,7 +25,11 @@ const readGitCommit = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const buildId = process.env.CF_PAGES_COMMIT_SHA?.slice(0, 12) ?? process.env.GITHUB_SHA?.slice(0, 12) ?? readGitCommit() ?? "local";
|
||||
const buildId = process.env.WORKERS_CI_COMMIT_SHA?.slice(0, 12)
|
||||
?? process.env.CF_PAGES_COMMIT_SHA?.slice(0, 12)
|
||||
?? process.env.GITHUB_SHA?.slice(0, 12)
|
||||
?? readGitCommit()
|
||||
?? "local";
|
||||
const appVersion = readPackageVersion();
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
@@ -20,7 +20,7 @@ Chinese version:
|
||||
|
||||
- Read `AGENTS.md`, `README.md`, `.env.local.example`, and this file first.
|
||||
- Do not create a new Git branch. Work on `main`.
|
||||
- Prefer `bun run deploy:setup`, `bun run deploy:doctor`, and `bun run deploy`.
|
||||
- Prefer `bun run deploy:setup`, `bun run deploy:doctor`, and `bun run deploy` for initial installation. Then connect the Worker to Cloudflare Workers Builds for all routine updates.
|
||||
- Do not hard-code a personal Worker name, D1 database ID, R2 bucket name, account ID, API token, or domain in source files.
|
||||
- Use `.env.local` for local/private deployment values. It is git-ignored.
|
||||
- If the Cloudflare MCP or Cloudflare plugin is available, it may be used to inspect or create resources. If not, use Wrangler through the scripts in this repo.
|
||||
@@ -99,6 +99,18 @@ Chinese version:
|
||||
|
||||
Open the site, log in with `EDGE_EVER_AUTH_USERNAME` and the first login password, then create an MCP token from the in-app MCP settings.
|
||||
|
||||
8. Connect Cloudflare Workers Builds.
|
||||
|
||||
Prefer the automated setup command:
|
||||
|
||||
```sh
|
||||
bun run deploy:builds:setup
|
||||
```
|
||||
|
||||
It reads the fork remote and `.env.local`, then creates or updates the Cloudflare repository connection, production trigger, build commands, cache, watch paths, and build variables. The agent should complete the Cloudflare GitHub App authorization in the browser when available. If Cloudflare requires a build-token choice, use the exact Dashboard path and retry command shown in [Cloudflare Workers Builds](cloudflare-workers-builds.md).
|
||||
|
||||
Once connected, a GitHub **Sync fork** push automatically migrates and deploys the user's own instance. Do not configure the repository's GitHub Actions Worker deployment for this purpose.
|
||||
|
||||
## Optional Customization
|
||||
|
||||
Set these values in `.env.local` before `bun run deploy:setup` or `bun run deploy`:
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Cloudflare Workers Builds
|
||||
|
||||
EdgeEver uses Cloudflare Workers Builds for routine deployments. Connect each EdgeEver Worker to the `main` branch of its repository once; every later push, including a GitHub **Sync fork**, then builds, migrates, and deploys that instance automatically.
|
||||
|
||||
The same deployment commands are used for the official instances and every fork. Local `bun run deploy` remains the first-installation and emergency-recovery path.
|
||||
|
||||
## Connect an Instance
|
||||
|
||||
First create the instance resources and perform its first deployment as described in the [manual deployment guide](manual-deploy.md). Then use the setup command:
|
||||
|
||||
```sh
|
||||
bun run deploy:builds:setup
|
||||
```
|
||||
|
||||
It reads the fork's `origin` remote plus `.env.local`, then idempotently creates or updates the repository connection, production trigger, build commands, build cache, watch paths, and Build Variables/Secrets. On its first successful run it also starts a build of `main`, verifying the complete automated path. Rerun it whenever an instance setting changes; later runs update configuration without starting another build.
|
||||
|
||||
### One-time Cloudflare authorization
|
||||
|
||||
Cloudflare requires two account-level authorizations that cannot be safely automated by a repository script:
|
||||
|
||||
1. Install and authorize the **Cloudflare Workers & Pages** GitHub App for the fork. An Agent can open the Cloudflare dashboard and complete this browser consent; a user only needs to approve the GitHub authorization.
|
||||
2. Ensure the Worker has a Workers Builds **build token** with permission to deploy Workers and apply D1 migrations. If the setup command reports that it cannot choose a token, open **Worker** -> **Settings** -> **Builds** -> **API token**, create/select the token, then set its UUID as `EDGE_EVER_BUILDS_BUILD_TOKEN_UUID` in `.env.local` and rerun the command.
|
||||
|
||||
The setup API call itself needs a user-scoped token with **Workers Builds Configuration: Edit** and **Workers Scripts: Read**. Put it in `EDGE_EVER_BUILDS_API_TOKEN` (or reuse `CLOUDFLARE_API_TOKEN` if it has those permissions). This token is never uploaded to the Worker or Cloudflare Builds.
|
||||
|
||||
### Manual fallback
|
||||
|
||||
If the command cannot be used, configure the Worker in the Cloudflare dashboard:
|
||||
|
||||
1. Go to **Workers & Pages** and open the EdgeEver Worker.
|
||||
2. Open **Settings** -> **Builds** -> **Connect**.
|
||||
3. Authorize GitHub, select the instance repository, and choose the `main` production branch.
|
||||
4. Configure the following commands:
|
||||
|
||||
```text
|
||||
Build command: bun install --frozen-lockfile && bun run build:cloudflare
|
||||
Deploy command: bun run deploy:cloudflare-builds
|
||||
```
|
||||
|
||||
5. Add the instance values from `.env.local` under **Settings** -> **Builds** -> **Build variables and secrets**. Use a secret for `EDGE_EVER_AUTH_PASSWORD_HASH`.
|
||||
|
||||
The Worker selected in the dashboard must be the one named by `EDGE_EVER_WORKER_NAME`. The deployment command creates a temporary Wrangler configuration from these variables, so do not commit a D1 ID, R2 bucket name, route, or password hash.
|
||||
|
||||
## Required Build Variables
|
||||
|
||||
Copy the applicable `EDGE_EVER_*` values from the initial deployment's `.env.local` file:
|
||||
|
||||
```text
|
||||
EDGE_EVER_WORKER_NAME
|
||||
EDGE_EVER_WORKERS_DEV
|
||||
EDGE_EVER_D1_DATABASE_NAME
|
||||
EDGE_EVER_D1_DATABASE_ID
|
||||
EDGE_EVER_R2_BUCKET_NAME
|
||||
EDGE_EVER_R2_PREVIEW_BUCKET_NAME
|
||||
EDGE_EVER_AUTH_USERNAME
|
||||
EDGE_EVER_AUTH_PASSWORD_HASH # Build Secret
|
||||
EDGE_EVER_SESSION_TTL_DAYS
|
||||
EDGE_EVER_DEMO_MODE # optional
|
||||
EDGE_EVER_DEMO_RESET_CRON # optional
|
||||
EDGE_EVER_CUSTOM_DOMAIN # optional
|
||||
EDGE_EVER_ROUTE_PATTERN # optional
|
||||
```
|
||||
|
||||
For a multi-instance configuration, set `EDGE_EVER_INSTANCE` and the matching scoped variables such as `EDGE_EVER_PROD_D1_DATABASE_ID` instead. The same resolution rules apply in local deployment and Workers Builds.
|
||||
|
||||
Workers Builds needs an API token that can deploy Workers, update R2 bindings, and apply D1 migrations. If the automatically created Builds token does not have D1 edit permission, select or create a token with that permission before enabling automated deployments.
|
||||
|
||||
## What Runs on Every Push
|
||||
|
||||
```text
|
||||
bun install --frozen-lockfile
|
||||
-> bun run typecheck
|
||||
-> bun run build
|
||||
-> bun run db:migrate:remote
|
||||
-> wrangler deploy
|
||||
```
|
||||
|
||||
`wrangler d1 migrations apply` records completed SQL migrations in D1. Thus, every instance receives only newly added migration files before the matching Worker version is published.
|
||||
|
||||
## Updating a Fork
|
||||
|
||||
1. On GitHub, use **Sync fork** to merge upstream changes into the fork's `main` branch.
|
||||
2. Cloudflare Workers Builds detects the push and runs the sequence above.
|
||||
3. Inspect the build log in the Worker **Deployments** tab if the build fails.
|
||||
|
||||
No GitHub Actions secrets or local redeployment are needed after the initial Workers Builds connection.
|
||||
|
||||
## Limits
|
||||
|
||||
On the Workers Free plan, Workers Builds includes 3,000 build minutes per month and permits one concurrent build per account. See [Cloudflare Workers Builds limits and pricing](https://developers.cloudflare.com/workers/ci-cd/builds/limits-and-pricing/) for current limits.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Cloudflare Workers Builds 自动部署
|
||||
|
||||
EdgeEver 的日常发布统一使用 Cloudflare Workers Builds。每个 EdgeEver Worker 只需连接一次其仓库的 `main` 分支;之后任何推送(包括 GitHub 的 **Sync fork**)都会自动构建、迁移数据库并发布该实例。
|
||||
|
||||
官方实例和所有 Fork 使用完全相同的发布命令。本地 `bun run deploy` 仅用于首次安装和紧急修复。
|
||||
|
||||
## 为实例连接自动部署
|
||||
|
||||
先按[手动部署指南](manual-deploy.zh-CN.md)创建资源并完成首次部署,然后执行:
|
||||
|
||||
```sh
|
||||
bun run deploy:builds:setup
|
||||
```
|
||||
|
||||
该命令会读取 Fork 的 `origin` remote 与 `.env.local`,以幂等方式创建或更新仓库连接、生产触发器、构建命令、构建缓存、监听路径以及 Build Variables/Secrets。首次成功时还会自动构建一次 `main`,验证整条自动发布链路;后续重跑仅更新配置,不会再次触发构建。
|
||||
|
||||
### Cloudflare 的一次性授权
|
||||
|
||||
Cloudflare 有两项账号级授权,仓库脚本不能、也不应该绕过:
|
||||
|
||||
1. 为 Fork 安装并授权 **Cloudflare Workers & Pages** GitHub App。Agent 可以打开 Cloudflare 页面完成操作,用户只需确认 GitHub 授权。
|
||||
2. 确保 Worker 已有可部署 Worker 且可执行 D1 migration 的 Workers Builds **build token**。若命令提示无法选择 token,请打开 **Worker** -> **Settings** -> **Builds** -> **API token**,创建/选择该 token,将其 UUID 填入 `.env.local` 的 `EDGE_EVER_BUILDS_BUILD_TOKEN_UUID`,然后重试命令。
|
||||
|
||||
配置 API 本身还需要一个 user-scoped token,具有 **Workers Builds Configuration: Edit** 与 **Workers Scripts: Read** 权限。把它写入 `EDGE_EVER_BUILDS_API_TOKEN`;若现有 `CLOUDFLARE_API_TOKEN` 同时具备这些权限,也可直接复用。这个 token 不会上传到 Worker 或 Cloudflare Builds。
|
||||
|
||||
### 手动兜底
|
||||
|
||||
若不能运行命令,则在 Cloudflare Dashboard 中:
|
||||
|
||||
1. 打开 **Workers & Pages**,选择 EdgeEver Worker。
|
||||
2. 打开 **Settings** -> **Builds** -> **Connect**。
|
||||
3. 授权 GitHub,选择该实例对应的仓库,并选择 `main` 作为生产分支。
|
||||
4. 设置以下命令:
|
||||
|
||||
```text
|
||||
Build command: bun install --frozen-lockfile && bun run build:cloudflare
|
||||
Deploy command: bun run deploy:cloudflare-builds
|
||||
```
|
||||
|
||||
5. 在 **Settings** -> **Builds** -> **Build variables and secrets** 中,填入初次部署时 `.env.local` 的实例配置;`EDGE_EVER_AUTH_PASSWORD_HASH` 必须保存为 Secret。
|
||||
|
||||
Dashboard 中选中的 Worker 必须与 `EDGE_EVER_WORKER_NAME` 对应。部署命令会根据这些变量生成临时 Wrangler 配置,因此严禁把 D1 ID、R2 bucket、路由或密码 hash 提交进仓库。
|
||||
|
||||
## 必需的构建变量
|
||||
|
||||
从初次部署产生的 `.env.local` 复制适用的 `EDGE_EVER_*` 变量:
|
||||
|
||||
```text
|
||||
EDGE_EVER_WORKER_NAME
|
||||
EDGE_EVER_WORKERS_DEV
|
||||
EDGE_EVER_D1_DATABASE_NAME
|
||||
EDGE_EVER_D1_DATABASE_ID
|
||||
EDGE_EVER_R2_BUCKET_NAME
|
||||
EDGE_EVER_R2_PREVIEW_BUCKET_NAME
|
||||
EDGE_EVER_AUTH_USERNAME
|
||||
EDGE_EVER_AUTH_PASSWORD_HASH # Build Secret
|
||||
EDGE_EVER_SESSION_TTL_DAYS
|
||||
EDGE_EVER_DEMO_MODE # 可选
|
||||
EDGE_EVER_DEMO_RESET_CRON # 可选
|
||||
EDGE_EVER_CUSTOM_DOMAIN # 可选
|
||||
EDGE_EVER_ROUTE_PATTERN # 可选
|
||||
```
|
||||
|
||||
多实例场景则设置 `EDGE_EVER_INSTANCE`,并使用 `EDGE_EVER_PROD_D1_DATABASE_ID` 之类的带实例前缀变量。本地部署和 Workers Builds 使用相同的变量解析规则。
|
||||
|
||||
Workers Builds 使用的 API Token 必须拥有 Worker 发布、R2 binding 更新和 D1 migration 所需权限。如果 Cloudflare 自动创建的 Builds token 没有 D1 编辑权限,请选择或新建一个具备该权限的 token。
|
||||
|
||||
## 每次推送的执行顺序
|
||||
|
||||
```text
|
||||
bun install --frozen-lockfile
|
||||
-> bun run typecheck
|
||||
-> bun run build
|
||||
-> bun run db:migrate:remote
|
||||
-> wrangler deploy
|
||||
```
|
||||
|
||||
`wrangler d1 migrations apply` 会在 D1 中记录已完成的 SQL migration。因此每个实例只会在发布对应 Worker 版本之前执行新增加的 migration。
|
||||
|
||||
## 更新 Fork
|
||||
|
||||
1. 在 GitHub 的 Fork 页面点击 **Sync fork**,将上游更新合入 fork 的 `main`。
|
||||
2. Cloudflare Workers Builds 检测到 push 后自动执行上述流程。
|
||||
3. 若失败,可在 Worker 的 **Deployments** 页面查看 build log。
|
||||
|
||||
首次连接完成后,不再需要 GitHub Actions Secrets,也不需要本地重新部署。
|
||||
|
||||
## 额度
|
||||
|
||||
Workers Free 目前每月包含 3,000 分钟 Workers Builds,并且每个账号同时执行一个构建。最新限制请参考 [Cloudflare Workers Builds 限额与价格](https://developers.cloudflare.com/workers/ci-cd/builds/limits-and-pricing/)。
|
||||
+4
-12
@@ -68,18 +68,10 @@ Before running `bun run deploy`, copy the D1 `database_id`, R2 bucket name, and
|
||||
|
||||
---
|
||||
|
||||
## Updating to the Latest Version
|
||||
## Enable Automatic Updates
|
||||
|
||||
When a new version is released, you can sync your fork and redeploy to apply updates:
|
||||
After the first deployment, connect the Worker to your fork with [Cloudflare Workers Builds](cloudflare-workers-builds.md). This is the standard production deployment path for every EdgeEver instance.
|
||||
|
||||
1. Open your EdgeEver fork on GitHub.
|
||||
2. Click **Sync fork** to pull the latest official code into your fork.
|
||||
3. Pull the updates locally and redeploy:
|
||||
```sh
|
||||
git pull
|
||||
bun install
|
||||
bun run deploy:doctor
|
||||
bun run deploy
|
||||
```
|
||||
After the one-time connection, click **Sync fork** whenever you want upstream updates. The resulting push automatically builds the web app, applies new remote D1 migrations, and deploys the Worker. No GitHub Actions secrets or local redeployment are required.
|
||||
|
||||
> ⚠️ **Important**: Syncing the fork on GitHub only updates your repository code. It does not redeploy your Cloudflare instance. You must redeploy locally (or via an Agent) for the updates to take effect.
|
||||
Keep `bun run deploy` available for first installation and emergency recovery.
|
||||
@@ -68,18 +68,10 @@ bun run deploy
|
||||
|
||||
---
|
||||
|
||||
## 更新到最新版
|
||||
## 开启自动更新
|
||||
|
||||
当官方仓库发布新版本时,如果你的实例是通过 Fork 部署的,可以按照以下步骤拉取最新代码并更新:
|
||||
首次部署完成后,请按 [Cloudflare Workers Builds 自动部署](cloudflare-workers-builds.zh-CN.md) 将 Worker 连接到 Fork。它是所有 EdgeEver 实例的标准生产发布路径。
|
||||
|
||||
1. 打开你自己的 EdgeEver Fork 仓库页面。
|
||||
2. 点击页面上的 **Sync fork**,将官方仓库的最新代码同步到你的 Fork。
|
||||
3. 在本地项目目录中拉取更新并重新部署:
|
||||
```sh
|
||||
git pull
|
||||
bun install
|
||||
bun run deploy:doctor
|
||||
bun run deploy
|
||||
```
|
||||
一次连接完成后,只需在 Fork 页面点击 **Sync fork**。产生的 push 会自动构建 Web、应用新的远程 D1 migration 并发布 Worker;不需要再配置 GitHub Actions Secrets,也不需要本地重新部署。
|
||||
|
||||
> ⚠️ **注意**:同步 Fork 仅仅更新了你 GitHub 仓库里的代码,并不会自动更新已经部署在 Cloudflare 的 Worker/Pages 实例。必须在本地(或通过 Agent)重新执行部署命令,更新才会生效。
|
||||
请保留 `bun run deploy` 作为首次安装和紧急修复的入口。
|
||||
+4
-1
@@ -19,6 +19,7 @@
|
||||
"build:extension": "vite build --config apps/extension/vite.config.ts",
|
||||
"dev:extension": "vite --config apps/extension/vite.config.ts",
|
||||
"build:web": "vite build --config apps/web/vite.config.ts",
|
||||
"build:cloudflare": "bun run typecheck && bun run build",
|
||||
"build:site": "bun --cwd apps/site build",
|
||||
"preview:web": "vite preview --config apps/web/vite.config.ts --host 127.0.0.1 --port 4173",
|
||||
"preview:site": "bun --cwd apps/site preview",
|
||||
@@ -34,7 +35,9 @@
|
||||
"deploy:doctor": "bun scripts/cloudflare-deploy.mjs doctor",
|
||||
"deploy:setup": "bun scripts/cloudflare-deploy.mjs setup",
|
||||
"deploy": "bun run build && bun run deploy:worker",
|
||||
"deploy:worker": "bun run db:migrate:remote && bun scripts/run-wrangler.mjs deploy",
|
||||
"deploy:worker": "bun run deploy:cloudflare-builds",
|
||||
"deploy:cloudflare-builds": "bun run db:migrate:remote && bun scripts/run-wrangler.mjs deploy",
|
||||
"deploy:builds:setup": "bun scripts/cloudflare-workers-builds.mjs setup",
|
||||
"deploy:site": "bun run build:site && wrangler pages deploy apps/site/dist --project-name=edgeever-official"
|
||||
},
|
||||
"cloudflare": {
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const command = process.argv[2] ?? "setup";
|
||||
const dryRun = process.argv.includes("--dry-run");
|
||||
const apiBase = "https://api.cloudflare.com/client/v4";
|
||||
|
||||
if (!["setup"].includes(command)) {
|
||||
console.error("Usage: bun scripts/cloudflare-workers-builds.mjs setup [--dry-run]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const parseEnv = (content) => {
|
||||
const values = new Map();
|
||||
|
||||
for (const line of content.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
||||
|
||||
const separator = trimmed.indexOf("=");
|
||||
const key = trimmed.slice(0, separator).trim();
|
||||
let value = trimmed.slice(separator + 1).trim();
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
values.set(key, value.replace(/\\\$/g, "$"));
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
const localValues = existsSync(resolve(".env.local"))
|
||||
? parseEnv(readFileSync(resolve(".env.local"), "utf8"))
|
||||
: new Map();
|
||||
|
||||
const value = (name) => process.env[name]?.trim() || localValues.get(name)?.trim() || "";
|
||||
const instanceKey = value("EDGE_EVER_INSTANCE").replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();
|
||||
const instanceValue = (name) => (instanceKey ? value(`EDGE_EVER_${instanceKey}_${name}`) : "") || value(`EDGE_EVER_${name}`);
|
||||
|
||||
const gitRepository = () => {
|
||||
const configured = value("EDGE_EVER_GITHUB_REPOSITORY");
|
||||
if (/^[^/\s]+\/[^/\s]+$/.test(configured)) {
|
||||
const [owner, repository] = configured.split("/");
|
||||
return { owner, repository };
|
||||
}
|
||||
const remote = configured || (() => {
|
||||
try {
|
||||
return execFileSync("git", ["config", "--get", "remote.origin.url"], { encoding: "utf8" }).trim();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
})();
|
||||
const match = remote.match(/github\.com[/:]([^/]+)\/([^/#\s]+?)(?:\.git)?$/i);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
"Cannot identify the GitHub repository. Set EDGE_EVER_GITHUB_REPOSITORY=owner/repository or configure origin.",
|
||||
);
|
||||
}
|
||||
return { owner: match[1], repository: match[2] };
|
||||
};
|
||||
|
||||
const cfToken = value("EDGE_EVER_BUILDS_API_TOKEN") || value("CLOUDFLARE_API_TOKEN");
|
||||
const accountId = value("CLOUDFLARE_ACCOUNT_ID");
|
||||
const workerName = instanceValue("WORKER_NAME");
|
||||
|
||||
const request = async (method, path, body) => {
|
||||
if (dryRun) return undefined;
|
||||
|
||||
const response = await fetch(`${apiBase}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `Bearer ${cfToken}`,
|
||||
...(body ? { "Content-Type": "application/json" } : {}),
|
||||
},
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || payload.success === false) {
|
||||
const detail = payload.errors?.map((error) => error.message).join("; ") || response.statusText;
|
||||
throw new Error(`${method} ${path} failed: ${detail}`);
|
||||
}
|
||||
return payload.result;
|
||||
};
|
||||
|
||||
const buildVariables = () => {
|
||||
const merged = new Map(localValues);
|
||||
for (const [key, current] of Object.entries(process.env)) {
|
||||
if (current !== undefined) merged.set(key, current);
|
||||
}
|
||||
|
||||
const runtimeNames = [
|
||||
"WORKER_NAME",
|
||||
"WORKERS_DEV",
|
||||
"D1_DATABASE_NAME",
|
||||
"D1_DATABASE_ID",
|
||||
"R2_BUCKET_NAME",
|
||||
"R2_PREVIEW_BUCKET_NAME",
|
||||
"AUTH_USERNAME",
|
||||
"AUTH_PASSWORD_HASH",
|
||||
"SESSION_TTL_DAYS",
|
||||
"DEMO_MODE",
|
||||
"DEMO_RESET_CRON",
|
||||
"CUSTOM_DOMAIN",
|
||||
"ROUTE_PATTERN",
|
||||
];
|
||||
const entries = [];
|
||||
|
||||
if (instanceKey) entries.push(["EDGE_EVER_INSTANCE", instanceKey]);
|
||||
for (const name of runtimeNames) {
|
||||
const scoped = instanceKey ? `EDGE_EVER_${instanceKey}_${name}` : "";
|
||||
const key = scoped && merged.get(scoped)?.trim() ? scoped : `EDGE_EVER_${name}`;
|
||||
const current = merged.get(key)?.trim();
|
||||
if (current) entries.push([key, current]);
|
||||
}
|
||||
|
||||
if (!entries.some(([key]) => key.endsWith("AUTH_PASSWORD_HASH"))) {
|
||||
throw new Error("Missing EDGE_EVER_AUTH_PASSWORD_HASH. Run deploy:setup before configuring Workers Builds.");
|
||||
}
|
||||
|
||||
return Object.fromEntries(entries.map(([key, current]) => [key, {
|
||||
value: current.replace(/\\\$/g, "$"),
|
||||
is_secret: key.endsWith("AUTH_PASSWORD_HASH"),
|
||||
}]));
|
||||
};
|
||||
|
||||
const triggerPayload = (workerTag, repoConnectionUuid, buildTokenUuid) => ({
|
||||
external_script_id: workerTag,
|
||||
repo_connection_uuid: repoConnectionUuid,
|
||||
build_token_uuid: buildTokenUuid,
|
||||
trigger_name: "Deploy EdgeEver production",
|
||||
build_command: "bun install --frozen-lockfile && bun run build:cloudflare",
|
||||
deploy_command: "bun run deploy:cloudflare-builds",
|
||||
root_directory: "/",
|
||||
branch_includes: ["main"],
|
||||
branch_excludes: [],
|
||||
path_includes: ["*"],
|
||||
path_excludes: [
|
||||
"apps/site/*",
|
||||
"apps/mobile/*",
|
||||
"apps/extension/*",
|
||||
"docs/*",
|
||||
"README*",
|
||||
".github/*",
|
||||
],
|
||||
build_caching_enabled: true,
|
||||
});
|
||||
|
||||
const setup = async () => {
|
||||
if (!accountId) throw new Error("Missing CLOUDFLARE_ACCOUNT_ID.");
|
||||
if (!workerName) throw new Error("Missing EDGE_EVER_WORKER_NAME.");
|
||||
if (!cfToken) {
|
||||
throw new Error(
|
||||
"Missing EDGE_EVER_BUILDS_API_TOKEN (or CLOUDFLARE_API_TOKEN). It must be a user-scoped token with Workers Builds Configuration: Edit and Workers Scripts: Read.",
|
||||
);
|
||||
}
|
||||
|
||||
const repository = gitRepository();
|
||||
const variables = buildVariables();
|
||||
console.log(`[ok] repository: ${repository.owner}/${repository.repository}`);
|
||||
console.log(`[ok] Worker: ${workerName}`);
|
||||
console.log(`[ok] build variables: ${Object.keys(variables).length} (${Object.values(variables).filter((item) => item.is_secret).length} secret)`);
|
||||
|
||||
if (dryRun) {
|
||||
console.log("[ok] dry run complete; no Cloudflare configuration was changed");
|
||||
return;
|
||||
}
|
||||
|
||||
const github = await fetch(`https://api.github.com/repos/${repository.owner}/${repository.repository}`, {
|
||||
headers: { Accept: "application/vnd.github+json" },
|
||||
});
|
||||
const githubRepository = await github.json().catch(() => ({}));
|
||||
if (!github.ok || !githubRepository.id || !githubRepository.owner?.id) {
|
||||
throw new Error(`Cannot read GitHub repository ${repository.owner}/${repository.repository}. For a private repository, configure an accessible origin or run this command through an authenticated Agent.`);
|
||||
}
|
||||
|
||||
const scripts = await request("GET", `/accounts/${accountId}/workers/scripts`);
|
||||
const worker = scripts?.find((script) => script.id === workerName);
|
||||
if (!worker?.tag) {
|
||||
throw new Error(`Worker ${workerName} was not found. Complete the first bun run deploy before enabling Workers Builds.`);
|
||||
}
|
||||
|
||||
const requestedBuildToken = value("EDGE_EVER_BUILDS_BUILD_TOKEN_UUID");
|
||||
const buildTokens = await request("GET", `/accounts/${accountId}/builds/tokens`);
|
||||
const buildToken = requestedBuildToken
|
||||
? buildTokens?.find((token) => token.build_token_uuid === requestedBuildToken)
|
||||
: buildTokens?.length === 1 ? buildTokens[0] : undefined;
|
||||
if (!buildToken?.build_token_uuid) {
|
||||
throw new Error(
|
||||
"No unambiguous Workers Builds build token was found. In Cloudflare Dashboard open this Worker -> Settings -> Builds -> API token, create/select a token with D1 edit permission, then set EDGE_EVER_BUILDS_BUILD_TOKEN_UUID and rerun.",
|
||||
);
|
||||
}
|
||||
|
||||
const connection = await request("PUT", `/accounts/${accountId}/builds/repos/connections`, {
|
||||
provider_type: "github",
|
||||
provider_account_id: String(githubRepository.owner.id),
|
||||
provider_account_name: githubRepository.owner.login,
|
||||
repo_id: String(githubRepository.id),
|
||||
repo_name: githubRepository.name,
|
||||
});
|
||||
const repoConnectionUuid = connection?.repo_connection_uuid;
|
||||
if (!repoConnectionUuid) throw new Error("Cloudflare did not return repo_connection_uuid.");
|
||||
|
||||
const triggers = await request("GET", `/accounts/${accountId}/builds/workers/${worker.tag}/triggers`);
|
||||
const existing = triggers?.find((trigger) => trigger.branch_includes?.includes("main"));
|
||||
const payload = triggerPayload(worker.tag, repoConnectionUuid, buildToken.build_token_uuid);
|
||||
const trigger = existing
|
||||
? await request("PATCH", `/accounts/${accountId}/builds/triggers/${existing.trigger_uuid}`, payload)
|
||||
: await request("POST", `/accounts/${accountId}/builds/triggers`, payload);
|
||||
const triggerUuid = trigger?.trigger_uuid ?? existing?.trigger_uuid;
|
||||
if (!triggerUuid) throw new Error("Cloudflare did not return trigger_uuid.");
|
||||
|
||||
await request("PATCH", `/accounts/${accountId}/builds/triggers/${triggerUuid}/environment_variables`, variables);
|
||||
console.log(`[ok] ${existing ? "updated" : "created"} production trigger`);
|
||||
if (!existing) {
|
||||
const build = await request("POST", `/accounts/${accountId}/builds/triggers/${triggerUuid}/builds`, { branch: "main" });
|
||||
console.log(`[ok] started first build${build?.build_uuid ? `: ${build.build_uuid}` : ""}`);
|
||||
}
|
||||
console.log("[ok] Workers Builds is ready; future pushes to main deploy automatically");
|
||||
};
|
||||
|
||||
setup().catch((error) => {
|
||||
console.error(`[fail] ${error instanceof Error ? error.message : error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in new issue
Block a user