README.md
forge
An on-chain software forge, browsable in gnoweb and writable only through signed
transactions. The domain model lives in
gno.land/p/moul/forge/v0; this realm is the
wiring, the routes and the events.
The code itself is not here. Git objects stay in git, behind whatever mirror a repo declares. What the chain keeps is the part a forge is trusted for: which object a branch points at, in what order, on whose authority, and what was reviewed before it moved.
Routes
| path | page |
|---|---|
/ |
every repo, with counts |
/<ns>/<name> |
repo overview: refs, recent log, open changes and issues |
/<ns>/<name>/log |
the full reference log, paginated with ?page=N |
/<ns>/<name>/issues |
issues, ?page=N |
/<ns>/<name>/issues/<id> |
one issue and its replies |
/<ns>/<name>/changes |
change requests, ?page=N |
/<ns>/<name>/changes/<id> |
one change, its reviews and its replies |
/help |
what the realm is and how to call it |
Every action link on those pages is a txlink into the matching function, so
the whole forge is usable from gnoweb without a client.
Writing
1gnokey maketx call -pkgpath gno.land/r/moul/forge/v0 \
2 -func CreateRepo -args "moul/forge" -args "an on-chain forge" -args "" \
3 -gas-fee 1000000ugnot -gas-wanted 3000000 \
4 -broadcast -chainid <chain> -remote <rpc> <key>
1# move a branch: the fourth argument is the tip you expect to replace
2gnokey maketx call -pkgpath gno.land/r/moul/forge/v0 \
3 -func SetRef -args "moul/forge" -args "refs/heads/main" \
4 -args "<expected-oid>" -args "<new-oid>" -args "ship it" \
5 -gas-fee 1000000ugnot -gas-wanted 3000000 \
6 -broadcast -chainid <chain> -remote <rpc> <key>
A stale expectation aborts the transaction instead of overwriting the branch. A
move with no expectation is ForceSetRef, needs maintainer, and is recorded
as a force forever.
Arguments that would be slices are comma-separated strings (SetMirrors,
OpenIssue labels): a transaction can only carry strings, so a []string
parameter would not be callable from gnokey.
Reading
Read paths are free. Browse the routes above, or query:
1gnokey query vm/qeval -data 'gno.land/r/moul/forge/v0.RefOID("moul/forge","refs/heads/main")' -remote <rpc>
2gnokey query vm/qeval -data 'gno.land/r/moul/forge/v0.LogHead("moul/forge")' -remote <rpc>
LogHead is the digest of the last log entry, committing to every entry before
it. Pin it in a release note or a package manifest and the repo's whole history
becomes falsifiable by anyone who can read the chain.
Namespaces
A repo id is <namespace>/<name>. The namespace is either a name the caller
holds in r/sys/users or the caller's own bech32 address, checked on every
CreateRepo and Fork:
g1.../forgeworks for any account with nothing to register and nothing to lose, including a realm, whose address is its namespace. That is what makes a DAO-owned repo work.moul/forgerequires themoulname to resolve to the caller, renames included, since the registry resolves aliases to the same record.
Nobody can claim a namespace they do not own, so there is no squatting to arbitrate and no reservation list to maintain.
Roles
reader < writer < maintainer < admin < owner. Writers move refs, maintainers
force and merge, admins manage members and the merge policy, and the last owner
cannot be demoted. Opening an issue or a change request needs no role at all:
the author pays gas and locks the storage deposit for their own bytes, which is
the spam gate.
A role can be held by another realm, because the realm resolves its caller without requiring an end user. A repo whose owner is a DAO realm is a repo whose merge button is a governance vote, which is the thing a centralized forge structurally cannot offer.
Events
Every mutation emits one: RepoCreated, RepoForked, RepoUpdated,
MemberSet, RefLog, IssueOpened, IssueComment, IssueClosed,
IssueReopened, IssueLabeled, ChangeOpened, ChangeUpdated,
ChangeReviewed, ChangeComment, ChangeClosed, ChangeMerged. RefLog
carries the sequence number and the chain digest, so an indexer can mirror the
log without replaying state.
Costs
A realm write locks 100ugnot per byte, refundable when the data is deleted. A log entry is roughly 200 bytes including the digest, so a push costs about 0.02 GNOT of deposit plus gas: the reason this design anchors refs instead of storing objects. An issue with a 500-byte body is about 0.05 GNOT.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
Dependency graph:

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.