feat(updater): bridge 3.3.108 clients to v2.0.1
This commit is contained in:
parent
95bff2581b
commit
c7fa422d9b
@ -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
4
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|||||||
61
tests/updater-version.test.js
Normal file
61
tests/updater-version.test.js
Normal 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"
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user