diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index 3dbd9bf..1de6c2d 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -39,7 +39,8 @@ jobs: libwebkit2gtk-4.1-dev \ libappindicator3-dev \ librsvg2-dev \ - patchelf + patchelf \ + zsync - uses: pnpm/action-setup@v4 with: @@ -71,6 +72,15 @@ jobs: CMAKE_POLICY_VERSION_MINIMUM: 3.5 run: node src/celemod-ui/scripts/tauri.mjs build --no-bundle -- --locked + - name: Configure AppImage delta updates + if: runner.os == 'Linux' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master') + run: | + # Tagged builds follow the latest release, master builds follow the nightly release. + # appimagetool (through linuxdeploy) embeds this string and writes the matching .zsync file. + channel=nightly + if [[ "$GITHUB_REF" == refs/tags/* ]]; then channel=latest; fi + echo "UPDATE_INFORMATION=gh-releases-zsync|$GITHUB_REPOSITORY_OWNER|${GITHUB_REPOSITORY#*/}|$channel|CeleMod_*_amd64.AppImage.zsync" >> "$GITHUB_ENV" + - uses: tauri-apps/tauri-action@v0.6.2 if: runner.os != 'Windows' env: @@ -84,6 +94,17 @@ jobs: releaseDraft: false prerelease: false + - name: Collect AppImage delta update file + if: runner.os == 'Linux' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master') + run: | + # appimagetool writes the .zsync file into the working directory instead of the bundle directory. + mapfile -t zsync_files < <(find . -maxdepth 2 -name '*.AppImage.zsync') + if [[ ${#zsync_files[@]} -eq 0 ]]; then + echo "::error::appimagetool did not produce an AppImage .zsync file" + exit 1 + fi + mv -v "${zsync_files[@]}" target/release/bundle/appimage/ + - name: Upload macOS bundles if: runner.os == 'macOS' uses: actions/upload-artifact@v4 @@ -102,6 +123,20 @@ jobs: target/release/bundle/deb/*.deb if-no-files-found: error + - name: Upload AppImage delta update file + if: runner.os == 'Linux' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master') + uses: actions/upload-artifact@v4 + with: + name: CeleMod-linux-appimage-zsync + path: target/release/bundle/appimage/*.AppImage.zsync + if-no-files-found: error + + - name: Upload AppImage delta update to release + if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload "$GITHUB_REF_NAME" target/release/bundle/appimage/*.AppImage.zsync --clobber + - name: Rename Windows executable if: runner.os == 'Windows' shell: pwsh diff --git a/scripts/publish-nightly.cjs b/scripts/publish-nightly.cjs index f53e51b..551506c 100644 --- a/scripts/publish-nightly.cjs +++ b/scripts/publish-nightly.cjs @@ -14,7 +14,7 @@ module.exports = async function publishNightly({ github, context, core, director // Never hide the previous release unless all platforms produced their assets. const files = collectAssets(directory); const names = files.map(file => path.basename(file)); - for (const suffix of ['.exe', '.dmg', '.AppImage', '.deb', '.apk', '.apk.sha256']) { + for (const suffix of ['.exe', '.dmg', '.AppImage', '.AppImage.zsync', '.deb', '.apk', '.apk.sha256']) { if (!names.some(name => name.endsWith(suffix))) { throw new Error(`Missing nightly asset: ${suffix}`); } diff --git a/scripts/publish-nightly.test.cjs b/scripts/publish-nightly.test.cjs index 17c8ab1..53ea0d1 100644 --- a/scripts/publish-nightly.test.cjs +++ b/scripts/publish-nightly.test.cjs @@ -9,7 +9,9 @@ const publish = require('./publish-nightly.cjs'); function fixture(t, options = {}) { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'celemod-nightly-')); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); - const assets = ['CeleMod.exe', 'CeleMod.dmg', 'CeleMod.AppImage', 'CeleMod.deb', 'CeleMod.apk']; + const assets = [ + 'CeleMod.exe', 'CeleMod.dmg', 'CeleMod.AppImage', 'CeleMod.AppImage.zsync', 'CeleMod.deb', 'CeleMod.apk', + ]; for (const name of assets) fs.writeFileSync(path.join(directory, name), name); const hash = crypto.createHash('sha256').update('CeleMod.apk').digest('hex'); fs.writeFileSync(path.join(directory, 'CeleMod.apk.sha256'), `${hash} CeleMod.apk\n`); @@ -53,7 +55,7 @@ function fixture(t, options = {}) { test('creates a prerelease and tag only after uploading all platforms', async t => { const f = fixture(t, { newRelease: true, newTag: true }); await publish(f); - assert.equal(f.calls.filter(c => c.name === 'uploadReleaseAsset').length, 6); + assert.equal(f.calls.filter(c => c.name === 'uploadReleaseAsset').length, 7); const created = f.calls.find(c => c.name === 'createRelease').args; assert.equal(created.draft, true); assert.equal(created.prerelease, true); @@ -69,7 +71,7 @@ test('replaces all old assets and moves only the nightly tag', async t => { await publish(f); assert.equal(f.calls.find(c => c.name === 'updateRelease').args.draft, true); assert.deepEqual(f.calls.filter(c => c.name === 'deleteReleaseAsset').map(c => c.args.asset_id), [11, 12]); - assert.equal(f.calls.filter(c => c.name === 'uploadReleaseAsset').length, 6); + assert.equal(f.calls.filter(c => c.name === 'uploadReleaseAsset').length, 7); const moved = f.calls.find(c => c.name === 'updateRef').args; assert.equal(moved.ref, 'tags/nightly'); assert.equal(moved.sha, 'commit'); @@ -91,6 +93,13 @@ test('missing platform assets leave the previous release untouched', async t => assert.equal(f.calls.length, 0); }); +test('missing AppImage delta update leaves the previous release untouched', async t => { + const f = fixture(t); + fs.unlinkSync(path.join(f.directory, 'CeleMod.AppImage.zsync')); + await assert.rejects(publish(f), /Missing nightly asset: .AppImage.zsync/); + assert.equal(f.calls.length, 0); +}); + test('duplicate basenames from nested artifacts are rejected', async t => { const f = fixture(t); fs.mkdirSync(path.join(f.directory, 'nested'));