Files
winewill 1b6451a647 fix(desktop): follow the documented release version rules
Review found that `apps/desktop/README.md#release-versions` and the
2026-09-16 version-derivation note already specify this convention, and
that three of their requirements were reversed without saying so. The
rules themselves are right; what was wrong was leaving them undocumented
and rejecting one of the forms they prescribe.

A stable base takes `-test.YYYYMMDD.index`, which validation refused
because such a version ranks below its base. That ordering is the point:
the form belongs to a test feed that never serves the production
release. Both READMEs, the older note, and a new note now record which
requirements this change reverses and why.

Three defects the review found, each reproducible:

- `afterPack` compared the bundled runtime against the product version,
  which broke installed-update qualification: that flow passes a runtime
  whose version was deliberately rewritten. The expected version is now
  a parameter the preparer supplies, and `verifyDesktopRuntime` says
  "bundled X is not the expected Y" rather than naming Electron.
- Upload could only learn the published version from a shell variable,
  conflicting with "release fields come only from the target file". It
  reads the completion record packaging wrote, and a mismatch now prints
  the version that record holds.
- Automatic numbering read one listing page and one fixed directory. It
  now follows every page, discards a listing it could not finish rather
  than reusing an index that would overwrite a published installer,
  bounds the request through the client's own timeout, and reads the
  directory an unsigned build actually writes.

Also: `npm run` and `yarn run` set `npm_execpath` too, so the pnpm
resolution checks the path names pnpm; the argument separator can arrive
after a run script's preset arguments, not only first; and the toolchain
probe, parser, build commit, and tarball readers now have specs.
2026-09-21 16:04:08 +08:00

52 lines
2.3 KiB
TypeScript

import { execFileSync } from 'node:child_process'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { capture, pnpmCommand } from './process.ts'
import { packedIdentity, tarballFiles } from './tarball.ts'
const directories: string[] = []
afterEach(async () => {
for (const directory of directories.splice(0)) await rm(directory, { recursive: true, force: true })
})
/**
* Pack one manifest into a tarball shaped like `npm pack` output.
* @returns Absolute path of the archive.
*/
async function packedTarball(name: string, version: string): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), 'dsh-tarball-'))
directories.push(directory)
const contents = join(directory, 'package')
await import('node:fs/promises').then(async fs => fs.mkdir(contents))
await writeFile(join(contents, 'package.json'), JSON.stringify({ name, version }))
await writeFile(join(contents, 'index.js'), '')
const archive = join(directory, `${name.replaceAll('/', '-').replace('@', '')}-${version}.tgz`)
execFileSync('tar', ['-czf', basename(archive), 'package'], { cwd: dirname(archive) })
return archive
}
describe('release process helpers', () => {
it('runs pnpm through a JavaScript entry, which spawnSync can start on Windows', () => {
const [command, ...args] = pnpmCommand()
expect(command === 'pnpm' || /node(?:\.exe)?$/iu.test(command)).toBe(true)
if (command !== 'pnpm') expect(args[0]).toMatch(/pnpm/u)
expect(capture(command, [...args, '--version'])).toMatch(/^\d+\.\d+\.\d+/u)
})
})
describe('release tarball readers', () => {
it('lists members of an archive under an absolute path', async () => {
// GNU tar reads the colon in a Windows drive path as a remote host, so the readers run beside the archive.
const archive = await packedTarball('@deepseek-ai/dsh-probe', '1.2.3')
expect(tarballFiles(archive)).toEqual(expect.arrayContaining(['package/package.json', 'package/index.js']))
})
it('reads what an archive declares about itself', async () => {
const archive = await packedTarball('@deepseek-ai/dsh-probe', '1.2.3')
expect(packedIdentity(archive)).toEqual({ name: '@deepseek-ai/dsh-probe', version: '1.2.3' })
})
})