rtb-forge 0.8.0

Release-provider abstractions and backends (GitHub, GitLab, Bitbucket, Gitea, Codeberg, Direct) plus an async git-operations Repo built on gix. Part of the phpboyscout Rust toolkit.
Documentation
# AGENTS.md

This file provides guidance to AI coding agents (Claude Code, agy, codex, etc.) when working with code in this repository.

Ways of working are deliberately not repeated here. They live in the phpboyscout
skills, and naming one ages better than restating it.

## What this crate is

Two slices in one crate, each behind Cargo features: the `ReleaseProvider` trait
(`src/release.rs:236`) with six backends — GitHub, GitLab, Bitbucket, Gitea,
Codeberg, Direct — and the `Repo` type (`src/git/mod.rs:69`), an async wrapper
over `gix`, behind the default-on `git` feature.

Both are narrower than their names suggest, and that gap is where newcomers come
unstuck. `ReleaseProvider`'s four methods are all reads — no create-release, no
upload, no tag editing, none planned. `Repo` covers eleven operations and stops:
no branch, merge, rebase, stash, tag or remote management. Neither slice
"supports GitLab" or "wraps git" in general; each supports the subset those two
types name. Before asserting the crate can do something, check
[`docs/explanation/04-what-this-does-not-do.md`](docs/explanation/04-what-this-does-not-do.md)
— a complete inventory of limits.

Version 0.7.1, pre-1.0, so the surface still moves, though `rust-semver`
runs `cargo-semver-checks` on the release MR so a break moves the version rather
than slipping out in a patch. Formerly `rtb-vcs`, and **the rename changed the
crate identity and nothing else** — types, modules, features and the internal
`RTB_VCS_GIT_TOKEN` credential-helper variable (`src/git/auth.rs:16`) keep the old
name on purpose. Tidying it is a behaviour change, not a rename.

## `unsafe_code` is `deny` here, not `forbid` — leave it alone

rust-tool-base's workspace manifest requires every `src/lib.rs` to ship
`#![forbid(unsafe_code)]` as "the production guarantee". This crate deliberately
does not, and an agent applying that rule mechanically will break the build.

`linkme`'s `#[distributed_slice]` expands to a `#[link_section]` attribute, which
Rust 1.95 and later attribute to the `unsafe_code` lint. `forbid` cannot be
locally overridden; `deny` can. So `src/lib.rs:41` is `#![deny(unsafe_code)]`,
`Cargo.toml:20` matches it, and the six backend modules plus `src/http.rs` each
carry a module-level `#![allow(unsafe_code)]`. No hand-written `unsafe` block
exists in `src/`; the allowance covers a lint fired by an attribute.
`src/release.rs:304-311` explains why `RELEASE_PROVIDERS` holds
`fn() -> Box<dyn ProviderRegistration>` rather than structs — same workaround, so
do not flatten that either.

Nor is it local to this crate: `rtb-chat`, `rtb-cli`, `rtb-docs`, `rtb-mcp` and
`rtb-update` all ship `deny` for the same reason, with the allow at each `linkme`
site (`rust/cli/crates/rtb-cli/src/telemetry.rs:33`, for one). The manifest
comment is what has drifted. Background:
[`docs/explanation/02-link-time-registration.md`](docs/explanation/02-link-time-registration.md).

## The write paths shell out to `git`

`gix` handles `open`, `init`, `walk`, `diff`, `blame`, `status` and anonymous
`clone`. `commit`, `checkout`, `fetch`, `push` and *authenticated* `clone` run the
`git` binary as a subprocess, so a binary using those needs `git` on `PATH` at
runtime — which is why a distroless image fails on the first commit, not at build
time. Do not build behaviour on the split; moving a write path back to pure gix is
meant to be a patch release. Per-operation table in
[`docs/explanation/03-why-write-paths-shell-out.md`](docs/explanation/03-why-write-paths-shell-out.md).

## Keeping the feature matrix coherent when you add a backend

Every backend is gated so a tool with a binary-size budget compiles only what it
uses, and `rtb-update` exercises that for real: it takes this crate with
`default-features = false` and no features at all
(`rust/cli/crates/rtb-update/Cargo.toml:21`), so no backend registers in that
build and the binary supplies them. Gating that drifts breaks a consumer you
cannot see from here. Adding a backend means touching all of:

