ci: block new root-level entries (#11903)

* ci: guard repository root additions

* fix: clear existing type-aware lint warnings
This commit is contained in:
Neil 2026-08-01 01:48:24 -07:00 committed by GitHub
parent 169ec8f08d
commit edb5607e28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 172 additions and 9 deletions

View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <base-sha> <head-sha>" >&2
exit 2
fi
base_sha=$1
head_sha=$2
git rev-parse --verify "${base_sha}^{tree}" >/dev/null
git rev-parse --verify "${head_sha}^{tree}" >/dev/null
declare -A base_entries=()
while IFS= read -r -d '' entry; do
base_entries["$entry"]=1
done < <(git ls-tree -z --name-only "$base_sha")
blocked_entries=()
while IFS= read -r -d '' entry; do
if [[ -z "${base_entries[$entry]+present}" ]]; then
blocked_entries+=("$entry")
fi
done < <(git ls-tree -z --name-only "$head_sha")
if (( ${#blocked_entries[@]} == 0 )); then
echo "Root directory guard passed: no new root-level files or folders."
exit 0
fi
echo "::error title=Root-level additions blocked::New root-level files or folders bloat the GitHub landing page."
echo "Root directory guard failed."
echo "New root-level files or folders are not allowed because they bloat the GitHub landing page."
echo "Move each new entry under an existing top-level directory."
printf 'Blocked entries:\n'
printf ' %s\n' "${blocked_entries[@]}"
exit 1

View File

@ -93,6 +93,23 @@ jobs:
- name: Verify macOS entitlements
run: pnpm verify:macos-entitlements
root_directory_guard:
name: root directory guard
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- name: Reject new root-level files and folders
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bash .github/scripts/check-root-directory-entries.sh "$BASE_SHA" "$HEAD_SHA"
typecheck:
runs-on: ubuntu-latest
@ -377,6 +394,7 @@ jobs:
if: always()
needs:
- static_analysis
- root_directory_guard
- typecheck
- git_compatibility
- shell_contracts
@ -397,6 +415,7 @@ jobs:
- name: Require successful checks
env:
STATIC_ANALYSIS: ${{ needs.static_analysis.result }}
ROOT_DIRECTORY_GUARD: ${{ needs.root_directory_guard.result }}
TYPECHECK: ${{ needs.typecheck.result }}
GIT_COMPATIBILITY: ${{ needs.git_compatibility.result }}
SHELL_CONTRACTS: ${{ needs.shell_contracts.result }}
@ -406,6 +425,7 @@ jobs:
run: |
for result in \
"$STATIC_ANALYSIS" \
"$ROOT_DIRECTORY_GUARD" \
"$TYPECHECK" \
"$GIT_COMPATIBILITY" \
"$SHELL_CONTRACTS" \

View File

@ -0,0 +1,99 @@
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { parse } from 'yaml'
const projectDir = resolve(import.meta.dirname, '../..')
const guardScript = join(projectDir, '.github/scripts/check-root-directory-entries.sh')
const tempDirs = []
function git(cwd, args) {
return execFileSync('git', args, { cwd, encoding: 'utf8' }).trim()
}
function makeFixture() {
const root = mkdtempSync(join(tmpdir(), 'orca-root-directory-guard-'))
tempDirs.push(root)
git(root, ['init', '--quiet'])
git(root, ['config', 'user.email', 'root-directory-guard-test@example.com'])
git(root, ['config', 'user.name', 'Root Directory Guard Test'])
mkdirSync(join(root, 'config'), { recursive: true })
writeFileSync(join(root, 'config', 'base.txt'), 'base\n')
git(root, ['add', '-A'])
git(root, ['commit', '--quiet', '-m', 'base'])
return { root, base: git(root, ['rev-parse', 'HEAD']) }
}
function commitFiles(root, files) {
for (const [relativePath, contents] of files) {
const target = join(root, relativePath)
mkdirSync(dirname(target), { recursive: true })
writeFileSync(target, contents)
}
git(root, ['add', '-A'])
git(root, ['commit', '--quiet', '-m', 'head'])
return git(root, ['rev-parse', 'HEAD'])
}
function runGuard({ root, base, head }) {
return spawnSync('bash', [guardScript, base, head], {
cwd: root,
encoding: 'utf8'
})
}
afterEach(() => {
while (tempDirs.length > 0) {
rmSync(tempDirs.pop(), { force: true, recursive: true })
}
})
describe('root directory guard', () => {
it('allows additions inside an existing top-level directory', () => {
const fixture = makeFixture()
const head = commitFiles(fixture.root, [['config/new.txt', 'nested\n']])
const result = runGuard({ ...fixture, head })
expect(result.status).toBe(0)
expect(result.stdout).toContain('no new root-level files or folders')
})
it('rejects a new root-level file with the landing-page message', () => {
const fixture = makeFixture()
const head = commitFiles(fixture.root, [['new-root.md', 'too prominent\n']])
const result = runGuard({ ...fixture, head })
const output = `${result.stdout}\n${result.stderr}`
expect(result.status).toBe(1)
expect(output).toContain('bloat the GitHub landing page')
expect(output).toContain('new-root.md')
})
it('rejects a new top-level directory', () => {
const fixture = makeFixture()
const head = commitFiles(fixture.root, [['new-folder/file.txt', 'too prominent\n']])
const result = runGuard({ ...fixture, head })
const output = `${result.stdout}\n${result.stderr}`
expect(result.status).toBe(1)
expect(output).toContain('new-folder')
})
it('is wired into the PR verify gate', () => {
const workflow = parse(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8'))
const guardJob = workflow.jobs.root_directory_guard
const guardStep = guardJob.steps.find(
(step) => step.name === 'Reject new root-level files and folders'
)
expect(guardJob.name).toBe('root directory guard')
expect(guardJob.steps[0].with['fetch-depth']).toBe(0)
expect(guardStep.run).toContain('.github/scripts/check-root-directory-entries.sh')
expect(workflow.jobs.verify.needs).toContain('root_directory_guard')
})
})

View File

@ -173,6 +173,7 @@ describe('PR workflow parallelism', () => {
it('keeps verify as the aggregate required check', () => {
expect(workflow.jobs.verify.needs).toEqual([
'static_analysis',
'root_directory_guard',
'typecheck',
'git_compatibility',
'shell_contracts',

View File

@ -28,14 +28,16 @@ import {
stopDevApp,
waitForStoreReady
} from '../../config/scripts/windows-apphang-repro/electron-dev-session.mjs'
import { createCompletedOnboardingProfile } from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
import {
createCompletedOnboardingProfile,
safeRemoveLocalDirectory
} from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
import {
pollUntil,
rendererActionTimeoutMs,
runWithTimeout,
setupTimeoutMs
} from '../../config/scripts/windows-apphang-repro/repro-timing.mjs'
import { safeRemoveLocalDirectory } from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
const rootDir = path.resolve(fileURLToPath(new URL('../..', import.meta.url)))
const PARK_DELAY_MS = 1_500
@ -454,7 +456,8 @@ async function main() {
args.reportPath ??
path.join(
rootDir,
'tests', 'tools',
'tests',
'tools',
'benchmarks',
'results',
`cold-park-res-${args.label}-${stamp}.json`

View File

@ -34,14 +34,16 @@ import {
stopDevApp,
waitForStoreReady
} from '../../config/scripts/windows-apphang-repro/electron-dev-session.mjs'
import { createCompletedOnboardingProfile } from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
import {
createCompletedOnboardingProfile,
safeRemoveLocalDirectory
} from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
import {
pollUntil,
rendererActionTimeoutMs,
runWithTimeout,
setupTimeoutMs
} from '../../config/scripts/windows-apphang-repro/repro-timing.mjs'
import { safeRemoveLocalDirectory } from '../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
const rootDir = path.resolve(fileURLToPath(new URL('../..', import.meta.url)))
const scenarioTimeoutMs = 300_000
@ -582,7 +584,8 @@ async function main() {
args.reportPath ??
path.join(
rootDir,
'tests', 'tools',
'tests',
'tools',
'benchmarks',
'results',
`cold-park-${args.label}-${stamp}.json`

View File

@ -217,7 +217,7 @@ export async function captureFailureDiagnostics(page, dir, label) {
* An expected tab id prevents post-restore probes from accepting another tab. */
export async function waitForTerminalReady(page, timeoutMs = 60_000, terminalTabId = null) {
const selector = terminalTabId
? `[data-terminal-tab-id="${terminalTabId}"]:visible`
? `[data-terminal-tab-id="${String(terminalTabId)}"]:visible`
: TERMINAL_SURFACE_VISIBLE
const surface = page.locator(selector).first()
await surface.waitFor({ state: 'visible', timeout: timeoutMs })
@ -399,7 +399,7 @@ export async function focusActiveTerminal(page, terminalTabId = null) {
// focusing so typed commands actually reach the shell.
await dismissKnownOverlays(page)
const selector = terminalTabId
? `[data-terminal-tab-id="${terminalTabId}"]:visible`
? `[data-terminal-tab-id="${String(terminalTabId)}"]:visible`
: TERMINAL_SURFACE_VISIBLE
const surface = page.locator(selector).first()
const click = surface.click({ position: { x: 24, y: 24 }, timeout: 15_000 })

View File

@ -85,7 +85,7 @@ export function silentInstall(setupExe, { timeoutMs = 180_000, installDir = null
// no other flags for a per-user install. /D, when present, MUST be last.
const args = ['/S']
if (installDir) {
args.push(`/D=${installDir}`)
args.push(`/D=${String(installDir)}`)
}
const proc = spawnSync(setupExe, args, { encoding: 'utf8' })
if (proc.error) {