Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 131 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,16 +253,47 @@ jobs:
- name: Package app bundle with standardized name (unix)
if: runner.os != 'Windows'
shell: bash
env:
MATRIX_PLATFORM: ${{ matrix.platform }}
MATRIX_TARGET: ${{ matrix.target }}
GITHUB_REF: ${{ github.ref }}
GITHUB_REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
IFS='/' read -r os arch <<< "${{ matrix.platform }}"
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
version="${{ github.ref_name }}"; version="${version#v}"
IFS='/' read -r os arch <<< "$MATRIX_PLATFORM"
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
version="$GITHUB_REF_NAME"; version="${version#v}"
else
version=$(grep -m1 '^version' src-tauri/Cargo.toml | sed 's/.*"\(.*\)"/\1/')
fi
bundle_dir="src-tauri/target/${{ matrix.target }}/release/bundle"
if [[ "$os" == "darwin" ]]; then
bundle_dir="src-tauri/target/$MATRIX_TARGET/release/bundle"
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
# Serve tauri's exact updater artifact so its minisign .sig stays valid.
# Renaming is fine — minisign signs file *contents*, not the filename.
if [[ "$os" == "darwin" ]]; then
sig=$(find "$bundle_dir" -name '*.app.tar.gz.sig' -type f | head -1)
elif [[ "$os" == "linux" ]]; then
sig=$(find "$bundle_dir" -name '*.AppImage.tar.gz.sig' -type f | head -1)
else
sig=$(find "$bundle_dir" -name '*.sig' -type f | head -1)
fi
if [[ -z "$sig" ]]; then
echo "::error::No updater .sig under $bundle_dir. Is TAURI_SIGNING_PRIVATE_KEY set?"
find "$bundle_dir" -maxdepth 3 -type f | sed 's/^/found: /' || true
exit 1
fi
signed="${sig%.sig}"
case "$(basename "$signed")" in
*.app.tar.gz) ext="app.tar.gz" ;;
*.AppImage.tar.gz) ext="AppImage.tar.gz" ;;
*.AppImage) ext="AppImage" ;;
*) ext="${signed##*.}" ;;
esac
asset="Risuko_${version}_${os}_${arch}.${ext}"
cp "$signed" "$asset"
cp "$sig" "$asset.sig"
echo "UPDATER_SIG_FILE=$asset.sig" >> "$GITHUB_ENV"
elif [[ "$os" == "darwin" ]]; then
asset="Risuko_${version}_${os}_${arch}.app.tar.gz"
tar czf "$asset" -C "$bundle_dir/macos" "Risuko.app"
else
Expand All @@ -276,18 +307,37 @@ jobs:
- name: Package app bundle with standardized name (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
MATRIX_PLATFORM: ${{ matrix.platform }}
MATRIX_TARGET: ${{ matrix.target }}
GITHUB_REF: ${{ github.ref }}
GITHUB_REF_NAME: ${{ github.ref_name }}
run: |
$parts = "${{ matrix.platform }}" -split "/"
$parts = $env:MATRIX_PLATFORM -split "/"
$os = $parts[0]; $arch = $parts[1]
if ("${{ github.ref }}" -match "^refs/tags/") {
$version = "${{ github.ref_name }}" -replace "^v", ""
if ($env:GITHUB_REF -match "^refs/tags/") {
$version = $env:GITHUB_REF_NAME -replace "^v", ""
} else {
$cargo = Get-Content "src-tauri/Cargo.toml" -Raw
$version = [regex]::Match($cargo, '^version\s*=\s*"([^"]+)"', [System.Text.RegularExpressions.RegexOptions]::Multiline).Groups[1].Value
}
$setup = Get-ChildItem "src-tauri/target/${{ matrix.target }}/release/bundle/nsis" -Filter "*-setup.exe" | Select-Object -First 1
$nsis = "src-tauri/target/$env:MATRIX_TARGET/release/bundle/nsis"
$asset = "Risuko_${version}_${os}_${arch}.setup.exe"
Copy-Item $setup.FullName $asset
if ($env:GITHUB_REF -match "^refs/tags/") {
# Serve tauri's exact signed installer so its minisign .sig stays valid.
$sig = Get-ChildItem $nsis -Filter "*-setup.exe.sig" | Select-Object -First 1
if (-not $sig) {
Write-Error "No updater .sig under $nsis (is TAURI_SIGNING_PRIVATE_KEY set?)"
exit 1
}
$signed = $sig.FullName -replace '\.sig$', ''
Copy-Item $signed $asset
Copy-Item $sig.FullName "$asset.sig"
"UPDATER_SIG_FILE=$asset.sig" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
} else {
$setup = Get-ChildItem $nsis -Filter "*-setup.exe" | Select-Object -First 1
Copy-Item $setup.FullName $asset
}
"APP_ASSET=$asset" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
"APP_PLATFORM_LABEL=${os}-${arch}" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8

Expand All @@ -297,6 +347,38 @@ jobs:
set -euo pipefail
node scripts/write-sha256-sidecar.mjs "$APP_ASSET"

# Record this platform's updater entry (key + asset + signature) for the
# aggregation job that assembles latest.json across all matrix jobs.
- name: Emit updater manifest fragment
if: startsWith(github.ref, 'refs/tags/')
shell: bash
env:
MATRIX_TARGET: ${{ matrix.target }}
APP_ASSET: ${{ env.APP_ASSET }}
UPDATER_SIG_FILE: ${{ env.UPDATER_SIG_FILE }}
run: |
set -euo pipefail
triple="$MATRIX_TARGET"
arch="${triple%%-*}" # x86_64 | aarch64 | i686
case "$triple" in
*apple-darwin) tos=darwin ;;
*windows*) tos=windows ;;
*linux*) tos=linux ;;
*) echo "::error::unknown target triple $triple"; exit 1 ;;
esac
key="${tos}-${arch}"
signature=$(tr -d '\n' < "$UPDATER_SIG_FILE")
mkdir -p fragments
printf '{"key":"%s","asset":"%s","signature":"%s"}' "$key" "$APP_ASSET" "$signature" > "fragments/$key.json"
echo "fragment: fragments/${key}.json -> $APP_ASSET"

