ci: block new root-level entries (#11903)
* ci: guard repository root additions * fix: clear existing type-aware lint warnings
This commit is contained in:
parent
169ec8f08d
commit
edb5607e28
|
|
@ -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
|
||||
|
|
@ -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" \
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue