From 562edef4dc7204eff5a94ba6e2c2a4e64a278b76 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 24 Jul 2026 09:55:48 +0800 Subject: [PATCH] fix(changelog): filter non-app releases --- .github/scripts/sync-changelog.mjs | 10 ++++-- .github/scripts/sync-changelog.test.mjs | 37 ++++++++++++++++++- crates/dbx-core/src/changelog.rs | 48 +++++++++++++++++++++++-- docs/lib/changelog.test.ts | 12 +++++++ docs/lib/changelog.ts | 4 ++- docs/lib/releaseTags.ts | 5 +++ 6 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 docs/lib/changelog.test.ts create mode 100644 docs/lib/releaseTags.ts diff --git a/.github/scripts/sync-changelog.mjs b/.github/scripts/sync-changelog.mjs index 28e1918d15..6d1616f9bb 100644 --- a/.github/scripts/sync-changelog.mjs +++ b/.github/scripts/sync-changelog.mjs @@ -13,6 +13,7 @@ const OUT_EN = "releases-en.json"; const LATEST_EN_OUT = "latest-en.json"; const LATEST_NOTES_OUT = "latest-notes.json"; const EN_CACHE_URL = process.env.CHANGELOG_EN_CACHE_URL || "https://dl.dbxio.com/changelog/releases-en.json"; +const APP_RELEASE_TAG_PATTERN = /^v[0-9]+[.][0-9]+[.][0-9]+(?:[.-][0-9A-Za-z.-]+)?$/; const SECTION_MAP = { 新功能: "added", @@ -121,11 +122,16 @@ export function buildReleaseSourceHash(release) { .digest("hex"); } +export function isAppRelease(release) { + return !release.draft && !release.prerelease && APP_RELEASE_TAG_PATTERN.test(release.tag_name || ""); +} + export function buildReleasesJson(releases, now = new Date()) { return { updatedAt: now.toISOString(), releases: releases - .filter((r) => !r.draft && !r.prerelease && !r.tag_name.startsWith("agents-")) + // This repository has independent app, agent, and package release streams. + .filter(isAppRelease) .sort((a, b) => new Date(b.published_at) - new Date(a.published_at)) .map((r) => ({ tag: r.tag_name, @@ -159,7 +165,7 @@ function buildLatestEnNotes(enReleasesJson) { export function buildLatestReleaseNotes(releases) { const latest = releases - .filter((release) => !release.draft && !release.prerelease && !release.tag_name.startsWith("agents-")) + .filter(isAppRelease) .sort((a, b) => new Date(b.published_at) - new Date(a.published_at))[0]; if (!latest) return null; return { version: latest.tag_name, notes: latest.body || "" }; diff --git a/.github/scripts/sync-changelog.test.mjs b/.github/scripts/sync-changelog.test.mjs index c279388394..9d327cb8c3 100644 --- a/.github/scripts/sync-changelog.test.mjs +++ b/.github/scripts/sync-changelog.test.mjs @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { buildLatestReleaseNotes } from "./sync-changelog.mjs"; +import { buildLatestReleaseNotes, buildReleasesJson } from "./sync-changelog.mjs"; test("buildLatestReleaseNotes returns the curated latest release body", () => { const result = buildLatestReleaseNotes([ @@ -26,3 +26,38 @@ test("buildLatestReleaseNotes returns the curated latest release body", () => { notes: "### 新功能\n- new\n\n### 下载安装\n- assets", }); }); + +test("changelog excludes agent and package release streams", () => { + const releases = [ + { + tag_name: "packages-v0.4.42", + draft: false, + prerelease: false, + published_at: "2026-07-23T02:00:00Z", + body: "### Changed\n- packages", + }, + { + tag_name: "agents-v0.2.64", + draft: false, + prerelease: false, + published_at: "2026-07-23T01:00:00Z", + body: "### Changed\n- agents", + }, + { + tag_name: "v0.5.66", + draft: false, + prerelease: false, + published_at: "2026-07-23T00:00:00Z", + body: "### Changed\n- app", + }, + ]; + + assert.deepEqual( + buildReleasesJson(releases, new Date("2026-07-24T00:00:00Z")).releases.map((release) => release.tag), + ["v0.5.66"], + ); + assert.deepEqual(buildLatestReleaseNotes(releases), { + version: "v0.5.66", + notes: "### Changed\n- app", + }); +}); diff --git a/crates/dbx-core/src/changelog.rs b/crates/dbx-core/src/changelog.rs index 6fc108367a..60f22dba09 100644 --- a/crates/dbx-core/src/changelog.rs +++ b/crates/dbx-core/src/changelog.rs @@ -54,6 +54,39 @@ pub fn normalize_changelog_lang(lang: &str) -> &'static str { } } +fn is_app_release_tag(tag: &str) -> bool { + let Some(version) = tag.strip_prefix('v') else { + return false; + }; + let mut parts = version.splitn(3, '.'); + let Some(major) = parts.next() else { + return false; + }; + let Some(minor) = parts.next() else { + return false; + }; + let Some(patch_and_suffix) = parts.next() else { + return false; + }; + if major.is_empty() + || minor.is_empty() + || !major.bytes().all(|byte| byte.is_ascii_digit()) + || !minor.bytes().all(|byte| byte.is_ascii_digit()) + { + return false; + } + + let patch_len = patch_and_suffix.bytes().take_while(u8::is_ascii_digit).count(); + if patch_len == 0 { + return false; + } + let suffix = &patch_and_suffix[patch_len..]; + suffix.is_empty() + || ((suffix.starts_with('.') || suffix.starts_with('-')) + && suffix.len() > 1 + && suffix[1..].bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'.' || byte == b'-')) +} + pub async fn fetch_changelog(lang: &str) -> Result { let lang = normalize_changelog_lang(lang); let client = build_changelog_http_client()?; @@ -68,7 +101,8 @@ pub async fn fetch_changelog(lang: &str) -> Result { .map_err(|e| format!("Failed to fetch changelog: {e}"))?; let mut data: ChangelogData = resp.json().await.map_err(|e| format!("Failed to parse changelog: {e}"))?; - data.releases.retain(|release| !release.tag.trim().is_empty()); + // Treat R2 as untrusted cached data so auxiliary release streams never leak into the app UI. + data.releases.retain(|release| is_app_release_tag(release.tag.trim())); Ok(data) } @@ -86,7 +120,7 @@ fn build_changelog_http_client() -> Result { #[cfg(test)] mod tests { - use super::normalize_changelog_lang; + use super::{is_app_release_tag, normalize_changelog_lang}; #[test] fn normalizes_changelog_lang() { @@ -96,4 +130,14 @@ mod tests { assert_eq!(normalize_changelog_lang("en"), "en"); assert_eq!(normalize_changelog_lang("ja"), "en"); } + + #[test] + fn recognizes_only_app_release_tags() { + assert!(is_app_release_tag("v0.5.66")); + assert!(is_app_release_tag("v1.2.3-hotfix.1")); + assert!(!is_app_release_tag("packages-v0.4.42")); + assert!(!is_app_release_tag("agents-v0.2.64")); + assert!(!is_app_release_tag("v0.5")); + assert!(!is_app_release_tag("v0.5.x")); + } } diff --git a/docs/lib/changelog.test.ts b/docs/lib/changelog.test.ts new file mode 100644 index 0000000000..59c8bba532 --- /dev/null +++ b/docs/lib/changelog.test.ts @@ -0,0 +1,12 @@ +import assert from "node:assert/strict"; +import { test } from "vitest"; + +import { isAppReleaseTag } from "./releaseTags"; + +test("recognizes only DBX app release tags", () => { + assert.equal(isAppReleaseTag("v0.5.66"), true); + assert.equal(isAppReleaseTag("v1.2.3-hotfix.1"), true); + assert.equal(isAppReleaseTag("packages-v0.4.42"), false); + assert.equal(isAppReleaseTag("agents-v0.2.64"), false); + assert.equal(isAppReleaseTag("v0.5.x"), false); +}); diff --git a/docs/lib/changelog.ts b/docs/lib/changelog.ts index da67b1c64b..316698858d 100644 --- a/docs/lib/changelog.ts +++ b/docs/lib/changelog.ts @@ -1,4 +1,5 @@ import { requestJson } from "@/lib/httpJson"; +import { isAppReleaseTag } from "@/lib/releaseTags"; export type ChangelogItem = { title: string; @@ -119,7 +120,8 @@ export async function fetchGitHubChangelog(): Promise { return { updatedAt: new Date().toISOString(), releases: releases - .filter((release) => !release.draft && !release.prerelease && !release.tag_name.startsWith("agents-")) + // GitHub contains separate app, agent, and package release streams. + .filter((release) => !release.draft && !release.prerelease && isAppReleaseTag(release.tag_name)) .map((release) => ({ tag: release.tag_name, name: release.name || release.tag_name, diff --git a/docs/lib/releaseTags.ts b/docs/lib/releaseTags.ts new file mode 100644 index 0000000000..cb35186bf7 --- /dev/null +++ b/docs/lib/releaseTags.ts @@ -0,0 +1,5 @@ +const APP_RELEASE_TAG_PATTERN = /^v[0-9]+[.][0-9]+[.][0-9]+(?:[.-][0-9A-Za-z.-]+)?$/; + +export function isAppReleaseTag(tag: string) { + return APP_RELEASE_TAG_PATTERN.test(tag); +}