Skip to main content

sr_core/publishers/
mod.rs

1//! Publishers — typed adapters that know how to check + push per registry.
2//!
3//! The `Publisher` trait carries two operations: `check` queries the target
4//! registry for the desired version (answering "is work needed?") and `run`
5//! invokes the publishing tool. Built-in publishers (`cargo`, `npm`,
6//! `docker`, `pypi`, `go`) know their registry's API and the command they
7//! shell out to. `Custom` is the escape hatch for arbitrary commands.
8//!
9//! Publishers do not fetch from files or manage working directories beyond
10//! the package path; the dispatcher reads package metadata (e.g. the
11//! Cargo.toml package name) and hands it to the publisher.
12
13use crate::config::{PackageConfig, PublishConfig};
14use crate::error::ReleaseError;
15
16pub mod cargo;
17pub mod custom;
18pub mod docker;
19pub mod go;
20pub mod npm;
21pub mod pypi;
22
23/// The reconciler verdict from a publisher's state check.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum PublishState {
26    /// Registry already has this version — no work needed.
27    Completed,
28    /// Version is absent from the registry — run() should execute.
29    Needed,
30    /// Could not determine (network error, auth failure, registry 5xx,
31    /// rate limit, unsupported). `run()` should still execute: the publish
32    /// command itself is the authoritative check, and every built-in
33    /// publish tool (`cargo publish`, `npm publish`, `uv publish`,
34    /// `docker push`) is idempotent against an already-published version
35    /// and exits non-zero with a distinct "already exists" message that
36    /// surfaces in the job log. That trade-off is deliberate: a transient
37    /// crates.io 503 should not block a release.
38    Unknown(String),
39}
40
41/// Context handed to publishers for check + run. Everything a publisher
42/// needs to decide "is work needed?" and "what command should I run?".
43pub struct PublishCtx<'a> {
44    /// Package config — publishers may read `path` and `version_files` to
45    /// derive a package name or dockerfile.
46    pub package: &'a PackageConfig,
47    /// Version string being released (e.g. "1.2.3").
48    pub version: &'a str,
49    /// Full tag name (e.g. "v1.2.3").
50    pub tag: &'a str,
51    /// Dry run — publishers must not mutate state when true. `check` is
52    /// always safe; `run` becomes a log-only preview.
53    pub dry_run: bool,
54    /// Env vars forwarded to any shell invocation (SR_VERSION, SR_TAG, etc.).
55    pub env: &'a [(&'a str, &'a str)],
56}
57
58pub trait Publisher {
59    /// Machine-readable name, for logs.
60    fn name(&self) -> &'static str;
61
62    /// Query the registry for the current state.
63    fn check(&self, ctx: &PublishCtx<'_>) -> Result<PublishState, ReleaseError>;
64
65    /// Perform the publish. Callers must have checked `check()` first;
66    /// a `Completed` result should short-circuit before calling `run`.
67    fn run(&self, ctx: &PublishCtx<'_>) -> Result<(), ReleaseError>;
68}
69
70/// Dispatch `PublishConfig` → boxed `Publisher`. Returns a trait object so
71/// the Publish stage can call `check`/`run` uniformly.
72pub fn publisher_for(cfg: &PublishConfig) -> Box<dyn Publisher> {
73    match cfg {
74        PublishConfig::Cargo {
75            features,
76            registry,
77            workspace,
78        } => Box::new(cargo::CargoPublisher {
79            features: features.clone(),
80            registry: registry.clone(),
81            workspace: *workspace,
82        }),
83        PublishConfig::Npm {
84            registry,
85            access,
86            workspace,
87        } => Box::new(npm::NpmPublisher {
88            registry: registry.clone(),
89            access: access.clone(),
90            workspace: *workspace,
91        }),
92        PublishConfig::Docker {
93            image,
94            platforms,
95            dockerfile,
96        } => Box::new(docker::DockerPublisher {
97            image: image.clone(),
98            platforms: platforms.clone(),
99            dockerfile: dockerfile.clone(),
100        }),
101        PublishConfig::Pypi {
102            repository,
103            workspace,
104            dist_dir,
105        } => Box::new(pypi::PypiPublisher {
106            repository: repository.clone(),
107            workspace: *workspace,
108            dist_dir: dist_dir.clone(),
109        }),
110        PublishConfig::Go => Box::new(go::GoPublisher),
111        PublishConfig::Custom {
112            command,
113            check,
114            cwd,
115        } => Box::new(custom::CustomPublisher {
116            command: command.clone(),
117            check: check.clone(),
118            cwd: cwd.clone(),
119        }),
120    }
121}