Skip to content

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/latest

Response 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/latest

Response 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

Released under the MIT License.