mirror of
https://github.com/we-promise/sure.git
synced 2026-06-08 20:29:05 +00:00
ci(preview): render Cloudflare config from trusted template (#2207)
This commit is contained in:
116
.github/workflows/preview-deploy.yml
vendored
116
.github/workflows/preview-deploy.yml
vendored
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
100
test/javascript/preview_deploy/render_preview_config_test.cjs
Normal file
100
test/javascript/preview_deploy/render_preview_config_test.cjs
Normal 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), "");
|
||||
});
|
||||
});
|
||||
114
workers/preview/deploy/render_preview_config.cjs
Normal file
114
workers/preview/deploy/render_preview_config.cjs
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user