From 5158944ad35ea1caaba7cf01d25ad071249ea8aa Mon Sep 17 00:00:00 2001 From: Matsu Date: Fri, 18 Sep 2026 09:57:03 +0000 Subject: [PATCH] ci: Refactor codespace previews script-base (#39042) --- .devcontainer/codespaces/README.md | 4 ++-- .devcontainer/preview/devcontainer.json | 2 +- .devcontainer/preview/preview-signin-hook.cjs | 2 +- .github/WORKFLOWS.md | 18 +++++++++--------- .github/scripts/codespace-preview.mjs | 8 ++++---- .github/scripts/package.json | 2 +- .github/workflows/ci-pull-requests.yml | 10 +++------- package.json | 4 ++-- .../{ => codespace-preview}/preview-labels.mjs | 0 .../preview-labels.test.mjs | 0 .../{ => codespace-preview}/preview-phases.mjs | 0 .../preview-phases.test.mjs | 0 .../preview-remote-env.mjs | 0 .../preview-remote-env.test.mjs | 0 .../{ => codespace-preview}/preview-serve.mjs | 4 ++-- scripts/{ => codespace-preview}/preview.mjs | 5 +++-- .../{ => codespace-preview}/serve-ready.mjs | 2 +- .../serve-ready.test.mjs | 2 +- scripts/dev-up.mjs | 2 +- 19 files changed, 31 insertions(+), 34 deletions(-) rename scripts/{ => codespace-preview}/preview-labels.mjs (100%) rename scripts/{ => codespace-preview}/preview-labels.test.mjs (100%) rename scripts/{ => codespace-preview}/preview-phases.mjs (100%) rename scripts/{ => codespace-preview}/preview-phases.test.mjs (100%) rename scripts/{ => codespace-preview}/preview-remote-env.mjs (100%) rename scripts/{ => codespace-preview}/preview-remote-env.test.mjs (100%) rename scripts/{ => codespace-preview}/preview-serve.mjs (99%) rename scripts/{ => codespace-preview}/preview.mjs (98%) rename scripts/{ => codespace-preview}/serve-ready.mjs (97%) rename scripts/{ => codespace-preview}/serve-ready.test.mjs (98%) diff --git a/.devcontainer/codespaces/README.md b/.devcontainer/codespaces/README.md index a7ecbc0a995..80e6beb931c 100644 --- a/.devcontainer/codespaces/README.md +++ b/.devcontainer/codespaces/README.md @@ -93,7 +93,7 @@ needs `gh` with the codespace scope, the same as `pnpm session`. present. `preview:debug` sets `N8N_LOG_LEVEL=debug`. Adding or removing one re-serves the box; it never creates or deletes one. From a laptop the labels apply the same way — `pnpm preview refresh ` reads them from the PR. The - toggles are defined in `scripts/preview-labels.mjs`; add new ones there. + toggles are defined in `scripts/codespace-preview/preview-labels.mjs`; add new ones there. - **Configure the instance from a webhook.** A preview also reads extra environment from an n8n webhook, so a value can change without a commit. It needs the `CODESPACE_ENV_URL`, `CODESPACE_ENV_USER` and `CODESPACE_ENV_PASSWORD` @@ -108,7 +108,7 @@ needs `gh` with the codespace scope, the same as `pnpm session`. builds a new one. - **A PR from a fork gets no preview.** A codespace's token is scoped to `n8n-io/n8n`, so it cannot check out a fork head. -- **A PR that predates this tooling has no `scripts/preview-serve.mjs`.** The +- **A PR that predates this tooling has no `scripts/codespace-preview/preview-serve.mjs`.** The serve step says so and stops; rebase the PR on master and retry. ## Agent worker (drive a session from n8n) diff --git a/.devcontainer/preview/devcontainer.json b/.devcontainer/preview/devcontainer.json index 34d6adbc46d..558e230598a 100644 --- a/.devcontainer/preview/devcontainer.json +++ b/.devcontainer/preview/devcontainer.json @@ -36,7 +36,7 @@ "postStartCommand": "git config --global --get-all credential.helper 2>/dev/null | grep -qs gitcredential-refresh || git config --global --add credential.helper /usr/local/bin/gitcredential-refresh.sh", // Declarations only. All of these are set at the repository level, so Codespaces // injects them into every preview box whether or not they are listed here. - // scripts/preview-serve.mjs reads them with codespaceSecret(). + // scripts/codespace-preview/preview-serve.mjs reads them with codespaceSecret(). "secrets": { "N8N_LICENSE_ACTIVATION_KEY": { "description": "Sandbox licence key. The preview:enterprise label decides whether it reaches n8n." diff --git a/.devcontainer/preview/preview-signin-hook.cjs b/.devcontainer/preview/preview-signin-hook.cjs index de3a7cfce69..914dfadf7d9 100644 --- a/.devcontainer/preview/preview-signin-hook.cjs +++ b/.devcontainer/preview/preview-signin-hook.cjs @@ -1,6 +1,6 @@ /** * One-click sign-in for preview instances. Loaded through EXTERNAL_HOOK_FILES by - * scripts/preview-serve.mjs, so it is never part of a packaged n8n. + * scripts/codespace-preview/preview-serve.mjs, so it is never part of a packaged n8n. * * The route performs an ordinary login on the visitor's behalf: it calls * /rest/login with the preview instance's own seeded credentials and forwards the diff --git a/.github/WORKFLOWS.md b/.github/WORKFLOWS.md index 2b926ba06b9..c7ae6599731 100644 --- a/.github/WORKFLOWS.md +++ b/.github/WORKFLOWS.md @@ -254,7 +254,7 @@ parallelism). See the `--build-via-mcp` section in | `preview:debug` | `util-codespace-preview.yml` | Re-serves the instance with `N8N_LOG_LEVEL=debug` | **Why:** A reviewer gets a running instance of the PR without a Docker build or a -cloud deploy. The workflow calls `scripts/preview.mjs`, which keeps one codespace +cloud deploy. The workflow calls `scripts/codespace-preview/preview.mjs`, which keeps one codespace for each PR (display name `preview/pr-`) and shares port 5678 with the organization. A later push serves the new head in the same box. Removing the label, or closing the PR, deletes the box. @@ -266,7 +266,7 @@ scoped to `n8n-io/n8n` and cannot check out a fork head. `up` and `refresh` take minutes, and an absent comment looks the same as a broken preview. So the comment goes up before the box work starts, as a checklist of the -phases in `scripts/preview-phases.mjs`, and is edited for each phase and once a +phases in `scripts/codespace-preview/preview-phases.mjs`, and is edited for each phase and once a minute after that. The final URL replaces it in place. A comment **edit sends no notification** — only a create does. That is what makes a @@ -310,12 +310,12 @@ A `preview:*` label configures an instance that already exists, so adding or removing one re-serves the box instead of creating or deleting it. It does nothing on a PR without `codespace-preview`. -The vocabulary lives in `scripts/preview-labels.mjs`, which both ends import: +The vocabulary lives in `scripts/codespace-preview/preview-labels.mjs`, which both ends import: `preview.mjs` turns the PR's labels into slugs, and `preview-serve.mjs` turns those slugs into environment inside the box. Add a toggle there, in one place. Two things cross the gap, and both are shape-checked rather than trusted: a -`preview:*` slug, and a phase key from `scripts/preview-phases.mjs`. The `gh +`preview:*` slug, and a phase key from `scripts/codespace-preview/preview-phases.mjs`. The `gh codespace ssh` command is a shell string that appears in the box's process list, so a value is never passed through it — `preview:enterprise` resolves to a licence key inside the box, not on the runner. @@ -336,7 +336,7 @@ in this repository, so that is the set of people who already have write access. #### Preview environment from a webhook A preview can also take environment from an n8n webhook we control, so a value -can change without a commit and a merge. `scripts/preview-remote-env.mjs` fetches +can change without a commit and a merge. `scripts/codespace-preview/preview-remote-env.mjs` fetches it, and `preview-serve.mjs` hands the result to the backend. It needs three **Codespaces** secrets on `n8n-io/n8n`, again not Actions secrets: @@ -802,11 +802,11 @@ Scripts in `.github/scripts/`: | Script | Purpose | Called By | |---------------------------------|-------------------------------------------------------------------------|--------------------------------| | `codespace-preview.mjs` | Map a `pull_request` event or a manual operation onto a preview operation, comment the result | `util-codespace-preview.yml` | -| `../../scripts/preview.mjs` | One codespace for each PR: `up`, `refresh`, `down`, `ls`. `--json` for CI | `codespace-preview.mjs`, developers | -| `../../scripts/preview-remote-env.mjs` | Fetch extra environment for a preview from the webhook, inside the box | `../../scripts/preview-serve.mjs` | -| `../../scripts/preview-phases.mjs` | The phase vocabulary and its one-line marker, so the runner, the box and the comment cannot drift | `codespace-preview.mjs`, `../../scripts/preview.mjs`, `../../scripts/preview-serve.mjs` | +| `../../scripts/codespace-preview/preview.mjs` | One codespace for each PR: `up`, `refresh`, `down`, `ls`. `--json` for CI | `codespace-preview.mjs`, developers | +| `../../scripts/codespace-preview/preview-remote-env.mjs` | Fetch extra environment for a preview from the webhook, inside the box | `../../scripts/codespace-preview/preview-serve.mjs` | +| `../../scripts/codespace-preview/preview-phases.mjs` | The phase vocabulary and its one-line marker, so the runner, the box and the comment cannot drift | `codespace-preview.mjs`, `../../scripts/codespace-preview/preview.mjs`, `../../scripts/codespace-preview/preview-serve.mjs` | -`scripts/preview.mjs` is also the developer entry point (`pnpm preview up `). +`scripts/codespace-preview/preview.mjs` is also the developer entry point (`pnpm preview up `). In `--json` mode stdout carries one line for each phase and then the report object, and all human progress goes to stderr. So a workflow can follow a run that also streams an in-box build log. The reader tells the two apart by the `url` field: the diff --git a/.github/scripts/codespace-preview.mjs b/.github/scripts/codespace-preview.mjs index 0aa00797308..e4e58e57e5e 100644 --- a/.github/scripts/codespace-preview.mjs +++ b/.github/scripts/codespace-preview.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Drives a PR preview instance from CI. `scripts/preview.mjs` does the work; this +// Drives a PR preview instance from CI. `scripts/codespace-preview/preview.mjs` does the work; this // script maps the pull_request event onto one of its operations and reports the // result back to the PR as a single, edited-in-place comment. // @@ -23,8 +23,8 @@ import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; -import { PREVIEW_LABEL_PREFIX } from '../../scripts/preview-labels.mjs'; -import { PREVIEW_PHASES, createLineSplitter, phaseIndex } from '../../scripts/preview-phases.mjs'; +import { PREVIEW_LABEL_PREFIX } from '../../scripts/codespace-preview/preview-labels.mjs'; +import { PREVIEW_PHASES, createLineSplitter, phaseIndex } from '../../scripts/codespace-preview/preview-phases.mjs'; import { ensureEnvVar, findCommentByMarker, @@ -50,7 +50,7 @@ export const WORKFLOW_URL = // What a manual run may ask for. `ls` is absent: it needs no PR and posts no comment. export const DISPATCH_OPERATIONS = ['up', 'refresh', 'down']; // Resolved against this file, so the script runs the same from any directory. -const PREVIEW_SCRIPT = fileURLToPath(new URL('../../scripts/preview.mjs', import.meta.url)); +const PREVIEW_SCRIPT = fileURLToPath(new URL('../../scripts/codespace-preview/preview.mjs', import.meta.url)); // Often enough to look alive, rarely enough that the edit history stays readable. const HEARTBEAT_MS = 60_000; diff --git a/.github/scripts/package.json b/.github/scripts/package.json index e57e9026409..87fab26eefb 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -6,7 +6,7 @@ }, "packageManager": "pnpm@12.4.2", "scripts": { - "test": "node --test --experimental-test-module-mocks ./*.test.mjs ./docker/*.test.mjs ./owners/*.test.mjs ./quality/*.test.mjs ./slack/*.test.mjs ./stale/*.test.mjs ../../scripts/*.test.mjs ../../scripts/licenses/*.test.mjs ../../scripts/mutation-health/*.test.mjs ../../.devcontainer/codespaces/*.test.mjs", + "test": "node --test --experimental-test-module-mocks ./*.test.mjs ./docker/*.test.mjs ./owners/*.test.mjs ./quality/*.test.mjs ./slack/*.test.mjs ./stale/*.test.mjs ../../scripts/*.test.mjs ../../scripts/codespace-preview/*.test.mjs ../../scripts/licenses/*.test.mjs ../../scripts/mutation-health/*.test.mjs ../../.devcontainer/codespaces/*.test.mjs", "generate-sbom": "FETCH_LICENSE=true cdxgen -t pnpm --no-install-deps --profile license-compliance --spec-version 1.6 -o ../../sbom-source.cdx.json ../../compiled/", "enrich-sbom": "node ../../scripts/licenses/enrich-sbom.mjs ../../sbom-source.cdx.json", "render-licenses-md": "node ../../scripts/licenses/render-licenses-md.mjs ../../sbom-source.cdx.json ../../packages/cli/THIRD_PARTY_LICENSES.md ../../compiled/node_modules", diff --git a/.github/workflows/ci-pull-requests.yml b/.github/workflows/ci-pull-requests.yml index c50bb991f79..6fe15b84a77 100644 --- a/.github/workflows/ci-pull-requests.yml +++ b/.github/workflows/ci-pull-requests.yml @@ -78,8 +78,7 @@ jobs: !scripts/agent-setup.mjs !scripts/backend-module/** !scripts/cloud-session.mjs - !scripts/preview.mjs - !scripts/preview-serve.mjs + !scripts/codespace-preview/** !scripts/licenses/** !scripts/mutation-health/** !scripts/sync-agent-skill-links.mjs @@ -95,8 +94,7 @@ jobs: !scripts/agent-setup.mjs !scripts/backend-module/** !scripts/cloud-session.mjs - !scripts/preview.mjs - !scripts/preview-serve.mjs + !scripts/codespace-preview/** !scripts/licenses/** !scripts/mutation-health/** !scripts/sync-agent-skill-links.mjs @@ -134,9 +132,7 @@ jobs: OWNERS scripts/*.test.mjs scripts/codespace-env.mjs - scripts/preview.mjs - scripts/preview* - scripts/serve-ready.mjs + scripts/codespace-preview/** scripts/licenses/** scripts/mutation-health/** cubic.yaml diff --git a/package.json b/package.json index 426f43089c7..d78d7c48805 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "dev-metrics:reset": "node scripts/dev-metrics/setup.mjs --reset", "clean": "turbo run clean", "session": "node scripts/cloud-session.mjs", - "preview": "node scripts/preview.mjs", - "preview:serve": "node scripts/preview-serve.mjs", + "preview": "node scripts/codespace-preview/preview.mjs", + "preview:serve": "node scripts/codespace-preview/preview-serve.mjs", "session:opencode": "node scripts/cloud-session.mjs --opencode", "session:shell": "node scripts/cloud-session.mjs --shell", "reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs", diff --git a/scripts/preview-labels.mjs b/scripts/codespace-preview/preview-labels.mjs similarity index 100% rename from scripts/preview-labels.mjs rename to scripts/codespace-preview/preview-labels.mjs diff --git a/scripts/preview-labels.test.mjs b/scripts/codespace-preview/preview-labels.test.mjs similarity index 100% rename from scripts/preview-labels.test.mjs rename to scripts/codespace-preview/preview-labels.test.mjs diff --git a/scripts/preview-phases.mjs b/scripts/codespace-preview/preview-phases.mjs similarity index 100% rename from scripts/preview-phases.mjs rename to scripts/codespace-preview/preview-phases.mjs diff --git a/scripts/preview-phases.test.mjs b/scripts/codespace-preview/preview-phases.test.mjs similarity index 100% rename from scripts/preview-phases.test.mjs rename to scripts/codespace-preview/preview-phases.test.mjs diff --git a/scripts/preview-remote-env.mjs b/scripts/codespace-preview/preview-remote-env.mjs similarity index 100% rename from scripts/preview-remote-env.mjs rename to scripts/codespace-preview/preview-remote-env.mjs diff --git a/scripts/preview-remote-env.test.mjs b/scripts/codespace-preview/preview-remote-env.test.mjs similarity index 100% rename from scripts/preview-remote-env.test.mjs rename to scripts/codespace-preview/preview-remote-env.test.mjs diff --git a/scripts/preview-serve.mjs b/scripts/codespace-preview/preview-serve.mjs similarity index 99% rename from scripts/preview-serve.mjs rename to scripts/codespace-preview/preview-serve.mjs index a23baa34d1a..517c676d324 100644 --- a/scripts/preview-serve.mjs +++ b/scripts/codespace-preview/preview-serve.mjs @@ -13,7 +13,7 @@ import { dirname, resolve } from 'node:path'; import { setTimeout as sleep } from 'node:timers/promises'; import { fileURLToPath } from 'node:url'; -import { codespaceSecret } from './codespace-env.mjs'; +import { codespaceSecret } from '../codespace-env.mjs'; import { envForSlugs } from './preview-labels.mjs'; import { phaseMarkerLine } from './preview-phases.mjs'; import { fetchRemoteEnv } from './preview-remote-env.mjs'; @@ -36,7 +36,7 @@ const OWNER_EMAIL = process.env.PREVIEW_OWNER_EMAIL ?? 'preview@n8n.io'; const OWNER_PASSWORD = process.env.PREVIEW_OWNER_PASSWORD ?? 'PreviewInstance1'; const HEAP_MB = process.env.PREVIEW_BUILD_HEAP_MB ?? '6144'; // Resolve from this file, not cwd, so the tmux command lands in the right tree. -const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); const port = servePort(); const healthPath = serveHealthPath(); diff --git a/scripts/preview.mjs b/scripts/codespace-preview/preview.mjs similarity index 98% rename from scripts/preview.mjs rename to scripts/codespace-preview/preview.mjs index c48804ff785..0522e16ba58 100644 --- a/scripts/preview.mjs +++ b/scripts/codespace-preview/preview.mjs @@ -171,10 +171,11 @@ const serveCommand = (pr, head) => { `git checkout --detach ${head.headRefOid}`, // The serve script comes from the checked-out PR head, so a PR cut before this // tooling landed on master does not have it. Say so, instead of letting pnpm - // report a missing script. + // report a missing script. A head from before the scripts moved into + // scripts/codespace-preview still has it at the old path. // An `||` here would also fire when an earlier step in this && chain failed, // reporting the wrong cause. `if` keeps the test self-contained. - 'if [ ! -f scripts/preview-serve.mjs ]; then echo "This PR predates the preview tooling. Rebase it on master and retry."; exit 1; fi', + 'if [ ! -f scripts/codespace-preview/preview-serve.mjs ] && [ ! -f scripts/preview-serve.mjs ]; then echo "This PR predates the preview tooling. Rebase it on master and retry."; exit 1; fi', // After the guard above: a PR that predates the tooling must not tick off // "Install dependencies" and then stop. ...marker('install'), diff --git a/scripts/serve-ready.mjs b/scripts/codespace-preview/serve-ready.mjs similarity index 97% rename from scripts/serve-ready.mjs rename to scripts/codespace-preview/serve-ready.mjs index d9cb633961d..500c959f1a1 100644 --- a/scripts/serve-ready.mjs +++ b/scripts/codespace-preview/serve-ready.mjs @@ -5,7 +5,7 @@ import { execFileSync } from 'node:child_process'; import { existsSync } from 'node:fs'; import { setTimeout as sleep } from 'node:timers/promises'; -import { codespaceName, forwardingDomain } from './codespace-env.mjs'; +import { codespaceName, forwardingDomain } from '../codespace-env.mjs'; const CODESPACES_DIR = '/workspaces/.codespaces'; diff --git a/scripts/serve-ready.test.mjs b/scripts/codespace-preview/serve-ready.test.mjs similarity index 98% rename from scripts/serve-ready.test.mjs rename to scripts/codespace-preview/serve-ready.test.mjs index 042b5959257..6fe88c95ce0 100644 --- a/scripts/serve-ready.test.mjs +++ b/scripts/codespace-preview/serve-ready.test.mjs @@ -1,4 +1,4 @@ -// node --test scripts/serve-ready.test.mjs +// node --test scripts/codespace-preview/serve-ready.test.mjs import assert from 'node:assert/strict'; import { mkdtempSync } from 'node:fs'; import { createServer } from 'node:http'; diff --git a/scripts/dev-up.mjs b/scripts/dev-up.mjs index 40b4bbe1914..d14914d7802 100644 --- a/scripts/dev-up.mjs +++ b/scripts/dev-up.mjs @@ -19,7 +19,7 @@ import { servePort, shareWithOrg, waitForHealth, -} from './serve-ready.mjs'; +} from './codespace-preview/serve-ready.mjs'; const build = process.argv.includes('--build'); const port = servePort();