Skip to main content

A scriptable CLI for Forgejo/Gitea/Codeberg REST APIs.

Project description

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

  • issuecreate · get · list · edit · delete, plus label {add|remove|set|clear|list}, dep {add|remove} (blockers), and link (post a Refs/Depends/Closes comment).
  • prcreate · 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}.
  • commentcreate · list · edit · delete.
  • label — repo labels: create · list.
  • milestonecreate · 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).
  • releasecreate · list · get · get-tag · edit · delete.
  • release-assetlist · 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).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

forj-0.7.0.dev1.tar.gz (79.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

forj-0.7.0.dev1-py3-none-any.whl (45.6 kB view details)

Uploaded Python 3

File details

Details for the file forj-0.7.0.dev1.tar.gz.

File metadata

  • Download URL: forj-0.7.0.dev1.tar.gz
  • Upload date:
  • Size: 79.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for forj-0.7.0.dev1.tar.gz
Algorithm Hash digest
SHA256 3acfb236b3c4175f8f215cae9cf4e980fb0606e1c89eb0729cabd97194698206
MD5 822c23aa764e26d3ce36a93d2ceeedfb
BLAKE2b-256 c75401bf3b96a04552b82083d28f1ca287a6eab001f71d380ef2f26109addf04

See more details on using hashes here.

File details

Details for the file forj-0.7.0.dev1-py3-none-any.whl.

File metadata

  • Download URL: forj-0.7.0.dev1-py3-none-any.whl
  • Upload date:
  • Size: 45.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for forj-0.7.0.dev1-py3-none-any.whl
Algorithm Hash digest
SHA256 9bd78d9132572ac46f72243ca2a063009bf2788d17a4d962c8729326fc03df0e
MD5 3d8e3fbbb2d8a642f9efa3fcc503f67b
BLAKE2b-256 2d82793cb1eddb89956095b589401f720447d7476e2a84f1c5fb8605d0032e27

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page