chore(ci): upload auto-update needed files & merge latest-mac.yml

Close #900
This commit is contained in:
Neko Ayaka
2026-01-08 15:52:02 +08:00
parent 774b819e4d
commit 531a426b10
5 changed files with 324 additions and 3 deletions
+51
View File
@@ -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 }}
+1
View File
@@ -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": {
@@ -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<string, UpdateInfoFile>()
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<UpdateInfo> {
const raw = await readFile(filePath, 'utf8')
return yaml.parse(raw) as UpdateInfo
}
async function main() {
const cli = cac('merge-latest-mac')
.option('--input <path>', 'Input latest-mac yml file', { default: [], type: [String] })
.option('--dir <path>', 'Scan directory for latest-mac*.yml files', { default: '' })
.option('--output <path>', '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)
})
@@ -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)
}
}
+108 -2
View File
@@ -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<FilenameOutputEntry[]> {
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)