feat: add tauri-updater support for Autobuild channel

- Refactor updater.mjs: extract shared utilities, remove dead alpha
  channel, add `autobuild` channel via CLI arg
- Add override-updater-endpoints.mjs to switch tauri.conf.json
  endpoints to updater-autobuild at build time
- Add release-update-autobuild job to autobuild workflow
- Change allowDowngrades to true so custom compareVersions handles
  build-metadata versions (+autobuild.MMDD.hash) correctly
- Fix original bug: linux-x86/i686 wrote .url instead of .signature
  in the .sig handler
This commit is contained in:
Tunglies 2026-04-06 00:29:10 +08:00
parent b8fbabae04
commit 3320b300de
No known key found for this signature in database
GPG Key ID: B9B01B389469B3E8
5 changed files with 461 additions and 268 deletions

View File

@ -206,6 +206,9 @@ jobs:
- name: Release ${{ env.TAG_CHANNEL }} Version - name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest run: pnpm release-version autobuild-latest
- name: Override updater endpoints for autobuild
run: pnpm override-updater-endpoints autobuild
- name: Add Rust Target - name: Add Rust Target
run: | run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed # Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
@ -304,6 +307,9 @@ jobs:
- name: Release ${{ env.TAG_CHANNEL }} Version - name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest run: pnpm release-version autobuild-latest
- name: Override updater endpoints for autobuild
run: pnpm override-updater-endpoints autobuild
- name: 'Setup for linux' - name: 'Setup for linux'
run: |- run: |-
sudo ls -lR /etc/apt/ sudo ls -lR /etc/apt/
@ -449,6 +455,9 @@ jobs:
- name: Release ${{ env.TAG_CHANNEL }} Version - name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest run: pnpm release-version autobuild-latest
- name: Override updater endpoints for autobuild
run: pnpm override-updater-endpoints autobuild
- name: Download WebView2 Runtime - name: Download WebView2 Runtime
run: | run: |
invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab invoke-webrequest -uri https://github.com/westinyang/WebView2RuntimeArchive/releases/download/133.0.3065.92/Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab -outfile Microsoft.WebView2.FixedVersionRuntime.133.0.3065.92.${{ matrix.arch }}.cab
@ -510,6 +519,37 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update-autobuild:
name: Release Autobuild Updater
runs-on: ubuntu-latest
needs:
[
autobuild-x86-windows-macos-linux,
autobuild-arm-linux,
autobuild-x86-arm-windows_webview2,
]
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Node
uses: actions/setup-node@v6
with:
node-version: '24.14.1'
- uses: pnpm/action-setup@v5.0.0
name: Install pnpm
with:
run_install: false
- name: Pnpm install
run: pnpm i
- name: Release autobuild updater file
run: pnpm updater-autobuild
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
notify-telegram: notify-telegram:
name: Notify Telegram name: Notify Telegram
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -519,6 +559,7 @@ jobs:
autobuild-x86-windows-macos-linux, autobuild-x86-windows-macos-linux,
autobuild-arm-linux, autobuild-arm-linux,
autobuild-x86-arm-windows_webview2, autobuild-x86-arm-windows_webview2,
release-update-autobuild,
] ]
steps: steps:
- name: Checkout repository - name: Checkout repository

View File

@ -16,7 +16,9 @@
"web:serve": "vite preview", "web:serve": "vite preview",
"prebuild": "node scripts/prebuild.mjs", "prebuild": "node scripts/prebuild.mjs",
"updater": "node scripts/updater.mjs", "updater": "node scripts/updater.mjs",
"updater-autobuild": "node scripts/updater.mjs autobuild",
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs", "updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
"override-updater-endpoints": "node scripts/override-updater-endpoints.mjs",
"portable": "node scripts/portable.mjs", "portable": "node scripts/portable.mjs",
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs", "portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
"fix-alpha-version": "node scripts/fix-alpha_version.mjs", "fix-alpha-version": "node scripts/fix-alpha_version.mjs",

