Development
Run these from the repository root. mise install provides the whole
toolchain, and mise trust once lets mise read mise.toml. The tasks live in
shared/config/mise/tasks/, and mise tasks lists them.
Checking
Section titled “Checking”mise run fmt # format every file, as shared/config/dprint.json configuresmise run lint # formatting, prose, links, buf, spec, workflows, Dockerfiles, # task scripts, helm, tsc, biomemise run test # gradle build (tests, detekt, architecture, coverage), vitest, # web buildThe rust: tasks cover the Rust code:
mise run rust:fmt # cargo fmtmise run rust:lint # cargo fmt --check, clippy, rustdoc, machete, denymise run rust:test # cargo testCI runs lint, test, rust:lint, and rust:test. ADR 0015 lists the rules they enforce. ADR 0015 lists the rules they enforce.
Running
Section titled “Running”With podman, which builds both images from the repository root:
mise run up # server on :8080, vcsd on :50052mise run smoke # in a second terminal: web, server, vcsd and cliSet JJFORGE_PORT for both when something else holds port 8080.
Without containers:
(cd rust && cargo run --bin vcsd)gradle -p server bootRun(cd rust && cargo run --bin jf -- echo hi)curl -X POST localhost:8080/api/echo -H 'content-type: application/json' -d '{"message":"hi"}'JJFORGE_ENDPOINT points the CLI at another vcsd, such as
https://jjforge.example.com.
Trying the API
Section titled “Trying the API”shared/http/ holds Hurl files: requests with asserts on
each response. mise run smoke runs them against mise run up, and they run
against any other server too:
hurl --test --variable server=https://jjforge.example.com shared/http/*.hurlHurl cannot send gRPC, so vcsd is exercised through the jf CLI.
mise run lint also checks every relative link and anchor in the Markdown
with lychee. mise run site:build builds the docs site in shared/site/ from
these documents.
Changing a contract
Section titled “Changing a contract”A contract changes before its implementation, and every operation cites the requirements it serves. Planning has the rules.
shared/openapi.yaml: the public API, written by hand. Every operation lists its requirement issues inx-requirements.mise run spec:lintlints it with Redocly and is part ofmise run lint.mise run spec:tracechecks that every operation cites only issues of type Requirement. The echo operation is exempt until it is removed.mise run spec:breakingcompares it withmainusing oasdiff. CI accepts a breaking change only when the PR title marks it with!, as infeat!: rename the org field.- The server build generates Kotlin interfaces and models from it, and one
stub controller per tag implements them. An operation that isn’t built
yet answers 501.
ControllerContractTestfails on a controller that implements no interface or maps a route of its own, and on an interface without a controller. See ADR 0011.
shared/proto/: the internal contract the server uses to call vcsd. CI runsbuf breakingagainstmain, because the server and vcsd run different versions during a rolling deploy.
Releasing
Section titled “Releasing”A v* tag publishes the server and vcsd images, the CLI binaries, and the
chart to oci://ghcr.io/nca-apprentices/charts/jjforge, all with the same
version. Deploying it is a separate change in
nca-apprentices/infra.
The release also writes Formula/jf.rb in
nca-apprentices/homebrew-tap,
so brew install nca-apprentices/tap/jf installs the new jf on macOS
(Apple silicon) and Linux. mise run release:formula <tag> <dir> prints the
same formula from a directory of jf-<target> binaries. The tap’s deploy key,
stored as the secret HOMEBREW_TAP_DEPLOY_KEY, lets the release push to it.