diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c738c7287..411d8e01c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/crates/dbx-core/src/update.rs b/crates/dbx-core/src/update.rs index 49ce0e18b..b93c5aef6 100644 --- a/crates/dbx-core/src/update.rs +++ b/crates/dbx-core/src/update.rs @@ -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, + #[serde(skip)] + pub github: Option, +} + +#[derive(Debug, Deserialize)] +pub struct GithubReleaseMetadata { + pub name: Option, + pub html_url: Option, + pub body: Option, } #[derive(Debug, Serialize)] @@ -39,7 +49,12 @@ pub async fn fetch_latest_release() -> Result { .and_then(|r| r.error_for_status()) { Ok(resp) => { - return resp.json::().await.map_err(|e| format!("Failed to parse update response: {e}")); + let mut release = + resp.json::().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 { Err(format!("Failed to check updates: {last_err}")) } +async fn fetch_github_release_metadata( + client: &reqwest::Client, + version: &str, +) -> Result { + 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::() + .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真实发布说明"); + } } diff --git a/src/App.vue b/src/App.vue index 88e866edd..bf8558af8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -701,6 +701,7 @@ onUnmounted(() => { (); 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(
DBX

{{ t("settings.aboutDescription") }}

-
v0.5.0
+
+ {{ displayedAppVersion }} +
diff --git a/src/components/layout/AppDialogs.vue b/src/components/layout/AppDialogs.vue index b1cee91a9..0061e9d12 100644 --- a/src/components/layout/AppDialogs.vue +++ b/src/components/layout/AppDialogs.vue @@ -25,6 +25,7 @@ const props = defineProps<{ showConnectionDialog: boolean; showSettingsDialog: boolean; settingsInitialTab?: string; + appVersion?: string; showDangerDialog: boolean; dangerSql: string; }>(); @@ -102,6 +103,7 @@ watch( { + 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); +});