From 5532356d13c2f124141fdfeb214aa7d6d2ccfd14 Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Mon, 23 Mar 2026 21:38:05 +0800 Subject: [PATCH] chore(ci): incorrect manifest path resolution for Windows signed artifact --- .../scripts/regenerate-windows-latest.test.ts | 104 ++++++++++++++++++ .../scripts/regenerate-windows-latest.ts | 90 +++++++++++---- apps/stage-tamagotchi/vitest.config.ts | 2 +- 3 files changed, 172 insertions(+), 24 deletions(-) create mode 100644 apps/stage-tamagotchi/scripts/regenerate-windows-latest.test.ts diff --git a/apps/stage-tamagotchi/scripts/regenerate-windows-latest.test.ts b/apps/stage-tamagotchi/scripts/regenerate-windows-latest.test.ts new file mode 100644 index 000000000..e840837ba --- /dev/null +++ b/apps/stage-tamagotchi/scripts/regenerate-windows-latest.test.ts @@ -0,0 +1,104 @@ +import process from 'node:process' + +import { Buffer } from 'node:buffer' +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' + +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import * as yaml from 'yaml' + +import { hashFile, regenerateWindowsLatest } from './regenerate-windows-latest' + +describe('regenerateWindowsLatest', () => { + let originalCwd: string + + beforeEach(() => { + originalCwd = process.cwd() + }) + + afterEach(() => { + process.chdir(originalCwd) + }) + + it('resolves workspace-root-relative paths from the package cwd and rewrites latest.yml from the signed installer', async () => { + const root = await mkdtemp(join(tmpdir(), 'airi-regenerate-windows-latest-')) + const workspaceRoot = join(root, 'repo') + const packageDir = join(workspaceRoot, 'apps', 'stage-tamagotchi') + const bundleDir = join(packageDir, 'bundle') + + await mkdir(bundleDir, { recursive: true }) + await writeFile(join(workspaceRoot, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n', 'utf8') + await writeFile(join(bundleDir, 'AIRI-1.2.3-windows-x64-setup.exe'), 'signed-binary-content', 'utf8') + await writeFile(join(bundleDir, 'latest.yml'), yaml.stringify({ + version: 'stale-version', + path: 'stale.exe', + sha512: 'stale-sha512', + releaseDate: '2026-01-02T03:04:05.000Z', + stagingPercentage: 25, + files: [{ url: 'stale.exe', sha512: 'stale-sha512', size: 10 }], + }), 'utf8') + + process.chdir(packageDir) + + const nextUpdateInfo = await regenerateWindowsLatest({ + input: 'apps/stage-tamagotchi/bundle/AIRI-1.2.3-windows-x64-setup.exe', + output: 'apps/stage-tamagotchi/bundle/latest.yml', + version: '1.2.3', + }) + + const expectedHashes = await hashFile(join(bundleDir, 'AIRI-1.2.3-windows-x64-setup.exe')) + expect(nextUpdateInfo).toMatchObject({ + version: '1.2.3', + path: 'AIRI-1.2.3-windows-x64-setup.exe', + sha512: expectedHashes.sha512, + sha2: expectedHashes.sha256, + releaseDate: '2026-01-02T03:04:05.000Z', + stagingPercentage: 25, + files: [ + { + url: 'AIRI-1.2.3-windows-x64-setup.exe', + sha512: expectedHashes.sha512, + }, + ], + }) + expect(nextUpdateInfo.files[0]?.size).toBe(Buffer.byteLength('signed-binary-content')) + + const persisted = yaml.parse(await readFile(join(bundleDir, 'latest.yml'), 'utf8')) + expect(persisted).toMatchObject(nextUpdateInfo) + }) + + it('also works with package-relative paths', async () => { + const root = await mkdtemp(join(tmpdir(), 'airi-regenerate-windows-latest-')) + const packageDir = join(root, 'apps', 'stage-tamagotchi') + const bundleDir = join(packageDir, 'bundle') + + await mkdir(bundleDir, { recursive: true }) + await writeFile(join(bundleDir, 'AIRI-9.9.9-windows-x64-setup.exe'), 'another-signed-binary', 'utf8') + process.chdir(packageDir) + + await regenerateWindowsLatest({ + input: 'bundle/AIRI-9.9.9-windows-x64-setup.exe', + output: 'bundle/latest.yml', + version: '9.9.9', + releaseDate: '2026-03-23T00:00:00.000Z', + }) + + const persisted = yaml.parse(await readFile(resolve(bundleDir, 'latest.yml'), 'utf8')) + const expectedHashes = await hashFile(join(bundleDir, 'AIRI-9.9.9-windows-x64-setup.exe')) + expect(persisted).toMatchObject({ + version: '9.9.9', + path: 'AIRI-9.9.9-windows-x64-setup.exe', + sha512: expectedHashes.sha512, + sha2: expectedHashes.sha256, + releaseDate: '2026-03-23T00:00:00.000Z', + files: [ + { + url: 'AIRI-9.9.9-windows-x64-setup.exe', + sha512: expectedHashes.sha512, + }, + ], + }) + }) +}) diff --git a/apps/stage-tamagotchi/scripts/regenerate-windows-latest.ts b/apps/stage-tamagotchi/scripts/regenerate-windows-latest.ts index 804ade151..1c059588d 100644 --- a/apps/stage-tamagotchi/scripts/regenerate-windows-latest.ts +++ b/apps/stage-tamagotchi/scripts/regenerate-windows-latest.ts @@ -1,9 +1,10 @@ import { createHash } from 'node:crypto' -import { createReadStream } from 'node:fs' -import { readFile, stat, writeFile } from 'node:fs/promises' -import { basename, resolve } from 'node:path' -import { exit } from 'node:process' +import { createReadStream, existsSync } from 'node:fs' +import { mkdir, readFile, stat, writeFile } from 'node:fs/promises' +import { basename, dirname, resolve } from 'node:path' +import { cwd, exit } from 'node:process' +import { findWorkspaceDir } from '@pnpm/find-workspace-dir' import { cac } from 'cac' import * as yaml from 'yaml' @@ -24,7 +25,7 @@ interface WindowsUpdateInfo { [key: string]: unknown } -async function hashFile(filePath: string): Promise<{ sha512: string, sha256: string }> { +export async function hashFile(filePath: string): Promise<{ sha512: string, sha256: string }> { return await new Promise((resolveHash, reject) => { const sha512 = createHash('sha512') const sha256 = createHash('sha256') @@ -44,7 +45,7 @@ async function hashFile(filePath: string): Promise<{ sha512: string, sha256: str }) } -async function readExistingUpdateInfo(filePath: string): Promise> { +export async function readExistingUpdateInfo(filePath: string): Promise> { try { const raw = await readFile(filePath, 'utf8') return (yaml.parse(raw) ?? {}) as Partial @@ -54,19 +55,35 @@ async function readExistingUpdateInfo(filePath: string): Promise', 'Signed Windows installer path', { type: [String] }) - .option('--output ', 'Output latest.yml path', { default: 'bundle/latest.yml' }) - .option('--version ', 'Version to write into latest.yml', { type: [String] }) - .option('--release-date ', 'Release date to write into latest.yml', { type: [String] }) +export async function resolveFromWorkspace(inputPath: string): Promise { + const resolved = resolve(inputPath) + if (existsSync(resolved)) { + return resolved + } - const args = cli.parse() + const workspaceRoot = await findWorkspaceDir(cwd()) + if (workspaceRoot) { + const workspaceResolved = resolve(workspaceRoot, inputPath) + if (existsSync(workspaceResolved)) { + return workspaceResolved + } + } - const input = String(args.options.input?.[0] || '').trim() - const output = String(args.options.output || '').trim() - const version = String(args.options.version?.[0] || '').trim() - const releaseDate = String(args.options.releaseDate?.[0] || '').trim() + return resolved +} + +export interface RegenerateWindowsLatestOptions { + input: string + output: string + version: string + releaseDate?: string +} + +export async function regenerateWindowsLatest(options: RegenerateWindowsLatestOptions): Promise { + const input = String(options.input || '').trim() + const output = String(options.output || '').trim() + const version = String(options.version || '').trim() + const releaseDate = String(options.releaseDate || '').trim() if (!input) { throw new Error('--input is required') @@ -78,8 +95,8 @@ async function main() { throw new Error('--version is required') } - const inputPath = resolve(input) - const outputPath = resolve(output) + const inputPath = await resolveFromWorkspace(input) + const outputPath = await resolveFromWorkspace(output) const fileStats = await stat(inputPath) const { sha512, sha256 } = await hashFile(inputPath) const existing = await readExistingUpdateInfo(outputPath) @@ -101,10 +118,37 @@ async function main() { releaseDate: releaseDate || existing.releaseDate || new Date().toISOString(), } + await mkdir(dirname(outputPath), { recursive: true }) await writeFile(outputPath, yaml.stringify(nextUpdateInfo), 'utf8') + + return nextUpdateInfo } -main().catch((error) => { - console.error(error) - exit(1) -}) +async function main() { + const cli = cac('regenerate-windows-latest') + .option('--input ', 'Signed Windows installer path', { type: [String] }) + .option('--output ', 'Output latest.yml path', { default: 'bundle/latest.yml' }) + .option('--version ', 'Version to write into latest.yml', { type: [String] }) + .option('--release-date ', 'Release date to write into latest.yml', { type: [String] }) + + const args = cli.parse() + + const input = String(args.options.input?.[0] || '').trim() + const output = String(args.options.output || '').trim() + const version = String(args.options.version?.[0] || '').trim() + const releaseDate = String(args.options.releaseDate?.[0] || '').trim() + + await regenerateWindowsLatest({ + input, + output, + version, + releaseDate, + }) +} + +if (import.meta.main) { + main().catch((error) => { + console.error(error) + exit(1) + }) +} diff --git a/apps/stage-tamagotchi/vitest.config.ts b/apps/stage-tamagotchi/vitest.config.ts index 5d54d7722..616ca92a2 100644 --- a/apps/stage-tamagotchi/vitest.config.ts +++ b/apps/stage-tamagotchi/vitest.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { - include: ['src/**/*.test.ts'], + include: ['src/**/*.test.ts', 'scripts/**/*.test.ts'], exclude: ['**/node_modules/**', '**/.git/**'], }, })