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.
This commit is contained in:
Neil
2026-10-18 03:41:49 -07:00
committed by GitHub
parent 2d1ec53533
commit db17b33ef9
7 changed files with 196 additions and 1 deletions
+4
View File
@@ -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
@@ -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
@@ -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
@@ -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]))
}
@@ -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
)
})
})
@@ -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],
+2 -1
View File
@@ -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",