ci: Refactor codespace previews script-base (#39042)

This commit is contained in:
Matsu
2026-09-18 09:57:03 +00:00
committed by GitHub
parent b37fdc2d3a
commit 5158944ad3
19 changed files with 31 additions and 34 deletions
+2 -2
View File
@@ -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 <pr>` 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)
+1 -1
View File
@@ -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."
@@ -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
+9 -9
View File
@@ -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-<number>`) 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 <pr>`).
`scripts/codespace-preview/preview.mjs` is also the developer entry point (`pnpm preview up <pr>`).
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
+4 -4
View File
@@ -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;
+1 -1
View File
@@ -6,7 +6,7 @@
},
"packageManager": "[email protected]",
"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",
+3 -7
View File
@@ -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
+2 -2
View File
@@ -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",
@@ -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 ?? '[email protected]';
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();
@@ -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'),
@@ -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';
@@ -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';
+1 -1
View File
@@ -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();