Getting started
On this page 8
Running ReviewOS on your own machine, from nothing to a repository you have pushed to. Fifteen minutes, most of it waiting for a toolchain to download.
This page is the local one. For an instance other people use, read Self-hosting instead: it covers TLS, backups, and the parts that only matter when losing the data would be a problem.
What you need
Nothing installed beforehand, which is the point of the setup step below. It fetches these through pantry, into the project rather than onto your system:
- Bun 1.3.14 or newer
- PostgreSQL 17
- git 2.47 or newer
- gnupg 2.4.8 or newer, for verifying commit signatures
Git is a real dependency rather than a detail. Repositories are ordinary bare repositories on disk
and every operation on them is the git binary, spawned. There is no reimplementation of git here
to go subtly wrong, and there is nothing about your data locked inside this application.
Install
git clone https://github.com/ReviewOS/reviewos.org.git
cd reviewos.org
./buddy setup
./buddy setup installs the toolchain, starts Postgres, creates the database named in your .env,
and runs the migrations. It also writes deps.yaml from config/deps.ts and your .env, so that
file is generated and not worth editing.
Then:
./buddy dev
The application comes up on https://reviewos.localhost, with a local certificate. If the port is
taken, PORT in .env moves it.
Before signing in, ask the instance how it is:
./buddy instance:check
It reads the configuration, the database, the queue, and the repository directory, and says what is
wrong and what to do about it. It exits non-zero on anything fatal, so a start script can use it.
A warning about APP_KEY in development is expected until you run ./buddy key:generate; in
production it refuses to start instead.
The first account
Registration is open by default so the first person can get in, and the first account is exempt from the closed setting for the same reason: an instance closed before anybody signed up is one nobody can administer.
Register through the interface. To make that account an administrator:
psql -U postgres reviewos -c "UPDATE users SET is_admin = true WHERE handle = 'you'"
There is no bootstrap command that does this, deliberately. A command that grants administrator rights is a command somebody can be talked into running.
A believable instance, without the typing
If you want something to look at rather than something to fill in:
./buddy seed:demo
That writes accounts, organizations, repositories with real history, issues, pull requests with review threads, and notifications. It is the fastest way to see what a screen looks like with a hundred files in a diff rather than three.
Your first repository
1. Create it. New in the header, which is /new, or POST /api/repos with a token. A repository is a row
and a bare repository on disk, created together: a row with nothing behind it answers every clone
with a confusing error, and a repository on disk with no row is invisible and never collected.
2. Get a credential. Settings, then Tokens. A token carries per-scope access - code, issues, pull requests, checks, webhooks, administration - each of them none, read, or write. Give it the least that does the job; the point of scopes is that a token left in a CI config cannot do everything you can.
3. Push over HTTPS, with the token as the password and any user name:
git remote add origin https://reviewos.localhost/git/you/your-repository.git
git push -u origin main
Or over SSH. The daemon is separate and off unless you start it, because a git server listening on a port is a decision rather than a default:
./buddy git:ssh
Add your public key under Settings, then Keys. Clone URLs use ssh:// and carry the port, because
the short git@host:path form has nowhere to put one - the colon is already the path separator:
git clone ssh://git@localhost:2222/you/your-repository.git
Set SSH_PORT in .env and the repository page starts offering the SSH URL beside the HTTPS one.
Until an operator says where SSH answers, no SSH URL is shown at all: a clone URL that cannot
connect is worse than one fewer, because it looks like the forge is broken rather than like a
feature is off.
The page on your profile
/{handle} is a profile, and the text at the top of it is a markdown file in a repository called
.profile - the same for a person and an organization:
# on the instance, once
git init .profile && cd .profile
printf '# Who we are\n' > README.md
git add . && git commit -m 'the page my profile shows'
git remote add origin https://reviewos.localhost/git/you/.profile.git
git push -u origin main
README.md at its root is the page. profile/README.md is read too, so a profile written for
somewhere else can be copied across without being rearranged.
Arriving from GitHub, two more places are read before this one gives up, so an instance that
mirrors an organization shows the page it already publishes: a mirrored .github at
profile/README.md, which is where GitHub keeps an organization's, and the repository named after
the handle, which is where it keeps a person's.
./buddy mirror:add --remote yourorg/.github --owner yourorg
A mirror added that way is a mirror only the person who typed it knows about, which is how this instance came to be missing exactly one repository - the profile - while carrying a hundred and fourteen others. So the set an instance is supposed to have can be a file instead, applied on every deploy:
./buddy mirror:apply mirrors.yml --plan # what would change, touching nothing
./buddy mirror:apply mirrors.yml
mirrors:
- remote: yourorg/.github
owner: yourorg
It is additive: a mirror on the instance and not in the file is counted and left alone, so a partial
file can never take a repository off the box, and a line naming an owner that does not exist yet is
skipped rather than fatal. This repository applies its own mirrors.yml from the deploy's pre-start
step in config/cloud.ts.
All of them are read through the same permission check as any other file, so a private repository is not a way to publish a page to people who may not see it.
What happens when you push
Worth knowing early, because it explains most of what you will see afterwards. The push runs through
pre-receive and post-receive hooks this instance installed:
- Branch protection and push rules are decided before the objects are accepted, so a rejected push leaves nothing behind.
- Secret scanning reads the incoming objects out of the quarantine directory git gives the hook.
- Afterwards the push is recorded:
pushed_at, closed issues, cross references, the activity feed, and any webhook or check that was waiting on the new head.
Without GIT_HOOK_SECRET set, the hooks cannot post back and none of that recording happens - the
objects land and the forge learns nothing about them. ./buddy setup generates one; buddy git:hooks
reinstalls the hooks after you change it.
Working on ReviewOS itself
./buddy lint # pickier, with --fix
./buddy typecheck # app/, config/, resources/, routes/
./buddy test # the suite
./buddy dev:docs # this site, including the roadmap
The roadmap in docs/todo is the honest picture of what exists. A box is ticked in the same commit as the work it describes, so an empty box means the feature is not there yet rather than that nobody wrote it down.
If you are about to change something, Architecture explains where things live, and Contributing explains the order to build them in.