View File

@ -0,0 +1,32 @@
/**
* Override updater endpoints in tauri.conf.json for a specific channel.
*
* Usage:
* node scripts/override-updater-endpoints.mjs <channel>
*
* Example:
* node scripts/override-updater-endpoints.mjs autobuild
* # Changes: .../releases/download/updater/update.json
* # : .../releases/download/updater-autobuild/update.json
*/
import fs from 'fs'
import path from 'path'
const channel = process.argv[2]
if (!channel) {
console.error('Usage: node scripts/override-updater-endpoints.mjs <channel>')
process.exit(1)
}
const configPath = path.join(process.cwd(), 'src-tauri', 'tauri.conf.json')
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
config.plugins.updater.endpoints = config.plugins.updater.endpoints.map(
(endpoint) =>
endpoint.replace('/download/updater/', `/download/updater-${channel}/`),
)
fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
console.log(`[INFO]: Updater endpoints switched to "${channel}" channel:`)
config.plugins.updater.endpoints.forEach((e) => console.log(` ${e}`))

View File

@ -3,26 +3,203 @@ import fetch from 'node-fetch'
import { resolveUpdateLog, resolveUpdateLogDefault } from './updatelog.mjs' import { resolveUpdateLog, resolveUpdateLogDefault } from './updatelog.mjs'
// Add stable update JSON filenames /// get the signature file content
const UPDATE_TAG_NAME = 'updater' async function getSignature(url) {
const UPDATE_JSON_FILE = 'update.json' const response = await fetch(url, {
const UPDATE_JSON_PROXY = 'update-proxy.json' method: 'GET',
// Add alpha update JSON filenames headers: { 'Content-Type': 'application/octet-stream' },
const ALPHA_TAG_NAME = 'updater-alpha' })
const ALPHA_UPDATE_JSON_FILE = 'update.json' return response.text()
const ALPHA_UPDATE_JSON_PROXY = 'update-proxy.json' }
/// generate update.json function buildPlatformData() {
/// upload to update tag's release asset return {
async function resolveUpdater() { win64: { signature: '', url: '' },
if (process.env.GITHUB_TOKEN === undefined) { linux: { signature: '', url: '' },
throw new Error('GITHUB_TOKEN is required') darwin: { signature: '', url: '' },
'darwin-aarch64': { signature: '', url: '' },
'darwin-intel': { signature: '', url: '' },
'darwin-x86_64': { signature: '', url: '' },
'linux-x86_64': { signature: '', url: '' },
'linux-x86': { signature: '', url: '' },
'linux-i686': { signature: '', url: '' },
'linux-aarch64': { signature: '', url: '' },
'linux-armv7': { signature: '', url: '' },
'windows-x86_64': { signature: '', url: '' },
'windows-aarch64': { signature: '', url: '' },
'windows-x86': { signature: '', url: '' },
'windows-i686': { signature: '', url: '' },
}
}
/// Map release assets to platform update data
async function processAssets(release, updateData) {
const promises = release.assets.map(async (asset) => {
const { name, browser_download_url } = asset
// win64 url
if (name.endsWith('x64-setup.exe')) {
updateData.platforms.win64.url = browser_download_url
updateData.platforms['windows-x86_64'].url = browser_download_url
}
// win64 signature
if (name.endsWith('x64-setup.exe.sig')) {
const sig = await getSignature(browser_download_url)
updateData.platforms.win64.signature = sig
updateData.platforms['windows-x86_64'].signature = sig
}
// win32 url
if (name.endsWith('x86-setup.exe')) {
updateData.platforms['windows-x86'].url = browser_download_url
updateData.platforms['windows-i686'].url = browser_download_url
}
// win32 signature
if (name.endsWith('x86-setup.exe.sig')) {
const sig = await getSignature(browser_download_url)
updateData.platforms['windows-x86'].signature = sig
updateData.platforms['windows-i686'].signature = sig
}
// win arm url
if (name.endsWith('arm64-setup.exe')) {
updateData.platforms['windows-aarch64'].url = browser_download_url
}
// win arm signature
if (name.endsWith('arm64-setup.exe.sig')) {
const sig = await getSignature(browser_download_url)
updateData.platforms['windows-aarch64'].signature = sig
}
// darwin url (intel)
if (name.endsWith('.app.tar.gz') && !name.includes('aarch')) {
updateData.platforms.darwin.url = browser_download_url
updateData.platforms['darwin-intel'].url = browser_download_url
updateData.platforms['darwin-x86_64'].url = browser_download_url
}
// darwin signature (intel)
if (name.endsWith('.app.tar.gz.sig') && !name.includes('aarch')) {
const sig = await getSignature(browser_download_url)
updateData.platforms.darwin.signature = sig
updateData.platforms['darwin-intel'].signature = sig
updateData.platforms['darwin-x86_64'].signature = sig
}
// darwin url (aarch)
if (name.endsWith('aarch64.app.tar.gz')) {
updateData.platforms['darwin-aarch64'].url = browser_download_url
// 使linux可以检查更新
updateData.platforms.linux.url = browser_download_url
updateData.platforms['linux-x86_64'].url = browser_download_url
updateData.platforms['linux-x86'].url = browser_download_url
updateData.platforms['linux-i686'].url = browser_download_url
updateData.platforms['linux-aarch64'].url = browser_download_url
updateData.platforms['linux-armv7'].url = browser_download_url
}
// darwin signature (aarch)
if (name.endsWith('aarch64.app.tar.gz.sig')) {
const sig = await getSignature(browser_download_url)
updateData.platforms['darwin-aarch64'].signature = sig
updateData.platforms.linux.signature = sig
updateData.platforms['linux-x86_64'].signature = sig
updateData.platforms['linux-x86'].signature = sig
updateData.platforms['linux-i686'].signature = sig
updateData.platforms['linux-aarch64'].signature = sig
updateData.platforms['linux-armv7'].signature = sig
}
})
await Promise.allSettled(promises)
}
/// Remove platforms without URLs, generate proxy data
function finalizeUpdateData(updateData) {
Object.entries(updateData.platforms).forEach(([key, value]) => {
if (!value.url) {
console.log(`[Error]: failed to parse release for "${key}"`)
delete updateData.platforms[key]
}
})
const proxyData = JSON.parse(JSON.stringify(updateData))
Object.entries(proxyData.platforms).forEach(([key, value]) => {
if (value.url) {
proxyData.platforms[key].url = 'https://update.hwdns.net/' + value.url
} else {
console.log(`[Error]: proxyData.platforms.${key} is null`)
}
})
return proxyData
}
/// Upload update JSON files to a release tag (creates release if not found)
async function uploadToRelease(
github,
options,
{ tagName, jsonFile, proxyFile, releaseName, releaseBody, prerelease },
updateData,
proxyData,
) {
let updateRelease
try {
const response = await github.rest.repos.getReleaseByTag({
...options,
tag: tagName,
})
updateRelease = response.data
console.log(
`Found existing ${tagName} release with ID: ${updateRelease.id}`,
)
} catch (error) {
if (error.status === 404) {
console.log(`Release ${tagName} not found, creating...`)
const createResponse = await github.rest.repos.createRelease({
...options,
tag_name: tagName,
name: releaseName,
body: releaseBody,
prerelease: !!prerelease,
})
updateRelease = createResponse.data
console.log(`Created ${tagName} release with ID: ${updateRelease.id}`)
} else {
throw error
}
} }
const options = { owner: context.repo.owner, repo: context.repo.repo } // Delete existing assets with matching names
const github = getOctokit(process.env.GITHUB_TOKEN) for (const asset of updateRelease.assets) {
if (asset.name === jsonFile || asset.name === proxyFile) {
await github.rest.repos
.deleteReleaseAsset({ ...options, asset_id: asset.id })
.catch(console.error)
}
}
// Fetch all tags using pagination // Upload new assets
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: jsonFile,
data: JSON.stringify(updateData, null, 2),
})
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: proxyFile,
data: JSON.stringify(proxyData, null, 2),
})
console.log(`Successfully uploaded update files to ${tagName}`)
}
// ─── Channel: stable (default) ──────────────────────────<E29480><E29480><EFBFBD>───────────────────
async function resolveStableChannel(github, options) {
// Fetch tags to find the latest stable release (vX.Y.Z)
let allTags = [] let allTags = []
let page = 1 let page = 1
const perPage = 100 const perPage = 100
@ -33,289 +210,226 @@ async function resolveUpdater() {
per_page: perPage, per_page: perPage,
page: page, page: page,
}) })
allTags = allTags.concat(pageTags) allTags = allTags.concat(pageTags)
if (pageTags.length < perPage) break
// Break if we received fewer tags than requested (last page)
if (pageTags.length < perPage) {
break
}
page++ page++
} }
const tags = allTags console.log(`Retrieved ${allTags.length} tags in total`)
console.log(`Retrieved ${tags.length} tags in total`)
// More flexible tag detection with regex patterns const stableTag = allTags.find((t) => /^v\d+\.\d+\.\d+$/.test(t.name))
const stableTagRegex = /^v\d+\.\d+\.\d+$/ // Matches vX.Y.Z format
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i // Matches exact alpha/beta/rc/pre tags
// Get the latest stable tag and pre-release tag
const stableTag = tags.find((t) => stableTagRegex.test(t.name))
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name))
console.log('All tags:', tags.map((t) => t.name).join(', '))
console.log('Stable tag:', stableTag ? stableTag.name : 'None found') console.log('Stable tag:', stableTag ? stableTag.name : 'None found')
console.log(
'Pre-release tag:',
preReleaseTag ? preReleaseTag.name : 'None found',
)
console.log()
// Process stable release if (!stableTag) {
if (stableTag) { console.log('No stable tag found, nothing to do')
await processRelease(github, options, stableTag, false) return
} }
// Process pre-release if found
if (preReleaseTag) {
await processRelease(github, options, preReleaseTag, true)
}
}
// Process a release (stable or alpha) and generate update files
async function processRelease(github, options, tag, isAlpha) {
if (!tag) return
try { try {
const { data: release } = await github.rest.repos.getReleaseByTag({ const { data: release } = await github.rest.repos.getReleaseByTag({
...options, ...options,
tag: tag.name, tag: stableTag.name,
}) })
const updateData = { const updateData = {
name: tag.name, name: stableTag.name,
notes: await resolveUpdateLog(tag.name).catch(() => notes: await resolveUpdateLog(stableTag.name).catch(() =>
resolveUpdateLogDefault().catch(() => 'No changelog available'), resolveUpdateLogDefault().catch(() => 'No changelog available'),
), ),
pub_date: new Date().toISOString(), pub_date: new Date().toISOString(),
platforms: { platforms: buildPlatformData(),
win64: { signature: '', url: '' }, // compatible with older formats
linux: { signature: '', url: '' }, // compatible with older formats
darwin: { signature: '', url: '' }, // compatible with older formats
'darwin-aarch64': { signature: '', url: '' },
'darwin-intel': { signature: '', url: '' },
'darwin-x86_64': { signature: '', url: '' },
'linux-x86_64': { signature: '', url: '' },
'linux-x86': { signature: '', url: '' },
'linux-i686': { signature: '', url: '' },
'linux-aarch64': { signature: '', url: '' },
'linux-armv7': { signature: '', url: '' },
'windows-x86_64': { signature: '', url: '' },
'windows-aarch64': { signature: '', url: '' },
'windows-x86': { signature: '', url: '' },
'windows-i686': { signature: '', url: '' },
},
} }
const promises = release.assets.map(async (asset) => { await processAssets(release, updateData)
const { name, browser_download_url } = asset
// Process all the platform URL and signature data
// win64 url
if (name.endsWith('x64-setup.exe')) {
updateData.platforms.win64.url = browser_download_url
updateData.platforms['windows-x86_64'].url = browser_download_url
}
// win64 signature
if (name.endsWith('x64-setup.exe.sig')) {
const sig = await getSignature(browser_download_url)
updateData.platforms.win64.signature = sig
updateData.platforms['windows-x86_64'].signature = sig
}
// win32 url
if (name.endsWith('x86-setup.exe')) {
updateData.platforms['windows-x86'].url = browser_download_url
updateData.platforms['windows-i686'].url = browser_download_url
}
// win32 signature
if (name.endsWith('x86-setup.exe.sig')) {
const sig = await getSignature(browser_download_url)
updateData.platforms['windows-x86'].signature = sig
updateData.platforms['windows-i686'].signature = sig
}
// win arm url
if (name.endsWith('arm64-setup.exe')) {
updateData.platforms['windows-aarch64'].url = browser_download_url
}
// win arm signature
if (name.endsWith('arm64-setup.exe.sig')) {
const sig = await getSignature(browser_download_url)
updateData.platforms['windows-aarch64'].signature = sig
}
// darwin url (intel)
if (name.endsWith('.app.tar.gz') && !name.includes('aarch')) {
updateData.platforms.darwin.url = browser_download_url
updateData.platforms['darwin-intel'].url = browser_download_url
updateData.platforms['darwin-x86_64'].url = browser_download_url
}
// darwin signature (intel)
if (name.endsWith('.app.tar.gz.sig') && !name.includes('aarch')) {
const sig = await getSignature(browser_download_url)
updateData.platforms.darwin.signature = sig
updateData.platforms['darwin-intel'].signature = sig
updateData.platforms['darwin-x86_64'].signature = sig
}
// darwin url (aarch)
if (name.endsWith('aarch64.app.tar.gz')) {
updateData.platforms['darwin-aarch64'].url = browser_download_url
// 使linux可以检查更新
updateData.platforms.linux.url = browser_download_url
updateData.platforms['linux-x86_64'].url = browser_download_url
updateData.platforms['linux-x86'].url = browser_download_url
updateData.platforms['linux-i686'].url = browser_download_url
updateData.platforms['linux-aarch64'].url = browser_download_url
updateData.platforms['linux-armv7'].url = browser_download_url
}
// darwin signature (aarch)
if (name.endsWith('aarch64.app.tar.gz.sig')) {
const sig = await getSignature(browser_download_url)
updateData.platforms['darwin-aarch64'].signature = sig
updateData.platforms.linux.signature = sig
updateData.platforms['linux-x86_64'].signature = sig
updateData.platforms['linux-x86'].url = browser_download_url
updateData.platforms['linux-i686'].url = browser_download_url
updateData.platforms['linux-aarch64'].signature = sig
updateData.platforms['linux-armv7'].signature = sig
}
})
await Promise.allSettled(promises)
console.log(updateData) console.log(updateData)
// maybe should test the signature as well const proxyData = finalizeUpdateData(updateData)
// delete the null field
Object.entries(updateData.platforms).forEach(([key, value]) => {
if (!value.url) {
console.log(`[Error]: failed to parse release for "${key}"`)
delete updateData.platforms[key]
}
})
// Generate a proxy update file for accelerated GitHub resources await uploadToRelease(
const updateDataNew = JSON.parse(JSON.stringify(updateData)) github,
options,
Object.entries(updateDataNew.platforms).forEach(([key, value]) => { {
if (value.url) { tagName: 'updater',
updateDataNew.platforms[key].url = jsonFile: 'update.json',
'https://update.hwdns.net/' + value.url proxyFile: 'update-proxy.json',
} else { releaseName: 'Auto-update Stable Channel',
console.log(`[Error]: updateDataNew.platforms.${key} is null`) releaseBody:
} 'This release contains the update information for stable channel.',
}) prerelease: false,
},
// Get the appropriate updater release based on isAlpha flag updateData,
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME proxyData,
console.log(
`Processing ${isAlpha ? 'alpha' : 'stable'} release:`,
releaseTag,
) )
try {
let updateRelease
try {
// Try to get the existing release
const response = await github.rest.repos.getReleaseByTag({
...options,
tag: releaseTag,
})
updateRelease = response.data
console.log(
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
)
} catch (error) {
// If release doesn't exist, create it
if (error.status === 404) {
console.log(
`Release with tag ${releaseTag} not found, creating new release...`,
)
const createResponse = await github.rest.repos.createRelease({
...options,
tag_name: releaseTag,
name: isAlpha
? 'Auto-update Alpha Channel'
: 'Auto-update Stable Channel',
body: `This release contains the update information for ${isAlpha ? 'alpha' : 'stable'} channel.`,
prerelease: isAlpha,
})
updateRelease = createResponse.data
console.log(
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
)
} else {
// If it's another error, throw it
throw error
}
}
// File names based on release type
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY
// Delete existing assets with these names
for (const asset of updateRelease.assets) {
if (asset.name === jsonFile) {
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: asset.id,
})
}
if (asset.name === proxyFile) {
await github.rest.repos
.deleteReleaseAsset({ ...options, asset_id: asset.id })
.catch(console.error) // do not break the pipeline
}
}
// Upload new assets
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: jsonFile,
data: JSON.stringify(updateData, null, 2),
})
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: proxyFile,
data: JSON.stringify(updateDataNew, null, 2),
})
console.log(
`Successfully uploaded ${isAlpha ? 'alpha' : 'stable'} update files to ${releaseTag}`,
)
} catch (error) {
console.error(
`Failed to process ${isAlpha ? 'alpha' : 'stable'} release:`,
error.message,
)
}
} catch (error) { } catch (error) {
if (error.status === 404) { if (error.status === 404) {
console.log(`Release not found for tag: ${tag.name}, skipping...`) console.log(`Release not found for tag: ${stableTag.name}, skipping...`)
} else { } else {
console.error(`Failed to get release for tag: ${tag.name}`, error.message) console.error(
`Failed to get release for tag: ${stableTag.name}`,
error.message,
)
} }
} }
} }
// get the signature file content // ─── Channel: autobuild ─────────────────────────────────────────────────────
async function getSignature(url) {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/octet-stream' },
})
return response.text() function parseBaseVersion(version) {
if (!version) return null
const match = version.replace(/^v/i, '').match(/^(\d+)\.(\d+)\.(\d+)/)
if (!match) return null
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])]
} }
resolveUpdater().catch(console.error) function compareBase(a, b) {
if (!a || !b) return 0
for (let i = 0; i < 3; i++) {
if (a[i] > b[i]) return 1
if (a[i] < b[i]) return -1
}
return 0
}
function extractVersionFromAssets(assets) {
for (const asset of assets) {
const match = asset.name.match(
/Clash[._]Verge[_-]([\d]+\.[\d]+\.[\d]+(?:[+][^\s_]+)?)/,
)
if (match) return match[1]
}
return null
}
async function resolveAutobuildChannel(github, options) {
// 1. Get autobuild release
let autobuildRelease = null
try {
const { data } = await github.rest.repos.getReleaseByTag({
...options,
tag: 'autobuild',
})
autobuildRelease = data
} catch (error) {
if (error.status === 404) {
console.log('No autobuild release found')
} else {
throw error
}
}
// 2. Get latest stable release (vX.Y.Z tag)
let stableRelease = null
let stableTag = null
const { data: tags } = await github.rest.repos.listTags({
...options,
per_page: 20,
})
stableTag = tags.find((t) => /^v\d+\.\d+\.\d+$/.test(t.name))
if (stableTag) {
try {
const { data } = await github.rest.repos.getReleaseByTag({
...options,
tag: stableTag.name,
})
stableRelease = data
} catch (error) {
if (error.status !== 404) throw error
}
}
// 3. Compare base versions — stable wins only if strictly higher
const autobuildVersion = autobuildRelease
? extractVersionFromAssets(autobuildRelease.assets)
: null
const stableVersion = stableTag?.name?.replace(/^v/, '') ?? null
console.log(
`Autobuild version: ${autobuildVersion} (base: ${parseBaseVersion(autobuildVersion)})`,
)
console.log(
`Stable version: ${stableVersion} (base: ${parseBaseVersion(stableVersion)})`,
)
let useRelease, useVersion
const cmp = compareBase(
parseBaseVersion(stableVersion),
parseBaseVersion(autobuildVersion),
)
if (cmp > 0 && stableRelease) {
console.log('→ Using stable release (higher base version)')
useRelease = stableRelease
useVersion = stableVersion
} else if (autobuildRelease && autobuildVersion) {
console.log('→ Using autobuild release')
useRelease = autobuildRelease
useVersion = autobuildVersion
} else if (stableRelease) {
console.log('→ Falling back to stable release (no autobuild available)')
useRelease = stableRelease
useVersion = stableVersion
} else {
console.log('No releases found, nothing to do')
return
}
// 4. Build update data
const notes = await resolveUpdateLogDefault().catch(
() =>
'More new features are now supported. Check release page for details.',
)
const updateData = {
version: useVersion,
name: useVersion,
notes,
pub_date: new Date().toISOString(),
platforms: buildPlatformData(),
}
await processAssets(useRelease, updateData)
console.log(updateData)
const proxyData = finalizeUpdateData(updateData)
await uploadToRelease(
github,
options,
{
tagName: 'updater-autobuild',
jsonFile: 'update.json',
proxyFile: 'update-proxy.json',
releaseName: 'Auto-update AutoBuild Channel',
releaseBody:
'This release contains the update information for the AutoBuild channel.',
prerelease: true,
},
updateData,
proxyData,
)
}
// ─── Main ────────────────────────────────────────────────────────────────────
async function main() {
if (!process.env.GITHUB_TOKEN) {
throw new Error('GITHUB_TOKEN is required')
}
const options = { owner: context.repo.owner, repo: context.repo.repo }
const github = getOctokit(process.env.GITHUB_TOKEN)
const channel = process.argv[2]
if (channel === 'autobuild') {
console.log('=== Resolving autobuild channel ===')
await resolveAutobuildChannel(github, options)
} else {
console.log('=== Resolving stable channel ===')
await resolveStableChannel(github, options)
}
}
main().catch(console.error)

View File

@ -134,7 +134,11 @@ const localVersionNormalized = normalizeVersion(appVersion)
export const checkUpdateSafe = async ( export const checkUpdateSafe = async (
options?: CheckOptions, options?: CheckOptions,
): Promise<Update | null> => { ): Promise<Update | null> => {
const result = await check({ ...(options ?? {}), allowDowngrades: false }) // allowDowngrades: true so the Tauri plugin always returns an Update object.
// Semver ignores build metadata (+autobuild.MMDD.hash), so the plugin would
// treat same-base autobuild versions as equal and return null.
// Our custom compareVersions below handles the actual upgrade decision.
const result = await check({ ...(options ?? {}), allowDowngrades: true })
if (!result) return null if (!result) return null
const remoteVersion = resolveRemoteVersion(result) const remoteVersion = resolveRemoteVersion(result)