Forge Documentation A product of RHost

Forge documentation

Source control, code review and pipelines from RHost. Everything runs on our own servers — no external service holds our code, and the software makes no outbound calls except where noted.

First steps

Create a repository from New repository, then push an existing project into it:

git remote add origin git@forge.rhost.rw:<you>/<repo>.git
git push -u origin main

Use the SSH form above and you will never be asked for a password. The HTTPS form works too, but prompts unless you set up a token.

SSH keys — the no-password setup

Forge's SSH service listens on port 2222, not 22. Add this once to ~/.ssh/config and short GitHub-style URLs work:

Host forge.rhost.rw
    HostName forge.rhost.rw
    Port 2222
    User git
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

Create a key if you do not have one, then add the public half under Settings → SSH keys:

ssh-keygen -t ed25519 -C "you@rhost.rw"
cat ~/.ssh/id_ed25519.pub     # paste this, never the file without .pub

Check it works:

ssh -T git@forge.rhost.rw
git clone git@forge.rhost.rw:<you>/<repo>.git
Already cloned over HTTPS? Switch an existing checkout with git remote set-url origin git@forge.rhost.rw:<you>/<repo>.git

HTTPS and access tokens

On a machine without your key, use HTTPS with a token — never your account password. Tokens are scoped and can be revoked individually.

Create one under Settings → Applications, then at the prompt enter your username and paste the token as the password. To stop being asked every time:

# macOS
git config --global credential.helper osxkeychain
# Linux
git config --global credential.helper 'cache --timeout=86400'

Or let the CLI answer for you — see Log in.

Install the command line tool

forge is the official CLI for Forge: issues, pull requests, releases, repositories and administration from the terminal.

There are only two commands you ever install: forge and forge-update, both on your own machine. Anything else with "forge" in the name — forge-runner, forge-docker and the rest — runs on the server and is the administrator's concern, not yours.
# macOS (Apple Silicon) — adjust the filename for your platform
curl -fLo forge https://forge.rhost.rw/assets/cli/forge-darwin-arm64
chmod +x forge && sudo mv forge /usr/local/bin/
forge --version

Verify the download against SHA256SUMS:

curl -fLO https://forge.rhost.rw/assets/cli/SHA256SUMS
shasum -a 256 -c SHA256SUMS --ignore-missing
macOS Gatekeeper. The binary is unsigned, so the first run may be blocked. Allow it with xattr -d com.apple.quarantine /usr/local/bin/forge, or via System Settings → Privacy & Security.

Log in

Create a token under Settings → Applications, then:

forge login add --name forge --url https://forge.rhost.rw --token <TOKEN>
forge whoami
forge login list

Let the CLI answer git's HTTPS credential prompts as well — this writes a credential helper into your ~/.gitconfig:

forge login helper setup

Inside a repository checkout the CLI detects the server and repo automatically. Outside one, pass --login forge and --repo <owner>/<name>.

Repositories

forge repos list
forge repos search <term>
forge repos create --name my-project --private
forge repos fork --repo owner/name
forge clone owner/name           # clones using your configured login
forge open                       # open the current repo in a browser

Issues

forge issues list
forge issues list --state closed --labels bug
forge issues 42                       # show one issue
forge issues create --title "Login fails" --body "Steps to reproduce…"
forge issues close 42
forge issues reopen 42
forge comments 42                     # read the thread
forge comments add 42 --body "Fixed in 1.2.0"

Pull requests

forge pulls list
forge pulls create --title "Add search" --base main --head feature/search
forge pulls checkout 17               # check the PR branch out locally
forge pulls review 17                 # approve / comment / request changes
forge pulls merge 17
forge pulls close 17
forge pulls checkout is the one people miss most. It fetches the PR branch and switches to it in one step, which is far quicker than doing it by hand.

Releases, labels, milestones, more

forge releases list
forge releases create --tag v1.2.0 --title "1.2.0" --note "Changelog…"
forge releases create --tag v1.2.0 --asset ./dist/app.tar.gz
forge labels list
forge labels create --name urgent --color e11d21
forge milestones list
forge branches list
forge wiki list
forge webhooks list
forge times list                       # tracked time on issues and PRs
forge notifications                    # what needs your attention