- name: Upload updater fragment
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: updater-fragment-${{ matrix.target }}
path: fragments/*.json

- name: Upload standardized app bundle to GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
Expand All @@ -305,6 +387,7 @@ jobs:
files: |
${{ env.APP_ASSET }}
${{ env.APP_ASSET }}.sha256
${{ env.APP_ASSET }}.sig
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Expand Down Expand Up @@ -440,3 +523,41 @@ jobs:
else
echo "DMG directory not found: $dmg_dir"
fi

updater-manifest:
name: Build updater latest.json
needs: release
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 24

- name: Download updater fragments
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
path: fragments
pattern: updater-fragment-*
merge-multiple: true

- name: Build latest.json
shell: bash
run: node scripts/build-updater-manifest.mjs fragments latest.json
env:
VERSION: ${{ github.ref_name }}
REPO: ${{ github.repository }}

- name: Upload latest.json to GitHub Release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
with:
tag_name: ${{ github.ref_name }}
files: latest.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
4 changes: 2 additions & 2 deletions README-CN.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Risuko

<p>
<a href="https://risuko.vercel.app">
<a href="https://risuko.app">
<img src="./static/logo.svg" width="256" alt="Risuko App Icon" />
</a>
</p>
Expand All @@ -17,7 +17,7 @@

Risuko 是一款全能的下载工具,支持下载 HTTP、FTP、BT、磁力链等资源。它的界面简洁易用,希望大家喜欢 👻。

✈️ 去 [官网](https://risuko.vercel.app) 逛逛
✈️ 去 [官网](https://risuko.app) 逛逛

## 💽 安装

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Risuko

<p>
<a href="https://risuko.vercel.app">
<a href="https://risuko.app">
<img src="./static/logo.svg" width="256" alt="Risuko App Icon" />
</a>
</p>
Expand All @@ -19,7 +19,7 @@ Risuko is a full-featured download manager that supports downloading HTTP, FTP,

Risuko has a clean and easy to use interface. I hope you will like it 👻.

✈️ [Official Website](https://risuko.vercel.app)
✈️ [Official Website](https://risuko.app)

## 💽 Installation

Expand Down
43 changes: 43 additions & 0 deletions scripts/build-updater-manifest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env node


import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";

export function buildManifest(dir, { version, repo } = {}) {
version = String(version ?? "").replace(/^v/i, "");
if (!version) throw new Error("version required");
if (!repo) throw new Error("repo (owner/repo) required");
const base = `https://github.com/${repo}/releases/download/v${version}`;
const platforms = {};
for (const file of readdirSync(dir)) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
if (!file.endsWith(".json")) continue;
const frag = JSON.parse(readFileSync(join(dir, file), "utf8"));
if (!frag.key || !frag.asset || !frag.signature) {
throw new Error(`incomplete fragment: ${file}`);
}
if (platforms[frag.key]) {
throw new Error(`duplicate fragment key: ${frag.key}`);
}
platforms[frag.key] = { signature: frag.signature, url: `${base}/${frag.asset}` };
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if (Object.keys(platforms).length === 0) throw new Error(`no fragments in ${dir}`);

return { version, notes: "", pub_date: new Date().toISOString(), platforms };
}

// CLI: node scripts/build-updater-manifest.mjs <fragments-dir> [out]
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
const [dir, out = "latest.json"] = process.argv.slice(2);
if (!dir) {
console.error("Usage: build-updater-manifest.mjs <fragments-dir> [out]");
process.exit(2);
}
const manifest = buildManifest(dir, {
version: process.env.VERSION,
repo: process.env.REPO,
});
writeFileSync(out, `${JSON.stringify(manifest, null, 2)}\n`);
console.log(`wrote ${out}: ${Object.keys(manifest.platforms).join(", ")}`);
}
85 changes: 85 additions & 0 deletions scripts/build-updater-manifest.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildManifest } from "./build-updater-manifest.mjs";

const dir = mkdtempSync(join(tmpdir(), "frag-"));

try {
writeFileSync(
join(dir, "darwin-aarch64.json"),
JSON.stringify({
key: "darwin-aarch64",
asset: "Risuko_1.2.3_darwin_arm64.app.tar.gz",
signature: "SIG_A",
}),
);
writeFileSync(
join(dir, "windows-x86_64.json"),
JSON.stringify({
key: "windows-x86_64",
asset: "Risuko_1.2.3_win32_x64.setup.exe",
signature: "SIG_W",
}),
);

const m = buildManifest(dir, {
version: "v1.2.3",
repo: "YueMiyuki/Risuko",
});

assert.equal(m.version, "1.2.3");
assert.match(m.pub_date, /^\d{4}-\d{2}-\d{2}T/);
assert.equal(m.platforms["darwin-aarch64"].signature, "SIG_A");
assert.equal(m.platforms["windows-x86_64"].signature, "SIG_W");
assert.equal(
m.platforms["darwin-aarch64"].url,
"https://github.com/YueMiyuki/Risuko/releases/download/v1.2.3/Risuko_1.2.3_darwin_arm64.app.tar.gz",
);
assert.equal(
m.platforms["windows-x86_64"].url,
"https://github.com/YueMiyuki/Risuko/releases/download/v1.2.3/Risuko_1.2.3_win32_x64.setup.exe",
);
assert.throws(() => buildManifest(dir, { repo: "x/y" }), /version required/);
assert.throws(() => buildManifest(dir, { version: "1.0.0" }), /repo .* required/);

const emptyDir = mkdtempSync(join(tmpdir(), "frag-empty-"));
try {
assert.throws(() => buildManifest(emptyDir, { version: "1.0.0", repo: "x/y" }), /no fragments/);
} finally {
rmSync(emptyDir, { recursive: true, force: true });
}

const badDir = mkdtempSync(join(tmpdir(), "frag-bad-"));
try {
writeFileSync(join(badDir, "bad.json"), JSON.stringify({ key: "linux-x86_64", asset: "a" }));
assert.throws(
() => buildManifest(badDir, { version: "1.0.0", repo: "x/y" }),
/incomplete fragment/,
);
} finally {
rmSync(badDir, { recursive: true, force: true });
}

const dupDir = mkdtempSync(join(tmpdir(), "frag-dup-"));
try {
const frag = JSON.stringify({
key: "linux-x86_64",
asset: "a.AppImage.tar.gz",
signature: "SIG",
});
writeFileSync(join(dupDir, "a.json"), frag);
writeFileSync(join(dupDir, "b.json"), frag);
assert.throws(
() => buildManifest(dupDir, { version: "1.0.0", repo: "x/y" }),
/duplicate fragment key/,
);
} finally {
rmSync(dupDir, { recursive: true, force: true });
}

console.log("ok");
} finally {
rmSync(dir, { recursive: true, force: true });
}
Loading
Loading