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
git remote set-url origin git@forge.rhost.rw:<you>/<repo>.gitHTTPS 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.
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
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
| Symptom | Cause and fix |
|---|---|
| Still asked for a username and password | The 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 binary | Quarantine on a downloaded, non-notarised file: xattr -d com.apple.quarantine /usr/local/bin/forge |
no available login | No server configured — run forge login add. |
| Push rejected, PolinRider indicators | Push scanning found a known supply-chain marker. The message names the file — inspect it before doing anything else. |
| Push rejected, protected branch | The branch forbids force-push or direct push. Open a pull request instead. |
Full command reference
| Group | Commands | Purpose |
|---|---|---|
| Entities | issues pulls labels milestones releases times organizations repos branches actions wiki webhooks comments | List, create and update everything in a repository |
| Helpers | open notifications clone api | Open in browser, check notifications, clone, raw API calls |
| Misc | whoami admin | Identity, and administration for Forge admins |
| Setup | logins logout ssh-keys | Manage 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/*:
| Setting | Effect |
|---|---|
| Disable push | All changes must arrive through a pull request |
| Disable force push | History cannot be rewritten — rejected at the server |
| Required approvals | Number of approving reviews before merge |
| Require signed commits | Only GPG/SSH-signed commits are accepted |
| Protected file patterns | Lock specific paths regardless of branch |
| Block admin merge override | Admins cannot bypass the rules either |
main.Organisations — people and roles
An organisation has two roles. Both are managed from Organisation → People.
| Role | What they can do |
|---|---|
| Owner | Everything: every repository, every setting, adding and removing people. |
| Member | Belongs to the organisation and can be mentioned and assigned, but gets access to a repository only when they are added to it. |
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.
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:
| State | Meaning |
|---|---|
| Pending | Sent, not yet answered, still valid. The expiry is shown beside it. |
| Accepted | They joined, and when. They now appear in the member list above. |
| Declined | They said no. Nothing was granted. Invite again sends a fresh invitation. |
| Expired | Unanswered 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.
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:
- Someone other than the author must approve it. The approval only counts if the reviewer is an organisation member and did not author, commit or push to the pull request.
- The pull request must be merged by an organisation member who is not its author. Any such member may merge, not only the approver.
- New commits pushed to the pull request dismiss earlier approvals.
| Setting | Effect |
|---|---|
| Covered branches | Each repository's default branch is always covered. Add more patterns, one per line (default suggestion: release/* and prod) |
| Required approvals | 1–10, default 1 |
| Accounts allowed to push directly | Service 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 push | Off 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.
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:
| Piece | What it is |
|---|---|
| Bot account | Named <org>-<integration>-bot. Cannot practically log in interactively — it has no known password — but behaves like any other account for API calls. |
| Team | Grants 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 token | Scoped 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. |
Setting one up
- Go to Organisation → Settings → Integrations. Only organisation owners can reach this page.
- If a repository you want isn't listed, it needs a Project linked first (see above).
- Enter a name for the system you're connecting — free text, used to derive the bot account's username.
- Check the repositories this integration should have access to, then create it.
- 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>]
}
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.
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.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
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 from | The runner serves |
|---|---|
| A repository's settings | only that repository |
| An organisation's settings | every 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
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
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:
| Check | How |
|---|---|
| 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>
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.
| Organisation | Repository | Result |
|---|---|---|
| Required | anything | Enabled — a repository cannot switch it off |
| Not required | Enabled | Enabled for that repository |
| Not required | Disabled | Disabled |
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
| Task | How |
|---|---|
| Create an account | Register at /user/sign_up, then confirm the link emailed to you |
| Forgotten password | Use the reset link on the sign-in page, or ask an administrator |
| Two-factor auth | Settings → Security → enable TOTP or a passkey |
| Private repositories | Choose visibility at creation, change under Settings |
| Teams and permissions | Create an organisation, then grant teams read / write / admin per repository |
What Forge deliberately does not do
| Behaviour | Why |
|---|---|
| Avatars are uploads only | No third-party avatar service is contacted; until you upload one you get a generated initial |
| Spell-check is English only | The dictionary is served from this machine rather than fetched from a CDN |
| Interface language is English | Other translations ship but are not offered |
| No external sign-in | Accounts 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.