Raw API access

Anything the CLI does not wrap, call directly — authentication is handled for you:

forge api /user
forge api /repos/<owner>/<repo>/branches
forge api --method POST /repos/<owner>/<repo>/issues \
     --data '{"title":"From the API"}'

Verifying a download

Every release ships checksums.txt. An interrupted download can produce a file of exactly the right size with corrupt content, so check before installing:

shasum -a 256 forge-0.1.0-darwin-arm64      # macOS
sha256sum forge-0.1.0-linux-amd64          # Linux

Keeping the CLI up to date

forge-update            # update if a newer release exists
forge-update --check    # report only, change nothing
forge-update --force    # reinstall the current version

It picks the build for your platform, verifies it against the published checksums, clears the macOS quarantine flag and replaces the binary in place. A checksum mismatch aborts without touching your installation.

When something does not work

SymptomCause and fix
Still asked for a username and passwordThe remote is HTTPS with no helper configured. Run forge login helper setup, or switch to SSH: git remote set-url origin git@forge.rhost.rw:owner/repo.git
Permission denied (publickey)Your key is not on your account, or the URL omits the port. Forge's SSH listens on 2222 — use the ~/.ssh/config entry above.
macOS refuses to open the binaryQuarantine on a downloaded, non-notarised file: xattr -d com.apple.quarantine /usr/local/bin/forge
no available loginNo server configured — run forge login add.
Push rejected, PolinRider indicatorsPush scanning found a known supply-chain marker. The message names the file — inspect it before doing anything else.
Push rejected, protected branchThe branch forbids force-push or direct push. Open a pull request instead.

Full command reference

GroupCommandsPurpose
Entitiesissues pulls labels milestones releases times organizations repos branches actions wiki webhooks commentsList, create and update everything in a repository
Helpersopen notifications clone apiOpen in browser, check notifications, clone, raw API calls
Miscwhoami adminIdentity, and administration for Forge admins
Setuplogins logout ssh-keysManage servers, credentials and keys

Every command has its own help: forge <command> --help. Shell completion is available with forge shellcompletion bash|zsh|fish.

Pull requests in the web UI

Push a branch, then open a pull request from the repository's Pull requests tab. You get the commit list, the combined diff, per-line comments, reviews and a merge button. The merge strategy (merge commit, squash or rebase) is set per repository under Settings → Pull requests.

Branch protection

