fix: fix release

This commit is contained in:
lucasdpt
2026-06-19 11:24:26 +02:00
parent 81f66e25eb
commit 3a9a555f19
4 changed files with 83 additions and 46 deletions
-1
View File
@@ -55,4 +55,3 @@ jobs:
GITEA_URL: ${{ github.server_url }} GITEA_URL: ${{ github.server_url }}
GITEA_OWNER: ${{ github.repository_owner }} GITEA_OWNER: ${{ github.repository_owner }}
GITEA_REPO: ${{ github.event.repository.name }} GITEA_REPO: ${{ github.event.repository.name }}
GITEA_TAG: latest
+3 -1
View File
@@ -31,7 +31,9 @@ linux:
- deb - deb
maintainer: oflauncher maintainer: oflauncher
category: Game category: Game
artifactName: ${productName}-${version}.${ext} # Pas de version dans le nom : OFLauncher.AppImage est toujours écrasé sur
# place par l'auto-update, les raccourcis bureau restent valides après maj.
artifactName: ${productName}.${ext}
# Publication des binaires du launcher (auto-update). # Publication des binaires du launcher (auto-update).
# Provider "generic" : electron-updater lit latest.yml à cette URL fixe. # Provider "generic" : electron-updater lit latest.yml à cette URL fixe.
# Les artefacts (latest.yml + installeur + .blockmap) sont uploadés sur une # Les artefacts (latest.yml + installeur + .blockmap) sont uploadés sur une
+68 -34
View File
@@ -1,18 +1,18 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* Publie les artefacts d'auto-update du launcher sur une release Gitea à tag * Publie les artefacts du launcher sur deux releases Gitea :
* fixe ("latest"). electron-updater (provider generic) lit ensuite latest.yml
* à l'URL configurée dans electron-builder.yml.
* *
* Pré-requis : * 1. Release à tag fixe "latest" — electron-updater (provider generic) lit
* - `npm run build:win` a produit dist/ (latest.yml + installeur + .blockmap) * latest.yml à cette URL pour proposer les mises à jour automatiques.
* - variable d'env GITEA_TOKEN (scope write:repository)
* *
* Config par variables d'env (avec valeurs par défaut) : * 2. Release versionnée "vX.Y.Z" — archive permanente consultable dans l'UI
* Gitea, utile pour le suivi des versions et les téléchargements manuels.
*
* Config par variables d'env :
* GITEA_URL base de l'instance (def. https://gitea.ldpt.fr) * GITEA_URL base de l'instance (def. https://gitea.ldpt.fr)
* GITEA_OWNER propriétaire du repo (def. zertus) * GITEA_OWNER propriétaire du repo (def. zertus)
* GITEA_REPO nom du repo launcher (def. OFLauncher) * GITEA_REPO nom du repo launcher (def. OFLauncher)
* GITEA_TAG tag fixe de la release (def. latest) * GITEA_TOKEN token write:repository (obligatoire)
*/ */
import { readdir, readFile } from 'node:fs/promises' import { readdir, readFile } from 'node:fs/promises'
import { join } from 'node:path' import { join } from 'node:path'
@@ -20,7 +20,6 @@ import { join } from 'node:path'
const BASE = process.env.GITEA_URL ?? 'https://gitea.ldpt.fr' const BASE = process.env.GITEA_URL ?? 'https://gitea.ldpt.fr'
const OWNER = process.env.GITEA_OWNER ?? 'zertus' const OWNER = process.env.GITEA_OWNER ?? 'zertus'
const REPO = process.env.GITEA_REPO ?? 'OFLauncher' const REPO = process.env.GITEA_REPO ?? 'OFLauncher'
const TAG = process.env.GITEA_TAG ?? 'latest'
const TOKEN = process.env.GITEA_TOKEN const TOKEN = process.env.GITEA_TOKEN
const DIST = join(process.cwd(), 'dist') const DIST = join(process.cwd(), 'dist')
@@ -31,12 +30,16 @@ if (!TOKEN) {
process.exit(1) process.exit(1)
} }
/** Lit la version courante depuis package.json. */
async function readVersion() {
const pkg = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf8'))
return pkg.version // ex. "3.1.0"
}
/** /**
* Fichiers de dist/ à publier pour l'auto-update. * Fichiers de dist/ à publier.
* - Windows : latest.yml + l'installeur NSIS (+ .blockmap) * - Windows : latest.yml + installeur NSIS (+ .blockmap)
* - Linux : latest-linux.yml + l'AppImage (+ .blockmap) ; le .deb est publié * - Linux : latest-linux.yml + AppImage (+ .blockmap) + .deb
* pour téléchargement manuel (electron-updater ne l'utilise pas).
* electron-updater choisit le bon latest*.yml selon la plateforme.
*/ */
function isUpdateArtifact(name) { function isUpdateArtifact(name) {
return ( return (
@@ -61,65 +64,96 @@ async function api(path, init = {}) {
}) })
if (!res.ok) { if (!res.ok) {
const body = await res.text().catch(() => '') const body = await res.text().catch(() => '')
throw new Error(`Gitea ${init.method ?? 'GET'} ${path} -> ${res.status} ${body}`) throw new Error(`Gitea ${init.method ?? 'GET'} ${path} ${res.status} ${body}`)
} }
return res.status === 204 ? null : res.json() return res.status === 204 ? null : res.json()
} }
/** Récupère la release au tag fixe, ou la crée si absente. */ /** Récupère la release par tag, ou la crée avec les options données. */
async function getOrCreateRelease() { async function getOrCreateRelease(tag, createBody) {
const res = await fetch(`${API}/repos/${OWNER}/${REPO}/releases/tags/${TAG}`, { const res = await fetch(`${API}/repos/${OWNER}/${REPO}/releases/tags/${tag}`, {
headers: { Authorization: `token ${TOKEN}`, Accept: 'application/json' } headers: { Authorization: `token ${TOKEN}`, Accept: 'application/json' }
}) })
if (res.ok) return res.json() if (res.ok) return res.json()
if (res.status !== 404) { if (res.status !== 404) {
throw new Error(`Gitea GET release -> ${res.status} ${await res.text()}`) throw new Error(`Gitea GET release/${tag} ${res.status} ${await res.text()}`)
} }
console.log(`Création de la release "${TAG}"…`) console.log(`Création de la release "${tag}"…`)
return api(`/repos/${OWNER}/${REPO}/releases`, { return api(`/repos/${OWNER}/${REPO}/releases`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify(createBody)
tag_name: TAG,
name: 'Dernière version',
body: 'Artefacts dauto-update du launcher (écrasés à chaque publication).'
})
}) })
} }
async function deleteExistingAssets(releaseId) { async function deleteExistingAssets(releaseId) {
const assets = await api(`/repos/${OWNER}/${REPO}/releases/${releaseId}/assets`) const assets = await api(`/repos/${OWNER}/${REPO}/releases/${releaseId}/assets`)
for (const a of assets) { for (const a of assets) {
console.log(`Suppression de l'ancien asset ${a.name}`) console.log(` Suppression de l'ancien asset "${a.name}"`)
await api(`/repos/${OWNER}/${REPO}/releases/${releaseId}/assets/${a.id}`, { await api(`/repos/${OWNER}/${REPO}/releases/${releaseId}/assets/${a.id}`, {
method: 'DELETE' method: 'DELETE'
}) })
} }
} }
async function uploadAsset(releaseId, name) { async function uploadAssets(releaseId, files) {
for (const name of files) {
const buf = await readFile(join(DIST, name)) const buf = await readFile(join(DIST, name))
const form = new FormData() const form = new FormData()
form.append('attachment', new Blob([buf]), name) form.append('attachment', new Blob([buf]), name)
console.log(`Upload de ${name} (${(buf.length / 1e6).toFixed(1)} Mo)…`) console.log(` Upload "${name}" (${(buf.length / 1e6).toFixed(1)} Mo)…`)
await api( await api(
`/repos/${OWNER}/${REPO}/releases/${releaseId}/assets?name=${encodeURIComponent(name)}`, `/repos/${OWNER}/${REPO}/releases/${releaseId}/assets?name=${encodeURIComponent(name)}`,
{ method: 'POST', body: form } { method: 'POST', body: form }
) )
} }
}
async function publishRelease(tag, createBody, files) {
console.log(`\n── Release "${tag}" ──`)
const release = await getOrCreateRelease(tag, createBody)
await deleteExistingAssets(release.id)
await uploadAssets(release.id, files)
console.log(` ✓ Release "${tag}" publiée.`)
}
async function main() { async function main() {
const version = await readVersion()
const versionTag = `v${version}`
const files = (await readdir(DIST)).filter(isUpdateArtifact) const files = (await readdir(DIST)).filter(isUpdateArtifact)
if (!files.some((f) => f === 'latest.yml')) { if (!files.some((f) => f === 'latest.yml')) {
throw new Error('dist/latest.yml introuvable — lance dabord `npm run build:win`.') throw new Error('dist/latest.yml introuvable — lance d'abord `npm run build:win`.')
} }
console.log(`Publication sur ${OWNER}/${REPO} (tag ${TAG}) : ${files.join(', ')}`) console.log(`Version : ${versionTag}`)
console.log(`Artefacts : ${files.join(', ')}`)
console.log(`Dépôt : ${OWNER}/${REPO}`)
const release = await getOrCreateRelease() // 1. Release fixe "latest" — point d'ancrage de l'auto-update.
await deleteExistingAssets(release.id) await publishRelease(
for (const name of files) await uploadAsset(release.id, name) 'latest',
{
tag_name: 'latest',
name: 'Auto-update (dernière version)',
body: 'Artefacts d'auto-update écrasés à chaque publication. Version courante : ' + versionTag,
prerelease: false
},
files
)
console.log('✓ Publication terminée.') // 2. Release versionnée — archive permanente par version.
await publishRelease(
versionTag,
{
tag_name: versionTag,
name: `OFLauncher ${versionTag}`,
body: '',
prerelease: false
},
files
)
console.log('\n✓ Publication terminée.')
} }
main().catch((e) => { main().catch((e) => {
+5 -3
View File
@@ -24,10 +24,12 @@ export function initUpdater(): void {
return return
} }
// On gère l'install manuellement (bouton "Redémarrer"), mais on installe
// quand même à la fermeture si la maj a été téléchargée.
autoUpdater.autoDownload = true autoUpdater.autoDownload = true
autoUpdater.autoInstallOnAppQuit = true // Sur Linux (AppImage), l'install automatique au quit utilise execFileSync
// et bloque le processus en attendant que le nouveau AppImage se ferme —
// comportement très inattendu. On désactive : l'utilisateur clique le bouton
// "Redémarrer pour installer" qui, lui, utilise spawnLog (async) et fonctionne.
autoUpdater.autoInstallOnAppQuit = process.platform !== 'linux'
autoUpdater.on('checking-for-update', () => { autoUpdater.on('checking-for-update', () => {
emit.updateStatus({ state: 'checking' }) emit.updateStatus({ state: 'checking' })