mirror of
https://github.com/beilunyang/moemail.git
synced 2026-05-06 20:02:52 +08:00
Merge pull request #34 from sdrpsps/chore/deploy-script
chore: Update deploy script
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
AUTH_GITHUB_ID = ""
|
||||
AUTH_GITHUB_SECRET = ""
|
||||
AUTH_SECRET = ""
|
||||
AUTH_SECRET = ""
|
||||
|
||||
CLOUDFLARE_API_TOKEN = ""
|
||||
CLOUDFLARE_ACCOUNT_ID = ""
|
||||
DATABASE_NAME = ""
|
||||
KV_NAMESPACE_NAME = ""
|
||||
|
||||
CUSTOM_DOMAIN = ""
|
||||
141
.github/workflows/deploy.yml
vendored
141
.github/workflows/deploy.yml
vendored
@@ -5,22 +5,6 @@ on:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_migrations:
|
||||
description: 'Run database migrations'
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
deploy_email_worker:
|
||||
description: 'Deploy email Worker'
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
deploy_cleanup_worker:
|
||||
description: 'Deploy cleanup Worker'
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -51,112 +35,21 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Check if database migrations have changes
|
||||
- name: Check migrations changes
|
||||
id: check_migrations
|
||||
if: github.event_name == 'push'
|
||||
- name: Run deploy script
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
PROJECT_NAME: ${{ secrets.PROJECT_NAME }}
|
||||
DATABASE_NAME: ${{ secrets.DATABASE_NAME }}
|
||||
KV_NAMESPACE_NAME: ${{ secrets.KV_NAMESPACE_NAME }}
|
||||
CUSTOM_DOMAIN: ${{ secrets.CUSTOM_DOMAIN }}
|
||||
AUTH_GITHUB_ID: ${{ secrets.AUTH_GITHUB_ID }}
|
||||
AUTH_GITHUB_SECRET: ${{ secrets.AUTH_GITHUB_SECRET }}
|
||||
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
|
||||
run: pnpm dlx tsx scripts/deploy/index.ts
|
||||
|
||||
# Clean up
|
||||
- name: Post deployment cleanup
|
||||
run: |
|
||||
if [ -z "${{ steps.previoustag.outputs.tag }}" ]; then
|
||||
echo "migrations_changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
if git diff ${{ steps.previoustag.outputs.tag }}..HEAD --name-only | grep -q "^drizzle/"; then
|
||||
echo "migrations_changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "migrations_changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
# Process configuration files
|
||||
- name: Process configuration files
|
||||
run: |
|
||||
# Process wrangler.example.toml
|
||||
if [ -f wrangler.example.toml ]; then
|
||||
cp wrangler.example.toml wrangler.toml
|
||||
sed -i "s/database_name = \".*\"/database_name = \"${{ secrets.DATABASE_NAME }}\"/" wrangler.toml
|
||||
sed -i "s/database_id = \".*\"/database_id = \"${{ secrets.DATABASE_ID }}\"/" wrangler.toml
|
||||
sed -i "s/id = \"\"/id = \"${{ secrets.KV_NAMESPACE_ID }}\"/" wrangler.toml
|
||||
fi
|
||||
|
||||
# Process wrangler.email.example.toml
|
||||
if [ -f wrangler.email.example.toml ]; then
|
||||
cp wrangler.email.example.toml wrangler.email.toml
|
||||
sed -i "s/database_name = \".*\"/database_name = \"${{ secrets.DATABASE_NAME }}\"/" wrangler.email.toml
|
||||
sed -i "s/database_id = \".*\"/database_id = \"${{ secrets.DATABASE_ID }}\"/" wrangler.email.toml
|
||||
fi
|
||||
|
||||
# Process wrangler.cleanup.example.toml
|
||||
if [ -f wrangler.cleanup.example.toml ]; then
|
||||
cp wrangler.cleanup.example.toml wrangler.cleanup.toml
|
||||
sed -i "s/database_name = \".*\"/database_name = \"${{ secrets.DATABASE_NAME }}\"/" wrangler.cleanup.toml
|
||||
sed -i "s/database_id = \".*\"/database_id = \"${{ secrets.DATABASE_ID }}\"/" wrangler.cleanup.toml
|
||||
fi
|
||||
|
||||
# Run database migrations if needed
|
||||
- name: Run database migrations
|
||||
if: |
|
||||
github.event_name == 'push' && steps.check_migrations.outputs.migrations_changed == 'true' ||
|
||||
github.event_name == 'workflow_dispatch' && github.event.inputs.run_migrations == 'true'
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: pnpm db:migrate-remote
|
||||
|
||||
# Check if workers have changes
|
||||
- name: Check workers changes
|
||||
id: check_changes
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
if [ -z "${{ steps.previoustag.outputs.tag }}" ]; then
|
||||
# Check email worker and its dependencies
|
||||
if git ls-files | grep -q -E "workers/email-receiver.ts|app/lib/schema.ts|app/lib/webhook.ts|app/config/webhook.ts"; then
|
||||
echo "email_worker_changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "email_worker_changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
# Check cleanup worker
|
||||
if git ls-files | grep -q "workers/cleanup.ts"; then
|
||||
echo "cleanup_worker_changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cleanup_worker_changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
# Check email worker and its dependencies changes
|
||||
if git diff ${{ steps.previoustag.outputs.tag }}..HEAD --name-only | grep -q -E "workers/email-receiver.ts|app/lib/schema.ts|app/lib/webhook.ts|app/config/webhook.ts"; then
|
||||
echo "email_worker_changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "email_worker_changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
# Check cleanup worker changes
|
||||
if git diff ${{ steps.previoustag.outputs.tag }}..HEAD --name-only | grep -q "workers/cleanup.ts"; then
|
||||
echo "cleanup_worker_changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cleanup_worker_changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
# Deploy Pages application
|
||||
- name: Deploy Pages
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: pnpm run deploy:pages
|
||||
|
||||
# Deploy email worker if changed or manually triggered
|
||||
- name: Deploy Email Worker
|
||||
if: |
|
||||
github.event_name == 'push' && steps.check_changes.outputs.email_worker_changed == 'true' ||
|
||||
github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_email_worker == 'true'
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: pnpm run deploy:email
|
||||
|
||||
# Deploy cleanup worker if changed or manually triggered
|
||||
- name: Deploy Cleanup Worker
|
||||
if: |
|
||||
github.event_name == 'push' && steps.check_changes.outputs.cleanup_worker_changed == 'true' ||
|
||||
github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_cleanup_worker == 'true'
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: pnpm run deploy:cleanup
|
||||
rm -f .env*.*
|
||||
rm -f wrangler*.json
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -46,4 +46,8 @@ wrangler.email.toml
|
||||
wrangler.cleanup.toml
|
||||
|
||||
public/workbox-*.js
|
||||
public/sw.js
|
||||
public/sw.js
|
||||
|
||||
wrangler.json
|
||||
wrangler.cleanup.json
|
||||
wrangler.email.json
|
||||
14
package.json
14
package.json
@@ -8,14 +8,14 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"build:pages": "npx @cloudflare/next-on-pages",
|
||||
"db:migrate-local": "bun run scripts/migrate.ts local",
|
||||
"db:migrate-remote": "bun run scripts/migrate.ts remote",
|
||||
"db:migrate-local": "tsx scripts/migrate.ts local",
|
||||
"db:migrate-remote": "tsx scripts/migrate.ts remote",
|
||||
"webhook-test-server": "bun run scripts/webhook-test-server.ts",
|
||||
"generate-test-data": "wrangler dev scripts/generate-test-data.ts",
|
||||
"dev:cleanup": "wrangler dev --config wrangler.cleanup.toml --test-scheduled",
|
||||
"dev:cleanup": "wrangler dev --config wrangler.cleanup.json --test-scheduled",
|
||||
"test:cleanup": "curl http://localhost:8787/__scheduled",
|
||||
"deploy:email": "wrangler deploy --config wrangler.email.toml",
|
||||
"deploy:cleanup": "wrangler deploy --config wrangler.cleanup.toml",
|
||||
"deploy:email": "wrangler deploy --config wrangler.email.json",
|
||||
"deploy:cleanup": "wrangler deploy --config wrangler.cleanup.json",
|
||||
"deploy:pages": "npm run build:pages && wrangler pages deploy .vercel/output/static --branch main"
|
||||
},
|
||||
"type": "module",
|
||||
@@ -53,18 +53,20 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20241127.0",
|
||||
"@iarna/toml": "^3.0.0",
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/next-pwa": "^5.6.9",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"bun": "^1.1.39",
|
||||
"cloudflare": "^4.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-kit": "^0.28.1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5",
|
||||
"vercel": "39.1.1",
|
||||
"wrangler": "^3.91.0"
|
||||
|
||||
480
pnpm-lock.yaml
generated
480
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
92
scripts/deploy/cloudflare.ts
Normal file
92
scripts/deploy/cloudflare.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import Cloudflare from "cloudflare";
|
||||
import "dotenv/config";
|
||||
|
||||
const CF_ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID!;
|
||||
const CF_API_TOKEN = process.env.CLOUDFLARE_API_TOKEN;
|
||||
const CUSTOM_DOMAIN = process.env.CUSTOM_DOMAIN;
|
||||
const PROJECT_NAME = process.env.PROJECT_NAME || "moemail";
|
||||
const DATABASE_NAME = process.env.DATABASE_NAME || "moemail-db";
|
||||
const KV_NAMESPACE_NAME = process.env.KV_NAMESPACE_NAME || "moemail-kv";
|
||||
|
||||
const client = new Cloudflare({
|
||||
apiKey: CF_API_TOKEN,
|
||||
});
|
||||
|
||||
export const getPages = async () => {
|
||||
const projectInfo = await client.pages.projects.get(PROJECT_NAME, {
|
||||
account_id: CF_ACCOUNT_ID,
|
||||
});
|
||||
|
||||
return projectInfo;
|
||||
};
|
||||
|
||||
export const createPages = async () => {
|
||||
console.log(`🆕 Creating new Cloudflare Pages project: "${PROJECT_NAME}"`);
|
||||
|
||||
const project = await client.pages.projects.create({
|
||||
account_id: CF_ACCOUNT_ID,
|
||||
name: PROJECT_NAME,
|
||||
production_branch: "main",
|
||||
});
|
||||
|
||||
if (CUSTOM_DOMAIN) {
|
||||
console.log("🔗 Setting pages domain...");
|
||||
|
||||
await client.pages.projects.domains.create(PROJECT_NAME, {
|
||||
account_id: CF_ACCOUNT_ID,
|
||||
name: CUSTOM_DOMAIN?.split("://")[1],
|
||||
});
|
||||
|
||||
console.log("✅ Pages domain set successfully");
|
||||
}
|
||||
|
||||
console.log("✅ Project created successfully");
|
||||
|
||||
return project;
|
||||
};
|
||||
|
||||
export const getDatabase = async () => {
|
||||
const database = await client.d1.database.get(DATABASE_NAME, {
|
||||
account_id: CF_ACCOUNT_ID,
|
||||
});
|
||||
|
||||
return database;
|
||||
};
|
||||
|
||||
export const createDatabase = async () => {
|
||||
console.log(`🆕 Creating new D1 database: "${DATABASE_NAME}"`);
|
||||
|
||||
const database = await client.d1.database.create({
|
||||
account_id: CF_ACCOUNT_ID,
|
||||
name: DATABASE_NAME,
|
||||
});
|
||||
|
||||
console.log("✅ Database created successfully");
|
||||
|
||||
return database;
|
||||
};
|
||||
|
||||
export const getKVNamespaceList = async () => {
|
||||
const kvNamespaces = [];
|
||||
|
||||
for await (const namespace of client.kv.namespaces.list({
|
||||
account_id: CF_ACCOUNT_ID,
|
||||
})) {
|
||||
kvNamespaces.push(namespace);
|
||||
}
|
||||
|
||||
return kvNamespaces;
|
||||
};
|
||||
|
||||
export const createKVNamespace = async () => {
|
||||
console.log(`🆕 Creating new KV namespace: "${KV_NAMESPACE_NAME}"`);
|
||||
|
||||
const kvNamespace = await client.kv.namespaces.create({
|
||||
account_id: CF_ACCOUNT_ID,
|
||||
title: KV_NAMESPACE_NAME,
|
||||
});
|
||||
|
||||
console.log("✅ KV namespace created successfully");
|
||||
|
||||
return kvNamespace;
|
||||
};
|
||||
432
scripts/deploy/index.ts
Normal file
432
scripts/deploy/index.ts
Normal file
@@ -0,0 +1,432 @@
|
||||
import { NotFoundError } from "cloudflare";
|
||||
import "dotenv/config";
|
||||
import { execSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
createDatabase,
|
||||
createKVNamespace,
|
||||
createPages,
|
||||
getDatabase,
|
||||
getKVNamespaceList,
|
||||
getPages,
|
||||
} from "./cloudflare";
|
||||
|
||||
const PROJECT_NAME = process.env.PROJECT_NAME || "moemail";
|
||||
const DATABASE_NAME = process.env.DATABASE_NAME || "moemail-db";
|
||||
const KV_NAMESPACE_NAME = process.env.KV_NAMESPACE_NAME || "moemail-kv";
|
||||
const CUSTOM_DOMAIN = process.env.CUSTOM_DOMAIN;
|
||||
|
||||
/**
|
||||
* 验证必要的环境变量
|
||||
*/
|
||||
const validateEnvironment = () => {
|
||||
const requiredEnvVars = ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN"];
|
||||
const missing = requiredEnvVars.filter((varName) => !process.env[varName]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`Missing required environment variables: ${missing.join(", ")}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理JSON配置文件
|
||||
*/
|
||||
const setupConfigFile = (examplePath: string, targetPath: string) => {
|
||||
try {
|
||||
// 如果目标文件已存在,则跳过
|
||||
if (existsSync(targetPath)) {
|
||||
console.log(`✨ Configuration ${targetPath} already exists.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!existsSync(examplePath)) {
|
||||
console.log(`⚠️ Example file ${examplePath} does not exist, skipping...`);
|
||||
return;
|
||||
}
|
||||
|
||||
const configContent = readFileSync(examplePath, "utf-8");
|
||||
const json = JSON.parse(configContent);
|
||||
|
||||
// 处理数据库配置
|
||||
if (json.d1_databases && json.d1_databases.length > 0) {
|
||||
json.d1_databases[0].database_name = DATABASE_NAME;
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
writeFileSync(targetPath, JSON.stringify(json, null, 2));
|
||||
console.log(`✅ Configuration ${targetPath} setup successfully.`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to setup ${targetPath}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置所有Wrangler配置文件
|
||||
*/
|
||||
const setupWranglerConfigs = () => {
|
||||
console.log("🔧 Setting up Wrangler configuration files...");
|
||||
|
||||
const configs = [
|
||||
{ example: "wrangler.example.json", target: "wrangler.json" },
|
||||
{ example: "wrangler.email.example.json", target: "wrangler.email.json" },
|
||||
{ example: "wrangler.cleanup.example.json", target: "wrangler.cleanup.json" },
|
||||
];
|
||||
|
||||
// 处理每个配置文件
|
||||
for (const config of configs) {
|
||||
setupConfigFile(
|
||||
resolve(config.example),
|
||||
resolve(config.target)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新数据库ID到所有配置文件
|
||||
*/
|
||||
const updateDatabaseConfig = (dbId: string) => {
|
||||
console.log(`📝 Updating database ID (${dbId}) in configurations...`);
|
||||
|
||||
// 更新所有配置文件
|
||||
const configFiles = [
|
||||
"wrangler.json",
|
||||
"wrangler.email.json",
|
||||
"wrangler.cleanup.json",
|
||||
];
|
||||
|
||||
for (const filename of configFiles) {
|
||||
const configPath = resolve(filename);
|
||||
if (!existsSync(configPath)) continue;
|
||||
|
||||
try {
|
||||
const json = JSON.parse(readFileSync(configPath, "utf-8"));
|
||||
if (json.d1_databases && json.d1_databases.length > 0) {
|
||||
json.d1_databases[0].database_id = dbId;
|
||||
}
|
||||
writeFileSync(configPath, JSON.stringify(json, null, 2));
|
||||
console.log(`✅ Updated database ID in ${filename}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to update ${filename}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新KV命名空间ID到所有配置文件
|
||||
*/
|
||||
const updateKVConfig = (namespaceId: string) => {
|
||||
console.log(`📝 Updating KV namespace ID (${namespaceId}) in configurations...`);
|
||||
|
||||
// KV命名空间只在主wrangler.json中使用
|
||||
const wranglerPath = resolve("wrangler.json");
|
||||
if (existsSync(wranglerPath)) {
|
||||
try {
|
||||
const json = JSON.parse(readFileSync(wranglerPath, "utf-8"));
|
||||
if (json.kv_namespaces && json.kv_namespaces.length > 0) {
|
||||
json.kv_namespaces[0].id = namespaceId;
|
||||
}
|
||||
writeFileSync(wranglerPath, JSON.stringify(json, null, 2));
|
||||
console.log(`✅ Updated KV namespace ID in wrangler.json`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to update wrangler.json:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查并创建数据库
|
||||
*/
|
||||
const checkAndCreateDatabase = async () => {
|
||||
console.log(`🔍 Checking if database "${DATABASE_NAME}" exists...`);
|
||||
|
||||
try {
|
||||
const database = await getDatabase();
|
||||
|
||||
if (!database || !database.uuid) {
|
||||
throw new Error('Database object is missing a valid UUID');
|
||||
}
|
||||
|
||||
updateDatabaseConfig(database.uuid);
|
||||
console.log(`✅ Database "${DATABASE_NAME}" already exists (ID: ${database.uuid})`);
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
console.log(`⚠️ Database not found, creating new database...`);
|
||||
try {
|
||||
const database = await createDatabase();
|
||||
|
||||
if (!database || !database.uuid) {
|
||||
throw new Error('Database object is missing a valid UUID');
|
||||
}
|
||||
|
||||
updateDatabaseConfig(database.uuid);
|
||||
console.log(`✅ Database "${DATABASE_NAME}" created successfully (ID: ${database.uuid})`);
|
||||
} catch (createError) {
|
||||
console.error(`❌ Failed to create database:`, createError);
|
||||
throw createError;
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ An error occurred while checking the database:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 迁移数据库
|
||||
*/
|
||||
const migrateDatabase = () => {
|
||||
console.log("📝 Migrating remote database...");
|
||||
try {
|
||||
execSync("pnpm run db:migrate-remote", { stdio: "inherit" });
|
||||
console.log("✅ Database migration completed successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Database migration failed:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查并创建KV命名空间
|
||||
*/
|
||||
const checkAndCreateKVNamespace = async () => {
|
||||
console.log(`🔍 Checking if KV namespace "${KV_NAMESPACE_NAME}" exists...`);
|
||||
|
||||
try {
|
||||
let namespace;
|
||||
|
||||
const namespaceList = await getKVNamespaceList();
|
||||
namespace = namespaceList.find(ns => ns.title === KV_NAMESPACE_NAME);
|
||||
|
||||
if (namespace && namespace.id) {
|
||||
updateKVConfig(namespace.id);
|
||||
console.log(`✅ KV namespace "${KV_NAMESPACE_NAME}" found by name (ID: ${namespace.id})`);
|
||||
} else {
|
||||
console.log("⚠️ KV namespace not found by name, creating new KV namespace...");
|
||||
namespace = await createKVNamespace();
|
||||
updateKVConfig(namespace.id);
|
||||
console.log(`✅ KV namespace "${KV_NAMESPACE_NAME}" created successfully (ID: ${namespace.id})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ An error occurred while checking the KV namespace:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查并创建Pages项目
|
||||
*/
|
||||
const checkAndCreatePages = async () => {
|
||||
console.log(`🔍 Checking if project "${PROJECT_NAME}" exists...`);
|
||||
|
||||
try {
|
||||
await getPages();
|
||||
console.log("✅ Project already exists, proceeding with update...");
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
console.log("⚠️ Project not found, creating new project...");
|
||||
const pages = await createPages();
|
||||
|
||||
if (!CUSTOM_DOMAIN && pages.subdomain) {
|
||||
console.log("⚠️ CUSTOM_DOMAIN is empty, using pages default domain...");
|
||||
console.log("📝 Updating environment variables...");
|
||||
|
||||
// 更新环境变量为默认的Pages域名
|
||||
const appUrl = `https://${pages.subdomain}`;
|
||||
updateEnvVar("CUSTOM_DOMAIN", appUrl);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ An error occurred while checking the project:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 推送Pages密钥
|
||||
*/
|
||||
const pushPagesSecret = () => {
|
||||
console.log("🔐 Pushing environment secrets to Pages...");
|
||||
|
||||
try {
|
||||
// 确保.env文件存在
|
||||
if (!existsSync(resolve('.env'))) {
|
||||
setupEnvFile();
|
||||
}
|
||||
|
||||
// 创建一个临时文件,只包含运行时所需的环境变量
|
||||
const envContent = readFileSync(resolve('.env'), 'utf-8');
|
||||
const runtimeEnvFile = resolve('.env.runtime');
|
||||
|
||||
// 定义运行时所需的环境变量列表
|
||||
const runtimeEnvVars = ['AUTH_GITHUB_ID', 'AUTH_GITHUB_SECRET', 'AUTH_SECRET'];
|
||||
|
||||
// 从.env文件中提取运行时变量
|
||||
const runtimeEnvContent = envContent
|
||||
.split('\n')
|
||||
.filter(line => {
|
||||
const trimmedLine = line.trim();
|
||||
// 跳过注释和空行
|
||||
if (!trimmedLine || trimmedLine.startsWith('#')) return false;
|
||||
|
||||
// 检查是否为运行时所需的环境变量
|
||||
for (const varName of runtimeEnvVars) {
|
||||
if (line.startsWith(`${varName} =`) || line.startsWith(`${varName}=`)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
// 写入临时文件
|
||||
writeFileSync(runtimeEnvFile, runtimeEnvContent);
|
||||
|
||||
// 使用临时文件推送secrets
|
||||
execSync(`pnpm dlx wrangler pages secret bulk ${runtimeEnvFile}`, { stdio: "inherit" });
|
||||
|
||||
// 清理临时文件
|
||||
execSync(`rm ${runtimeEnvFile}`, { stdio: "inherit" });
|
||||
|
||||
console.log("✅ Secrets pushed successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to push secrets:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 部署Pages应用
|
||||
*/
|
||||
const deployPages = () => {
|
||||
console.log("🚧 Deploying to Cloudflare Pages...");
|
||||
try {
|
||||
execSync("pnpm run build:pages && pnpm dlx wrangler pages deploy .vercel/output/static --branch main", { stdio: "inherit" });
|
||||
console.log("✅ Pages deployment completed successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Pages deployment failed:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 部署Email Worker
|
||||
*/
|
||||
const deployEmailWorker = () => {
|
||||
console.log("🚧 Deploying Email Worker...");
|
||||
try {
|
||||
execSync("pnpm dlx wrangler deploy --config wrangler.email.json", { stdio: "inherit" });
|
||||
console.log("✅ Email Worker deployed successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Email Worker deployment failed:", error);
|
||||
// 继续执行而不中断
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 部署Cleanup Worker
|
||||
*/
|
||||
const deployCleanupWorker = () => {
|
||||
console.log("🚧 Deploying Cleanup Worker...");
|
||||
try {
|
||||
execSync("pnpm dlx wrangler deploy --config wrangler.cleanup.json", { stdio: "inherit" });
|
||||
console.log("✅ Cleanup Worker deployed successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Cleanup Worker deployment failed:", error);
|
||||
// 继续执行而不中断
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建或更新环境变量文件
|
||||
*/
|
||||
const setupEnvFile = () => {
|
||||
console.log("📄 Setting up environment file...");
|
||||
const envFilePath = resolve(".env");
|
||||
const envExamplePath = resolve(".env.example");
|
||||
|
||||
// 如果.env文件不存在,则从.env.example复制创建
|
||||
if (!existsSync(envFilePath) && existsSync(envExamplePath)) {
|
||||
console.log("⚠️ .env file does not exist, creating from example...");
|
||||
|
||||
// 从示例文件复制
|
||||
let envContent = readFileSync(envExamplePath, "utf-8");
|
||||
|
||||
// 填充当前的环境变量
|
||||
const envVarMatches = envContent.match(/^([A-Z_]+)\s*=\s*".*?"/gm);
|
||||
if (envVarMatches) {
|
||||
for (const match of envVarMatches) {
|
||||
const varName = match.split("=")[0].trim();
|
||||
if (process.env[varName]) {
|
||||
const regex = new RegExp(`${varName}\\s*=\\s*".*?"`, "g");
|
||||
envContent = envContent.replace(regex, `${varName} = "${process.env[varName]}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(envFilePath, envContent);
|
||||
console.log("✅ .env file created from example");
|
||||
} else if (existsSync(envFilePath)) {
|
||||
console.log("✨ .env file already exists");
|
||||
} else {
|
||||
console.error("❌ .env.example file not found!");
|
||||
throw new Error(".env.example file not found");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新环境变量
|
||||
*/
|
||||
const updateEnvVar = (name: string, value: string) => {
|
||||
// 首先更新进程环境变量
|
||||
process.env[name] = value;
|
||||
|
||||
// 然后尝试更新.env文件
|
||||
const envFilePath = resolve(".env");
|
||||
if (!existsSync(envFilePath)) {
|
||||
setupEnvFile();
|
||||
}
|
||||
|
||||
let envContent = readFileSync(envFilePath, "utf-8");
|
||||
const regex = new RegExp(`^${name}\\s*=\\s*".*?"`, "m");
|
||||
|
||||
if (envContent.match(regex)) {
|
||||
envContent = envContent.replace(regex, `${name} = "${value}"`);
|
||||
} else {
|
||||
envContent += `\n${name} = "${value}"`;
|
||||
}
|
||||
|
||||
writeFileSync(envFilePath, envContent);
|
||||
console.log(`✅ Updated ${name} in .env file`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
const main = async () => {
|
||||
try {
|
||||
console.log("🚀 Starting deployment process...");
|
||||
|
||||
validateEnvironment();
|
||||
setupEnvFile();
|
||||
setupWranglerConfigs();
|
||||
await checkAndCreateDatabase();
|
||||
migrateDatabase();
|
||||
await checkAndCreateKVNamespace();
|
||||
await checkAndCreatePages();
|
||||
pushPagesSecret();
|
||||
deployPages();
|
||||
deployEmailWorker();
|
||||
deployCleanupWorker();
|
||||
|
||||
console.log("🎉 Deployment completed successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Deployment failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -1,4 +1,3 @@
|
||||
import { parse } from '@iarna/toml'
|
||||
import { readFileSync } from 'fs'
|
||||
import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
@@ -27,23 +26,23 @@ async function migrate() {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Read wrangler.toml
|
||||
const wranglerPath = join(process.cwd(), 'wrangler.toml')
|
||||
// Read wrangler.json
|
||||
const wranglerPath = join(process.cwd(), 'wrangler.json')
|
||||
let wranglerContent: string
|
||||
|
||||
try {
|
||||
wranglerContent = readFileSync(wranglerPath, 'utf-8')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
console.error('Error: wrangler.toml not found')
|
||||
console.error('Error: wrangler.json not found')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Parse wrangler.toml
|
||||
const config = parse(wranglerContent) as unknown as WranglerConfig
|
||||
// Parse wrangler.json
|
||||
const config = JSON.parse(wranglerContent) as WranglerConfig
|
||||
|
||||
if (!config.d1_databases?.[0]?.database_name) {
|
||||
console.error('Error: Database name not found in wrangler.toml')
|
||||
console.error('Error: Database name not found in wrangler.json')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -53,7 +52,7 @@ async function migrate() {
|
||||
console.log('Generating migrations...')
|
||||
await execAsync('drizzle-kit generate')
|
||||
|
||||
// Apply migrations
|
||||
// Applying migrations
|
||||
console.log(`Applying migrations to ${mode} database: ${dbName}`)
|
||||
await execAsync(`wrangler d1 migrations apply ${dbName} --${mode}`)
|
||||
|
||||
@@ -64,4 +63,4 @@ async function migrate() {
|
||||
}
|
||||
}
|
||||
|
||||
migrate()
|
||||
migrate()
|
||||
18
wrangler.cleanup.example.json
Normal file
18
wrangler.cleanup.example.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "cleanup-worker",
|
||||
"main": "workers/cleanup.ts",
|
||||
"compatibility_date": "2024-03-20",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"triggers": {
|
||||
"crons": ["0 * * * *"]
|
||||
},
|
||||
"d1_databases": [
|
||||
{
|
||||
"binding": "DB",
|
||||
"migrations_dir": "drizzle",
|
||||
"database_name": "${DATABASE_NAME}",
|
||||
"database_id": "${DATABASE_ID}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
name = "cleanup-worker"
|
||||
main = "workers/cleanup.ts"
|
||||
compatibility_date = "2024-03-20"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
# 每 1 小时运行一次
|
||||
[triggers]
|
||||
crons = ["0 * * * *"]
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
migrations_dir = "drizzle"
|
||||
database_name = ""
|
||||
database_id = ""
|
||||
15
wrangler.email.example.json
Normal file
15
wrangler.email.example.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "email-receiver-worker",
|
||||
"compatibility_date": "2024-03-20",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"main": "workers/email-receiver.ts",
|
||||
"d1_databases": [
|
||||
{
|
||||
"binding": "DB",
|
||||
"migrations_dir": "drizzle",
|
||||
"database_name": "${DATABASE_NAME}",
|
||||
"database_id": "${DATABASE_ID}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
name = "email-receiver-worker"
|
||||
compatibility_date = "2024-03-20"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
main = "workers/email-receiver.ts"
|
||||
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
migrations_dir = "drizzle"
|
||||
database_name = ""
|
||||
database_id = ""
|
||||
21
wrangler.example.json
Normal file
21
wrangler.example.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "moemail",
|
||||
"compatibility_date": "2024-03-20",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"pages_build_output_dir": ".vercel/output/static",
|
||||
"d1_databases": [
|
||||
{
|
||||
"binding": "DB",
|
||||
"database_name": "${DATABASE_NAME}",
|
||||
"database_id": "${DATABASE_ID}",
|
||||
"migrations_dir": "drizzle"
|
||||
}
|
||||
],
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "SITE_CONFIG",
|
||||
"id": "${KV_NAMESPACE_ID}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
name = "moemail"
|
||||
compatibility_date = "2024-03-20"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
pages_build_output_dir = ".vercel/output/static"
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
migrations_dir = "drizzle"
|
||||
database_name = ""
|
||||
database_id = ""
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "SITE_CONFIG"
|
||||
id = ""
|
||||
Reference in New Issue
Block a user