Skip to content

Commit 7ecf979

Browse files
committed
feat(cloudflare): inject build-time secrets into wrangler image_vars
Adds scripts/cloudflare-build.sh — a deploy wrapper that: 1. Reads R2 + Turbo secrets from the Cloudflare Workers Build env (the dashboard's 'Build configuration → Variables and Secrets'). 2. Renders a wrangler.deploy.jsonc by overlaying those secrets onto wrangler.jsonc's image_vars block. 3. Hands off to 'npx wrangler deploy --config wrangler.deploy.jsonc'. 4. Cleans up the rendered file in an EXIT trap so secrets don't outlive the build. Why this is needed: - wrangler image_vars passes string values literally — there is no ${VAR} interpolation against the build env. - Cloudflare Workers Builds does NOT auto-forward dashboard env vars to docker --build-arg. They're only available to the build script itself ('npx wrangler deploy'). - So the only way to surface dashboard secrets to the docker build is to compose a wrangler config at build time that has the values inlined into image_vars. Setup needed in Cloudflare dashboard: - Workers & Pages → primalprinting → Settings → Build configuration: Build command: pnpm install --frozen-lockfile && bash scripts/cloudflare-build.sh Deploy command: (leave empty — script handles the deploy) - Variables and Secrets — keep the existing Secrets: R2_S3_ENDPOINT, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, TURBO_TOKEN The rendered wrangler.deploy.jsonc is gitignored (would contain plaintext secrets if accidentally committed). Locally verified: - Missing required vars → script exits 1 with a clear error message. - All vars set → image_vars correctly merged, secrets masked in logs.
1 parent bd4710d commit 7ecf979

2 files changed

Lines changed: 159 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ terraform/crash.log
5555

5656
# Cloudflare
5757
.wrangler
58+
# Build-time rendered config — generated by scripts/cloudflare-build.sh,
59+
# contains plaintext secrets, must never be committed.
60+
wrangler.deploy.jsonc
5861

5962
# Turbo
6063
.turbo

scripts/cloudflare-build.sh

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env bash
2+
# scripts/cloudflare-build.sh
3+
#
4+
# Cloudflare Workers Builds entrypoint.
5+
#
6+
# Why this script exists:
7+
# - The Dockerfile build step needs build-time access to R2 credentials
8+
# and a Turborepo Remote Cache token so it can:
9+
# 1. Mirror static assets to R2 (`pnpm build:headless`)
10+
# 2. Hit the remote build cache (Turbo)
11+
# - These are sensitive values that must not be committed to the repo.
12+
# - wrangler's `image_vars` field passes string values *literally* (no
13+
# `${VAR}` interpolation against the build env), and Cloudflare does
14+
# not auto-forward dashboard env vars as docker `--build-arg`s either.
15+
# - So we need a build wrapper that reads secrets from the build env
16+
# (Cloudflare dashboard → Build configuration → Variables and Secrets)
17+
# and merges them into `image_vars` at deploy time, in memory.
18+
#
19+
# How it works:
20+
# 1. Validate the required secret env vars are present.
21+
# 2. Generate a deploy-time wrangler config by overlaying the secrets
22+
# onto wrangler.jsonc's `image_vars`.
23+
# 3. Hand off to `wrangler deploy` with the temporary config.
24+
# 4. Clean up the temp file in a trap so secrets don't outlive the build.
25+
#
26+
# Required env vars (set as **Secrets** in Cloudflare dashboard →
27+
# Workers → primalprinting → Settings → Build configuration →
28+
# Variables and Secrets):
29+
# - R2_S3_ENDPOINT
30+
# - R2_ACCESS_KEY_ID
31+
# - R2_SECRET_ACCESS_KEY
32+
#
33+
# Optional:
34+
# - TURBO_TOKEN (enables Turborepo Remote Cache; build still works without)
35+
36+
set -euo pipefail
37+
38+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39+
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
40+
SOURCE_CONFIG="${REPO_ROOT}/wrangler.jsonc"
41+
DEPLOY_CONFIG="${REPO_ROOT}/wrangler.deploy.jsonc"
42+
43+
cleanup() {
44+
# Always wipe the rendered config — it contains plaintext secrets.
45+
rm -f "${DEPLOY_CONFIG}"
46+
}
47+
trap cleanup EXIT INT TERM
48+
49+
REQUIRED_SECRETS=(
50+
"R2_S3_ENDPOINT"
51+
"R2_ACCESS_KEY_ID"
52+
"R2_SECRET_ACCESS_KEY"
53+
)
54+
55+
missing=()
56+
for var in "${REQUIRED_SECRETS[@]}"; do
57+
if [ -z "${!var:-}" ]; then
58+
missing+=("${var}")
59+
fi
60+
done
61+
if [ ${#missing[@]} -gt 0 ]; then
62+
echo "✗ scripts/cloudflare-build.sh: missing required env vars:" >&2
63+
for var in "${missing[@]}"; do
64+
echo " - ${var}" >&2
65+
done
66+
echo "" >&2
67+
echo " Set these as Secrets in Cloudflare dashboard:" >&2
68+
echo " Workers & Pages → primalprinting → Settings →" >&2
69+
echo " Build configuration → Variables and Secrets" >&2
70+
exit 1
71+
fi
72+
73+
# ── Render deploy-time wrangler config ────────────────────────────────────
74+
# Use Node (already on PATH from the build env) to safely merge JSON
75+
# without depending on `jq`. Logic:
76+
# - Read wrangler.jsonc (jsonc-aware: strip /* */ and // comments).
77+
# - Locate the first containers[] entry.
78+
# - Overlay secret values from the build env onto its `image_vars` map.
79+
# - Re-serialize as plain JSON (wrangler accepts both .jsonc and .json).
80+
# - Write with mode 0600 so other build-host users can't read secrets.
81+
echo "→ Rendering deploy-time wrangler config with secrets injected"
82+
83+
SOURCE_CONFIG="${SOURCE_CONFIG}" DEPLOY_CONFIG="${DEPLOY_CONFIG}" node - <<'NODE_SCRIPT'
84+
const fs = require("node:fs");
85+
86+
const SOURCE = process.env.SOURCE_CONFIG;
87+
const DEST = process.env.DEPLOY_CONFIG;
88+
89+
// Minimal JSONC stripper — handles /* */ and // comments while preserving
90+
// strings and backslash escapes. Sufficient for our own config file.
91+
function stripJsonc(input) {
92+
let out = "";
93+
let i = 0;
94+
const n = input.length;
95+
while (i < n) {
96+
const c = input[i];
97+
const next = input[i + 1];
98+
if (c === '"') {
99+
out += c;
100+
i++;
101+
while (i < n) {
102+
const cc = input[i];
103+
out += cc;
104+
if (cc === "\\" && i + 1 < n) { out += input[i + 1]; i += 2; continue; }
105+
if (cc === '"') { i++; break; }
106+
i++;
107+
}
108+
continue;
109+
}
110+
if (c === "/" && next === "/") {
111+
while (i < n && input[i] !== "\n") i++;
112+
continue;
113+
}
114+
if (c === "/" && next === "*") {
115+
i += 2;
116+
while (i < n && !(input[i] === "*" && input[i + 1] === "/")) i++;
117+
i += 2;
118+
continue;
119+
}
120+
out += c;
121+
i++;
122+
}
123+
return out.replace(/,(\s*[}\]])/g, "$1");
124+
}
125+
126+
const config = JSON.parse(stripJsonc(fs.readFileSync(SOURCE, "utf8")));
127+
128+
if (!Array.isArray(config.containers) || config.containers.length === 0) {
129+
console.error("✗ wrangler.jsonc has no containers[] entry to inject into");
130+
process.exit(1);
131+
}
132+
const container = config.containers[0];
133+
container.image_vars = {
134+
...(container.image_vars ?? {}),
135+
R2_S3_ENDPOINT: process.env.R2_S3_ENDPOINT,
136+
R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID,
137+
R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY,
138+
...(process.env.TURBO_TOKEN ? { TURBO_TOKEN: process.env.TURBO_TOKEN } : {}),
139+
};
140+
141+
fs.writeFileSync(DEST, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
142+
143+
const SECRET_KEYS = new Set([
144+
"R2_ACCESS_KEY_ID",
145+
"R2_SECRET_ACCESS_KEY",
146+
"TURBO_TOKEN",
147+
]);
148+
console.log(" • image_vars merged:");
149+
for (const [k, v] of Object.entries(container.image_vars)) {
150+
console.log(" -", k, "=", SECRET_KEYS.has(k) ? (v ? "<set>" : "<unset>") : v);
151+
}
152+
NODE_SCRIPT
153+
154+
# ── Hand off to wrangler ──────────────────────────────────────────────────
155+
echo "→ Running wrangler deploy with rendered config"
156+
exec npx wrangler deploy --config "${DEPLOY_CONFIG}"

0 commit comments

Comments
 (0)