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}