pre-release filtering
All checks were successful
Build and Release / build (push) Successful in 2m16s
Build and Release / sign (push) Successful in 2m6s

This commit is contained in:
Emil Lerch 2025-07-20 13:31:42 -07:00
parent a9a0e7e9f3
commit 6ad55474fa
Signed by: lobo
GPG key ID: A7B62D657EF764F8
6 changed files with 583 additions and 56 deletions

View file

@ -67,6 +67,7 @@ pub const Release = struct {
description: []const u8,
provider: []const u8,
is_tag: bool = false,
is_prerelease: bool = false, // Track if this is a prerelease/draft
pub fn deinit(self: Release, allocator: Allocator) void {
allocator.free(self.repo_name);
@ -205,8 +206,25 @@ pub fn main() !u8 {
// Sort all releases by published date (most recent first)
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
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);
// Write to output file
@ -218,12 +236,61 @@ pub fn main() !u8 {
_ = checkFileSizeAndWarn(atom_content.len);
// 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});
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 {
if (timestamp == 0) {
return try allocator.dupe(u8, "beginning of time");
@ -496,6 +563,7 @@ test {
std.testing.refAllDecls(@import("timestamp_tests.zig"));
std.testing.refAllDecls(@import("atom.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/GitLab.zig"));
std.testing.refAllDecls(@import("providers/SourceHut.zig"));

View file

@ -4,6 +4,7 @@ const json = std.json;
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const utils = @import("../utils.zig");
const tag_filter = @import("../tag_filter.zig");
const Release = @import("../main.zig").Release;
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;
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;
if (published_at_value != .string) continue;
@ -235,7 +243,7 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
const release = Release{
.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),
.html_url = try allocator.dupe(u8, html_url_value.string),
.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);
}
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));
}
}

View file

@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const Thread = std.Thread;
const utils = @import("../utils.zig");
const tag_filter = @import("../tag_filter.zig");
const Release = @import("../main.zig").Release;
const Provider = @import("../Provider.zig");
@ -488,6 +489,10 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
for (array.items) |item| {
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_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),
.provider = try allocator.dupe(u8, "github"),
.is_tag = false,
.is_prerelease = is_prerelease or is_draft, // Mark as prerelease if either flag is true
};
try releases.append(release);
@ -507,57 +513,9 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
return releases;
}
fn shouldSkipTag(allocator: std.mem.Allocator, tag_name: []const u8) bool {
// 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",
"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;
/// Wrapper function for backward compatibility and testing
pub fn shouldSkipTag(allocator: std.mem.Allocator, tag_name: []const u8) bool {
return tag_filter.shouldSkipTag(allocator, tag_name);
}
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;
// 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;
@ -1012,6 +970,114 @@ test "addNonReleaseTags should not add duplicate tags" {
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" {
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"}}]}}}}

View file

@ -4,6 +4,7 @@ const json = std.json;
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const utils = @import("../utils.zig");
const tag_filter = @import("../tag_filter.zig");
const Release = @import("../main.zig").Release;
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_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{
.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),
.html_url = try allocator.dupe(u8, obj.get("_links").?.object.get("self").?.string),
.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);
}
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));
}
}

View file

@ -4,6 +4,7 @@ const json = std.json;
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const utils = @import("../utils.zig");
const tag_filter = @import("../tag_filter.zig");
const Release = @import("../main.zig").Release;
const Provider = @import("../Provider.zig");
@ -128,6 +129,11 @@ fn fetchReleasesMultiRepo(allocator: Allocator, client: *http.Client, token: []c
else
"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
const release = Release{
.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());
}
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
View 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);
}
}