- **`Cargo.toml:32-67`** — declare the feature, and add it to `default` (`:38`)
  if it should be on. A REST backend takes `_http`; one that delegates takes what
  it delegates to, as `codeberg = ["gitea"]` does (`:47`), which is why no build
  has Codeberg without Gitea.
- **`src/lib.rs`** — a `#[cfg(feature = "…")] pub mod` line, plus that
  module-level `#![allow(unsafe_code)]` inside the new module.
- **`src/config.rs`** — the params struct and the `ReleaseSourceConfig` variant,
  neither of which is feature-gated. The variant exists even in a build without
  its backend, so such a config deserialises fine and then `lookup` returns
  `None`. That asymmetry is intentional; do not cfg-gate the enum without a spec.
- **`docs/reference/01-cargo-features.md`**, a table people configure from that
  goes stale silently, and a wiremock-backed `tests/<name>_backend.rs`.

A *downstream* tool's own backend needs none of this and must not be added here;
its route is
[`docs/how-to/04-add-a-custom-backend.md`](docs/how-to/04-add-a-custom-backend.md).

## Its place in the decomposition

- **Depends on `rtb-credentials`**, optionally, only under the `git` feature
  (`Cargo.toml:54`, `:121`). It releases ahead of this crate, so absorb its bump
  rather than tagging past it.
- **Three crates depend on this one**, all resolving by published crates.io
  version rather than by path: `rtb-cli-bin` and `rtb-update` require it, and the
  `rust-tool-base` umbrella takes it optionally behind a `forge` feature that is
  *not* in that crate's default set — though it still reaches a default umbrella
  build transitively via `rtb-update`. `rtb-update` lives in the four-crate
  `rust/cli` workspace, so a repo-level dependency search misses it.
- **This crate carries its own `docs/`**, the Rust toolkit's pattern, unlike the
  Go side where a family's docs live with its core. Specs, though, are **wiki
  pages** on rust-tool-base: the git-operations slice is
  [spec 0023]https://gitlab.com/phpboyscout/rust-tool-base/-/wikis/specs/0023-v0.5-scope,
  which is where the source comments citing it point.
- Releases run through colophon (`.colophon.yaml`), with cicd's `rust-release`
  publishing the tag. `just ci` (`fmt-check`, `lint`, `docs`, `test`, `audit`) is the local gate;
  it excludes the testcontainers Gitea suite, which is `just test-integration` and
  needs Docker.

## Which skills apply here

> **The estate has no Rust-specific skills yet**, so this is the
> language-agnostic set only. Known gap, worth revisiting once it is filled.

| When | Skill |
|---|---|
| Starting a feature, a component, or any non-trivial change | `spec-driven-development` |
| Working in a repo other than the one you were invoked in | `cross-repo-worktree` |
| Building a feature or fixing a bug against the BDD suite | `test-first-discipline` |
| Deciding whether a change belongs in `Repo` or in the caller | `deep-modules` |
| Writing or restructuring this crate's `docs/` | `diataxis-docs` |
| Writing anything others will read and check | `checkable-claims` |
| Concluding anything from code or a checkout you did not just write | `verify-dont-trust` |
| Writing a commit message or a merge request description | `conventional-commits`, `pre-1-0-release-safety` |
| Committing, branching, merging, or opening a merge request | `forge-publish-workflow` |
| Judging a Renovate bump, especially a `gix` one | `assess-before-bumping` |
| Releasing several crates in this toolkit together | `release-train` |

> Skills are a Claude Code mechanism, shipped by the
> [phpboyscout marketplace]https://gitlab.com/phpboyscout/claude-code-plugins. An
> agent without them should treat a named skill as a topic to ask about, not load.

## House rules

- Linear history: rebase and fast-forward, never squash-merge from the UI. No AI
  attribution in anything published, and never at-mention anyone.
- `feat`, `fix`, `perf` and `refactor` all cut a release, and a change that
  repoints or removes a public interface is `feat`, not `refactor`, or it ships
  as a patch. Never
  cut a release yourself; that is the maintainer's call, every time.