Under Settings → Branches, a rule can target a branch name or a glob such as release/*:

SettingEffect
Disable pushAll changes must arrive through a pull request
Disable force pushHistory cannot be rewritten — rejected at the server
Required approvalsNumber of approving reviews before merge
Require signed commitsOnly GPG/SSH-signed commits are accepted
Protected file patternsLock specific paths regardless of branch
Block admin merge overrideAdmins cannot bypass the rules either
Protected branches also cannot be deleted. Force-push and deletion are the two techniques used to rewrite history and hide malicious commits, which is why both are worth disabling on main.

Organisations — people and roles

An organisation has two roles. Both are managed from Organisation → People.

RoleWhat they can do
OwnerEverything: every repository, every setting, adding and removing people.
MemberBelongs to the organisation and can be mentioned and assigned, but gets access to a repository only when they are added to it.
A Member does not automatically see private repositories. That is deliberate and matches GitHub: membership and repository access are separate things. Give access per repository, under that repository's Settings → Collaborators.

To change someone's role, use the dropdown next to their name on the People page. It asks you to confirm, because the change applies immediately and demoting yourself removes your own ability to undo it. The last owner cannot be demoted at all — an organisation with no owner could not be administered by anyone.

The Remove button on a member's row takes them out of the organisation entirely, including yourself. On your own row it reads Leave. There is no undo; an owner has to add you back.

Inviting people

Type into the box at the top of the People page. Matching accounts appear as you type; click one to select it. Someone who has no account yet can be invited by typing their full email address instead — they get an email with a link that joins them to the organisation in the role you chose.

The person invited gets an email with a link. Opening it offers them Join or Decline — they are not added until they answer. Every invitation is listed on the People page with where it got to:

StateMeaning
PendingSent, not yet answered, still valid. The expiry is shown beside it.
AcceptedThey joined, and when. They now appear in the member list above.
DeclinedThey said no. Nothing was granted. Invite again sends a fresh invitation.
ExpiredUnanswered for more than seven days. The link no longer works; Invite again issues a new one.

Revoke withdraws an unanswered invitation immediately. On an accepted one the button reads Clear and only removes the record — it does not remove the person, who is now a member. Take them out from the member list above instead.

Invitations expire after seven days. An invitation link that is never used stops working rather than staying valid for ever, so an old mailbox does not remain a way into your organisation.

Outside collaborators

Someone can work on a single repository without joining the organisation at all. Add them under that repository's Settings → Collaborators with Read, Write or Admin.

They will not appear in the organisation's member list, will not see any of its other repositories, and hold no organisation-wide permissions. Access is per repository, so removing them from one repository does not affect another.

Reviewed merges (branch policy)

Repository branch protection is configured one repository at a time. An organisation owner can instead switch on one rule for every repository the organisation owns, under Organisation → Settings → Branch policy → Require reviewed merges. It is off by default and takes effect immediately when enabled.

On a covered branch, every change must arrive through a pull request:

SettingEffect
Covered branchesEach repository's default branch is always covered. Add more patterns, one per line (default suggestion: release/* and prod)
Required approvals1–10, default 1
Accounts allowed to push directlyService accounts (for example a deploy bot) that may push to covered branches without a pull request. They are still bound by the merge rules when merging
Allow Actions job tokens to pushOff by default; only for workflows that must push to a covered branch

What is blocked on a covered branch, for everyone including organisation owners and repository admins: git push, force-push and deletion; the web editor, upload and delete; the web cherry-pick and revert; API file writes; renaming the branch; and marking a pull request as manually merged. The web editor and cherry-pick default to commit to a new branch, which opens the pull request page. Creating a branch that points at a commit the repository already has is allowed. Tags are not covered.

Things to know. The rule sits on top of repository protection and cannot be weakened from inside a repository; the stricter of the two applies. You can enable it only when the organisation has at least two members, otherwise nothing could ever be merged. Only a site administrator can override it in an emergency, and each override is written to the server log and the admin notices. Pull mirrors and package registry repositories are not covered.

External integrations

Any organisation owner can connect an external system — an issue tracker, a support desk, anything that wants to file and update issues — from Organisation → Settings → Integrations. No administrator or server access is needed. The mechanism is generic: nothing about it is specific to any one external product, so the same steps work regardless of what you're connecting.

What it provisions

Creating an integration automatically sets up three things, atomically:

PieceWhat it is
Bot accountNamed <org>-<integration>-bot. Cannot practically log in interactively — it has no known password — but behaves like any other account for API calls.
TeamGrants that bot issue-only write access on exactly the repositories chosen, and nothing else. It cannot push code, read other repositories, or touch anything besides issues in the linked repos.
Access tokenScoped to write:issue and read:repository only. Shown once, in a banner right after creation — copy it immediately, it is never shown again and never stored in plaintext.
Only repositories that already have at least one linked Project are selectable when creating an integration. Link one under that repository's Projects tab first if it doesn't have one yet.

Setting one up

  1. Go to Organisation → Settings → Integrations. Only organisation owners can reach this page.
  2. If a repository you want isn't listed, it needs a Project linked first (see above).
  3. Enter a name for the system you're connecting — free text, used to derive the bot account's username.
  4. Check the repositories this integration should have access to, then create it.
  5. Copy the one-time token from the banner at the top of the page that follows. The rest of that page — bot username, linked repositories, the exact API calls, the correlation convention — stays available afterwards, and can be printed or saved as a PDF at any time.

Deleting an integration is immediate and tears down the bot account, team and token together in one step. The repositories and their issues are never touched — only the bot's access is removed.

Building the other side

Hand this to whoever is building the connector on the external system: create issues with POST /api/v1/repos/<owner>/<repo>/issues, including "projects": [<id>] so the issue lands directly in the linked Project's board. Update or close them with PATCH /api/v1/repos/<owner>/<repo>/issues/<number> and a "state" field. Store the returned issue number as the correlation key for future updates.

POST https://forge.rhost.rw/api/v1/repos/<owner>/<repo>/issues
Authorization: token <the bot's token>
Content-Type: application/json

{
  "title": "...",
  "body": "<!-- forge-integration:<slug>; external-id:<your-id> -->\n...",
  "projects": [<project id>]
}
This is a one-way integration. The external system creates and updates issues in Forge; Forge does not push changes back automatically.

A full, detailed developer guide — authentication, both API calls with complete examples, the correlation convention, and exactly what the bot token cannot do — is available as a download, useful regardless of which external system is being connected:

CI — Actions

Workflows live in .forge/workflows/ (or .github/workflows/, so projects imported from elsewhere run unchanged) and use familiar YAML:

name: CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "building"

Which runner your job lands on

runs-on is matched against a runner's labels. A shared runner is provided for everyone, so you do not have to set anything up. It answers to any of these, all of which mean the same machine:

runs-on: ubuntu-latest     # or ubuntu-22.04, ubuntu-24.04
runs-on: linux
runs-on: self-hosted

There is also a host label, but it is reserved: it runs directly on the build machine with elevated rights and is restricted to the platform's own deployment pipelines.

Every job runs in a fresh Ubuntu 22.04 container that is destroyed when the job ends — the same model GitHub uses. You are root inside it, apt-get install works, and nothing you do carries over to the next job or leaks onto the host.

You do not need your own runner. Write runs-on: ubuntu-latest and the shared runner picks it up. Actions such as actions/checkout are served from this server, so pipelines run with no access to the internet at all.
macOS and Windows runners do not exist here. runs-on: macos-latest or windows-latest will sit in Waiting for ever, because that would need Apple or Windows hardware. Linux only.

If a job sits in Waiting forever, the usual cause is a runs-on value no runner advertises — there is no error for this, the job simply never starts. Check the label against the list above.

Running your own runner

You do not need one — runs-on: ubuntu-latest already works on the shared runner. Register your own when a job needs something the shared machine does not have: particular hardware, a licensed toolchain, or access to a network only your machine can reach.

1. Get the runner

curl -fSLo forge-runner https://forge.rhost.rw/assets/runner/forge-runner-linux-amd64
curl -fSLo SHA256SUMS  https://forge.rhost.rw/assets/runner/SHA256SUMS
sha256sum -c SHA256SUMS
chmod +x forge-runner
Only the Linux x86_64 build is hosted here, because that is what runners normally run on, and it is the exact version this server expects. For macOS or Windows, fetch the matching act_runner release from upstream — that download happens on your machine, not on the server, so it does not affect anything hosted here. Keep the major version in step with the build above (v3.3.0); an older runner may fail to register.

2. Get a registration token

Open Settings → Actions → Runners on the repository or organisation the runner should serve, and copy the token there. Where you take it from decides what the runner can serve:

Token fromThe runner serves
A repository's settingsonly that repository
An organisation's settingsevery repository that organisation owns

3. Register it

./forge-runner register --no-interactive \
  --instance https://forge.rhost.rw \
  --token <the token you copied> \
  --name my-build-box \
  --labels my-build-box
The instance URL must be https://forge.rhost.rw — the address you reach in a browser. An internal address or 127.0.0.1 will register and then never pick up a job, because the runner keeps polling that address afterwards.

4. Start it — this is a separate step

./forge-runner daemon
Registering does not start anything. After register your runner appears in the settings page and looks ready, but it will never take a job until daemon is running. This is the single most common reason a new runner "does not work".

To keep it running across reboots, install it as a service:

sudo tee /etc/systemd/system/forge-runner.service >/dev/null <<'UNIT'
[Unit]
Description=Forge CI runner
After=network-online.target
Wants=network-online.target

[Service]
User=YOUR-USER
WorkingDirectory=/path/to/runner
ExecStart=/path/to/runner/forge-runner daemon
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl enable --now forge-runner

5. Send it work

jobs:
  build:
    runs-on: my-build-box     # must match --labels exactly
    steps:
      - uses: actions/checkout@v4
      - run: make

If a repository has its own runner and the shared one would also match the same label, the repository's runner wins. Your jobs are not taken by the shared machine.

When a runner does not pick up jobs

A job that matches no runner does not fail — it sits in Waiting for ever with no error. Work down this list:

CheckHow
Is the daemon actually running?forge-runner daemon in the foreground, or systemctl status forge-runner. Registering alone is not enough.
Does runs-on match the label exactly?Compare the workflow against the label shown in Settings → Actions → Runners. my-box and my_box are different.
Can the machine reach https://forge.rhost.rw?curl -I https://forge.rhost.rw/api/v1/version from that machine. The runner polls continuously over HTTPS, not just at registration.
Is the runner online?The settings page shows when it was last seen. If it never goes online, read the daemon output.
Is the token still valid?Registration tokens expire. Take a fresh one and register again.
Is the runner in the right scope?A runner registered on repository A cannot serve repository B. Register at the organisation instead.

The daemon prints why it cannot start or connect. Run it in the foreground once and read the output before anything else — it usually says plainly what is wrong.

Actions resolve from here, not from the internet

uses: actions/checkout@v4 and the other common actions are mirrored onto this server, so pipelines need no outbound access and are not subject to anyone else's rate limits. Ask an administrator if you need an action that is not mirrored yet.

Secrets are set per repository or organisation under Settings → Actions → Secrets and are masked in job logs. The runtime provides FORGE_TOKEN and GITHUB_TOKEN automatically; secret names cannot start with FORGE_ or GITHUB_.

forge actions list
forge actions run <workflow>
A runner must be registered before anything executes. Jobs queue until then — that is normal, not a failure. Ask an administrator.

Push scanning

Repositories can reject pushes containing indicators of the PolinRider supply-chain campaign, which appends obfuscated JavaScript to the end of real config files and hides payloads inside .woff2 fonts.

Enable it with the checkbox on the New repository page, or later under Settings → Options → Push scanning. Organisation owners can require it for every repository they own from Organisation → Settings.

OrganisationRepositoryResult
RequiredanythingEnabled — a repository cannot switch it off
Not requiredEnabledEnabled for that repository
Not requiredDisabledDisabled

A rejected push is refused before any object enters the repository, and the error names exactly what was found. If you believe it is a false positive, contact an administrator rather than trying to work around it.

Large files

Git LFS is enabled. Track large binaries instead of committing them directly:

git lfs install
git lfs track "*.psd"
git add .gitattributes

Packages

The built-in registry supports npm, PyPI, Maven, NuGet, Cargo, Composer, Docker and more. Each package page shows the exact configuration to paste for that ecosystem. Find them under your profile or organisation → Packages.

REST API

Interactive documentation lives at /api/swagger. Authenticate with a token:

curl -H "Authorization: token <TOKEN>" \
     https://forge.rhost.rw/api/v1/user/repos

Accounts

TaskHow
Create an accountRegister at /user/sign_up, then confirm the link emailed to you
Forgotten passwordUse the reset link on the sign-in page, or ask an administrator
Two-factor authSettings → Security → enable TOTP or a passkey
Private repositoriesChoose visibility at creation, change under Settings
Teams and permissionsCreate an organisation, then grant teams read / write / admin per repository

What Forge deliberately does not do

BehaviourWhy
Avatars are uploads onlyNo third-party avatar service is contacted; until you upload one you get a generated initial
Spell-check is English onlyThe dictionary is served from this machine rather than fetched from a CDN
Interface language is EnglishOther translations ship but are not offered
No external sign-inAccounts are local; nothing authenticates against an outside provider

Getting help

Administrators can be reached through the usual support support channel. Include the repository name, what you ran, and the exact error text — the server log line matching your request is what makes a problem quick to find.