fix(updater): show real app version and release notes

This commit is contained in:
t8y2 2026-05-12 15:15:46 +08:00
parent 724813c72a
commit 01c4b744ef
6 changed files with 128 additions and 7 deletions

View File

@ -113,6 +113,25 @@ jobs:
echo "TAURI_SIGNING_PRIVATE_KEY_PASSWORD=${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}" >> "$GITHUB_ENV"
fi
- name: Generate release notes
id: release-notes
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BODY="$(gh api "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \
-f tag_name="${GITHUB_REF_NAME}" \
--jq '.body')"
if [ -z "$BODY" ]; then
BODY="DBX ${GITHUB_REF_NAME}"
fi
DELIM="RELEASE_NOTES_$(date +%s%N)"
{
echo "body<<$DELIM"
printf '%s\n' "$BODY"
echo "$DELIM"
} >> "$GITHUB_OUTPUT"
- name: Build Tauri app
uses: tauri-apps/tauri-action@v0
env:
@ -124,7 +143,7 @@ jobs:
with:
tagName: ${{ github.ref_name }}
releaseName: 'DBX ${{ github.ref_name }}'
releaseBody: 'See the assets below to download and install.'
releaseBody: ${{ steps.release-notes.outputs.body }}
releaseDraft: true
prerelease: false
args: --target ${{ matrix.target }}

View File

