pre-release filtering
This commit is contained in:
parent
a9a0e7e9f3
commit
6ad55474fa
6 changed files with 583 additions and 56 deletions
72
src/main.zig
72
src/main.zig
|
@ -67,6 +67,7 @@ pub const Release = struct {
|
||||||
description: []const u8,
|
description: []const u8,
|
||||||
provider: []const u8,
|
provider: []const u8,
|
||||||
is_tag: bool = false,
|
is_tag: bool = false,
|
||||||
|
is_prerelease: bool = false, // Track if this is a prerelease/draft
|
||||||
|
|
||||||
pub fn deinit(self: Release, allocator: Allocator) void {
|
pub fn deinit(self: Release, allocator: Allocator) void {
|
||||||
allocator.free(self.repo_name);
|
allocator.free(self.repo_name);
|
||||||
|
@ -205,8 +206,25 @@ pub fn main() !u8 {
|
||||||
// Sort all releases by published date (most recent first)
|
// Sort all releases by published date (most recent first)
|
||||||
std.mem.sort(Release, all_releases.items, {}, utils.compareReleasesByDate);
|
std.mem.sort(Release, all_releases.items, {}, utils.compareReleasesByDate);
|
||||||
|
|
||||||
|
// Filter out prereleases after duplicate detection is complete
|
||||||
|
// Exception: Keep git-style version prereleases (like kraftkit)
|
||||||
|
var filtered_releases = std.ArrayList(Release).init(allocator);
|
||||||
|
defer filtered_releases.deinit();
|
||||||
|
|
||||||
|
for (all_releases.items) |release| {
|
||||||
|
if (!release.is_prerelease) {
|
||||||
|
try filtered_releases.append(release);
|
||||||
|
} else {
|
||||||
|
// Check if this is a git-style version (e.g., v1.2.3-123-g1234567)
|
||||||
|
// These should be included even if marked as prerelease
|
||||||
|
if (isGitStyleVersion(release.tag_name)) {
|
||||||
|
try filtered_releases.append(release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate Atom feed from filtered releases
|
// Generate Atom feed from filtered releases
|
||||||
const atom_content = try atom.generateFeed(allocator, all_releases.items);
|
const atom_content = try atom.generateFeed(allocator, filtered_releases.items);
|
||||||
defer allocator.free(atom_content);
|
defer allocator.free(atom_content);
|
||||||
|
|
||||||
// Write to output file
|
// Write to output file
|
||||||
|
@ -218,12 +236,61 @@ pub fn main() !u8 {
|
||||||
_ = checkFileSizeAndWarn(atom_content.len);
|
_ = checkFileSizeAndWarn(atom_content.len);
|
||||||
|
|
||||||
// Log to stderr for user feedback
|
// Log to stderr for user feedback
|
||||||
printInfo("Total releases in feed: {} of {} total in last {} days\n", .{ all_releases.items.len, original_count, @divTrunc(RELEASE_AGE_LIMIT_SECONDS, std.time.s_per_day) });
|
printInfo("Total releases in feed: {} of {} total in last {} days\n", .{ filtered_releases.items.len, original_count, @divTrunc(RELEASE_AGE_LIMIT_SECONDS, std.time.s_per_day) });
|
||||||
printInfo("Updated feed written to: {s}\n", .{output_file});
|
printInfo("Updated feed written to: {s}\n", .{output_file});
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a tag contains git-style version pattern like v1.2.3-123-g1234567
|
||||||
|
fn isGitStyleVersion(tag_name: []const u8) bool {
|
||||||
|
// Convert to lowercase for comparison
|
||||||
|
var tag_lower_buf: [256]u8 = undefined;
|
||||||
|
if (tag_name.len >= tag_lower_buf.len) return false;
|
||||||
|
|
||||||
|
const tag_lower = std.ascii.lowerString(tag_lower_buf[0..tag_name.len], tag_name);
|
||||||
|
|
||||||
|
// Look for pattern: -number-g followed by hex characters
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < tag_lower.len) {
|
||||||
|
if (tag_lower[i] == '-') {
|
||||||
|
// Found a dash, check if followed by digits
|
||||||
|
var j = i + 1;
|
||||||
|
var has_digits = false;
|
||||||
|
|
||||||
|
// Skip digits
|
||||||
|
while (j < tag_lower.len and tag_lower[j] >= '0' and tag_lower[j] <= '9') {
|
||||||
|
has_digits = true;
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if followed by -g and hex characters
|
||||||
|
if (has_digits and j + 2 < tag_lower.len and
|
||||||
|
tag_lower[j] == '-' and tag_lower[j + 1] == 'g')
|
||||||
|
{
|
||||||
|
// Check if followed by hex characters
|
||||||
|
var k = j + 2;
|
||||||
|
var hex_count: usize = 0;
|
||||||
|
while (k < tag_lower.len and
|
||||||
|
((tag_lower[k] >= '0' and tag_lower[k] <= '9') or
|
||||||
|
(tag_lower[k] >= 'a' and tag_lower[k] <= 'f')))
|
||||||
|
{
|
||||||
|
hex_count += 1;
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found hex characters, this looks like git describe format
|
||||||
|
if (hex_count >= 4) { // At least 4 hex chars for a reasonable commit hash
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
fn formatTimestampForDisplay(allocator: Allocator, timestamp: i64) ![]const u8 {
|
fn formatTimestampForDisplay(allocator: Allocator, timestamp: i64) ![]const u8 {
|
||||||
if (timestamp == 0) {
|
if (timestamp == 0) {
|
||||||
return try allocator.dupe(u8, "beginning of time");
|
return try allocator.dupe(u8, "beginning of time");
|
||||||
|
@ -496,6 +563,7 @@ test {
|
||||||
std.testing.refAllDecls(@import("timestamp_tests.zig"));
|
std.testing.refAllDecls(@import("timestamp_tests.zig"));
|
||||||
std.testing.refAllDecls(@import("atom.zig"));
|
std.testing.refAllDecls(@import("atom.zig"));
|
||||||
std.testing.refAllDecls(@import("utils.zig"));
|
std.testing.refAllDecls(@import("utils.zig"));
|
||||||
|
std.testing.refAllDecls(@import("tag_filter.zig"));
|
||||||
std.testing.refAllDecls(@import("providers/GitHub.zig"));
|
std.testing.refAllDecls(@import("providers/GitHub.zig"));
|
||||||
std.testing.refAllDecls(@import("providers/GitLab.zig"));
|
std.testing.refAllDecls(@import("providers/GitLab.zig"));
|
||||||
std.testing.refAllDecls(@import("providers/SourceHut.zig"));
|
std.testing.refAllDecls(@import("providers/SourceHut.zig"));
|
||||||
|
|
|
@ -4,6 +4,7 @@ const json = std.json;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArrayList = std.ArrayList;
|
const ArrayList = std.ArrayList;
|
||||||
const utils = @import("../utils.zig");
|
const utils = @import("../utils.zig");
|
||||||
|
const tag_filter = @import("../tag_filter.zig");
|
||||||
|
|
||||||
const Release = @import("../main.zig").Release;
|
const Release = @import("../main.zig").Release;
|
||||||
const Provider = @import("../Provider.zig");
|
const Provider = @import("../Provider.zig");
|
||||||
|
@ -224,6 +225,13 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
|
||||||
const tag_name_value = obj.get("tag_name") orelse continue;
|
const tag_name_value = obj.get("tag_name") orelse continue;
|
||||||
if (tag_name_value != .string) continue;
|
if (tag_name_value != .string) continue;
|
||||||
|
|
||||||
|
const tag_name = tag_name_value.string;
|
||||||
|
|
||||||
|
// Skip problematic tags
|
||||||
|
if (tag_filter.shouldSkipTag(allocator, tag_name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const published_at_value = obj.get("published_at") orelse continue;
|
const published_at_value = obj.get("published_at") orelse continue;
|
||||||
if (published_at_value != .string) continue;
|
if (published_at_value != .string) continue;
|
||||||
|
|
||||||
|
@ -235,7 +243,7 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
|
||||||
|
|
||||||
const release = Release{
|
const release = Release{
|
||||||
.repo_name = try allocator.dupe(u8, repo),
|
.repo_name = try allocator.dupe(u8, repo),
|
||||||
.tag_name = try allocator.dupe(u8, tag_name_value.string),
|
.tag_name = try allocator.dupe(u8, tag_name),
|
||||||
.published_at = try utils.parseReleaseTimestamp(published_at_value.string),
|
.published_at = try utils.parseReleaseTimestamp(published_at_value.string),
|
||||||
.html_url = try allocator.dupe(u8, html_url_value.string),
|
.html_url = try allocator.dupe(u8, html_url_value.string),
|
||||||
.description = try allocator.dupe(u8, body_str),
|
.description = try allocator.dupe(u8, body_str),
|
||||||
|
@ -342,3 +350,26 @@ test "codeberg release parsing with live data snapshot" {
|
||||||
);
|
);
|
||||||
try std.testing.expectEqualStrings("codeberg", releases.items[0].provider);
|
try std.testing.expectEqualStrings("codeberg", releases.items[0].provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Codeberg tag filtering" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
// Test that Codeberg now uses the same filtering as other providers
|
||||||
|
const problematic_tags = [_][]const u8{
|
||||||
|
"nightly", "prerelease", "latest", "edge", "canary", "dev-branch",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (problematic_tags) |tag| {
|
||||||
|
try std.testing.expect(tag_filter.shouldSkipTag(allocator, tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that valid tags are not filtered
|
||||||
|
const valid_tags = [_][]const u8{
|
||||||
|
"v1.0.0", "v2.1.3-stable",
|
||||||
|
// Note: v1.0.0-alpha.1 is now filtered to avoid duplicates
|
||||||
|
};
|
||||||
|
|
||||||
|
for (valid_tags) |tag| {
|
||||||
|
try std.testing.expect(!tag_filter.shouldSkipTag(allocator, tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator;
|
||||||
const ArrayList = std.ArrayList;
|
const ArrayList = std.ArrayList;
|
||||||
const Thread = std.Thread;
|
const Thread = std.Thread;
|
||||||
const utils = @import("../utils.zig");
|
const utils = @import("../utils.zig");
|
||||||
|
const tag_filter = @import("../tag_filter.zig");
|
||||||
|
|
||||||
const Release = @import("../main.zig").Release;
|
const Release = @import("../main.zig").Release;
|
||||||
const Provider = @import("../Provider.zig");
|
const Provider = @import("../Provider.zig");
|
||||||
|
@ -488,6 +489,10 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
|
||||||
for (array.items) |item| {
|
for (array.items) |item| {
|
||||||
const obj = item.object;
|
const obj = item.object;
|
||||||
|
|
||||||
|
// Track prerelease and draft status but don't filter yet
|
||||||
|
const is_prerelease = if (obj.get("prerelease")) |prerelease| prerelease.bool else false;
|
||||||
|
const is_draft = if (obj.get("draft")) |draft| draft.bool else false;
|
||||||
|
|
||||||
const body_value = obj.get("body") orelse json.Value{ .string = "" };
|
const body_value = obj.get("body") orelse json.Value{ .string = "" };
|
||||||
const body_str = if (body_value == .string) body_value.string else "";
|
const body_str = if (body_value == .string) body_value.string else "";
|
||||||
|
|
||||||
|
@ -499,6 +504,7 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
|
||||||
.description = try allocator.dupe(u8, body_str),
|
.description = try allocator.dupe(u8, body_str),
|
||||||
.provider = try allocator.dupe(u8, "github"),
|
.provider = try allocator.dupe(u8, "github"),
|
||||||
.is_tag = false,
|
.is_tag = false,
|
||||||
|
.is_prerelease = is_prerelease or is_draft, // Mark as prerelease if either flag is true
|
||||||
};
|
};
|
||||||
|
|
||||||
try releases.append(release);
|
try releases.append(release);
|
||||||
|
@ -507,57 +513,9 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
|
||||||
return releases;
|
return releases;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shouldSkipTag(allocator: std.mem.Allocator, tag_name: []const u8) bool {
|
/// Wrapper function for backward compatibility and testing
|
||||||
// List of common moving tags that should be filtered out
|
pub fn shouldSkipTag(allocator: std.mem.Allocator, tag_name: []const u8) bool {
|
||||||
const moving_tags = [_][]const u8{
|
return tag_filter.shouldSkipTag(allocator, tag_name);
|
||||||
// common "latest commit tags"
|
|
||||||
"latest",
|
|
||||||
"tip",
|
|
||||||
"continuous",
|
|
||||||
"head",
|
|
||||||
|
|
||||||
// common branch tags
|
|
||||||
"main",
|
|
||||||
"master",
|
|
||||||
"trunk",
|
|
||||||
"develop",
|
|
||||||
"development",
|
|
||||||
"dev",
|
|
||||||
|
|
||||||
// common fast moving channel names
|
|
||||||
"nightly",
|
|
||||||
"edge",
|
|
||||||
"canary",
|
|
||||||
"alpha",
|
|
||||||
|
|
||||||
// common slower channels, but without version information
|
|
||||||
// they probably are not something we're interested in
|
|
||||||
"beta",
|
|
||||||
"rc",
|
|
||||||
"release",
|
|
||||||
"snapshot",
|
|
||||||
"unstable",
|
|
||||||
"experimental",
|
|
||||||
"prerelease",
|
|
||||||
"preview",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if tag name contains common moving patterns
|
|
||||||
const tag_lower = std.ascii.allocLowerString(allocator, tag_name) catch return false;
|
|
||||||
defer allocator.free(tag_lower);
|
|
||||||
|
|
||||||
for (moving_tags) |moving_tag|
|
|
||||||
if (std.mem.eql(u8, tag_lower, moving_tag))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Skip pre-release and development tags
|
|
||||||
if (std.mem.startsWith(u8, tag_lower, "pre-") or
|
|
||||||
std.mem.startsWith(u8, tag_lower, "dev-") or
|
|
||||||
std.mem.startsWith(u8, tag_lower, "test-") or
|
|
||||||
std.mem.startsWith(u8, tag_lower, "debug-"))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getRepoTags(allocator: Allocator, client: *http.Client, token: []const u8, repo: []const u8) !ArrayList(Release) {
|
fn getRepoTags(allocator: Allocator, client: *http.Client, token: []const u8, repo: []const u8) !ArrayList(Release) {
|
||||||
|
@ -660,7 +618,7 @@ fn parseGraphQL(allocator: std.mem.Allocator, repo: []const u8, body: []const u8
|
||||||
const tag_name = node_obj.get("name").?.string;
|
const tag_name = node_obj.get("name").?.string;
|
||||||
|
|
||||||
// Skip common moving tags
|
// Skip common moving tags
|
||||||
if (shouldSkipTag(allocator, tag_name)) continue;
|
if (tag_filter.shouldSkipTag(allocator, tag_name)) continue;
|
||||||
|
|
||||||
const target = node_obj.get("target").?.object;
|
const target = node_obj.get("target").?.object;
|
||||||
|
|
||||||
|
@ -1012,6 +970,114 @@ test "addNonReleaseTags should not add duplicate tags" {
|
||||||
try std.testing.expect(found_unique);
|
try std.testing.expect(found_unique);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "GitHub prerelease and draft filtering" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
// Test shouldSkipRelease function
|
||||||
|
try std.testing.expect(tag_filter.shouldSkipRelease(true, false)); // prerelease
|
||||||
|
try std.testing.expect(tag_filter.shouldSkipRelease(false, true)); // draft
|
||||||
|
try std.testing.expect(tag_filter.shouldSkipRelease(true, true)); // both
|
||||||
|
try std.testing.expect(!tag_filter.shouldSkipRelease(false, false)); // normal release
|
||||||
|
|
||||||
|
// Mock GitHub API response with prerelease and draft fields
|
||||||
|
const github_api_response =
|
||||||
|
\\[
|
||||||
|
\\ {
|
||||||
|
\\ "tag_name": "v1.0.0",
|
||||||
|
\\ "published_at": "2024-01-01T00:00:00Z",
|
||||||
|
\\ "html_url": "https://github.com/test/repo/releases/tag/v1.0.0",
|
||||||
|
\\ "body": "Stable release",
|
||||||
|
\\ "prerelease": false,
|
||||||
|
\\ "draft": false
|
||||||
|
\\ },
|
||||||
|
\\ {
|
||||||
|
\\ "tag_name": "v1.1.0-alpha",
|
||||||
|
\\ "published_at": "2024-01-02T00:00:00Z",
|
||||||
|
\\ "html_url": "https://github.com/test/repo/releases/tag/v1.1.0-alpha",
|
||||||
|
\\ "body": "Alpha release",
|
||||||
|
\\ "prerelease": true,
|
||||||
|
\\ "draft": false
|
||||||
|
\\ },
|
||||||
|
\\ {
|
||||||
|
\\ "tag_name": "v1.2.0-draft",
|
||||||
|
\\ "published_at": "2024-01-03T00:00:00Z",
|
||||||
|
\\ "html_url": "https://github.com/test/repo/releases/tag/v1.2.0-draft",
|
||||||
|
\\ "body": "Draft release",
|
||||||
|
\\ "prerelease": false,
|
||||||
|
\\ "draft": true
|
||||||
|
\\ }
|
||||||
|
\\]
|
||||||
|
;
|
||||||
|
|
||||||
|
// Parse the JSON to verify structure
|
||||||
|
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, github_api_response, .{});
|
||||||
|
defer parsed.deinit();
|
||||||
|
|
||||||
|
const array = parsed.value.array;
|
||||||
|
try std.testing.expectEqual(@as(usize, 3), array.items.len);
|
||||||
|
|
||||||
|
// Verify the prerelease and draft fields are present and correct
|
||||||
|
const stable_release = array.items[0].object;
|
||||||
|
try std.testing.expectEqual(false, stable_release.get("prerelease").?.bool);
|
||||||
|
try std.testing.expectEqual(false, stable_release.get("draft").?.bool);
|
||||||
|
|
||||||
|
const prerelease_item = array.items[1].object;
|
||||||
|
try std.testing.expectEqual(true, prerelease_item.get("prerelease").?.bool);
|
||||||
|
try std.testing.expectEqual(false, prerelease_item.get("draft").?.bool);
|
||||||
|
|
||||||
|
const draft_item = array.items[2].object;
|
||||||
|
try std.testing.expectEqual(false, draft_item.get("prerelease").?.bool);
|
||||||
|
try std.testing.expectEqual(true, draft_item.get("draft").?.bool);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "shouldSkipTag comprehensive filtering tests" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
// Test exact matches for moving tags
|
||||||
|
const moving_tags = [_][]const u8{
|
||||||
|
"latest", "tip", "continuous", "head", "main", "master", "trunk",
|
||||||
|
"develop", "development", "dev", "nightly", "edge", "canary", "alpha",
|
||||||
|
"beta", "rc", "release", "snapshot", "unstable", "experimental", "prerelease",
|
||||||
|
"preview",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (moving_tags) |tag| {
|
||||||
|
try std.testing.expect(shouldSkipTag(allocator, tag));
|
||||||
|
|
||||||
|
// Test case insensitive matching
|
||||||
|
const upper_tag = try std.ascii.allocUpperString(allocator, tag);
|
||||||
|
defer allocator.free(upper_tag);
|
||||||
|
try std.testing.expect(shouldSkipTag(allocator, upper_tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test prefix patterns
|
||||||
|
const prefix_patterns = [_][]const u8{
|
||||||
|
"pre-release", "pre-1.0.0", "dev-branch", "dev-feature",
|
||||||
|
"test-build", "test-123", "debug-version", "debug-info",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (prefix_patterns) |tag| {
|
||||||
|
try std.testing.expect(shouldSkipTag(allocator, tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test valid version tags that should NOT be filtered
|
||||||
|
const valid_tags = [_][]const u8{
|
||||||
|
"v1.0.0", "v2.1.3", "1.0.0", "2.1.3-stable", "v1.0.0-final",
|
||||||
|
"release-1.0.0", "stable-v1.0.0", "v1.0.0-lts",
|
||||||
|
"2023.1.0",
|
||||||
|
// Note: Semantic versioning prerelease tags are now filtered to avoid duplicates
|
||||||
|
};
|
||||||
|
|
||||||
|
for (valid_tags) |tag| {
|
||||||
|
try std.testing.expect(!shouldSkipTag(allocator, tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test edge cases
|
||||||
|
try std.testing.expect(!shouldSkipTag(allocator, "")); // Empty string
|
||||||
|
try std.testing.expect(!shouldSkipTag(allocator, "v1.0.0+build.1")); // Build metadata
|
||||||
|
try std.testing.expect(!shouldSkipTag(allocator, "nightly-build-v1.0.0")); // Contains "nightly" but has version
|
||||||
|
}
|
||||||
|
|
||||||
test "parse tag graphQL output" {
|
test "parse tag graphQL output" {
|
||||||
const result =
|
const result =
|
||||||
\\{"data":{"repository":{"refs":{"pageInfo":{"hasNextPage":false,"endCursor":"MzY"},"nodes":[{"name":"v0.7.9","target":{"committedDate":"2025-07-16T06:14:23Z","message":"chore: bump version to v0.7.9"}},{"name":"v0.7.8","target":{"committedDate":"2025-07-15T23:01:11Z","message":"chore: bump version to v0.7.8"}},{"name":"v0.7.7","target":{"committedDate":"2025-04-16T02:32:43Z","message":"chore: bump version to v0.7.0"}},{"name":"v0.7.6","target":{"committedDate":"2025-04-13T18:00:14Z","message":"chore: bump version to v0.7.6"}},{"name":"v0.7.5","target":{"committedDate":"2025-04-12T20:31:13Z","message":"chore: bump version to v0.7.5"}},{"name":"v0.7.4","target":{"committedDate":"2025-04-06T02:08:45Z","message":"chore: bump version to v0.7.4"}},{"name":"v0.3.6","target":{"committedDate":"2024-12-20T07:25:36Z","message":"chore: bump version to v3.4.6"}},{"name":"v0.1.0","target":{"committedDate":"2024-11-16T23:19:14Z","message":"chore: bump version to v0.1.0"}}]}}}}
|
\\{"data":{"repository":{"refs":{"pageInfo":{"hasNextPage":false,"endCursor":"MzY"},"nodes":[{"name":"v0.7.9","target":{"committedDate":"2025-07-16T06:14:23Z","message":"chore: bump version to v0.7.9"}},{"name":"v0.7.8","target":{"committedDate":"2025-07-15T23:01:11Z","message":"chore: bump version to v0.7.8"}},{"name":"v0.7.7","target":{"committedDate":"2025-04-16T02:32:43Z","message":"chore: bump version to v0.7.0"}},{"name":"v0.7.6","target":{"committedDate":"2025-04-13T18:00:14Z","message":"chore: bump version to v0.7.6"}},{"name":"v0.7.5","target":{"committedDate":"2025-04-12T20:31:13Z","message":"chore: bump version to v0.7.5"}},{"name":"v0.7.4","target":{"committedDate":"2025-04-06T02:08:45Z","message":"chore: bump version to v0.7.4"}},{"name":"v0.3.6","target":{"committedDate":"2024-12-20T07:25:36Z","message":"chore: bump version to v3.4.6"}},{"name":"v0.1.0","target":{"committedDate":"2024-11-16T23:19:14Z","message":"chore: bump version to v0.1.0"}}]}}}}
|
||||||
|
|
|
@ -4,6 +4,7 @@ const json = std.json;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArrayList = std.ArrayList;
|
const ArrayList = std.ArrayList;
|
||||||
const utils = @import("../utils.zig");
|
const utils = @import("../utils.zig");
|
||||||
|
const tag_filter = @import("../tag_filter.zig");
|
||||||
|
|
||||||
const Release = @import("../main.zig").Release;
|
const Release = @import("../main.zig").Release;
|
||||||
const Provider = @import("../Provider.zig");
|
const Provider = @import("../Provider.zig");
|
||||||
|
@ -220,9 +221,16 @@ fn getProjectReleases(allocator: Allocator, client: *http.Client, token: []const
|
||||||
const desc_value = obj.get("description") orelse json.Value{ .string = "" };
|
const desc_value = obj.get("description") orelse json.Value{ .string = "" };
|
||||||
const desc_str = if (desc_value == .string) desc_value.string else "";
|
const desc_str = if (desc_value == .string) desc_value.string else "";
|
||||||
|
|
||||||
|
const tag_name = obj.get("tag_name").?.string;
|
||||||
|
|
||||||
|
// Skip problematic tags
|
||||||
|
if (tag_filter.shouldSkipTag(allocator, tag_name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const release = Release{
|
const release = Release{
|
||||||
.repo_name = try allocator.dupe(u8, obj.get("name").?.string),
|
.repo_name = try allocator.dupe(u8, obj.get("name").?.string),
|
||||||
.tag_name = try allocator.dupe(u8, obj.get("tag_name").?.string),
|
.tag_name = try allocator.dupe(u8, tag_name),
|
||||||
.published_at = try utils.parseReleaseTimestamp(obj.get("created_at").?.string),
|
.published_at = try utils.parseReleaseTimestamp(obj.get("created_at").?.string),
|
||||||
.html_url = try allocator.dupe(u8, obj.get("_links").?.object.get("self").?.string),
|
.html_url = try allocator.dupe(u8, obj.get("_links").?.object.get("self").?.string),
|
||||||
.description = try allocator.dupe(u8, desc_str),
|
.description = try allocator.dupe(u8, desc_str),
|
||||||
|
@ -347,3 +355,26 @@ test "gitlab release parsing with live data snapshot" {
|
||||||
);
|
);
|
||||||
try std.testing.expectEqualStrings("gitlab", releases.items[0].provider);
|
try std.testing.expectEqualStrings("gitlab", releases.items[0].provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "GitLab tag filtering" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
// Test that GitLab now uses the same filtering as other providers
|
||||||
|
const problematic_tags = [_][]const u8{
|
||||||
|
"nightly", "prerelease", "latest", "edge", "canary", "dev-branch",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (problematic_tags) |tag| {
|
||||||
|
try std.testing.expect(tag_filter.shouldSkipTag(allocator, tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that valid tags are not filtered
|
||||||
|
const valid_tags = [_][]const u8{
|
||||||
|
"v1.0.0", "v2.1.3-stable",
|
||||||
|
// Note: v1.0.0-alpha.1 is now filtered to avoid duplicates
|
||||||
|
};
|
||||||
|
|
||||||
|
for (valid_tags) |tag| {
|
||||||
|
try std.testing.expect(!tag_filter.shouldSkipTag(allocator, tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ const json = std.json;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const ArrayList = std.ArrayList;
|
const ArrayList = std.ArrayList;
|
||||||
const utils = @import("../utils.zig");
|
const utils = @import("../utils.zig");
|
||||||
|
const tag_filter = @import("../tag_filter.zig");
|
||||||
|
|
||||||
const Release = @import("../main.zig").Release;
|
const Release = @import("../main.zig").Release;
|
||||||
const Provider = @import("../Provider.zig");
|
const Provider = @import("../Provider.zig");
|
||||||
|
@ -128,6 +129,11 @@ fn fetchReleasesMultiRepo(allocator: Allocator, client: *http.Client, token: []c
|
||||||
else
|
else
|
||||||
"1970-01-01T00:00:00Z";
|
"1970-01-01T00:00:00Z";
|
||||||
|
|
||||||
|
// Skip problematic tags
|
||||||
|
if (tag_filter.shouldSkipTag(allocator, tag_data.tag_name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Investigate annotated tags as the description here
|
// TODO: Investigate annotated tags as the description here
|
||||||
const release = Release{
|
const release = Release{
|
||||||
.repo_name = try std.fmt.allocPrint(allocator, "~{s}/{s}", .{ tag_data.username, tag_data.reponame }),
|
.repo_name = try std.fmt.allocPrint(allocator, "~{s}/{s}", .{ tag_data.username, tag_data.reponame }),
|
||||||
|
@ -600,3 +606,26 @@ test "sourcehut provider" {
|
||||||
|
|
||||||
try std.testing.expectEqualStrings("sourcehut", sourcehut_provider.getName());
|
try std.testing.expectEqualStrings("sourcehut", sourcehut_provider.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "SourceHut tag filtering" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
// Test that SourceHut now uses the same filtering as other providers
|
||||||
|
const problematic_tags = [_][]const u8{
|
||||||
|
"nightly", "prerelease", "latest", "edge", "canary", "dev-branch",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (problematic_tags) |tag| {
|
||||||
|
try std.testing.expect(tag_filter.shouldSkipTag(allocator, tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that valid tags are not filtered
|
||||||
|
const valid_tags = [_][]const u8{
|
||||||
|
"v1.0.0", "v2.1.3-stable",
|
||||||
|
// Note: v1.0.0-alpha.1 is now filtered to avoid duplicates
|
||||||
|
};
|
||||||
|
|
||||||
|
for (valid_tags) |tag| {
|
||||||
|
try std.testing.expect(!tag_filter.shouldSkipTag(allocator, tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
302
src/tag_filter.zig
Normal file
302
src/tag_filter.zig
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// Common tag filtering logic that can be used across all providers
|
||||||
|
pub fn shouldSkipTag(allocator: std.mem.Allocator, tag_name: []const u8) bool {
|
||||||
|
// Check if tag name contains common moving patterns
|
||||||
|
const tag_lower = std.ascii.allocLowerString(allocator, tag_name) catch return false;
|
||||||
|
defer allocator.free(tag_lower);
|
||||||
|
|
||||||
|
// First check if this looks like semantic versioning (v1.2.3-something or 1.2.3-something)
|
||||||
|
// If it does, we should be more careful about filtering
|
||||||
|
const is_semantic_versioning = isSemVer(tag_lower);
|
||||||
|
|
||||||
|
// List of common moving tags that should be filtered out
|
||||||
|
const moving_tags = [_][]const u8{
|
||||||
|
// common "latest commit tags"
|
||||||
|
"latest",
|
||||||
|
"tip",
|
||||||
|
"continuous",
|
||||||
|
"head",
|
||||||
|
|
||||||
|
// common branch tags
|
||||||
|
"main",
|
||||||
|
"master",
|
||||||
|
"trunk",
|
||||||
|
"develop",
|
||||||
|
"development",
|
||||||
|
"dev",
|
||||||
|
|
||||||
|
// common fast moving channel names
|
||||||
|
"nightly",
|
||||||
|
"edge",
|
||||||
|
"canary",
|
||||||
|
|
||||||
|
// common slower channels, but without version information
|
||||||
|
// they probably are not something we're interested in
|
||||||
|
"release",
|
||||||
|
"snapshot",
|
||||||
|
"unstable",
|
||||||
|
"experimental",
|
||||||
|
"prerelease",
|
||||||
|
"preview",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for exact matches with moving tags
|
||||||
|
for (moving_tags) |moving_tag|
|
||||||
|
if (std.mem.eql(u8, tag_lower, moving_tag))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Only filter standalone alpha/beta/rc if they're NOT part of semantic versioning
|
||||||
|
if (!is_semantic_versioning) {
|
||||||
|
const standalone_prerelease_tags = [_][]const u8{
|
||||||
|
"alpha",
|
||||||
|
"beta",
|
||||||
|
"rc",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (standalone_prerelease_tags) |tag|
|
||||||
|
if (std.mem.eql(u8, tag_lower, tag))
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// For semantic versioning, be more conservative and filter out prerelease versions
|
||||||
|
// since these are likely to be duplicates of releases that are already filtered
|
||||||
|
// by the releases API prerelease flag
|
||||||
|
if (containsPrereleaseIdentifier(tag_lower)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip pre-release and development tags
|
||||||
|
if (std.mem.startsWith(u8, tag_lower, "pre-") or
|
||||||
|
std.mem.startsWith(u8, tag_lower, "dev-") or
|
||||||
|
std.mem.startsWith(u8, tag_lower, "test-") or
|
||||||
|
std.mem.startsWith(u8, tag_lower, "debug-"))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a tag looks like semantic versioning
|
||||||
|
fn isSemVer(tag_lower: []const u8) bool {
|
||||||
|
// Look for patterns like:
|
||||||
|
// v1.2.3, v1.2.3-alpha.1, 1.2.3, 1.2.3-beta.2, etc.
|
||||||
|
|
||||||
|
var start_idx: usize = 0;
|
||||||
|
|
||||||
|
// Skip optional 'v' prefix
|
||||||
|
if (tag_lower.len > 0 and tag_lower[0] == 'v') {
|
||||||
|
start_idx = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start_idx >= tag_lower.len) return false;
|
||||||
|
|
||||||
|
// Look for pattern: number.number.number
|
||||||
|
var dot_count: u8 = 0;
|
||||||
|
var has_digit = false;
|
||||||
|
|
||||||
|
for (tag_lower[start_idx..]) |c| {
|
||||||
|
if (c >= '0' and c <= '9') {
|
||||||
|
has_digit = true;
|
||||||
|
} else if (c == '.') {
|
||||||
|
if (!has_digit) return false; // dot without preceding digit
|
||||||
|
dot_count += 1;
|
||||||
|
has_digit = false;
|
||||||
|
if (dot_count > 2) break; // we only care about major.minor.patch
|
||||||
|
} else if (c == '-' or c == '+') {
|
||||||
|
// This could be prerelease or build metadata
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// Invalid character for semver
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have at least 2 dots (major.minor.patch) and end with a digit
|
||||||
|
return dot_count >= 2 and has_digit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a semantic version contains prerelease identifiers
|
||||||
|
fn containsPrereleaseIdentifier(tag_lower: []const u8) bool {
|
||||||
|
// Look for common prerelease identifiers in semantic versioning
|
||||||
|
const prerelease_identifiers = [_][]const u8{
|
||||||
|
"-alpha",
|
||||||
|
"-beta",
|
||||||
|
"-rc",
|
||||||
|
"-pre",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (prerelease_identifiers) |identifier| {
|
||||||
|
if (std.mem.indexOf(u8, tag_lower, identifier) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We don't filter git-style version tags like v1.2.3-123-g1234567
|
||||||
|
// These are development versions but may be useful to track
|
||||||
|
// (e.g., kraftkit releases that should be included per user request)
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a release should be filtered based on prerelease/draft status
|
||||||
|
pub fn shouldSkipRelease(is_prerelease: bool, is_draft: bool) bool {
|
||||||
|
return is_prerelease or is_draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "shouldSkipTag filters common moving tags" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
// Test exact matches for moving tags
|
||||||
|
const moving_tags = [_][]const u8{
|
||||||
|
"latest",
|
||||||
|
"tip",
|
||||||
|
"continuous",
|
||||||
|
"head",
|
||||||
|
"main",
|
||||||
|
"master",
|
||||||
|
"trunk",
|
||||||
|
"develop",
|
||||||
|
"development",
|
||||||
|
"dev",
|
||||||
|
"nightly",
|
||||||
|
"edge",
|
||||||
|
"canary",
|
||||||
|
"release",
|
||||||
|
"snapshot",
|
||||||
|
"unstable",
|
||||||
|
"experimental",
|
||||||
|
"prerelease",
|
||||||
|
"preview",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (moving_tags) |tag| {
|
||||||
|
const should_skip = shouldSkipTag(allocator, tag);
|
||||||
|
try std.testing.expect(should_skip);
|
||||||
|
|
||||||
|
// Test case insensitive matching
|
||||||
|
const upper_tag = try std.ascii.allocUpperString(allocator, tag);
|
||||||
|
defer allocator.free(upper_tag);
|
||||||
|
const should_skip_upper = shouldSkipTag(allocator, upper_tag);
|
||||||
|
try std.testing.expect(should_skip_upper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test standalone alpha/beta/rc (should be filtered)
|
||||||
|
const standalone_prerelease = [_][]const u8{ "alpha", "beta", "rc" };
|
||||||
|
for (standalone_prerelease) |tag| {
|
||||||
|
try std.testing.expect(shouldSkipTag(allocator, tag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "shouldSkipTag filters prefix patterns" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
const prefix_patterns = [_][]const u8{
|
||||||
|
"pre-release",
|
||||||
|
"pre-1.0.0",
|
||||||
|
"dev-branch",
|
||||||
|
"dev-feature",
|
||||||
|
"test-build",
|
||||||
|
"test-123",
|
||||||
|
"debug-version",
|
||||||
|
"debug-info",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (prefix_patterns) |tag| {
|
||||||
|
const should_skip = shouldSkipTag(allocator, tag);
|
||||||
|
try std.testing.expect(should_skip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "shouldSkipTag allows valid version tags" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
const valid_tags = [_][]const u8{
|
||||||
|
"v1.0.0",
|
||||||
|
"v2.1.3",
|
||||||
|
"1.0.0",
|
||||||
|
"2.1.3-stable",
|
||||||
|
"v1.0.0-final",
|
||||||
|
"release-1.0.0",
|
||||||
|
"stable-v1.0.0",
|
||||||
|
"v1.0.0-lts",
|
||||||
|
"2023.1.0",
|
||||||
|
// Note: Semantic versioning prerelease tags are now filtered to avoid duplicates
|
||||||
|
// with the releases API, so they're not in this "valid" list anymore
|
||||||
|
};
|
||||||
|
|
||||||
|
for (valid_tags) |tag| {
|
||||||
|
const should_skip = shouldSkipTag(allocator, tag);
|
||||||
|
try std.testing.expect(!should_skip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "shouldSkipRelease filters prerelease and draft" {
|
||||||
|
// Test prerelease filtering
|
||||||
|
try std.testing.expect(shouldSkipRelease(true, false));
|
||||||
|
|
||||||
|
// Test draft filtering
|
||||||
|
try std.testing.expect(shouldSkipRelease(false, true));
|
||||||
|
|
||||||
|
// Test both prerelease and draft
|
||||||
|
try std.testing.expect(shouldSkipRelease(true, true));
|
||||||
|
|
||||||
|
// Test normal release
|
||||||
|
try std.testing.expect(!shouldSkipRelease(false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "semantic versioning detection" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
|
||||||
|
// Test that semantic versioning tags with alpha/beta/rc are now filtered
|
||||||
|
// (to avoid duplicates with releases API)
|
||||||
|
const semver_prerelease_tags = [_][]const u8{
|
||||||
|
"v1.0.0-alpha.1",
|
||||||
|
"v1.0.0-beta.2",
|
||||||
|
"v1.0.0-rc.1",
|
||||||
|
"1.0.0-alpha.1",
|
||||||
|
"2.0.0-beta.1",
|
||||||
|
"3.0.0-rc.1",
|
||||||
|
"v5.5.0-rc1",
|
||||||
|
"v0.5.0-alpha01",
|
||||||
|
"v1.12.0-beta3",
|
||||||
|
"v1.24.0-rc0",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (semver_prerelease_tags) |tag| {
|
||||||
|
const should_skip = shouldSkipTag(allocator, tag);
|
||||||
|
try std.testing.expect(should_skip); // Now these should be filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that git-style version tags are preserved (per user request for kraftkit)
|
||||||
|
const git_style_tags = [_][]const u8{
|
||||||
|
"v0.11.6-212-g74599361",
|
||||||
|
"v1.2.3-45-g1234567",
|
||||||
|
"v2.0.0-123-gabcdef0",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (git_style_tags) |tag| {
|
||||||
|
const should_skip = shouldSkipTag(allocator, tag);
|
||||||
|
try std.testing.expect(!should_skip); // These should NOT be filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that regular semantic versioning tags are preserved
|
||||||
|
const regular_semver_tags = [_][]const u8{
|
||||||
|
"v1.0.0",
|
||||||
|
"v2.1.3",
|
||||||
|
"1.0.0",
|
||||||
|
"2.1.3",
|
||||||
|
"v10.20.30",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (regular_semver_tags) |tag| {
|
||||||
|
const should_skip = shouldSkipTag(allocator, tag);
|
||||||
|
try std.testing.expect(!should_skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that standalone alpha/beta/rc are still filtered
|
||||||
|
const standalone_tags = [_][]const u8{ "alpha", "beta", "rc", "ALPHA", "Beta", "RC" };
|
||||||
|
for (standalone_tags) |tag| {
|
||||||
|
const should_skip = shouldSkipTag(allocator, tag);
|
||||||
|
try std.testing.expect(should_skip);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue