feat: support text and secret types in Worker deployment variables
This commit is contained in:
1 parent
8fbe63036b
commit
2fd37586a6
4 files changed
+98
-24
No files matched your search
@@ -26,8 +26,17 @@ on:
|
|||||||
# 可选:
|
# 可选:
|
||||||
# R2_BUCKET_NAME - R2 存储桶名称
|
# R2_BUCKET_NAME - R2 存储桶名称
|
||||||
# WORKER_NAME - Worker 名称(默认 cloudflare-imgbed,手动触发时可在界面覆盖)
|
# WORKER_NAME - Worker 名称(默认 cloudflare-imgbed,手动触发时可在界面覆盖)
|
||||||
# WORKER_VARS - JSON 格式的业务环境变量(非必要不建议配置,
|
# WORKER_VARS - JSON 格式的业务环境变量,兼容直接值写法。
|
||||||
# 部署完成后请在管理面板中配置所有设置)
|
# 对象写法支持 value 和可选 type(text/secret,默认 text)。
|
||||||
|
# text 随 Worker 部署,secret 在部署后统一上传为 Cloudflare Secret。
|
||||||
|
# 非必要不建议配置,业务设置请在管理面板中配置。
|
||||||
|
# 示例(请替换密钥,迁移现有配置时保持密钥值不变):
|
||||||
|
# {
|
||||||
|
# "CUSTOM_DOMAIN": "https://img.example.com",
|
||||||
|
# "TG_CHAT_ID": { "value": "123456", "type": "text" },
|
||||||
|
# "AI_CONFIG_SECRET": { "value": "至少32个字符的随机密钥", "type": "secret" },
|
||||||
|
# "TG_BOT_TOKEN": { "value": "你的Bot Token", "type": "secret" }
|
||||||
|
# }
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
@@ -65,6 +74,7 @@ jobs:
|
|||||||
run: node deploy/worker/generate-routes.js
|
run: node deploy/worker/generate-routes.js
|
||||||
|
|
||||||
- name: Generate wrangler config
|
- name: Generate wrangler config
|
||||||
|
id: config
|
||||||
if: steps.check.outputs.skip != 'true'
|
if: steps.check.outputs.skip != 'true'
|
||||||
env:
|
env:
|
||||||
WORKER_NAME: ${{ inputs.worker_name || secrets.WORKER_NAME }}
|
WORKER_NAME: ${{ inputs.worker_name || secrets.WORKER_NAME }}
|
||||||
@@ -82,3 +92,18 @@ jobs:
|
|||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
wranglerVersion: '4'
|
wranglerVersion: '4'
|
||||||
command: deploy --config deploy/worker/wrangler.toml
|
command: deploy --config deploy/worker/wrangler.toml
|
||||||
|
|
||||||
|
- name: Upload Worker secrets
|
||||||
|
if: steps.check.outputs.skip != 'true' && steps.config.outputs.has_secrets == 'true'
|
||||||
|
uses: cloudflare/wrangler-action@v3
|
||||||
|
with:
|
||||||
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
wranglerVersion: '4'
|
||||||
|
command: secret bulk "${{ steps.config.outputs.secrets_path }}" --config deploy/worker/wrangler.toml
|
||||||
|
|
||||||
|
- name: Clean up Worker secrets file
|
||||||
|
if: always() && steps.config.outputs.has_secrets == 'true'
|
||||||
|
env:
|
||||||
|
WORKER_SECRETS_FILE: ${{ steps.config.outputs.secrets_path }}
|
||||||
|
run: rm -f -- "$WORKER_SECRETS_FILE"
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
data/*
|
data/*
|
||||||
node_modules
|
node_modules
|
||||||
/wrangler.toml
|
/wrangler.toml
|
||||||
|
/deploy/worker/worker-secrets.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* 根据环境变量生成 deploy/worker/wrangler.toml
|
* 根据环境变量生成 deploy/worker/wrangler.toml
|
||||||
* 用于 GitHub Actions 部署,从 Secrets/Variables 读取配置
|
* 用于 GitHub Actions 部署,从 GitHub Secrets 读取配置
|
||||||
*
|
*
|
||||||
* 环境变量:
|
* 环境变量:
|
||||||
* WORKER_NAME - Worker 名称(默认 cloudflare-imgbed)
|
* WORKER_NAME - Worker 名称(默认 cloudflare-imgbed)
|
||||||
* D1_DATABASE_ID - D1 数据库 ID
|
* D1_DATABASE_ID - D1 数据库 ID
|
||||||
* KV_NAMESPACE_ID - KV 命名空间 ID
|
* KV_NAMESPACE_ID - KV 命名空间 ID
|
||||||
* R2_BUCKET_NAME - R2 存储桶名称
|
* R2_BUCKET_NAME - R2 存储桶名称
|
||||||
* WORKER_VARS - JSON 格式的业务环境变量
|
* WORKER_VARS - JSON 对象,值可直接填写或使用 { value, type: 'text' | 'secret' }
|
||||||
|
* type 默认 text;secret 单独生成文件供部署后批量上传
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { writeFileSync } from 'fs';
|
import { appendFileSync, writeFileSync } from 'fs';
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
@@ -20,6 +21,45 @@ const outputPath = join(__dirname, 'wrangler.toml');
|
|||||||
const env = process.env;
|
const env = process.env;
|
||||||
const name = env.WORKER_NAME || 'cloudflare-imgbed';
|
const name = env.WORKER_NAME || 'cloudflare-imgbed';
|
||||||
|
|
||||||
|
// 先验证全部配置,避免错误的 secret 配置被降级成普通变量或被静默跳过。
|
||||||
|
const textVars = Object.create(null);
|
||||||
|
const secretVars = Object.create(null);
|
||||||
|
if (env.WORKER_VARS) {
|
||||||
|
let vars;
|
||||||
|
try {
|
||||||
|
vars = JSON.parse(env.WORKER_VARS);
|
||||||
|
} catch {
|
||||||
|
// JSON 解析错误可能包含原始密钥片段,不输出解析器的错误信息。
|
||||||
|
console.error('Error: WORKER_VARS must be a valid JSON object.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (vars === null || typeof vars !== 'object' || Array.isArray(vars)) {
|
||||||
|
console.error('Error: WORKER_VARS must be a JSON object.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
for (const [key, entry] of Object.entries(vars)) {
|
||||||
|
let type = 'text';
|
||||||
|
let value = entry;
|
||||||
|
if (entry !== null && typeof entry === 'object') {
|
||||||
|
if (Array.isArray(entry) || !Object.hasOwn(entry, 'value')) {
|
||||||
|
console.error('Error: Each WORKER_VARS descriptor must contain value and an optional text/secret type.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
type = Object.hasOwn(entry, 'type') ? entry.type : 'text';
|
||||||
|
value = entry.value;
|
||||||
|
}
|
||||||
|
if (type !== 'text' && type !== 'secret') {
|
||||||
|
console.error('Error: WORKER_VARS type must be text or secret.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (value !== null && !['string', 'number', 'boolean'].includes(typeof value)) {
|
||||||
|
console.error('Error: WORKER_VARS value must be a string, number, boolean, or null.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
(type === 'secret' ? secretVars : textVars)[key] = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let toml = `name = "${name}"
|
let toml = `name = "${name}"
|
||||||
main = "index.js"
|
main = "index.js"
|
||||||
compatibility_date = "2024-08-21"
|
compatibility_date = "2024-08-21"
|
||||||
@@ -64,28 +104,30 @@ bucket_name = "${env.R2_BUCKET_NAME}"
|
|||||||
|
|
||||||
toml += `
|
toml += `
|
||||||
[vars]
|
[vars]
|
||||||
# AI_CONFIG_SECRET = "" # 至少 32 个字符的随机密钥,用于加密保存 AI 供应商 API Key
|
# Secret 类型的变量在部署后单独上传,不写入此配置。
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 业务环境变量(从 JSON 解析)
|
// JSON 字符串转义适用于 TOML 基本字符串,另外转义 TOML 禁止的 DEL 字符。
|
||||||
if (env.WORKER_VARS) {
|
const tomlString = (value) => JSON.stringify(value).replace(/\u007f/g, '\\u007f');
|
||||||
try {
|
const configWithoutVars = toml;
|
||||||
const vars = JSON.parse(env.WORKER_VARS);
|
for (const [key, value] of Object.entries(textVars)) {
|
||||||
const entries = Object.entries(vars);
|
toml += `${tomlString(key)} = ${tomlString(value)}\n`;
|
||||||
if (entries.length > 0) {
|
|
||||||
for (const [key, value] of entries) {
|
|
||||||
toml += `${key} = "${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Warning: WORKER_VARS is not valid JSON, skipping:', e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFileSync(outputPath, toml, 'utf8');
|
writeFileSync(outputPath, toml, 'utf8');
|
||||||
|
|
||||||
// 打印配置(隐藏敏感值)
|
const hasSecrets = Object.keys(secretVars).length > 0;
|
||||||
const safeToml = toml
|
let secretsPath;
|
||||||
|
if (hasSecrets) {
|
||||||
|
secretsPath = join(env.RUNNER_TEMP || __dirname, 'worker-secrets.json');
|
||||||
|
writeFileSync(secretsPath, JSON.stringify(secretVars), { encoding: 'utf8', mode: 0o600 });
|
||||||
|
}
|
||||||
|
if (env.GITHUB_OUTPUT) {
|
||||||
|
appendFileSync(env.GITHUB_OUTPUT, `has_secrets=${hasSecrets}\n${hasSecrets ? `secrets_path=${secretsPath}\n` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有业务变量的值都隐藏,避免依赖变量命名规则判断是否敏感。
|
||||||
|
const safeToml = configWithoutVars
|
||||||
.replace(/database_id = ".*"/g, 'database_id = "***"')
|
.replace(/database_id = ".*"/g, 'database_id = "***"')
|
||||||
.replace(/(id = )".*"/g, '$1"***"')
|
.replace(/(id = )".*"/g, '$1"***"')
|
||||||
.replace(/(TOKEN.*= )".*"/gi, '$1"***"')
|
.replace(/(TOKEN.*= )".*"/gi, '$1"***"')
|
||||||
@@ -94,3 +136,7 @@ const safeToml = toml
|
|||||||
|
|
||||||
console.log('Generated deploy/worker/wrangler.toml:');
|
console.log('Generated deploy/worker/wrangler.toml:');
|
||||||
console.log(safeToml);
|
console.log(safeToml);
|
||||||
|
for (const key of Object.keys(textVars)) {
|
||||||
|
console.log(`${JSON.stringify(key)} = "***"`);
|
||||||
|
}
|
||||||
|
console.log(`Prepared ${Object.keys(secretVars).length} secret(s) for upload after deployment.`);
|
||||||
@@ -5,9 +5,11 @@
|
|||||||
# 1. D1 数据库 - 取消注释并填入你的 database_id
|
# 1. D1 数据库 - 取消注释并填入你的 database_id
|
||||||
# 2. R2 存储桶 - 取消注释并填入你的 bucket_name
|
# 2. R2 存储桶 - 取消注释并填入你的 bucket_name
|
||||||
# 3. KV 命名空间 - 如果使用 KV 而非 D1,取消注释并填入 id
|
# 3. KV 命名空间 - 如果使用 KV 而非 D1,取消注释并填入 id
|
||||||
# 4. 环境变量 - 在 [vars] 中配置
|
# 4. 普通环境变量 - 在 [vars] 中配置;敏感值请使用 wrangler secret put/bulk
|
||||||
#
|
#
|
||||||
# GitHub Actions 部署时会自动从 Secrets/Variables 生成此文件,
|
# GitHub Actions 部署时会自动从 GitHub Secrets 生成此文件,
|
||||||
|
# WORKER_VARS 支持 { "变量名": { "value": "值", "type": "text或secret" } },
|
||||||
|
# type 默认为 text,secret 会在部署后统一上传,不写入 [vars]。
|
||||||
# 无需手动编辑。详见 .github/workflows/deploy-worker.yml
|
# 无需手动编辑。详见 .github/workflows/deploy-worker.yml
|
||||||
|
|
||||||
name = "cloudflare-imgbed"
|
name = "cloudflare-imgbed"
|
||||||
@@ -27,7 +29,7 @@ not_found_handling = "single-page-application"
|
|||||||
binding = "IMAGES"
|
binding = "IMAGES"
|
||||||
|
|
||||||
[vars]
|
[vars]
|
||||||
# AI_CONFIG_SECRET = "" # 至少 32 个字符的随机密钥,用于加密保存 AI 供应商 API Key
|
# AI_CONFIG_SECRET 请配置为 Secret,值为至少 32 个字符的随机密钥。
|
||||||
# TG_BOT_TOKEN = ""
|
# TG_BOT_TOKEN = ""
|
||||||
# TG_CHAT_ID = ""
|
# TG_CHAT_ID = ""
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user