@ -5,6 +5,7 @@ const LATEST_JSON_URLS: &[&str] = &[
"https://gh-proxy.org/https://github.com/t8y2/dbx/releases/latest/download/latest.json",
"https://github.com/t8y2/dbx/releases/latest/download/latest.json",
];
const GITHUB_RELEASE_API_PREFIX: &str = "https://api.github.com/repos/t8y2/dbx/releases/tags/v";
const RELEASE_URL_PREFIX: &str = "https://github.com/t8y2/dbx/releases/tag/v";
#[derive(Debug, Deserialize)]
@ -12,6 +13,15 @@ pub struct TauriRelease {
pub version: String,
#[serde(default)]
pub notes: Option<String>,
#[serde(skip)]
pub github: Option<GithubReleaseMetadata>,
}
#[derive(Debug, Deserialize)]
pub struct GithubReleaseMetadata {
pub name: Option<String>,
pub html_url: Option<String>,
pub body: Option<String>,
}
#[derive(Debug, Serialize)]
@ -39,7 +49,12 @@ pub async fn fetch_latest_release() -> Result<TauriRelease, String> {
.and_then(|r| r.error_for_status())
{
Ok(resp) => {
return resp.json::<TauriRelease>().await.map_err(|e| format!("Failed to parse update response: {e}"));
let mut release =
resp.json::<TauriRelease>().await.map_err(|e| format!("Failed to parse update response: {e}"))?;
if let Ok(github) = fetch_github_release_metadata(&client, &release.version).await {
release.github = Some(github);
}
return Ok(release);
}
Err(e) => {
last_err = format!("{e}");
@ -49,19 +64,61 @@ pub async fn fetch_latest_release() -> Result<TauriRelease, String> {
Err(format!("Failed to check updates: {last_err}"))
}
async fn fetch_github_release_metadata(
client: &reqwest::Client,
version: &str,
) -> Result<GithubReleaseMetadata, String> {
let url = format!("{GITHUB_RELEASE_API_PREFIX}{}", normalize_version(version));
client
.get(url)
.header(reqwest::header::USER_AGENT, "dbx-update-checker")
.send()
.await
.and_then(|r| r.error_for_status())
.map_err(|e| format!("{e}"))?
.json::<GithubReleaseMetadata>()
.await
.map_err(|e| format!("Failed to parse GitHub release response: {e}"))
}
pub fn build_update_info(release: TauriRelease, current_version: &str) -> UpdateInfo {
let latest_version = normalize_version(&release.version);
let github = release.github.as_ref();
let release_notes = github
.and_then(|metadata| non_empty(metadata.body.as_deref()))
.map(ToOwned::to_owned)
.or(release.notes)
.unwrap_or_default();
let release_name = github
.and_then(|metadata| non_empty(metadata.name.as_deref()))
.map(ToOwned::to_owned)
.unwrap_or_else(|| format!("DBX v{latest_version}"));
let release_url = github
.and_then(|metadata| non_empty(metadata.html_url.as_deref()))
.map(ToOwned::to_owned)
.unwrap_or_else(|| format!("{RELEASE_URL_PREFIX}{latest_version}"));
UpdateInfo {
update_available: is_newer_version(&latest_version, current_version),
current_version: current_version.to_string(),
release_name: format!("DBX v{latest_version}"),
release_url: format!("{RELEASE_URL_PREFIX}{latest_version}"),
release_notes: release.notes.unwrap_or_default(),
release_name,
release_url,
release_notes,
latest_version,
}
}
fn non_empty(value: Option<&str>) -> Option<&str> {
value.and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(value)
}
})
}
pub fn normalize_version(version: &str) -> String {
version.trim().trim_start_matches('v').to_string()
}
@ -91,7 +148,7 @@ pub fn is_newer_version(latest: &str, current: &str) -> bool {
#[cfg(test)]
mod tests {
use super::{is_newer_version, normalize_version};
use super::{build_update_info, is_newer_version, normalize_version, GithubReleaseMetadata, TauriRelease};
#[test]
fn normalizes_tag_versions() {
@ -106,4 +163,23 @@ mod tests {
assert!(!is_newer_version("0.2.0", "0.2.0"));
assert!(!is_newer_version("0.1.9", "0.2.0"));
}
#[test]
fn update_info_prefers_github_release_metadata() {
let release = TauriRelease {
version: "0.5.3".to_string(),
notes: Some("See the assets below to download and install.".to_string()),
github: Some(GithubReleaseMetadata {
name: Some("DBX v0.5.3".to_string()),
html_url: Some("https://github.com/t8y2/dbx/releases/tag/v0.5.3".to_string()),
body: Some("### 新功能\n\n真实发布说明".to_string()),
}),
};
let info = build_update_info(release, "0.5.2");
assert_eq!(info.release_name, "DBX v0.5.3");
assert_eq!(info.release_url, "https://github.com/t8y2/dbx/releases/tag/v0.5.3");
assert_eq!(info.release_notes, "### 新功能\n\n真实发布说明");
}
}

View File

@ -701,6 +701,7 @@ onUnmounted(() => {
<AppDialogs
:show-connection-dialog="showConnectionDialog"
:show-settings-dialog="showSettingsDialog"
:app-version="appVersion"
:show-danger-dialog="showDangerDialog"
:danger-sql="dangerSql"
@update:show-connection-dialog="showConnectionDialog = $event"

View File

@ -34,6 +34,7 @@ const { toast } = useToast();
const props = defineProps<{
open: boolean;
initialTab?: string;
appVersion?: string;
}>();
const emit = defineEmits<{
@ -113,6 +114,7 @@ function setAppLayout(value: "separated" | "classic") {
const activeSettingsTab = ref("editor");
const isWeb = !isTauriRuntime();
const displayedAppVersion = computed(() => (props.appVersion ? `v${props.appVersion}` : ""));
function openExternalUrl(url: string) {
if (isTauriRuntime()) {
@ -933,7 +935,12 @@ watch(
<div class="text-lg font-semibold">DBX</div>
<p class="text-sm text-muted-foreground">{{ t("settings.aboutDescription") }}</p>
</div>
<div class="rounded-md border bg-background px-2 py-1 text-xs text-muted-foreground">v0.5.0</div>
<div
v-if="displayedAppVersion"
class="rounded-md border bg-background px-2 py-1 text-xs text-muted-foreground"
>
{{ displayedAppVersion }}
</div>
</div>
</div>

View File

@ -25,6 +25,7 @@ const props = defineProps<{
showConnectionDialog: boolean;
showSettingsDialog: boolean;
settingsInitialTab?: string;
appVersion?: string;
showDangerDialog: boolean;
dangerSql: string;
}>();
@ -102,6 +103,7 @@ watch(
<EditorSettingsDialog
:open="showSettingsDialog"
:initial-tab="settingsInitialTab || 'editor'"
:app-version="appVersion"
@update:open="emit('update:showSettingsDialog', $event)"
/>
<DangerConfirmDialog

View File

@ -0,0 +1,16 @@
import { readFileSync } from "node:fs";
import { strict as assert } from "node:assert";
import test from "node:test";
test("settings about panel uses the app version prop instead of a hard-coded version", () => {
const source = readFileSync("src/components/editor/EditorSettingsDialog.vue", "utf8");
assert.equal(source.includes("v0.5.0"), false);
assert.match(source, /appVersion/);
});
test("release workflow does not publish the default Tauri release body", () => {
const source = readFileSync(".github/workflows/release.yml", "utf8");
assert.equal(source.includes("See the assets below to download and install."), false);
});