fix(changelog): filter non-app releases

This commit is contained in:
t8y2 2026-07-24 09:55:48 +08:00
parent 1870be4a81
commit 562edef4dc
No known key found for this signature in database
6 changed files with 110 additions and 6 deletions

View File

@ -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 || "" };

View File

@ -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",
});
});

View File

@ -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<ChangelogData, String> {
let lang = normalize_changelog_lang(lang);
let client = build_changelog_http_client()?;
@ -68,7 +101,8 @@ pub async fn fetch_changelog(lang: &str) -> Result<ChangelogData, String> {
.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<reqwest::Client, String> {
#[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"));
}
}

View File

@ -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);
});

View File

@ -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<ChangelogData> {
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,

5
docs/lib/releaseTags.ts Normal file
View File

@ -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);
}