Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

forj

A scriptable command-line client for Forgejo / Gitea / Codeberg REST APIs — issues, pull requests, comments, reviews, labels, releases, and release assets. Every operation is one unambiguous command, so a person or a bot never has to hand-write a REST call.

  • Typed payloads, one dependency. The REST request/response bodies are Pydantic models (forj's only runtime dependency) — so responses are parsed and validated, not dict-wrangled. Everything else is the standard library (urllib, json, argparse, tomllib); runs on Python ≥ 3.11.
  • Repo-agnostic. The target forge, repository, and credentials all come from the environment — nothing is hard-coded — so one install drives any Forgejo/Gitea host.
  • Safe by default. FORJ_DRY_RUN=1 previews any request without sending it, and destructive deletes are doubly gated (--yes and FORJ_ALLOW_DELETE).

Why

The token you get from a Codeberg repo (write:repository + write:issue) is enough to file issues, open PRs, review, and cut releases — but tea's login demands the read:user scope, which Codeberg won't grant to a repo-scoped token. forj talks to the REST API directly and sends the token only in the Authorization header, so a plain repo-scoped token is all it needs.

forj was extracted from ZulipToSignal's single-file scripts/forge.py into a standalone, MVC-structured package.

Install

pip install -e .          # from a clone; add '.[dev]' for the test/lint tooling

This installs the forj console script (also runnable as python -m forj).

Configure

forj reads its target and credentials from the environment:

Variable Required Default Purpose
FORJ_REPO yes — The owner/name repository to operate on
FORJ_TOKEN for live calls — Access token (sent only in the Authorization header). Also read from ~/.config/forj/token
FORJ_API_ROOT no https://codeberg.org/api/v1 REST API root of the forge
FORJ_BASE_BRANCH no main Default base branch for pr create
FORJ_DRY_RUN no off Preview requests on stderr without sending them (no token read, no network)
FORJ_ALLOW_DELETE no off The environment half of the two-key delete gate
FORJ_CHANGELOG no docs/CHANGELOG.md Changelog read by release create --notes-from-changelog
FORJ_CONFIG no ~/.config/forj/config.toml Multi-token config for least-privilege / differently-scoped tokens

A boolean flag (FORJ_DRY_RUN, FORJ_ALLOW_DELETE) is on for any value other than 0/false/no/off/empty (case-insensitive).

export FORJ_REPO=owner/name
export FORJ_TOKEN=…           # or put it in ~/.config/forj/token

Multiple tokens (preferred for anything non-trivial). When one token can't cover everything — e.g. the Packages API needs a write:package scope a repo-scoped token can't carry — describe each token in ~/.config/forj/config.toml; forj selects the narrowest match (repo, then owner, then the default entry) that carries the scope an operation needs. See CONTRIBUTING → Setup for the format.

Where does the repo come from? The repository an operation acts on is always FORJ_REPO (owner/name) — that's the single target. A config entry's repos and owners lists are the opposite direction: they say which configured token applies to which targets, so forj can pick the right token for the FORJ_REPO (and its owner) it's acting on. --owner on package commands overrides the owner for that call only.

Usage

forj issue list                              # open issues, with inline metadata
forj issue list --milestone v0.2.0 --label bug --unblocked   # filter by metadata
forj pr list --author alice --draft          # filter PRs too
forj issue create "Title" "Body"
forj issue edit 12 --milestone v0.2.0 --assignees alice,bob
forj issue label add 12 bug enhancement      # labels resolve by name or id
forj issue link 12 5 6 --kind depends        # 'Depends on #5, #6' + a real blocker
forj issue dep add 12 3 --repo other/repo    # cross-repo blocker (issue in another repo)

forj pr create feat/x "Add X" "Closes #12"   # head -> base (default: main)
forj pr status 34
forj pr review create 34 --event REQUEST_CHANGES \
     --comment "src/app.py:42:needs a guard here"
forj pr review comments 34                    # all active (unresolved) inline comments
forj pr merge 34 squash

forj milestone create v0.2.0 --due-date 2026-08-01
forj milestone assign v0.2.0 12 15 34         # bulk-assign a milestone to issues/PRs
forj milestone move v0.1.0 v0.2.0             # move every issue+PR onto another milestone
forj issue edit 12 --unset-milestone          # clear one item's milestone
forj pr create feat/x "Fix" "Closes #12" --inherit-milestone  # PR inherits #12's milestone
forj release create v0.1.0 --notes-from-changelog
FORJ_DRY_RUN=1 forj issue delete 12          # preview a gated op safely

forj api repos/{owner}/{repo}/branches        # raw passthrough (any endpoint)
forj api repos/{owner}/{repo}/issues -F index=5 -f title=Hi   # POST typed/string fields

forj --json issue get 12                     # machine-readable JSON (also FORJ_JSON=1)
forj --json pr status 34 | jq .mergeable     # scriptable structured output

Any list subcommand takes -v/--verbose to expand each item to its full metadata. issue list / pr list also filter by metadata: --label (repeatable), --author, --milestone, plus issue list --assignee / --blocked / --unblocked and pr list --draft / --ready.

The global --json flag (or FORJ_JSON=1) switches every command to machine-readable JSON: reads/lists/creates emit their parsed model(s), and mutations emit an {"ok": true, "message": ...} status object — so output is stable for scripts and LLM tooling. Human text is the default. forj --version prints the installed version.

Command surface

  • issue — create · get · list · edit · delete, plus label {add|remove|set|clear|list}, dep {add|remove} (blockers), and link (post a Refs/Depends/Closes comment).
  • pr — create · list · status · edit · delete · merge · draft · ready · retarget · update, plus the shared label {…} sub-group, reviewer {add|remove}, and review {create|list|get|comments|submit|dismiss|delete}.
  • comment — create · list · edit · delete.
  • label — repo labels: create · list.
  • milestone — create · list · get · edit · assign · unassign · move · close · reopen · delete (get/edit/assign/move/close/reopen/delete take a milestone id or title, resolved like --milestone; assign <ms> <n…> bulk-assigns it to issues/PRs, unassign <n…> clears theirs, move <from> <to> sweeps every issue and PR onto another milestone, and close/reopen are shorthands for edit --state). get --issues lists a milestone's issues and PRs. Clear a single item's milestone with issue/pr edit --unset-milestone; pr create --inherit-milestone sets a new PR's milestone from the first issue its body closes. A milestone's open/closed counts include PRs (Forgejo models PRs as issues), and assigning to a closed milestone is allowed (with a soft note).
  • release — create · list · get · get-tag · edit · delete.
  • release-asset — list · get · upload · delete.
  • package — owner-scoped packages: list · get · delete (needs a read:package/write:package token via ~/.config/forj/config.toml).
  • delete-branch — delete a remote branch (e.g. after its PR merges).
  • api — raw REST passthrough to any endpoint forj doesn't wrap (-X method, -F/-f fields, --input body, -H headers; {owner}/{repo} resolve from FORJ_REPO). The escape hatch — unguarded raw access.

Run forj --help (or forj <entity> --help) for the full tree. Deletes (issue/pr/comment/pr review/milestone/release/release-asset/package delete) additionally require --yes and FORJ_ALLOW_DELETE.

Development

See CONTRIBUTING.md for setup (make install), the quality gates, the module/architecture map, and the git workflow. AI-agent-specific policies are in AGENTS.md. Licensed MIT (see pyproject.toml).

Metadata

Release files for forj 0.7.0.dev1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for forj 0.7.0.dev1
File Size Uploaded
forj-0.7.0.dev1.tar.gz 79.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for forj 0.7.0.dev1
File Interpreter ABI Platform
forj-0.7.0.dev1-py3-none-any.whl Python 3 none any Details

Total release size: 125.2 kB

Release files / forj-0.7.0.dev1.tar.gz

Download URL forj-0.7.0.dev1.tar.gz
Size 79.6 kB
Tags Source
SHA-256 checksum
How to use checksums
3acfb236b3c4175f8f215cae9cf4e980fb0606e1c89eb0729cabd97194698206
BLAKE2b-256 checksum
How to use checksums
c75401bf3b96a04552b82083d28f1ca287a6eab001f71d380ef2f26109addf04
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Release files / forj-0.7.0.dev1-py3-none-any.whl

Download URL forj-0.7.0.dev1-py3-none-any.whl
Size 45.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9bd78d9132572ac46f72243ca2a063009bf2788d17a4d962c8729326fc03df0e
BLAKE2b-256 checksum
How to use checksums
2d82793cb1eddb89956095b589401f720447d7476e2a84f1c5fb8605d0032e27
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.7.0.dev1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page