chore(ci): incorrect manifest path resolution for Windows signed artifact
This commit is contained in:
@@ -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,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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<Partial<WindowsUpdateInfo>> {
|
||||
export async function readExistingUpdateInfo(filePath: string): Promise<Partial<WindowsUpdateInfo>> {
|
||||
try {
|
||||
const raw = await readFile(filePath, 'utf8')
|
||||
return (yaml.parse(raw) ?? {}) as Partial<WindowsUpdateInfo>
|
||||
@@ -54,19 +55,35 @@ async function readExistingUpdateInfo(filePath: string): Promise<Partial<Windows
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const cli = cac('regenerate-windows-latest')
|
||||
.option('--input <path>', 'Signed Windows installer path', { type: [String] })
|
||||
.option('--output <path>', 'Output latest.yml path', { default: 'bundle/latest.yml' })
|
||||
.option('--version <version>', 'Version to write into latest.yml', { type: [String] })
|
||||
.option('--release-date <date>', 'Release date to write into latest.yml', { type: [String] })
|
||||
export async function resolveFromWorkspace(inputPath: string): Promise<string> {
|
||||
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<WindowsUpdateInfo> {
|
||||
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 <path>', 'Signed Windows installer path', { type: [String] })
|
||||
.option('--output <path>', 'Output latest.yml path', { default: 'bundle/latest.yml' })
|
||||
.option('--version <version>', 'Version to write into latest.yml', { type: [String] })
|
||||
.option('--release-date <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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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/**'],
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user