From 531a426b107491d30eae46f758b069671cb6b2b4 Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Thu, 8 Jan 2026 15:51:22 +0800 Subject: [PATCH] chore(ci): upload auto-update needed files & merge latest-mac.yml Close #900 --- .github/workflows/release-tamagotchi.yml | 51 ++++++ apps/stage-tamagotchi/package.json | 1 + .../scripts/merge-latest-mac.ts | 155 ++++++++++++++++++ .../scripts/rename-artifacts.ts | 10 +- apps/stage-tamagotchi/scripts/utils.ts | 110 ++++++++++++- 5 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 apps/stage-tamagotchi/scripts/merge-latest-mac.ts diff --git a/.github/workflows/release-tamagotchi.yml b/.github/workflows/release-tamagotchi.yml index 30f0f2e5a..70709d37c 100644 --- a/.github/workflows/release-tamagotchi.yml +++ b/.github/workflows/release-tamagotchi.yml @@ -305,6 +305,14 @@ jobs: run: | pnpm run -F @proj-airi/stage-tamagotchi rename-artifacts ${{ matrix.target }} --release --auto-tag + - name: Upload macOS update info (Automatic + macOS Only) + if: ${{ github.event_name == 'release' && (matrix.os == 'macos-latest' || matrix.os == 'macos-15-intel') }} + uses: actions/upload-artifact@v4 + with: + name: latest-mac-yml-${{ matrix.arch }} + path: apps/stage-tamagotchi/bundle/latest-mac.yml + if-no-files-found: error + - name: Upload To GitHub Releases (Automatic) if: ${{ github.event_name == 'release' }} uses: softprops/action-gh-release@v2 @@ -316,9 +324,52 @@ jobs: apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.deb apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.rpm apps/stage-tamagotchi/bundle/${{ env.PRODUCT_NAME }}-*.flatpak + apps/stage-tamagotchi/bundle/latest-*.yml append_body: true # - name: Clean up keychain and provisioning profile (macOS Only) # if: ${{ always() && (matrix.os == 'macos-15-intel' || matrix.os == 'macos-latest') }} # run: | # security delete-keychain $RUNNER_TEMP/app-signing.keychain-db + + merge-mac-latest: + name: Merge macOS latest-mac.yml + if: ${{ github.event_name == 'release' }} + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v4 + with: + run_install: false + - uses: actions/setup-node@v5 + with: + node-version: lts/* + cache: pnpm + - run: pnpm install --frozen-lockfile + + - name: Download latest-mac.yml (x64) + uses: actions/download-artifact@v4 + with: + name: latest-mac-yml-x64 + path: artifacts/x64 + + - name: Download latest-mac.yml (arm64) + uses: actions/download-artifact@v4 + with: + name: latest-mac-yml-arm64 + path: artifacts/arm64 + + - name: Merge latest-mac.yml + run: | + pnpm -F @proj-airi/stage-tamagotchi exec tsx scripts/merge-latest-mac.ts \ + --input artifacts/x64/latest-mac.yml \ + --input artifacts/arm64/latest-mac.yml \ + --output apps/stage-tamagotchi/bundle/latest-mac.yml + + - name: Upload merged latest-mac.yml + uses: softprops/action-gh-release@v2 + with: + files: apps/stage-tamagotchi/bundle/latest-mac.yml + tag_name: ${{ github.event.release.tag_name }} diff --git a/apps/stage-tamagotchi/package.json b/apps/stage-tamagotchi/package.json index 136905366..33916ae23 100644 --- a/apps/stage-tamagotchi/package.json +++ b/apps/stage-tamagotchi/package.json @@ -27,6 +27,7 @@ "build:mac": "pnpm run build && electron-builder --mac", "build:linux": "pnpm run build && electron-builder --linux", "rename-artifacts": "mkdir -p bundle && tsx scripts/rename-artifacts.ts", + "merge-latest-mac": "tsx scripts/merge-latest-mac.ts", "artifacts-metadata": "tsx scripts/artifacts-metadata.ts" }, "dependencies": { diff --git a/apps/stage-tamagotchi/scripts/merge-latest-mac.ts b/apps/stage-tamagotchi/scripts/merge-latest-mac.ts new file mode 100644 index 000000000..28b87c34b --- /dev/null +++ b/apps/stage-tamagotchi/scripts/merge-latest-mac.ts @@ -0,0 +1,155 @@ +import { existsSync, readdirSync } from 'node:fs' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { exit } from 'node:process' + +import { cac } from 'cac' + +import * as yaml from 'yaml' + +interface UpdateInfoFile { + url: string + sha2?: string + sha512?: string + size?: number +} + +interface UpdateInfo { + files?: UpdateInfoFile[] + path?: string + sha2?: string + sha512?: string + [key: string]: unknown +} + +type Platform = 'x64' | 'arm64' | 'both' | 'none' + +function detectPlatform(updateInfo: UpdateInfo): Platform { + const urls: string[] = [] + if (Array.isArray(updateInfo.files)) { + for (const file of updateInfo.files) { + if (typeof file?.url === 'string') { + urls.push(file.url) + } + } + } + if (typeof updateInfo.path === 'string') { + urls.push(updateInfo.path) + } + + // eslint-disable-next-line regexp/no-unused-capturing-group + const hasArm64 = urls.some(url => /(^|[-_/])arm64([-.]|$)/i.test(url)) + // eslint-disable-next-line regexp/no-unused-capturing-group + const hasX64FromName = urls.some(url => /(^|[-_/])x64([-.]|$)/i.test(url)) + const hasMacZip = urls.some(url => /-mac\.zip$/i.test(url) && !/arm64/i.test(url)) + const hasX64 = hasX64FromName || hasMacZip + + if (hasX64 && hasArm64) { + return 'both' + } + if (hasX64) { + return 'x64' + } + if (hasArm64) { + return 'arm64' + } + return 'none' +} + +function mergeFiles(arm64: UpdateInfo, x64: UpdateInfo): UpdateInfo { + const arm64Files = Array.isArray(arm64.files) ? arm64.files : [] + const x64Files = Array.isArray(x64.files) ? x64.files : [] + + const byUrl = new Map() + for (const file of [...arm64Files, ...x64Files]) { + if (file?.url) { + byUrl.set(file.url, file) + } + } + + return { + ...arm64, + files: [...byUrl.values()], + } +} + +async function readUpdateInfo(filePath: string): Promise { + const raw = await readFile(filePath, 'utf8') + return yaml.parse(raw) as UpdateInfo +} + +async function main() { + const cli = cac('merge-latest-mac') + .option('--input ', 'Input latest-mac yml file', { default: [], type: [String] }) + .option('--dir ', 'Scan directory for latest-mac*.yml files', { default: '' }) + .option('--output ', 'Output file path', { default: '' }) + + const args = cli.parse() + const inputs = (args.options.input as string[]).filter(Boolean) + const dir = String(args.options.dir || '').trim() + + let files: string[] = [] + if (inputs.length > 0) { + files = inputs.map(file => resolve(file)) + } + else { + const scanDir = dir || resolve('bundle') + const candidates = readdirSync(scanDir).filter( + file => file.startsWith('latest-mac') && file.endsWith('.yml'), + ) + files = candidates.map(file => resolve(scanDir, file)) + } + + if (files.length === 0) { + throw new Error('No latest-mac*.yml files found') + } + + const entries: { filePath: string, updateInfo: UpdateInfo, platform: Platform }[] = [] + for (const filePath of files) { + if (!existsSync(filePath)) { + continue + } + const updateInfo = await readUpdateInfo(filePath) + const platform = detectPlatform(updateInfo) + entries.push({ filePath, updateInfo, platform }) + } + + if (entries.length === 0) { + throw new Error('No readable latest-mac*.yml files found') + } + + const outputPath = String(args.options.output || '').trim() + || resolve(dir || 'bundle', 'latest-mac.yml') + await mkdir(dirname(outputPath), { recursive: true }) + + const mergedEntry = entries.find(entry => entry.platform === 'both') + if (mergedEntry) { + await writeFile(outputPath, yaml.stringify(mergedEntry.updateInfo), 'utf8') + return + } + + const x64Entries = entries.filter(entry => entry.platform === 'x64') + const arm64Entries = entries.filter(entry => entry.platform === 'arm64') + + if (x64Entries.length === 0 && arm64Entries.length === 0) { + throw new Error('No x64 or arm64 update info found') + } + + if (x64Entries.length === 0) { + await writeFile(outputPath, yaml.stringify(arm64Entries[0].updateInfo), 'utf8') + return + } + + if (arm64Entries.length === 0) { + await writeFile(outputPath, yaml.stringify(x64Entries[0].updateInfo), 'utf8') + return + } + + const merged = mergeFiles(arm64Entries[0].updateInfo, x64Entries[0].updateInfo) + await writeFile(outputPath, yaml.stringify(merged), 'utf8') +} + +main().catch((error) => { + console.error(error) + exit(1) +}) diff --git a/apps/stage-tamagotchi/scripts/rename-artifacts.ts b/apps/stage-tamagotchi/scripts/rename-artifacts.ts index ddd03c1ee..3bd71b708 100644 --- a/apps/stage-tamagotchi/scripts/rename-artifacts.ts +++ b/apps/stage-tamagotchi/scripts/rename-artifacts.ts @@ -1,6 +1,6 @@ import process from 'node:process' -import { mkdirSync, readdirSync, renameSync } from 'node:fs' +import { existsSync, mkdirSync, readdirSync, renameSync } from 'node:fs' import { join } from 'node:path' import { cac } from 'cac' @@ -73,6 +73,14 @@ async function main() { const renameFrom = join(srcPrefix, filename.outputFilename) const renameTo = join(bundlePrefix, filename.releaseArtifactFilename) console.info('renaming, from:', renameFrom, 'to:', renameTo) + if (!existsSync(renameFrom)) { + const message = `missing artifact: ${renameFrom}` + if (filename.optional) { + console.warn(message) + continue + } + throw new Error(message) + } renameSync(renameFrom, renameTo) } } diff --git a/apps/stage-tamagotchi/scripts/utils.ts b/apps/stage-tamagotchi/scripts/utils.ts index 0a4485929..d5ff0bca4 100644 --- a/apps/stage-tamagotchi/scripts/utils.ts +++ b/apps/stage-tamagotchi/scripts/utils.ts @@ -82,6 +82,7 @@ interface FilenameOutputEntry { releaseArtifactFilename: string productName: string version: string + optional?: boolean } export function mapArchFor( @@ -118,6 +119,28 @@ export function mapArchFor( } } +function getLatestUpdateFilename(target: string): string | null { + switch (target) { + case 'x86_64-pc-windows-msvc': + return 'latest.yml' + case 'x86_64-unknown-linux-gnu': + return 'latest-linux.yml' + case 'aarch64-unknown-linux-gnu': + return 'latest-linux-arm64.yml' + case 'aarch64-apple-darwin': + case 'x86_64-apple-darwin': + return 'latest-mac.yml' + default: + return null + } +} + +function getMacZipFilename(productName: string, version: string, target: string): string { + const arch = mapArchFor(target, 'zip') + const archPrefix = arch === 'x64' ? '' : `${arch}-` + return `${productName}-${version}-${archPrefix}mac.zip` +} + export async function getFilenames(target: string, options: { release: boolean, autoTag: boolean, tag: string[] }): Promise { const electronBuilder = await getElectronBuilderConfig() const version = await getVersion(options) @@ -153,6 +176,15 @@ export async function getFilenames(target: string, options: { release: boolean, productName, version, }, + { + target: 'x86_64-pc-windows-msvc', + extension: 'latest.yml', + outputFilename: 'latest.yml', + releaseArtifactFilename: 'latest.yml', + productName, + version, + optional: true, + }, ] case 'x86_64-unknown-linux-gnu': { @@ -239,6 +271,19 @@ export async function getFilenames(target: string, options: { release: boolean, ) } + const latestUpdateFilename = getLatestUpdateFilename(target) + if (latestUpdateFilename) { + artifacts.push({ + target: 'x86_64-unknown-linux-gnu', + extension: latestUpdateFilename, + outputFilename: latestUpdateFilename, + releaseArtifactFilename: latestUpdateFilename, + productName, + version, + optional: true, + }) + } + return artifacts } case 'aarch64-unknown-linux-gnu': @@ -326,10 +371,24 @@ export async function getFilenames(target: string, options: { release: boolean, ) } + const latestUpdateFilename = getLatestUpdateFilename(target) + if (latestUpdateFilename) { + artifacts.push({ + target: 'aarch64-unknown-linux-gnu', + extension: latestUpdateFilename, + outputFilename: latestUpdateFilename, + releaseArtifactFilename: latestUpdateFilename, + productName, + version, + optional: true, + }) + } + return artifacts } case 'aarch64-apple-darwin': - return [ + { + const artifacts: FilenameOutputEntry[] = [ { target: 'aarch64-apple-darwin', extension: 'dmg', @@ -351,8 +410,32 @@ export async function getFilenames(target: string, options: { release: boolean, version, }, ] + + artifacts.push( + { + target: 'aarch64-apple-darwin', + extension: 'zip', + outputFilename: getMacZipFilename(productName, beforeVersion, target), + releaseArtifactFilename: getMacZipFilename(productName, version, target), + productName, + version, + }, + { + target: 'aarch64-apple-darwin', + extension: 'latest-mac.yml', + outputFilename: 'latest-mac.yml', + releaseArtifactFilename: 'latest-mac.yml', + productName, + version, + optional: true, + }, + ) + + return artifacts + } case 'x86_64-apple-darwin': - return [ + { + const artifacts: FilenameOutputEntry[] = [ { target: 'x86_64-apple-darwin', extension: 'dmg', @@ -374,6 +457,29 @@ export async function getFilenames(target: string, options: { release: boolean, version, }, ] + + artifacts.push( + { + target: 'x86_64-apple-darwin', + extension: 'zip', + outputFilename: getMacZipFilename(productName, beforeVersion, target), + releaseArtifactFilename: getMacZipFilename(productName, version, target), + productName, + version, + }, + { + target: 'x86_64-apple-darwin', + extension: 'latest-mac.yml', + outputFilename: 'latest-mac.yml', + releaseArtifactFilename: 'latest-mac.yml', + productName, + version, + optional: true, + }, + ) + + return artifacts + } default: console.error('Target is not supported') process.exit(1)