forgejo refactor

This commit is contained in:
Emil Lerch 2025-07-20 15:18:10 -07:00
parent e2199c2636
commit 00eacdf8fc
Signed by: lobo
GPG key ID: A7B62D657EF764F8
7 changed files with 142 additions and 57 deletions

View file

@ -37,7 +37,13 @@ Create a `config.json` file with your API tokens:
{ {
"github_token": "your_github_token", "github_token": "your_github_token",
"gitlab_token": "your_gitlab_token", "gitlab_token": "your_gitlab_token",
"codeberg_token": "your_codeberg_token", "forgejo": [
{
"name": "codeberg",
"base_url": "https://codeberg.org",
"token": "your_codeberg_access_token_here"
}
],
"sourcehut": { "sourcehut": {
"repositories": [ "repositories": [
"~sircmpwn/aerc", "~sircmpwn/aerc",
@ -52,7 +58,7 @@ Create a `config.json` file with your API tokens:
- **GitHub**: Create a Personal Access Token with and `user:read` scope. Classic is preferred (see note) - **GitHub**: Create a Personal Access Token with and `user:read` scope. Classic is preferred (see note)
- **GitLab**: Create a Personal Access Token with `read_api` scope - **GitLab**: Create a Personal Access Token with `read_api` scope
- **Codeberg**: Create an Access Token in your account settings - **Codeberg**: Create an Access Token in your account settings. read:repository and read:user
- **SourceHut**: No token required for public repositories. Specify repositories to track in the configuration. - **SourceHut**: No token required for public repositories. Specify repositories to track in the configuration.
Note on GitHub PATs. Some GitHub orgs will place additional restrictions on Note on GitHub PATs. Some GitHub orgs will place additional restrictions on

View file

@ -4,7 +4,7 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const integration = b.option(bool, "integration", "Run integration tests") orelse false; const integration = b.option(bool, "integration", "Run integration tests") orelse false;
const provider = b.option([]const u8, "provider", "Test specific provider (github, gitlab, codeberg, sourcehut)"); const provider = b.option([]const u8, "provider", "Test specific provider (github, gitlab, forgejo, sourcehut)");
const test_debug = b.option(bool, "test-debug", "Enable debug output in tests") orelse false; const test_debug = b.option(bool, "test-debug", "Enable debug output in tests") orelse false;
// Add Zeit dependency // Add Zeit dependency
@ -79,7 +79,7 @@ pub fn build(b: *std.Build) void {
// Individual provider test steps // Individual provider test steps
const github_step = b.step("test-github", "Test GitHub provider only"); const github_step = b.step("test-github", "Test GitHub provider only");
const gitlab_step = b.step("test-gitlab", "Test GitLab provider only"); const gitlab_step = b.step("test-gitlab", "Test GitLab provider only");
const codeberg_step = b.step("test-codeberg", "Test Codeberg provider only"); const forgejo_step = b.step("test-forgejo", "Test Forgejo provider only");
const sourcehut_step = b.step("test-sourcehut", "Test SourceHut provider only"); const sourcehut_step = b.step("test-sourcehut", "Test SourceHut provider only");
const github_tests = b.addTest(.{ const github_tests = b.addTest(.{
@ -106,17 +106,17 @@ pub fn build(b: *std.Build) void {
gitlab_test_debug_option.addOption(bool, "test_debug", test_debug); gitlab_test_debug_option.addOption(bool, "test_debug", test_debug);
gitlab_tests.root_module.addOptions("build_options", gitlab_test_debug_option); gitlab_tests.root_module.addOptions("build_options", gitlab_test_debug_option);
const codeberg_tests = b.addTest(.{ const forgejo_tests = b.addTest(.{
.name = "codeberg-tests", .name = "forgejo-tests",
.root_source_file = b.path("src/integration_tests.zig"), .root_source_file = b.path("src/integration_tests.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.filters = &[_][]const u8{"Codeberg provider"}, .filters = &[_][]const u8{"Forgejo provider"},
}); });
codeberg_tests.root_module.addImport("zeit", zeit_dep.module("zeit")); forgejo_tests.root_module.addImport("zeit", zeit_dep.module("zeit"));
const codeberg_test_debug_option = b.addOptions(); const forgejo_test_debug_option = b.addOptions();
codeberg_test_debug_option.addOption(bool, "test_debug", test_debug); forgejo_test_debug_option.addOption(bool, "test_debug", test_debug);
codeberg_tests.root_module.addOptions("build_options", codeberg_test_debug_option); forgejo_tests.root_module.addOptions("build_options", forgejo_test_debug_option);
const sourcehut_tests = b.addTest(.{ const sourcehut_tests = b.addTest(.{
.name = "sourcehut-tests", .name = "sourcehut-tests",
@ -132,6 +132,6 @@ pub fn build(b: *std.Build) void {
github_step.dependOn(&b.addRunArtifact(github_tests).step); github_step.dependOn(&b.addRunArtifact(github_tests).step);
gitlab_step.dependOn(&b.addRunArtifact(gitlab_tests).step); gitlab_step.dependOn(&b.addRunArtifact(gitlab_tests).step);
codeberg_step.dependOn(&b.addRunArtifact(codeberg_tests).step); forgejo_step.dependOn(&b.addRunArtifact(forgejo_tests).step);
sourcehut_step.dependOn(&b.addRunArtifact(sourcehut_tests).step); sourcehut_step.dependOn(&b.addRunArtifact(sourcehut_tests).step);
} }

View file

@ -1,7 +1,13 @@
{ {
"github_token": "ghp_your_github_personal_access_token_here", "github_token": "ghp_your_github_personal_access_token_here",
"gitlab_token": "glpat-your_gitlab_personal_access_token_here", "gitlab_token": "glpat-your_gitlab_personal_access_token_here",
"codeberg_token": "your_codeberg_access_token_here", "forgejo": [
{
"name": "codeberg",
"base_url": "https://codeberg.org",
"token": "your_codeberg_access_token_here"
}
],
"sourcehut": { "sourcehut": {
"token": "your_sourcehut_token_here", "token": "your_sourcehut_token_here",
"repositories": [ "repositories": [

View file

@ -2,6 +2,31 @@ const std = @import("std");
const json = std.json; const json = std.json;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
pub const ForgejoInstance = struct {
name: []const u8,
base_url: []const u8,
token: []const u8,
allocator: Allocator,
pub fn deinit(self: *const ForgejoInstance) void {
self.allocator.free(self.name);
self.allocator.free(self.base_url);
self.allocator.free(self.token);
}
};
pub const ForgejoConfig = struct {
instances: []ForgejoInstance,
allocator: Allocator,
pub fn deinit(self: *const ForgejoConfig) void {
for (self.instances) |*instance| {
instance.deinit();
}
self.allocator.free(self.instances);
}
};
pub const SourceHutConfig = struct { pub const SourceHutConfig = struct {
token: ?[]const u8 = null, token: ?[]const u8 = null,
repositories: [][]const u8, repositories: [][]const u8,
@ -19,7 +44,8 @@ pub const SourceHutConfig = struct {
pub const Config = struct { pub const Config = struct {
github_token: ?[]const u8 = null, github_token: ?[]const u8 = null,
gitlab_token: ?[]const u8 = null, gitlab_token: ?[]const u8 = null,
codeberg_token: ?[]const u8 = null, codeberg_token: ?[]const u8 = null, // Legacy support
forgejo: ?ForgejoConfig = null,
sourcehut: ?SourceHutConfig = null, sourcehut: ?SourceHutConfig = null,
allocator: Allocator, allocator: Allocator,
@ -27,6 +53,7 @@ pub const Config = struct {
if (self.github_token) |token| self.allocator.free(token); if (self.github_token) |token| self.allocator.free(token);
if (self.gitlab_token) |token| self.allocator.free(token); if (self.gitlab_token) |token| self.allocator.free(token);
if (self.codeberg_token) |token| self.allocator.free(token); if (self.codeberg_token) |token| self.allocator.free(token);
if (self.forgejo) |*forgejo_config| forgejo_config.deinit();
if (self.sourcehut) |*sh_config| sh_config.deinit(); if (self.sourcehut) |*sh_config| sh_config.deinit();
} }
}; };
@ -72,6 +99,28 @@ pub fn parseConfigFromJson(allocator: Allocator, json_content: []const u8) !Conf
}; };
} }
// Parse forgejo instances
var forgejo_config: ?ForgejoConfig = null;
if (root.get("forgejo")) |forgejo_obj| {
const forgejo_array = forgejo_obj.array;
var instances = try allocator.alloc(ForgejoInstance, forgejo_array.items.len);
for (forgejo_array.items, 0..) |instance_obj, i| {
const instance = instance_obj.object;
instances[i] = ForgejoInstance{
.name = try allocator.dupe(u8, instance.get("name").?.string),
.base_url = try allocator.dupe(u8, instance.get("base_url").?.string),
.token = try allocator.dupe(u8, instance.get("token").?.string),
.allocator = allocator,
};
}
forgejo_config = ForgejoConfig{
.instances = instances,
.allocator = allocator,
};
}
return Config{ return Config{
.github_token = if (root.get("github_token")) |v| switch (v) { .github_token = if (root.get("github_token")) |v| switch (v) {
.string => |s| if (s.len > 0) try allocator.dupe(u8, s) else null, .string => |s| if (s.len > 0) try allocator.dupe(u8, s) else null,
@ -88,6 +137,7 @@ pub fn parseConfigFromJson(allocator: Allocator, json_content: []const u8) !Conf
.null => null, .null => null,
else => null, else => null,
} else null, } else null,
.forgejo = forgejo_config,
.sourcehut = sourcehut_config, .sourcehut = sourcehut_config,
.allocator = allocator, .allocator = allocator,
}; };

View file

@ -6,7 +6,7 @@ const atom = @import("atom.zig");
const Release = @import("main.zig").Release; const Release = @import("main.zig").Release;
const GitHub = @import("providers/GitHub.zig"); const GitHub = @import("providers/GitHub.zig");
const GitLab = @import("providers/GitLab.zig"); const GitLab = @import("providers/GitLab.zig");
const Codeberg = @import("providers/Codeberg.zig"); const Forgejo = @import("providers/Forgejo.zig");
const SourceHut = @import("providers/SourceHut.zig"); const SourceHut = @import("providers/SourceHut.zig");
const config = @import("config.zig"); const config = @import("config.zig");
@ -436,24 +436,24 @@ test "GitLab provider integration" {
} }
} }
test "Codeberg provider integration" { test "Forgejo provider integration" {
const allocator = testing.allocator; const allocator = testing.allocator;
// Load config to get token // Load config to get token
const app_config = config.loadConfig(allocator, "config.json") catch |err| { const app_config = config.loadConfig(allocator, "config.json") catch |err| {
testPrint("Skipping Codeberg test - config not available: {}\n", .{err}); testPrint("Skipping Forgejo test - config not available: {}\n", .{err});
return; return;
}; };
defer app_config.deinit(); defer app_config.deinit();
if (app_config.codeberg_token == null) { if (app_config.codeberg_token == null) {
testPrint("Skipping Codeberg test - no token configured\n", .{}); testPrint("Skipping Forgejo test - no token configured\n", .{});
return; return;
} }
var provider = Codeberg.init(app_config.codeberg_token.?); var provider = Forgejo.init("codeberg", "https://codeberg.org", app_config.codeberg_token.?);
const releases = provider.fetchReleases(allocator) catch |err| { const releases = provider.fetchReleases(allocator) catch |err| {
testPrint("Codeberg provider error: {}\n", .{err}); testPrint("Forgejo provider error: {}\n", .{err});
return; // Skip test if provider fails return; // Skip test if provider fails
}; };
defer { defer {
@ -463,7 +463,7 @@ test "Codeberg provider integration" {
releases.deinit(); releases.deinit();
} }
testPrint("Codeberg: Found {} releases\n", .{releases.items.len}); testPrint("Forgejo: Found {} releases\n", .{releases.items.len});
// Verify releases have required fields // Verify releases have required fields
for (releases.items) |release| { for (releases.items) |release| {

View file

@ -6,8 +6,8 @@ const Thread = std.Thread;
const GitHub = @import("providers/GitHub.zig"); const GitHub = @import("providers/GitHub.zig");
const GitLab = @import("providers/GitLab.zig"); const GitLab = @import("providers/GitLab.zig");
const Codeberg = @import("providers/Codeberg.zig");
const SourceHut = @import("providers/SourceHut.zig"); const SourceHut = @import("providers/SourceHut.zig");
const ForgejoRegistry = @import("ForgejoRegistry.zig");
const atom = @import("atom.zig"); const atom = @import("atom.zig");
const config = @import("config.zig"); const config = @import("config.zig");
const zeit = @import("zeit"); const zeit = @import("zeit");
@ -136,7 +136,7 @@ pub fn main() !u8 {
// Initialize providers with their tokens (need to persist for the lifetime of the program) // Initialize providers with their tokens (need to persist for the lifetime of the program)
var github_provider: ?GitHub = null; var github_provider: ?GitHub = null;
var gitlab_provider: ?GitLab = null; var gitlab_provider: ?GitLab = null;
var codeberg_provider: ?Codeberg = null; var forgejo_registry: ?ForgejoRegistry = null;
var sourcehut_provider: ?SourceHut = null; var sourcehut_provider: ?SourceHut = null;
if (app_config.github_token) |token| { if (app_config.github_token) |token| {
@ -147,15 +147,32 @@ pub fn main() !u8 {
gitlab_provider = GitLab.init(token); gitlab_provider = GitLab.init(token);
try providers.append(gitlab_provider.?.provider()); try providers.append(gitlab_provider.?.provider());
} }
if (app_config.codeberg_token) |token| {
codeberg_provider = Codeberg.init(token); // Handle Forgejo instances (including legacy codeberg_token)
try providers.append(codeberg_provider.?.provider()); forgejo_registry = ForgejoRegistry.init(allocator, &app_config) catch |err| {
const stderr = std.io.getStdErr().writer();
stderr.print("Error initializing Forgejo registry: {}\n", .{err}) catch {};
return err;
};
if (forgejo_registry) |*registry| {
const forgejo_providers = try registry.providers();
defer registry.deinitProviders(forgejo_providers);
for (forgejo_providers) |provider| {
try providers.append(provider);
}
} }
if (app_config.sourcehut) |sh_config| if (sh_config.repositories.len > 0 and sh_config.token != null) { if (app_config.sourcehut) |sh_config| if (sh_config.repositories.len > 0 and sh_config.token != null) {
sourcehut_provider = SourceHut.init(sh_config.token.?, sh_config.repositories); sourcehut_provider = SourceHut.init(sh_config.token.?, sh_config.repositories);
try providers.append(sourcehut_provider.?.provider()); try providers.append(sourcehut_provider.?.provider());
}; };
// Cleanup forgejo registry when done
defer if (forgejo_registry) |*registry| {
registry.deinit();
};
// Fetch releases from all providers concurrently using thread pool // Fetch releases from all providers concurrently using thread pool
const provider_results = try fetchReleasesFromAllProviders(allocator, providers.items); const provider_results = try fetchReleasesFromAllProviders(allocator, providers.items);
defer { defer {
@ -567,5 +584,6 @@ test {
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"));
std.testing.refAllDecls(@import("providers/Codeberg.zig")); std.testing.refAllDecls(@import("providers/Forgejo.zig"));
std.testing.refAllDecls(@import("ForgejoRegistry.zig"));
} }

View file

@ -9,12 +9,18 @@ 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");
name: []const u8,
base_url: []const u8,
token: []const u8, token: []const u8,
const Self = @This(); const Self = @This();
pub fn init(token: []const u8) Self { pub fn init(name: []const u8, base_url: []const u8, token: []const u8) Self {
return Self{ .token = token }; return Self{
.name = name,
.base_url = base_url,
.token = token,
};
} }
pub fn provider(self: *Self) Provider { pub fn provider(self: *Self) Provider {
@ -27,8 +33,8 @@ pub fn fetchReleases(self: *Self, allocator: Allocator) !ArrayList(Release) {
var releases = ArrayList(Release).init(allocator); var releases = ArrayList(Release).init(allocator);
// Get starred repositories (Codeberg uses Gitea API) // Get starred repositories (uses Forgejo/Gitea API)
const starred_repos = try getStarredRepos(allocator, &client, self.token); const starred_repos = try getStarredRepos(allocator, &client, self.base_url, self.token);
defer { defer {
for (starred_repos.items) |repo| { for (starred_repos.items) |repo| {
allocator.free(repo); allocator.free(repo);
@ -39,9 +45,9 @@ pub fn fetchReleases(self: *Self, allocator: Allocator) !ArrayList(Release) {
// Get releases for each repo // Get releases for each repo
for (starred_repos.items) |repo| { for (starred_repos.items) |repo| {
// TODO: Investigate the tags/releases situation similar to GitHub // TODO: Investigate the tags/releases situation similar to GitHub
const repo_releases = getRepoReleases(allocator, &client, self.token, repo) catch |err| { const repo_releases = getRepoReleases(allocator, &client, self.base_url, self.token, self.name, repo) catch |err| {
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
stderr.print("Error fetching Codeberg releases for {s}: {}\n", .{ repo, err }) catch {}; stderr.print("Error fetching {s} releases for {s}: {}\n", .{ self.name, repo, err }) catch {};
continue; continue;
}; };
defer repo_releases.deinit(); defer repo_releases.deinit();
@ -56,11 +62,10 @@ pub fn fetchReleases(self: *Self, allocator: Allocator) !ArrayList(Release) {
} }
pub fn getName(self: *Self) []const u8 { pub fn getName(self: *Self) []const u8 {
_ = self; return self.name;
return "codeberg";
} }
fn getStarredRepos(allocator: Allocator, client: *http.Client, token: []const u8) !ArrayList([]const u8) { fn getStarredRepos(allocator: Allocator, client: *http.Client, base_url: []const u8, token: []const u8) !ArrayList([]const u8) {
var repos = ArrayList([]const u8).init(allocator); var repos = ArrayList([]const u8).init(allocator);
errdefer { errdefer {
// Clean up any allocated repo names if we fail // Clean up any allocated repo names if we fail
@ -78,7 +83,7 @@ fn getStarredRepos(allocator: Allocator, client: *http.Client, token: []const u8
const per_page: u32 = 100; const per_page: u32 = 100;
while (true) { while (true) {
const url = try std.fmt.allocPrint(allocator, "https://codeberg.org/api/v1/user/starred?limit={d}&page={d}", .{ per_page, page }); const url = try std.fmt.allocPrint(allocator, "{s}/api/v1/user/starred?limit={d}&page={d}", .{ base_url, per_page, page });
defer allocator.free(url); defer allocator.free(url);
const uri = try std.Uri.parse(url); const uri = try std.Uri.parse(url);
@ -99,15 +104,15 @@ fn getStarredRepos(allocator: Allocator, client: *http.Client, token: []const u8
if (req.response.status != .ok) { if (req.response.status != .ok) {
if (req.response.status == .unauthorized) { if (req.response.status == .unauthorized) {
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
stderr.print("Codeberg API: Unauthorized - check your token and scopes\n", .{}) catch {}; stderr.print("Forgejo API: Unauthorized - check your token and scopes\n", .{}) catch {};
return error.Unauthorized; return error.Unauthorized;
} else if (req.response.status == .forbidden) { } else if (req.response.status == .forbidden) {
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
stderr.print("Codeberg API: Forbidden - token may lack required scopes (read:repository)\n", .{}) catch {}; stderr.print("Forgejo API: Forbidden - token may lack required scopes (read:repository)\n", .{}) catch {};
return error.Forbidden; return error.Forbidden;
} }
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
stderr.print("Codeberg API request failed with status: {}\n", .{req.response.status}) catch {}; stderr.print("Forgejo API request failed with status: {}\n", .{req.response.status}) catch {};
return error.HttpRequestFailed; return error.HttpRequestFailed;
} }
@ -116,7 +121,7 @@ fn getStarredRepos(allocator: Allocator, client: *http.Client, token: []const u8
const parsed = json.parseFromSlice(json.Value, allocator, body, .{}) catch |err| { const parsed = json.parseFromSlice(json.Value, allocator, body, .{}) catch |err| {
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
stderr.print("Error parsing Codeberg starred repos JSON (page {d}): {}\n", .{ page, err }) catch {}; stderr.print("Error parsing Forgejo starred repos JSON (page {d}): {}\n", .{ page, err }) catch {};
return error.JsonParseError; return error.JsonParseError;
}; };
defer parsed.deinit(); defer parsed.deinit();
@ -152,7 +157,7 @@ fn getStarredRepos(allocator: Allocator, client: *http.Client, token: []const u8
return repos; return repos;
} }
fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8, repo: []const u8) !ArrayList(Release) { fn getRepoReleases(allocator: Allocator, client: *http.Client, base_url: []const u8, token: []const u8, provider_name: []const u8, repo: []const u8) !ArrayList(Release) {
var releases = ArrayList(Release).init(allocator); var releases = ArrayList(Release).init(allocator);
errdefer { errdefer {
// Clean up any allocated releases if we fail // Clean up any allocated releases if we fail
@ -162,7 +167,7 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
releases.deinit(); releases.deinit();
} }
const url = try std.fmt.allocPrint(allocator, "https://codeberg.org/api/v1/repos/{s}/releases", .{repo}); const url = try std.fmt.allocPrint(allocator, "{s}/api/v1/repos/{s}/releases", .{ base_url, repo });
defer allocator.free(url); defer allocator.free(url);
const uri = try std.Uri.parse(url); const uri = try std.Uri.parse(url);
@ -186,19 +191,19 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
if (req.response.status != .ok) { if (req.response.status != .ok) {
if (req.response.status == .unauthorized) { if (req.response.status == .unauthorized) {
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
stderr.print("Codeberg API: Unauthorized for repo {s} - check your token and scopes\n", .{repo}) catch {}; stderr.print("Forgejo API: Unauthorized for repo {s} - check your token and scopes\n", .{repo}) catch {};
return error.Unauthorized; return error.Unauthorized;
} else if (req.response.status == .forbidden) { } else if (req.response.status == .forbidden) {
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
stderr.print("Codeberg API: Forbidden for repo {s} - token may lack required scopes\n", .{repo}) catch {}; stderr.print("Forgejo API: Forbidden for repo {s} - token may lack required scopes\n", .{repo}) catch {};
return error.Forbidden; return error.Forbidden;
} else if (req.response.status == .not_found) { } else if (req.response.status == .not_found) {
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
stderr.print("Codeberg API: Repository {s} not found or no releases\n", .{repo}) catch {}; stderr.print("Forgejo API: Repository {s} not found or no releases\n", .{repo}) catch {};
return error.NotFound; return error.NotFound;
} }
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
stderr.print("Codeberg API request failed for repo {s} with status: {}\n", .{ repo, req.response.status }) catch {}; stderr.print("Forgejo API request failed for repo {s} with status: {}\n", .{ repo, req.response.status }) catch {};
return error.HttpRequestFailed; return error.HttpRequestFailed;
} }
@ -207,7 +212,7 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
const parsed = json.parseFromSlice(json.Value, allocator, body, .{}) catch |err| { const parsed = json.parseFromSlice(json.Value, allocator, body, .{}) catch |err| {
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
stderr.print("Error parsing Codeberg releases JSON for {s}: {}\n", .{ repo, err }) catch {}; stderr.print("Error parsing Forgejo releases JSON for {s}: {}\n", .{ repo, err }) catch {};
return error.JsonParseError; return error.JsonParseError;
}; };
defer parsed.deinit(); defer parsed.deinit();
@ -247,7 +252,7 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
.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),
.provider = try allocator.dupe(u8, "codeberg"), .provider = try allocator.dupe(u8, provider_name),
.is_tag = false, .is_tag = false,
}; };
@ -264,15 +269,15 @@ fn getRepoReleases(allocator: Allocator, client: *http.Client, token: []const u8
return releases; return releases;
} }
test "codeberg provider name" { test "forgejo provider name" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
_ = allocator; _ = allocator;
var codeberg_provider = init("dummy_token"); var forgejo_provider = init("codeberg", "https://codeberg.org", "dummy_token");
try std.testing.expectEqualStrings("codeberg", codeberg_provider.getName()); try std.testing.expectEqualStrings("codeberg", forgejo_provider.getName());
} }
test "codeberg release parsing with live data snapshot" { test "forgejo release parsing with live data snapshot" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
// Sample Codeberg API response for releases (captured from real API) // Sample Codeberg API response for releases (captured from real API)
@ -326,7 +331,7 @@ test "codeberg release parsing with live data snapshot" {
.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),
.provider = try allocator.dupe(u8, "codeberg"), .provider = try allocator.dupe(u8, "test-forgejo"),
.is_tag = false, .is_tag = false,
}; };
@ -348,13 +353,13 @@ test "codeberg release parsing with live data snapshot" {
))), ))),
releases.items[0].published_at, releases.items[0].published_at,
); );
try std.testing.expectEqualStrings("codeberg", releases.items[0].provider); try std.testing.expectEqualStrings("test-forgejo", releases.items[0].provider);
} }
test "Codeberg tag filtering" { test "Forgejo tag filtering" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
// Test that Codeberg now uses the same filtering as other providers // Test that Forgejo now uses the same filtering as other providers
const problematic_tags = [_][]const u8{ const problematic_tags = [_][]const u8{
"nightly", "prerelease", "latest", "edge", "canary", "dev-branch", "nightly", "prerelease", "latest", "edge", "canary", "dev-branch",
}; };