diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 76d7c52a5..779d0dbc3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,9 +22,6 @@ jobs:
- name: Scan for security vulnerabilities in Ruby dependencies
run: bin/brakeman --no-pager
- - name: Validate preview deploy workflow hardening
- run: ruby bin/preview_deploy_security_check.rb
-
scan_js:
runs-on: ubuntu-latest
timeout-minutes: 10
diff --git a/.github/workflows/pipelock.yml b/.github/workflows/pipelock.yml
index d0e7c72cb..2a8feea35 100644
--- a/.github/workflows/pipelock.yml
+++ b/.github/workflows/pipelock.yml
@@ -30,7 +30,3 @@ jobs:
config/locales/views/reports/
docs/hosting/ai.md
app/models/provider/binance.rb
- workers/preview/package-lock.json
- # Preview Dockerfile uses local dev credentials (rails/rails) that are
- # not real secrets; Dockerfile format does not support inline # pipelock:ignore
- Dockerfile.preview
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 5c9e4fd50..a2a7f3da3 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -3,7 +3,7 @@ name: Pull Request
on:
workflow_dispatch:
pull_request:
- types: [opened, synchronize, reopened, labeled]
+ types: [opened, synchronize, reopened]
paths-ignore:
- 'charts/**'
@@ -13,72 +13,3 @@ permissions:
jobs:
ci:
uses: ./.github/workflows/ci.yml
-
- preview_image:
- needs: ci
- if: |
- contains(github.event.pull_request.labels.*.name, 'preview-cf') &&
- (github.event.action != 'labeled' || github.event.label.name == 'preview-cf')
- name: Build Cloudflare preview image
- runs-on: ubuntu-latest
- timeout-minutes: 30
- permissions:
- contents: read
- env:
- PR_NUMBER: ${{ github.event.pull_request.number }}
- HEAD_SHA: ${{ github.event.pull_request.head.sha }}
- IMAGE_TAG: sure-preview-pr-${{ github.event.pull_request.number }}:${{ github.event.pull_request.head.sha }}
- steps:
- - name: Checkout PR code
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- with:
- persist-credentials: false
-
- - name: Build preview image without secrets
- run: |
- set -euo pipefail
-
- image_archive="$RUNNER_TEMP/sure-preview-image.tar.gz"
- manifest_file="$RUNNER_TEMP/sure-preview-image.manifest.json"
-
- docker build \
- --platform linux/amd64 \
- --build-arg "BUILD_COMMIT_SHA=${HEAD_SHA}" \
- -f Dockerfile.preview \
- -t "${IMAGE_TAG}" \
- .
-
- docker image inspect "${IMAGE_TAG}" >/dev/null
- docker save "${IMAGE_TAG}" | gzip -1 > "$image_archive"
- archive_sha256="$(sha256sum "$image_archive" | awk '{print $1}')"
- image_id="$(docker image inspect --format '{{.Id}}' "${IMAGE_TAG}")"
-
- printf '%s\n' "$archive_sha256" > "$RUNNER_TEMP/sure-preview-image.sha256"
- ARCHIVE_SHA256="$archive_sha256" IMAGE_ID="$image_id" node - "$manifest_file" <<'NODE'
- const fs = require('node:fs');
-
- const manifestPath = process.argv[2];
- const manifest = {
- artifactVersion: 1,
- archivePath: 'sure-preview-image.tar.gz',
- archiveSha256: process.env.ARCHIVE_SHA256,
- headSha: process.env.HEAD_SHA,
- imageId: process.env.IMAGE_ID,
- imageTag: process.env.IMAGE_TAG,
- prNumber: process.env.PR_NUMBER,
- };
-
- fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
- NODE
- jq -e . "$manifest_file" >/dev/null
-
- - name: Upload preview image artifact
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
- with:
- name: preview-image-pr-${{ env.PR_NUMBER }}-${{ env.HEAD_SHA }}
- path: |
- ${{ runner.temp }}/sure-preview-image.tar.gz
- ${{ runner.temp }}/sure-preview-image.sha256
- ${{ runner.temp }}/sure-preview-image.manifest.json
- if-no-files-found: error
- retention-days: 3
diff --git a/.github/workflows/preview-cleanup.yml b/.github/workflows/preview-cleanup.yml
deleted file mode 100644
index 052708868..000000000
--- a/.github/workflows/preview-cleanup.yml
+++ /dev/null
@@ -1,249 +0,0 @@
-name: Cleanup PR Previews
-
-on:
- # Run hourly to check for expired previews
- schedule:
- - cron: '0 * * * *'
-
- # Immediately cleanup when PR is closed
- pull_request:
- types: [closed, unlabeled]
-
- # Allow manual trigger
- workflow_dispatch:
- inputs:
- pr_number:
- description: 'PR number to cleanup (optional, cleans all expired if empty)'
- required: false
- type: string
-
-permissions:
- contents: read
- deployments: write
-
-jobs:
- cleanup-on-close:
- name: Cleanup closed PR preview
- if: |
- github.event_name == 'pull_request' &&
- (
- (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview-cf')) ||
- (github.event.action == 'unlabeled' && github.event.label.name == 'preview-cf')
- )
- runs-on: ubuntu-latest
- timeout-minutes: 10
-
- steps:
- - name: Checkout code
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- with:
- persist-credentials: false
-
- - name: Delete preview Worker
- env:
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PREVIEW_API_TOKEN || secrets.CLOUDFLARE_API_TOKEN }}
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- run: |
- set -euo pipefail
-
- WORKER_NAME="sure-preview-${{ github.event.pull_request.number }}"
- echo "Deleting Worker: $WORKER_NAME"
-
- if [ -z "${CLOUDFLARE_API_TOKEN:-}" ] || [ -z "${CLOUDFLARE_ACCOUNT_ID:-}" ]; then
- echo "Missing Cloudflare credentials; refusing to mark preview cleanup as successful"
- exit 1
- fi
-
- # Delete the worker (this also stops any running containers)
- response_file="$(mktemp)"
- http_status="$(curl -sS -o "$response_file" -w "%{http_code}" -X DELETE \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/workers/scripts/$WORKER_NAME?force=true" \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json")" || {
- echo "Failed to delete preview Worker via Cloudflare API"
- exit 1
- }
- response="$(cat "$response_file")"
-
- if [ -z "$response" ] && [[ "$http_status" =~ ^2 ]]; then
- echo "Deleted Worker: $WORKER_NAME"
- elif [ -n "$response" ] && echo "$response" | jq -e '.success == true' >/dev/null; then
- echo "Deleted Worker: $WORKER_NAME"
- elif [ -n "$response" ] && echo "$response" | jq -e '[.errors[]?.code] | index(10007)' >/dev/null; then
- echo "$response" | jq -c '.errors'
- echo "Worker may not exist"
- else
- echo "Cloudflare API failed to delete preview Worker (HTTP $http_status)"
- if [ -n "$response" ]; then
- echo "$response" | jq -c '.errors // .' || printf '%s\n' "$response"
- else
- echo "No response body"
- fi
- exit 1
- fi
-
- - name: Delete GitHub Deployment
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- with:
- script: |
- const environment = `preview-pr-${{ github.event.pull_request.number }}`;
- const description = context.payload.action === 'closed'
- ? 'PR closed - preview deleted'
- : 'preview-cf label removed - preview deleted';
-
- try {
- // Get deployments for this environment
- const { data: deployments } = await github.rest.repos.listDeployments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- environment: environment
- });
-
- // Mark all deployments as inactive
- for (const deployment of deployments) {
- await github.rest.repos.createDeploymentStatus({
- owner: context.repo.owner,
- repo: context.repo.repo,
- deployment_id: deployment.id,
- state: 'inactive',
- description
- });
- }
-
- console.log(`Marked ${deployments.length} deployments as inactive`);
- } catch (error) {
- console.log('No deployments to cleanup or error:', error.message);
- }
-
- cleanup-expired:
- name: Cleanup expired previews
- if: github.repository == 'we-promise/sure' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
- runs-on: ubuntu-latest
- timeout-minutes: 15
-
- steps:
- - name: Checkout code
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- with:
- persist-credentials: false
-
- - name: Setup Node.js
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
- with:
- node-version: "24"
-
- - name: Install Wrangler
- run: npm install -g wrangler
-
- - name: Cleanup expired previews
- env:
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- PR_INPUT: ${{ inputs.pr_number }}
- run: |
- # If specific PR number provided, only cleanup that one
- if [ -n "$PR_INPUT" ]; then
- if [[ "$PR_INPUT" =~ ^[1-9][0-9]*$ ]]; then
- PR_NUM="$PR_INPUT"
- WORKER_NAME="sure-preview-$PR_NUM"
- echo "Manually deleting Worker: $WORKER_NAME"
- wrangler delete --name "$WORKER_NAME" --force || echo "Worker may not exist"
-
- # Cleanup GitHub deployment for this PR
- echo "Cleaning up GitHub deployment for PR #$PR_NUM"
- gh api \
- -X GET "/repos/${{ github.repository }}/deployments?environment=preview-pr-$PR_NUM" \
- --jq '.[].id' 2>/dev/null | while read -r DEPLOY_ID; do
- if [ -n "$DEPLOY_ID" ]; then
- gh api \
- -X POST "/repos/${{ github.repository }}/deployments/$DEPLOY_ID/statuses" \
- -f state=inactive \
- -f description="Preview manually deleted" || true
- fi
- done || echo "No deployments to cleanup or error occurred"
- else
- echo "Invalid PR number input '$PR_INPUT'; skipping manual cleanup"
- fi
-
- exit 0
- fi
-
- # Get list of all preview workers
- echo "Fetching list of preview workers..."
-
- # Use Cloudflare API to list workers and read modified_on from the list response.
- # The per-script endpoint returns raw script content, not JSON metadata.
- WORKERS_RESPONSE=$(curl -fsS -X GET \
- "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/workers/scripts" \
- -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
- -H "Content-Type: application/json") || {
- echo "Failed to fetch preview worker list from Cloudflare"
- exit 1
- }
-
- if ! echo "$WORKERS_RESPONSE" | jq -e '.success == true and (.result | type == "array")' >/dev/null 2>&1; then
- echo "Cloudflare API returned an invalid worker list response"
- echo "$WORKERS_RESPONSE" | jq -c '.errors // .'
- exit 1
- fi
-
- WORKERS=$(echo "$WORKERS_RESPONSE" | jq -r '
- .result[]
- | select(.id | startswith("sure-preview-"))
- | [.id, (.modified_on // "")]
- | @tsv
- ')
-
- if [ -z "$WORKERS" ]; then
- echo "No preview workers found"
- exit 0
- fi
-
- echo "Found preview workers:"
- echo "$WORKERS" | cut -f1
-
- # Check each worker's deployment time
- CUTOFF_TIME=$(date -d '24 hours ago' +%s)
-
- while IFS=$'\t' read -r WORKER MODIFIED_ON; do
- [ -n "$WORKER" ] || continue
- echo "Checking $WORKER..."
-
- if [ -z "$MODIFIED_ON" ]; then
- echo "No modified_on timestamp for $WORKER; skipping"
- continue
- fi
-
- if ! MODIFIED_TS=$(date -d "$MODIFIED_ON" +%s 2>/dev/null); then
- echo "Invalid modified_on timestamp for $WORKER ($MODIFIED_ON); skipping"
- continue
- fi
-
- if [ "$MODIFIED_TS" -lt "$CUTOFF_TIME" ]; then
- echo "Worker $WORKER is older than 24 hours, deleting..."
- if wrangler delete --name "$WORKER" --force; then
- # Extract PR number and cleanup GitHub deployment
- PR_NUM="${WORKER#sure-preview-}"
- if [[ "$PR_NUM" =~ ^[1-9][0-9]*$ ]]; then
- echo "Cleaning up GitHub deployment for PR #$PR_NUM"
- gh api \
- -X GET "/repos/${{ github.repository }}/deployments?environment=preview-pr-$PR_NUM" \
- --jq '.[].id' 2>/dev/null | while read -r DEPLOY_ID; do
- gh api \
- -X POST "/repos/${{ github.repository }}/deployments/$DEPLOY_ID/statuses" \
- -f state=inactive \
- -f description="Preview expired after 24 hours" || true
- done || echo "No deployments to cleanup or error occurred"
- else
- echo "Could not extract a valid PR number from $WORKER; skipping deployment cleanup"
- fi
- else
- echo "Failed to delete $WORKER; skipping deployment status update"
- fi
- else
- echo "Worker $WORKER is still within 24-hour window, keeping..."
- fi
- done <<< "$WORKERS"
-
- echo "Cleanup complete"
diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml
deleted file mode 100644
index 3064ab9f6..000000000
--- a/.github/workflows/preview-deploy.yml
+++ /dev/null
@@ -1,636 +0,0 @@
-name: Deploy PR Preview
-
-on:
- workflow_run:
- workflows: ["Pull Request"]
- types: [completed]
-
-permissions:
- contents: read
-
-jobs:
- preview-gate:
- if: |
- github.event.workflow_run.event == 'pull_request' &&
- github.event.workflow_run.conclusion == 'success'
- name: Validate preview deployment gates
- runs-on: ubuntu-latest
- timeout-minutes: 10
- permissions:
- actions: read
- contents: read
- pull-requests: read
- outputs:
- artifact_name: ${{ steps.preview.outputs.artifact_name }}
- head_sha: ${{ steps.preview.outputs.head_sha }}
- is_fork: ${{ steps.preview.outputs.is_fork }}
- pr_number: ${{ steps.preview.outputs.pr_number }}
- resolution_source: ${{ steps.preview.outputs.resolution_source }}
- should_deploy: ${{ steps.preview.outputs.should_deploy }}
-
- steps:
- - name: Checkout trusted preview resolver
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- with:
- ref: ${{ github.event.repository.default_branch }}
- path: trusted-preview-resolver
- persist-credentials: false
- sparse-checkout: |
- workers/preview/deploy
-
- - name: Resolve preview request
- id: preview
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- with:
- script: |
- const { resolvePreviewRequest } = require('./trusted-preview-resolver/workers/preview/deploy/resolve_preview_request.cjs');
- await resolvePreviewRequest({ github, context, core });
-
- deployment_record:
- needs: preview-gate
- if: needs.preview-gate.outputs.should_deploy == 'true'
- name: Create GitHub Deployment
- runs-on: ubuntu-latest
- timeout-minutes: 5
- permissions:
- contents: read
- deployments: write
- outputs:
- deployment_id: ${{ steps.deployment.outputs.result }}
- env:
- HEAD_SHA: ${{ needs.preview-gate.outputs.head_sha }}
- IS_FORK: ${{ needs.preview-gate.outputs.is_fork }}
- PR_NUMBER: ${{ needs.preview-gate.outputs.pr_number }}
-
- steps:
- - name: Create GitHub Deployment
- if: env.IS_FORK == 'false'
- id: deployment
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- with:
- script: |
- const prNumber = process.env.PR_NUMBER;
- const headSha = process.env.HEAD_SHA;
- const deployment = await github.rest.repos.createDeployment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- ref: headSha,
- environment: `preview-pr-${prNumber}`,
- auto_merge: false,
- required_contexts: [],
- description: 'PR Preview Deployment'
- });
- return deployment.data.id;
- result-encoding: string
-
- deploy-preview:
- needs: [preview-gate, deployment_record]
- if: |
- always() &&
- needs.preview-gate.outputs.should_deploy == 'true' &&
- (needs.deployment_record.result == 'success' || needs.deployment_record.result == 'skipped')
- name: Deploy to Cloudflare Containers
- runs-on: ubuntu-latest
- timeout-minutes: 45
- concurrency:
- group: preview-deploy-${{ needs.preview-gate.outputs.pr_number }}
- cancel-in-progress: true
- environment: preview
- permissions:
- actions: read
- contents: read
- outputs:
- preview_url: ${{ steps.deploy.outputs.preview_url }}
- env:
- ARTIFACT_NAME: ${{ needs.preview-gate.outputs.artifact_name }}
- HEAD_SHA: ${{ needs.preview-gate.outputs.head_sha }}
- IS_FORK: ${{ needs.preview-gate.outputs.is_fork }}
- PR_NUMBER: ${{ needs.preview-gate.outputs.pr_number }}
- RESOLUTION_SOURCE: ${{ needs.preview-gate.outputs.resolution_source }}
-
- steps:
- - name: Checkout trusted preview tooling
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- with:
- ref: ${{ github.event.repository.default_branch }}
- path: trusted
- persist-credentials: false
- sparse-checkout: |
- workers/preview
-
- - name: Download preview image artifact
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
- with:
- name: ${{ env.ARTIFACT_NAME }}
- run-id: ${{ github.event.workflow_run.id }}
- github-token: ${{ github.token }}
- path: ${{ runner.temp }}/preview-image
-
- - name: Setup Node.js
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
- with:
- node-version: "24"
-
- - name: Verify preview image artifact checksum
- run: |
- set -euo pipefail
-
- image_archive="$RUNNER_TEMP/preview-image/sure-preview-image.tar.gz"
- checksum_file="$RUNNER_TEMP/preview-image/sure-preview-image.sha256"
- manifest_file="$RUNNER_TEMP/preview-image/sure-preview-image.manifest.json"
- expected_files="$(mktemp)"
- actual_files="$(mktemp)"
-
- test -f "$image_archive"
- test -f "$checksum_file"
- test -f "$manifest_file"
-
- printf '%s\n' \
- sure-preview-image.manifest.json \
- sure-preview-image.sha256 \
- sure-preview-image.tar.gz | sort > "$expected_files"
- find "$RUNNER_TEMP/preview-image" -maxdepth 1 -type f -printf '%f\n' | sort > "$actual_files"
-
- if ! diff -u "$expected_files" "$actual_files"; then
- echo "Preview image artifact contained unexpected files" >&2
- exit 1
- fi
-
- expected_checksum="$(tr -d '[:space:]' < "$checksum_file")"
- actual_checksum="$(sha256sum "$image_archive" | awk '{print $1}')"
-
- if [ "$expected_checksum" != "$actual_checksum" ]; then
- echo "Preview image artifact checksum mismatch" >&2
- exit 1
- fi
-
- node - "$manifest_file" "$expected_checksum" <<'NODE'
- const fs = require('node:fs');
-
- const manifestPath = process.argv[2];
- const expectedChecksum = process.argv[3];
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
- const expectedImageTag = `sure-preview-pr-${process.env.PR_NUMBER}:${process.env.HEAD_SHA}`;
- const expected = {
- artifactVersion: 1,
- archivePath: 'sure-preview-image.tar.gz',
- archiveSha256: expectedChecksum,
- headSha: process.env.HEAD_SHA,
- imageTag: expectedImageTag,
- prNumber: process.env.PR_NUMBER,
- };
-
- for (const [key, value] of Object.entries(expected)) {
- if (manifest[key] !== value) {
- throw new Error(`Preview image manifest ${key} mismatch`);
- }
- }
-
- if (!/^sha256:[a-f0-9]{64}$/.test(manifest.imageId || '')) {
- throw new Error('Preview image manifest imageId is invalid');
- }
- NODE
-
- - name: Prepare trusted preview deploy workspace
- run: |
- set -euo pipefail
-
- preview_dir="$RUNNER_TEMP/sure-preview-worker"
- rm -rf "$preview_dir"
- mkdir -p "$preview_dir"
-
- cp trusted/workers/preview/package.json "$preview_dir/package.json"
- cp trusted/workers/preview/package-lock.json "$preview_dir/package-lock.json"
- cp trusted/workers/preview/tsconfig.json "$preview_dir/tsconfig.json"
- cp trusted/workers/preview/wrangler.toml "$preview_dir/wrangler.toml"
- 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
- exit 1
- fi
-
- cd "$preview_dir"
- npm ci --ignore-scripts --no-audit --no-fund
-
- - name: Load preview image artifact
- run: |
- set -euo pipefail
-
- image_archive="$RUNNER_TEMP/preview-image/sure-preview-image.tar.gz"
- manifest_file="$RUNNER_TEMP/preview-image/sure-preview-image.manifest.json"
- expected_image="sure-preview-pr-${PR_NUMBER}:${HEAD_SHA}"
-
- gzip -dc "$image_archive" | docker load
- docker image inspect "$expected_image" >/dev/null
- expected_image_id="$(node -e 'const fs = require("node:fs"); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1], "utf8")).imageId);' "$manifest_file")"
- actual_image_id="$(docker image inspect --format '{{.Id}}' "$expected_image")"
-
- if [ "$expected_image_id" != "$actual_image_id" ]; then
- echo "Loaded preview image ID did not match artifact manifest" >&2
- exit 1
- fi
-
- - name: Push preview image to Cloudflare registry
- id: image
- env:
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PREVIEW_API_TOKEN || secrets.CLOUDFLARE_API_TOKEN }}
- run: |
- 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}"
- push_log="$RUNNER_TEMP/wrangler-containers-push.log"
- clean_log="$RUNNER_TEMP/wrangler-containers-push.clean.log"
- push_status=0
-
- # wrangler containers push validates wrangler.toml, so point the trusted
- # config at a registry-shaped ref while it pushes the verified local image.
- 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
- push_status=${PIPESTATUS[0]}
- set -e
- perl -pe 's/\e\[[0-9;]*[A-Za-z]//g' "$push_log" > "$clean_log"
-
- if [ "$push_status" -ne 0 ]; then
- exit "$push_status"
- fi
-
- 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
- exit 1
- fi
-
- echo "image_ref=${image_ref}" >> "$GITHUB_OUTPUT"
-
- - name: Configure trusted preview image reference
- env:
- IMAGE_REF: ${{ steps.image.outputs.image_ref }}
- run: |
- set -euo pipefail
-
- source_config="$RUNNER_TEMP/sure-preview-worker/wrangler.source.toml"
- config_path="$RUNNER_TEMP/sure-preview-worker/wrangler.toml"
- # 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"
- "$RUNNER_TEMP/sure-preview-worker/deploy/redact_preview_log.sh" < "$config_path" > "$redacted_config"
- cat "$redacted_config"
-
- - name: Deploy to Cloudflare Containers
- id: deploy
- env:
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_PREVIEW_API_TOKEN || secrets.CLOUDFLARE_API_TOKEN }}
- CLOUDFLARE_WORKERS_SUBDOMAIN: ${{ secrets.CLOUDFLARE_WORKERS_SUBDOMAIN }}
- run: |
- set -euo pipefail
-
- cd "$RUNNER_TEMP/sure-preview-worker"
- deploy_log="$RUNNER_TEMP/wrangler-deploy.log"
- clean_deploy_log="$RUNNER_TEMP/wrangler-deploy.clean.log"
-
- deploy_once() {
- set +e
- ./node_modules/.bin/wrangler deploy --config wrangler.toml --var "PR_NUMBER:${PR_NUMBER}" 2>&1 | tee "$deploy_log" | ./deploy/redact_preview_log.sh
- local deploy_status=${PIPESTATUS[0]}
- set -e
- return "$deploy_status"
- }
-
- if ! deploy_once; then
- perl -pe 's/\e\[[0-9;]*[A-Za-z]//g' "$deploy_log" > "$clean_deploy_log"
-
- if grep -F "associated with a different durable object namespace" "$clean_deploy_log" >/dev/null; then
- echo "Detected stale Cloudflare container app state for PR ${PR_NUMBER}; deleting preview Worker and retrying once."
- if ! ./node_modules/.bin/wrangler delete --name "sure-preview-${PR_NUMBER}" --force; then
- echo "Preview Worker delete failed; continuing to the single retry so wrangler deploy reports the final error if the stale state remains." >&2
- fi
- deploy_once
- else
- exit 1
- fi
- fi
-
- # Get the deployment URL
- PREVIEW_URL="https://sure-preview-${PR_NUMBER}.${CLOUDFLARE_WORKERS_SUBDOMAIN}.workers.dev"
- echo "preview_url=${PREVIEW_URL}" >> "$GITHUB_OUTPUT"
-
- - name: Warm preview container
- env:
- PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
- run: |
- echo "Triggering preview wake-up..."
- curl -fsS --connect-timeout 5 --max-time 15 "$PREVIEW_URL/_container_status" >/dev/null || true
-
- - name: Collect preview diagnostics
- if: success()
- env:
- PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
- run: |
- set -euo pipefail
-
- 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"
-
- # ~100 polls x ~3.5s = up to ~6 min. The preview must cold-start the
- # container (≈1.3 GB image pull + sandbox init) and generate the full
- # demo dataset before previewReady flips. A real Cloudflare standard-1
- # run measured previewReady at ~195s (rails ~46s, demo data ~149s) —
- # the old 40-poll (~140s) budget would have failed a working preview.
- # Keep generous headroom; the loop still breaks early on ready/failed.
- for attempt in $(seq 1 100); do
- if curl -fsS --connect-timeout 5 --max-time 15 "$PREVIEW_URL/_container_status" -o "$diagnostics_file"; then
- 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" ] || ! 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
- echo "Preview diagnostics from _container_status reported previewFailed=true:" >&2
- jq -c . "$diagnostics_file" >&2
- exit 1
- fi
-
- if ! jq -e '.previewReady == true' "$diagnostics_file" >/dev/null; then
- echo "Preview diagnostics from _container_status did not reach previewReady=true:" >&2
- jq -c . "$diagnostics_file" >&2
- exit 1
- fi
-
- if ! jq -e '.timings.previewReadyAt != null and .timings.secondsToPreviewReady != null' "$diagnostics_file" >/dev/null; then
- echo "Preview diagnostics are missing readiness timing fields:" >&2
- jq -c . "$diagnostics_file" >&2
- exit 1
- fi
-
- - name: Upload preview diagnostics
- if: always() && steps.deploy.outputs.preview_url != ''
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
- with:
- name: preview-diagnostics-pr-${{ env.PR_NUMBER }}-${{ env.HEAD_SHA }}
- path: ${{ runner.temp }}/preview-diagnostics
- if-no-files-found: error
- retention-days: 3
-
- - name: Collect preview failure diagnostics
- if: failure()
- run: |
- set -euo pipefail
-
- diagnostics_dir="$RUNNER_TEMP/preview-failure-diagnostics"
- manifest_file="$RUNNER_TEMP/preview-image/sure-preview-image.manifest.json"
- redaction_helper="$RUNNER_TEMP/sure-preview-worker/deploy/redact_preview_log.sh"
- mkdir -p "$diagnostics_dir"
-
- jq -n \
- --arg artifactName "$ARTIFACT_NAME" \
- --arg headSha "$HEAD_SHA" \
- --arg isFork "$IS_FORK" \
- --arg prNumber "$PR_NUMBER" \
- --arg resolutionSource "$RESOLUTION_SOURCE" \
- '{
- artifactName: $artifactName,
- headSha: $headSha,
- isFork: $isFork,
- prNumber: $prNumber,
- resolutionSource: $resolutionSource
- }' > "$diagnostics_dir/preview-request.json"
-
- sanitize_copy() {
- local source="$1"
- local destination="$2"
- if [ -f "$source" ]; then
- if [ -x "$redaction_helper" ]; then
- "$redaction_helper" < "$source" > "$destination"
- else
- cp "$source" "$destination"
- fi
- fi
- }
-
- if [ -f "$manifest_file" ]; then
- jq '{
- artifactVersion,
- archivePath,
- archiveSha256,
- headSha,
- imageId,
- imageTag,
- prNumber
- }' "$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
- sanitize_copy "$RUNNER_TEMP/wrangler-deploy.clean.log" "$diagnostics_dir/wrangler-deploy.log"
- else
- sanitize_copy "$RUNNER_TEMP/wrangler-deploy.log" "$diagnostics_dir/wrangler-deploy.log"
- fi
-
- find "$diagnostics_dir" -maxdepth 1 -type f -print
-
- - name: Upload preview failure diagnostics
- if: failure()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
- with:
- name: preview-failure-diagnostics-pr-${{ env.PR_NUMBER }}-${{ env.HEAD_SHA }}
- path: ${{ runner.temp }}/preview-failure-diagnostics
- if-no-files-found: error
- retention-days: 3
-
- - name: Prepare cleanup metadata
- if: success()
- run: |
- set -euo pipefail
-
- metadata_dir="$RUNNER_TEMP/preview-cleanup-metadata"
- mkdir -p "$metadata_dir"
- "$RUNNER_TEMP/sure-preview-worker/deploy/redact_preview_log.sh" \
- < "$RUNNER_TEMP/sure-preview-worker/wrangler.toml" \
- > "$metadata_dir/wrangler.toml"
-
- - name: Store cleanup metadata
- if: success()
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
- with:
- name: preview-cleanup-pr-${{ env.PR_NUMBER }}
- path: ${{ runner.temp }}/preview-cleanup-metadata/wrangler.toml
- retention-days: 2
-
- deployment_status:
- needs: [preview-gate, deployment_record, deploy-preview]
- if: |
- always() &&
- needs.preview-gate.outputs.should_deploy == 'true' &&
- needs.preview-gate.outputs.is_fork == 'false' &&
- needs.deployment_record.result == 'success'
- name: Update GitHub Deployment Status
- runs-on: ubuntu-latest
- timeout-minutes: 5
- permissions:
- contents: read
- deployments: write
- env:
- DEPLOYMENT_ID: ${{ needs.deployment_record.outputs.deployment_id }}
- DEPLOY_RESULT: ${{ needs.deploy-preview.result }}
- PREVIEW_URL: ${{ needs.deploy-preview.outputs.preview_url }}
-
- steps:
- - name: Update Deployment Status
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- with:
- script: |
- const state = process.env.DEPLOY_RESULT === 'success' ? 'success' : 'failure';
- const previewUrl = process.env.PREVIEW_URL || undefined;
- await github.rest.repos.createDeploymentStatus({
- owner: context.repo.owner,
- repo: context.repo.repo,
- deployment_id: Number(process.env.DEPLOYMENT_ID),
- state: state,
- environment_url: state === 'success' ? previewUrl : undefined,
- description: state === 'success' ? 'Preview deployed successfully' : 'Preview deployment failed'
- });
-
- preview_comment:
- needs: [preview-gate, deploy-preview]
- if: needs.deploy-preview.result == 'success'
- name: Comment on PR
- runs-on: ubuntu-latest
- timeout-minutes: 5
- permissions:
- contents: read
- pull-requests: write
- env:
- HEAD_SHA: ${{ needs.preview-gate.outputs.head_sha }}
- PR_NUMBER: ${{ needs.preview-gate.outputs.pr_number }}
- PREVIEW_URL: ${{ needs.deploy-preview.outputs.preview_url }}
-
- steps:
- - name: Comment on PR
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- with:
- script: |
- const previewUrl = process.env.PREVIEW_URL;
- const issueNumber = Number(process.env.PR_NUMBER);
- const headSha = process.env.HEAD_SHA;
- const commentBody = [
- '## 🚀 Preview Deployment Ready',
- '',
- "Your preview environment has been deployed to Cloudflare Containers with the PR's Docker image.",
- '',
- `**Preview URL:** ${previewUrl}`,
- '',
- '> ⏰ This preview is intended to be cleaned up after **24 hours** of the last deployment once the cleanup workflow is live on the default branch.',
- '> 💤 The container will sleep after 30 minutes of inactivity and wake on the next request.',
- '',
- '---',
- `Deployed from commit ${headSha}`,
- ].join('\n');
-
- // Find existing comment
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issueNumber
- });
-
- const botComment = comments.find(comment =>
- comment.user.type === 'Bot' &&
- comment.body.includes('Preview Deployment Ready')
- );
-
- if (botComment) {
- await github.rest.issues.updateComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: botComment.id,
- body: commentBody
- });
- } else {
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issueNumber,
- body: commentBody
- });
- }
diff --git a/Dockerfile.preview b/Dockerfile.preview
deleted file mode 100644
index 06a958a5e..000000000
--- a/Dockerfile.preview
+++ /dev/null
@@ -1,385 +0,0 @@
-# syntax = docker/dockerfile:1
-
-# Preview Dockerfile for Cloudflare Containers
-# Includes PostgreSQL and Redis for self-contained development testing
-
-ARG RUBY_VERSION=3.4.9
-FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base
-
-WORKDIR /rails
-
-# Install base packages including PostgreSQL and Redis servers
-RUN apt-get update -qq \
- && apt-get install --no-install-recommends -y \
- curl libvips postgresql postgresql-client redis-server libyaml-0-2 procps sudo openssl strace \
- && rm -rf /var/lib/apt/lists /var/cache/apt/archives
-
-# Set development environment
-ARG BUILD_COMMIT_SHA
-ENV RAILS_ENV="development" \
- BUNDLE_PATH="/usr/local/bundle" \
- BUILD_COMMIT_SHA=${BUILD_COMMIT_SHA}
-
-# Build stage
-FROM base AS build
-
-RUN apt-get update -qq \
- && apt-get install --no-install-recommends -y build-essential libpq-dev git pkg-config libyaml-dev \
- && rm -rf /var/lib/apt/lists /var/cache/apt/archives
-
-COPY .ruby-version Gemfile Gemfile.lock ./
-RUN bundle install \
- && rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git \
- && bundle exec bootsnap precompile --gemfile -j 0
-
-COPY . .
-
-RUN bundle exec bootsnap precompile -j 0 app/ lib/
-
-# Precompile assets
-RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
-
-# Final stage
-FROM base
-
-# Create rails user and configure PostgreSQL/Redis permissions
-RUN groupadd --system --gid 1000 rails && \
- useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
- echo "rails ALL=(ALL) NOPASSWD: /usr/bin/pg_ctlcluster, /usr/bin/redis-server" > /etc/sudoers.d/rails && \
- chmod 0440 /etc/sudoers.d/rails
-
-# Configure PostgreSQL to allow local connections. Target the highest-version
-# cluster's pg_hba.conf -- the same one the entrypoint starts (ls
-# /etc/postgresql | sort -V | tail -1) -- so the trust rules always land on the
-# cluster that actually runs, even if multiple majors are present.
-RUN PG_VERSION="$(ls /etc/postgresql 2>/dev/null | sort -V | tail -1)" && \
- PG_HBA="/etc/postgresql/${PG_VERSION}/main/pg_hba.conf" && \
- if [ -z "$PG_VERSION" ] || [ ! -f "$PG_HBA" ]; then \
- echo "ERROR: pg_hba.conf not found for cluster '${PG_VERSION:-none}'; cannot configure local trust" >&2; \
- exit 1; \
- fi && \
- echo "local all all trust" > "$PG_HBA" && \
- echo "host all all 127.0.0.1/32 trust" >> "$PG_HBA" && \
- echo "host all all ::1/128 trust" >> "$PG_HBA"
-
-# Use file-backed dynamic shared memory instead of POSIX /dev/shm.
-# Cloudflare Containers provide only a tiny /dev/shm, and PostgreSQL's default
-# dynamic_shared_memory_type = posix FATALs on startup there with
-# "could not resize shared memory segment ... No space left on device", which
-# kills the container before it can serve a port. mmap keeps DSM in the data
-# directory ($PGDATA/pg_dynshmem), removing the /dev/shm dependency. Local
-# Docker hides this because its default /dev/shm is 64MB.
-#
-# Select the highest-version cluster's config -- the same one the entrypoint
-# starts (ls /etc/postgresql | sort -V | tail -1) -- so the override always
-# lands on the cluster that actually runs, even if multiple majors are present.
-RUN PG_VERSION="$(ls /etc/postgresql 2>/dev/null | sort -V | tail -1)" && \
- PG_CONF="/etc/postgresql/${PG_VERSION}/main/postgresql.conf" && \
- if [ -z "$PG_VERSION" ] || [ ! -f "$PG_CONF" ]; then \
- echo "ERROR: postgresql.conf not found for cluster '${PG_VERSION:-none}'; cannot disable /dev/shm DSM dependency" >&2; \
- exit 1; \
- fi && \
- sed -i 's/^[[:space:]]*dynamic_shared_memory_type[[:space:]]*=/# &/' "$PG_CONF" && \
- printf '\n# Preview: avoid /dev/shm dependency (small in Cloudflare Containers)\ndynamic_shared_memory_type = mmap\n' >> "$PG_CONF" && \
- grep -qx 'dynamic_shared_memory_type = mmap' "$PG_CONF" && \
- echo "Configured dynamic_shared_memory_type=mmap in $PG_CONF"
-
-# Create database directory with correct permissions
-RUN mkdir -p /var/run/postgresql && \
- chown -R postgres:postgres /var/run/postgresql && \
- chmod 2775 /var/run/postgresql
-
-# Copy built artifacts
-COPY --chown=rails:rails --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
-COPY --chown=rails:rails --from=build /rails /rails
-
-# Create preview entrypoint script inline
-RUN cat > /rails/bin/preview-entrypoint << 'ENTRYPOINT_EOF'
-#!/bin/bash
-set -e
-
-cd /rails
-
-# Diagnostics posts are best-effort and must NEVER stall boot: the worker's
-# Durable Object can be unresponsive while it waits for this container's port,
-# so an unbounded curl here deadlocks startup against the port check.
-emit_status_sync() {
- if [ -n "$PREVIEW_ORIGIN" ] && [ -n "$PREVIEW_DIAGNOSTICS_NONCE" ]; then
- local stage="$1"
- local detail="$2"
- local payload
- payload=$(STAGE="$stage" DETAIL="$detail" ruby -rjson -e 'print JSON.generate({stage: ENV.fetch("STAGE"), detail: ENV.fetch("DETAIL", "")})' 2>/dev/null) || return 0
- curl -fsS --connect-timeout 2 --max-time 5 -X POST "$PREVIEW_ORIGIN/_container_event" \
- -H 'content-type: application/json' \
- -H "x-preview-diagnostics-nonce: $PREVIEW_DIAGNOSTICS_NONCE" \
- --data "$payload" >/dev/null || true
- fi
-}
-
-emit_status() {
- emit_status_sync "$1" "$2" &
-}
-
-summarize_log_tail() {
- local file="$1"
- local label="$2"
-
- if [ ! -f "$file" ]; then
- printf '%s log unavailable' "$label"
- return 0
- fi
-
- tail -n 80 "$file" 2>&1 |
- sed 's/"/'"'"'/g' |
- tr '\n' ' ' |
- sed 's/ */ /g' |
- cut -c 1-1600
-}
-
-fail_preview() {
- local detail="$1"
- trap - ERR
- # Always log to stderr too: the HTTP diagnostics channel can be unreachable
- # while the worker's Durable Object is still waiting for this container's
- # port, but stderr is captured by Cloudflare container observability, so the
- # real reason survives even when the event POST does not.
- echo "PREVIEW FAILED: ${detail}" >&2
- emit_status_sync failed "$detail"
- exit 1
-}
-
-postgres_cluster_snapshot() {
- local snapshot=""
- local cluster_status
- local postgres_log
-
- if command -v pg_lsclusters >/dev/null 2>&1; then
- cluster_status="$(pg_lsclusters 2>&1 | tr '\n' '|' | sed 's/"/'"'"'/g' | cut -c 1-500)"
- snapshot="clusters=${cluster_status}"
- fi
-
- postgres_log="/var/log/postgresql/postgresql-${PG_VERSION}-main.log"
- if [ -f "$postgres_log" ]; then
- snapshot="${snapshot} log=$(summarize_log_tail "$postgres_log" postgres)"
- fi
-
- printf '%s' "$snapshot"
-}
-
-trap 'emit_status_sync failed "preview-entrypoint failed on line ${LINENO}"' ERR
-emit_status boot "preview-entrypoint started"
-
-# Bind :3000 immediately with a tiny placeholder responder. Cloudflare's
-# container supervisor only waits ~20s for the port, while the full stack
-# (redis, postgres, migrations) needs 60s+ on a basic instance. The
-# placeholder answers 503 with a meta-refresh; the worker still gates
-# previewReady on the real Rails /up probe and sample data, so readiness
-# semantics are unchanged. It is replaced by the real server below.
-ruby -rsocket -e '
- server = TCPServer.new("0.0.0.0", 3000)
- body = "
Sure preview warmingSure preview is booting (database setup in progress). This page refreshes automatically."
- loop do
- client = begin
- server.accept
- rescue StandardError
- next
- end
- begin
- # Never read from the client: the response is static, and a blocking read
- # would let one silent connection (e.g. a bare TCP port probe) wedge this
- # single-threaded loop and starve every later probe.
- client.write("HTTP/1.1 503 Service Unavailable\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: #{body.bytesize}\r\nRetry-After: 3\r\nConnection: close\r\n\r\n#{body}")
- rescue StandardError
- ensure
- begin
- client.close
- rescue StandardError
- end
- end
- end
-' >/tmp/port-placeholder.log 2>&1 &
-PLACEHOLDER_PID=$!
-
-# Confirm the placeholder actually bound :3000 before reporting it. A plain
-# TCP connect is used because the placeholder intentionally answers 503.
-PLACEHOLDER_BOUND=0
-for i in {1..10}; do
- if (exec 3<>/dev/tcp/127.0.0.1/3000) 2>/dev/null; then
- PLACEHOLDER_BOUND=1
- break
- fi
- if ! kill -0 "$PLACEHOLDER_PID" 2>/dev/null; then
- break
- fi
- sleep 0.2
-done
-if [ "$PLACEHOLDER_BOUND" -eq 1 ]; then
- emit_status port-placeholder "bound :3000 placeholder responder (pid ${PLACEHOLDER_PID})"
-else
- echo "Warning: :3000 placeholder did not come up; continuing boot" >&2
- emit_status port-placeholder-missing "placeholder responder failed to bind :3000: $(summarize_log_tail /tmp/port-placeholder.log port-placeholder)"
-fi
-
-REDIS_READY=0
-POSTGRES_READY=0
-
-# Start Redis
-echo "Starting Redis..."
-emit_status redis-start "starting redis"
-sudo redis-server --daemonize yes --bind 127.0.0.1
-
-# Wait for Redis to be ready
-echo "Waiting for Redis to be ready..."
-for i in {1..10}; do
- if redis-cli ping > /dev/null 2>&1; then
- echo "Redis is ready"
- emit_status redis-ready "redis is ready"
- REDIS_READY=1
- break
- fi
- sleep 1
-done
-
-if [ "$REDIS_READY" -ne 1 ]; then
- fail_preview "redis did not become ready in time"
-fi
-
-# Start PostgreSQL
-echo "Starting PostgreSQL..."
-emit_status postgres-start "starting postgres"
-PG_VERSION=$(ls /etc/postgresql/ | sort -V | tail -1)
-if [ -z "$PG_VERSION" ]; then
- fail_preview "could not determine installed PostgreSQL version"
-fi
-if sudo pg_ctlcluster --skip-systemctl-redirect "$PG_VERSION" main status > /dev/null 2>&1; then
- emit_status postgres-already-running "postgres cluster already running"
-else
- POSTGRES_START_LOG=/tmp/postgres-start.log
- if ! sudo pg_ctlcluster --skip-systemctl-redirect "$PG_VERSION" main start >"$POSTGRES_START_LOG" 2>&1; then
- fail_preview "pg_ctlcluster start failed: $(summarize_log_tail "$POSTGRES_START_LOG" pg_ctlcluster-start) | $(postgres_cluster_snapshot)"
- fi
-fi
-
-# Wait for PostgreSQL to be ready
-echo "Waiting for PostgreSQL to be ready..."
-for i in {1..30}; do
- if pg_isready -h localhost -U postgres > /dev/null 2>&1; then
- echo "PostgreSQL is ready"
- emit_status postgres-ready "postgres is ready"
- POSTGRES_READY=1
- break
- fi
- sleep 1
-done
-
-if [ "$POSTGRES_READY" -ne 1 ]; then
- fail_preview "postgres did not become ready in time: $(postgres_cluster_snapshot)"
-fi
-
-# Create database user and database if they don't exist
-echo "Setting up database..."
-emit_status db-setup "setting up database"
-psql -h localhost -U postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='rails'" | grep -q 1 || \
- psql -h localhost -U postgres -c "CREATE USER rails WITH SUPERUSER PASSWORD 'rails';"
-
-psql -h localhost -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='sure_development'" | grep -q 1 || \
- psql -h localhost -U postgres -c "CREATE DATABASE sure_development OWNER rails;"
-
-# Set DATABASE_URL if not already set
-export DATABASE_URL="${DATABASE_URL:-postgres://rails:rails@localhost:5432/sure_development}"
-
-# Set REDIS_URL if not already set
-export REDIS_URL="${REDIS_URL:-redis://localhost:6379/0}"
-
-# Generate SECRET_KEY_BASE if not set
-export SECRET_KEY_BASE="${SECRET_KEY_BASE:-$(openssl rand -hex 64)}"
-
-# Run database migrations
-echo "Running database migrations..."
-emit_status db-prepare "running rails db:prepare"
-/rails/bin/rails db:prepare
-emit_status db-prepare-done "rails db:prepare finished"
-
-# Defer all demo-data creation until after Rails is up so preview can boot first
-echo "Checking demo dataset..."
-emit_status demo-data-check "checking for default demo user"
-DEMO_EMAIL="${DEMO_USER_EMAIL:-user@example.com}"
-DEMO_EMAIL_SQL=${DEMO_EMAIL//\'/\'\'}
-DEMO_SEED="${DEMO_DATA_SEED:-880}"
-DEMO_HAS_USER=0
-DEMO_HAS_DATA=0
-
-if psql "$DATABASE_URL" -tAc "SELECT 1 FROM users WHERE email = '${DEMO_EMAIL_SQL}' LIMIT 1" | grep -q 1; then
- DEMO_HAS_USER=1
- emit_status demo-data-user-present "default demo user already exists"
-fi
-
-if psql "$DATABASE_URL" -tAc "SELECT 1 FROM accounts a JOIN users u ON u.family_id = a.family_id WHERE u.email = '${DEMO_EMAIL_SQL}' LIMIT 1" | grep -q 1; then
- DEMO_HAS_DATA=1
- emit_status demo-data-skip "demo financial data already exists"
-else
- emit_status demo-data-deferred "deferring demo data creation until after rails boot"
-fi
-
-# Release :3000 for the real server. The brief listener gap is handled by the
-# worker, which catches containerFetch errors and serves its wait page.
-kill "$PLACEHOLDER_PID" 2>/dev/null || true
-wait "$PLACEHOLDER_PID" 2>/dev/null || true
-
-# Execute the main command with an internal readiness probe
-echo "Starting Rails server..."
-emit_status rails-start "starting rails server"
-"$@" > /tmp/rails.log 2>&1 &
-RAILS_PID=$!
-
-for i in {1..180}; do
- if curl -fsS http://127.0.0.1:3000/up > /dev/null 2>&1; then
- emit_status rails-up-ready "rails responded on localhost:3000/up"
-
- if [ "$DEMO_HAS_USER" -ne 1 ] || [ "$DEMO_HAS_DATA" -ne 1 ]; then
- emit_status demo-data-load "creating/backfilling demo dataset in background (seed=${DEMO_SEED})"
- (
- (
- DEMO_USER_EMAIL="$DEMO_EMAIL" DEMO_DATA_SEED="$DEMO_SEED" /rails/bin/rails runner '
- email = ENV.fetch("DEMO_USER_EMAIL")
- generator = Demo::Generator.new(seed: ENV.fetch("DEMO_DATA_SEED"))
- user = User.find_by(email: email)
-
- unless user
- generator.generate_empty_data!(skip_clear: true)
- user = User.find_by!(email: email)
- end
-
- has_accounts = user.family.accounts.exists?
- generator.generate_new_user_data_for!(user.family, email: user.email) unless has_accounts
- '
- ) > /tmp/demo-data.log 2>&1 && \
- emit_status demo-data-ready "default demo dataset loaded in background" || \
- emit_status demo-data-failed "background demo dataset load failed: $(summarize_log_tail /tmp/demo-data.log demo-data)"
- ) &
- fi
-
- break
- fi
- sleep 1
-done
-
-if ! curl -fsS http://127.0.0.1:3000/up > /dev/null 2>&1; then
- emit_status rails-up-timeout "rails did not answer localhost:3000/up in time"
- emit_status rails-process-status "$(ps -o pid=,ppid=,stat=,comm=,args= -p "$RAILS_PID" 2>/dev/null | tr -s ' ' | sed 's/^ //')"
- emit_status rails-process-wchan "$(cat /proc/$RAILS_PID/wchan 2>/dev/null | tr '\n' ' ' | cut -c 1-200)"
- emit_status rails-process-children "$(ps -o pid=,ppid=,stat=,comm=,args= --ppid "$RAILS_PID" 2>/dev/null | tail -n +2 | tr '\n' '|' | cut -c 1-600)"
- emit_status rails-socket-state "$(ruby -e 'hex="0BB8"; rows=File.readlines("/proc/net/tcp")+File.readlines("/proc/net/tcp6"); hits=rows.select{|l| l.include?(":#{hex} ")}.map{|l| l.strip.split[3] rescue nil}.compact; puts(hits.empty? ? "no-listener" : hits.join(","))' 2>&1 | tr '\n' ' ' | cut -c 1-400)"
- emit_status rails-log-tail "$(tail -n 40 /tmp/rails.log 2>&1 | sed 's/"/'"'"'/g' | tr '\n' ' ' | cut -c 1-1200)"
-fi
-
-wait "$RAILS_PID"
-ENTRYPOINT_EOF
-RUN chmod 755 /rails/bin/preview-entrypoint && chown rails:rails /rails/bin/preview-entrypoint
-
-USER 1000:1000
-
-ENTRYPOINT ["/rails/bin/preview-entrypoint"]
-
-EXPOSE 3000
-CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
diff --git a/bin/preview_deploy_security_check.rb b/bin/preview_deploy_security_check.rb
deleted file mode 100644
index e7fedb0ca..000000000
--- a/bin/preview_deploy_security_check.rb
+++ /dev/null
@@ -1,531 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-%w[json pathname yaml].each { |library| require library }
-
-ROOT = File.expand_path("..", __dir__)
-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")
-PINNED_ACTION = /\A[^@\s]+@[a-f0-9]{40}\z/
-EXPECTED_ACTION_PINS = {
- "actions/checkout" => "93cb6efe18208431cddfb8368fd83d5badbf9bfd", # v5
- "actions/download-artifact" => "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53", # v6
- "actions/github-script" => "ed597411d8f924073f98dfc5c65a23a2325f34cd", # v8
- "actions/setup-node" => "48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e", # v6
- "actions/upload-artifact" => "b7c566a772e6b6bfb58ed0dc250532a479d7789f" # v6
-}.freeze
-INLINE_SECRET_EXPRESSION = /\$\{\{\s*secrets\s*(?:\.|\[)/i
-INLINE_PR_EXPRESSION = /
- \$\{\{\s*
- github\s*
- (?:\.\s*event|\[\s*['"]event['"]\s*\])\s*
- (?:\.\s*pull_request|\[\s*['"]pull_request['"]\s*\])
-/ix
-PR_CONTROLLED_WORKDIR = %r{\A(?:pr|workers/preview)(?:/|\z)}
-GITHUB_WORKSPACE_PREFIX = %r{
- \A
- (?:
- \$GITHUB_WORKSPACE |
- \$\{\{\s*github\s*(?:\.\s*workspace|\[\s*['"]workspace['"]\s*\])\s*\}\}
- )
- (?:/|\z)
-}ix
-EXPECTED_TOP_LEVEL_PERMISSIONS = { "contents" => "read" }.freeze
-EXPECTED_GATE_PERMISSIONS = { "actions" => "read", "contents" => "read", "pull-requests" => "read" }.freeze
-EXPECTED_IMAGE_PERMISSIONS = { "contents" => "read" }.freeze
-EXPECTED_DEPLOYMENT_PERMISSIONS = {
- "contents" => "read",
- "deployments" => "write"
-}.freeze
-EXPECTED_DEPLOY_PERMISSIONS = {
- "actions" => "read",
- "contents" => "read"
-}.freeze
-EXPECTED_COMMENT_PERMISSIONS = {
- "contents" => "read",
- "pull-requests" => "write"
-}.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 = [
- 'cp trusted/workers/preview/package.json "$preview_dir/package.json"',
- 'cp trusted/workers/preview/package-lock.json "$preview_dir/package-lock.json"',
- 'cp trusted/workers/preview/tsconfig.json "$preview_dir/tsconfig.json"',
- 'cp trusted/workers/preview/wrangler.toml "$preview_dir/wrangler.toml"',
- '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
-REQUIRED_IMAGE_BUILD_LINES = [
- "docker build",
- "--platform linux/amd64",
- '--build-arg "BUILD_COMMIT_SHA=${HEAD_SHA}"',
- "-f Dockerfile.preview",
- '-t "${IMAGE_TAG}"',
- 'docker save "${IMAGE_TAG}" | gzip -1 > "$image_archive"',
- 'sha256sum "$image_archive"',
- "sure-preview-image.manifest.json",
- 'ARCHIVE_SHA256="$archive_sha256" IMAGE_ID="$image_id" node - "$manifest_file"',
- "JSON.stringify(manifest, null, 2)",
- 'jq -e . "$manifest_file"'
-].freeze
-
-def fail_check(message)
- warn "preview-deploy security check failed: #{message}"
- exit 1
-end
-
-def assert(value, message)
- fail_check(message) unless value
-end
-
-def workflow_on(workflow)
- workflow["on"] || workflow[true] || fail_check("workflow is missing on trigger")
-end
-
-def step!(steps, name)
- steps.find { |step| step["name"] == name } || fail_check("missing #{name.inspect} step")
-end
-
-def run(step)
- step.fetch("run", "")
-end
-
-def step_body(step)
- [ run(step), step.dig("with", "script") ].compact.join("\n")
-end
-
-def env_hash(node)
- node.fetch("env", {})
-end
-
-def assert_run_includes(step, *needles)
- script = step_body(step)
- needles.each { |needle| assert(script.include?(needle), "#{step["name"]} must include #{needle.inspect}") }
- script
-end
-
-def normalized_working_directory(value)
- path = value.to_s.strip.sub(GITHUB_WORKSPACE_PREFIX, "")
- normalized = Pathname.new(path).cleanpath.to_s
-
- normalized == "." ? "" : normalized
-end
-
-def environment_name(job)
- environment = job["environment"]
- environment.is_a?(Hash) ? environment["name"] : environment
-end
-
-def assert_pinned_actions!(steps)
- steps.each do |step|
- uses = step["uses"]
- next unless uses
- next if uses.start_with?("./")
-
- assert(uses.match?(PINNED_ACTION), "#{step["name"] || uses} must pin external actions")
-
- action, sha = uses.split("@", 2)
- expected_sha = EXPECTED_ACTION_PINS[action]
- assert(sha == expected_sha, "#{step["name"] || uses} must pin #{action} to #{expected_sha}") if expected_sha
- end
-end
-
-def assert_no_inline_expressions!(steps)
- inline_scripts = steps.flat_map { |step| [ run(step), step.dig("with", "script") ] }.compact.join("\n")
- assert(!inline_scripts.match?(INLINE_SECRET_EXPRESSION), "secrets must enter scripts through env")
- assert(!inline_scripts.match?(INLINE_PR_EXPRESSION), "PR fields must enter scripts through env")
-end
-
-def assert_secret_env_sources!(step, expected_keys)
- env = step.fetch("env")
-
- assert(env.keys.sort == expected_keys, "#{step["name"]} secret env keys must be #{expected_keys.inspect}")
- assert(expected_keys.all? { |name| env.fetch(name).start_with?("${{ secrets.") }, "#{step["name"]} secret env must be sourced from GitHub secrets")
-end
-
-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)
-
-preview_on = workflow_on(preview_workflow)
-pr_on = workflow_on(pr_workflow)
-preview_jobs = preview_workflow.fetch("jobs")
-pr_jobs = pr_workflow.fetch("jobs")
-gate_job = preview_jobs.fetch("preview-gate")
-image_job = pr_jobs.fetch("preview_image")
-deployment_record_job = preview_jobs.fetch("deployment_record")
-deploy_job = preview_jobs.fetch("deploy-preview")
-deployment_status_job = preview_jobs.fetch("deployment_status")
-preview_comment_job = preview_jobs.fetch("preview_comment")
-gate_steps = gate_job.fetch("steps")
-image_steps = image_job.fetch("steps")
-deployment_record_steps = deployment_record_job.fetch("steps")
-deploy_steps = deploy_job.fetch("steps")
-deployment_status_steps = deployment_status_job.fetch("steps")
-preview_comment_steps = preview_comment_job.fetch("steps")
-deploy_step_names = deploy_steps.map { |step| step["name"] }
-wrangler = lockfile.fetch("packages").fetch("node_modules/wrangler")
-
-gate_trusted_checkout = step!(gate_steps, "Checkout trusted preview resolver")
-resolve_preview = step!(gate_steps, "Resolve preview request")
-
-pr_checkout = step!(image_steps, "Checkout PR code")
-build_image = step!(image_steps, "Build preview image without secrets")
-upload_image = step!(image_steps, "Upload preview image artifact")
-
-create_deployment = step!(deployment_record_steps, "Create GitHub Deployment")
-trusted_checkout = step!(deploy_steps, "Checkout trusted preview tooling")
-download_artifact = step!(deploy_steps, "Download preview image artifact")
-verify_checksum = step!(deploy_steps, "Verify preview image artifact checksum")
-prepare = step!(deploy_steps, "Prepare trusted preview deploy workspace")
-load_image = step!(deploy_steps, "Load preview image artifact")
-push_image = step!(deploy_steps, "Push preview image to Cloudflare registry")
-configure_image = step!(deploy_steps, "Configure trusted preview image reference")
-deploy = step!(deploy_steps, "Deploy to Cloudflare Containers")
-warm_preview = step!(deploy_steps, "Warm preview container")
-collect_diagnostics = step!(deploy_steps, "Collect preview diagnostics")
-upload_diagnostics = step!(deploy_steps, "Upload preview diagnostics")
-collect_failure_diagnostics = step!(deploy_steps, "Collect preview failure diagnostics")
-upload_failure_diagnostics = step!(deploy_steps, "Upload preview failure diagnostics")
-prepare_cleanup_metadata = step!(deploy_steps, "Prepare cleanup metadata")
-store_cleanup_metadata = step!(deploy_steps, "Store cleanup metadata")
-update_deployment_status = step!(deployment_status_steps, "Update Deployment Status")
-comment_on_pr = step!(preview_comment_steps, "Comment on PR")
-
-[
- [ "preview trigger", preview_on.keys, [ "workflow_run" ] ],
- [ "preview trigger workflows", preview_on.dig("workflow_run", "workflows"), [ "Pull Request" ] ],
- [ "preview trigger types", preview_on.dig("workflow_run", "types"), [ "completed" ] ],
- [ "preview top-level permissions", preview_workflow.fetch("permissions"), EXPECTED_TOP_LEVEL_PERMISSIONS ],
- [ "preview workflow jobs", preview_jobs.keys, [ "preview-gate", "deployment_record", "deploy-preview", "deployment_status", "preview_comment" ] ],
- [ "PR workflow trigger types", pr_on.dig("pull_request", "types"), %w[opened synchronize reopened labeled] ],
- [ "PR workflow paths-ignore", pr_on.dig("pull_request", "paths-ignore"), [ "charts/**" ] ],
- [ "PR workflow permissions", pr_workflow.fetch("permissions"), EXPECTED_TOP_LEVEL_PERMISSIONS ],
- [ "PR workflow jobs", pr_jobs.keys, [ "ci", "preview_image" ] ],
- [ "preview gate permissions", gate_job.fetch("permissions"), EXPECTED_GATE_PERMISSIONS ],
- [ "preview gate timeout", gate_job.fetch("timeout-minutes"), 10 ],
- [ "preview gate should_deploy output", gate_job.dig("outputs", "should_deploy"), "${{ steps.preview.outputs.should_deploy }}" ],
- [ "preview gate artifact output", gate_job.dig("outputs", "artifact_name"), "${{ steps.preview.outputs.artifact_name }}" ],
- [ "preview gate head output", gate_job.dig("outputs", "head_sha"), "${{ steps.preview.outputs.head_sha }}" ],
- [ "preview gate fork output", gate_job.dig("outputs", "is_fork"), "${{ steps.preview.outputs.is_fork }}" ],
- [ "preview gate PR output", gate_job.dig("outputs", "pr_number"), "${{ steps.preview.outputs.pr_number }}" ],
- [ "preview gate resolution source output", gate_job.dig("outputs", "resolution_source"), "${{ steps.preview.outputs.resolution_source }}" ],
- [ "preview image needs", image_job.fetch("needs"), "ci" ],
- [ "preview image permissions", image_job.fetch("permissions"), EXPECTED_IMAGE_PERMISSIONS ],
- [ "preview image timeout", image_job.fetch("timeout-minutes"), 30 ],
- [ "image PR_NUMBER env", image_job.dig("env", "PR_NUMBER"), "${{ github.event.pull_request.number }}" ],
- [ "image HEAD_SHA env", image_job.dig("env", "HEAD_SHA"), "${{ github.event.pull_request.head.sha }}" ],
- [ "image tag env", image_job.dig("env", "IMAGE_TAG"), "sure-preview-pr-${{ github.event.pull_request.number }}:${{ github.event.pull_request.head.sha }}" ],
- [ "deployment record needs", deployment_record_job.fetch("needs"), "preview-gate" ],
- [ "deployment record if", deployment_record_job.fetch("if"), "needs.preview-gate.outputs.should_deploy == 'true'" ],
- [ "deployment record permissions", deployment_record_job.fetch("permissions"), EXPECTED_DEPLOYMENT_PERMISSIONS ],
- [ "deployment record timeout", deployment_record_job.fetch("timeout-minutes"), 5 ],
- [ "deployment record output", deployment_record_job.dig("outputs", "deployment_id"), "${{ steps.deployment.outputs.result }}" ],
- [ "deployment record HEAD_SHA env", deployment_record_job.dig("env", "HEAD_SHA"), "${{ needs.preview-gate.outputs.head_sha }}" ],
- [ "deployment record IS_FORK env", deployment_record_job.dig("env", "IS_FORK"), "${{ needs.preview-gate.outputs.is_fork }}" ],
- [ "deployment record PR_NUMBER env", deployment_record_job.dig("env", "PR_NUMBER"), "${{ needs.preview-gate.outputs.pr_number }}" ],
- [ "deploy job needs", deploy_job.fetch("needs"), [ "preview-gate", "deployment_record" ] ],
- [ "deploy job permissions", deploy_job.fetch("permissions"), EXPECTED_DEPLOY_PERMISSIONS ],
- [ "deploy job environment", environment_name(deploy_job), "preview" ],
- [ "deploy job timeout", deploy_job.fetch("timeout-minutes"), 45 ],
- [ "deploy preview output", deploy_job.dig("outputs", "preview_url"), "${{ steps.deploy.outputs.preview_url }}" ],
- [ "deploy concurrency group", deploy_job.dig("concurrency", "group"), "preview-deploy-${{ needs.preview-gate.outputs.pr_number }}" ],
- [ "deploy concurrency cancellation", deploy_job.dig("concurrency", "cancel-in-progress"), true ],
- [ "deploy ARTIFACT_NAME env", deploy_job.dig("env", "ARTIFACT_NAME"), "${{ needs.preview-gate.outputs.artifact_name }}" ],
- [ "deploy HEAD_SHA env", deploy_job.dig("env", "HEAD_SHA"), "${{ needs.preview-gate.outputs.head_sha }}" ],
- [ "deploy IS_FORK env", deploy_job.dig("env", "IS_FORK"), "${{ needs.preview-gate.outputs.is_fork }}" ],
- [ "deploy PR_NUMBER env", deploy_job.dig("env", "PR_NUMBER"), "${{ needs.preview-gate.outputs.pr_number }}" ],
- [ "deploy RESOLUTION_SOURCE env", deploy_job.dig("env", "RESOLUTION_SOURCE"), "${{ needs.preview-gate.outputs.resolution_source }}" ],
- [ "deployment status needs", deployment_status_job.fetch("needs"), [ "preview-gate", "deployment_record", "deploy-preview" ] ],
- [ "deployment status permissions", deployment_status_job.fetch("permissions"), EXPECTED_DEPLOYMENT_PERMISSIONS ],
- [ "deployment status timeout", deployment_status_job.fetch("timeout-minutes"), 5 ],
- [ "deployment status DEPLOYMENT_ID env", deployment_status_job.dig("env", "DEPLOYMENT_ID"), "${{ needs.deployment_record.outputs.deployment_id }}" ],
- [ "deployment status DEPLOY_RESULT env", deployment_status_job.dig("env", "DEPLOY_RESULT"), "${{ needs.deploy-preview.result }}" ],
- [ "deployment status PREVIEW_URL env", deployment_status_job.dig("env", "PREVIEW_URL"), "${{ needs.deploy-preview.outputs.preview_url }}" ],
- [ "preview comment needs", preview_comment_job.fetch("needs"), [ "preview-gate", "deploy-preview" ] ],
- [ "preview comment if", preview_comment_job.fetch("if"), "needs.deploy-preview.result == 'success'" ],
- [ "preview comment permissions", preview_comment_job.fetch("permissions"), EXPECTED_COMMENT_PERMISSIONS ],
- [ "preview comment timeout", preview_comment_job.fetch("timeout-minutes"), 5 ],
- [ "preview comment HEAD_SHA env", preview_comment_job.dig("env", "HEAD_SHA"), "${{ needs.preview-gate.outputs.head_sha }}" ],
- [ "preview comment PR_NUMBER env", preview_comment_job.dig("env", "PR_NUMBER"), "${{ needs.preview-gate.outputs.pr_number }}" ],
- [ "preview comment PREVIEW_URL env", preview_comment_job.dig("env", "PREVIEW_URL"), "${{ needs.deploy-preview.outputs.preview_url }}" ],
- [ "gate trusted checkout ref", gate_trusted_checkout.dig("with", "ref"), "${{ github.event.repository.default_branch }}" ],
- [ "gate trusted checkout path", gate_trusted_checkout.dig("with", "path"), "trusted-preview-resolver" ],
- [ "gate trusted checkout credentials", gate_trusted_checkout.dig("with", "persist-credentials"), false ],
- [ "PR checkout credentials", pr_checkout.dig("with", "persist-credentials"), false ],
- [ "upload artifact name", upload_image.dig("with", "name"), "preview-image-pr-${{ env.PR_NUMBER }}-${{ env.HEAD_SHA }}" ],
- [ "upload artifact retention", upload_image.dig("with", "retention-days"), 3 ],
- [ "trusted checkout ref", trusted_checkout.dig("with", "ref"), "${{ github.event.repository.default_branch }}" ],
- [ "trusted checkout path", trusted_checkout.dig("with", "path"), "trusted" ],
- [ "trusted checkout credentials", trusted_checkout.dig("with", "persist-credentials"), false ],
- [ "download artifact name", download_artifact.dig("with", "name"), "${{ env.ARTIFACT_NAME }}" ],
- [ "download artifact run id", download_artifact.dig("with", "run-id"), "${{ github.event.workflow_run.id }}" ],
- [ "download artifact token", download_artifact.dig("with", "github-token"), "${{ github.token }}" ],
- [ "download artifact path", download_artifact.dig("with", "path"), "${{ runner.temp }}/preview-image" ],
- [ "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"), 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()" ],
- [ "failure diagnostics upload name", upload_failure_diagnostics.dig("with", "name"), "preview-failure-diagnostics-pr-${{ env.PR_NUMBER }}-${{ env.HEAD_SHA }}" ],
- [ "failure diagnostics upload path", upload_failure_diagnostics.dig("with", "path"), EXPECTED_FAILURE_DIAGNOSTICS_PATH ],
- [ "failure diagnostics upload retention", upload_failure_diagnostics.dig("with", "retention-days"), 3 ],
- [ "cleanup metadata prepare if", prepare_cleanup_metadata.fetch("if"), "success()" ],
- [ "cleanup metadata upload if", store_cleanup_metadata.fetch("if"), "success()" ],
- [ "cleanup metadata upload name", store_cleanup_metadata.dig("with", "name"), "preview-cleanup-pr-${{ env.PR_NUMBER }}" ],
- [ "cleanup metadata upload path", store_cleanup_metadata.dig("with", "path"), EXPECTED_CLEANUP_METADATA_PATH ],
- [ "cleanup metadata upload retention", store_cleanup_metadata.dig("with", "retention-days"), 2 ],
- [ "Wrangler binary", wrangler.dig("bin", "wrangler"), "bin/wrangler.js" ]
-].each { |label, actual, expected| assert(actual == expected, "#{label}: expected #{actual.inspect} to equal #{expected.inspect}") }
-
-assert(pr_on.key?("pull_request"), "PR workflow must still run on pull_request")
-assert(!preview_on.key?("pull_request_target"), "privileged preview deploy workflow must not run on pull_request_target")
-assert(!preview_on.key?("pull_request"), "privileged preview deploy workflow must not run directly on pull_request")
-assert(gate_job.fetch("if").include?("github.event.workflow_run.event == 'pull_request'"), "preview gate must only accept pull_request workflow runs")
-assert(gate_job.fetch("if").include?("github.event.workflow_run.conclusion == 'success'"), "preview gate must only accept successful PR workflow runs")
-assert(image_job.fetch("if").include?("preview-cf"), "preview image build must stay gated by preview-cf")
-assert(deploy_job.fetch("if").include?("needs.preview-gate.outputs.should_deploy == 'true'"), "privileged preview deploy must depend on the gate output")
-assert(deploy_job.fetch("if").include?("needs.deployment_record.result == 'success'"), "privileged preview deploy must require deployment record success or skip")
-assert(deploy_job.fetch("if").include?("needs.deployment_record.result == 'skipped'"), "privileged preview deploy must allow skipped deployment records")
-assert(deployment_status_job.fetch("if").include?("needs.preview-gate.outputs.is_fork == 'false'"), "deployment status job must only run for same-repository PRs")
-assert(deployment_status_job.fetch("if").include?("needs.deployment_record.result == 'success'"), "deployment status job must require a created deployment")
-assert(gate_job["environment"].nil?, "preview gate must not use a protected secret-bearing environment")
-assert(image_job["environment"].nil?, "preview image build must not use a protected secret-bearing environment")
-assert(deployment_record_job["environment"].nil?, "deployment record job must not use a protected secret-bearing environment")
-assert(deployment_status_job["environment"].nil?, "deployment status job must not use a protected secret-bearing environment")
-assert(preview_comment_job["environment"].nil?, "preview comment job must not use a protected secret-bearing environment")
-assert(lockfile.dig("packages", "", "devDependencies", "wrangler"), "Wrangler must stay a root dev dependency")
-assert(lockfile.fetch("lockfileVersion") >= 3, "preview tooling lockfile must preserve npm ci integrity metadata")
-assert(wrangler.fetch("resolved").start_with?("https://registry.npmjs.org/wrangler/-/wrangler-"), "Wrangler must resolve from npm registry")
-assert(wrangler.fetch("integrity").start_with?("sha512-"), "Wrangler lockfile entry must keep npm integrity metadata")
-assert(gate_trusted_checkout.dig("with", "sparse-checkout").to_s.include?("workers/preview/deploy"), "trusted gate checkout must include preview resolver")
-assert(trusted_checkout.dig("with", "sparse-checkout").to_s.include?("workers/preview"), "trusted checkout must include preview tooling")
-assert(deploy_step_names.compact.uniq == deploy_step_names.compact, "workflow step names must stay unique for security checks")
-assert([ gate_trusted_checkout, resolve_preview ].map { |step| gate_steps.index(step) }.each_cons(2).all? { |left, right| left < right }, "gate workflow steps must checkout trusted resolver before use")
-required_deploy_order = [
- trusted_checkout,
- download_artifact,
- verify_checksum,
- prepare,
- load_image,
- push_image,
- configure_image,
- deploy,
- warm_preview,
- collect_diagnostics,
- upload_diagnostics,
- collect_failure_diagnostics,
- upload_failure_diagnostics,
- prepare_cleanup_metadata,
- store_cleanup_metadata
-]
-assert(
- required_deploy_order.map { |step| deploy_steps.index(step) }
- .each_cons(2).all? { |left, right| left < right },
- "deploy workflow steps must preserve safe cross-run artifact deploy order"
-)
-assert(deploy_steps.none? { |step| step["name"] == "Checkout PR code" }, "privileged deploy job must not checkout PR code")
-assert(env_hash(deploy_job).keys.none? { |name| name.start_with?("CLOUDFLARE_") }, "Cloudflare secrets must not be job-wide")
-assert(env_hash(gate_job).keys.none? { |name| name.start_with?("CLOUDFLARE_") }, "preview gate must not receive Cloudflare secrets")
-assert(env_hash(image_job).keys.none? { |name| name.start_with?("CLOUDFLARE_") }, "preview image build must not receive Cloudflare secrets")
-assert(env_hash(deployment_record_job).keys.none? { |name| name.start_with?("CLOUDFLARE_") }, "deployment record job must not receive Cloudflare secrets")
-assert(env_hash(deployment_status_job).keys.none? { |name| name.start_with?("CLOUDFLARE_") }, "deployment status job must not receive Cloudflare secrets")
-assert(env_hash(preview_comment_job).keys.none? { |name| name.start_with?("CLOUDFLARE_") }, "preview comment job must not receive Cloudflare secrets")
-assert(upload_image.dig("with", "path").to_s.include?("sure-preview-image.tar.gz"), "preview image artifact must include the image archive")
-assert(upload_image.dig("with", "path").to_s.include?("sure-preview-image.sha256"), "preview image artifact must include the checksum")
-assert(upload_image.dig("with", "path").to_s.include?("sure-preview-image.manifest.json"), "preview image artifact must include the manifest")
-
-assert_pinned_actions!(gate_steps)
-assert_pinned_actions!(image_steps)
-assert_pinned_actions!(deployment_record_steps)
-assert_pinned_actions!(deploy_steps)
-assert_pinned_actions!(deployment_status_steps)
-assert_pinned_actions!(preview_comment_steps)
-assert_no_inline_expressions!(gate_steps)
-assert_no_inline_expressions!(image_steps)
-assert_no_inline_expressions!(deployment_record_steps)
-assert_no_inline_expressions!(deploy_steps)
-assert_no_inline_expressions!(deployment_status_steps)
-assert_no_inline_expressions!(preview_comment_steps)
-
-all_steps = gate_steps + image_steps + deployment_record_steps + deploy_steps + deployment_status_steps + preview_comment_steps
-assert(all_steps.none? { |step| env_hash(step).values.join("\n").match?(INLINE_SECRET_EXPRESSION) && ![ push_image, deploy ].include?(step) }, "only Cloudflare steps may reference GitHub secrets")
-assert(deploy_steps.none? { |step| normalized_working_directory(step["working-directory"]).match?(PR_CONTROLLED_WORKDIR) }, "privileged deploy steps must not run from PR-controlled dirs")
-assert(deploy_steps.none? { |step| run(step).include?("npx wrangler") }, "privileged deploy workflow must not use npx wrangler")
-assert(deploy_steps.none? { |step| run(step).match?(/Dockerfile\.preview|docker build|docker save/) }, "privileged deploy job must not build PR Dockerfiles")
-assert(deploy_steps.none? { |step| run(step).include?("${GITHUB_WORKSPACE}/pr") || run(step).include?(" pr/") }, "privileged deploy job must not reference PR checkout paths")
-assert((deployment_record_steps + deployment_status_steps + preview_comment_steps).none? { |step| [ step["uses"], run(step) ].compact.join("\n").include?("download-artifact") }, "GitHub write jobs must not download PR artifacts")
-assert((deployment_record_steps + deployment_status_steps + preview_comment_steps).none? { |step| run(step).match?(/docker |wrangler|npm /) }, "GitHub write jobs must not execute preview artifact, deploy, or package tooling")
-assert(image_steps.none? { |step| env_hash(step).keys.any? { |key| key.start_with?("CLOUDFLARE_") } }, "preview image workflow must not expose Cloudflare secret env")
-assert(image_steps.none? { |step| [ run(step), env_hash(step).values.join("\n") ].join("\n").match?(INLINE_SECRET_EXPRESSION) }, "preview image workflow must not reference GitHub secrets")
-
-assert_run_includes(
- resolve_preview,
- "require('./trusted-preview-resolver/workers/preview/deploy/resolve_preview_request.cjs')",
- "resolvePreviewRequest({ github, context, core })"
-)
-
-[
- "workflowRun.pull_requests?.[0]",
- "github.rest.repos.listPullRequestsAssociatedWithCommit",
- "parsePreviewArtifactName",
- "artifactPullRequestNumbers.length === 1",
- "conflicts with workflow_run PR",
- "conflicts with commit-associated PRs",
- "pullRequest.head.sha !== headSha",
- "is stale for PR",
- "preview-cf",
- "filename.startsWith(\".github/workflows/\")",
- "preview-image-pr-${prNumber}-${headSha}",
- "!item.expired",
- "core.setOutput(\"artifact_name\", artifactName)",
- "core.setOutput(\"is_fork\", String(isFork))",
- "core.setOutput(\"resolution_source\", selected.source)",
- "core.setOutput(\"should_deploy\", \"true\")"
-].each { |needle| assert(resolver_script.include?(needle), "preview resolver must include #{needle.inspect}") }
-
-artifact_resolution_index = resolver_script.index("artifactPullRequestNumbers.length === 1")
-workflow_run_fallback_index = resolver_script.index("if (workflowRunPullRequestNumber)")
-assert(artifact_resolution_index, "preview resolver must inspect exact preview artifacts")
-assert(workflow_run_fallback_index, "preview resolver must retain workflow_run PR fallback")
-assert(
- artifact_resolution_index < workflow_run_fallback_index,
- "preview resolver must prefer exact preview artifacts before workflow_run PR metadata"
-)
-assert(File.executable?(REDACTION_HELPER_PATH), "preview log redaction helper must be executable")
-[
- "registry\\.cloudflare\\.com/[^/]+/",
- "Authorization|Proxy-Authorization",
- "X-Auth-Key|X-Auth-Email|X-Api-Key|Api-Key",
- "api_key|access_token|refresh_token|auth_token|key|private_key",
- "CLOUDFLARE_ACCOUNT_ID=",
- "CLOUDFLARE_API_TOKEN|API_KEY|ACCESS_TOKEN|REFRESH_TOKEN|AUTH_TOKEN|PRIVATE_KEY",
- "",
- ""
-].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")
-assert(prepare_run.include?('preview_dir="$RUNNER_TEMP/sure-preview-worker"'), "trusted workspace must be created under RUNNER_TEMP")
-assert(deploy_steps.select { |step| run(step).match?(/npm (ci|install)/) }.map { |step| step["name"] } == [ prepare["name"] ], "only prepare may install deploy tooling")
-
-image_build_run = assert_run_includes(build_image, *REQUIRED_IMAGE_BUILD_LINES)
-assert(image_build_run.include?("set -euo pipefail"), "preview image build must fail closed")
-assert(!image_build_run.include?("CLOUDFLARE_"), "preview image build must not receive Cloudflare secrets")
-assert(!image_build_run.include?('cat > "$manifest_file" < kept.has(record))",
- 'const PREVIEW_DIAGNOSTICS_NONCE = "${PREVIEW_DIAGNOSTICS_NONCE}"',
- "PREVIEW_DIAGNOSTICS_NONCE",
- 'request.headers.get("x-preview-diagnostics-nonce")',
- "return new Response(\"not found\", { status: 404 })",
- "timings: PreviewTimings",
- "buildPreviewTimings",
- "const previewReady = sampleDataReady && railsResponding",
- "previewReadyAt",
- "secondsToRailsReady",
- "secondsToDemoDataReady",
- "secondsFromRailsReadyToDemoDataReady",
- "secondsToPreviewReady"
-].each { |needle| assert(preview_worker_script.include?(needle), "preview worker must include #{needle.inspect}") }
-
-[
- '[ -n "$PREVIEW_ORIGIN" ] && [ -n "$PREVIEW_DIAGNOSTICS_NONCE" ]',
- '-H "x-preview-diagnostics-nonce: $PREVIEW_DIAGNOSTICS_NONCE"'
-].each { |needle| assert(preview_dockerfile.include?(needle), "preview Dockerfile entrypoint must include #{needle.inspect}") }
-
-secret_steps = deploy_steps.select { |step| env_hash(step).then { |env| env.key?("CLOUDFLARE_API_TOKEN") || env.key?("CLOUDFLARE_ACCOUNT_ID") } }
-assert(secret_steps.map { |step| step["name"] } == [ push_image["name"], deploy["name"] ], "only image push and deploy may receive Cloudflare secrets")
-assert_secret_env_sources!(push_image, EXPECTED_PUSH_SECRET_ENV)
-assert_secret_env_sources!(deploy, EXPECTED_DEPLOY_SECRET_ENV)
-secret_steps.each do |step|
- assert(step["working-directory"].nil?, "#{step["name"]} must not run from a PR-controlled working directory")
- assert(!run(step).match?(/npx wrangler|npm (ci|install)|docker build|docker save|docker run/), "#{step["name"]} must not execute PR-controlled build or package tooling with secrets")
-end
-
-puts "preview-deploy security check passed"
diff --git a/test/javascript/preview_deploy/render_preview_config_test.cjs b/test/javascript/preview_deploy/render_preview_config_test.cjs
deleted file mode 100644
index 70deab73f..000000000
--- a/test/javascript/preview_deploy/render_preview_config_test.cjs
+++ /dev/null
@@ -1,100 +0,0 @@
-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), "");
- });
-});
diff --git a/test/javascript/preview_deploy/resolve_preview_request_test.cjs b/test/javascript/preview_deploy/resolve_preview_request_test.cjs
deleted file mode 100644
index a3fb32a1f..000000000
--- a/test/javascript/preview_deploy/resolve_preview_request_test.cjs
+++ /dev/null
@@ -1,345 +0,0 @@
-const assert = require("node:assert/strict");
-const { describe, it } = require("node:test");
-
-const {
- parsePreviewArtifactName,
- resolvePreviewRequest,
- selectPullRequestNumber,
-} = require("../../../workers/preview/deploy/resolve_preview_request.cjs");
-
-function contextFor(workflowRun) {
- return {
- repo: {
- owner: "we-promise",
- repo: "sure",
- },
- payload: {
- workflow_run: workflowRun,
- },
- };
-}
-
-function previewArtifact(prNumber, headSha, extra = {}) {
- return {
- name: `preview-image-pr-${prNumber}-${headSha}`,
- expired: false,
- ...extra,
- };
-}
-
-function openPullRequest(number, headSha, fullName = "we-promise/sure", extra = {}) {
- return {
- number,
- state: "open",
- labels: [{ name: "preview-cf" }],
- head: {
- sha: headSha,
- repo: {
- full_name: fullName,
- },
- },
- base: {
- repo: {
- full_name: "we-promise/sure",
- },
- },
- ...extra,
- };
-}
-
-function fakeGithub({ artifacts = [], associatedPullRequests = [], pullRequest, files = [] }) {
- return {
- paginate: async (endpoint, params) => {
- if (endpoint.endpointName === "listWorkflowRunArtifacts") {
- assert.equal(params.run_id, 123);
- return artifacts;
- }
-
- if (endpoint.endpointName === "listFiles") {
- return files;
- }
-
- throw new Error(`unexpected paginate endpoint ${endpoint.endpointName}`);
- },
- rest: {
- actions: {
- listWorkflowRunArtifacts: { endpointName: "listWorkflowRunArtifacts" },
- },
- pulls: {
- get: async ({ pull_number }) => {
- assert.equal(pull_number, pullRequest.number);
- return { data: pullRequest };
- },
- listFiles: { endpointName: "listFiles" },
- },
- repos: {
- listPullRequestsAssociatedWithCommit: async () => ({ data: associatedPullRequests }),
- },
- },
- };
-}
-
-function fakeCore() {
- const outputs = {};
- const messages = [];
- let failure = null;
-
- return {
- core: {
- info: (message) => messages.push(message),
- setFailed: (message) => {
- failure = message;
- },
- setOutput: (name, value) => {
- outputs[name] = value;
- },
- },
- get failure() {
- return failure;
- },
- messages,
- outputs,
- };
-}
-
-describe("parsePreviewArtifactName", () => {
- it("parses preview image artifact names", () => {
- const parsed = parsePreviewArtifactName("preview-image-pr-2017-4f1159e99c7785bc370f53510284c251fabdb75b");
-
- assert.deepEqual(parsed, {
- prNumber: 2017,
- headSha: "4f1159e99c7785bc370f53510284c251fabdb75b",
- });
- });
-
- it("rejects malformed names", () => {
- assert.equal(parsePreviewArtifactName("preview-image-pr-0-4f1159e99c7785bc370f53510284c251fabdb75b"), null);
- assert.equal(parsePreviewArtifactName("preview-image-pr-2017-notasha"), null);
- assert.equal(parsePreviewArtifactName("other-artifact"), null);
- });
-});
-
-describe("selectPullRequestNumber", () => {
- const headSha = "4f1159e99c7785bc370f53510284c251fabdb75b";
- const context = contextFor({ id: 123, head_sha: headSha });
-
- it("prefers the preview artifact when commit association matches", () => {
- const selected = selectPullRequestNumber({
- runPullRequest: undefined,
- artifacts: [previewArtifact(2017, headSha)],
- associatedPullRequests: [openPullRequest(2017, headSha, "Rene0422/sure")],
- context,
- headSha,
- });
-
- assert.deepEqual(selected, {
- prNumber: 2017,
- source: "artifact_name+commit_association",
- });
- });
-
- it("records workflow_run as a corroborating source when it matches the preview artifact", () => {
- const selected = selectPullRequestNumber({
- runPullRequest: { number: 2017 },
- artifacts: [previewArtifact(2017, headSha)],
- associatedPullRequests: [],
- context,
- headSha,
- });
-
- assert.deepEqual(selected, {
- prNumber: 2017,
- source: "artifact_name+workflow_run",
- });
- });
-
- it("records workflow_run and commit association when both match the preview artifact", () => {
- const selected = selectPullRequestNumber({
- runPullRequest: { number: 2017 },
- artifacts: [previewArtifact(2017, headSha)],
- associatedPullRequests: [openPullRequest(2017, headSha, "Rene0422/sure")],
- context,
- headSha,
- });
-
- assert.deepEqual(selected, {
- prNumber: 2017,
- source: "artifact_name+workflow_run+commit_association",
- });
- });
-
- it("uses a matching artifact when the same head SHA is associated with more than one PR", () => {
- const selected = selectPullRequestNumber({
- runPullRequest: undefined,
- artifacts: [previewArtifact(2060, headSha)],
- associatedPullRequests: [
- openPullRequest(2059, headSha),
- openPullRequest(2060, headSha),
- ],
- context,
- headSha,
- });
-
- assert.deepEqual(selected, {
- prNumber: 2060,
- source: "artifact_name+commit_association",
- });
- });
-
- it("fails closed when workflow metadata disagrees with the preview artifact", () => {
- const selected = selectPullRequestNumber({
- runPullRequest: { number: 1985 },
- artifacts: [previewArtifact(1798, headSha)],
- associatedPullRequests: [openPullRequest(1798, headSha)],
- context,
- headSha,
- });
-
- assert.equal(selected.prNumber, undefined);
- assert.equal(typeof selected.error, "string");
- assert.match(selected.error, /conflicts with workflow_run PR 1985/);
- });
-
- it("fails closed when commit association disagrees with the preview artifact", () => {
- const selected = selectPullRequestNumber({
- runPullRequest: undefined,
- artifacts: [previewArtifact(1798, headSha)],
- associatedPullRequests: [openPullRequest(1985, headSha)],
- context,
- headSha,
- });
-
- assert.equal(selected.prNumber, undefined);
- assert.equal(typeof selected.error, "string");
- assert.match(selected.error, /conflicts with commit-associated PRs 1985/);
- });
-
- it("refuses ambiguous associated PRs without a single matching artifact", () => {
- const selected = selectPullRequestNumber({
- runPullRequest: undefined,
- artifacts: [],
- associatedPullRequests: [
- openPullRequest(2059, headSha),
- openPullRequest(2060, headSha),
- ],
- context,
- headSha,
- });
-
- assert.match(selected.error, /multiple open pull requests/);
- });
-});
-
-describe("resolvePreviewRequest", () => {
- const headSha = "4f1159e99c7785bc370f53510284c251fabdb75b";
- const workflowRun = {
- id: 123,
- head_sha: headSha,
- pull_requests: [],
- };
-
- it("resolves fork PRs from commit association and marks deployment creation as skippable", async () => {
- const pullRequest = openPullRequest(2017, headSha, "Rene0422/sure");
- const state = fakeCore();
- const github = fakeGithub({
- artifacts: [previewArtifact(2017, headSha)],
- associatedPullRequests: [pullRequest],
- pullRequest,
- });
-
- await resolvePreviewRequest({ github, context: contextFor(workflowRun), core: state.core });
-
- assert.equal(state.failure, null);
- assert.equal(state.outputs.should_deploy, "true");
- assert.equal(state.outputs.pr_number, "2017");
- assert.equal(state.outputs.head_sha, headSha);
- assert.equal(state.outputs.artifact_name, `preview-image-pr-2017-${headSha}`);
- assert.equal(state.outputs.is_fork, "true");
- assert.equal(state.outputs.resolution_source, "artifact_name+commit_association");
- });
-
- it("resolves PRs from artifact names when workflow and commit association metadata are unavailable", async () => {
- const pullRequest = openPullRequest(2017, headSha, "Rene0422/sure");
- const state = fakeCore();
- const github = fakeGithub({
- artifacts: [previewArtifact(2017, headSha)],
- associatedPullRequests: [],
- pullRequest,
- });
-
- await resolvePreviewRequest({ github, context: contextFor(workflowRun), core: state.core });
-
- assert.equal(state.failure, null);
- assert.equal(state.outputs.should_deploy, "true");
- assert.equal(state.outputs.pr_number, "2017");
- assert.equal(state.outputs.head_sha, headSha);
- assert.equal(state.outputs.artifact_name, `preview-image-pr-2017-${headSha}`);
- assert.equal(state.outputs.is_fork, "true");
- assert.equal(state.outputs.resolution_source, "artifact_name");
- assert.match(state.messages.join("\n"), /Resolved PR 2017 from artifact_name; fork=true/);
- });
-
- it("treats stale workflow runs as successful no-ops", async () => {
- const currentHeadSha = "c79a325513160e651680170f817d802395c38d86";
- const pullRequest = openPullRequest(2060, currentHeadSha);
- const state = fakeCore();
- const github = fakeGithub({
- artifacts: [previewArtifact(2060, headSha)],
- associatedPullRequests: [openPullRequest(2060, headSha)],
- pullRequest,
- });
-
- await resolvePreviewRequest({ github, context: contextFor(workflowRun), core: state.core });
-
- assert.equal(state.failure, null);
- assert.equal(state.outputs.should_deploy, "false");
- assert.match(state.messages.join("\n"), /is stale for PR 2060/);
- });
-
- it("fails closed when a labeled PR changed workflow files", async () => {
- const pullRequest = openPullRequest(2060, headSha);
- const state = fakeCore();
- const github = fakeGithub({
- artifacts: [previewArtifact(2060, headSha)],
- associatedPullRequests: [pullRequest],
- pullRequest,
- files: [{ filename: ".github/workflows/pr.yml" }],
- });
-
- await resolvePreviewRequest({ github, context: contextFor(workflowRun), core: state.core });
-
- assert.match(state.failure, /base-trusted workflow definitions/);
- assert.equal(state.outputs.should_deploy, "false");
- });
-
- it("fails closed when the expected artifact is missing", async () => {
- const pullRequest = openPullRequest(2060, headSha);
- const state = fakeCore();
- const github = fakeGithub({
- artifacts: [],
- associatedPullRequests: [pullRequest],
- pullRequest,
- });
-
- await resolvePreviewRequest({ github, context: contextFor(workflowRun), core: state.core });
-
- assert.match(state.failure, /did not publish preview-image-pr-2060-/);
- assert.equal(state.outputs.should_deploy, "false");
- });
-
- it("skips PRs without the preview label before requiring an artifact", async () => {
- const pullRequest = openPullRequest(2060, headSha, "we-promise/sure", { labels: [] });
- const state = fakeCore();
- const github = fakeGithub({
- artifacts: [],
- associatedPullRequests: [pullRequest],
- pullRequest,
- });
-
- await resolvePreviewRequest({ github, context: contextFor(workflowRun), core: state.core });
-
- assert.equal(state.failure, null);
- assert.equal(state.outputs.should_deploy, "false");
- assert.match(state.messages.join("\n"), /does not have the preview-cf label/);
- });
-});
diff --git a/workers/preview/deploy/redact_preview_log.sh b/workers/preview/deploy/redact_preview_log.sh
deleted file mode 100755
index 40633ff4e..000000000
--- a/workers/preview/deploy/redact_preview_log.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-perl -pe '
- s#registry\.cloudflare\.com/[^/]+/#registry.cloudflare.com//#g;
- s#((?:Authorization|Proxy-Authorization):\s*Bearer\s+)[^[:space:]]+#$1#gi;
- s#((?:X-Auth-Key|X-Auth-Email|X-Api-Key|Api-Key):\s*)[^[:space:]]+#$1#gi;
- s#([?&](?:token|api_key|access_token|refresh_token|auth_token|key|private_key)=)[^&[:space:]]+#$1#gi;
- s#("(?:token|api_key|access_token|refresh_token|auth_token|secret|client_secret|private_key)"\s*:\s*")[^"]*#$1#gi;
- s#(CLOUDFLARE_ACCOUNT_ID=)[^[:space:]]+#$1#g;
- s#((?:CLOUDFLARE_API_TOKEN|API_KEY|ACCESS_TOKEN|REFRESH_TOKEN|AUTH_TOKEN|PRIVATE_KEY)=)[^[:space:]]+#$1#g;
-'
diff --git a/workers/preview/deploy/render_preview_config.cjs b/workers/preview/deploy/render_preview_config.cjs
deleted file mode 100644
index 4afeed72e..000000000
--- a/workers/preview/deploy/render_preview_config.cjs
+++ /dev/null
@@ -1,114 +0,0 @@
-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 ");
- }
-
- 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 ");
- }
-
- 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,
-};
diff --git a/workers/preview/deploy/resolve_preview_request.cjs b/workers/preview/deploy/resolve_preview_request.cjs
deleted file mode 100644
index 596a1e3c0..000000000
--- a/workers/preview/deploy/resolve_preview_request.cjs
+++ /dev/null
@@ -1,230 +0,0 @@
-const PREVIEW_ARTIFACT_PATTERN = /^preview-image-pr-([1-9][0-9]*)-([a-f0-9]{40})$/;
-
-function parsePreviewArtifactName(name) {
- const match = PREVIEW_ARTIFACT_PATTERN.exec(name);
- if (!match) return null;
-
- return {
- prNumber: Number(match[1]),
- headSha: match[2],
- };
-}
-
-function repoFullName(context) {
- return `${context.repo.owner}/${context.repo.repo}`;
-}
-
-function labelsIncludePreview(pullRequest) {
- return pullRequest.labels.some((label) => label.name === "preview-cf");
-}
-
-function artifactCandidates(artifacts, headSha) {
- return artifacts
- .filter((artifact) => !artifact.expired)
- .map((artifact) => ({
- artifact,
- parsed: parsePreviewArtifactName(artifact.name),
- }))
- .filter((candidate) => candidate.parsed?.headSha === headSha);
-}
-
-function uniqueNumbers(candidates) {
- return [...new Set(candidates.map((candidate) => candidate.parsed.prNumber))];
-}
-
-function uniquePullRequestNumbers(pullRequests) {
- return [...new Set(pullRequests.map((pullRequest) => pullRequest.number))];
-}
-
-function associatedPullRequestsForHead(associatedPullRequests, context, headSha) {
- const baseRepo = repoFullName(context);
-
- return associatedPullRequests.filter((pullRequest) => (
- pullRequest.state === "open" &&
- pullRequest.head?.sha === headSha &&
- pullRequest.base?.repo?.full_name === baseRepo
- ));
-}
-
-function selectPullRequestNumber({ runPullRequest, artifacts, associatedPullRequests, context, headSha }) {
- const associatedHeadPullRequests = associatedPullRequestsForHead(associatedPullRequests, context, headSha);
- const associatedPullRequestNumbers = uniquePullRequestNumbers(associatedHeadPullRequests);
- const artifactPullRequestNumbers = uniqueNumbers(artifactCandidates(artifacts, headSha));
- const workflowRunPullRequestNumber = runPullRequest?.number ?? null;
-
- if (artifactPullRequestNumbers.length > 1) {
- return {
- error: `Workflow run ${headSha} published preview artifacts for multiple pull requests`,
- };
- }
-
- if (artifactPullRequestNumbers.length === 1) {
- const artifactPullRequestNumber = artifactPullRequestNumbers[0];
-
- if (workflowRunPullRequestNumber && workflowRunPullRequestNumber !== artifactPullRequestNumber) {
- return {
- error: `Preview artifact PR ${artifactPullRequestNumber} conflicts with workflow_run PR ${workflowRunPullRequestNumber}`,
- };
- }
-
- if (
- associatedPullRequestNumbers.length > 0 &&
- !associatedPullRequestNumbers.includes(artifactPullRequestNumber)
- ) {
- return {
- error: `Preview artifact PR ${artifactPullRequestNumber} conflicts with commit-associated PRs ${associatedPullRequestNumbers.join(", ")}`,
- };
- }
-
- const corroboratingSources = [];
- if (workflowRunPullRequestNumber === artifactPullRequestNumber) corroboratingSources.push("workflow_run");
- if (associatedPullRequestNumbers.includes(artifactPullRequestNumber)) {
- corroboratingSources.push("commit_association");
- }
-
- return {
- prNumber: artifactPullRequestNumber,
- source:
- corroboratingSources.length > 0
- ? `artifact_name+${corroboratingSources.join("+")}`
- : "artifact_name",
- };
- }
-
- if (workflowRunPullRequestNumber) {
- if (
- associatedPullRequestNumbers.length > 0 &&
- !associatedPullRequestNumbers.includes(workflowRunPullRequestNumber)
- ) {
- return {
- error: `workflow_run PR ${workflowRunPullRequestNumber} conflicts with commit-associated PRs ${associatedPullRequestNumbers.join(", ")}`,
- };
- }
-
- return {
- prNumber: workflowRunPullRequestNumber,
- source: "workflow_run",
- };
- }
-
- if (associatedHeadPullRequests.length === 1) {
- return {
- prNumber: associatedHeadPullRequests[0].number,
- source: "commit_association",
- };
- }
-
- if (associatedHeadPullRequests.length > 1) {
- return {
- error: `Workflow run head SHA ${headSha} is associated with multiple open pull requests and no single preview artifact matched`,
- };
- }
-
- return {
- prNumber: null,
- source: "none",
- };
-}
-
-async function resolvePreviewRequest({ github, context, core }) {
- const workflowRun = context.payload.workflow_run;
- const runPullRequest = workflowRun.pull_requests?.[0];
- const headSha = workflowRun.head_sha;
-
- core.setOutput("should_deploy", "false");
-
- const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
- owner: context.repo.owner,
- repo: context.repo.repo,
- run_id: workflowRun.id,
- per_page: 100,
- });
-
- const { data: associatedPullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
- owner: context.repo.owner,
- repo: context.repo.repo,
- commit_sha: headSha,
- });
-
- const selected = selectPullRequestNumber({
- runPullRequest,
- artifacts,
- associatedPullRequests,
- context,
- headSha,
- });
-
- if (selected.error) {
- core.setFailed(selected.error);
- return;
- }
-
- if (!selected.prNumber) {
- core.info("Workflow run is not associated with an open pull request");
- return;
- }
-
- const prNumber = selected.prNumber;
- const { data: pullRequest } = await github.rest.pulls.get({
- owner: context.repo.owner,
- repo: context.repo.repo,
- pull_number: prNumber,
- });
-
- if (pullRequest.state !== "open") {
- core.info(`PR ${prNumber} is ${pullRequest.state}; skipping preview deploy`);
- return;
- }
-
- if (pullRequest.head.sha !== headSha) {
- core.info(`Workflow run head SHA ${headSha} is stale for PR ${prNumber}; current head is ${pullRequest.head.sha}`);
- return;
- }
-
- const hasPreviewLabel = labelsIncludePreview(pullRequest);
- if (!hasPreviewLabel) {
- core.info(`PR ${prNumber} does not have the preview-cf label`);
- return;
- }
-
- const files = await github.paginate(github.rest.pulls.listFiles, {
- owner: context.repo.owner,
- repo: context.repo.repo,
- pull_number: prNumber,
- per_page: 100,
- });
- const workflowChanges = files
- .map((file) => file.filename)
- .filter((filename) => filename.startsWith(".github/workflows/"));
-
- if (workflowChanges.length > 0) {
- core.setFailed(`Preview deployment requires base-trusted workflow definitions; changed workflow files: ${workflowChanges.join(", ")}`);
- return;
- }
-
- const artifactName = `preview-image-pr-${prNumber}-${headSha}`;
- const artifact = artifacts.find((item) => item.name === artifactName && !item.expired);
-
- if (!artifact) {
- core.setFailed(`Pull Request workflow run ${workflowRun.id} did not publish ${artifactName}`);
- return;
- }
-
- const isFork = pullRequest.head.repo?.full_name !== repoFullName(context);
- core.info(`Resolved PR ${prNumber} from ${selected.source}; fork=${isFork}`);
-
- core.setOutput("artifact_name", artifactName);
- core.setOutput("head_sha", headSha);
- core.setOutput("is_fork", String(isFork));
- core.setOutput("pr_number", String(prNumber));
- core.setOutput("resolution_source", selected.source);
- core.setOutput("should_deploy", "true");
-}
-
-module.exports = {
- artifactCandidates,
- associatedPullRequestsForHead,
- parsePreviewArtifactName,
- resolvePreviewRequest,
- selectPullRequestNumber,
-};
diff --git a/workers/preview/package-lock.json b/workers/preview/package-lock.json
deleted file mode 100644
index ffa57e247..000000000
--- a/workers/preview/package-lock.json
+++ /dev/null
@@ -1,1584 +0,0 @@
-{
- "name": "sure-preview-worker",
- "version": "1.0.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "sure-preview-worker",
- "version": "1.0.0",
- "devDependencies": {
- "@cloudflare/containers": "^0.3.3",
- "@cloudflare/workers-types": "^4.20250124.0",
- "typescript": "^5.0.0",
- "wrangler": "^4.103.0"
- }
- },
- "node_modules/@cloudflare/containers": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/@cloudflare/containers/-/containers-0.3.3.tgz",
- "integrity": "sha512-ZSXmArCoo5bVTp8pGAJdl5WKmwtZDcffJqr4JcZEbSmMIFjU+AlBqgysuxXMgu03Rp239cOdqerbjK7H0K2krQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/@cloudflare/kv-asset-handler": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
- "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==",
- "dev": true,
- "license": "MIT OR Apache-2.0",
- "engines": {
- "node": ">=22.0.0"
- }
- },
- "node_modules/@cloudflare/unenv-preset": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz",
- "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==",
- "dev": true,
- "license": "MIT OR Apache-2.0",
- "peerDependencies": {
- "unenv": "2.0.0-rc.24",
- "workerd": ">1.20260305.0 <2.0.0-0"
- },
- "peerDependenciesMeta": {
- "workerd": {
- "optional": true
- }
- }
- },
- "node_modules/@cloudflare/workerd-darwin-64": {
- "version": "1.20260617.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260617.1.tgz",
- "integrity": "sha512-jWwmgEVVWbsHNrLSNXzwjJaH90VzRxq1cWkQFUidxyeUPnMxemeNE8I9qFAfrpzGgE11e9sKDcE3ettJW08swQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@cloudflare/workerd-darwin-arm64": {
- "version": "1.20260617.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260617.1.tgz",
- "integrity": "sha512-LHH7b565g9znfCUOkwbec6FG2rmRbsgCy6aJiU9KN662mNheWl5sw/iKleiFSiljPKQQP3HkjnC/NSkdgi/aSA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@cloudflare/workerd-linux-64": {
- "version": "1.20260617.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260617.1.tgz",
- "integrity": "sha512-FMnaAKXe4Cfd8TQurCVd9fs2XQVBFRCsP+Id/SRdUv89MlwYu9zXfoyx6BxM+brPTIUK38SHbo8iaxiwzLi9JQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@cloudflare/workerd-linux-arm64": {
- "version": "1.20260617.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260617.1.tgz",
- "integrity": "sha512-MRoifFYcqbxxIIQy7PqO5tFY/qPFSnjXzakWl0sO93l+HLyG35jRAgOi6jfqa4kBxc7gKKtH861DcewjxUfkjA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@cloudflare/workerd-windows-64": {
- "version": "1.20260617.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260617.1.tgz",
- "integrity": "sha512-rgBV9wQrv0OSKgCTTbhFUFY3sLGNANZ88aqaLvtmEn2gmbFVb1J4PDGochVUdB7NSEp4D/ghHva6/8SZmbONpw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@cloudflare/workers-types": {
- "version": "4.20260619.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260619.1.tgz",
- "integrity": "sha512-bprsNzG0DapQPFwU2AvQlQ6FUO7Y4bKWaPBzLNI7nBSqlHsK0P62xx2NaNT6i/htzAgQspKZm1D32y0RxofrRQ==",
- "dev": true,
- "license": "MIT OR Apache-2.0"
- },
- "node_modules/@cspotcode/source-map-support": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
- "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/trace-mapping": "0.3.9"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@emnapi/runtime": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
- "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
- "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
- "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
- "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
- "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
- "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
- "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
- "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
- "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
- "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
- "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
- "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
- "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
- "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
- "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
- "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
- "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
- "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
- "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
- "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
- "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
- "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
- "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
- "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
- "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@img/colour": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
- "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@img/sharp-darwin-arm64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
- "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-arm64": "1.2.4"
- }
- },
- "node_modules/@img/sharp-darwin-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
- "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.2.4"
- }
- },
- "node_modules/@img/sharp-libvips-darwin-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
- "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
- "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
- "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
- "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-ppc64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
- "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-riscv64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
- "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-s390x": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
- "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-x64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
- "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
- "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
- "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-linux-arm": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
- "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.2.4"
- }
- },
- "node_modules/@img/sharp-linux-arm64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
- "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.2.4"
- }
- },
- "node_modules/@img/sharp-linux-ppc64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
- "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-ppc64": "1.2.4"
- }
- },
- "node_modules/@img/sharp-linux-riscv64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
- "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-riscv64": "1.2.4"
- }
- },
- "node_modules/@img/sharp-linux-s390x": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
- "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-s390x": "1.2.4"
- }
- },
- "node_modules/@img/sharp-linux-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
- "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-x64": "1.2.4"
- }
- },
- "node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
- "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
- }
- },
- "node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
- "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
- }
- },
- "node_modules/@img/sharp-wasm32": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
- "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/runtime": "^1.7.0"
- },
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-arm64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
- "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-ia32": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
- "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
- "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
- "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.0.3",
- "@jridgewell/sourcemap-codec": "^1.4.10"
- }
- },
- "node_modules/@poppinss/colors": {
- "version": "4.1.6",
- "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
- "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "kleur": "^4.1.5"
- }
- },
- "node_modules/@poppinss/dumper": {
- "version": "0.6.5",
- "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",
- "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@poppinss/colors": "^4.1.5",
- "@sindresorhus/is": "^7.0.2",
- "supports-color": "^10.0.0"
- }
- },
- "node_modules/@poppinss/exception": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz",
- "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@sindresorhus/is": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",
- "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/is?sponsor=1"
- }
- },
- "node_modules/@speed-highlight/core": {
- "version": "1.2.17",
- "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz",
- "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==",
- "dev": true,
- "license": "CC0-1.0"
- },
- "node_modules/blake3-wasm": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
- "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cookie": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
- "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/error-stack-parser-es": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
- "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/esbuild": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
- "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.1",
- "@esbuild/android-arm": "0.28.1",
- "@esbuild/android-arm64": "0.28.1",
- "@esbuild/android-x64": "0.28.1",
- "@esbuild/darwin-arm64": "0.28.1",
- "@esbuild/darwin-x64": "0.28.1",
- "@esbuild/freebsd-arm64": "0.28.1",
- "@esbuild/freebsd-x64": "0.28.1",
- "@esbuild/linux-arm": "0.28.1",
- "@esbuild/linux-arm64": "0.28.1",
- "@esbuild/linux-ia32": "0.28.1",
- "@esbuild/linux-loong64": "0.28.1",
- "@esbuild/linux-mips64el": "0.28.1",
- "@esbuild/linux-ppc64": "0.28.1",
- "@esbuild/linux-riscv64": "0.28.1",
- "@esbuild/linux-s390x": "0.28.1",
- "@esbuild/linux-x64": "0.28.1",
- "@esbuild/netbsd-arm64": "0.28.1",
- "@esbuild/netbsd-x64": "0.28.1",
- "@esbuild/openbsd-arm64": "0.28.1",
- "@esbuild/openbsd-x64": "0.28.1",
- "@esbuild/openharmony-arm64": "0.28.1",
- "@esbuild/sunos-x64": "0.28.1",
- "@esbuild/win32-arm64": "0.28.1",
- "@esbuild/win32-ia32": "0.28.1",
- "@esbuild/win32-x64": "0.28.1"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/kleur": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
- "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/miniflare": {
- "version": "4.20260617.1",
- "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260617.1.tgz",
- "integrity": "sha512-Go3/gzStm99QHptsSgU+q1S+xDfLoRgwjJNY80kaTVi0ENhTyqKq+sc4xZiWBSbM7uUcJwmzm8+QFKtcYLJ9nw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@cspotcode/source-map-support": "0.8.1",
- "sharp": "0.34.5",
- "undici": "7.28.0",
- "workerd": "1.20260617.1",
- "ws": "8.21.0",
- "youch": "4.1.0-beta.10"
- },
- "bin": {
- "miniflare": "bootstrap.js"
- },
- "engines": {
- "node": ">=22.0.0"
- }
- },
- "node_modules/path-to-regexp": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
- "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/semver": {
- "version": "7.8.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
- "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/sharp": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
- "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
- "dev": true,
- "hasInstallScript": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@img/colour": "^1.0.0",
- "detect-libc": "^2.1.2",
- "semver": "^7.7.3"
- },
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-darwin-arm64": "0.34.5",
- "@img/sharp-darwin-x64": "0.34.5",
- "@img/sharp-libvips-darwin-arm64": "1.2.4",
- "@img/sharp-libvips-darwin-x64": "1.2.4",
- "@img/sharp-libvips-linux-arm": "1.2.4",
- "@img/sharp-libvips-linux-arm64": "1.2.4",
- "@img/sharp-libvips-linux-ppc64": "1.2.4",
- "@img/sharp-libvips-linux-riscv64": "1.2.4",
- "@img/sharp-libvips-linux-s390x": "1.2.4",
- "@img/sharp-libvips-linux-x64": "1.2.4",
- "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
- "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
- "@img/sharp-linux-arm": "0.34.5",
- "@img/sharp-linux-arm64": "0.34.5",
- "@img/sharp-linux-ppc64": "0.34.5",
- "@img/sharp-linux-riscv64": "0.34.5",
- "@img/sharp-linux-s390x": "0.34.5",
- "@img/sharp-linux-x64": "0.34.5",
- "@img/sharp-linuxmusl-arm64": "0.34.5",
- "@img/sharp-linuxmusl-x64": "0.34.5",
- "@img/sharp-wasm32": "0.34.5",
- "@img/sharp-win32-arm64": "0.34.5",
- "@img/sharp-win32-ia32": "0.34.5",
- "@img/sharp-win32-x64": "0.34.5"
- }
- },
- "node_modules/supports-color": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
- "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
- "license": "0BSD",
- "optional": true
- },
- "node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/undici": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
- "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=20.18.1"
- }
- },
- "node_modules/unenv": {
- "version": "2.0.0-rc.24",
- "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
- "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "pathe": "^2.0.3"
- }
- },
- "node_modules/workerd": {
- "version": "1.20260617.1",
- "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260617.1.tgz",
- "integrity": "sha512-Re5pl6pdowt3ZmWUzGlOuB7jbRIIPetgKalmo4cYmucQnVhpo7/3e4MfpekbhLi2EhZZz5EY9NWRu8zFzuEZew==",
- "dev": true,
- "hasInstallScript": true,
- "license": "Apache-2.0",
- "bin": {
- "workerd": "bin/workerd"
- },
- "engines": {
- "node": ">=16"
- },
- "optionalDependencies": {
- "@cloudflare/workerd-darwin-64": "1.20260617.1",
- "@cloudflare/workerd-darwin-arm64": "1.20260617.1",
- "@cloudflare/workerd-linux-64": "1.20260617.1",
- "@cloudflare/workerd-linux-arm64": "1.20260617.1",
- "@cloudflare/workerd-windows-64": "1.20260617.1"
- }
- },
- "node_modules/wrangler": {
- "version": "4.103.0",
- "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.103.0.tgz",
- "integrity": "sha512-3Lv1P5t2xcSEkSTKtG+Lz+3JFryuU7YPLkaCUj7gNe+CJsjZJLtUwqsh1x595QBxkIbCE0GAvDx2DCJUU4+oqw==",
- "dev": true,
- "license": "MIT OR Apache-2.0",
- "dependencies": {
- "@cloudflare/kv-asset-handler": "0.5.0",
- "@cloudflare/unenv-preset": "2.16.1",
- "blake3-wasm": "2.1.5",
- "esbuild": "0.28.1",
- "miniflare": "4.20260617.1",
- "path-to-regexp": "6.3.0",
- "unenv": "2.0.0-rc.24",
- "workerd": "1.20260617.1"
- },
- "bin": {
- "cf-wrangler": "bin/cf-wrangler.js",
- "wrangler": "bin/wrangler.js",
- "wrangler2": "bin/wrangler.js"
- },
- "engines": {
- "node": ">=22.0.0"
- },
- "optionalDependencies": {
- "fsevents": "2.3.3"
- },
- "peerDependencies": {
- "@cloudflare/workers-types": "^4.20260617.1"
- },
- "peerDependenciesMeta": {
- "@cloudflare/workers-types": {
- "optional": true
- }
- }
- },
- "node_modules/ws": {
- "version": "8.21.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
- "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/youch": {
- "version": "4.1.0-beta.10",
- "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",
- "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@poppinss/colors": "^4.1.5",
- "@poppinss/dumper": "^0.6.4",
- "@speed-highlight/core": "^1.2.7",
- "cookie": "^1.0.2",
- "youch-core": "^0.3.3"
- }
- },
- "node_modules/youch-core": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",
- "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@poppinss/exception": "^1.2.2",
- "error-stack-parser-es": "^1.0.5"
- }
- }
- }
-}
diff --git a/workers/preview/package.json b/workers/preview/package.json
deleted file mode 100644
index 8ade8d8c9..000000000
--- a/workers/preview/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "sure-preview-worker",
- "version": "1.0.0",
- "private": true,
- "scripts": {
- "deploy": "wrangler deploy",
- "dev": "wrangler dev"
- },
- "devDependencies": {
- "@cloudflare/containers": "^0.3.3",
- "@cloudflare/workers-types": "^4.20250124.0",
- "typescript": "^5.0.0",
- "wrangler": "^4.103.0"
- }
-}
diff --git a/workers/preview/src/index.ts b/workers/preview/src/index.ts
deleted file mode 100644
index e3c901479..000000000
--- a/workers/preview/src/index.ts
+++ /dev/null
@@ -1,497 +0,0 @@
-import { Container } from "@cloudflare/containers";
-
-interface Env {
- RAILS_CONTAINER: DurableObjectNamespace;
-}
-
-interface DiagnosticPayload {
- stage?: string;
- detail?: string;
-}
-
-interface DiagnosticRecord {
- event?: string;
- at?: string;
- payload?: DiagnosticPayload;
- state?: { status?: string; lastChange?: number };
- message?: string;
-}
-
-interface PreviewProgress {
- phase: "cold" | "warming" | "loading-demo-data" | "ready" | "failed";
- stage: string | null;
- message: string;
- detail: string;
-}
-
-interface PreviewTimings {
- containerStartedAt: string | null;
- bootStartedAt: string | null;
- railsStartedAt: string | null;
- railsReadyAt: string | null;
- demoDataStartedAt: string | null;
- demoDataReadyAt: string | null;
- demoDataReadyStage: string | null;
- demoDataFailedAt: string | null;
- previewReadyAt: string | null;
- secondsToRailsReady: number | null;
- secondsToDemoDataReady: number | null;
- secondsFromRailsReadyToDemoDataReady: number | null;
- secondsToPreviewReady: number | null;
-}
-
-interface PreviewStatusPayload {
- state: unknown;
- containerRunning: boolean;
- diagnostics: DiagnosticRecord | null;
- diagnosticsHistory: DiagnosticRecord[];
- previewReady: boolean;
- previewFailed: boolean;
- timings: PreviewTimings;
- progress: PreviewProgress;
-}
-
-const DIAGNOSTICS_KEY = "preview-diagnostics";
-const DIAGNOSTICS_HISTORY_KEY = "preview-diagnostics-history";
-const DIAGNOSTICS_HISTORY_LIMIT = 50;
-const PREVIEW_DIAGNOSTICS_NONCE = "${PREVIEW_DIAGNOSTICS_NONCE}";
-const READY_STAGES = new Set(["demo-data-ready", "demo-data-skip"]);
-const FAILED_STAGES = new Set(["demo-data-failed", "failed"]);
-const TIMING_ANCHOR_STAGES = new Set([
- "boot",
- "rails-start",
- "rails-up-ready",
- "demo-data-check",
- "demo-data-deferred",
- "demo-data-load",
- "demo-data-ready",
- "demo-data-skip",
- "demo-data-failed",
- "failed",
-]);
-const WAITING_MESSAGES: Record = {
- boot: "Waking preview…",
- "redis-start": "Starting Redis…",
- "redis-ready": "Redis is ready.",
- "postgres-start": "Starting PostgreSQL…",
- "postgres-ready": "PostgreSQL is ready.",
- "postgres-already-running": "PostgreSQL is already running.",
- "db-setup": "Setting up the preview database…",
- "db-prepare": "Running database setup…",
- "db-prepare-done": "Database setup finished.",
- "demo-data-check": "Checking sample data…",
- "demo-data-user-present": "Found the demo user. Verifying sample data…",
- "demo-data-deferred": "Rails is up. Loading sample data…",
- "demo-data-load": "Loading sample data…",
- "demo-data-ready": "Sample data is ready.",
- "demo-data-skip": "Sample data is already ready.",
- "demo-data-failed": "Sample data failed to load.",
- "rails-start": "Starting Rails…",
- "rails-up-ready": "Rails is up. Finishing sample data…",
- "rails-up-timeout": "Rails is taking longer than expected to start.",
-};
-
-export class RailsContainer extends Container {
- defaultPort = 3000;
- pingEndpoint = "localhost/up";
- entrypoint = ["/rails/bin/preview-entrypoint", "bundle", "exec", "puma", "-C", "config/puma.rb"];
- envVars = {
- RAILS_ENV: "development",
- RAILS_LOG_TO_STDOUT: "true",
- RAILS_SERVE_STATIC_FILES: "true",
- SECRET_KEY_BASE: "preview-secret-key-base-for-pr-${PR_NUMBER}",
- APP_DOMAIN: "sure-preview-${PR_NUMBER}.sure-finances.workers.dev",
- APP_URL: "https://sure-preview-${PR_NUMBER}.sure-finances.workers.dev",
- RAILS_FORCE_SSL: "false",
- RAILS_ASSUME_SSL: "false",
- ACTIVE_STORAGE_SERVICE: "local",
- DISABLE_BOOTSNAP: "1",
- BINDING: "::",
- DEMO_DATA_SEED: "${PR_NUMBER}",
- PREVIEW_ORIGIN: "https://sure-preview-${PR_NUMBER}.sure-finances.workers.dev",
- PREVIEW_DIAGNOSTICS_NONCE,
- };
- sleepAfter = "30m";
- enableInternet = true;
-
- get runtimeContainer() {
- return this.ctx.container!;
- }
-
- async recordDiagnostic(payload: Record): Promise {
- const diagnostic = {
- ...payload,
- state: await this.getState(),
- } as DiagnosticRecord;
-
- await this.ctx.storage.put(DIAGNOSTICS_KEY, diagnostic);
-
- const history =
- ((await this.ctx.storage.get(DIAGNOSTICS_HISTORY_KEY)) as DiagnosticRecord[] | undefined) ?? [];
-
- history.push(diagnostic);
- await this.ctx.storage.put(DIAGNOSTICS_HISTORY_KEY, this.trimDiagnosticsHistory(history));
- }
-
- private isTimingAnchor(record: DiagnosticRecord): boolean {
- return (
- record.event === "start" ||
- (record.event === "entrypoint" &&
- typeof record.payload?.stage === "string" &&
- TIMING_ANCHOR_STAGES.has(record.payload.stage))
- );
- }
-
- private trimDiagnosticsHistory(history: DiagnosticRecord[]): DiagnosticRecord[] {
- if (history.length <= DIAGNOSTICS_HISTORY_LIMIT) return history;
-
- const anchors = history.filter((record) => this.isTimingAnchor(record)).slice(-DIAGNOSTICS_HISTORY_LIMIT);
- const anchored = new Set(anchors);
- const remainingSlots = Math.max(DIAGNOSTICS_HISTORY_LIMIT - anchors.length, 0);
- const recentNonAnchors =
- remainingSlots > 0 ? history.filter((record) => !anchored.has(record)).slice(-remainingSlots) : [];
- const kept = new Set([...anchors, ...recentNonAnchors]);
-
- return history.filter((record) => kept.has(record));
- }
-
- private isAttemptStart(record: DiagnosticRecord): boolean {
- return (
- record.event === "start" ||
- (record.event === "entrypoint" && typeof record.payload?.stage === "string" && record.payload.stage === "boot")
- );
- }
-
- private diagnosticsForLatestAttempt(allDiagnostics: DiagnosticRecord[]): DiagnosticRecord[] {
- for (let index = allDiagnostics.length - 1; index >= 0; index -= 1) {
- if (this.isAttemptStart(allDiagnostics[index])) {
- return allDiagnostics.slice(index);
- }
- }
-
- return allDiagnostics;
- }
-
- private async getDiagnostics(): Promise<{
- state: unknown;
- containerRunning: boolean;
- diagnostics: DiagnosticRecord | null;
- diagnosticsHistory: DiagnosticRecord[];
- }> {
- return {
- state: await this.getState(),
- containerRunning: this.runtimeContainer.running,
- diagnostics: ((await this.ctx.storage.get(DIAGNOSTICS_KEY)) as DiagnosticRecord | undefined) ?? null,
- diagnosticsHistory:
- ((await this.ctx.storage.get(DIAGNOSTICS_HISTORY_KEY)) as DiagnosticRecord[] | undefined) ?? [],
- };
- }
-
- private async probeRailsUp(): Promise {
- try {
- const response = await this.containerFetch(new Request("https://container.internal/up"), this.defaultPort);
- return response.ok;
- } catch {
- return false;
- }
- }
-
- private validTimestamp(value: string | undefined): string | null {
- if (!value) return null;
-
- const timestamp = Date.parse(value);
- return Number.isNaN(timestamp) ? null : value;
- }
-
- private secondsBetween(startAt: string | null, endAt: string | null): number | null {
- if (!startAt || !endAt) return null;
-
- const start = Date.parse(startAt);
- const end = Date.parse(endAt);
- if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
-
- return Math.round(((end - start) / 1000) * 100) / 100;
- }
-
- private buildPreviewTimings(attemptDiagnostics: DiagnosticRecord[], previewReady: boolean): PreviewTimings {
- const entrypointDiagnostics = attemptDiagnostics.filter(
- (item) => item.event === "entrypoint" && typeof item.payload?.stage === "string"
- );
- const firstEventAt = (event: string) =>
- this.validTimestamp(attemptDiagnostics.find((item) => item.event === event)?.at);
- const firstStageAt = (...stages: string[]) =>
- this.validTimestamp(entrypointDiagnostics.find((item) => stages.includes(item.payload?.stage ?? ""))?.at);
-
- const containerStartedAt = firstEventAt("start");
- const bootStartedAt = firstStageAt("boot") ?? containerStartedAt;
- const railsStartedAt = firstStageAt("rails-start");
- const railsReadyAt = firstStageAt("rails-up-ready");
- const demoDataStartedAt =
- firstStageAt("demo-data-load") ?? firstStageAt("demo-data-deferred") ?? firstStageAt("demo-data-check");
- const demoDataReady = entrypointDiagnostics.find((item) => READY_STAGES.has(item.payload?.stage ?? ""));
- const demoDataReadyAt = this.validTimestamp(demoDataReady?.at);
- const demoDataReadyStage = demoDataReady?.payload?.stage ?? null;
- const demoDataFailedAt = firstStageAt("demo-data-failed");
- const previewReadyAt = previewReady ? (demoDataReadyAt ?? railsReadyAt) : null;
-
- return {
- containerStartedAt,
- bootStartedAt,
- railsStartedAt,
- railsReadyAt,
- demoDataStartedAt,
- demoDataReadyAt,
- demoDataReadyStage,
- demoDataFailedAt,
- previewReadyAt,
- secondsToRailsReady: this.secondsBetween(bootStartedAt, railsReadyAt),
- secondsToDemoDataReady: this.secondsBetween(demoDataStartedAt, demoDataReadyAt),
- secondsFromRailsReadyToDemoDataReady: this.secondsBetween(railsReadyAt, demoDataReadyAt),
- secondsToPreviewReady: this.secondsBetween(bootStartedAt, previewReadyAt),
- };
- }
-
- private async buildPreviewStatus(base: {
- state: unknown;
- containerRunning: boolean;
- diagnostics: DiagnosticRecord | null;
- diagnosticsHistory: DiagnosticRecord[];
- }, options?: { probe?: boolean }): Promise {
- const allDiagnostics = [...base.diagnosticsHistory, ...(base.diagnostics ? [base.diagnostics] : [])];
- const attemptDiagnostics = this.diagnosticsForLatestAttempt(allDiagnostics);
- const entrypointDiagnostics = attemptDiagnostics.filter(
- (item) => item.event === "entrypoint" && typeof item.payload?.stage === "string"
- );
- const latestEntrypoint = entrypointDiagnostics.at(-1) ?? null;
- const latestStage = latestEntrypoint?.payload?.stage ?? null;
- const latestDetail = latestEntrypoint?.payload?.detail ?? base.diagnostics?.message ?? "";
- const sampleDataReady = entrypointDiagnostics.some((item) => READY_STAGES.has(item.payload?.stage ?? ""));
- const liveProbeReady = options?.probe ? await this.probeRailsUp() : false;
- const railsResponding =
- liveProbeReady ||
- (typeof base.state === "object" && base.state !== null && "status" in base.state
- ? (base.state as { status?: string }).status === "healthy"
- : false) ||
- entrypointDiagnostics.some((item) => item.payload?.stage === "rails-up-ready");
- const previewReady = sampleDataReady && railsResponding;
- const previewFailed =
- entrypointDiagnostics.some((item) => FAILED_STAGES.has(item.payload?.stage ?? "")) ||
- base.diagnostics?.event === "error";
- const timings = this.buildPreviewTimings(attemptDiagnostics, previewReady);
-
- let phase: PreviewProgress["phase"] = "cold";
- if (previewFailed) {
- phase = "failed";
- } else if (previewReady) {
- phase = "ready";
- } else if (
- latestStage === "demo-data-load" ||
- latestStage === "demo-data-deferred" ||
- latestStage === "rails-up-ready" ||
- latestStage === "demo-data-check" ||
- latestStage === "demo-data-user-present"
- ) {
- phase = "loading-demo-data";
- } else if (base.containerRunning || latestEntrypoint) {
- phase = "warming";
- }
-
- const message = sampleDataReady && !previewReady
- ? "Finishing preview startup…"
- : (latestStage ? WAITING_MESSAGES[latestStage] : undefined) ??
- (previewFailed
- ? "Preview startup hit an error."
- : previewReady
- ? "Preview is ready."
- : base.containerRunning
- ? "Warming preview…"
- : "Starting preview…");
-
- return {
- ...base,
- previewReady,
- previewFailed,
- timings,
- progress: {
- phase,
- stage: latestStage,
- message,
- detail: latestDetail,
- },
- };
- }
-
- private wantsHtml(request: Request): boolean {
- if (request.method !== "GET") return false;
- const accept = request.headers.get("accept") ?? "";
- const secFetchDest = request.headers.get("sec-fetch-dest") ?? "";
- return accept.includes("text/html") || secFetchDest === "document";
- }
-
- private renderWaitPage(request: Request, status: PreviewStatusPayload, errorMessage?: string): Response {
- const targetPath = new URL(request.url).pathname + new URL(request.url).search;
- const escapedTargetPath = JSON.stringify(targetPath);
- const escapedMessage = JSON.stringify(status.progress.message);
- const escapedDetail = JSON.stringify(
- status.progress.detail || errorMessage || "This preview is waking up and loading sample data."
- );
-
- const html = `
-
-
-
-
- Waking preview…
-
-
-
-
-
-
-
-
-
Please wait — this preview is cold-starting and will redirect automatically when the sample data is ready.
-
-
-
-
-
-`;
-
- return new Response(html, {
- status: status.previewFailed ? 503 : 202,
- headers: {
- "content-type": "text/html; charset=utf-8",
- "cache-control": "no-store, max-age=0",
- "retry-after": "3",
- },
- });
- }
-
- override async fetch(request: Request): Promise {
- const url = new URL(request.url);
-
- if (url.pathname === "/_container_status") {
- return Response.json(await this.buildPreviewStatus(await this.getDiagnostics(), { probe: true }));
- }
-
- if (url.pathname === "/_container_event" && request.method === "POST") {
- if (request.headers.get("x-preview-diagnostics-nonce") !== PREVIEW_DIAGNOSTICS_NONCE) {
- return new Response("not found", { status: 404 });
- }
-
- const payload = await request.json();
- await this.recordDiagnostic({
- event: "entrypoint",
- at: new Date().toISOString(),
- payload,
- });
- return new Response("ok");
- }
-
- try {
- return await this.containerFetch(request, this.defaultPort);
- } catch (error) {
- await this.recordDiagnostic({
- event: "container-fetch-error",
- at: new Date().toISOString(),
- message: error instanceof Error ? error.message : String(error),
- });
-
- const status = await this.buildPreviewStatus(await this.getDiagnostics());
- if (this.wantsHtml(request) && !status.previewReady) {
- return this.renderWaitPage(
- request,
- status,
- error instanceof Error ? error.message : String(error)
- );
- }
-
- return new Response(
- `Failed to serve preview container: ${error instanceof Error ? error.message : String(error)}`,
- { status: 500 }
- );
- }
- }
-
- override async onStart(): Promise {
- await this.recordDiagnostic({
- event: "start",
- at: new Date().toISOString(),
- });
- }
-
- override async onStop(stopParams: { exitCode?: number; reason?: string }): Promise {
- await this.recordDiagnostic({
- event: "stop",
- at: new Date().toISOString(),
- exitCode: stopParams.exitCode,
- reason: stopParams.reason,
- });
- }
-
- override async onError(error: unknown): Promise {
- console.error("Rails container error:", error);
- await this.recordDiagnostic({
- event: "error",
- at: new Date().toISOString(),
- message: error instanceof Error ? error.message : String(error),
- });
- throw error;
- }
-}
-
-export default {
- async fetch(
- request: Request,
- env: Env,
- _ctx: ExecutionContext
- ): Promise {
- const id = env.RAILS_CONTAINER.idFromName("preview");
- const container = env.RAILS_CONTAINER.get(id);
-
- return container.fetch(request);
- },
-};
diff --git a/workers/preview/tsconfig.json b/workers/preview/tsconfig.json
deleted file mode 100644
index 61f2674cb..000000000
--- a/workers/preview/tsconfig.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2022",
- "module": "ES2022",
- "moduleResolution": "bundler",
- "lib": ["ES2022"],
- "types": ["@cloudflare/workers-types"],
- "strict": true,
- "noEmit": true,
- "skipLibCheck": true
- },
- "include": ["src/**/*.ts"]
-}
diff --git a/workers/preview/wrangler.toml b/workers/preview/wrangler.toml
deleted file mode 100644
index 9845919d2..000000000
--- a/workers/preview/wrangler.toml
+++ /dev/null
@@ -1,48 +0,0 @@
-# Cloudflare Containers configuration for PR preview deployments
-# This file is used as a template - the GitHub workflow substitutes PR_NUMBER
-
-name = "sure-preview-${PR_NUMBER}"
-main = "./src/index.ts"
-compatibility_date = "2026-05-11"
-
-# Enable workers.dev subdomain for preview access
-workers_dev = true
-
-# Enable container logs
-[observability]
-enabled = true
-
-# Container configuration - uses preview Dockerfile with embedded PostgreSQL
-# Note: path is relative to this config file location
-[[containers]]
-class_name = "RailsContainer"
-image = "../../Dockerfile.preview"
-# standard-1 (1/2 vCPU, 4 GiB RAM, 8 GB disk), not basic (1/4 vCPU, 1 GiB,
-# 4 GB): this single container runs postgres + redis + puma AND generates the
-# full demo dataset (Demo::Generator: ~12 years of transactions). That
-# generation peaks just over 1 GiB, so basic OOM-kills it (exit 137) mid-boot
-# and the preview never reaches demo-data-ready. Verified on a real Cloudflare
-# standard-1 deploy: rails ready ~46s, demo data ~149s, previewReady ~195s,
-# peak well under 4 GiB, no OOM.
-instance_type = "standard-1"
-max_instances = 1
-
-# Durable Object binding for the container
-[[durable_objects.bindings]]
-name = "RAILS_CONTAINER"
-class_name = "RailsContainer"
-
-# Required migration for Durable Objects with SQLite
-[[migrations]]
-tag = "v1"
-new_sqlite_classes = ["RailsContainer"]
-
-# Environment variables passed to the Rails container
-[vars]
-RAILS_ENV = "development"
-RAILS_LOG_TO_STDOUT = "true"
-RAILS_SERVE_STATIC_FILES = "true"
-
-# Note: SECRET_KEY_BASE and DATABASE_URL are auto-generated by the entrypoint
-# for development previews. For custom configuration, set via:
-# wrangler secret put SECRET_KEY_BASE