fix(ci): mirror missing lint steps in PR workflow (#10601) (#10623)

Reviewed with an independent reproduction. Added the allowlist entry that unblocked verify:localization-coverage on main, the 4th drifted step, and a parity gate that fails when pnpm lint's chain contains a script absent from pr.yml.
This commit is contained in:
Henry Su 2026-07-27 18:26:43 -05:00 committed by GitHub
parent 3baffb49ff
commit abcdc04f6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 131 additions and 0 deletions

View File

@ -31,9 +31,15 @@ jobs:
- name: Lint
run: pnpm exec oxlint --format github
- name: Check switch exhaustiveness
run: pnpm run lint:switch-exhaustiveness
- name: Check styled scrollbars
run: pnpm check:styled-scrollbars
- name: Check quadratic buffer concatenation
run: pnpm run check:quadratic-buffer-concat
- name: Check reliability gate manifest
run: pnpm run check:reliability-gates
@ -46,6 +52,17 @@ jobs:
- name: Verify skill freshness manifest
run: pnpm run verify:skill-bundle-manifest
- name: Verify localization catalog
run: pnpm run verify:localization-catalog
- name: Verify localization coverage
run: pnpm run verify:localization-coverage
# Why: project-owned type declarations must live in .ts so tsc
# actually checks them. TypeScript's skipLibCheck: true (inherited
# from @electron-toolkit/tsconfig) silently widens unresolved names
# in .d.ts to `any`, which is how #1186 shipped a broken IPC signature
# past typecheck. See docs/preload-typecheck-hole.md.
- name: Guard against project-owned .d.ts in preload/shared
run: |
matches=$(find src/preload src/shared -name '*.d.ts' 2>/dev/null || true)

View File

@ -40,5 +40,33 @@
"text": "Idioma",
"dynamic": false,
"count": 1
},
{
"filePath": "src/renderer/src/components/settings/terminal-advanced-platform-search.ts",
"kind": "object-property:keywords",
"text": "Ghostty",
"dynamic": false,
"count": 2
},
{
"filePath": "src/renderer/src/components/settings/terminal-advanced-platform-search.ts",
"kind": "object-property:keywords",
"text": "ghostty",
"dynamic": false,
"count": 2
},
{
"filePath": "src/renderer/src/components/settings/terminal-pane-appearance-search.ts",
"kind": "object-property:keywords",
"text": "Ghostty",
"dynamic": false,
"count": 1
},
{
"filePath": "src/renderer/src/components/settings/terminal-pane-appearance-search.ts",
"kind": "object-property:keywords",
"text": "ghostty",
"dynamic": false,
"count": 1
}
]

View File

@ -0,0 +1,86 @@
import { readFileSync } from 'node:fs'
import { parse } from 'yaml'
import { describe, expect, it } from 'vitest'
// Why: pr.yml re-lists the `lint` chain as individual steps so a single failure
// does not mask the rest and oxlint keeps its `--format github` annotations.
// Hand-maintained mirrors drift (#10601: three verifiers never ran on PRs), so
// this gate fails the moment a `lint` step has no counterpart in pr.yml.
// Flags that only change reporting, so they must not split two otherwise identical commands.
const REPORTING_FLAGS_WITH_VALUE = new Set(['--format', '--reporter'])
const REPORTING_FLAGS = new Set(['--quiet'])
const PACKAGE_RUNNER_TOKENS = new Set(['pnpm', 'npm', 'yarn', 'npx', 'run', 'exec', 'node'])
function splitCommandChain(command) {
return command
.split(/\n|&&|;/)
.map((part) => part.trim())
.filter(Boolean)
}
function canonicalize(command) {
const tokens = command.split(/\s+/)
const canonical = []
for (let index = 0; index < tokens.length; index += 1) {
const token = tokens[index]
if (canonical.length === 0 && PACKAGE_RUNNER_TOKENS.has(token)) {
continue
}
if (REPORTING_FLAGS.has(token)) {
continue
}
if (REPORTING_FLAGS_WITH_VALUE.has(token)) {
index += 1
continue
}
canonical.push(token)
}
return canonical.join(' ')
}
/** Expands `pnpm run x` indirection until every entry is a real binary invocation. */
function resolveLeafCommands(command, scripts, seen = new Set()) {
const leaves = []
for (const part of splitCommandChain(command)) {
const scriptName = part.match(/^(?:pnpm|npm|yarn)(?:\s+run)?\s+([\w:-]+)$/)?.[1]
if (scriptName && scripts[scriptName] && !seen.has(scriptName)) {
leaves.push(
...resolveLeafCommands(scripts[scriptName], scripts, new Set([...seen, scriptName]))
)
continue
}
leaves.push(canonicalize(part))
}
return leaves
}
describe('PR workflow lint parity', () => {
it('runs every `pnpm lint` step on pull requests', () => {
const { scripts } = JSON.parse(readFileSync('package.json', 'utf8'))
const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
// Scan every job: which one hosts the lint steps is an organizational
// detail that has already been renamed once (verify -> static_analysis).
const workflowCommands = new Set(
Object.values(workflow.jobs)
.flatMap((job) => job.steps ?? [])
.filter((step) => typeof step.run === 'string')
.flatMap((step) => resolveLeafCommands(step.run, scripts))
)
const missing = resolveLeafCommands(scripts.lint, scripts).filter(
(leaf) => !workflowCommands.has(leaf)
)
expect(
missing,
`.github/workflows/pr.yml is missing lint steps: ${missing.join(', ')}. ` +
'Add a step for each one so PR CI matches `pnpm lint`.'
).toEqual([])
})
})