diff --git a/lib/updater.js b/lib/updater.js index ac38810..722f5f6 100644 --- a/lib/updater.js +++ b/lib/updater.js @@ -37,6 +37,14 @@ function isNewer(remote, current) { return r.patch > c.patch; } +function resolveReleaseVersion(release) { + for (const value of [release && release.name, release && release.tag_name]) { + const match = String(value || '').match(/(?:^|[^\d])v?(\d+\.\d+\.\d+)(?=$|[^\d.])/i); + if (match) return match[1]; + } + return ''; +} + function pickSetupAsset(assets) { if (!Array.isArray(assets)) return null; // Prefer asset with "setup" in the name (case-insensitive) @@ -90,11 +98,12 @@ async function checkForUpdate() { } const release = releases[0]; - const remoteVersion = release.tag_name || release.name || ''; + const remoteVersion = resolveReleaseVersion(release); + const transportTag = release.tag_name || ''; const currentVersion = getCurrentVersion(); if (!isNewer(remoteVersion, currentVersion)) { - cachedCheck = { available: false, currentVersion, remoteVersion }; + cachedCheck = { available: false, currentVersion, remoteVersion, transportTag }; cachedCheckTs = Date.now(); return cachedCheck; } @@ -109,7 +118,8 @@ async function checkForUpdate() { cachedCheck = { available: true, currentVersion, - remoteVersion: remoteVersion.replace(/^v/i, ''), + remoteVersion, + transportTag, releaseUrl: release.html_url, assetUrl: setupAsset.browser_download_url, assetSize: setupAsset.size, @@ -280,4 +290,4 @@ function abortUpdate() { } } -module.exports = { checkForUpdate, installUpdate, abortUpdate }; +module.exports = { checkForUpdate, installUpdate, abortUpdate, isNewer, resolveReleaseVersion }; diff --git a/package-lock.json b/package-lock.json index d920776..3f7927c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "multi-hoster-uploader", - "version": "3.3.16", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "multi-hoster-uploader", - "version": "3.3.16", + "version": "2.0.1", "dependencies": { "chokidar": "^3.6.0", "undici": "^7.16.0", diff --git a/package.json b/package.json index 939b687..5306566 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multi-hoster-uploader", - "version": "3.3.108", + "version": "2.0.1", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "main": "main.js", "scripts": { diff --git a/scripts/release_gitea.mjs b/scripts/release_gitea.mjs index 630d75b..dbb1251 100644 --- a/scripts/release_gitea.mjs +++ b/scripts/release_gitea.mjs @@ -1,27 +1,53 @@ #!/usr/bin/env node import { execSync } from 'child_process'; import { createHash } from 'crypto'; -import { readFileSync, writeFileSync, statSync, createReadStream, existsSync } from 'fs'; +import { writeFileSync, statSync, createReadStream, existsSync } from 'fs'; import { resolve, basename } from 'path'; +import { pathToFileURL } from 'url'; const ROOT = resolve(import.meta.dirname, '..'); -const PKG_PATH = resolve(ROOT, 'package.json'); const RELEASE_DIR = resolve(ROOT, 'release'); const PRODUCT_NAME = 'Multi-Hoster-Upload'; // --- CLI args --- -const args = process.argv.slice(2); -const dryRun = args.includes('--dry-run'); -const version = args.find(a => /^\d+\.\d+\.\d+$/.test(a)); -const notes = args.filter(a => a !== version && a !== '--dry-run').join(' ') || ''; +export function parseReleaseArgs(args) { + const version = Array.isArray(args) ? args[0] : ''; + if (!/^\d+\.\d+\.\d+$/.test(version || '')) { + throw new Error('Usage: node scripts/release_gitea.mjs --transport-tag [release notes] [--dry-run]'); + } -if (!version) { - console.error('Usage: node scripts/release_gitea.mjs [release notes] [--dry-run]'); - console.error('Example: node scripts/release_gitea.mjs 1.0.1 "Bugfix release"'); - process.exit(1); + const transportTagIndex = args.indexOf('--transport-tag'); + const transportTag = transportTagIndex >= 0 ? args[transportTagIndex + 1] : ''; + if (!/^v\d+\.\d+\.\d+$/.test(transportTag)) { + throw new Error('--transport-tag must match vX.Y.Z'); + } + + const excludedIndexes = new Set([0, transportTagIndex, transportTagIndex + 1]); + const notes = args.filter((arg, index) => !excludedIndexes.has(index) && arg !== '--dry-run').join(' '); + return { version, transportTag, notes, dryRun: args.includes('--dry-run') }; } -const tag = `v${version}`; +export function createReleasePlan(options) { + const releaseTitle = `${PRODUCT_NAME} v${options.version}`; + const setupName = `${PRODUCT_NAME} Setup ${options.version}.exe`; + const portableName = `${PRODUCT_NAME} ${options.version}.exe`; + return { + ...options, + tag: options.transportTag, + releaseTitle, + releaseBody: options.notes || releaseTitle, + setupName, + portableName, + expectedArtifacts: [setupName, portableName, 'latest.yml'], + blockmapName: `${setupName}.blockmap` + }; +} + +export function renderLatestYml(plan, sha, size, releaseDate = new Date().toISOString()) { + return `version: ${plan.version}\nfiles:\n - url: ${plan.setupName}\n sha512: ${sha}\n size: ${size}\npath: ${plan.setupName}\nsha512: ${sha}\nreleaseDate: '${releaseDate}'\n`; +} + +let dryRun = false; // --- Helpers --- function run(cmd, opts = {}) { @@ -109,8 +135,11 @@ async function uploadAsset(releaseId, filePath, token) { } // --- Main --- -async function main() { - console.log(`\nReleasing ${PRODUCT_NAME} ${tag}${dryRun ? ' [DRY RUN]' : ''}\n`); +async function main(args = process.argv.slice(2)) { + const plan = createReleasePlan(parseReleaseArgs(args)); + const { version, tag } = plan; + dryRun = plan.dryRun; + console.log(`\nReleasing ${plan.releaseTitle} via ${tag}${dryRun ? ' [DRY RUN]' : ''}\n`); // 1. Resolve remote const remote = resolveGiteaRemote(); @@ -138,18 +167,19 @@ async function main() { if (!recoveryMode) { // 4. Update package.json version - const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf-8')); - pkg.version = version; - if (!dryRun) writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\n', 'utf-8'); - console.log(`Updated package.json -> ${version}`); + run(`npm version ${version} --no-git-tag-version --allow-same-version`); + console.log(`Updated package.json and package-lock.json -> ${version}`); // 5. Build console.log('\nBuilding...'); run('npm run release:win', { stdio: 'inherit' }); // 6. Git commit + tag + push - run('git add package.json'); - run(`git commit -m "release: ${tag}"`); + const versionStatus = run('git status --porcelain -- package.json package-lock.json', { allowDry: true }); + if (versionStatus) { + run('git add package.json package-lock.json'); + run(`git commit -m "release: ${tag}"`); + } run(`git tag ${tag}`); run(`git push ${remote.name} HEAD`); run(`git push ${remote.name} ${tag}`); @@ -157,8 +187,7 @@ async function main() { // 6b. Regenerate latest.yml to ensure correct SHA-512 { - const setupName = `${PRODUCT_NAME} Setup ${version}.exe`; - const setupPath = resolve(RELEASE_DIR, setupName); + const setupPath = resolve(RELEASE_DIR, plan.setupName); if (existsSync(setupPath)) { const sha = await new Promise((res, rej) => { const h = createHash('sha512'); @@ -168,18 +197,14 @@ async function main() { s.on('error', rej); }); const size = statSync(setupPath).size; - const yml = `version: ${version}\nfiles:\n - url: ${setupName}\n sha512: ${sha}\n size: ${size}\npath: ${setupName}\nsha512: ${sha}\nreleaseDate: '${new Date().toISOString()}'\n`; + const yml = renderLatestYml(plan, sha, size); writeFileSync(resolve(RELEASE_DIR, 'latest.yml'), yml, 'utf-8'); console.log('Regenerated latest.yml with correct SHA-512'); } } // 7. Verify artifacts - const expectedArtifacts = [ - `${PRODUCT_NAME} Setup ${version}.exe`, - `${PRODUCT_NAME} ${version}.exe`, - 'latest.yml' - ]; + const expectedArtifacts = plan.expectedArtifacts; for (const name of expectedArtifacts) { const p = resolve(RELEASE_DIR, name); @@ -190,7 +215,7 @@ async function main() { } // Also check for blockmap - const blockmapName = `${PRODUCT_NAME} Setup ${version}.exe.blockmap`; + const blockmapName = plan.blockmapName; const hasBlockmap = existsSync(resolve(RELEASE_DIR, blockmapName)); console.log('\nArtifacts verified.'); @@ -203,14 +228,13 @@ async function main() { } // 9. Create release - const releaseBody = notes || `${PRODUCT_NAME} ${tag}`; let releaseId; const { status: createStatus, data: createData } = await giteaApi( 'POST', `/api/v1/repos/Administrator/${PRODUCT_NAME}/releases`, token, - { tag_name: tag, name: `${PRODUCT_NAME} ${tag}`, body: releaseBody } + { tag_name: tag, name: plan.releaseTitle, body: plan.releaseBody } ); if (createStatus === 409 || createStatus === 422) { @@ -234,7 +258,10 @@ async function main() { console.log(`\nDone! Release: ${process.env.GITEA_BASE_URL || 'https://git.24-music.de'}/Administrator/${PRODUCT_NAME}/releases/tag/${tag}\n`); } -main().catch(err => { - console.error('\nRelease failed:', err.message); - process.exit(1); -}); +const entryPoint = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ''; +if (entryPoint === import.meta.url) { + main().catch(err => { + console.error('\nRelease failed:', err.message); + process.exit(1); + }); +} diff --git a/tests/updater-version.test.js b/tests/updater-version.test.js new file mode 100644 index 0000000..227dfa7 --- /dev/null +++ b/tests/updater-version.test.js @@ -0,0 +1,61 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); +const { pathToFileURL } = require('node:url'); + +const { isNewer, resolveReleaseVersion } = require('../lib/updater'); + +test('bridge title resolves product version instead of transport tag', () => { + assert.equal(resolveReleaseVersion({ name: 'Multi-Hoster-Upload v2.0.1', tag_name: 'v3.3.109' }), '2.0.1'); + assert.equal(isNewer('2.0.1', '2.0.1'), false); + assert.equal(isNewer('2.0.2', '2.0.1'), true); +}); + +test('release CLI rejects a malformed transport tag before release work', () => { + const script = path.resolve(__dirname, '../scripts/release_gitea.mjs'); + const result = spawnSync(process.execPath, [script, '2.0.1', '--transport-tag', '3.3.109', 'Bridge', '--dry-run'], { + cwd: path.resolve(__dirname, '..'), + encoding: 'utf8' + }); + + assert.equal(result.status, 1); + assert.match(result.stderr, /--transport-tag must match vX\.Y\.Z/); + assert.doesNotMatch(result.stdout, /npm run release:win/); +}); + +test('release plan keeps product artifacts separate from the transport tag', () => { + const script = path.resolve(__dirname, '../scripts/release_gitea.mjs'); + const moduleUrl = pathToFileURL(script).href; + const source = ` + import { createReleasePlan, parseReleaseArgs, renderLatestYml } from ${JSON.stringify(moduleUrl)}; + const plan = createReleasePlan(parseReleaseArgs(['2.0.1', '--transport-tag', 'v3.3.109', 'Bridge', 'notes'])); + const latestYml = renderLatestYml(plan, 'abc123', 456, '2026-08-07T12:00:00.000Z'); + process.stdout.write(JSON.stringify({ + version: plan.version, + transportTag: plan.transportTag, + releaseTitle: plan.releaseTitle, + releaseBody: plan.releaseBody, + expectedArtifacts: plan.expectedArtifacts, + latestYml + })); + `; + const result = spawnSync(process.execPath, ['--input-type=module', '--eval', source], { + cwd: path.resolve(__dirname, '..'), + encoding: 'utf8' + }); + + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(JSON.parse(result.stdout), { + version: '2.0.1', + transportTag: 'v3.3.109', + releaseTitle: 'Multi-Hoster-Upload v2.0.1', + releaseBody: 'Bridge notes', + expectedArtifacts: [ + 'Multi-Hoster-Upload Setup 2.0.1.exe', + 'Multi-Hoster-Upload 2.0.1.exe', + 'latest.yml' + ], + latestYml: "version: 2.0.1\nfiles:\n - url: Multi-Hoster-Upload Setup 2.0.1.exe\n sha512: abc123\n size: 456\npath: Multi-Hoster-Upload Setup 2.0.1.exe\nsha512: abc123\nreleaseDate: '2026-08-07T12:00:00.000Z'\n" + }); +});