7acbc7250302
changelog: use oldest save per version to show version-intro message, not follow-up CONTINUITY updates (v3.3.1)
- time
- 2026-06-13 16:03 UTC · 4 months ago
- machine
- koh2
- agent
- claude-sonnet-4-6
- session
- fix update changelog
- parent
- d90a1016c675
↓ nina-7acbc725.tar.gzatlas at this save
files changed3 files
VERSION+1 −1build.zig.zon+1 −1src/commands.zig+40 −15build.zig.zon+1 −1
@@ -9,7 +9,7 @@99 .name = .nina,1010 // This is a [Semantic Version](https://semver.org/).1111 // In a future version of Zig it will be used for package deduplication.12- .version = "3.2.5",12+ .version = "3.3.1",1313 // Together with name, this represents a globally unique package1414 // identifier. This field is generated by the Zig toolchain when the1515 // package is first created, and then *never changes*. This allowssrc/commands.zig+40 −15
@@ -553,9 +553,13 @@553553 const repo = after_api[0..repo_end];554554 if (repo.len == 0) return error.InvalidUpdateSource;555555 556- return std.fmt.allocPrint(gpa, "{s}/{s}/archives.json?limit=40", .{ origin, repo });556+ return std.fmt.allocPrint(gpa, "{s}/{s}/archives.json?limit=100", .{ origin, repo });557557 }558558 559+// Saves arrive newest-first. For each version in the update range we want the OLDEST560+// save message (the one that introduced that version), not a follow-up cleanup/docs save.561+// We overwrite the tracked message on each occurrence, so the final stored value per562+// version is always the oldest (last-seen in a newest-first stream).559563 fn releaseNotesFromArchiveJson(gpa: std.mem.Allocator, body: []const u8, current: []const u8, latest: []const u8) ![]u8 {560564 const parsed = std.json.parseFromSlice(std.json.Value, gpa, body, .{}) catch return gpa.dupe(u8, "");561565 defer parsed.deinit();@@ -564,9 +568,12 @@564568 const saves_v = parsed.value.object.get("saves") orelse return gpa.dupe(u8, "");565569 if (saves_v != .array) return gpa.dupe(u8, "");566570 567- var out: std.ArrayList(u8) = .empty;568- errdefer out.deinit(gpa);569- var any = false;571+ // version string → oldest message seen (both slice into parsed JSON, valid until deinit)572+ var best_msg = std.StringHashMap([]const u8).init(gpa);573+ defer best_msg.deinit();574+ var order: std.ArrayList([]const u8) = .empty;575+ defer order.deinit(gpa);576+570577 for (saves_v.array.items) |save| {571578 if (save != .object) continue;572579 const ver_v = save.object.get("version") orelse continue;@@ -576,19 +583,34 @@576583 const msg = std.mem.trim(u8, msg_v.string, " \t\r\n");577584 if (msg.len == 0 or !versionInUpdateRange(ver, current, latest)) continue;578585 579- try appendFmt(&out, gpa, "## {s}\n", .{ver});580- try appendFmt(&out, gpa, "- {s}\n\n", .{msg});581- any = true;586+ const entry = try best_msg.getOrPut(ver);587+ if (!entry.found_existing) try order.append(gpa, ver);588+ // Always overwrite: saves are newest-first, so last write = oldest save = version-intro message589+ entry.value_ptr.* = msg;582590 }583591 584- if (!any) {592+ if (order.items.len == 0) return gpa.dupe(u8, "");593+594+ var out: std.ArrayList(u8) = .empty;595+ errdefer out.deinit(gpa);596+ for (order.items) |ver| {597+ const msg = best_msg.get(ver).?;598+ // Use only the first line as the summary; multi-line saves may have implementation details below599+ const summary_end = std.mem.indexOf(u8, msg, "\n") orelse msg.len;600+ const summary = std.mem.trim(u8, msg[0..summary_end], " \t\r");601+ if (summary.len == 0) continue;602+ try appendLine(&out, gpa, "## {s}\n", .{ver});603+ try appendLine(&out, gpa, "- {s}\n\n", .{summary});604+ }605+606+ if (out.items.len == 0) {585607 out.deinit(gpa);586608 return gpa.dupe(u8, "");587609 }588610 return out.toOwnedSlice(gpa);589611 }590612 591-fn appendFmt(out: *std.ArrayList(u8), gpa: std.mem.Allocator, comptime template: []const u8, args: anytype) !void {613+fn appendLine(out: *std.ArrayList(u8), gpa: std.mem.Allocator, comptime template: []const u8, args: anytype) !void {592614 const line = try std.fmt.allocPrint(gpa, template, args);593615 defer gpa.free(line);594616 try out.appendSlice(gpa, line);@@ -643,21 +665,24 @@643665 try std.testing.expect(!remoteVersionNewer("3.2.4", "3.2.4"));644666 }645667 646-test "releaseNotesFromArchiveJson builds changelog from versioned saves" {668+test "releaseNotesFromArchiveJson uses oldest save per version" {669+ // Saves arrive newest-first. For 3.2.3, the newest save is a CONTINUITY update;670+ // the oldest is the version-intro message. We expect the intro message in the output.647671 const body =648672 \\{649673 \\ "saves": [650- \\ {"version":"3.2.3","message":"upgrade: live build panel during rebuild phase"},651- \\ {"version":"3.1.9","message":"old release"},652- \\ {"version":null,"message":"unversioned"}674+ \\ {"version":"3.2.3","message":"wip: record verification in CONTINUITY"},675+ \\ {"version":"3.2.1","message":"old release"},676+ \\ {"version":"3.2.3","message":"add streaming deflate path"}653677 \\ ]654678 \\}655679 ;656- const notes = try releaseNotesFromArchiveJson(std.testing.allocator, body, "3.1.9", "3.2.3");680+ const notes = try releaseNotesFromArchiveJson(std.testing.allocator, body, "3.2.1", "3.2.3");657681 defer std.testing.allocator.free(notes);658682 659683 try std.testing.expect(std.mem.indexOf(u8, notes, "## 3.2.3") != null);660- try std.testing.expect(std.mem.indexOf(u8, notes, "- upgrade: live build panel") != null);684+ try std.testing.expect(std.mem.indexOf(u8, notes, "- add streaming deflate path") != null);685+ try std.testing.expect(std.mem.indexOf(u8, notes, "CONTINUITY") == null);661686 try std.testing.expect(std.mem.indexOf(u8, notes, "old release") == null);662687 }663688