From db17b33ef939b4582f8c62ade42fdf13fbc48d27 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:41:49 -0700 Subject: [PATCH] ci: ratchet the legacy relay fence (importers of ssh/legacy-relay/ and COMPAT(legacy-relay-bridge) seams) (#30317) Both lists may only shrink, so the 2027-01-15 retirement edits a known set of files. --- .github/workflows/pr.yml | 4 + config/legacy-relay-compat-tags-baseline.txt | 42 ++++++++ config/legacy-relay-importers-baseline.txt | 15 +++ .../check-legacy-relay-fence-ratchet.mjs | 101 ++++++++++++++++++ .../check-legacy-relay-fence-ratchet.test.mjs | 31 ++++++ config/scripts/pr-preflight-gates.test.mjs | 1 + package.json | 3 +- 7 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 config/legacy-relay-compat-tags-baseline.txt create mode 100644 config/legacy-relay-importers-baseline.txt create mode 100644 config/scripts/check-legacy-relay-fence-ratchet.mjs create mode 100644 config/scripts/check-legacy-relay-fence-ratchet.test.mjs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index cbf81160a09..1171ea64efc 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -408,6 +408,10 @@ jobs: if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/check-e2e-typecheck-ratchet.mjs + - name: Enforce legacy relay fence ratchet + if: needs.code_paths.outputs.static_analysis == 'true' + run: node config/scripts/check-legacy-relay-fence-ratchet.mjs + - name: Check Node runtime pin if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/check-node-runtime-pin.mjs diff --git a/config/legacy-relay-compat-tags-baseline.txt b/config/legacy-relay-compat-tags-baseline.txt new file mode 100644 index 00000000000..04999e172a9 --- /dev/null +++ b/config/legacy-relay-compat-tags-baseline.txt @@ -0,0 +1,42 @@ +# Files outside src/main/ssh/legacy-relay/ tagged COMPAT(legacy-relay-bridge). RATCHET: may only SHRINK. +# Each tag marks code the relay retirement (2027-01-15) deletes or edits. +# Prune: pnpm check:legacy-relay-fence-ratchet --prune +config/scripts/pr-code-change-scope.mjs +config/scripts/pr-e2e-source-routing.mjs +src/main/ipc/ssh-connect-flow.ts +src/main/ipc/ssh-host-server-connect.ts +src/main/ipc/ssh-host-server-on-connect-wiring.test.ts +src/main/ipc/ssh-host-server-on-connect-wiring.ts +src/main/ipc/ssh-session-teardown.ts +src/main/ipc/ssh.ts +src/main/runtime/orchestration/terminal-authority/verify-orchestration-compatibility-caller.ts +src/main/runtime/runtime-terminal-contracts.ts +src/main/ssh/orcad-migration-delta-move.test.ts +src/main/ssh/orcad-migration-delta-move.ts +src/main/ssh/orcad-migration-repository-link.test.ts +src/main/ssh/orcad-migration-terminal-gate-host-wide.test.ts +src/main/ssh/orcad-migration-terminal-gate.test.ts +src/main/ssh/orcad-migration-terminal-gate.ts +src/main/ssh/orcad-runtime-conversion.test.ts +src/main/ssh/ssh-host-server-on-connect.test.ts +src/main/ssh/ssh-host-server-on-connect.ts +src/main/ssh/ssh-target-orcad-preflight.ts +src/preload/api-types.ts +src/preload/api/legacy-relay-bridge-api.ts +src/renderer/src/components/terminal-pane/legacy-relay-bridge-panes.ts +src/renderer/src/components/terminal-pane/legacy-relay-bridge-transport.ts +src/renderer/src/components/terminal-pane/pty-connection/foreground-output-refresh.ts +src/renderer/src/components/terminal-pane/pty-connection/unserved-ssh-pane-connect.ts +src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.ts +src/renderer/src/runtime/web-runtime-session-legacy-relay-bridged-activation.test.ts +src/renderer/src/store/terminals/legacy-relay-bridged-tab-close.ts +src/shared/legacy-relay-bridge-contract.ts +src/shared/ssh-types.ts +src/shared/telemetry-ssh-host-server-event-schemas.ts +tests/e2e/cross-version-wire/legacy-relay-bridge.unit.test.ts +tests/e2e/cross-version-wire/released-relay-daemon.ts +tests/e2e/cross-version-wire/released-relay-transport.ts +tests/e2e/helpers/build-released-relay-bundle.mts +tests/e2e/helpers/legacy-relay-docker-host.ts +tests/e2e/helpers/orcad-upgrade-profile.ts +tests/e2e/ssh-orcad-legacy-relay-bridge.spec.ts diff --git a/config/legacy-relay-importers-baseline.txt b/config/legacy-relay-importers-baseline.txt new file mode 100644 index 00000000000..0fab6a1a129 --- /dev/null +++ b/config/legacy-relay-importers-baseline.txt @@ -0,0 +1,15 @@ +# Files outside src/main/ssh/legacy-relay/ that import it. RATCHET: may only SHRINK. +# The relay retirement (2027-01-15) deletes the folder and edits exactly these files. +# Prune: pnpm check:legacy-relay-fence-ratchet --prune +src/main/ipc/orcad-delta-move-handlers.ts +src/main/ipc/pty/runtime/kill-legacy-relay-bridged.test.ts +src/main/ipc/pty/runtime/kill.ts +src/main/ipc/ssh-connect-decision-cancellation.test.ts +src/main/ipc/ssh-connect-flow.ts +src/main/ipc/ssh-host-server-on-connect-wiring.ts +src/main/ipc/ssh-session-teardown.ts +src/main/ipc/ssh.ts +src/main/ssh/orcad-migration-terminal-gate-host-wide.test.ts +src/main/ssh/orcad-runtime-conversion-wiring.ts +tests/e2e/cross-version-wire/legacy-relay-bridge.unit.test.ts +tests/e2e/helpers/legacy-relay-docker-host.ts diff --git a/config/scripts/check-legacy-relay-fence-ratchet.mjs b/config/scripts/check-legacy-relay-fence-ratchet.mjs new file mode 100644 index 00000000000..19dec071943 --- /dev/null +++ b/config/scripts/check-legacy-relay-fence-ratchet.mjs @@ -0,0 +1,101 @@ +import path from 'node:path' +import process from 'node:process' +import { pathToFileURL } from 'node:url' +import { readTrackedSources, runEntryRatchet } from './ratchet-baseline.mjs' +import { collectModuleSpecifiers } from './static-module-specifiers.mjs' + +// COMPAT(legacy-relay-bridge): fences what the 2027-01-15 relay retirement deletes. Everything +// that exists only for released relay hosts lives in LEGACY_FOLDER; files outside it that import +// it, or carry the COMPAT tag, are the seams that retirement edits. Both lists may only shrink. + +const LEGACY_FOLDER = 'src/main/ssh/legacy-relay' +const SCAN_ROOTS = ['src', 'tests', 'config', 'mobile/src'] +const COMPAT_TAG = 'COMPAT(legacy-relay-bridge)' +// The gate names the tag it counts; it is not a seam. +const SELF = 'config/scripts/check-legacy-relay-fence-ratchet.' +const CODE = /\.(ts|tsx|mts|cts|js|mjs|cjs)$/ + +function isOutsideFolder(rel) { + return !rel.startsWith(`${LEGACY_FOLDER}/`) +} + +/** True when a relative specifier from `file` lands inside the legacy folder. */ +export function importsLegacyFolder(file, specifier) { + const normalized = specifier.replaceAll('\\', '/') + if (!normalized.startsWith('.')) { + return false + } + const target = path.posix.normalize(path.posix.join(path.posix.dirname(file), normalized)) + return target === LEGACY_FOLDER || target.startsWith(`${LEGACY_FOLDER}/`) +} + +export function collectLegacyRelayImporters( + root = process.cwd(), + specifiersFor = collectModuleSpecifiers +) { + const sources = readTrackedSources( + root, + SCAN_ROOTS, + (rel) => CODE.test(rel) && isOutsideFolder(rel) + ) + return [...sources] + .filter( + ([rel, text]) => + text.includes('legacy-relay') && + specifiersFor(rel, text).some((s) => importsLegacyFolder(rel, s)) + ) + .map(([rel]) => rel) + .sort() +} + +export function collectLegacyRelayCompatTags(root = process.cwd()) { + const sources = readTrackedSources( + root, + SCAN_ROOTS, + (rel) => isOutsideFolder(rel) && !rel.endsWith('-baseline.txt') && !rel.startsWith(SELF) + ) + return [...sources] + .filter(([, text]) => text.includes(COMPAT_TAG)) + .map(([rel]) => rel) + .sort() +} + +const IMPORTERS = { + name: 'legacy-relay-fence', + baselinePath: 'config/legacy-relay-importers-baseline.txt', + header: [ + `# Files outside ${LEGACY_FOLDER}/ that import it. RATCHET: may only SHRINK.`, + '# The relay retirement (2027-01-15) deletes the folder and edits exactly these files.', + '# Prune: pnpm check:legacy-relay-fence-ratchet --prune' + ], + collect: (root) => collectLegacyRelayImporters(root), + fix: [ + `New code must not reach into ${LEGACY_FOLDER}/. Released relay hosts are served only by the`, + 'existing seams; put relay-only code inside the folder and reach it from an existing importer.' + ] +} + +const TAGS = { + name: 'legacy-relay-fence', + baselinePath: 'config/legacy-relay-compat-tags-baseline.txt', + header: [ + `# Files outside ${LEGACY_FOLDER}/ tagged ${COMPAT_TAG}. RATCHET: may only SHRINK.`, + '# Each tag marks code the relay retirement (2027-01-15) deletes or edits.', + '# Prune: pnpm check:legacy-relay-fence-ratchet --prune' + ], + collect: (root) => collectLegacyRelayCompatTags(root), + fix: [ + `A new ${COMPAT_TAG} seam outside ${LEGACY_FOLDER}/ grows what retirement must edit.`, + 'Keep relay-only behaviour inside the folder instead.' + ] +} + +export function main(root = process.cwd(), arg = undefined) { + const importers = runEntryRatchet(IMPORTERS, root, arg) + const tags = runEntryRatchet(TAGS, root, arg) + return importers || tags +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exit(main(process.cwd(), process.argv[2])) +} diff --git a/config/scripts/check-legacy-relay-fence-ratchet.test.mjs b/config/scripts/check-legacy-relay-fence-ratchet.test.mjs new file mode 100644 index 00000000000..a449285ae6b --- /dev/null +++ b/config/scripts/check-legacy-relay-fence-ratchet.test.mjs @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest' +import { importsLegacyFolder } from './check-legacy-relay-fence-ratchet.mjs' + +describe('importsLegacyFolder', () => { + it('matches a relative import that lands in the legacy relay folder', () => { + expect( + importsLegacyFolder('src/main/ipc/x.ts', '../ssh/legacy-relay/legacy-relay-client') + ).toBe(true) + expect(importsLegacyFolder('src/main/ssh/x.ts', './legacy-relay/legacy-relay-client')).toBe( + true + ) + expect( + importsLegacyFolder( + 'tests/e2e/a.spec.ts', + '../../src/main/ssh/legacy-relay/legacy-relay-client' + ) + ).toBe(true) + }) + + it('ignores lookalike folders, dot segments that leave it, and package imports', () => { + expect(importsLegacyFolder('src/main/ssh/x.ts', './legacy-relay-tools/a')).toBe(false) + expect(importsLegacyFolder('src/main/ssh/x.ts', './legacy-relay/../ssh-connection')).toBe(false) + expect(importsLegacyFolder('src/main/ssh/x.ts', 'legacy-relay/a')).toBe(false) + }) + + it('normalizes Windows separators', () => { + expect(importsLegacyFolder('src/main/ssh/x.ts', '.\\legacy-relay\\legacy-relay-client')).toBe( + true + ) + }) +}) diff --git a/config/scripts/pr-preflight-gates.test.mjs b/config/scripts/pr-preflight-gates.test.mjs index 08259181537..dad604c88b6 100644 --- a/config/scripts/pr-preflight-gates.test.mjs +++ b/config/scripts/pr-preflight-gates.test.mjs @@ -247,6 +247,7 @@ it('pins every foreground and background step to its selected phase', () => { ['Enforce path-literal ratchet', staticPhase], ['Enforce worktree-id composer ratchet', staticPhase], ['Enforce e2e typecheck ratchet', staticPhase], + ['Enforce legacy relay fence ratchet', staticPhase], ['Check Node runtime pin', staticPhase], ['Decide whether the orcad terminal smoke runs', staticPhase], ['Verify the generated RPC params catalog', staticPhase], diff --git a/package.json b/package.json index 82e1fba9934..372c5538d98 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "audit:perf": "oxlint --config config/oxlint-performance-audit.json --format json src", "test:perf:contracts": "vitest run --config config/vitest.performance.config.ts", "format": "oxfmt --write .", - "lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:focus-read-ratchet && pnpm run check:layout-writer-ratchets && pnpm run check:deprecated-runtime-owner-ratchet && pnpm run check:path-literal-ratchet && pnpm run check:readme-local-links && pnpm run check:node-runtime-pin && pnpm run verify:rpc-params-catalog && pnpm run verify:acp-protocol && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage && pnpm run check:process-host-imports && pnpm run check:main-sync-spawn-ratchet && pnpm run check:compat-boundary-imports", + "lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:focus-read-ratchet && pnpm run check:layout-writer-ratchets && pnpm run check:deprecated-runtime-owner-ratchet && pnpm run check:path-literal-ratchet && pnpm run check:legacy-relay-fence-ratchet && pnpm run check:readme-local-links && pnpm run check:node-runtime-pin && pnpm run verify:rpc-params-catalog && pnpm run verify:acp-protocol && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage && pnpm run check:process-host-imports && pnpm run check:main-sync-spawn-ratchet && pnpm run check:compat-boundary-imports", "audit:code-quality": "pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run audit:react-doctor", "audit:code-quality:native": "oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings", "audit:code-quality:type-aware": "oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings", @@ -40,6 +40,7 @@ "check:ts-nocheck-ratchet": "node config/scripts/check-ts-nocheck-ratchet.mjs", "check:deprecated-runtime-owner-ratchet": "node config/scripts/check-deprecated-runtime-owner-ratchet.mjs", "check:path-literal-ratchet": "node config/scripts/check-path-literal-ratchet.mjs", + "check:legacy-relay-fence-ratchet": "node config/scripts/check-legacy-relay-fence-ratchet.mjs", "check:worktree-id-composer-ratchet": "node config/scripts/check-worktree-id-composer-ratchet.mjs", "check:e2e-typecheck-ratchet": "node config/scripts/check-e2e-typecheck-ratchet.mjs", "check:runtime-electron-ratchet": "node config/scripts/check-runtime-electron-ratchet.mjs",