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:
parent
3baffb49ff
commit
abcdc04f6b
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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([])
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue