2026-01-30 11:50:20 +08:00
import { execSync } from 'child_process' ;
import path from 'path' ;
import { fileURLToPath } from 'url' ;
import { readFileSync , readdirSync } from 'fs' ;
import fetch from 'node-fetch' ;
2025-06-03 17:51:55 +02:00
2026-01-30 11:50:20 +08:00
const dirname = path . join ( path . dirname ( fileURLToPath ( import . meta . url )));
const conventionalChangelog = path . join ( dirname , '..' , 'node_modules' , '.bin' , 'conventional-changelog' );
const version = JSON . parse ( readFileSync ( path . join ( dirname , '..' , 'package.json' ), 'utf8' )). version ;
2023-09-18 00:08:27 +00:00
2026-01-30 11:50:20 +08:00
// changelog (use last non-empty)
let releaseChangelog =
execSync ( ` ${ conventionalChangelog } --preset angular --release-count 1` ). toString () ||
execSync ( ` ${ conventionalChangelog } --preset angular --release-count 2` ). toString ();
releaseChangelog = releaseChangelog . trim (). replace ( /\n\n+/g , '\n\n' );
2023-09-18 00:08:27 +00:00
2026-01-30 11:50:20 +08:00
// discover artifacts
const distDir = path . join ( dirname , '..' , 'dist' );
let files = [];
try {
files = readdirSync ( distDir );
} catch {}
// In CI finalization, dist is empty. Prefer GitHub release assets when available.
const getReleaseAssetNames = async () => {
try {
const token = process . env . GITHUB_TOKEN ;
const repo = process . env . GITHUB_REPOSITORY ; // owner/repo
const tag = process . env . GITHUB_REF_NAME || `v ${ version } ` ;
if ( ! token || ! repo || ! tag ) return [];
const res = await fetch ( `https://api.github.com/repos/ ${ repo } /releases/tags/ ${ tag } ` , {
headers : {
Authorization : `Bearer ${ token } ` ,
'User-Agent' : '5chan-release-notes' ,
'X-GitHub-Api-Version' : '2022-11-28' ,
Accept : 'application/vnd.github+json' ,
},
});
if ( ! res . ok ) return [];
const data = await res . json ();
return ( data . assets || []). map (( a ) => a . name ). filter ( Boolean );
} catch ( e ) {
return [];
}
};
const remoteFiles = await getReleaseAssetNames ();
if ( remoteFiles . length ) {
files = remoteFiles ;
}
2026-02-11 21:27:18 +08:00
const linkTo = ( file ) => `https://github.com/bitsocialhq/5chan/releases/download/v ${ version } / ${ file } ` ;
2026-01-30 11:50:20 +08:00
const has = ( s , sub ) => s . toLowerCase (). includes ( sub );
const isArm = ( s ) => has ( s , 'arm64' ) || has ( s , 'aarch64' );
const isX64 = ( s ) => has ( s , 'x64' ) || has ( s , 'x86_64' ) || ! isArm ( s );
// buckets
const androidApk = files . find (( f ) => f . endsWith ( '.apk' ));
const htmlZip = files . find (( f ) => f . includes ( '5chan-html' ) && f . endsWith ( '.zip' ));
const linux = files . filter (( f ) => f . endsWith ( '.AppImage' ));
const mac = files . filter (( f ) => f . endsWith ( '.dmg' ));
const win = files . filter (( f ) => f . toLowerCase (). endsWith ( '.exe' ));
const linuxArm = linux . find ( isArm );
const linuxX64 = linux . find ( isX64 );
const macArm = mac . find ( isArm );
const macX64 = mac . find ( isX64 );
const winSetupX64 = win . find (( f ) => has ( f , 'setup' ) && isX64 ( f ));
const winPortableX64 = win . find (( f ) => has ( f , 'portable' ) && isX64 ( f ));
// small section builder without push()
const section = ( title , lines ) => {
const body = lines . filter ( Boolean ). join ( '\n' );
return body ? `### ${ title } \n ${ body } ` : '' ;
};
const macSection = section ( 'macOS' , [
macArm && `- Apple Silicon (arm64): [Download DMG]( ${ linkTo ( macArm ) } )` ,
macX64 && `- Intel (x64): [Download DMG]( ${ linkTo ( macX64 ) } )` ,
mac . length > 0 &&
`- If macOS shows "5chan.app is damaged and can't be opened. You should move it to the Trash.", run this in Terminal to fix it: \`xattr -dr com.apple.quarantine "/Applications/5chan.app"\` then open the app again.` ,
]);
const winSection = section ( 'Windows' , [
winSetupX64 && `- Installer (x64): [Download EXE]( ${ linkTo ( winSetupX64 ) } )` ,
winPortableX64 && `- Portable (x64): [Download EXE]( ${ linkTo ( winPortableX64 ) } )` ,
win . length > 0 &&
`- If Windows shows "Windows protected your PC" (SmartScreen), click "More info" then "Run anyway". To permanently allow, right-click the .exe → Properties → check "Unblock", or run in PowerShell: \`Unblock-File -Path .\\path\\to\\file.exe\`.` ,
]);
const linuxSection = section ( 'Linux' , [
linuxArm && `- AppImage (arm64): [Download]( ${ linkTo ( linuxArm ) } )` ,
linuxX64 && `- AppImage (x64): [Download]( ${ linkTo ( linuxX64 ) } )` ,
]);
const androidSection = section ( 'Android' , [ androidApk && `- APK: [Download]( ${ linkTo ( androidApk ) } )` ]);
const htmlSection = section ( 'Static HTML build' , [ htmlZip && `- 5chan-html (zip): [Download]( ${ linkTo ( htmlZip ) } )` ]);
const downloads = [ macSection , winSection , linuxSection , androidSection , htmlSection ]. filter ( Boolean ). join ( '\n\n' );
2023-09-18 00:08:27 +00:00
2026-02-10 14:01:44 +08:00
// One-liner summary of what changed in this release. Update before each release.
2026-02-16 19:20:53 +08:00
const oneLinerDescription =
'This version syncs board directories from GitHub on start and build, adds the macOS app icon, improves reply rendering performance with progressive loading, enforces mod queue role access, adds board address and vote tooltips, and includes several bug fixes.' ;
2026-02-10 14:01:44 +08:00
const releaseBody = ` ${ oneLinerDescription }
2025-06-03 17:51:55 +02:00
2025-10-17 23:30:35 +02:00
- Web app: https://5chan.app
2026-01-30 11:50:20 +08:00
- Decentralized web app via IPFS/IPNS gateways (works on any browser): [5chan.eth.limo](https://5chan.eth.limo), [5chan.eth.link](https://5chan.eth.link), [dweb.link/ipfs.io](https://dweb.link/ipns/5chan.eth)
- Decentralized web app via IPFS node you run: https://5chan.eth (works on [Brave Browser](https://brave.com/) or use [IPFS Companion](https://docs.ipfs.tech/install/ipfs-companion/#prerequisites))
2025-06-03 17:51:55 +02:00
## Downloads
2026-01-30 11:50:20 +08:00
${ downloads }
2023-09-18 00:08:27 +00:00
2026-01-30 11:50:20 +08:00
## Changes
2023-09-18 00:08:27 +00:00
2026-01-30 11:50:20 +08:00
${ releaseChangelog } ` ;
console . log ( releaseBody );