crates/gunnar-forge/Cargo.toml
[package]
name = "gunnar-forge"
description = "Lists a user's repositories on GitHub, Codeberg/Forgejo and GitLab over the forge REST API, normalised to one Repo shape, so a picker can offer them for mirroring"
readme = "README.md"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
# Said here although the workspace key now says the same (2026-10-11): this
# crate's published pages already name it, and gunnar's own forge serves the
# canonical tree — gunnar's own dogfood.
repository = "https://git.gunnar.rs/nordisk/gunnar"
# crates.io metadata — required (`description`, a license) or it refuses the
# upload, and `keywords`/`categories` are what make the crate findable at all.
# Five keywords is the cap and each is ≤20 bytes; the categories are three real
# slugs from crates.io's fixed list, in the order that describes this crate:
# it is an API client (`api-bindings`) for forge HTTP endpoints
# (`web-programming::http-client`) used by developer tooling
# (`development-tools`). A slug that is not on the list is a 400, so these are
# not invented.
keywords = ["git", "forge", "github", "gitlab", "forgejo"]
categories = ["api-bindings", "web-programming::http-client", "development-tools"]
# **The one override of the workspace's `publish = false`.** The reason is the
# section below, and it is stated once there rather than twice; what this line
# adds is that the exception is now a line somebody has to write, instead of the
# absence of a rule that applied to nobody.
publish = true
# ── WHY THIS CRATE PUBLISHES AND ITS SIBLINGS DO NOT ────────────────────────
#
# `[repos.gunnar]` in the constellation descriptor is `release = false` — the
# appliance ships as an image, not to crates.io — and that stays true of every
# other member here. This crate is the single named exception, because
# `gunnar-ui` is a SEPARATE repository and needs it: without a registry copy,
# `cargo build -p gunnar-ui-server` in a fresh `gunnar-ui` clone requires a
# `gunnar` checkout parked at exactly `../../../gunnar`, which is a build
# instruction no clone can carry. The reason is written into the descriptor too,
# beside the `release = false` line, so the exception is visible from either end.
#
# It is also the crate that can leave: it links no sibling (ureq, serde_json,
# thiserror and nothing else), so publishing it drags no half of the appliance
# onto a public registry with it.
# ── why this crate is NOT in the appliance graph, stated in the manifest ─────
#
# `xtask/src/guard.rs` resolves the appliance by asking `cargo tree` what
# `gunnar-cli --no-default-features --features server` links. Nothing in that
# graph names `gunnar-forge` and nothing may: the appliance has no outbound
# HTTPS and must never hold a user's forge token. The two callers are
# `gunnar-cli` on a laptop and the axum BFF on the front VM, both of which have
# a filesystem, a network and a human behind them.
#
# So the guard does not track this crate, and that is the correct state rather
# than an oversight — see the report in the commit that added it. The day
# something on the server half depends on it, the appliance profile's crate
# count moves and `check_appliance_growth` says so.
[dependencies]
# The constellation's HTTP client (skade's `lineage-http`/`geo-http`, aegir's
# S3 SigV4). Blocking, pure Rust, rustls — no OpenSSL, no C, no async runtime
# dragged into a crate whose whole job is "issue a few GETs and stop".
#
# `default-features = false` + `tls` drops `gzip`, which is `flate2` +
# `miniz_oxide` for a payload that is at most a few hundred KiB of JSON on the
# largest account anyone has. ureq only sends `Accept-Encoding: gzip` when that
# feature is on, so turning it off cannot produce a body we then fail to
# decode; it makes the transfer bigger and the tree smaller.
#
# MEASURED 2026-09-08:
#
# cargo tree -p gunnar-forge -e normal --prefix none | sort -u | wc -l
# -> 57 (this crate + 56 third-party)
#
# and of those 56, only THREE are new to this repository:
#
# comm -23 <forge tree> <cargo tree -p gunnar-cli>
# -> ureq 2.12.1, webpki-roots 0.26.11, webpki-roots 1.0.9
# (0.26 is a thin shim that re-exports 1.0, so it is one root store)
#
# Everything else — rustls, ring, url and its 18-crate icu/idna tail, base64,
# zeroize — `gunnar-cli` already links today. The trade is therefore visible
# rather than argued: the number is here, and the number that matters is 3.
ureq = { version = "3", default-features = false, features = ["rustls"] }
# Read as `Value` on purpose, with no `serde_derive` anywhere. Two reasons, and
# the second is the load-bearing one:
#
# 1. `serde_derive` is a proc macro and brings `syn`/`quote`/`proc-macro2`
# into `cargo tree -e normal`, which is the number this crate is judged on.
# 2. A `#[derive(Deserialize)]` struct decides in advance what a forge's JSON
# looks like. GitHub, Gitea/Forgejo and GitLab all add fields continuously
# and Forgejo forks of Gitea *remove* them; a strict shape turns "the forge
# added a key" into "the picker is empty", which is the worst possible
# failure for a list. Reading the six keys we actually need out of a
# `Value` degrades to `None`/`false` instead.
serde_json = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
# The integration tests build fixture pages by loading one REAL repository
# object per forge and overriding four keys — see `tests/common/mod.rs`. An
# integration test cannot reach the library's own dependencies, so it is named
# again here.
serde_json = { workspace = true }
# NOTE: there is no `testing` feature. `src/testing.rs` is `pub mod testing`
# unconditionally, exactly as `monetize-cloud-impl` and `monetize-pay-impl` do
# it, because that is the pattern this constellation already has and a second
# one would be a second thing to learn.
#
# The alternative — gating it behind a feature — reads tidier and does not
# work: an integration test in `tests/` links this crate as an ordinary
# dependency with default features, so a gated `testing` module is invisible to
# exactly the tests that need it, and the usual escape (a dev-dependency on
# oneself) is a package cycle. The module costs nothing but its own bytes: it
# pulls in no dependency, only `std::net`.