From 6612c2021e8e3221f1cce99cf317a6329adf27c9 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:36:35 -0400 Subject: [PATCH] Keep the demo token in an environment and its address in the config Run the upload and promote jobs in the demo environment, which only main and release tags can use. Declare demo.sandobserver.com in the Worker config and apply it with triggers deploy when a version goes live, and strip the route from the first-run placeholder so a placeholder never takes the address. --- .github/workflows/demo.yml | 28 +++++++++++++++++++++++++--- scripts/demo/wrangler.jsonc | 1 + test/repo/demo-workflow.test.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 test/repo/demo-workflow.test.js diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml index 82ab4f88..1e16b344 100644 --- a/.github/workflows/demo.yml +++ b/.github/workflows/demo.yml @@ -136,6 +136,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 permissions: {} + environment: demo outputs: url: ${{ steps.version.outputs.url }} id: ${{ steps.version.outputs.id }} @@ -214,7 +215,8 @@ jobs: mkdir -p placeholder/site printf 'Stackyard demo

The demo is being prepared.

\n' > placeholder/site/index.html printf '/ /index.html 200\n' > placeholder/site/_redirects - cp upload/wrangler.jsonc placeholder/ + # Without the routes, so a placeholder never takes the demo address. + node -e 'const c = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); delete c.routes; require("fs").writeFileSync(process.argv[2], JSON.stringify(c, null, 2));' upload/wrangler.jsonc placeholder/wrangler.jsonc - name: Create the Worker if: steps.creds.outputs.set == 'true' && steps.worker.outputs.exists == 'false' @@ -303,11 +305,31 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 permissions: {} + environment: demo steps: - - name: Deploy the version + - name: Checkout the Wrangler config + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + path: repo + sparse-checkout: scripts/demo/wrangler.jsonc + sparse-checkout-cone-mode: false + + - name: Prepare the config + run: | + set -euo pipefail + mkdir -p promote/site + cp repo/scripts/demo/wrangler.jsonc promote/ + rm -rf repo + + # Version uploads leave routes alone; triggers deploy applies the demo address. + - name: Deploy the version and its address uses: cloudflare/wrangler-action@953926a2e2182532811c01a25e53647d93bf07c0 # v4.1.3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} wranglerVersion: ${{ env.WRANGLER_VERSION }} - command: versions deploy ${{ needs.upload.outputs.id }}@100 --name stackyard-demo --yes --message "Stackyard ${{ inputs.version }}" + workingDirectory: promote + command: | + versions deploy ${{ needs.upload.outputs.id }}@100 --yes --message "Stackyard ${{ inputs.version }}" + triggers deploy diff --git a/scripts/demo/wrangler.jsonc b/scripts/demo/wrangler.jsonc index 688219c6..2a8491c6 100644 --- a/scripts/demo/wrangler.jsonc +++ b/scripts/demo/wrangler.jsonc @@ -4,6 +4,7 @@ "compatibility_date": "2026-10-01", "workers_dev": true, "preview_urls": true, + "routes": [{ "pattern": "demo.sandobserver.com", "custom_domain": true }], "assets": { "directory": "./site", "html_handling": "none", diff --git a/test/repo/demo-workflow.test.js b/test/repo/demo-workflow.test.js new file mode 100644 index 00000000..b1306c4e --- /dev/null +++ b/test/repo/demo-workflow.test.js @@ -0,0 +1,33 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const yaml = require('js-yaml'); + +const root = path.join(__dirname, '..', '..'); +const wf = yaml.load(fs.readFileSync(path.join(root, '.github', 'workflows', 'demo.yml'), 'utf8')); +const config = JSON.parse(fs.readFileSync(path.join(root, 'scripts', 'demo', 'wrangler.jsonc'), 'utf8')); +const usesSecret = job => JSON.stringify(job).includes('secrets.CLOUDFLARE'); + +test('only the demo environment hands out the Cloudflare token', () => { + for (const [name, job] of Object.entries(wf.jobs)) { + if (usesSecret(job)) + assert.equal(job.environment, 'demo', `${name} reads a Cloudflare secret outside the environment`); + } + assert.ok(!usesSecret(wf.jobs.build), 'the build job runs dev dependencies'); + assert.ok(!usesSecret(wf.jobs.verify), 'the verify job runs dev dependencies'); +}); + +test('the demo address is in the config and applied when a version goes live', () => { + assert.deepEqual(config.routes, [{ pattern: 'demo.sandobserver.com', custom_domain: true }]); + const deploy = wf.jobs.promote.steps.find(s => String(s.with?.command || '').includes('versions deploy')); + assert.ok(deploy, 'the promote job no longer deploys a version'); + assert.match(deploy.with.command, /^\s*triggers deploy\s*$/m, 'a version upload alone leaves the address unset'); +}); + +test('the first-run placeholder never takes the demo address', () => { + const prep = wf.jobs.upload.steps.find(s => s.name === 'Prepare the placeholder'); + assert.ok(prep, 'the placeholder step is gone'); + assert.match(prep.run, /delete c\.routes/); + assert.doesNotMatch(prep.run, /cp upload\/wrangler\.jsonc placeholder/); +});