Custom Providers
updater.zig supports custom Git hosting providers, enabling you to use self-hosted Gitea, GitLab, Forgejo, or any other Git hosting solution.
Built-in Self-hosted Providers
Self-hosted Gitea/Forgejo
zig
const myGitea = updater.providers.gitea("https://git.mycompany.com");
const result = try updater.checkForUpdates(allocator, .{
.provider = myGitea,
.owner = "team",
.repo = "project",
.current_version = "1.0.0",
});Self-hosted GitLab
zig
const myGitlab = updater.providers.selfHostedGitlab("https://gitlab.mycompany.com");
const result = try updater.checkForUpdates(allocator, .{
.provider = myGitlab,
.owner = "team",
.repo = "project",
.current_version = "1.0.0",
});Creating a Fully Custom Provider
For Git hosting solutions with non-standard APIs, create a fully custom provider:
zig
const myProvider = updater.providers.custom(
"my-git-server", // Provider name
buildCustomUrl, // URL builder function
parseCustomResponse, // Response parser function
&customHeaders, // HTTP headers
);Complete Example
zig
const std = @import("std");
const updater = @import("updater");
// Custom headers
const customHeaders = [_]updater.HttpHeader{
.{ .name = "Accept", .value = "application/json" },
.{ .name = "User-Agent", .value = "my-app/1.0" },
.{ .name = "X-Api-Key", .value = "secret-key" },
};
// URL builder function
fn buildCustomUrl(
alloc: std.mem.Allocator,
owner: []const u8,
repo: []const u8,
) std.mem.Allocator.Error![]const u8 {
return std.fmt.allocPrint(
alloc,
"https://git.example.com/api/v1/repos/{s}/{s}/releases/latest",
.{ owner, repo },
);
}
// Response parser function
fn parseCustomResponse(
alloc: std.mem.Allocator,
body: []const u8,
) updater.GitProvider.ParseError!updater.ReleaseInfo {
const parsed = std.json.parseFromSlice(
std.json.Value,
alloc,
body,
.{},
) catch return error.InvalidJson;
defer parsed.deinit();
const root = parsed.value;
if (root != .object) return error.InvalidFormat;
// Extract version tag
const tag_field = root.object.get("version") orelse return error.MissingField;
if (tag_field != .string) return error.InvalidFormat;
const tag = alloc.dupe(u8, tag_field.string) catch return error.OutOfMemory;
errdefer alloc.free(tag);
// Extract release URL
const url_field = root.object.get("download_url") orelse return error.MissingField;
if (url_field != .string) return error.InvalidFormat;
const url = alloc.dupe(u8, url_field.string) catch return error.OutOfMemory;
return updater.ReleaseInfo{
.tag = tag,
.url = url,
};
}
// Create and use the provider
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const myProvider = updater.providers.custom(
"my-git-server",
buildCustomUrl,
parseCustomResponse,
&customHeaders,
);
const result = try updater.checkForUpdates(allocator, .{
.provider = myProvider,
.owner = "team",
.repo = "project",
.current_version = "1.0.0",
});
if (result.has_update) {
std.debug.print("Update: {s}\n", .{result.latest_version.?});
}
}The GitProvider Interface
zig
pub const GitProvider = struct {
/// Human-readable name of the provider
name: []const u8,
/// Build the URL used to fetch the latest release or tag
buildLatestUrl: *const fn (
allocator: std.mem.Allocator,
owner: []const u8,
repo: []const u8,
) std.mem.Allocator.Error![]const u8,
/// Parse the API response body and extract release information
parseLatest: *const fn (
allocator: std.mem.Allocator,
body: []const u8,
) ParseError!ReleaseInfo,
/// Optional request headers
headers: []const HttpHeader = &.{},
/// Base URL for self-hosted instances
base_url: ?[]const u8 = null,
/// Include prereleases
include_prereleases: bool = false,
pub const ParseError = error{
InvalidJson,
MissingField,
InvalidFormat,
OutOfMemory,
};
};The ReleaseInfo Structure
zig
pub const ReleaseInfo = struct {
tag: []const u8, // Version tag (required)
url: []const u8, // Release URL (required)
name: ?[]const u8 = null, // Release name (optional)
body: ?[]const u8 = null, // Release description (optional)
published_at: ?[]const u8 = null, // Publication date (optional)
prerelease: bool = false, // Is this a prerelease?
draft: bool = false, // Is this a draft?
pub fn deinit(self: *ReleaseInfo, allocator: std.mem.Allocator) void {
allocator.free(self.tag);
allocator.free(self.url);
if (self.name) |n| allocator.free(n);
if (self.body) |b| allocator.free(b);
if (self.published_at) |p| allocator.free(p);
}
};Common Provider Patterns
Gitea-compatible APIs
Many Git forges (Forgejo, Gogs, etc.) use Gitea-compatible APIs:
GET /api/v1/repos/{owner}/{repo}/releases/latestResponse format:
json
{
"tag_name": "v1.0.0",
"html_url": "https://git.example.com/owner/repo/releases/v1.0.0",
"name": "Release 1.0.0",
"body": "Release notes...",
"prerelease": false
}GitLab-compatible APIs
GET /api/v4/projects/{owner}%2F{repo}/releases/permalink/latestResponse format:
json
{
"tag_name": "v1.0.0",
"_links": {
"self": "https://gitlab.example.com/project/-/releases/v1.0.0"
},
"name": "Release 1.0.0",
"description": "Release notes..."
}Authentication
For private repositories, add authentication headers:
zig
const privateHeaders = [_]updater.HttpHeader{
.{ .name = "Accept", .value = "application/json" },
.{ .name = "Authorization", .value = "Bearer YOUR_TOKEN" },
};
const myProvider = updater.providers.custom(
"private-git",
buildUrl,
parseResponse,
&privateHeaders,
);Testing Custom Providers
Test your provider with mock responses:
zig
test "custom provider parses response" {
const allocator = std.testing.allocator;
const mock_response =
\\{
\\ "version": "v2.0.0",
\\ "download_url": "https://example.com/download/v2.0.0"
\\}
;
var info = try parseCustomResponse(allocator, mock_response);
defer info.deinit(allocator);
try std.testing.expectEqualStrings("v2.0.0", info.tag);
}Next Steps
- API Reference - Complete API documentation
- Safety and Opt-out - Privacy controls