authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-14 09:15:56-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-14 09:15:56-07:00
log05e3bcbc65d43adc70c900f176bec6c095b3b833
tree74f3bcd07a3db50b2ba4e06d509673c59bdcb334
parent955b787e6b9fb8666e84abfa6868671859d72f7e
signature Signed by PGP key B5690EEEBB952194

fix(desktop): keep preview releases out of the nightly update changelog (#11753)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

3 files changed, 70 insertions(+), 7 deletions(-)

apps/desktop/src/updates/DesktopUpdates.ts+1
......@@ -711,6 +711,7 @@ export const make = Effect.gen(function* () {
711711 const { releaseNotes, omittedReleaseCount } = normalizeDesktopUpdateReleaseNotes(
712712 info.releaseNotes,
713713 info.version,
714 state.channel,
714715 );
715716 yield* setState(
716717 reduceDesktopUpdateStateOnUpdateAvailable(
apps/desktop/src/updates/releaseNotes.test.ts+53-4
......@@ -21,6 +21,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
2121 "**Full Changelog**: https://github.com/pingdotgg/t3code/compare/old...new",
2222 ].join("\n"),
2323 "0.0.36-nightly.20260828.1213",
24 "nightly",
2425 );
2526
2627 expect(result).toEqual({
......@@ -50,6 +51,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
5051 "<h2>New Contributors</h2><ul><li>@human made their first contribution</li></ul>" +
5152 "<h2>Full Changelog</h2>",
5253 "1.2.3",
54 "latest",
5355 );
5456
5557 expect(result).toEqual({
......@@ -69,6 +71,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
6971 },
7072 ],
7173 "1.2.4",
74 "latest",
7275 );
7376
7477 expect(result.releaseNotes).toEqual([
......@@ -85,6 +88,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
8588 { version: "1.2.1", note: "- Older release" },
8689 ],
8790 "1.2.3",
91 "latest",
8892 );
8993
9094 expect(result).toEqual({
......@@ -96,6 +100,42 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
96100 });
97101 });
98102
103 it("drops releases from other trains before grouping on nightly", () => {
104 // electron-updater's full changelog is "every version above the running
105 // one", and preview sorts above nightly, so the preview cuts come first.
106 const releaseNotes = [
107 { version: "0.0.41-preview.20260914.1683", note: "- Maintainer test build" },
108 { version: "0.0.41-preview.20260913.1669", note: "- Maintainer test build" },
109 { version: "0.0.41-nightly.20260914.1707", note: "- Nightly change 2" },
110 { version: "0.0.41-nightly.20260914.1700", note: "- Nightly change 1" },
111 ];
112
113 const result = normalizeDesktopUpdateReleaseNotes(
114 releaseNotes,
115 "0.0.41-nightly.20260914.1707",
116 "nightly",
117 );
118
119 expect(result.releaseNotes.map(({ version }) => version)).toEqual([
120 "0.0.41-nightly.20260914.1707",
121 "0.0.41-nightly.20260914.1700",
122 ]);
123 expect(result.omittedReleaseCount).toBe(0);
124 });
125
126 it("keeps only stable releases on the latest channel", () => {
127 const result = normalizeDesktopUpdateReleaseNotes(
128 [
129 { version: "0.0.42", note: "- Stable change" },
130 { version: "0.0.42-nightly.20260915.1710", note: "- Nightly change" },
131 ],
132 "0.0.42",
133 "latest",
134 );
135
136 expect(result.releaseNotes.map(({ version }) => version)).toEqual(["0.0.42"]);
137 });
138
99139 it("counts valid groups before applying the six-release limit", () => {
100140 const releaseNotes = [
101141 { version: "1.3.9", note: "- Change 9" },
......@@ -108,7 +148,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
108148 { version: "1.3.2", note: "- Change 2" },
109149 ];
110150
111 const result = normalizeDesktopUpdateReleaseNotes(releaseNotes, "1.3.9");
151 const result = normalizeDesktopUpdateReleaseNotes(releaseNotes, "1.3.9", "latest");
112152
113153 expect(result.releaseNotes.map(({ version }) => version)).toEqual([
114154 "1.3.9",
......@@ -122,7 +162,11 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
122162 });
123163
124164 it("decodes valid HTML entities", () => {
125 const result = normalizeDesktopUpdateReleaseNotes("- Fix &amp; polish &#128512;", "1.0.0");
165 const result = normalizeDesktopUpdateReleaseNotes(
166 "- Fix &amp; polish &#128512;",
167 "1.0.0",
168 "latest",
169 );
126170 expect(result).toEqual({
127171 releaseNotes: [{ version: "1.0.0", items: ["Fix & polish 😀"], totalItems: 1 }],
128172 omittedReleaseCount: 0,
......@@ -140,6 +184,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
140184 null,
141185 ],
142186 "1.2.3",
187 "latest",
143188 );
144189
145190 expect(result).toEqual({
......@@ -149,14 +194,18 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
149194 });
150195
151196 it("returns an empty result for an invalid payload", () => {
152 expect(normalizeDesktopUpdateReleaseNotes({ note: "- Invalid" }, "1.0.0")).toEqual({
197 expect(normalizeDesktopUpdateReleaseNotes({ note: "- Invalid" }, "1.0.0", "latest")).toEqual({
153198 releaseNotes: [],
154199 omittedReleaseCount: 0,
155200 });
156201 });
157202
158203 it("does not throw on out-of-range numeric entities and keeps the literal", () => {
159 const result = normalizeDesktopUpdateReleaseNotes("- Broken entity &#9999999999;", "1.0.0");
204 const result = normalizeDesktopUpdateReleaseNotes(
205 "- Broken entity &#9999999999;",
206 "1.0.0",
207 "latest",
208 );
160209 expect(result).toEqual({
161210 releaseNotes: [{ version: "1.0.0", items: ["Broken entity &#9999999999;"], totalItems: 1 }],
162211 omittedReleaseCount: 0,
apps/desktop/src/updates/releaseNotes.ts+16-3
......@@ -1,4 +1,6 @@
1import type { DesktopUpdateReleaseNote } from "@t3tools/contracts";
1import type { DesktopUpdateChannel, DesktopUpdateReleaseNote } from "@t3tools/contracts";
2
3import { resolveDefaultDesktopUpdateChannel } from "./updateChannels.ts";
24
35interface ElectronReleaseNoteInfo {
46 readonly version: string;
......@@ -122,16 +124,27 @@ interface NormalizedDesktopUpdateReleaseNotes {
122124 readonly omittedReleaseCount: number;
123125}
124126
127/**
128 * Turns electron-updater's release notes into the groups the popover shows.
129 * With `fullChangelog` on (nightly), electron-updater collects every GitHub
130 * release whose version is semver-greater than the running one, whatever
131 * train it belongs to; a maintainers' `-preview.` cut sorts above every
132 * `-nightly.` of the same base version and would lead the list. Only
133 * releases on the channel being followed are kept, the same test the
134 * updater applies to the offered version itself.
135 */
125136export function normalizeDesktopUpdateReleaseNotes(
126137 releaseNotes: unknown,
127138 fallbackVersion: string,
139 channel: DesktopUpdateChannel,
128140): NormalizedDesktopUpdateReleaseNotes {
129 const rawNotes =
141 const rawNotes = (
130142 typeof releaseNotes === "string"
131143 ? [{ version: fallbackVersion, note: releaseNotes }]
132144 : Array.isArray(releaseNotes)
133145 ? releaseNotes.filter(isElectronReleaseNoteInfo)
134 : [];
146 : []
147 ).filter((entry) => resolveDefaultDesktopUpdateChannel(entry.version) === channel);
135148
136149 const normalizedNotes = rawNotes.flatMap((entry) => {
137150 const { items, totalItems } = extractReleaseNoteItems(entry.note);