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* () {...@@ -711,6 +711,7 @@ export const make = Effect.gen(function* () {
711 const { releaseNotes, omittedReleaseCount } = normalizeDesktopUpdateReleaseNotes(711 const { releaseNotes, omittedReleaseCount } = normalizeDesktopUpdateReleaseNotes(
712 info.releaseNotes,712 info.releaseNotes,
713 info.version,713 info.version,
714 state.channel,
714 );715 );
715 yield* setState(716 yield* setState(
716 reduceDesktopUpdateStateOnUpdateAvailable(717 reduceDesktopUpdateStateOnUpdateAvailable(
apps/desktop/src/updates/releaseNotes.test.ts+53-4
...@@ -21,6 +21,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {...@@ -21,6 +21,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
21 "**Full Changelog**: https://github.com/pingdotgg/t3code/compare/old...new",21 "**Full Changelog**: https://github.com/pingdotgg/t3code/compare/old...new",
22 ].join("\n"),22 ].join("\n"),
23 "0.0.36-nightly.20260828.1213",23 "0.0.36-nightly.20260828.1213",
24 "nightly",
24 );25 );
2526
26 expect(result).toEqual({27 expect(result).toEqual({
...@@ -50,6 +51,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {...@@ -50,6 +51,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
50 "<h2>New Contributors</h2><ul><li>@human made their first contribution</li></ul>" +51 "<h2>New Contributors</h2><ul><li>@human made their first contribution</li></ul>" +
51 "<h2>Full Changelog</h2>",52 "<h2>Full Changelog</h2>",
52 "1.2.3",53 "1.2.3",
54 "latest",
53 );55 );
5456
55 expect(result).toEqual({57 expect(result).toEqual({
...@@ -69,6 +71,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {...@@ -69,6 +71,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
69 },71 },
70 ],72 ],
71 "1.2.4",73 "1.2.4",
74 "latest",
72 );75 );
7376
74 expect(result.releaseNotes).toEqual([77 expect(result.releaseNotes).toEqual([
...@@ -85,6 +88,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {...@@ -85,6 +88,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
85 { version: "1.2.1", note: "- Older release" },88 { version: "1.2.1", note: "- Older release" },
86 ],89 ],
87 "1.2.3",90 "1.2.3",
91 "latest",
88 );92 );
8993
90 expect(result).toEqual({94 expect(result).toEqual({
...@@ -96,6 +100,42 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {...@@ -96,6 +100,42 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
96 });100 });
97 });101 });
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
99 it("counts valid groups before applying the six-release limit", () => {139 it("counts valid groups before applying the six-release limit", () => {
100 const releaseNotes = [140 const releaseNotes = [
101 { version: "1.3.9", note: "- Change 9" },141 { version: "1.3.9", note: "- Change 9" },
...@@ -108,7 +148,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {...@@ -108,7 +148,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
108 { version: "1.3.2", note: "- Change 2" },148 { version: "1.3.2", note: "- Change 2" },
109 ];149 ];
110150
111 const result = normalizeDesktopUpdateReleaseNotes(releaseNotes, "1.3.9");151 const result = normalizeDesktopUpdateReleaseNotes(releaseNotes, "1.3.9", "latest");
112152
113 expect(result.releaseNotes.map(({ version }) => version)).toEqual([153 expect(result.releaseNotes.map(({ version }) => version)).toEqual([
114 "1.3.9",154 "1.3.9",
...@@ -122,7 +162,11 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {...@@ -122,7 +162,11 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
122 });162 });
123163
124 it("decodes valid HTML entities", () => {164 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 );
126 expect(result).toEqual({170 expect(result).toEqual({
127 releaseNotes: [{ version: "1.0.0", items: ["Fix & polish 😀"], totalItems: 1 }],171 releaseNotes: [{ version: "1.0.0", items: ["Fix & polish 😀"], totalItems: 1 }],
128 omittedReleaseCount: 0,172 omittedReleaseCount: 0,
...@@ -140,6 +184,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {...@@ -140,6 +184,7 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
140 null,184 null,
141 ],185 ],
142 "1.2.3",186 "1.2.3",
187 "latest",
143 );188 );
144189
145 expect(result).toEqual({190 expect(result).toEqual({
...@@ -149,14 +194,18 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {...@@ -149,14 +194,18 @@ describe("normalizeDesktopUpdateReleaseNotes", () => {
149 });194 });
150195
151 it("returns an empty result for an invalid payload", () => {196 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({
153 releaseNotes: [],198 releaseNotes: [],
154 omittedReleaseCount: 0,199 omittedReleaseCount: 0,
155 });200 });
156 });201 });
157202
158 it("does not throw on out-of-range numeric entities and keeps the literal", () => {203 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 );
160 expect(result).toEqual({209 expect(result).toEqual({
161 releaseNotes: [{ version: "1.0.0", items: ["Broken entity &#9999999999;"], totalItems: 1 }],210 releaseNotes: [{ version: "1.0.0", items: ["Broken entity &#9999999999;"], totalItems: 1 }],
162 omittedReleaseCount: 0,211 omittedReleaseCount: 0,
apps/desktop/src/updates/releaseNotes.ts+16-3
...@@ -1,4 +1,6 @@...@@ -1,4 +1,6 @@
1import type { DesktopUpdateReleaseNote } from "@t3tools/contracts";1import type { DesktopUpdateChannel, DesktopUpdateReleaseNote } from "@t3tools/contracts";
2
3import { resolveDefaultDesktopUpdateChannel } from "./updateChannels.ts";
24
3interface ElectronReleaseNoteInfo {5interface ElectronReleaseNoteInfo {
4 readonly version: string;6 readonly version: string;
...@@ -122,16 +124,27 @@ interface NormalizedDesktopUpdateReleaseNotes {...@@ -122,16 +124,27 @@ interface NormalizedDesktopUpdateReleaseNotes {
122 readonly omittedReleaseCount: number;124 readonly omittedReleaseCount: number;
123}125}
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 */
125export function normalizeDesktopUpdateReleaseNotes(136export function normalizeDesktopUpdateReleaseNotes(
126 releaseNotes: unknown,137 releaseNotes: unknown,
127 fallbackVersion: string,138 fallbackVersion: string,
139 channel: DesktopUpdateChannel,
128): NormalizedDesktopUpdateReleaseNotes {140): NormalizedDesktopUpdateReleaseNotes {
129 const rawNotes =141 const rawNotes = (
130 typeof releaseNotes === "string"142 typeof releaseNotes === "string"
131 ? [{ version: fallbackVersion, note: releaseNotes }]143 ? [{ version: fallbackVersion, note: releaseNotes }]
132 : Array.isArray(releaseNotes)144 : Array.isArray(releaseNotes)
133 ? releaseNotes.filter(isElectronReleaseNoteInfo)145 ? releaseNotes.filter(isElectronReleaseNoteInfo)
134 : [];146 : []
147 ).filter((entry) => resolveDefaultDesktopUpdateChannel(entry.version) === channel);
135148
136 const normalizedNotes = rawNotes.flatMap((entry) => {149 const normalizedNotes = rawNotes.flatMap((entry) => {
137 const { items, totalItems } = extractReleaseNoteItems(entry.note);150 const { items, totalItems } = extractReleaseNoteItems(entry.note);