feat(updater): bridge 3.3.108 clients to v2.0.1

This commit is contained in:
Administrator 2026-08-07 18:02:16 +02:00
parent 95bff2581b
commit c7fa422d9b
5 changed files with 139 additions and 41 deletions

View File

@ -37,6 +37,14 @@ function isNewer(remote, current) {
return r.patch > c.patch; 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) { function pickSetupAsset(assets) {
if (!Array.isArray(assets)) return null; if (!Array.isArray(assets)) return null;
// Prefer asset with "setup" in the name (case-insensitive) // Prefer asset with "setup" in the name (case-insensitive)
@ -90,11 +98,12 @@ async function checkForUpdate() {
} }
const release = releases[0]; const release = releases[0];
const remoteVersion = release.tag_name || release.name || ''; const remoteVersion = resolveReleaseVersion(release);
const transportTag = release.tag_name || '';
const currentVersion = getCurrentVersion(); const currentVersion = getCurrentVersion();
if (!isNewer(remoteVersion, currentVersion)) { if (!isNewer(remoteVersion, currentVersion)) {
cachedCheck = { available: false, currentVersion, remoteVersion }; cachedCheck = { available: false, currentVersion, remoteVersion, transportTag };
cachedCheckTs = Date.now(); cachedCheckTs = Date.now();
return cachedCheck; return cachedCheck;
} }
@ -109,7 +118,8 @@ async function checkForUpdate() {
cachedCheck = { cachedCheck = {
available: true, available: true,
currentVersion, currentVersion,
remoteVersion: remoteVersion.replace(/^v/i, ''), remoteVersion,
transportTag,
releaseUrl: release.html_url, releaseUrl: release.html_url,
assetUrl: setupAsset.browser_download_url, assetUrl: setupAsset.browser_download_url,
assetSize: setupAsset.size, assetSize: setupAsset.size,
@ -280,4 +290,4 @@ function abortUpdate() {
} }
} }
module.exports = { checkForUpdate, installUpdate, abortUpdate }; module.exports = { checkForUpdate, installUpdate, abortUpdate, isNewer, resolveReleaseVersion };

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "3.3.16", "version": "2.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "3.3.16", "version": "2.0.1",
"dependencies": { "dependencies": {
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"undici": "^7.16.0", "undici": "^7.16.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "multi-hoster-uploader", "name": "multi-hoster-uploader",
"version": "3.3.108", "version": "2.0.1",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View File

@ -1,27 +1,53 @@
#!/usr/bin/env node #!/usr/bin/env node
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { createHash } from 'crypto'; 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 { resolve, basename } from 'path';
import { pathToFileURL } from 'url';
const ROOT = resolve(import.meta.dirname, '..'); const ROOT = resolve(import.meta.dirname, '..');
const PKG_PATH = resolve(ROOT, 'package.json');
const RELEASE_DIR = resolve(ROOT, 'release'); const RELEASE_DIR = resolve(ROOT, 'release');
const PRODUCT_NAME = 'Multi-Hoster-Upload'; const PRODUCT_NAME = 'Multi-Hoster-Upload';
// --- CLI args --- // --- CLI args ---
const args = process.argv.slice(2); export function parseReleaseArgs(args) {
const dryRun = args.includes('--dry-run'); const version = Array.isArray(args) ? args[0] : '';
const version = args.find(a => /^\d+\.\d+\.\d+$/.test(a)); if (!/^\d+\.\d+\.\d+$/.test(version || '')) {
const notes = args.filter(a => a !== version && a !== '--dry-run').join(' ') || ''; throw new Error('Usage: node scripts/release_gitea.mjs <version> --transport-tag <vX.Y.Z> [release notes] [--dry-run]');
if (!version) {
console.error('Usage: node scripts/release_gitea.mjs <version> [release notes] [--dry-run]');
console.error('Example: node scripts/release_gitea.mjs 1.0.1 "Bugfix release"');
process.exit(1);
} }
const tag = `v${version}`; 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') };
}
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 --- // --- Helpers ---
function run(cmd, opts = {}) { function run(cmd, opts = {}) {
@ -109,8 +135,11 @@ async function uploadAsset(releaseId, filePath, token) {
} }
// --- Main --- // --- Main ---
async function main() { async function main(args = process.argv.slice(2)) {
console.log(`\nReleasing ${PRODUCT_NAME} ${tag}${dryRun ? ' [DRY RUN]' : ''}\n`); 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 // 1. Resolve remote
const remote = resolveGiteaRemote(); const remote = resolveGiteaRemote();
@ -138,18 +167,19 @@ async function main() {
if (!recoveryMode) { if (!recoveryMode) {
// 4. Update package.json version // 4. Update package.json version
const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf-8')); run(`npm version ${version} --no-git-tag-version --allow-same-version`);
pkg.version = version; console.log(`Updated package.json and package-lock.json -> ${version}`);
if (!dryRun) writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
console.log(`Updated package.json -> ${version}`);
// 5. Build // 5. Build
console.log('\nBuilding...'); console.log('\nBuilding...');
run('npm run release:win', { stdio: 'inherit' }); run('npm run release:win', { stdio: 'inherit' });
// 6. Git commit + tag + push // 6. Git commit + tag + push
run('git add package.json'); 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 commit -m "release: ${tag}"`);
}
run(`git tag ${tag}`); run(`git tag ${tag}`);
run(`git push ${remote.name} HEAD`); run(`git push ${remote.name} HEAD`);
run(`git push ${remote.name} ${tag}`); run(`git push ${remote.name} ${tag}`);
@ -157,8 +187,7 @@ async function main() {
// 6b. Regenerate latest.yml to ensure correct SHA-512 // 6b. Regenerate latest.yml to ensure correct SHA-512
{ {
const setupName = `${PRODUCT_NAME} Setup ${version}.exe`; const setupPath = resolve(RELEASE_DIR, plan.setupName);
const setupPath = resolve(RELEASE_DIR, setupName);
if (existsSync(setupPath)) { if (existsSync(setupPath)) {
const sha = await new Promise((res, rej) => { const sha = await new Promise((res, rej) => {
const h = createHash('sha512'); const h = createHash('sha512');
@ -168,18 +197,14 @@ async function main() {
s.on('error', rej); s.on('error', rej);
}); });
const size = statSync(setupPath).size; 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'); writeFileSync(resolve(RELEASE_DIR, 'latest.yml'), yml, 'utf-8');
console.log('Regenerated latest.yml with correct SHA-512'); console.log('Regenerated latest.yml with correct SHA-512');
} }
} }
// 7. Verify artifacts // 7. Verify artifacts
const expectedArtifacts = [ const expectedArtifacts = plan.expectedArtifacts;
`${PRODUCT_NAME} Setup ${version}.exe`,
`${PRODUCT_NAME} ${version}.exe`,
'latest.yml'
];
for (const name of expectedArtifacts) { for (const name of expectedArtifacts) {
const p = resolve(RELEASE_DIR, name); const p = resolve(RELEASE_DIR, name);
@ -190,7 +215,7 @@ async function main() {
} }
// Also check for blockmap // Also check for blockmap
const blockmapName = `${PRODUCT_NAME} Setup ${version}.exe.blockmap`; const blockmapName = plan.blockmapName;
const hasBlockmap = existsSync(resolve(RELEASE_DIR, blockmapName)); const hasBlockmap = existsSync(resolve(RELEASE_DIR, blockmapName));
console.log('\nArtifacts verified.'); console.log('\nArtifacts verified.');
@ -203,14 +228,13 @@ async function main() {
} }
// 9. Create release // 9. Create release
const releaseBody = notes || `${PRODUCT_NAME} ${tag}`;
let releaseId; let releaseId;
const { status: createStatus, data: createData } = await giteaApi( const { status: createStatus, data: createData } = await giteaApi(
'POST', 'POST',
`/api/v1/repos/Administrator/${PRODUCT_NAME}/releases`, `/api/v1/repos/Administrator/${PRODUCT_NAME}/releases`,
token, token,
{ tag_name: tag, name: `${PRODUCT_NAME} ${tag}`, body: releaseBody } { tag_name: tag, name: plan.releaseTitle, body: plan.releaseBody }
); );
if (createStatus === 409 || createStatus === 422) { 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`); console.log(`\nDone! Release: ${process.env.GITEA_BASE_URL || 'https://git.24-music.de'}/Administrator/${PRODUCT_NAME}/releases/tag/${tag}\n`);
} }
const entryPoint = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : '';
if (entryPoint === import.meta.url) {
main().catch(err => { main().catch(err => {
console.error('\nRelease failed:', err.message); console.error('\nRelease failed:', err.message);
process.exit(1); process.exit(1);
}); });
}

View File

@ -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"
});
});