No description
  • Zig 82.1%
  • Python 10%
  • Nix 6.2%
  • Shell 0.9%
  • PowerShell 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie c121617ec1
Some checks failed
release / release (push) Successful in 35m9s
release / winget (push) Successful in 1m46s
test / test (push) Failing after 57m8s
test / forgejo (push) Successful in 29m40s
Version 0.8.0
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tu1d8M5LY3LCaJ6ATanWTS
2026-10-23 00:40:12 -05:00
.forgejo/workflows Build the release on a larger runner 2026-10-23 00:40:12 -05:00
api Generate Forgejo API bindings from the vendored specification 2026-09-06 12:28:33 -05:00
dist/windows Install the completion scripts with each package 2026-10-23 00:38:27 -05:00
docs Complete the command line in fish, answered by zfj __complete 2026-10-23 00:00:14 -05:00
LICENSES Generate Forgejo API bindings from the vendored specification 2026-09-06 12:28:33 -05:00
src Complete instances, workflow files, files and directories 2026-10-23 00:26:58 -05:00
tests Install the completion scripts with each package 2026-10-23 00:38:27 -05:00
.gitignore Generate Forgejo API bindings from the vendored specification 2026-09-06 12:28:33 -05:00
AGENTS.md Move the agent docs to zfj, and add CONTRIBUTING.md and references 2026-10-13 20:47:55 -05:00
build.zig Recognize a run's zero time in whatever offset Forgejo writes it 2026-10-17 14:10:25 -05:00
build.zig.zon Version 0.8.0 2026-10-23 00:40:12 -05:00
build.zig.zon.nix Recognize a run's zero time in whatever offset Forgejo writes it 2026-10-17 14:10:25 -05:00
CONTRIBUTING.md Complete the command line in fish, answered by zfj __complete 2026-10-23 00:00:14 -05:00
flake.lock Regenerate build.zig.zon.nix with the current zon2nix 2026-10-10 11:07:25 -05:00
flake.nix Complete the command line in fish, answered by zfj __complete 2026-10-23 00:00:14 -05:00
GLOSSARY.md Complete the command line in fish, answered by zfj __complete 2026-10-23 00:00:14 -05:00
msi.nix Install the completion scripts with each package 2026-10-23 00:38:27 -05:00
package.nix Install the completion scripts with each package 2026-10-23 00:38:27 -05:00
README.md Install the completion scripts with each package 2026-10-23 00:38:27 -05:00
release.nix Install the completion scripts with each package 2026-10-23 00:38:27 -05:00
REUSE.toml Update openapi2zig and package its dependencies for Nix 2026-09-12 11:31:25 -05:00

zfj

Zig bindings for the Forgejo API. All 506 operations and 246 models of Forgejo v16.0.3, typed, with the response of every call parsed into the model the API documentation names for it — and zfj, a command-line tool built on them that can perform any of those operations.

Requires Zig 0.17. The last release for Zig 0.16 is v0.1.0, and the zig-0.16 branch carries it.

The bindings are generated, not written

This repository holds the API description — api/swagger.v1.json, taken from the Forgejo v16.0.3 release — and a small program that turns it into Zig. The build runs that program and compiles what it prints; no generated source is checked in, so the bindings cannot drift from the specification they came from.

Updating to a new Forgejo release is therefore a matter of replacing one file. See Updating the specification.

zig build leaves the generated source at zig-out/api.zig, which is the reference for what the library actually exposes. zig build docs renders the same thing as browsable documentation.

Using it

Add the dependency:

zig fetch --save https://git.jcollie.dev/jeff/zfj/archive/main.tar.gz

then wire the module up in build.zig:

const zfj = b.dependency("zfj", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("zfj", zfj.module("zfj"));

Operations are grouped the way the Forgejo API documentation groups them, one client per tag — IssueClient, RepositoryClient, UserClient, OrganizationClient, AdminClient, NotificationClient, PackageClient, SettingsClient, MiscellaneousClient, ActivitypubClient:

const std = @import("std");
const zfj = @import("zfj");

pub fn main(init: std.process.Init) !u8 {
    const allocator = init.gpa;

    var client = zfj.Client.init(allocator, init.io, init.environ_map.get("FORGEJO_TOKEN").?);
    defer client.deinit();
    client.withBaseUrl("https://codeberg.org/api/v1");

    var issues = zfj.IssueClient.init(&client);

    var issue = try issues.issueGetIssue(.{
        .owner = "forgejo",
        .repo = "forgejo",
        .index = 42,
    });
    defer issue.deinit();

    std.debug.print("{s}\n", .{issue.value().title.?});
    return 0;
}

Every operation is also a plain function taking the client as its first argument, so the call above can equally be written zfj.issueGetIssue(&client, .{ ... }). The tag clients are a grouping over those functions and nothing more.

Three forms of each operation are generated:

Form Returns For
issueGetIssue Owned(Issue) the usual case: a parsed model, or error.ResponseError
issueGetIssueResult ApiResult(Issue) when the failing status code matters
issueGetIssueRaw RawResponse when the bytes matter

Owned(T) owns both the response body and the parsed model; deinit() frees both, and value() borrows the model out of it. Model fields are optional and default to null, because the API omits rather than nulls what it has no value for.

An operation's path and query values are passed as one struct, its params, whose type is named after the operation: issueGetIssue takes an IssueGetIssueParams. An operation that sends a body takes it as a separate argument, typed as the model the API documentation names for it. Many of those are Forgejo's own …Option models, such as CreateIssueOption, and are never params.

Operation names are Forgejo's operationIds with the first letter lowercased, so they can be looked up in the API documentation as they are. The one exception is renderRawMarkdown, Forgejo's renderMarkdownRaw, whose id is already the name of renderMarkdown's raw form.

Operations whose only successful response is empty — the deletes, mostly — return !void.

RawResponse and Owned(T) both keep the response's headers, and header(name) finds one regardless of case — which is how Forgejo says how many results there are (X-Total-Count) and where the next page is (Link).

The specification's summaries and descriptions are doc comments on the operations, their params and the models, so they appear in zig build docs and in an editor. The same text is also available while a program runs: zfj.operation_info.<operation> is an OperationInfo holding the operation's method, path and summary, and for each of its params the location, description and allowed values.

Authentication

The token passed to Client.init is sent as Authorization: Bearer <token>. Forgejo accepts that spelling as well as the token <token> its documentation describes; both are read by the same code path. Pass an empty string to make requests unauthenticated.

The command-line tool

zig build also builds zig-out/bin/zfj. Every operation the bindings export is a command of its own, named after the operation:

$ zfj login codeberg https://codeberg.org
Token for https://codeberg.org (made under Settings, Applications):
Logged in to codeberg (https://codeberg.org) as someone.
codeberg is the default instance.
$ zfj op issueGetIssue --owner forgejo --repo forgejo --index 42

zfj op alone lists the operations, and zfj op <operation> --help says what an operation does, the request it makes, and each of its params with its description and, where the API documentation gives them, the values it accepts. The flags after an operation's name are exactly its params, spelled as the API documentation spells them — --page, --type, --color and all. The tool's own options, such as --instance, --debug and --color for the terminal, go before the command and nowhere else, so the two can never be confused.

An operation that sends a request body reads it from stdin as JSON, and the JSON is checked against the request body's model before anything is sent, so a misspelled field is caught locally:

$ echo '{"title": "It broke"}' | zfj op issueCreateIssue --owner me --repo thing

An operation that uploads a file takes it as a path, and sends it under the path's own name; - reads it from stdin instead, named by --name:

$ zfj op issueCreateIssueAttachment --owner me --repo thing --index 42 --attachment crash.log

A list operation returns one page at a time. With --paginate, zfj follows the Link header Forgejo sends from page to page and writes every page's items as one JSON array:

$ zfj --paginate op issueListIssues --owner forgejo --repo forgejo --state all

A successful response is written to stdout exactly as Forgejo sent it. It is also parsed into the model the API documentation names for it; if that fails, zfj warns on stderr, and with --strict it fails instead. An error response goes to stderr, so stdout only ever holds a successful one.

Exit status Meaning
0 success
1 the instance answered with an error status
2 a usage error: an unknown option, a missing param, a request body that does not parse
3 no instance to reach, the configuration file cannot be read, or the request got no answer
4 with --strict, a successful response that does not parse into its model
5 with --exit-status, a run that did not succeed

A reader that stops early, such as head in zfj run view 12 --log | head, is not an error: zfj stops writing and exits 0, saying nothing on stderr.

Working with issues

zfj issue holds commands shaped around tasks rather than single operations, spelled as fj's are:

$ zfj issue list --state all -l bug
$ zfj issue view 42 --comments
$ zfj issue view 42 --web
$ zfj issue create -t "It broke" -F report.md -l bug
$ zfj issue comment 42 -b "Fixed in 1a2b3c4."
$ zfj issue edit 42 --add-label needs-info --add-assignee someone
$ zfj issue close 42 -c "Fixed in 1a2b3c4."
$ zfj issue reopen 42 -c "Still happens on Windows."
$ zfj issue block 42 --by 41
$ zfj issue blockers 42

The repository is named with -R <owner>/<repo>. Without it, it is read off the clone's upstream remote, or failing that its origin, and the remote has to be on the instance being reached — which, unless --instance or ZFJ_INSTANCE names another, is the configured instance on the remote's host. Without -b or -F, and on a terminal, create and comment open $EDITOR for the text. close and reopen take a comment with -c or -F, post it before changing the issue's state, and without either leave no comment. view --web opens the issue in $BROWSER when that is set, and otherwise in the user's chosen browser, through zig-openuri: the desktop portal on Linux, falling back to xdg-open where there is no portal (over ssh, say), and ShellExecuteExW on Windows. On macOS it runs open.

On a terminal, lists are columns and an issue is shown with its body as Markdown, through $ZFJ_PAGER, $PAGER or less (on Windows, only $ZFJ_PAGER). The editor is $VISUAL or $EDITOR, else vi (notepad on Windows). Piped elsewhere, a list is one issue to a line with its number, state, title and labels separated by tabs. --json writes Forgejo's own JSON instead; view --comments --json adds the comment thread to the issue as a comments list, and list --json honors --paginate. zfj issue <command> --help says what each takes.

Pull requests

zfj pr opens them and reads them:

$ git push origin my-branch
$ zfj pr create -t "Fix the thing" -b "It was broken." -l bug
$ zfj pr list --state all -B main
$ zfj pr view 42 --comments
$ zfj pr diff 42
$ zfj pr diff 42 --patch | git am

create opens a pull request from a branch already pushed. The head is the branch checked out unless -H names another, and the base is the repository's default branch unless -B names another. In a clone of a fork, where origin is the fork and upstream the repository it came from, the pull request goes to upstream from the branch on origin. Without -b or -F, and on a terminal, $EDITOR is opened for the description.

list filters by -l <label>, -A <author>, -B <base branch> and -H <head branch>, and shows a merged pull request as merged where Forgejo says only closed; piped elsewhere it is one to a line with its number, state, title, head and base branches, and labels separated by tabs. view takes --comments, --json and --web as issue view does. diff writes the unified diff, or with --patch the commits for git am. A pull request is numbered from the same sequence as issues, so commenting on one, labeling it and closing it are the zfj issue commands.

Runs

zfj run follows the runs of a repository's Forgejo Actions workflows, spelled as gh run's are:

$ zfj run list
$ zfj run list -w release.yaml -s failure
$ zfj run list -c HEAD
$ zfj run view 43
$ zfj run view 43 --exit-status && echo passed
$ zfj run view 43 --log-failed
$ zfj run view 43 --log -j build --attempt 1
$ zfj run watch 44 --exit-status
$ zfj run cancel 44
$ zfj run delete 44

list shows the newest runs first, from every branch, twenty to a page. It filters by -s <status> and -e <event> (each repeatable), -w <workflow file>, -b <branch> and -c <commit>. Inside a clone, -c takes anything git can resolve to a commit, such as HEAD or a short hash; outside one, it has to be the full hash. On a terminal it shows columns. Piped elsewhere, it writes one run per line with its run number, status, title, workflow, event, branch or tag, and start time, separated by tabs. --json writes Forgejo's JSON for the runs and honors --paginate. --web opens the repository's runs in the browser instead, where the only filters are the workflow and a single status.

view shows a run's title, status, workflow, event, branch or tag, and commit; when and by whom it was started and how long it took; and its jobs, each with its status and attempt number. --json writes Forgejo's JSON for the run with its jobs added as a jobs list, and --web opens the run in the browser.

view --log shows each job's log in place of the summary: the latest attempt, under a line naming the job, and as far as the job has got if it is still running. A job no runner has picked up yet is listed with no log yet. --log-failed shows only the jobs that failed. -j limits either one to a single job, named by its name, or by its ID when two jobs share a name. --attempt <n>, which needs -j, shows an earlier attempt instead of the latest.

watch follows a run until it finishes, checking every three seconds, or every -i <seconds>. On a terminal it redraws the run and its jobs in place. Piped elsewhere, it writes one line each time the run or a job changes status, such as run 44: running or job build: failure. A run that has already finished is shown once.

view and watch take --exit-status, which makes zfj exit with status 5 unless the run succeeded. A run that hasn't finished yet hasn't succeeded either.

cancel cancels whatever jobs of a run are still waiting or running, and says so when the run had already finished, since Forgejo answers the same way either way. delete deletes a run that has finished. Forgejo refuses to delete one that hasn't, so cancel it first. Neither command asks before acting.

zfj workflow run starts a run of a workflow whose on includes workflow_dispatch, on the repository's default branch unless --ref names another branch or tag. Each -f <key>=<value> fills one of the workflow's inputs. It prints the new run's number and where to see it; --json writes what Forgejo answered instead. Forgejo's API has no way to list a repository's workflows, so run is the only workflow command.

$ zfj workflow run deploy.yaml -f environment=staging
Started run 44 of deploy.yaml on main
https://git.jcollie.dev/jeff/thing/actions/runs/44

A run is named by its run number, the one the web interface shows in /actions/runs/43, rather than by the ID the API's operations take. A number the repository doesn't have exits with status 1. See ADR 0005.

Working with labels

zfj label lists a repository's labels and creates them:

$ zfj label list
$ zfj label create needs-triage -c d4c5f9 -d "Maintainer needs to evaluate this issue"
$ zfj label create needs-triage -c d4c5f9 -d "Waiting to be looked at" --force

Forgejo lets a repository hold two labels of the same name, so create looks for the name first and refuses one that already exists; --force updates that label's color and description instead, which makes a script setting up a repository's labels safe to run again. A color is six hex digits, with or without a leading #. list always reads every page; on a terminal it shows columns, and piped elsewhere one label to a line with its name, color and description separated by tabs. --json writes Forgejo's JSON, and -R names the repository as it does for zfj issue.

Repositories

zfj repo works with repositories themselves:

$ zfj repo view
$ zfj repo view forgejo/forgejo
$ zfj repo view thing --web
$ zfj repo list
$ zfj repo list forgejo
$ zfj repo clone forgejo/forgejo
$ zfj repo clone thing ~/src/thing -p ssh -- --depth 1
$ zfj repo create thing -d "A thing" --license MIT --gitignore Zig --clone
$ zfj repo create my-org/thing --private
$ zfj repo fork forgejo/forgejo --clone
$ zfj repo fork --remote
$ zfj repo edit -d "A better thing" --website https://thing.example
$ zfj repo edit me/thing --archive --disable wiki,projects
$ zfj repo delete me/thing

Here the repository is what the command acts on, so it's the argument rather than -R: <owner>/<repo>, or a bare <repo> for one of the authenticated user's own, or nothing for the one the clone's upstream or origin remote names. See ADR 0006. view shows whether the repository is public, private or internal, whether it's a fork and of what, a mirror, a template or archived, its description, default branch and website, its stars, forks, open issues and open pull requests, and its https and ssh clone URLs. --json writes Forgejo's JSON, and --web opens the repository in the browser.

list alone lists the authenticated user's repositories and those of the organizations they belong to; list <owner> lists a user's or an organization's. It honors -L and --paginate as issue list does; piped elsewhere it is one repository to a line with its full name, visibility, what else it is (fork, mirror, template, archived, separated by commas) and description, separated by tabs.

clone runs git clone, handing it whatever follows a --. A fork's clone is given the fork's parent as its upstream remote, which is where zfj's issue and pull request commands go from a clone that has one. It clones over https, or over ssh with -p ssh; zfj login <name> <url> --protocol ssh makes ssh the instance's default, and logging in again without --protocol keeps whichever was chosen.

create <name> creates a repository of the authenticated user's own, and create <owner>/<name> one in that organization. It takes -d for a description, --private, --template, --default-branch, and --label-set for a set of issue labels such as Default. --init makes a first commit with a README; --readme, --license and --gitignore (commas separate several) choose the templates for it and imply --init. It prints the new repository's address, or with --json Forgejo's JSON. Nothing local changes unless --clone clones the new repository, or --remote adds it to the clone in the working directory as origin. Nothing is pushed.

fork forks a repository to the authenticated user, or with --org to an organization, and --name names the fork otherwise. A fork already there is no failure: it is found, however it is named, and used instead, so forking twice is the same as forking once. It prints the fork's address. Nothing local changes unless --clone clones the fork, with the repository as upstream, or --remote, in a clone of the repository, renames its origin to upstream and adds the fork as origin. Either way the clone's issue and pull request commands still go to the repository, and pr create opens pull requests to it from the fork.

edit changes only what it is given: --name renames the repository, -d, --website and --default-branch set those, --private or --public, --archive or --unarchive, and --template or --no-template set its state, and --enable and --disable turn its units on and off — issues, wiki, pull-requests, projects, releases, packages and actions, with commas separating several. The merge settings and the rest of what Forgejo can change are for zfj op repoEdit. A rename leaves the clone's remotes alone, since Forgejo redirects the old name, and says how to point them at the new one. edit names the repository as <owner>/<repo> or takes the clone's remote, but never takes a bare <repo>, so it cannot change one of the wrong owner's.

delete takes the repository only in full, as <owner>/<repo> — never a bare name, and never from the clone's remote — so that running it in the wrong directory cannot be enough to delete anything. On a terminal it asks for the full name to be typed back, and deletes nothing unless it matches exactly; anywhere else it deletes nothing without --yes, which also skips the question on a terminal.

Instances

zfj login <name> <url> asks for a token — or reads it from stdin, when that is not a terminal — checks it by asking the instance whose it is, and keeps it. zfj instances lists what it has kept, zfj default <name> chooses the instance reached when no other is named, and zfj logout <name> forgets one. They are kept in $XDG_CONFIG_HOME/zfj/config.zon — or ~/.config/zfj/config.zon, or on Windows %APPDATA%\zfj\config.zon — which holds the tokens, and so on Unix is written readable by its owner alone.

A command reaches the instance named by --instance, or failing that by ZFJ_INSTANCE, or failing that the configured instance on the host of the clone's git remote, or failing that the default. Either may be a name or a URL; a URL need not be configured, and one that is not is reached anonymously. FORGEJO_TOKEN, when it is set, is used in place of the chosen instance's token.

zfj --version names the tool's version and the Forgejo release its API description came from — zfj 0.2.0 (Forgejo 16.0.3). The bindings carry the latter as zfj.forgejo_version.

--debug logs each request and the status of its response to stderr, and never logs headers, so the token cannot appear there.

Shell completion

zfj completion <shell> prints a completion script for fish, bash, zsh or PowerShell:

$ zfj completion fish | source
$ zfj completion fish > ~/.config/fish/completions/zfj.fish
$ source <(zfj completion bash)
$ zfj completion bash > ~/.local/share/bash-completion/completions/zfj
$ source <(zfj completion zsh)
$ zfj completion zsh > "${fpath[1]}/_zfj"

PowerShell, Windows PowerShell 5.1 as well as PowerShell 7, loads it from $PROFILE:

zfj completion powershell | Out-String | Invoke-Expression

zsh's script is loaded after compinit, or found on $fpath as _zfj. Installed with Nix, zfj's package puts the fish, bash and zsh scripts where those shells look for them, and nothing needs loading by hand. Each release archive carries all four scripts in completions/, and the Windows installer puts zfj.ps1 beside zfj.exe, so that $PROFILE can load it as . "$env:ProgramFiles\zfj\zfj.ps1" rather than run zfj on every start. bash shows no descriptions beside the candidates; fish and zsh do, and PowerShell shows them as tooltips.

The script asks zfj itself what can come next, so it completes every command, option and option value zfj has, the operations of zfj op with their summaries, and those operations' params, and stays right as zfj changes. Global options are offered only before the command, where zfj accepts them. It also completes what zfj can find out without asking the instance: the configured instances' names for --instance, logout and default, the workflow files of the clone for workflow run and run list -w, files for -F and for an operation's file uploads, and directories for repo clone. Nothing is fetched from the instance while completing, so issue numbers, labels and the like are not completed. See ADR 0007.

Installing on Windows

Each release has an installer, zfj-<version>-x86_64-windows.msi, which installs zfj.exe for all users into Program Files\zfj and puts that directory on the PATH, so zfj runs from any newly opened shell. Installing a later release replaces the earlier one, and Apps & Features removes it. It is an x64 installer, and Windows on Arm runs it under emulation; the aarch64-windows archive below is the native build.

The same installer is in the JCOllie winget source, which is signed with a self-signed certificate. From an elevated PowerShell, this trusts that certificate and adds the source, once:

irm https://winget.jcollie.dev/install.ps1 | iex

after which zfj installs and upgrades like any other package:

winget install -s JCOllie JCOllie.Zfj
winget upgrade JCOllie.Zfj

jeff/winget-pkgs holds the source's manifests and says more about it.

Downloading a release

Each release on git.jcollie.dev carries a ready-built zfj for four targets:

Archive Runs on
zfj-<version>-x86_64-windows.zip 64-bit Windows on x86
zfj-<version>-aarch64-windows.zip 64-bit Windows on Arm
zfj-<version>-x86_64-linux-musl.tar.gz Linux on x86-64, any distribution
zfj-<version>-aarch64-linux-musl.tar.gz Linux on 64-bit Arm, any distribution

The Linux binaries are statically linked against musl and need nothing from the system. Each archive unpacks into one directory, zfj-<version>-<target>, holding the executable, this README, and the license. SHA256SUMS beside them lists the checksum of each archive and of the installer; with any of them downloaded into the same directory,

$ sha256sum -c --ignore-missing SHA256SUMS

checks whichever of them are there.

Where this lives

The repository's home is git.jcollie.dev, with a mirror on Tangled at https://tangled.org/jcollie.dev/zfj:

$ git clone https://git.jcollie.dev/jeff/zfj.git

Reporting issues and contributing

Report problems and send pull requests on the Tangled mirror, https://tangled.org/jcollie.dev/zfj. CONTRIBUTING.md says what a change needs before it goes in.

Cloning with Radicle

The repository is also published on the Radicle peer-to-peer network, where a repository is found by its Repository ID rather than by a server name:

rad:z2LTU53rqByAo7pj2BM1vJHeKYJSb

With a local Radicle node running, clone it with:

$ rad clone rad:z2LTU53rqByAo7pj2BM1vJHeKYJSb

That fetches the repository, checks out the default branch, and starts seeding it so other peers can fetch from you. To follow the repository without checking out a working copy, use rad seed instead:

$ rad seed rad:z2LTU53rqByAo7pj2BM1vJHeKYJSb

If you already have a clone from the git remote above, you can attach it to the same Radicle repository rather than cloning again:

$ git remote add rad rad://z2LTU53rqByAo7pj2BM1vJHeKYJSb
$ git fetch rad

Updating the specification

$ zig build update-spec

fetches Forgejo's latest release and rewrites api/swagger.v1.json from it. To take a particular release instead — an older one, or a newer one that is not yet the latest across all of Forgejo's parallel release branches — name its tag:

$ zig build update-spec -Dforgejo-version=v16.0.3

Then run zig build test. Nothing else in the repository names a Forgejo version, so that is the whole of it.

The step is the only thing here that reaches the network, and nothing else depends on it: an ordinary build never runs it. What it fetches is templates/swagger/v1_json.tmpl from the release tag, which is the document an instance renders and serves at /swagger.v1.json. Taking it from the tag rather than from a running instance is what makes the vendored file say which Forgejo it describes — an instance's copy is stamped with whatever build happens to be deployed there.

The template is plain JSON but for two Go placeholders, the instance's version and its sub-URL, and both are required to be present. If a future Forgejo renames one or adds a third, the step fails rather than vendoring a file with a template expression left in the middle of it. What it fetched is also parsed and checked over — that it is JSON, that it is Swagger 2.0, that its version is the release asked for, and that it describes any paths at all — before it is allowed to replace the file in the source tree.

Building with Nix

$ nix build .#zfj

builds the bindings and the command-line tool and runs the tests without touching the network, leaving zfj in result/bin/ and the generated api.zig in result/. The Zig dependencies come from build.zig.zon.nix, which zon2nix generates from build.zig.zon; regenerate it whenever a dependency changes:

$ nix develop -c zon2nix --17 --nix=build.zig.zon.nix build.zig.zon

The same dependencies are exposed on their own as .#zig-deps, for handing to zig build --system. openapi2zig declares a .path dependency of its own, which --system cannot build from the store, so package.nix forks it from a copy inside the build root, as the zon2nix README describes.

nix run .# -- <arguments> runs the tool.

The release builds are packages too: .#zfj-x86_64-windows, .#zfj-aarch64-windows, .#zfj-x86_64-linux-musl and .#zfj-aarch64-linux-musl cross-build the stripped executable, and .#msi builds the Windows installer, with msitools' wixl from dist/windows/zfj.wxs, and .#release gathers everything a release publishes: the four archives, the installer, and SHA256SUMS. On x86-64 Linux,

$ nix build -L .#checks.x86_64-linux.windows

runs the Windows executable under Wine and checks that it answers the way the native build does and finds its configuration under %APPDATA%, then installs the MSI with msiexec, runs the installed zfj.exe, and uninstalls it. It does not reach the network, because Wine leaves out a structure that Zig's standard library reads while resolving a host name.

$ nix build -L .#checks.x86_64-linux.forgejo

runs the tool against a real Forgejo in a NixOS virtual machine — logging in, plumbing calls checked against their documented models, pagination, and every zfj issue command — and needs KVM.

References cited

License

MIT, and the repository follows the REUSE specification — reuse lint checks it. api/swagger.v1.json is Forgejo's own, distributed by them under the MIT license for the purpose of interoperability.