ci(preview): render Cloudflare config from trusted template (#2207)

This commit is contained in:
ghost
2026-06-05 21:54:40 -06:00
committed by GitHub
parent 1fd5c2e26d
commit 85d7695d1f
4 changed files with 307 additions and 67 deletions

View File

@@ -206,12 +206,14 @@ jobs:
cp -R trusted/workers/preview/src "$preview_dir/src"
mkdir -p "$preview_dir/deploy"
cp trusted/workers/preview/deploy/redact_preview_log.sh "$preview_dir/deploy/redact_preview_log.sh"
cp trusted/workers/preview/deploy/render_preview_config.cjs "$preview_dir/deploy/render_preview_config.cjs"
chmod 0755 "$preview_dir/deploy/redact_preview_log.sh"
diagnostics_nonce="$(openssl rand -hex 32)"
sed -i "s/\${PR_NUMBER}/${PR_NUMBER}/g" "$preview_dir/wrangler.toml"
sed -i "s/\${PR_NUMBER}/${PR_NUMBER}/g" "$preview_dir/src/index.ts"
sed -i "s/\${PREVIEW_DIAGNOSTICS_NONCE}/${diagnostics_nonce}/g" "$preview_dir/src/index.ts"
cp "$preview_dir/wrangler.toml" "$preview_dir/wrangler.source.toml"
if grep -F "\${PREVIEW_DIAGNOSTICS_NONCE}" "$preview_dir/src/index.ts" >/dev/null; then
echo "Preview diagnostics nonce placeholder was not replaced" >&2
@@ -248,6 +250,7 @@ jobs:
set -euo pipefail
cd "$RUNNER_TEMP/sure-preview-worker"
source_config="$RUNNER_TEMP/sure-preview-worker/wrangler.source.toml"
config_path="$RUNNER_TEMP/sure-preview-worker/wrangler.toml"
image_tag="sure-preview-pr-${PR_NUMBER}:${HEAD_SHA}"
temporary_image_ref="registry.cloudflare.com/${CLOUDFLARE_ACCOUNT_ID}/${image_tag}"
@@ -257,23 +260,8 @@ jobs:
# wrangler containers push validates wrangler.toml, so point the trusted
# config at a registry-shaped ref while it pushes the verified local image.
TEMPORARY_IMAGE_REF="$temporary_image_ref" node - "$config_path" <<'NODE'
const fs = require('node:fs');
const configPath = process.argv[2];
const imageRef = process.env.TEMPORARY_IMAGE_REF;
if (!/^registry\.cloudflare\.com\/[A-Za-z0-9_-]+\/sure-preview-pr-[1-9][0-9]*:[a-f0-9]{40}$/.test(imageRef || '')) {
throw new Error('Expected registry-shaped preview image ref before wrangler containers push');
}
const original = fs.readFileSync(configPath, 'utf8');
const updated = original.replace(/image = "[^"]+"/, `image = ${JSON.stringify(imageRef)}`);
if (updated === original) {
throw new Error('Expected wrangler.toml to contain an image entry to rewrite before push');
}
fs.writeFileSync(configPath, updated);
NODE
PREVIEW_IMAGE_REF="$temporary_image_ref" node ./deploy/render_preview_config.cjs render "$source_config" "$config_path"
cp "$config_path" "$RUNNER_TEMP/wrangler-push.toml"
set +e
./node_modules/.bin/wrangler containers push "$image_tag" 2>&1 | tee "$push_log" | ./deploy/redact_preview_log.sh
@@ -285,19 +273,7 @@ jobs:
exit "$push_status"
fi
image_ref="$(node - "$clean_log" <<'NODE'
const fs = require('node:fs');
const logPath = process.argv[2];
const log = fs.readFileSync(logPath, 'utf8');
const expectedSuffix = `sure-preview-pr-${process.env.PR_NUMBER}:${process.env.HEAD_SHA}`;
const pattern = /registry\.cloudflare\.com\/[A-Za-z0-9_-]+\/sure-preview-pr-[1-9][0-9]*:[a-f0-9]{40}/g;
const matches = [...log.matchAll(pattern)].map((match) => match[0]);
const imageRef = matches.findLast((candidate) => candidate.endsWith(`/${expectedSuffix}`));
if (imageRef) process.stdout.write(imageRef);
NODE
)"
image_ref="$(node ./deploy/render_preview_config.cjs find "$clean_log")"
if [ -z "$image_ref" ]; then
echo "Could not find Cloudflare registry image reference in wrangler output" >&2
@@ -312,30 +288,12 @@ jobs:
run: |
set -euo pipefail
source_config="$RUNNER_TEMP/sure-preview-worker/wrangler.source.toml"
config_path="$RUNNER_TEMP/sure-preview-worker/wrangler.toml"
# Use Node instead of sed so the replacement preserves TOML string syntax.
node - "$config_path" <<'NODE'
const fs = require('node:fs');
const configPath = process.argv[2];
const imageRef = process.env.IMAGE_REF;
const expectedSuffix = `sure-preview-pr-${process.env.PR_NUMBER}:${process.env.HEAD_SHA}`;
if (!/^registry\.cloudflare\.com\/[A-Za-z0-9_-]+\/sure-preview-pr-[1-9][0-9]*:[a-f0-9]{40}$/.test(imageRef || '')) {
throw new Error('Expected a Cloudflare registry image reference');
}
if (!imageRef.endsWith(`/${expectedSuffix}`)) {
throw new Error('Cloudflare registry image reference does not match this preview artifact');
}
const original = fs.readFileSync(configPath, 'utf8');
const updated = original.replace(/image = "[^"]+"/, `image = ${JSON.stringify(imageRef)}`);
if (updated === original) {
throw new Error('Expected wrangler.toml to contain an image entry to rewrite');
}
fs.writeFileSync(configPath, updated);
NODE
# Render from the preserved trusted source template so the push-time
# registry ref cannot make the final deploy rewrite stateful.
PREVIEW_IMAGE_REF="$IMAGE_REF" node "$RUNNER_TEMP/sure-preview-worker/deploy/render_preview_config.cjs" render "$source_config" "$config_path"
cp "$config_path" "$RUNNER_TEMP/wrangler-final.toml"
# Print a redacted copy for logs without mutating the config used by deploy.
redacted_config="$RUNNER_TEMP/wrangler-redacted.toml"
@@ -395,27 +353,66 @@ jobs:
run: |
set -euo pipefail
diagnostics_file="$RUNNER_TEMP/preview-diagnostics.json"
diagnostics_dir="$RUNNER_TEMP/preview-diagnostics"
diagnostics_file="$diagnostics_dir/preview-diagnostics.json"
latest_metrics_file="$diagnostics_dir/latest-metrics.json"
polls_log="$diagnostics_dir/metrics-polls.log"
summary_file="$diagnostics_dir/summary.md"
last_error=""
mkdir -p "$diagnostics_dir"
for attempt in $(seq 1 40); do
if curl -fsS --connect-timeout 5 --max-time 15 "$PREVIEW_URL/_container_status" -o "$diagnostics_file"; then
if jq -e '.previewReady == true or .previewFailed == true' "$diagnostics_file" >/dev/null; then
break
if jq -e . "$diagnostics_file" >/dev/null 2>&1; then
jq -c --argjson attempt "$attempt" --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{attempt: $attempt, at: $at, previewReady: (.previewReady // false), previewFailed: (.previewFailed // false), progress: (.progress // {}), timings: (.timings // {})}' \
"$diagnostics_file" >> "$polls_log"
jq '{previewReady: (.previewReady // false), previewFailed: (.previewFailed // false), progress: (.progress // {}), timings: (.timings // {})}' "$diagnostics_file" > "$latest_metrics_file"
if jq -e '.previewReady == true or .previewFailed == true' "$diagnostics_file" >/dev/null; then
break
fi
else
last_error="invalid diagnostics JSON on attempt ${attempt}"
raw_snippet="$(head -c 2048 "$diagnostics_file")"
latest_metrics_snapshot="none"
if [ -f "$latest_metrics_file" ]; then
latest_metrics_snapshot="$(head -c 2048 "$latest_metrics_file")"
fi
jq -nc --argjson attempt "$attempt" --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg error "$last_error" --arg latestMetrics "$latest_metrics_snapshot" --arg rawSnippet "$raw_snippet" \
'{attempt: $attempt, at: $at, error: $error, latestMetrics: $latestMetrics, rawSnippet: $rawSnippet}' >> "$polls_log"
fi
else
last_error="curl failed on attempt ${attempt}"
jq -nc --argjson attempt "$attempt" --arg at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg error "$last_error" \
'{attempt: $attempt, at: $at, error: $error}' >> "$polls_log"
fi
sleep 3
done
if [ ! -s "$diagnostics_file" ]; then
if [ ! -s "$diagnostics_file" ] || ! jq -e . "$diagnostics_file" >/dev/null 2>&1; then
jq -n --arg error "${last_error:-preview diagnostics unavailable}" \
--arg url "$PREVIEW_URL" \
'{previewReady: false, previewFailed: false, error: $error, previewUrl: $url}' > "$diagnostics_file"
fi
jq '{previewReady: (.previewReady // false), previewFailed: (.previewFailed // false), progress: (.progress // {}), timings: (.timings // {}), error: (.error // null)}' "$diagnostics_file" > "$latest_metrics_file"
{
echo "# Preview diagnostics"
echo
echo "- PR: ${PR_NUMBER}"
echo "- Commit: ${HEAD_SHA}"
echo "- Preview URL: ${PREVIEW_URL}"
echo "- Preview ready: $(jq -r '.previewReady // false' "$diagnostics_file")"
echo "- Preview failed: $(jq -r '.previewFailed // false' "$diagnostics_file")"
echo "- Phase: $(jq -r '.progress.phase // "unknown"' "$diagnostics_file")"
echo "- Stage: $(jq -r '.progress.stage // "unknown"' "$diagnostics_file")"
echo "- Seconds to Rails ready: $(jq -r '.timings.secondsToRailsReady // "unknown"' "$diagnostics_file")"
echo "- Seconds to demo data ready: $(jq -r '.timings.secondsToDemoDataReady // "unknown"' "$diagnostics_file")"
echo "- Seconds to preview ready: $(jq -r '.timings.secondsToPreviewReady // "unknown"' "$diagnostics_file")"
} > "$summary_file"
jq -c . "$diagnostics_file"
if jq -e '.previewFailed == true' "$diagnostics_file" >/dev/null; then
@@ -441,7 +438,7 @@ jobs:
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: preview-diagnostics-pr-${{ env.PR_NUMBER }}-${{ env.HEAD_SHA }}
path: ${{ runner.temp }}/preview-diagnostics.json
path: ${{ runner.temp }}/preview-diagnostics
if-no-files-found: error
retention-days: 3
@@ -493,6 +490,9 @@ jobs:
}' "$manifest_file" > "$diagnostics_dir/preview-image-manifest.json"
fi
sanitize_copy "$RUNNER_TEMP/sure-preview-worker/wrangler.source.toml" "$diagnostics_dir/wrangler-source.toml"
sanitize_copy "$RUNNER_TEMP/wrangler-push.toml" "$diagnostics_dir/wrangler-push.toml"
sanitize_copy "$RUNNER_TEMP/wrangler-final.toml" "$diagnostics_dir/wrangler-final.toml"
sanitize_copy "$RUNNER_TEMP/sure-preview-worker/wrangler.toml" "$diagnostics_dir/wrangler.toml"
sanitize_copy "$RUNNER_TEMP/wrangler-containers-push.clean.log" "$diagnostics_dir/wrangler-containers-push.log"
if [ -f "$RUNNER_TEMP/wrangler-deploy.clean.log" ]; then

View File

@@ -8,6 +8,7 @@ PREVIEW_WORKFLOW_PATH = File.join(ROOT, ".github/workflows/preview-deploy.yml")
PR_WORKFLOW_PATH = File.join(ROOT, ".github/workflows/pr.yml")
LOCKFILE_PATH = File.join(ROOT, "workers/preview/package-lock.json")
RESOLVER_PATH = File.join(ROOT, "workers/preview/deploy/resolve_preview_request.cjs")
CONFIG_RENDERER_PATH = File.join(ROOT, "workers/preview/deploy/render_preview_config.cjs")
REDACTION_HELPER_PATH = File.join(ROOT, "workers/preview/deploy/redact_preview_log.sh")
PREVIEW_WORKER_PATH = File.join(ROOT, "workers/preview/src/index.ts")
PREVIEW_DOCKERFILE_PATH = File.join(ROOT, "Dockerfile.preview")
@@ -52,6 +53,7 @@ EXPECTED_COMMENT_PERMISSIONS = {
}.freeze
EXPECTED_DEPLOY_SECRET_ENV = %w[CLOUDFLARE_ACCOUNT_ID CLOUDFLARE_API_TOKEN CLOUDFLARE_WORKERS_SUBDOMAIN].freeze
EXPECTED_PUSH_SECRET_ENV = %w[CLOUDFLARE_ACCOUNT_ID CLOUDFLARE_API_TOKEN].freeze
EXPECTED_DIAGNOSTICS_PATH = "${{ runner.temp }}/preview-diagnostics"
EXPECTED_FAILURE_DIAGNOSTICS_PATH = "${{ runner.temp }}/preview-failure-diagnostics"
EXPECTED_CLEANUP_METADATA_PATH = "${{ runner.temp }}/preview-cleanup-metadata/wrangler.toml"
REQUIRED_PREPARE_LINES = [
@@ -62,9 +64,11 @@ REQUIRED_PREPARE_LINES = [
'cp -R trusted/workers/preview/src "$preview_dir/src"',
'mkdir -p "$preview_dir/deploy"',
'cp trusted/workers/preview/deploy/redact_preview_log.sh "$preview_dir/deploy/redact_preview_log.sh"',
'cp trusted/workers/preview/deploy/render_preview_config.cjs "$preview_dir/deploy/render_preview_config.cjs"',
'chmod 0755 "$preview_dir/deploy/redact_preview_log.sh"',
'diagnostics_nonce="$(openssl rand -hex 32)"',
'sed -i "s/\${PREVIEW_DIAGNOSTICS_NONCE}/${diagnostics_nonce}/g" "$preview_dir/src/index.ts"',
'cp "$preview_dir/wrangler.toml" "$preview_dir/wrangler.source.toml"',
"Preview diagnostics nonce placeholder was not replaced",
"npm ci --ignore-scripts --no-audit --no-fund"
].freeze
@@ -160,6 +164,7 @@ preview_workflow = YAML.safe_load_file(PREVIEW_WORKFLOW_PATH, aliases: true)
pr_workflow = YAML.safe_load_file(PR_WORKFLOW_PATH, aliases: true)
lockfile = JSON.parse(File.read(LOCKFILE_PATH))
resolver_script = File.read(RESOLVER_PATH)
config_renderer_script = File.read(CONFIG_RENDERER_PATH)
redaction_helper_script = File.read(REDACTION_HELPER_PATH)
preview_worker_script = File.read(PREVIEW_WORKER_PATH)
preview_dockerfile = File.read(PREVIEW_DOCKERFILE_PATH)
@@ -282,7 +287,7 @@ comment_on_pr = step!(preview_comment_steps, "Comment on PR")
[ "fork deployment record guard", create_deployment.fetch("if"), "env.IS_FORK == 'false'" ],
[ "diagnostics upload if", upload_diagnostics.fetch("if"), "always() && steps.deploy.outputs.preview_url != ''" ],
[ "diagnostics upload name", upload_diagnostics.dig("with", "name"), "preview-diagnostics-pr-${{ env.PR_NUMBER }}-${{ env.HEAD_SHA }}" ],
[ "diagnostics upload path", upload_diagnostics.dig("with", "path"), "${{ runner.temp }}/preview-diagnostics.json" ],
[ "diagnostics upload path", upload_diagnostics.dig("with", "path"), EXPECTED_DIAGNOSTICS_PATH ],
[ "diagnostics upload retention", upload_diagnostics.dig("with", "retention-days"), 3 ],
[ "failure diagnostics collect if", collect_failure_diagnostics.fetch("if"), "failure()" ],
[ "failure diagnostics upload if", upload_failure_diagnostics.fetch("if"), "failure()" ],
@@ -423,6 +428,18 @@ assert(File.executable?(REDACTION_HELPER_PATH), "preview log redaction helper mu
"<redacted-account>"
].each { |needle| assert(redaction_helper_script.include?(needle), "preview log redaction helper must include #{needle.inspect}") }
[
"REGISTRY_IMAGE_REF_PATTERN",
"REGISTRY_IMAGE_REF_SCAN_PATTERN",
"function validateRegistryImageRef",
"function renderPreviewConfig",
"function findRegistryImageRef",
"Expected wrangler.toml source to contain exactly one image entry",
"Cloudflare registry image reference does not match this preview artifact",
"Cloudflare registry image reference account does not match this workflow",
"module.exports"
].each { |needle| assert(config_renderer_script.include?(needle), "preview config renderer must include #{needle.inspect}") }
prepare_run = assert_run_includes(prepare, *REQUIRED_PREPARE_LINES)
assert(!prepare_run.include?("npm install"), "prepare step must not use npm install")
assert(!prepare_run.include?("CLOUDFLARE_API_TOKEN"), "prepare step must not receive Cloudflare secrets")
@@ -440,29 +457,38 @@ push_image_run = assert_run_includes(
push_image,
"./node_modules/.bin/wrangler containers push",
"registry.cloudflare.com/${CLOUDFLARE_ACCOUNT_ID}/${image_tag}",
"registry\\.cloudflare\\.com\\/",
"image_ref=",
'source_config="$RUNNER_TEMP/sure-preview-worker/wrangler.source.toml"',
'config_path="$RUNNER_TEMP/sure-preview-worker/wrangler.toml"',
'temporary_image_ref="registry.cloudflare.com/${CLOUDFLARE_ACCOUNT_ID}/${image_tag}"',
'TEMPORARY_IMAGE_REF="$temporary_image_ref" node - "$config_path"',
"Expected registry-shaped preview image ref before wrangler containers push",
"Expected wrangler.toml to contain an image entry to rewrite before push"
'PREVIEW_IMAGE_REF="$temporary_image_ref" node ./deploy/render_preview_config.cjs render "$source_config" "$config_path"',
'cp "$config_path" "$RUNNER_TEMP/wrangler-push.toml"',
'image_ref="$(node ./deploy/render_preview_config.cjs find "$clean_log")"'
)
push_rewrite_index = push_image_run.index('TEMPORARY_IMAGE_REF="$temporary_image_ref" node - "$config_path"')
push_rewrite_index = push_image_run.index('PREVIEW_IMAGE_REF="$temporary_image_ref" node ./deploy/render_preview_config.cjs render "$source_config" "$config_path"')
push_command_index = push_image_run.index("./node_modules/.bin/wrangler containers push")
assert(
push_rewrite_index < push_command_index,
"push step must rewrite wrangler.toml to a registry-shaped image ref before wrangler validates it"
)
assert(push_image_run.index('wrangler.source.toml') < push_rewrite_index, "push step must render from the preserved trusted source config")
assert(!push_image_run.include?("LOCAL_IMAGE_TAG"), "push step must not write a local Docker tag into wrangler.toml")
assert(!push_image_run.include?("Expected local preview image tag"), "push step must not accept local Docker tags as wrangler config image refs")
assert_run_includes(push_image, 'tee "$push_log" | ./deploy/redact_preview_log.sh', "push_status=${PIPESTATUS[0]}")
assert_run_includes(configure_image, "registry\\.cloudflare\\.com", "expectedSuffix", "imageRef.endsWith", "Cloudflare registry image reference does not match this preview artifact", 'const original = fs.readFileSync', 'const updated = original.replace(/image = "[^"]+"/', "updated === original", "Expected wrangler.toml to contain an image entry to rewrite", "JSON.stringify(imageRef)", 'redact_preview_log.sh" < "$config_path"')
configure_image_run = assert_run_includes(
configure_image,
'source_config="$RUNNER_TEMP/sure-preview-worker/wrangler.source.toml"',
'PREVIEW_IMAGE_REF="$IMAGE_REF" node "$RUNNER_TEMP/sure-preview-worker/deploy/render_preview_config.cjs" render "$source_config" "$config_path"',
'cp "$config_path" "$RUNNER_TEMP/wrangler-final.toml"',
"preserved trusted source template",
'redact_preview_log.sh" < "$config_path"'
)
assert(!configure_image_run.include?('const updated = original.replace(/image = "[^"]+"/'), "final image configuration must use the tested renderer instead of inline regex replacement")
assert_run_includes(create_deployment, "github.rest.repos.createDeployment", "ref: headSha", "preview-pr-${prNumber}")
assert_run_includes(deploy, 'cd "$RUNNER_TEMP/sure-preview-worker"', "deploy_once()", "./node_modules/.bin/wrangler deploy --config wrangler.toml", '--var "PR_NUMBER:${PR_NUMBER}"', 'tee "$deploy_log" | ./deploy/redact_preview_log.sh', "deploy_status=${PIPESTATUS[0]}", "associated with a different durable object namespace", 'if ! ./node_modules/.bin/wrangler delete --name "sure-preview-${PR_NUMBER}" --force', "Preview Worker delete failed", "retrying once")
assert_run_includes(warm_preview, "$PREVIEW_URL/_container_status", "--connect-timeout 5", "--max-time 15")
assert_run_includes(collect_diagnostics, "$PREVIEW_URL/_container_status", "--connect-timeout 5", "--max-time 15", "seq 1 40", "preview-diagnostics.json", "jq -e '.previewReady == true or .previewFailed == true'", "jq -e '.previewFailed == true'", "Preview diagnostics from _container_status reported previewFailed=true", "jq -e '.previewReady == true'", "Preview diagnostics from _container_status did not reach previewReady=true", ".timings.previewReadyAt != null and .timings.secondsToPreviewReady != null", "Preview diagnostics are missing readiness timing fields", "exit 1")
assert_run_includes(collect_failure_diagnostics, "preview-failure-diagnostics", "preview-request.json", "preview-image-manifest.json", "wrangler.toml", "wrangler-containers-push.log", "wrangler-deploy.log", "redaction_helper=", 'sanitize_copy "$RUNNER_TEMP/sure-preview-worker/wrangler.toml"', "wrangler-deploy.clean.log", "resolutionSource")
assert_run_includes(collect_diagnostics, "$PREVIEW_URL/_container_status", "--connect-timeout 5", "--max-time 15", "seq 1 40", "preview-diagnostics", "preview-diagnostics.json", "latest-metrics.json", "metrics-polls.log", "summary.md", '! jq -e . "$diagnostics_file"', 'raw_snippet="$(head -c 2048 "$diagnostics_file")"', 'latest_metrics_snapshot="$(head -c 2048 "$latest_metrics_file")"', "rawSnippet", "latestMetrics", "jq -e '.previewReady == true or .previewFailed == true'", "jq -e '.previewFailed == true'", "Preview diagnostics from _container_status reported previewFailed=true", "jq -e '.previewReady == true'", "Preview diagnostics from _container_status did not reach previewReady=true", ".timings.previewReadyAt != null and .timings.secondsToPreviewReady != null", "Preview diagnostics are missing readiness timing fields", "exit 1")
assert_run_includes(collect_failure_diagnostics, "preview-failure-diagnostics", "preview-request.json", "preview-image-manifest.json", "wrangler-source.toml", "wrangler-push.toml", "wrangler-final.toml", "wrangler.toml", "wrangler-containers-push.log", "wrangler-deploy.log", "redaction_helper=", 'sanitize_copy "$RUNNER_TEMP/sure-preview-worker/wrangler.source.toml"', 'sanitize_copy "$RUNNER_TEMP/wrangler-push.toml"', 'sanitize_copy "$RUNNER_TEMP/wrangler-final.toml"', "wrangler-deploy.clean.log", "resolutionSource")
assert_run_includes(prepare_cleanup_metadata, "preview-cleanup-metadata", "redact_preview_log.sh", "$RUNNER_TEMP/sure-preview-worker/wrangler.toml", "$metadata_dir/wrangler.toml")
assert_run_includes(update_deployment_status, "github.rest.repos.createDeploymentStatus", "process.env.DEPLOY_RESULT === 'success'", "deployment_id: Number(process.env.DEPLOYMENT_ID)")
assert_run_includes(comment_on_pr, "github.rest.issues.listComments", "github.rest.issues.updateComment", "github.rest.issues.createComment", "Preview Deployment Ready")

View File

@@ -0,0 +1,100 @@
const assert = require("node:assert/strict");
const { describe, it } = require("node:test");
const {
findRegistryImageRef,
renderPreviewConfig,
validateRegistryImageRef,
} = require("../../../workers/preview/deploy/render_preview_config.cjs");
const options = {
accountId: "account_123",
prNumber: "2160",
headSha: "3f013c4d9193ff111295c89a6f833d59bd69d91e",
};
const imageRef =
"registry.cloudflare.com/account_123/sure-preview-pr-2160:3f013c4d9193ff111295c89a6f833d59bd69d91e";
describe("renderPreviewConfig", () => {
it("renders exactly one trusted TOML image entry to a registry reference", () => {
const source = [
'name = "sure-preview-2160"',
"",
"[[containers]]",
'image = "../../Dockerfile.preview"',
'class_name = "RailsContainer"',
"",
].join("\n");
const rendered = renderPreviewConfig(source, imageRef, options);
assert.ok(rendered.includes(`image = "${imageRef}"`));
assert.doesNotMatch(rendered, /Dockerfile\.preview/);
});
it("rejects missing image entries", () => {
assert.throws(
() => renderPreviewConfig('name = "sure-preview-2160"\n', imageRef, options),
/contain an image entry/
);
});
it("rejects duplicate image entries", () => {
const source = [
"[[containers]]",
'image = "../../Dockerfile.preview"',
"",
"[[containers]]",
'image = "../../OtherDockerfile"',
"",
].join("\n");
assert.throws(() => renderPreviewConfig(source, imageRef, options), /exactly one image entry/);
});
it("rejects local Docker tags as deploy image refs", () => {
assert.throws(
() => renderPreviewConfig('image = "../../Dockerfile.preview"\n', "my-local-image:latest", options),
/Cloudflare registry image reference/
);
});
});
describe("validateRegistryImageRef", () => {
it("accepts the expected registry ref", () => {
assert.equal(validateRegistryImageRef(imageRef, options), imageRef);
});
it("rejects registry refs for another PR", () => {
const wrongPr =
"registry.cloudflare.com/account_123/sure-preview-pr-2161:3f013c4d9193ff111295c89a6f833d59bd69d91e";
assert.throws(() => validateRegistryImageRef(wrongPr, options), /does not match this preview artifact/);
});
it("rejects registry refs for another account", () => {
const wrongAccount =
"registry.cloudflare.com/account_456/sure-preview-pr-2160:3f013c4d9193ff111295c89a6f833d59bd69d91e";
assert.throws(() => validateRegistryImageRef(wrongAccount, options), /account does not match/);
});
});
describe("findRegistryImageRef", () => {
it("extracts the expected registry image ref from wrangler output", () => {
const log = [
"Pushing image layers",
"Published registry.cloudflare.com/account_123/sure-preview-pr-2160:3f013c4d9193ff111295c89a6f833d59bd69d91e",
"Done",
].join("\n");
assert.equal(findRegistryImageRef(log, options), imageRef);
});
it("ignores registry refs that do not match this preview artifact", () => {
const log =
"Published registry.cloudflare.com/account_123/sure-preview-pr-2161:3f013c4d9193ff111295c89a6f833d59bd69d91e";
assert.equal(findRegistryImageRef(log, options), "");
});
});

View File

@@ -0,0 +1,114 @@
const fs = require("node:fs");
const IMAGE_FIELD_PATTERN = /^(\s*image\s*=\s*)"([^"]*)"(\s*(?:#.*)?)$/gm;
const REGISTRY_IMAGE_REF_PATTERN =
/^registry\.cloudflare\.com\/([A-Za-z0-9_-]+)\/(sure-preview-pr-([1-9][0-9]*):([a-f0-9]{40}))$/;
const REGISTRY_IMAGE_REF_SCAN_PATTERN =
/registry\.cloudflare\.com\/[A-Za-z0-9_-]+\/sure-preview-pr-[1-9][0-9]*:[a-f0-9]{40}/g;
function expectedImageTag({ prNumber, headSha }) {
if (!/^[1-9][0-9]*$/.test(String(prNumber || ""))) {
throw new Error("Expected a numeric preview PR number");
}
if (!/^[a-f0-9]{40}$/.test(String(headSha || ""))) {
throw new Error("Expected a 40-character preview head SHA");
}
return `sure-preview-pr-${prNumber}:${headSha}`;
}
function validateRegistryImageRef(imageRef, { accountId, prNumber, headSha }) {
const match = REGISTRY_IMAGE_REF_PATTERN.exec(imageRef || "");
if (!match) {
throw new Error("Expected a Cloudflare registry image reference");
}
const expectedTag = expectedImageTag({ prNumber, headSha });
if (match[2] !== expectedTag) {
throw new Error("Cloudflare registry image reference does not match this preview artifact");
}
if (accountId && match[1] !== accountId) {
throw new Error("Cloudflare registry image reference account does not match this workflow");
}
return imageRef;
}
function renderPreviewConfig(source, imageRef, options) {
validateRegistryImageRef(imageRef, options);
const matches = [...source.matchAll(IMAGE_FIELD_PATTERN)];
if (matches.length === 0) {
throw new Error("Expected wrangler.toml source to contain an image entry");
}
if (matches.length > 1) {
throw new Error("Expected wrangler.toml source to contain exactly one image entry");
}
return source.replace(IMAGE_FIELD_PATTERN, `$1${JSON.stringify(imageRef)}$3`);
}
function findRegistryImageRef(log, options) {
const matches = [...new Set(log.match(REGISTRY_IMAGE_REF_SCAN_PATTERN) || [])];
const matchedRef = matches.find((candidate) => {
try {
validateRegistryImageRef(candidate, options);
return true;
} catch {
return false;
}
});
return matchedRef || "";
}
function envOptions() {
return {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
prNumber: process.env.PR_NUMBER,
headSha: process.env.HEAD_SHA,
};
}
function runCli() {
const command = process.argv[2];
if (command === "render") {
const sourcePath = process.argv[3];
const destinationPath = process.argv[4];
const imageRef = process.env.PREVIEW_IMAGE_REF;
if (!sourcePath || !destinationPath) {
throw new Error("Usage: render_preview_config.cjs render <source> <destination>");
}
const rendered = renderPreviewConfig(fs.readFileSync(sourcePath, "utf8"), imageRef, envOptions());
fs.writeFileSync(destinationPath, rendered);
return;
}
if (command === "find") {
const logPath = process.argv[3];
if (!logPath) {
throw new Error("Usage: render_preview_config.cjs find <wrangler-log>");
}
process.stdout.write(findRegistryImageRef(fs.readFileSync(logPath, "utf8"), envOptions()));
return;
}
throw new Error(`Unknown command ${command || ""}`);
}
if (require.main === module) {
runCli();
}
module.exports = {
findRegistryImageRef,
renderPreviewConfig,
validateRegistryImageRef,
};