1use std::path::PathBuf;
2
3use callisto_model::{Ecosystem, GroupName, ManifestError, PackageId, TagTemplateError, VersionParseError};
4
5pub use crate::locate::LocateError;
6
7#[derive(Clone, Debug, thiserror::Error, miette::Diagnostic, PartialEq, Eq)]
8#[allow(clippy::result_large_err)]
9#[non_exhaustive]
10pub enum GraphError {
11 #[error(transparent)]
12 #[diagnostic(transparent)]
13 Locate(#[from] LocateError),
14
15 #[error(transparent)]
16 #[diagnostic(transparent)]
17 Manifest(#[from] ManifestError),
18
19 #[error(transparent)]
20 #[diagnostic(transparent)]
21 Config(#[from] ConfigError),
22
23 #[error(transparent)]
24 #[diagnostic(transparent)]
25 Format(#[from] callisto_format::ParseError),
26
27 #[error("parsing changeset {}: {source}", .path.display())]
28 ParseChangeset {
29 path: PathBuf,
30 source: callisto_format::ParseError,
31 },
32
33 #[error(transparent)]
34 #[diagnostic(transparent)]
35 Bump(#[from] callisto_format::BumpError),
36
37 #[error(transparent)]
38 #[diagnostic(transparent)]
39 Changelog(#[from] callisto_changelog::ChangelogError),
40
41 #[cfg(feature = "inference")]
42 #[error(transparent)]
43 Conventional(#[from] callisto_conventional::ConventionalError),
44
45 #[error(transparent)]
46 TagTemplate(#[from] callisto_model::TagTemplateError),
47
48 #[error(transparent)]
49 VersionParse(#[from] callisto_model::VersionParseError),
50
51 #[error(transparent)]
52 #[diagnostic(transparent)]
53 Model(#[from] callisto_model::ModelError),
54
55 #[error(transparent)]
56 #[diagnostic(transparent)]
57 Vcs(#[from] callisto_vcs::VcsError),
58
59 #[error("command error: {0}")]
60 Command(#[from] callisto_model::CommandError),
61
62 #[error("package `{id}` is defined at multiple paths: {}", .paths.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(", "))]
63 #[diagnostic(code(E100), help("Ensure package IDs are unique across workspace manifest paths."))]
64 DuplicatePackage { id: PackageId, paths: Vec<PathBuf> },
65
66 #[error("package at `{path}` declares conflicting identities: {}", .ids.iter().map(|i| i.display_name()).collect::<Vec<_>>().join(", "))]
67 #[diagnostic(code(E101), help("Align package name declarations in manifest files."))]
68 SplitIdentity { path: PathBuf, ids: Vec<PackageId> },
69
70 #[error("package `{id}` was not found in the workspace")]
71 #[diagnostic(
72 code(E102),
73 help("Verify package is included in workspace members in callisto.toml.")
74 )]
75 UnknownPackage { id: PackageId },
76
77 #[error("name `{name}` is ambiguous in this workspace; candidates: {}", .candidates.iter().map(|c| c.display_name()).collect::<Vec<_>>().join(", "))]
78 #[diagnostic(
79 code(E103),
80 help("Use fully-qualified package ID with ecosystem prefix (e.g. cargo:pkg).")
81 )]
82 AmbiguousName { name: String, candidates: Vec<PackageId> },
83
84 #[error("dependency cycle detected: {}", .cycle.iter().map(|i| i.display_name()).collect::<Vec<_>>().join(" -> "))]
85 #[diagnostic(
86 code(E104),
87 help("Refactor workspace dependencies to break the cyclic dependency chain.")
88 )]
89 Cycle { cycle: Vec<PackageId> },
90
91 #[error("cascade failed to converge after {iterations} iterations")]
92 #[diagnostic(code(E105), help("Check for oscillating peer or linked group dependencies."))]
93 CascadeNotConverged { iterations: usize },
94
95 #[error("fixed group `{group}` members have divergent on-disk versions: {}", .members.iter().map(|(id, v)| format!("{}={}", id.display_name(), v.render())).collect::<Vec<_>>().join(", "))]
96 #[diagnostic(code(E106), help("Align on-disk versions for all members of the fixed group."))]
97 FixedGroupDivergent {
98 group: GroupName,
99 members: Vec<(PackageId, callisto_model::Version)>,
100 },
101
102 #[error("group `{group}` members use incompatible versioning grammars: {}", .members.iter().map(|(id, v)| format!("{}={:?}", id.display_name(), v.grammar())).collect::<Vec<_>>().join(", "))]
103 #[diagnostic(code(E107))]
104 GroupGrammarMismatch {
105 group: GroupName,
106 members: Vec<(PackageId, callisto_model::Version)>,
107 },
108
109 #[error("group `{group}` lists member `{member}`, which was not found in the workspace")]
110 #[diagnostic(code(E108))]
111 MissingGroupMember { group: GroupName, member: String },
112
113 #[error("package `{package}` is listed in multiple conflicting groups: {}", .groups.iter().map(|g| g.as_str()).collect::<Vec<_>>().join(", "))]
114 #[diagnostic(code(E109))]
115 ConflictingGroupMembership { package: PackageId, groups: Vec<GroupName> },
116
117 #[error("version dependency edge from `{from}` to `{to}` involves incompatible grammars: {source}")]
118 GrammarMismatch {
119 from: PackageId,
120 to: PackageId,
121 #[source]
122 source: callisto_model::GrammarMismatch,
123 },
124
125 #[error("on-disk versions changed since plan was generated for `{package}`: expected {}, found {}", .expected.render(), .found.render())]
126 OnDiskVersionDrift {
127 package: PackageId,
128 expected: callisto_model::Version,
129 found: callisto_model::Version,
130 },
131
132 #[error("cannot apply version plan: manifest `{}` is at version {}, expected {} (pre-apply) or {} (already applied — safe to retry)", .path.display(), .found.render(), .expected_from.render(), .expected_to.render())]
133 #[diagnostic(
134 code(E117),
135 help(
136 "The manifest version does not match the plan's from or to version. \
137 This may indicate the manifest was modified outside of callisto after the plan was generated."
138 )
139 )]
140 UnexpectedManifestVersion {
141 path: PathBuf,
142 expected_from: callisto_model::Version,
143 expected_to: callisto_model::Version,
144 found: callisto_model::Version,
145 },
146
147 #[error("workspace root `{root_manifest}` has conflicting version updates: {details}")]
148 WorkspaceVersionConflict { root_manifest: PathBuf, details: String },
149
150 #[error("failed to parse .changeset/pre.json: {0}")]
151 #[diagnostic(
152 code(E114),
153 help("Check that .changeset/pre.json is valid JSON and was not partially written. Delete the file and re-run `callisto pre enter` to recover.")
154 )]
155 PreJson(callisto_format::PreJsonError),
156
157 #[error("failed to read .changeset/pre.json: {message}")]
158 #[diagnostic(
159 code(E115),
160 help(
161 "Check that .changeset/pre.json is readable. Delete the file and re-run `callisto pre enter` to recover."
162 )
163 )]
164 PreJsonRead { message: String },
165
166 #[error("package `{package}` declares platform targets via both `{napi_source}` and `{maturin_source}`; only one source is allowed")]
167 #[diagnostic(
168 code(E118),
169 help("Remove one of the two target declarations -- either napi.targets in package.json or [tool.maturin].targets in pyproject.toml -- from the package's manifest.")
170 )]
171 ConflictingPlatformTargetSources {
172 package: PackageId,
173 napi_source: &'static str,
174 maturin_source: &'static str,
175 },
176
177 #[error(
178 "package `{package}` configures publish-to target `{target}` (ecosystem `{}`), but its detected ecosystem is `{}`",
179 .target_ecosystem.prefix(),
180 .package_ecosystems.iter().map(|e| e.prefix()).collect::<Vec<_>>().join(", ")
181 )]
182 #[diagnostic(
183 code(E119),
184 help("Remove the mismatched target from publish-to, or fix the [[package]]/[[package-set]] rule so it only matches packages in that ecosystem.")
185 )]
186 PublishTargetEcosystemMismatch {
187 package: PackageId,
188 target: String,
189 target_ecosystem: Ecosystem,
190 package_ecosystems: Vec<Ecosystem>,
191 },
192
193 #[error(
194 "package `{package}` sets `publishConfig.registry` to `{url}`, which is not an operator-approved npm registry"
195 )]
196 #[diagnostic(
197 code(E120),
198 help(
199 "`publishConfig.registry` in package.json is manifest-controlled data (a PR author \
200 can set it in their own package.json), not operator config, so it is never trusted \
201 verbatim as a publish destination. The URL must use the `https` scheme and must \
202 exactly match a `url` configured on an `npm`-kind entry in `[registries]` in \
203 callisto.toml. Add the registry there if it is a legitimate private registry, or \
204 remove the override from package.json."
205 )
206 )]
207 UntrustedNpmRegistry { package: PackageId, url: String },
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use callisto_vcs::VcsError;
214
215 #[test]
219 fn graph_error_vcs_is_transparent_no_prefix() {
220 let inner = VcsError::Git("some git error".to_string());
221 let expected_msg = format!("{inner}");
222 let graph_err = GraphError::Vcs(inner);
223 assert_eq!(
224 format!("{graph_err}"),
225 expected_msg,
226 "GraphError::Vcs must be transparent (no 'vcs error: ' prefix)"
227 );
228 }
229
230 #[test]
234 fn conflicting_platform_target_sources_message_names_package_and_both_sources() {
235 use callisto_model::PackageId;
236
237 let err = GraphError::ConflictingPlatformTargetSources {
238 package: PackageId::Bare("native-mod".to_string()),
239 napi_source: "napi.targets",
240 maturin_source: "[tool.maturin].targets",
241 };
242 let msg = format!("{err}");
243 assert!(msg.contains("native-mod"), "message must name the package: {msg}");
244 assert!(msg.contains("napi.targets"), "message must name napi_source: {msg}");
245 assert!(
246 msg.contains("[tool.maturin].targets"),
247 "message must name maturin_source: {msg}"
248 );
249 }
250}
251
252#[derive(Clone, Debug, thiserror::Error, miette::Diagnostic, PartialEq, Eq)]
253#[non_exhaustive]
254pub enum ConfigError {
255 #[error("failed to read `{path}`: {message}")]
256 #[diagnostic(code(E110))]
257 Read { path: PathBuf, message: String },
258
259 #[error("`{path}` is not valid TOML: {message}")]
260 #[diagnostic(code(E111), help("Verify callisto.toml TOML syntax formatting."))]
261 ParseToml { path: PathBuf, message: String },
262
263 #[error("[[package-set]] `{pattern}` matched no packages")]
264 PackageSetMatchedNothing { pattern: String },
265
266 #[error("[[package]] `{pattern}` matched no package")]
267 PackageMatchedNothing { pattern: String },
268
269 #[error("package `{package}` is claimed by more than one [[package-set]]: {}", .patterns.join(", "))]
270 OverlappingPackageSets { package: String, patterns: Vec<String> },
271
272 #[error("group `{group}` and group `{other}` both list `{member}`")]
273 ConflictingGroupNames {
274 group: GroupName,
275 other: GroupName,
276 member: String,
277 },
278
279 #[error("group `{group}` has no members")]
280 EmptyGroup { group: GroupName },
281
282 #[error("duplicate group name `{group}`")]
283 DuplicateGroupName { group: GroupName },
284
285 #[error("`publish-to` names registry key `{key}`, which no [registries.*] block defines")]
286 UnknownRegistry { key: String },
287
288 #[error("`{path}` sets unknown callisto key `{key}`")]
289 UnknownKey { path: PathBuf, key: String },
290
291 #[error("`cascade.bump-severity` is `{found}`; expected `patch` or `minor`")]
292 InvalidBumpSeverity { found: String },
293
294 #[error("`pre-major-inference` is `{found}`; expected `off`, `conservative`, or `conservative-feat`")]
295 InvalidPreMajorInference { found: String },
296
297 #[error(
298 "changesets.dir `{dir}` is an absolute path or contains `..` path components and would escape the workspace root"
299 )]
300 #[diagnostic(
301 code(E116),
302 help("Use a forward-slash-separated path relative to the workspace root that is not absolute and does not contain '..' components.")
303 )]
304 InvalidChangesetsDir { dir: String },
305
306 #[error("`changelog = \"{value}\"` on `{pattern}` is an absolute path or contains `..` path components and would escape the workspace root")]
307 #[diagnostic(
308 code(E113),
309 help("Use a forward-slash-separated path relative to the package root that does not contain '..' components.")
310 )]
311 InvalidChangelogPath { pattern: String, value: String },
312
313 #[error(transparent)]
314 Tag(#[from] TagTemplateError),
315
316 #[error(transparent)]
317 VersionParse(#[from] VersionParseError),
318}