Files
workdsh/scripts/verify-layout.mjs
T
t4we fan 4895b38531 fix(ci): make verify-layout robust on Windows checkouts
The Windows CI run failed because the note-hash check hashes the working
tree with git hash-object, and a CRLF checkout changes the blob hash.
Hash the committed blob via git rev-parse HEAD:<path> instead, which is
identical on every host and keeps the recorded values valid. Also accept
the Windows materialization of the CLAUDE.md symlink (a regular file
holding the target name), and enforce LF for the outer working tree with
a root .gitattributes, matching the submodule convention.
2026-08-16 20:50:18 +08:00

107 lines
4.9 KiB
JavaScript

import { execFileSync } from 'node:child_process'
import { existsSync, lstatSync, readFileSync, readlinkSync } from 'node:fs'
import { basename, resolve } from 'node:path'
const root = resolve(import.meta.dirname, '..')
const readJson = path => JSON.parse(readFileSync(resolve(root, path), 'utf8'))
const run = (command, args, cwd = root) => execFileSync(command, args, {
cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
}).trim()
const fail = message => { throw new Error(`verify-layout: ${message}`) }
const workspace = readJson('package.json')
const upstream = readJson('upstream.json')
const plugin = readJson('dsh-plugin-desktop/package.json')
const upstreamPackage = readJson('deepseek-harness/package.json')
const noteDirectory = '.agents/notes/implemented/process'
const noteName = '2026-08-15-pinned-upstream-and-isolated-yarn-workspace'
const notePaths = [`${noteDirectory}/${noteName}.md`, `${noteDirectory}/${noteName}.zh.md`]
const noteRecordPath = `${noteDirectory}/${noteName}.i18n.yaml`
if (workspace.packageManager !== '[email protected]') {
fail('the product workspace must pin [email protected]')
}
if (JSON.stringify(workspace.workspaces) !== JSON.stringify(['dsh-plugin-desktop'])) {
fail('the root Yarn workspace must contain only dsh-plugin-desktop')
}
if (plugin.packageManager !== undefined) {
fail('dsh-plugin-desktop must inherit the root Yarn release')
}
const claudePath = resolve(root, 'CLAUDE.md')
const claudeStat = lstatSync(claudePath)
// Windows checkouts materialize the symlink as a regular file holding the
// target name; accept both forms so the pointer stays verified on every host.
const claudeTarget = claudeStat.isSymbolicLink()
? readlinkSync(claudePath)
: readFileSync(claudePath, 'utf8').trim()
if (claudeTarget !== 'AGENTS.md') {
fail('CLAUDE.md must link to the outer repository AGENTS.md')
}
for (const legacyFile of [
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'dsh-plugin-desktop/pnpm-lock.yaml',
'dsh-plugin-desktop/pnpm-workspace.yaml',
]) {
if (existsSync(resolve(root, legacyFile))) fail(`${legacyFile} must not exist`)
}
if (run('git', ['config', '-f', '.gitmodules', '--get', 'submodule.deepseek-harness.path']) !== 'deepseek-harness') {
fail('the upstream submodule path must be deepseek-harness')
}
if (run('git', ['config', '-f', '.gitmodules', '--get', 'submodule.deepseek-harness.url']) !== upstream.repository) {
fail('the upstream submodule URL differs from upstream.json')
}
if (typeof upstreamPackage.packageManager !== 'string' || !upstreamPackage.packageManager.startsWith('pnpm@')) {
fail('the upstream checkout must retain its pnpm package manager')
}
for (const [owner, manifest] of [['root', workspace], ['plugin', plugin]]) {
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies', 'resolutions']) {
for (const [name, range] of Object.entries(manifest[field] ?? {})) {
if (typeof range !== 'string') continue
if (/^(?:workspace|portal|link):/u.test(range)
|| (range.startsWith('file:') && range.includes('deepseek-harness'))) {
fail(`${owner} ${field}.${name} bypasses the published DSH package boundary`)
}
}
}
}
const [mode, object] = run('git', ['ls-files', '--stage', '--', 'deepseek-harness']).split(/\s+/u)
if (mode !== '160000') fail('deepseek-harness must be tracked as a Git submodule')
if (object !== upstream.commit) fail(`submodule index is ${object}, expected ${upstream.commit}`)
const upstreamDir = resolve(root, 'deepseek-harness')
if (run('git', ['rev-parse', 'HEAD'], upstreamDir) !== upstream.commit) {
fail('checked-out upstream commit differs from upstream.json')
}
if (run('git', ['status', '--porcelain'], upstreamDir) !== '') {
fail('deepseek-harness contains local changes')
}
if (run('git', ['remote', 'get-url', 'origin'], upstreamDir) !== upstream.repository) {
fail('deepseek-harness origin differs from upstream.json')
}
if (upstreamPackage.version !== upstream.sourceVersion) {
fail('deepseek-harness package version differs from upstream.json')
}
for (const name of Object.keys(plugin.dependencies).filter(name => name === '@deepseek-ai/dsh' || name.startsWith('@deepseek-ai/dsh-'))) {
if (plugin.dependencies[name] !== upstream.runtimePackageVersion) {
fail(`${name} must use the recorded DSH runtime package family`)
}
}
const noteRecord = readFileSync(resolve(root, noteRecordPath), 'utf8')
for (const notePath of notePaths) {
// Hash the committed blob, not the working tree: checkout line endings
// differ per host, while HEAD:<path> is identical everywhere.
const expected = run('git', ['rev-parse', `HEAD:${notePath}`])
const recordLine = `${basename(notePath)}: ${expected}`
if (!noteRecord.split('\n').includes(recordLine)) {
fail(`${noteRecordPath} is stale for ${notePath}`)
}
}
process.stdout.write(`verify-layout: Yarn workspace and upstream ${upstream.commit.slice(0, 10)} are consistent\n`)