Skip to main content

vgi_forge/
forge.rs

1//! The [`Forge`] trait (§5.8).
2
3use async_trait::async_trait;
4use http::HeaderMap;
5
6use crate::bootstrap::{BootstrapStep, StepOutcome, VgiConfig};
7use crate::error::{ForgeError, Result};
8use crate::event::{Drift, ForgeEvent, default_diff};
9use crate::model::{
10    ApplyReport, BindCallback, BindRequest, BindStep, Capabilities, ForgeAccount, ForgeKind,
11    LinkCallback, LinkStep, Namespace, NamespaceBinding, Projection, RepoSpec, RepoState,
12    RoleAssignment, Unlisted,
13};
14use crate::resource::Resource;
15use crate::rights::{EffectiveRights, ForgeRole, RoleMap, collapse_to_ladder};
16
17/// One forge implementation. Stateless apart from its credentials and the
18/// namespaces it has been told about; the core owns all desired state and
19/// hands the adapter a plan.
20///
21/// Object-safe (through `async-trait`) so a bridge can hold one
22/// `Box<dyn Forge>` per forge host and dispatch on a resource's host. Methods
23/// with a sensible forge-neutral answer have a default; an adapter overrides
24/// only what its forge does differently.
25#[async_trait]
26pub trait Forge: Send + Sync {
27    /// Which forge software this is.
28    fn kind(&self) -> ForgeKind;
29
30    /// The forge host this adapter serves (`github.com`, a GHES host,
31    /// `codeberg.org`). Every resource it accepts starts with it; a
32    /// resource on another host is refused rather than sent to the wrong
33    /// forge.
34    fn host(&self) -> &str;
35
36    /// What this forge, and this namespace on it, can do. The core and the
37    /// UX branch on this, never on [`Forge::kind`].
38    fn capabilities(&self, ns: &Namespace) -> Capabilities;
39
40    // ── identity and binding ─────────────────────────────────────────────
41
42    /// Start binding a namespace: where to send the admin.
43    async fn begin_bind(&self, req: BindRequest) -> Result<BindStep>;
44
45    /// Finish a bind from the forge's callback. Validates the state nonce and
46    /// that the credential landed on the expected owner.
47    async fn complete_bind(&self, cb: BindCallback) -> Result<NamespaceBinding>;
48
49    /// Start linking a member's forge account. `member` is their DID, for
50    /// the adapter's audit trail; nothing forge-side sees it.
51    async fn begin_account_link(&self, member: &str) -> Result<LinkStep>;
52
53    /// Finish linking: the account's numeric id and current login.
54    async fn complete_account_link(&self, cb: LinkCallback) -> Result<ForgeAccount>;
55
56    // ── resources ────────────────────────────────────────────────────────
57
58    /// Canonical form of a forge path. The default applies the
59    /// `owner[/repo]` grammar GitHub and Forgejo share and refuses a
60    /// resource on another host.
61    fn normalize(&self, raw: &str) -> Result<Resource> {
62        let resource = Resource::parse_owner_repo(raw)?;
63        if resource.host() != self.host() {
64            return Err(ForgeError::WrongResource {
65                resource: resource.to_string(),
66                expected: format!("a resource on `{}`", self.host()),
67            });
68        }
69        Ok(resource)
70    }
71
72    /// Observe a repository's current state.
73    async fn inspect(&self, repo: &Resource) -> Result<RepoState>;
74
75    // ── repo lifecycle ───────────────────────────────────────────────────
76
77    /// Create a repository. Refuses one that already exists with
78    /// [`ForgeError::AlreadyExists`] — adopting it is a separate, elevated
79    /// decision (§5.6), not something a retry should do silently.
80    async fn create_repo(&self, spec: &RepoSpec) -> Result<RepoState>;
81
82    /// Archive a repository. Idempotent.
83    async fn archive_repo(&self, repo: &Resource) -> Result<()>;
84
85    // ── projection ───────────────────────────────────────────────────────
86
87    /// Rights → this forge's role for one person on one repository in `ns`.
88    /// The default asks `map` for a role and rounds it down onto the
89    /// namespace's ladder.
90    fn map_role(&self, ns: &Namespace, rights: EffectiveRights, map: &RoleMap) -> ForgeRole {
91        collapse_to_ladder(map.requested(rights), &self.capabilities(ns).role_levels)
92    }
93
94    /// Converge people's direct roles on a repository to `desired`.
95    /// Collaborators `desired` does not mention are handled per `unlisted`.
96    async fn apply_roles(
97        &self,
98        repo: &Resource,
99        desired: &[RoleAssignment],
100        unlisted: Unlisted,
101    ) -> Result<ApplyReport>;
102
103    /// The steps that turn commit trust on for this forge's CI.
104    fn bootstrap_plan(&self, repo: &RepoSpec, cfg: &VgiConfig) -> Result<Vec<BootstrapStep>>;
105
106    /// Run one step, check-then-apply.
107    async fn run_step(&self, repo: &Resource, step: &BootstrapStep) -> Result<StepOutcome>;
108
109    // ── events and drift ─────────────────────────────────────────────────
110
111    /// Verify and translate a webhook. `Ok(None)` for a verified delivery
112    /// the core has no use for; `Err` for one that failed verification —
113    /// which must not be acted on.
114    fn parse_event(&self, headers: &HeaderMap, body: &[u8]) -> Result<Option<ForgeEvent>>;
115
116    /// Compare observed state with the projection.
117    fn diff(&self, observed: &RepoState, desired: &Projection) -> Vec<Drift> {
118        default_diff(observed, desired)
119    }
120}