feat: support text and secret types in Worker deployment variables

This commit is contained in:
MarSeventh committed 2026-10-01 10:28:17 +08:00
1 parent 8fbe63036b
commit 2fd37586a6
4 files changed
+98 -24

No files matched your search

+27 -2
View File
@@ -26,8 +26,17 @@ on:
# 可选:
# R2_BUCKET_NAME - R2 存储桶名称
# 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:
deploy:
@@ -65,6 +74,7 @@ jobs:
run: node deploy/worker/generate-routes.js
- name: Generate wrangler config
id: config
if: steps.check.outputs.skip != 'true'
env:
WORKER_NAME: ${{ inputs.worker_name || secrets.WORKER_NAME }}
@@ -82,3 +92,18 @@ jobs:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: '4'
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"
+1
View File
@@ -2,6 +2,7 @@
data/*
node_modules
/wrangler.toml
/deploy/worker/worker-secrets.json
.DS_Store
*.sqlite
+65 -19
View File
@@ -1,16 +1,17 @@
/**
* 根据环境变量生成 deploy/worker/wrangler.toml
* 用于 GitHub Actions 部署,从 Secrets/Variables 读取配置
* 用于 GitHub Actions 部署,从 GitHub Secrets 读取配置
*
* 环境变量:
* WORKER_NAME - Worker 名称(默认 cloudflare-imgbed)
* D1_DATABASE_ID - D1 数据库 ID
* KV_NAMESPACE_ID - KV 命名空间 ID
* 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 { fileURLToPath } from 'url';
@@ -20,6 +21,45 @@ const outputPath = join(__dirname, 'wrangler.toml');
const env = process.env;
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}"
main = "index.js"
compatibility_date = "2024-08-21"
@@ -64,28 +104,30 @@ bucket_name = "${env.R2_BUCKET_NAME}"
toml += `
[vars]
# AI_CONFIG_SECRET = "" # 至少 32 个字符的随机密钥,用于加密保存 AI 供应商 API Key
# Secret 类型的变量在部署后单独上传,不写入此配置。
`;
// 业务环境变量(从 JSON 解析)
if (env.WORKER_VARS) {
try {
const vars = JSON.parse(env.WORKER_VARS);
const entries = Object.entries(vars);
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);
}
// JSON 字符串转义适用于 TOML 基本字符串,另外转义 TOML 禁止的 DEL 字符。
const tomlString = (value) => JSON.stringify(value).replace(/\u007f/g, '\\u007f');
const configWithoutVars = toml;
for (const [key, value] of Object.entries(textVars)) {
toml += `${tomlString(key)} = ${tomlString(value)}\n`;
}
writeFileSync(outputPath, toml, 'utf8');
// 打印配置(隐藏敏感值)
const safeToml = toml
const hasSecrets = Object.keys(secretVars).length > 0;
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(/(id = )".*"/g, '$1"***"')
.replace(/(TOKEN.*= )".*"/gi, '$1"***"')
@@ -94,3 +136,7 @@ const safeToml = toml
console.log('Generated deploy/worker/wrangler.toml:');
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 -3
View File
@@ -5,9 +5,11 @@
# 1. D1 数据库 - 取消注释并填入你的 database_id
# 2. R2 存储桶 - 取消注释并填入你的 bucket_name
# 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
name = "cloudflare-imgbed"
@@ -27,7 +29,7 @@ not_found_handling = "single-page-application"
binding = "IMAGES"
[vars]
# AI_CONFIG_SECRET = "" # 至少 32 个字符的随机密钥,用于加密保存 AI 供应商 API Key
# AI_CONFIG_SECRET 请配置为 Secret,值为至少 32 个字符的随机密钥。
# TG_BOT_TOKEN = ""
# TG_CHAT_ID = ""