1#![forbid(clippy::indexing_slicing)]
2
3use std::collections::BTreeMap;
57use std::collections::BTreeSet;
58use std::env;
59use std::fmt;
60use std::fs;
61use std::path::Path;
62use std::path::PathBuf;
63
64pub mod analysis;
65pub mod git;
66pub mod lint;
67
68pub use analysis::*;
69use ignore::gitignore::Gitignore;
70use ignore::gitignore::GitignoreBuilder;
71use semver::Version;
72use serde::Deserialize;
73use serde::Serialize;
74use thiserror::Error;
75
76pub type MonochangeResult<T> = Result<T, MonochangeError>;
77
78pub const DEFAULT_RELEASE_TITLE_PRIMARY: &str = "{{ version }} ({{ date }})";
80pub const DEFAULT_RELEASE_TITLE_NAMESPACED: &str = "{{ id }} {{ version }} ({{ date }})";
82pub const DEFAULT_CHANGELOG_VERSION_TITLE_PRIMARY: &str =
84 "{% if tag_url %}[{{ version }}]({{ tag_url }}){% else %}{{ version }}{% endif %} ({{ date }})";
85pub const DEFAULT_CHANGELOG_VERSION_TITLE_NAMESPACED: &str = "{% if tag_url %}{{ id }} [{{ version }}]({{ tag_url }}){% else %}{{ id }} {{ version }}{% endif %} ({{ date }})";
87
88pub const DEFAULT_INITIAL_CHANGELOG_HEADER_MONOCHANGE: &str = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThis changelog is managed by [monochange](https://github.com/monochange/monochange).";
90pub const DEFAULT_INITIAL_CHANGELOG_HEADER_KEEP_A_CHANGELOG: &str = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).";
92
93#[derive(Debug, Error)]
94#[non_exhaustive]
95pub enum MonochangeError {
96 #[error("io error: {0}")]
97 Io(String),
98 #[error("config error: {0}")]
99 Config(String),
100 #[error("discovery error: {0}")]
101 Discovery(String),
102 #[error("{0}")]
103 Diagnostic(String),
104 #[error("io error at {path:?}: {source}")]
105 IoSource {
106 path: PathBuf,
107 source: std::io::Error,
108 },
109 #[error("parse error at {path:?}: {source}")]
110 Parse {
111 path: PathBuf,
112 source: Box<dyn std::error::Error + Send + Sync>,
113 },
114 #[cfg(feature = "http")]
115 #[error("http error {context}: {source}")]
116 HttpRequest {
117 context: String,
118 source: reqwest::Error,
119 },
120 #[error("interactive error: {message}")]
121 Interactive { message: String },
122 #[error("cancelled")]
123 Cancelled,
124}
125
126impl MonochangeError {
127 #[must_use]
129 pub fn render(&self) -> String {
130 match self {
131 Self::Diagnostic(report) => report.clone(),
132 Self::IoSource { path, source } => {
133 format!("io error at {}: {source}", path.display())
134 }
135 Self::Parse { path, source } => {
136 format!("parse error at {}: {source}", path.display())
137 }
138 #[cfg(feature = "http")]
139 Self::HttpRequest { context, source } => {
140 format!("http error {context}: {source}")
141 }
142 Self::Interactive { message } => message.clone(),
143 Self::Cancelled => "cancelled".to_string(),
144 _ => self.to_string(),
145 }
146 }
147}
148
149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
150#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
151#[serde(rename_all = "snake_case")]
152#[non_exhaustive]
153pub enum BumpSeverity {
154 None,
155 #[default]
156 Patch,
157 Minor,
158 Major,
159}
160
161impl BumpSeverity {
162 #[must_use]
164 pub fn is_release(self) -> bool {
165 self != Self::None
166 }
167
168 #[must_use]
173 pub fn is_pre_stable(version: &Version) -> bool {
174 version.major == 0
175 }
176
177 #[must_use]
179 pub fn apply_to_version(self, version: &Version) -> Version {
180 let effective = if Self::is_pre_stable(version) {
181 match self {
182 Self::Major => Self::Minor,
183 Self::Minor => Self::Patch,
184 other => other,
185 }
186 } else {
187 self
188 };
189
190 let mut next = version.clone();
191 match effective {
192 Self::None => next,
193 Self::Patch => {
194 next.patch += 1;
195 next.pre = semver::Prerelease::EMPTY;
196 next.build = semver::BuildMetadata::EMPTY;
197 next
198 }
199 Self::Minor => {
200 next.minor += 1;
201 next.patch = 0;
202 next.pre = semver::Prerelease::EMPTY;
203 next.build = semver::BuildMetadata::EMPTY;
204 next
205 }
206 Self::Major => {
207 next.major += 1;
208 next.minor = 0;
209 next.patch = 0;
210 next.pre = semver::Prerelease::EMPTY;
211 next.build = semver::BuildMetadata::EMPTY;
212 next
213 }
214 }
215 }
216}
217
218impl fmt::Display for BumpSeverity {
219 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
220 formatter.write_str(match self {
221 Self::None => "none",
222 Self::Patch => "patch",
223 Self::Minor => "minor",
224 Self::Major => "major",
225 })
226 }
227}
228
229#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
230#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
231#[serde(rename_all = "snake_case")]
232#[non_exhaustive]
233pub enum Ecosystem {
234 Cargo,
235 Npm,
236 Deno,
237 Dart,
238 Flutter,
239 Python,
240 Go,
241}
242
243impl Ecosystem {
244 #[must_use]
246 pub fn as_str(self) -> &'static str {
247 match self {
248 Self::Cargo => "cargo",
249 Self::Npm => "npm",
250 Self::Deno => "deno",
251 Self::Dart => "dart",
252 Self::Flutter => "flutter",
253 Self::Python => "python",
254 Self::Go => "go",
255 }
256 }
257}
258
259impl From<EcosystemType> for Ecosystem {
260 fn from(value: EcosystemType) -> Self {
261 match value {
262 EcosystemType::Cargo => Self::Cargo,
263 EcosystemType::Npm => Self::Npm,
264 EcosystemType::Deno => Self::Deno,
265 EcosystemType::Dart => Self::Dart,
266 EcosystemType::Python => Self::Python,
267 EcosystemType::Go => Self::Go,
268 }
269 }
270}
271
272impl From<PackageType> for Ecosystem {
273 fn from(value: PackageType) -> Self {
274 match value {
275 PackageType::Cargo => Self::Cargo,
276 PackageType::Npm => Self::Npm,
277 PackageType::Deno => Self::Deno,
278 PackageType::Dart => Self::Dart,
279 PackageType::Flutter => Self::Flutter,
280 PackageType::Python => Self::Python,
281 PackageType::Go => Self::Go,
282 }
283 }
284}
285
286impl fmt::Display for Ecosystem {
287 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
288 formatter.write_str(self.as_str())
289 }
290}
291
292impl std::str::FromStr for Ecosystem {
293 type Err = ();
294
295 fn from_str(string: &str) -> Result<Self, Self::Err> {
296 match string {
297 "cargo" => Ok(Self::Cargo),
298 "npm" => Ok(Self::Npm),
299 "deno" => Ok(Self::Deno),
300 "dart" => Ok(Self::Dart),
301 "flutter" => Ok(Self::Flutter),
302 "python" => Ok(Self::Python),
303 "go" => Ok(Self::Go),
304 _ => Err(()),
305 }
306 }
307}
308
309#[must_use]
310pub fn default_registry_kind_for_ecosystem(ecosystem: Ecosystem) -> Option<RegistryKind> {
311 match ecosystem {
312 Ecosystem::Cargo => Some(RegistryKind::CratesIo),
313 Ecosystem::Npm => Some(RegistryKind::Npm),
314 Ecosystem::Deno => Some(RegistryKind::Jsr),
315 Ecosystem::Dart | Ecosystem::Flutter => Some(RegistryKind::PubDev),
316 Ecosystem::Python => Some(RegistryKind::Pypi),
317 Ecosystem::Go => Some(RegistryKind::GoProxy),
318 }
319}
320
321#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
322#[serde(rename_all = "snake_case")]
323#[non_exhaustive]
324pub enum PublishState {
325 Public,
326 Private,
327 Unpublished,
328 Excluded,
329}
330
331#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
332#[serde(rename_all = "snake_case")]
333#[non_exhaustive]
334pub enum DependencyKind {
335 Runtime,
336 Development,
337 Build,
338 Peer,
339 Workspace,
340 Unknown,
341}
342
343impl fmt::Display for DependencyKind {
344 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
345 formatter.write_str(match self {
346 Self::Runtime => "runtime",
347 Self::Development => "development",
348 Self::Build => "build",
349 Self::Peer => "peer",
350 Self::Workspace => "workspace",
351 Self::Unknown => "unknown",
352 })
353 }
354}
355
356#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
357#[serde(rename_all = "snake_case")]
358#[non_exhaustive]
359pub enum DependencySourceKind {
360 Manifest,
361 Workspace,
362 Transitive,
363}
364
365#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
366pub struct PackageDependency {
367 pub name: String,
368 pub kind: DependencyKind,
369 pub version_constraint: Option<String>,
370 pub optional: bool,
371}
372
373#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
374pub struct PackageRecord {
375 pub id: String,
376 pub name: String,
377 pub ecosystem: Ecosystem,
378 pub manifest_path: PathBuf,
379 pub workspace_root: PathBuf,
380 pub current_version: Option<Version>,
381 pub publish_state: PublishState,
382 pub version_group_id: Option<String>,
383 pub metadata: BTreeMap<String, String>,
384 pub declared_dependencies: Vec<PackageDependency>,
385}
386
387impl PackageRecord {
388 #[allow(clippy::needless_pass_by_value)]
389 #[must_use]
391 pub fn new(
392 ecosystem: Ecosystem,
393 name: impl Into<String>,
394 manifest_path: PathBuf,
395 workspace_root: PathBuf,
396 current_version: Option<Version>,
397 publish_state: PublishState,
398 ) -> Self {
399 let name = name.into();
400 let normalized_workspace_root = normalize_path(&workspace_root);
401 let normalized_manifest_path = normalize_path(&manifest_path);
402 let id_path = relative_to_root(&normalized_workspace_root, &normalized_manifest_path)
403 .unwrap_or_else(|| normalized_manifest_path.clone());
404 let id = format!("{}:{}", ecosystem.as_str(), id_path.to_string_lossy());
405
406 Self {
407 id,
408 name,
409 ecosystem,
410 manifest_path: normalized_manifest_path,
411 workspace_root: normalized_workspace_root,
412 current_version,
413 publish_state,
414 version_group_id: None,
415 metadata: BTreeMap::new(),
416 declared_dependencies: Vec::new(),
417 }
418 }
419
420 #[must_use]
422 pub fn relative_manifest_path(&self, root: &Path) -> Option<PathBuf> {
423 relative_to_root(root, &self.manifest_path)
424 }
425}
426
427#[must_use]
429pub fn normalize_path(path: &Path) -> PathBuf {
430 let absolute = if path.is_absolute() {
431 path.to_path_buf()
432 } else {
433 env::current_dir().map_or_else(|_| path.to_path_buf(), |cwd| cwd.join(path))
434 };
435 fs::canonicalize(&absolute).unwrap_or(absolute)
436}
437
438#[must_use]
440pub fn relative_to_root(root: &Path, path: &Path) -> Option<PathBuf> {
441 let normalized_root = normalize_path(root);
442 let normalized_path = normalize_path(path);
443 normalized_path
444 .strip_prefix(&normalized_root)
445 .ok()
446 .map(Path::to_path_buf)
447}
448
449#[derive(Clone, Debug)]
450pub struct DiscoveryPathFilter {
451 root: PathBuf,
452 gitignore: Gitignore,
453}
454
455impl DiscoveryPathFilter {
456 #[must_use]
458 pub fn new(root: &Path) -> Self {
459 let root = normalize_path(root);
460 let mut builder = GitignoreBuilder::new(&root);
461 for path in [root.join(".gitignore"), root.join(".git/info/exclude")] {
462 if path.is_file() {
463 let _ = builder.add(path);
464 }
465 }
466 let gitignore = builder.build().unwrap_or_else(|_| Gitignore::empty());
467
468 Self { root, gitignore }
469 }
470
471 #[must_use]
473 pub fn allows(&self, path: &Path) -> bool {
474 !self.is_ignored(path, path.is_dir())
475 }
476
477 #[must_use]
479 pub fn should_descend(&self, path: &Path) -> bool {
480 !self.is_ignored(path, true)
481 }
482
483 fn is_ignored(&self, path: &Path, is_dir: bool) -> bool {
484 if ignored_discovery_dir_name(path) || self.has_nested_git_worktree_ancestor(path, is_dir) {
485 return true;
486 }
487
488 self.matches_gitignore(path, is_dir)
489 }
490
491 fn matches_gitignore(&self, path: &Path, is_dir: bool) -> bool {
492 let normalized_path = normalize_path(path);
493 normalized_path
494 .strip_prefix(&self.root)
495 .ok()
496 .is_some_and(|relative| {
497 self.gitignore
498 .matched_path_or_any_parents(relative, is_dir)
499 .is_ignore()
500 })
501 }
502
503 fn has_nested_git_worktree_ancestor(&self, path: &Path, is_dir: bool) -> bool {
504 let normalized_path = normalize_path(path);
505 let mut current = if is_dir {
506 normalized_path.clone()
507 } else {
508 normalized_path
509 .parent()
510 .unwrap_or(&normalized_path)
511 .to_path_buf()
512 };
513
514 while current.starts_with(&self.root) && current != self.root {
515 if current.join(".git").exists() {
516 return true;
517 }
518 let Some(parent) = current.parent() else {
519 break;
520 };
521 current = parent.to_path_buf();
522 }
523
524 false
525 }
526}
527
528fn ignored_discovery_dir_name(path: &Path) -> bool {
529 path.components().any(|component| {
530 component.as_os_str().to_str().is_some_and(|name| {
531 matches!(
532 name,
533 ".git" | "target" | "node_modules" | ".devenv" | ".claude" | "book"
534 )
535 })
536 })
537}
538
539#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
540pub struct DependencyEdge {
541 pub from_package_id: String,
542 pub to_package_id: String,
543 pub dependency_kind: DependencyKind,
544 pub source_kind: DependencySourceKind,
545 pub version_constraint: Option<String>,
546 pub is_optional: bool,
547 pub is_direct: bool,
548}
549
550#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
551#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
552#[serde(rename_all = "snake_case")]
553#[non_exhaustive]
554pub enum PackageType {
555 Cargo,
556 Npm,
557 Deno,
558 Dart,
559 Flutter,
560 Python,
561 Go,
562}
563
564impl PackageType {
565 #[must_use]
567 pub fn as_str(self) -> &'static str {
568 match self {
569 Self::Cargo => "cargo",
570 Self::Npm => "npm",
571 Self::Deno => "deno",
572 Self::Dart => "dart",
573 Self::Flutter => "flutter",
574 Self::Python => "python",
575 Self::Go => "go",
576 }
577 }
578}
579
580#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
581#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
582#[serde(rename_all = "snake_case")]
583#[non_exhaustive]
584pub enum VersionFormat {
585 #[default]
586 Namespaced,
587 Primary,
588}
589
590#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
591#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
592#[serde(rename_all = "snake_case")]
593#[non_exhaustive]
594pub enum EcosystemType {
595 Cargo,
596 Npm,
597 Deno,
598 Dart,
599 Python,
600 Go,
601}
602
603impl EcosystemType {
604 #[must_use]
611 #[deprecated(
612 since = "0.3.5",
613 note = "Use the ecosystem crate's `default_dependency_version_prefix()` instead"
614 )]
615 pub fn default_prefix(self) -> &'static str {
616 match self {
617 Self::Cargo | Self::Go => "",
618 Self::Npm | Self::Deno | Self::Dart => "^",
619 Self::Python => ">=",
620 }
621 }
622
623 #[must_use]
630 #[deprecated(
631 since = "0.3.5",
632 note = "Use the ecosystem crate's `default_dependency_fields()` instead"
633 )]
634 pub fn default_fields(self) -> &'static [&'static str] {
635 match self {
636 Self::Cargo => &["dependencies", "dev-dependencies", "build-dependencies"],
637 Self::Npm => &["dependencies", "devDependencies", "peerDependencies"],
638 Self::Deno => &["imports"],
639 Self::Dart => &["dependencies", "dev_dependencies"],
640 Self::Python => &["dependencies"],
641 Self::Go => &["require"],
642 }
643 }
644}
645
646#[derive(Clone, Copy, Debug, Eq, PartialEq)]
647struct JsonSpan {
648 start: usize,
649 end: usize,
650}
651
652pub fn strip_json_comments(contents: &str) -> String {
654 let bytes = contents.as_bytes();
655 let mut output = String::with_capacity(contents.len());
656 let mut cursor = 0usize;
657 while let Some(&byte) = bytes.get(cursor) {
658 if byte == b'"' {
659 let start = cursor;
660 cursor += 1;
661 while let Some(&string_byte) = bytes.get(cursor) {
662 cursor += 1;
663 if string_byte == b'\\' {
664 cursor += usize::from(bytes.get(cursor).is_some());
665 continue;
666 }
667 if string_byte == b'"' {
668 break;
669 }
670 }
671 output.push_str(&contents[start..cursor]);
672 continue;
673 }
674 if byte == b'/' && bytes.get(cursor + 1) == Some(&b'/') {
675 cursor += 2;
676 while let Some(&line_byte) = bytes.get(cursor) {
677 if line_byte == b'\n' {
678 break;
679 }
680 cursor += 1;
681 }
682 continue;
683 }
684 if byte == b'/' && bytes.get(cursor + 1) == Some(&b'*') {
685 cursor += 2;
686 while bytes.get(cursor).is_some() {
687 if bytes.get(cursor) == Some(&b'*') && bytes.get(cursor + 1) == Some(&b'/') {
688 cursor += 2;
689 break;
690 }
691 cursor += 1;
692 }
693 continue;
694 }
695 output.push(char::from(byte));
696 cursor += 1;
697 }
698 output
699}
700
701#[must_use = "the manifest update result must be checked"]
703pub fn update_json_manifest_text(
704 contents: &str,
705 owner_version: Option<&str>,
706 fields: &[&str],
707 versioned_deps: &BTreeMap<String, String>,
708) -> MonochangeResult<String> {
709 let root_start = json_root_object_start(contents)?;
710 let mut replacements = Vec::<(JsonSpan, String)>::new();
711 if let Some(owner_version) = owner_version
712 && let Some(span) = find_json_object_field_value_span(contents, root_start, "version")?
713 .filter(|span| json_span_is_string(contents, *span))
714 {
715 replacements.push((span, render_json_string(owner_version)?));
716 }
717 for field in fields {
718 let Some(field_span) = find_json_path_value_span(contents, root_start, field)? else {
719 continue;
720 };
721 if json_span_is_object(contents, field_span) {
722 for (dep_name, dep_version) in versioned_deps {
723 let Some(dep_span) =
724 find_json_object_field_value_span(contents, field_span.start, dep_name)?
725 .filter(|span| json_span_is_string(contents, *span))
726 else {
727 continue;
728 };
729 replacements.push((dep_span, render_json_string(dep_version)?));
730 }
731 continue;
732 }
733 if let Some(owner_version) = owner_version
734 && json_span_is_string(contents, field_span)
735 {
736 replacements.push((field_span, render_json_string(owner_version)?));
737 }
738 }
739 apply_json_replacements(contents, replacements)
740}
741
742fn render_json_string(value: &str) -> MonochangeResult<String> {
743 serde_json::to_string(value).map_err(|error| MonochangeError::Config(error.to_string()))
744}
745
746fn apply_json_replacements(
747 contents: &str,
748 mut replacements: Vec<(JsonSpan, String)>,
749) -> MonochangeResult<String> {
750 replacements.sort_by_key(|right| std::cmp::Reverse(right.0.start));
751 let mut updated = contents.to_string();
752 for (span, replacement) in replacements {
753 if span.start > span.end || span.end > updated.len() {
754 return Err(MonochangeError::Config(
755 "json edit range was out of bounds".to_string(),
756 ));
757 }
758 updated.replace_range(span.start..span.end, &replacement);
759 }
760 Ok(updated)
761}
762
763fn json_root_object_start(contents: &str) -> MonochangeResult<usize> {
764 let start = skip_json_ws_and_comments(contents, 0);
765 if contents.as_bytes().get(start) == Some(&b'{') {
766 Ok(start)
767 } else {
768 Err(MonochangeError::Config(
769 "expected JSON object at document root".to_string(),
770 ))
771 }
772}
773
774fn find_json_path_value_span(
775 contents: &str,
776 root_start: usize,
777 path: &str,
778) -> MonochangeResult<Option<JsonSpan>> {
779 let mut segments = path.split('.').filter(|segment| !segment.is_empty());
780 let Some(first) = segments.next() else {
781 return Ok(None);
782 };
783 let Some(mut span) = find_json_object_field_value_span(contents, root_start, first)? else {
784 return Ok(None);
785 };
786 for segment in segments {
787 if !json_span_is_object(contents, span) {
788 return Ok(None);
789 }
790 let Some(next_span) = find_json_object_field_value_span(contents, span.start, segment)?
791 else {
792 return Ok(None);
793 };
794 span = next_span;
795 }
796 Ok(Some(span))
797}
798
799fn find_json_object_field_value_span(
800 contents: &str,
801 object_start: usize,
802 key: &str,
803) -> MonochangeResult<Option<JsonSpan>> {
804 let bytes = contents.as_bytes();
805 if bytes.get(object_start) != Some(&b'{') {
806 return Err(MonochangeError::Config(
807 "expected JSON object when locating field".to_string(),
808 ));
809 }
810 let mut cursor = object_start + 1;
811 loop {
812 cursor = skip_json_ws_and_comments(contents, cursor);
813 match bytes.get(cursor) {
814 Some(b'}') => return Ok(None),
815 Some(b'"') => {}
816 Some(_) => {
817 return Err(MonochangeError::Config(
818 "expected JSON object key".to_string(),
819 ));
820 }
821 None => {
822 return Err(MonochangeError::Config(
823 "unterminated JSON object".to_string(),
824 ));
825 }
826 }
827 let (key_span, next) = parse_json_string_span(contents, cursor)?;
828 let key_text = &contents[key_span.start..key_span.end];
829 cursor = skip_json_ws_and_comments(contents, next);
830 if bytes.get(cursor) != Some(&b':') {
831 return Err(MonochangeError::Config(
832 "expected `:` after JSON object key".to_string(),
833 ));
834 }
835 cursor = skip_json_ws_and_comments(contents, cursor + 1);
836 let value_start = cursor;
837 let value_end = skip_json_value(contents, value_start)?;
838 if key_text == key {
839 return Ok(Some(JsonSpan {
840 start: value_start,
841 end: value_end,
842 }));
843 }
844 cursor = skip_json_ws_and_comments(contents, value_end);
845 match bytes.get(cursor) {
846 Some(b',') => {
847 cursor += 1;
848 }
849 Some(b'}') => return Ok(None),
850 Some(_) => {
851 return Err(MonochangeError::Config(
852 "expected `,` or `}` after JSON object value".to_string(),
853 ));
854 }
855 None => {
856 return Err(MonochangeError::Config(
857 "unterminated JSON object".to_string(),
858 ));
859 }
860 }
861 }
862}
863
864fn skip_json_value(contents: &str, start: usize) -> MonochangeResult<usize> {
865 let bytes = contents.as_bytes();
866 let cursor = skip_json_ws_and_comments(contents, start);
867 match bytes.get(cursor) {
868 Some(b'"') => parse_json_string_span(contents, cursor).map(|(_, next)| next),
869 Some(b'{') => skip_json_object(contents, cursor),
870 Some(b'[') => skip_json_array(contents, cursor),
871 Some(_) => Ok(skip_json_primitive(contents, cursor)),
872 None => {
873 Err(MonochangeError::Config(
874 "unexpected end of JSON input".to_string(),
875 ))
876 }
877 }
878}
879
880fn skip_json_object(contents: &str, object_start: usize) -> MonochangeResult<usize> {
881 let bytes = contents.as_bytes();
882 let mut cursor = object_start + 1;
883 loop {
884 cursor = skip_json_ws_and_comments(contents, cursor);
885 match bytes.get(cursor) {
886 Some(b'}') => return Ok(cursor + 1),
887 Some(b'"') => {}
888 Some(_) => {
889 return Err(MonochangeError::Config(
890 "expected JSON object key".to_string(),
891 ));
892 }
893 None => {
894 return Err(MonochangeError::Config(
895 "unterminated JSON object".to_string(),
896 ));
897 }
898 }
899 let (_, next) = parse_json_string_span(contents, cursor)?;
900 cursor = skip_json_ws_and_comments(contents, next);
901 if bytes.get(cursor) != Some(&b':') {
902 return Err(MonochangeError::Config(
903 "expected `:` after JSON object key".to_string(),
904 ));
905 }
906 cursor = skip_json_value(contents, cursor + 1)?;
907 cursor = skip_json_ws_and_comments(contents, cursor);
908 match bytes.get(cursor) {
909 Some(b',') => {
910 cursor += 1;
911 }
912 Some(b'}') => return Ok(cursor + 1),
913 Some(_) => {
914 return Err(MonochangeError::Config(
915 "expected `,` or `}` after JSON object value".to_string(),
916 ));
917 }
918 None => {
919 return Err(MonochangeError::Config(
920 "unterminated JSON object".to_string(),
921 ));
922 }
923 }
924 }
925}
926
927fn skip_json_array(contents: &str, array_start: usize) -> MonochangeResult<usize> {
928 let bytes = contents.as_bytes();
929 let mut cursor = array_start + 1;
930 loop {
931 cursor = skip_json_ws_and_comments(contents, cursor);
932 match bytes.get(cursor) {
933 Some(b']') => return Ok(cursor + 1),
934 Some(_) => {
935 cursor = skip_json_value(contents, cursor)?;
936 cursor = skip_json_ws_and_comments(contents, cursor);
937 match bytes.get(cursor) {
938 Some(b',') => {
939 cursor += 1;
940 }
941 Some(b']') => return Ok(cursor + 1),
942 Some(_) => {
943 return Err(MonochangeError::Config(
944 "expected `,` or `]` after JSON array value".to_string(),
945 ));
946 }
947 None => {
948 return Err(MonochangeError::Config(
949 "unterminated JSON array".to_string(),
950 ));
951 }
952 }
953 }
954 None => {
955 return Err(MonochangeError::Config(
956 "unterminated JSON array".to_string(),
957 ));
958 }
959 }
960 }
961}
962
963fn skip_json_primitive(contents: &str, start: usize) -> usize {
964 let bytes = contents.as_bytes();
965 let mut cursor = start;
966 while let Some(&byte) = bytes.get(cursor) {
967 if matches!(byte, b',' | b'}' | b']') || byte.is_ascii_whitespace() {
968 break;
969 }
970 if byte == b'/' && matches!(bytes.get(cursor + 1), Some(b'/' | b'*')) {
971 break;
972 }
973 cursor += 1;
974 }
975 cursor
976}
977
978fn parse_json_string_span(contents: &str, start: usize) -> MonochangeResult<(JsonSpan, usize)> {
979 let bytes = contents.as_bytes();
980 if bytes.get(start) != Some(&b'"') {
981 return Err(MonochangeError::Config("expected JSON string".to_string()));
982 }
983 let mut cursor = start + 1;
984 while let Some(&byte) = bytes.get(cursor) {
985 if byte == b'\\' {
986 let Some(&escape_char) = bytes.get(cursor + 1) else {
988 return Err(MonochangeError::Config(
989 "unterminated escape sequence in JSON string".to_string(),
990 ));
991 };
992 if escape_char == b'u' {
993 for offset in 2..6 {
995 match bytes.get(cursor + offset) {
996 Some(b) if b.is_ascii_hexdigit() => {}
997 Some(_) => {
998 return Err(MonochangeError::Config(format!(
999 "invalid unicode escape sequence in JSON string: expected hex digit at position {}",
1000 cursor + offset
1001 )));
1002 }
1003 None => {
1004 return Err(MonochangeError::Config(
1005 "incomplete unicode escape sequence in JSON string".to_string(),
1006 ));
1007 }
1008 }
1009 }
1010 cursor += 6;
1011 } else {
1012 cursor += 2;
1013 }
1014 continue;
1015 }
1016 if byte == b'"' {
1017 return Ok((
1018 JsonSpan {
1019 start: start + 1,
1020 end: cursor,
1021 },
1022 cursor + 1,
1023 ));
1024 }
1025 cursor += 1;
1026 }
1027 Err(MonochangeError::Config(
1028 "unterminated JSON string".to_string(),
1029 ))
1030}
1031
1032fn skip_json_ws_and_comments(contents: &str, start: usize) -> usize {
1033 let bytes = contents.as_bytes();
1034 let mut cursor = start;
1035 loop {
1036 while let Some(&byte) = bytes.get(cursor) {
1037 if !byte.is_ascii_whitespace() {
1038 break;
1039 }
1040 cursor += 1;
1041 }
1042 if bytes.get(cursor) == Some(&b'/') && bytes.get(cursor + 1) == Some(&b'/') {
1043 cursor += 2;
1044 while let Some(&byte) = bytes.get(cursor) {
1045 if byte == b'\n' {
1046 break;
1047 }
1048 cursor += 1;
1049 }
1050 continue;
1051 }
1052 if bytes.get(cursor) == Some(&b'/') && bytes.get(cursor + 1) == Some(&b'*') {
1053 cursor += 2;
1054 while bytes.get(cursor).is_some() {
1055 if bytes.get(cursor) == Some(&b'*') && bytes.get(cursor + 1) == Some(&b'/') {
1056 cursor += 2;
1057 break;
1058 }
1059 cursor += 1;
1060 }
1061 continue;
1062 }
1063 break;
1064 }
1065 cursor
1066}
1067
1068fn json_span_is_string(contents: &str, span: JsonSpan) -> bool {
1069 contents.as_bytes().get(span.start) == Some(&b'"')
1070 && span.end > span.start
1071 && contents.as_bytes().get(span.end - 1) == Some(&b'"')
1072}
1073
1074fn json_span_is_object(contents: &str, span: JsonSpan) -> bool {
1075 contents.as_bytes().get(span.start) == Some(&b'{')
1076 && span.end > span.start
1077 && contents.as_bytes().get(span.end - 1) == Some(&b'}')
1078}
1079
1080#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1081#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
1082pub struct VersionedFileDefinition {
1083 pub path: String,
1084 #[serde(rename = "type", default)]
1085 pub ecosystem_type: Option<EcosystemType>,
1086 #[serde(default)]
1087 pub prefix: Option<String>,
1088 #[serde(default)]
1089 pub fields: Option<Vec<String>>,
1090 #[serde(default)]
1091 pub name: Option<String>,
1092 #[serde(default)]
1093 pub regex: Option<String>,
1094}
1095
1096impl VersionedFileDefinition {
1097 #[must_use]
1099 pub fn uses_regex(&self) -> bool {
1100 self.regex.is_some()
1101 }
1102}
1103
1104#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1105#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1106pub enum ChangelogDefinition {
1107 Disabled,
1108 PackageDefault,
1109 PathPattern(String),
1110}
1111
1112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1113#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
1114#[serde(rename_all = "snake_case")]
1115#[non_exhaustive]
1116pub enum ChangelogFormat {
1117 #[default]
1118 Monochange,
1119 KeepAChangelog,
1120}
1121
1122#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1123#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1124pub struct ChangelogTarget {
1125 pub path: PathBuf,
1126 #[serde(default)]
1127 pub format: ChangelogFormat,
1128 #[serde(default, skip_serializing_if = "Option::is_none")]
1129 pub initial_header: Option<String>,
1130}
1131
1132#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1133#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1134pub struct ReleaseNotesSection {
1135 pub title: String,
1136 #[serde(default)]
1137 pub collapsed: bool,
1138 pub entries: Vec<String>,
1139}
1140
1141#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1142#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1143pub struct ReleaseNotesDocument {
1144 pub title: String,
1145 pub summary: Vec<String>,
1146 pub sections: Vec<ReleaseNotesSection>,
1147}
1148
1149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1150#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1151pub struct ChangelogSectionDef {
1152 pub heading: String,
1154 #[serde(default)]
1156 pub description: Option<String>,
1157 #[serde(default = "default_changelog_section_priority")]
1159 pub priority: i8,
1160}
1161
1162fn default_changelog_section_priority() -> i8 {
1163 100
1164}
1165
1166#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1167#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1168pub struct ChangelogSectionThresholds {
1169 #[serde(default = "default_changelog_collapse_threshold")]
1171 pub collapse: i8,
1172 #[serde(default = "default_changelog_ignored_threshold")]
1174 pub ignored: i8,
1175}
1176
1177fn default_changelog_collapse_threshold() -> i8 {
1178 i8::MAX
1179}
1180
1181fn default_changelog_ignored_threshold() -> i8 {
1182 i8::MAX
1183}
1184
1185impl Default for ChangelogSectionThresholds {
1186 fn default() -> Self {
1187 Self {
1188 collapse: default_changelog_collapse_threshold(),
1189 ignored: default_changelog_ignored_threshold(),
1190 }
1191 }
1192}
1193
1194#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1195#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1196pub struct ChangelogType {
1197 #[serde(default = "default_changelog_type_bump")]
1199 pub bump: BumpSeverity,
1200 pub section: String,
1202 #[serde(default)]
1204 pub description: Option<String>,
1205}
1206
1207fn default_changelog_type_bump() -> BumpSeverity {
1208 BumpSeverity::None
1209}
1210
1211#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1213#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1214pub struct ChangelogSettings {
1215 #[serde(default)]
1216 pub templates: Vec<String>,
1217 #[serde(default)]
1218 pub sections: BTreeMap<String, ChangelogSectionDef>,
1219 #[serde(default)]
1220 pub section_thresholds: ChangelogSectionThresholds,
1221 #[serde(default)]
1222 pub types: BTreeMap<String, ChangelogType>,
1223}
1224
1225impl Default for ChangelogSettings {
1226 fn default() -> Self {
1227 Self::defaults()
1228 }
1229}
1230
1231impl ChangelogSettings {
1232 #[must_use]
1234 pub fn defaults() -> Self {
1235 let mut sections = BTreeMap::new();
1236 sections.insert(
1237 "major".to_string(),
1238 ChangelogSectionDef {
1239 heading: "Major".to_string(),
1240 description: Some("Major version bumps".to_string()),
1241 priority: 5,
1242 },
1243 );
1244 sections.insert(
1245 "breaking".to_string(),
1246 ChangelogSectionDef {
1247 heading: "Breaking Change".to_string(),
1248 description: Some("API changes requiring migration".to_string()),
1249 priority: 10,
1250 },
1251 );
1252 sections.insert(
1253 "minor".to_string(),
1254 ChangelogSectionDef {
1255 heading: "Minor".to_string(),
1256 description: Some("Minor version bumps".to_string()),
1257 priority: 15,
1258 },
1259 );
1260 sections.insert(
1261 "feat".to_string(),
1262 ChangelogSectionDef {
1263 heading: "Added".to_string(),
1264 description: Some("New features added".to_string()),
1265 priority: 20,
1266 },
1267 );
1268 sections.insert(
1269 "change".to_string(),
1270 ChangelogSectionDef {
1271 heading: "Changed".to_string(),
1272 description: Some("Changes to existing functionality".to_string()),
1273 priority: 25,
1274 },
1275 );
1276 sections.insert(
1277 "fix".to_string(),
1278 ChangelogSectionDef {
1279 heading: "Fixed".to_string(),
1280 description: Some("Bug fixes".to_string()),
1281 priority: 30,
1282 },
1283 );
1284 sections.insert(
1285 "patch".to_string(),
1286 ChangelogSectionDef {
1287 heading: "Patch".to_string(),
1288 description: Some("Patch version bumps".to_string()),
1289 priority: 35,
1290 },
1291 );
1292 sections.insert(
1293 "test".to_string(),
1294 ChangelogSectionDef {
1295 heading: "Testing".to_string(),
1296 description: Some("Changes that only modify tests".to_string()),
1297 priority: 40,
1298 },
1299 );
1300 sections.insert(
1301 "refactor".to_string(),
1302 ChangelogSectionDef {
1303 heading: "Refactor".to_string(),
1304 description: Some("Code refactoring without functional changes".to_string()),
1305 priority: 40,
1306 },
1307 );
1308 sections.insert(
1309 "docs".to_string(),
1310 ChangelogSectionDef {
1311 heading: "Documentation".to_string(),
1312 description: Some("Changes that only modify documentation".to_string()),
1313 priority: 40,
1314 },
1315 );
1316 sections.insert(
1317 "security".to_string(),
1318 ChangelogSectionDef {
1319 heading: "Security".to_string(),
1320 description: Some("Security-related changes".to_string()),
1321 priority: 40,
1322 },
1323 );
1324 sections.insert(
1325 "perf".to_string(),
1326 ChangelogSectionDef {
1327 heading: "Performance".to_string(),
1328 description: Some("Performance improvements".to_string()),
1329 priority: 40,
1330 },
1331 );
1332 sections.insert(
1333 "none".to_string(),
1334 ChangelogSectionDef {
1335 heading: "None".to_string(),
1336 description: Some("No version bump".to_string()),
1337 priority: 50,
1338 },
1339 );
1340
1341 let mut types = BTreeMap::new();
1342 types.insert(
1343 "breaking".to_string(),
1344 ChangelogType {
1345 bump: BumpSeverity::Major,
1346 section: "breaking".to_string(),
1347 description: Some("Breaking change with major bump".to_string()),
1348 },
1349 );
1350 types.insert(
1351 "major".to_string(),
1352 ChangelogType {
1353 bump: BumpSeverity::Major,
1354 section: "major".to_string(),
1355 description: Some("Major version bump".to_string()),
1356 },
1357 );
1358 types.insert(
1359 "feat".to_string(),
1360 ChangelogType {
1361 bump: BumpSeverity::Minor,
1362 section: "feat".to_string(),
1363 description: Some(String::new()),
1364 },
1365 );
1366 types.insert(
1367 "minor".to_string(),
1368 ChangelogType {
1369 bump: BumpSeverity::Minor,
1370 section: "minor".to_string(),
1371 description: Some("Minor version bump".to_string()),
1372 },
1373 );
1374 types.insert(
1375 "change".to_string(),
1376 ChangelogType {
1377 bump: BumpSeverity::Minor,
1378 section: "change".to_string(),
1379 description: Some(String::new()),
1380 },
1381 );
1382 types.insert(
1383 "fix".to_string(),
1384 ChangelogType {
1385 bump: BumpSeverity::Patch,
1386 section: "fix".to_string(),
1387 description: Some(String::new()),
1388 },
1389 );
1390 types.insert(
1391 "patch".to_string(),
1392 ChangelogType {
1393 bump: BumpSeverity::Patch,
1394 section: "patch".to_string(),
1395 description: Some("Patch version bump".to_string()),
1396 },
1397 );
1398 types.insert(
1399 "refactor".to_string(),
1400 ChangelogType {
1401 bump: BumpSeverity::Patch,
1402 section: "refactor".to_string(),
1403 description: Some(String::new()),
1404 },
1405 );
1406 types.insert(
1407 "test".to_string(),
1408 ChangelogType {
1409 bump: BumpSeverity::None,
1410 section: "test".to_string(),
1411 description: Some(String::new()),
1412 },
1413 );
1414 types.insert(
1415 "none".to_string(),
1416 ChangelogType {
1417 bump: BumpSeverity::None,
1418 section: "none".to_string(),
1419 description: Some("No version bump".to_string()),
1420 },
1421 );
1422 types.insert(
1423 "docs".to_string(),
1424 ChangelogType {
1425 bump: BumpSeverity::None,
1426 section: "docs".to_string(),
1427 description: Some(String::new()),
1428 },
1429 );
1430 types.insert(
1431 "security".to_string(),
1432 ChangelogType {
1433 bump: BumpSeverity::None,
1434 section: "security".to_string(),
1435 description: Some(String::new()),
1436 },
1437 );
1438
1439 Self {
1440 templates: vec![
1441 "#### {{ summary }}\n\n{{ details }}\n\n{{ context }}".to_string(),
1442 "#### {{ summary }}\n\n{{ context }}".to_string(),
1443 "#### {{ summary }}\n\n{{ details }}".to_string(),
1444 "- {{ summary }}".to_string(),
1445 ],
1446 sections,
1447 section_thresholds: ChangelogSectionThresholds::default(),
1448 types,
1449 }
1450 }
1451}
1452
1453#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1454#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
1455#[serde(rename_all = "snake_case")]
1456#[non_exhaustive]
1457pub enum PublishMode {
1458 #[default]
1459 Builtin,
1460 External,
1461}
1462
1463impl PublishMode {
1464 #[must_use]
1466 pub fn as_str(self) -> &'static str {
1467 match self {
1468 Self::Builtin => "builtin",
1469 Self::External => "external",
1470 }
1471 }
1472}
1473
1474impl fmt::Display for PublishMode {
1475 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1476 formatter.write_str(self.as_str())
1477 }
1478}
1479
1480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1481#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
1482#[serde(rename_all = "snake_case")]
1483#[non_exhaustive]
1484pub enum RegistryKind {
1485 CratesIo,
1486 Npm,
1487 Jsr,
1488 PubDev,
1489 Pypi,
1490 GoProxy,
1491}
1492
1493impl RegistryKind {
1494 #[must_use]
1496 pub fn as_str(self) -> &'static str {
1497 match self {
1498 Self::CratesIo => "crates_io",
1499 Self::Npm => "npm",
1500 Self::Jsr => "jsr",
1501 Self::PubDev => "pub_dev",
1502 Self::Pypi => "pypi",
1503 Self::GoProxy => "go_proxy",
1504 }
1505 }
1506}
1507
1508impl fmt::Display for RegistryKind {
1509 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1510 formatter.write_str(self.as_str())
1511 }
1512}
1513
1514#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1515#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1516#[serde(untagged)]
1517pub enum PublishRegistry {
1518 Builtin(RegistryKind),
1519 Custom(String),
1520}
1521
1522#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1523#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
1524pub struct PlaceholderSettings {
1525 #[serde(default)]
1526 pub readme: Option<String>,
1527 #[serde(default)]
1528 pub readme_file: Option<PathBuf>,
1529}
1530
1531#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1532#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
1533pub struct PublishRateLimitSettings {
1534 #[serde(default)]
1535 pub enforce: bool,
1536}
1537
1538#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1539#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1540pub struct TrustedPublishingSettings {
1541 #[serde(default = "default_true")]
1542 pub enabled: bool,
1543 #[serde(default)]
1544 pub repository: Option<String>,
1545 #[serde(default)]
1546 pub workflow: Option<String>,
1547 #[serde(default)]
1548 pub environment: Option<String>,
1549}
1550
1551#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1552#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
1553pub struct PublishAttestationSettings {
1554 #[serde(default)]
1555 pub require_registry_provenance: bool,
1556}
1557
1558impl PublishAttestationSettings {
1559 #[must_use]
1560 pub fn is_default(&self) -> bool {
1561 self == &Self::default()
1562 }
1563}
1564
1565#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1566#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
1567pub struct ReleaseAttestationSettings {
1568 #[serde(default)]
1569 pub require_github_artifact_attestations: bool,
1570}
1571
1572impl ReleaseAttestationSettings {
1573 #[must_use]
1574 pub fn is_default(&self) -> bool {
1575 self == &Self::default()
1576 }
1577}
1578
1579impl Default for TrustedPublishingSettings {
1580 fn default() -> Self {
1581 Self {
1582 enabled: true,
1583 repository: None,
1584 workflow: None,
1585 environment: None,
1586 }
1587 }
1588}
1589
1590#[allow(clippy::struct_excessive_bools)]
1591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1592#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1593pub struct PublishSettings {
1594 #[serde(default = "default_true")]
1595 pub enabled: bool,
1596 #[serde(default)]
1597 pub mode: PublishMode,
1598 #[serde(default)]
1599 pub registry: Option<PublishRegistry>,
1600 #[serde(default)]
1601 pub trusted_publishing: TrustedPublishingSettings,
1602 #[serde(
1603 default,
1604 skip_serializing_if = "PublishAttestationSettings::is_default"
1605 )]
1606 pub attestations: PublishAttestationSettings,
1607 #[serde(default)]
1608 pub rate_limits: PublishRateLimitSettings,
1609 #[serde(default)]
1610 pub placeholder: PlaceholderSettings,
1611}
1612
1613impl Default for PublishSettings {
1614 fn default() -> Self {
1615 Self {
1616 enabled: true,
1617 mode: PublishMode::default(),
1618 registry: None,
1619 trusted_publishing: TrustedPublishingSettings::default(),
1620 attestations: PublishAttestationSettings::default(),
1621 rate_limits: PublishRateLimitSettings::default(),
1622 placeholder: PlaceholderSettings::default(),
1623 }
1624 }
1625}
1626
1627#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1628#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1629pub struct PackageDefinition {
1630 pub id: String,
1631 pub path: PathBuf,
1632 pub package_type: PackageType,
1633 pub changelog: Option<ChangelogTarget>,
1634 pub excluded_changelog_types: Vec<String>,
1635 pub empty_update_message: Option<String>,
1636 #[serde(default)]
1637 pub release_title: Option<String>,
1638 #[serde(default)]
1639 pub changelog_version_title: Option<String>,
1640 pub versioned_files: Vec<VersionedFileDefinition>,
1641 #[serde(default)]
1642 pub ignore_ecosystem_versioned_files: bool,
1643 #[serde(default)]
1644 pub ignored_paths: Vec<String>,
1645 #[serde(default)]
1646 pub additional_paths: Vec<String>,
1647 pub tag: bool,
1648 pub release: bool,
1649 pub version_format: VersionFormat,
1650 #[serde(default)]
1651 pub publish: PublishSettings,
1652}
1653
1654#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1655#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
1656pub enum GroupChangelogInclude {
1657 #[default]
1658 All,
1659 GroupOnly,
1660 Selected(BTreeSet<String>),
1661}
1662
1663#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1664#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1665pub struct GroupDefinition {
1666 pub id: String,
1667 pub packages: Vec<String>,
1668 pub changelog: Option<ChangelogTarget>,
1669 #[serde(default)]
1670 pub changelog_include: GroupChangelogInclude,
1671 pub excluded_changelog_types: Vec<String>,
1672 pub empty_update_message: Option<String>,
1673 #[serde(default)]
1674 pub release_title: Option<String>,
1675 #[serde(default)]
1676 pub changelog_version_title: Option<String>,
1677 pub versioned_files: Vec<VersionedFileDefinition>,
1678 pub tag: bool,
1679 pub release: bool,
1680 pub version_format: VersionFormat,
1681}
1682
1683#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1684#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1685pub struct WorkspaceDefaults {
1686 pub parent_bump: BumpSeverity,
1687 pub include_private: bool,
1688 pub warn_on_group_mismatch: bool,
1689 pub strict_version_conflicts: bool,
1690 pub package_type: Option<PackageType>,
1691 pub changelog: Option<ChangelogDefinition>,
1692 pub changelog_format: ChangelogFormat,
1693 pub empty_update_message: Option<String>,
1694 pub release_title: Option<String>,
1695 pub changelog_version_title: Option<String>,
1696}
1697
1698impl Default for WorkspaceDefaults {
1699 fn default() -> Self {
1700 Self {
1701 parent_bump: BumpSeverity::Patch,
1702 include_private: false,
1703 warn_on_group_mismatch: true,
1704 strict_version_conflicts: false,
1705 package_type: None,
1706 changelog: None,
1707 changelog_format: ChangelogFormat::Monochange,
1708 empty_update_message: None,
1709 release_title: None,
1710 changelog_version_title: None,
1711 }
1712 }
1713}
1714
1715#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1716#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
1717pub struct EcosystemSettings {
1718 #[serde(default)]
1719 pub enabled: Option<bool>,
1720 #[serde(default)]
1721 pub roots: Vec<String>,
1722 #[serde(default)]
1723 pub exclude: Vec<String>,
1724 #[serde(default)]
1725 pub dependency_version_prefix: Option<String>,
1726 #[serde(default)]
1727 pub versioned_files: Vec<VersionedFileDefinition>,
1728 #[serde(default)]
1729 pub lockfile_commands: Vec<LockfileCommandDefinition>,
1730 #[serde(default)]
1731 pub publish: PublishSettings,
1732}
1733
1734#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1735#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1736pub struct LockfileCommandDefinition {
1737 pub command: String,
1738 #[serde(default)]
1739 pub cwd: Option<PathBuf>,
1740 #[serde(default)]
1741 pub shell: ShellConfig,
1742}
1743
1744#[derive(Debug, Clone, Eq, PartialEq)]
1745pub struct LockfileCommandExecution {
1746 pub command: String,
1747 pub cwd: PathBuf,
1748 pub shell: ShellConfig,
1749}
1750
1751#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1752#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
1753#[serde(rename_all = "snake_case")]
1754pub enum CliInputKind {
1755 String,
1756 StringList,
1757 Path,
1758 Choice,
1759 Boolean,
1760}
1761
1762#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1763#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1764pub struct CliInputDefinition {
1765 pub name: String,
1766 #[serde(rename = "type")]
1767 pub kind: CliInputKind,
1768 #[serde(default)]
1769 pub help_text: Option<String>,
1770 #[serde(default)]
1771 pub required: bool,
1772 #[serde(default, deserialize_with = "deserialize_cli_input_default")]
1773 #[cfg_attr(feature = "schema", schemars(with = "Option<CliInputDefault>"))]
1774 pub default: Option<String>,
1775 #[serde(default)]
1776 pub choices: Vec<String>,
1777 #[serde(default)]
1778 pub short: Option<char>,
1779}
1780
1781#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1782#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1783#[serde(untagged)]
1784pub enum CliInputDefault {
1785 String(String),
1786 Boolean(bool),
1787 Integer(i64),
1788 Number(f64),
1789}
1790
1791fn deserialize_cli_input_default<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
1792where
1793 D: serde::Deserializer<'de>,
1794{
1795 let value = Option::<CliInputDefault>::deserialize(deserializer)?;
1796 Ok(value.map(|value| {
1797 match value {
1798 CliInputDefault::String(value) => value,
1799 CliInputDefault::Boolean(value) => value.to_string(),
1800 CliInputDefault::Integer(value) => value.to_string(),
1801 CliInputDefault::Number(value) => value.to_string(),
1802 }
1803 }))
1804}
1805
1806#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1807#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1808#[serde(untagged)]
1809pub enum CliStepInputValue {
1810 String(String),
1811 Boolean(bool),
1812 List(Vec<String>),
1813}
1814#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1815#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
1816#[serde(rename_all = "snake_case")]
1817pub enum CommandVariable {
1818 Version,
1819 GroupVersion,
1820 ReleasedPackages,
1821 ChangedFiles,
1822 Changesets,
1823}
1824
1825#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1827#[derive(Debug, Clone, Eq, PartialEq, Default)]
1828pub enum ShellConfig {
1829 #[default]
1830 None,
1831 Default,
1832 Custom(String),
1833}
1834
1835impl ShellConfig {
1836 #[must_use]
1838 pub fn shell_binary(&self) -> Option<&str> {
1839 match self {
1840 Self::None => None,
1841 Self::Default => Some("sh"),
1842 Self::Custom(shell) => Some(shell),
1843 }
1844 }
1845}
1846
1847impl Serialize for ShellConfig {
1848 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1849 match self {
1850 Self::None => serializer.serialize_bool(false),
1851 Self::Default => serializer.serialize_bool(true),
1852 Self::Custom(shell) => serializer.serialize_str(shell),
1853 }
1854 }
1855}
1856
1857impl<'de> Deserialize<'de> for ShellConfig {
1858 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1859 use serde::de;
1860
1861 struct ShellConfigVisitor;
1862
1863 impl de::Visitor<'_> for ShellConfigVisitor {
1864 type Value = ShellConfig;
1865
1866 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1867 formatter.write_str("a boolean or a shell name string")
1868 }
1869
1870 fn visit_bool<E: de::Error>(self, value: bool) -> Result<ShellConfig, E> {
1871 Ok(if value {
1872 ShellConfig::Default
1873 } else {
1874 ShellConfig::None
1875 })
1876 }
1877
1878 fn visit_str<E: de::Error>(self, value: &str) -> Result<ShellConfig, E> {
1879 if value.is_empty() {
1880 return Err(de::Error::invalid_value(
1881 de::Unexpected::Str(value),
1882 &"a non-empty shell name",
1883 ));
1884 }
1885 Ok(ShellConfig::Custom(value.to_string()))
1886 }
1887
1888 fn visit_string<E: de::Error>(self, value: String) -> Result<ShellConfig, E> {
1889 self.visit_str(&value)
1890 }
1891 }
1892
1893 deserializer.deserialize_any(ShellConfigVisitor)
1894 }
1895}
1896
1897#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1907#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
1908#[serde(tag = "type", deny_unknown_fields)]
1909#[non_exhaustive]
1910pub enum CliStepDefinition {
1911 Config {
1913 #[serde(default)]
1914 name: Option<String>,
1915 #[serde(default)]
1916 when: Option<String>,
1917 #[serde(default)]
1918 always_run: bool,
1919 #[serde(default)]
1920 inputs: BTreeMap<String, CliStepInputValue>,
1921 },
1922 Validate {
1925 #[serde(default)]
1926 name: Option<String>,
1927 #[serde(default)]
1928 when: Option<String>,
1929 #[serde(default)]
1930 always_run: bool,
1931 #[serde(default)]
1932 inputs: BTreeMap<String, CliStepInputValue>,
1933 },
1934 Discover {
1936 #[serde(default)]
1937 name: Option<String>,
1938 #[serde(default)]
1939 when: Option<String>,
1940 #[serde(default)]
1941 always_run: bool,
1942 #[serde(default)]
1943 inputs: BTreeMap<String, CliStepInputValue>,
1944 },
1945 DisplayVersions {
1947 #[serde(default)]
1948 name: Option<String>,
1949 #[serde(default)]
1950 when: Option<String>,
1951 #[serde(default)]
1952 always_run: bool,
1953 #[serde(default)]
1954 inputs: BTreeMap<String, CliStepInputValue>,
1955 },
1956 CreateChangeFile {
1959 #[serde(default)]
1960 name: Option<String>,
1961 #[serde(default)]
1962 when: Option<String>,
1963 #[serde(default)]
1964 always_run: bool,
1965 #[serde(default)]
1966 show_progress: Option<bool>,
1967 #[serde(default)]
1968 inputs: BTreeMap<String, CliStepInputValue>,
1969 },
1970 PrepareRelease {
1973 #[serde(default)]
1974 name: Option<String>,
1975 #[serde(default)]
1976 when: Option<String>,
1977 #[serde(default)]
1978 always_run: bool,
1979 #[serde(default)]
1980 inputs: BTreeMap<String, CliStepInputValue>,
1981 #[serde(default)]
1985 allow_empty_changesets: bool,
1986 },
1987 CommitRelease {
1991 #[serde(default)]
1992 name: Option<String>,
1993 #[serde(default)]
1994 when: Option<String>,
1995 #[serde(default)]
1996 always_run: bool,
1997 #[serde(default)]
1998 no_verify: bool,
1999 #[serde(default)]
2000 update_release_json: bool,
2001 #[serde(default)]
2002 inputs: BTreeMap<String, CliStepInputValue>,
2003 },
2004 VerifyReleaseBranch {
2006 #[serde(default)]
2007 name: Option<String>,
2008 #[serde(default)]
2009 when: Option<String>,
2010 #[serde(default)]
2011 always_run: bool,
2012 #[serde(default)]
2013 inputs: BTreeMap<String, CliStepInputValue>,
2014 },
2015 PublishRelease {
2020 #[serde(default)]
2021 name: Option<String>,
2022 #[serde(default)]
2023 when: Option<String>,
2024 #[serde(default)]
2025 always_run: bool,
2026 #[serde(default)]
2027 inputs: BTreeMap<String, CliStepInputValue>,
2028 },
2029 PlaceholderPublish {
2031 #[serde(default)]
2032 name: Option<String>,
2033 #[serde(default)]
2034 when: Option<String>,
2035 #[serde(default)]
2036 always_run: bool,
2037 #[serde(default)]
2038 inputs: BTreeMap<String, CliStepInputValue>,
2039 },
2040 PublishPackages {
2042 #[serde(default)]
2043 name: Option<String>,
2044 #[serde(default)]
2045 when: Option<String>,
2046 #[serde(default)]
2047 always_run: bool,
2048 #[serde(default)]
2049 inputs: BTreeMap<String, CliStepInputValue>,
2050 },
2051 PlanPublishRateLimits {
2053 #[serde(default)]
2054 name: Option<String>,
2055 #[serde(default)]
2056 when: Option<String>,
2057 #[serde(default)]
2058 always_run: bool,
2059 #[serde(default)]
2060 inputs: BTreeMap<String, CliStepInputValue>,
2061 },
2062 OpenReleaseRequest {
2067 #[serde(default)]
2068 name: Option<String>,
2069 #[serde(default)]
2070 when: Option<String>,
2071 #[serde(default)]
2072 always_run: bool,
2073 #[serde(default)]
2074 no_verify: bool,
2075 #[serde(default)]
2076 inputs: BTreeMap<String, CliStepInputValue>,
2077 },
2078 CommentReleasedIssues {
2083 #[serde(default)]
2084 name: Option<String>,
2085 #[serde(default)]
2086 when: Option<String>,
2087 #[serde(default)]
2088 always_run: bool,
2089 #[serde(default)]
2090 inputs: BTreeMap<String, CliStepInputValue>,
2091 },
2092 AffectedPackages {
2096 #[serde(default)]
2097 name: Option<String>,
2098 #[serde(default)]
2099 when: Option<String>,
2100 #[serde(default)]
2101 always_run: bool,
2102 #[serde(default)]
2103 inputs: BTreeMap<String, CliStepInputValue>,
2104 },
2105 DiagnoseChangesets {
2107 #[serde(default)]
2108 name: Option<String>,
2109 #[serde(default)]
2110 when: Option<String>,
2111 #[serde(default)]
2112 always_run: bool,
2113 #[serde(default)]
2114 inputs: BTreeMap<String, CliStepInputValue>,
2115 },
2116 RetargetRelease {
2121 #[serde(default)]
2122 name: Option<String>,
2123 #[serde(default)]
2124 when: Option<String>,
2125 #[serde(default)]
2126 always_run: bool,
2127 #[serde(default)]
2128 inputs: BTreeMap<String, CliStepInputValue>,
2129 },
2130 Command {
2134 #[serde(default)]
2135 name: Option<String>,
2136 #[serde(default)]
2137 when: Option<String>,
2138 #[serde(default)]
2139 always_run: bool,
2140 #[serde(default)]
2141 show_progress: Option<bool>,
2142 command: String,
2143 #[serde(default)]
2144 dry_run_command: Option<String>,
2145 #[serde(default)]
2146 shell: ShellConfig,
2147 #[serde(default)]
2148 id: Option<String>,
2149 #[serde(default)]
2150 variables: Option<BTreeMap<String, CommandVariable>>,
2151 #[serde(default)]
2152 inputs: BTreeMap<String, CliStepInputValue>,
2153 },
2154}
2155
2156impl CliStepDefinition {
2157 #[must_use]
2159 pub fn inputs(&self) -> &BTreeMap<String, CliStepInputValue> {
2160 match self {
2161 Self::Config { inputs, .. }
2162 | Self::Validate { inputs, .. }
2163 | Self::Discover { inputs, .. }
2164 | Self::DisplayVersions { inputs, .. }
2165 | Self::CreateChangeFile { inputs, .. }
2166 | Self::PrepareRelease { inputs, .. }
2167 | Self::CommitRelease { inputs, .. }
2168 | Self::VerifyReleaseBranch { inputs, .. }
2169 | Self::PublishRelease { inputs, .. }
2170 | Self::PlaceholderPublish { inputs, .. }
2171 | Self::PublishPackages { inputs, .. }
2172 | Self::PlanPublishRateLimits { inputs, .. }
2173 | Self::OpenReleaseRequest { inputs, .. }
2174 | Self::CommentReleasedIssues { inputs, .. }
2175 | Self::AffectedPackages { inputs, .. }
2176 | Self::DiagnoseChangesets { inputs, .. }
2177 | Self::RetargetRelease { inputs, .. }
2178 | Self::Command { inputs, .. } => inputs,
2179 }
2180 }
2181
2182 #[must_use]
2184 pub fn name(&self) -> Option<&str> {
2185 match self {
2186 Self::Config { name, .. }
2187 | Self::Validate { name, .. }
2188 | Self::Discover { name, .. }
2189 | Self::DisplayVersions { name, .. }
2190 | Self::CreateChangeFile { name, .. }
2191 | Self::PrepareRelease { name, .. }
2192 | Self::CommitRelease { name, .. }
2193 | Self::VerifyReleaseBranch { name, .. }
2194 | Self::PublishRelease { name, .. }
2195 | Self::PlaceholderPublish { name, .. }
2196 | Self::PublishPackages { name, .. }
2197 | Self::PlanPublishRateLimits { name, .. }
2198 | Self::OpenReleaseRequest { name, .. }
2199 | Self::CommentReleasedIssues { name, .. }
2200 | Self::AffectedPackages { name, .. }
2201 | Self::DiagnoseChangesets { name, .. }
2202 | Self::RetargetRelease { name, .. }
2203 | Self::Command { name, .. } => name.as_deref(),
2204 }
2205 }
2206
2207 #[must_use]
2209 pub fn display_name(&self) -> &str {
2210 self.name().unwrap_or(self.kind_name())
2211 }
2212
2213 #[must_use]
2215 pub fn when(&self) -> Option<&str> {
2216 match self {
2217 Self::Config { when, .. }
2218 | Self::Validate { when, .. }
2219 | Self::Discover { when, .. }
2220 | Self::DisplayVersions { when, .. }
2221 | Self::CreateChangeFile { when, .. }
2222 | Self::PrepareRelease { when, .. }
2223 | Self::CommitRelease { when, .. }
2224 | Self::VerifyReleaseBranch { when, .. }
2225 | Self::PublishRelease { when, .. }
2226 | Self::PlaceholderPublish { when, .. }
2227 | Self::PublishPackages { when, .. }
2228 | Self::PlanPublishRateLimits { when, .. }
2229 | Self::OpenReleaseRequest { when, .. }
2230 | Self::CommentReleasedIssues { when, .. }
2231 | Self::AffectedPackages { when, .. }
2232 | Self::DiagnoseChangesets { when, .. }
2233 | Self::RetargetRelease { when, .. }
2234 | Self::Command { when, .. } => when.as_deref(),
2235 }
2236 }
2237
2238 #[must_use]
2241 pub fn always_run(&self) -> bool {
2242 match self {
2243 Self::Config { always_run, .. }
2244 | Self::Validate { always_run, .. }
2245 | Self::Discover { always_run, .. }
2246 | Self::DisplayVersions { always_run, .. }
2247 | Self::CreateChangeFile { always_run, .. }
2248 | Self::PrepareRelease { always_run, .. }
2249 | Self::CommitRelease { always_run, .. }
2250 | Self::VerifyReleaseBranch { always_run, .. }
2251 | Self::PublishRelease { always_run, .. }
2252 | Self::PlaceholderPublish { always_run, .. }
2253 | Self::PublishPackages { always_run, .. }
2254 | Self::PlanPublishRateLimits { always_run, .. }
2255 | Self::OpenReleaseRequest { always_run, .. }
2256 | Self::CommentReleasedIssues { always_run, .. }
2257 | Self::AffectedPackages { always_run, .. }
2258 | Self::DiagnoseChangesets { always_run, .. }
2259 | Self::RetargetRelease { always_run, .. }
2260 | Self::Command { always_run, .. } => *always_run,
2261 }
2262 }
2263
2264 #[must_use]
2266 pub fn show_progress(&self) -> Option<bool> {
2267 match self {
2268 Self::CreateChangeFile { show_progress, .. } | Self::Command { show_progress, .. } => {
2269 *show_progress
2270 }
2271 _ => None,
2272 }
2273 }
2274
2275 #[must_use]
2277 pub fn kind_name(&self) -> &'static str {
2278 match self {
2279 Self::Config { .. } => "Config",
2280 Self::Validate { .. } => "Validate",
2281 Self::Discover { .. } => "Discover",
2282 Self::DisplayVersions { .. } => "DisplayVersions",
2283 Self::CreateChangeFile { .. } => "CreateChangeFile",
2284 Self::PrepareRelease { .. } => "PrepareRelease",
2285 Self::CommitRelease { .. } => "CommitRelease",
2286 Self::VerifyReleaseBranch { .. } => "VerifyReleaseBranch",
2287 Self::PublishRelease { .. } => "PublishRelease",
2288 Self::PlaceholderPublish { .. } => "PlaceholderPublish",
2289 Self::PublishPackages { .. } => "PublishPackages",
2290 Self::PlanPublishRateLimits { .. } => "PlanPublishRateLimits",
2291 Self::OpenReleaseRequest { .. } => "OpenReleaseRequest",
2292 Self::CommentReleasedIssues { .. } => "CommentReleasedIssues",
2293 Self::AffectedPackages { .. } => "AffectedPackages",
2294 Self::DiagnoseChangesets { .. } => "DiagnoseChangesets",
2295 Self::RetargetRelease { .. } => "RetargetRelease",
2296 Self::Command { .. } => "Command",
2297 }
2298 }
2299
2300 #[must_use]
2306 pub fn valid_input_names(&self) -> Option<&'static [&'static str]> {
2307 match self {
2308 Self::Config { .. } => Some(&[]),
2309 Self::Validate { .. } => Some(&["fix"]),
2310 Self::CommitRelease { .. } => Some(&["no_verify", "update_release_json"]),
2311 Self::VerifyReleaseBranch { .. } => Some(&["from"]),
2312 Self::Discover { .. } | Self::DisplayVersions { .. } | Self::PrepareRelease { .. } => {
2313 Some(&["format"])
2314 }
2315 Self::CommentReleasedIssues { .. } => {
2316 Some(&["format", "from-ref", "auto-close-issues"])
2317 }
2318 Self::PublishRelease { .. } => Some(&["format", "from-ref", "draft"]),
2319 Self::OpenReleaseRequest { .. } => Some(&["format", "no_verify"]),
2320 Self::PlaceholderPublish { .. } => Some(&["format", "package", "show-all"]),
2321 Self::PublishPackages { .. } => {
2322 Some(&[
2323 "format",
2324 "output",
2325 "package",
2326 "group",
2327 "ecosystem",
2328 "resume",
2329 ])
2330 }
2331 Self::PlanPublishRateLimits { .. } => {
2332 Some(&["format", "mode", "package", "ci", "readiness"])
2333 }
2334 Self::CreateChangeFile { .. } => {
2335 Some(&[
2336 "interactive",
2337 "package",
2338 "bump",
2339 "version",
2340 "reason",
2341 "type",
2342 "details",
2343 "output",
2344 ])
2345 }
2346 Self::AffectedPackages { .. } => {
2347 Some(&["format", "changed_paths", "from", "verify", "label"])
2348 }
2349 Self::DiagnoseChangesets { .. } => Some(&["format", "changeset"]),
2350 Self::RetargetRelease { .. } => Some(&["from", "target", "force", "sync_provider"]),
2351 Self::Command { .. } => None,
2352 }
2353 }
2354
2355 #[must_use]
2357 pub fn valid_input_choices(&self, name: &str) -> Option<&'static [&'static str]> {
2358 match self {
2359 Self::Discover { .. }
2360 | Self::DisplayVersions { .. }
2361 | Self::PrepareRelease { .. }
2362 | Self::PublishRelease { .. }
2363 | Self::CommentReleasedIssues { .. }
2364 | Self::OpenReleaseRequest { .. }
2365 | Self::AffectedPackages { .. }
2366 | Self::DiagnoseChangesets { .. }
2367 | Self::PlaceholderPublish { .. }
2368 | Self::PublishPackages { .. } => {
2369 match name {
2370 "format" => Some(&["text", "json", "md"]),
2371 _ => None,
2372 }
2373 }
2374 Self::PlanPublishRateLimits { .. } => {
2375 match name {
2376 "format" => Some(&["text", "json", "md"]),
2377 "mode" => Some(&["local", "ci"]),
2378 "ci" => Some(&["github", "gitlab", "generic"]),
2379 _ => None,
2380 }
2381 }
2382 Self::CreateChangeFile { .. } => {
2383 match name {
2384 "bump" => Some(&["major", "minor", "patch", "none"]),
2385 _ => None,
2386 }
2387 }
2388 Self::RetargetRelease { .. } | _ => None,
2389 }
2390 }
2391
2392 #[must_use]
2396 pub fn expected_input_kind(&self, name: &str) -> Option<CliInputKind> {
2397 match self {
2398 Self::Validate { .. } => {
2399 match name {
2400 "fix" => Some(CliInputKind::Boolean),
2401 _ => None,
2402 }
2403 }
2404 Self::CommitRelease { .. } => {
2405 match name {
2406 "no_verify" | "update_release_json" => Some(CliInputKind::Boolean),
2407 _ => None,
2408 }
2409 }
2410 Self::VerifyReleaseBranch { .. } => {
2411 match name {
2412 "from" => Some(CliInputKind::String),
2413 _ => None,
2414 }
2415 }
2416 Self::Config { .. } | Self::Command { .. } => None,
2417 Self::Discover { .. } | Self::DisplayVersions { .. } | Self::PrepareRelease { .. } => {
2418 matches!(name, "format").then_some(CliInputKind::Choice)
2419 }
2420 Self::CommentReleasedIssues { .. } => {
2421 match name {
2422 "format" => Some(CliInputKind::Choice),
2423 "from-ref" => Some(CliInputKind::String),
2424 "auto-close-issues" => Some(CliInputKind::Boolean),
2425 _ => None,
2426 }
2427 }
2428 Self::PublishRelease { .. } => {
2429 match name {
2430 "format" => Some(CliInputKind::Choice),
2431 "from-ref" => Some(CliInputKind::String),
2432 "draft" => Some(CliInputKind::Boolean),
2433 _ => None,
2434 }
2435 }
2436 Self::OpenReleaseRequest { .. } => {
2437 match name {
2438 "format" => Some(CliInputKind::Choice),
2439 "no_verify" => Some(CliInputKind::Boolean),
2440 _ => None,
2441 }
2442 }
2443 Self::PlaceholderPublish { .. } => {
2444 match name {
2445 "format" => Some(CliInputKind::Choice),
2446 "package" => Some(CliInputKind::StringList),
2447 "show-all" => Some(CliInputKind::Boolean),
2448 _ => None,
2449 }
2450 }
2451 Self::PublishPackages { .. } => {
2452 match name {
2453 "format" => Some(CliInputKind::Choice),
2454 "package" => Some(CliInputKind::StringList),
2455 "output" | "resume" => Some(CliInputKind::Path),
2456 _ => None,
2457 }
2458 }
2459 Self::PlanPublishRateLimits { .. } => {
2460 match name {
2461 "package" => Some(CliInputKind::StringList),
2462 "readiness" => Some(CliInputKind::Path),
2463 "format" | "mode" | "ci" => Some(CliInputKind::Choice),
2464 _ => None,
2465 }
2466 }
2467 Self::CreateChangeFile { .. } => {
2468 match name {
2469 "interactive" => Some(CliInputKind::Boolean),
2470 "package" => Some(CliInputKind::StringList),
2471 "bump" => Some(CliInputKind::Choice),
2472 "version" | "reason" | "type" | "details" => Some(CliInputKind::String),
2473 "output" => Some(CliInputKind::Path),
2474 _ => None,
2475 }
2476 }
2477 Self::AffectedPackages { .. } => {
2478 match name {
2479 "format" => Some(CliInputKind::Choice),
2480 "changed_paths" | "label" => Some(CliInputKind::StringList),
2481 "from" => Some(CliInputKind::String),
2482 "verify" => Some(CliInputKind::Boolean),
2483 _ => None,
2484 }
2485 }
2486 Self::DiagnoseChangesets { .. } => {
2487 match name {
2488 "format" => Some(CliInputKind::Choice),
2489 "changeset" => Some(CliInputKind::StringList),
2490 _ => None,
2491 }
2492 }
2493 Self::RetargetRelease { .. } => {
2494 match name {
2495 "from" | "target" => Some(CliInputKind::String),
2496 "force" | "sync_provider" => Some(CliInputKind::Boolean),
2497 _ => None,
2498 }
2499 }
2500 }
2501 }
2502
2503 pub fn step_kebab_name(&self) -> String {
2504 let name = self.kind_name();
2505 let mut result = String::new();
2506 let mut prev_upper = false;
2507 for ch in name.chars() {
2508 if ch.is_uppercase() {
2509 if !result.is_empty() && !prev_upper {
2510 result.push('-');
2511 }
2512 result.push(ch.to_ascii_lowercase());
2513 prev_upper = true;
2514 } else {
2515 result.push(ch);
2516 prev_upper = false;
2517 }
2518 }
2519 result
2520 }
2521
2522 #[must_use]
2524 pub fn step_inputs_schema(&self) -> Vec<CliInputDefinition> {
2525 let Some(names) = self.valid_input_names() else {
2526 return Vec::new();
2527 };
2528 names
2529 .iter()
2530 .map(|name| {
2531 let kind = self
2532 .expected_input_kind(name)
2533 .unwrap_or(CliInputKind::String);
2534 let choices = self
2535 .valid_input_choices(name)
2536 .map(|c| {
2537 #[allow(clippy::redundant_closure_for_method_calls)]
2538 c.iter().map(|s| s.to_string()).collect::<Vec<_>>()
2539 })
2540 .unwrap_or_default();
2541 let default = if *name == "sync_provider" {
2542 Some("true".to_string())
2543 } else {
2544 None
2545 };
2546 CliInputDefinition {
2547 name: name.to_string(),
2548 kind,
2549 help_text: None,
2550 required: false,
2551 default,
2552 choices,
2553 short: None,
2554 }
2555 })
2556 .collect()
2557 }
2558}
2559
2560#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2561#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
2562pub struct CliCommandDefinition {
2563 pub name: String,
2564 #[serde(default)]
2565 pub help_text: Option<String>,
2566 #[serde(default)]
2567 pub inputs: Vec<CliInputDefinition>,
2568 #[serde(default)]
2569 pub steps: Vec<CliStepDefinition>,
2570 #[serde(default)]
2573 pub dry_run: bool,
2574}
2575
2576#[must_use]
2578pub fn render_release_notes(format: ChangelogFormat, document: &ReleaseNotesDocument) -> String {
2579 match format {
2580 ChangelogFormat::Monochange => render_monochange_release_notes(document),
2581 ChangelogFormat::KeepAChangelog => render_keep_a_changelog_release_notes(document),
2582 }
2583}
2584
2585fn render_monochange_release_notes(document: &ReleaseNotesDocument) -> String {
2586 let mut lines = vec![format!("## {}", document.title), String::new()];
2587 for (index, paragraph) in document.summary.iter().enumerate() {
2588 if index > 0 {
2589 lines.push(String::new());
2590 }
2591 lines.push(paragraph.clone());
2592 }
2593 for section in &document.sections {
2594 if section.entries.is_empty() {
2595 continue;
2596 }
2597 if !lines.last().is_some_and(String::is_empty) {
2598 lines.push(String::new());
2599 }
2600 if section.collapsed {
2601 push_collapsed_release_note_section(&mut lines, section);
2602 continue;
2603 }
2604 lines.push(format!("### {}", section.title));
2605 lines.push(String::new());
2606 push_release_note_entries(&mut lines, §ion.entries);
2607 }
2608 lines.join("\n")
2609}
2610
2611fn render_keep_a_changelog_release_notes(document: &ReleaseNotesDocument) -> String {
2612 let mut lines = vec![format!("## {}", document.title), String::new()];
2613 for (index, paragraph) in document.summary.iter().enumerate() {
2614 if index > 0 {
2615 lines.push(String::new());
2616 }
2617 lines.push(paragraph.clone());
2618 }
2619 for section in &document.sections {
2620 if section.entries.is_empty() {
2621 continue;
2622 }
2623 if !lines.last().is_some_and(String::is_empty) {
2624 lines.push(String::new());
2625 }
2626 if section.collapsed {
2627 push_collapsed_release_note_section(&mut lines, section);
2628 continue;
2629 }
2630 lines.push(format!("### {}", section.title));
2631 lines.push(String::new());
2632 push_release_note_entries(&mut lines, §ion.entries);
2633 }
2634 lines.join("\n")
2635}
2636
2637fn push_collapsed_release_note_section(lines: &mut Vec<String>, section: &ReleaseNotesSection) {
2638 lines.push("<details>".to_string());
2639 lines.push(format!(
2640 "<summary><strong>{}</strong></summary>",
2641 section.title
2642 ));
2643 lines.push(String::new());
2644 push_release_note_entries(lines, §ion.entries);
2645 lines.push("</details>".to_string());
2646}
2647
2648fn push_release_note_entries(lines: &mut Vec<String>, entries: &[String]) {
2649 for (index, entry) in entries.iter().enumerate() {
2650 let trimmed = entry.trim();
2651 if trimmed.contains('\n') {
2652 lines.extend(trimmed.lines().map(ToString::to_string));
2653 if index + 1 < entries.len() {
2654 lines.push(String::new());
2655 }
2656 continue;
2657 }
2658 if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with('#') {
2659 lines.push(trimmed.to_string());
2660 } else {
2661 lines.push(format!("- {trimmed}"));
2662 }
2663 }
2664}
2665
2666#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2667#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
2668#[serde(rename_all = "snake_case")]
2669pub enum ReleaseOwnerKind {
2670 Package,
2671 Group,
2672}
2673
2674impl ReleaseOwnerKind {
2675 #[must_use]
2677 pub fn as_str(self) -> &'static str {
2678 match self {
2679 Self::Package => "package",
2680 Self::Group => "group",
2681 }
2682 }
2683}
2684
2685impl fmt::Display for ReleaseOwnerKind {
2686 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2687 formatter.write_str(self.as_str())
2688 }
2689}
2690
2691#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
2692#[serde(rename_all = "camelCase")]
2693pub struct ReleaseManifestTarget {
2694 pub id: String,
2695 pub kind: ReleaseOwnerKind,
2696 pub version: String,
2697 pub tag: bool,
2698 pub release: bool,
2699 pub version_format: VersionFormat,
2700 pub tag_name: String,
2701 pub members: Vec<String>,
2702 #[serde(default)]
2703 pub rendered_title: String,
2704 #[serde(default)]
2705 pub rendered_changelog_title: String,
2706}
2707
2708#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2709#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
2710#[serde(rename_all = "camelCase")]
2711pub struct ReleaseManifestChangelog {
2712 pub owner_id: String,
2713 pub owner_kind: ReleaseOwnerKind,
2714 pub path: PathBuf,
2715 pub format: ChangelogFormat,
2716 pub notes: ReleaseNotesDocument,
2717 pub rendered: String,
2718}
2719
2720#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2721#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
2722#[serde(rename_all = "camelCase")]
2723pub struct PackagePublicationTarget {
2724 pub package: String,
2725 pub ecosystem: Ecosystem,
2726 #[serde(default)]
2727 pub registry: Option<PublishRegistry>,
2728 pub version: String,
2729 #[serde(default)]
2730 pub mode: PublishMode,
2731 #[serde(default)]
2732 pub trusted_publishing: TrustedPublishingSettings,
2733 #[serde(
2734 default,
2735 skip_serializing_if = "PublishAttestationSettings::is_default"
2736 )]
2737 pub attestations: PublishAttestationSettings,
2738}
2739
2740#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
2741#[serde(rename_all = "snake_case")]
2742pub enum RateLimitOperation {
2743 PlaceholderPublish,
2744 Publish,
2745 Update,
2746}
2747
2748impl RateLimitOperation {
2749 #[must_use]
2750 pub fn as_str(self) -> &'static str {
2751 match self {
2752 Self::PlaceholderPublish => "placeholder_publish",
2753 Self::Publish => "publish",
2754 Self::Update => "update",
2755 }
2756 }
2757}
2758
2759impl fmt::Display for RateLimitOperation {
2760 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2761 formatter.write_str(self.as_str())
2762 }
2763}
2764
2765#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
2766#[serde(rename_all = "snake_case")]
2767pub enum RateLimitEvidenceKind {
2768 Official,
2769 SourceCode,
2770 Secondary,
2771 ConservativeDefault,
2772}
2773
2774#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
2775#[serde(rename_all = "snake_case")]
2776pub enum RateLimitConfidence {
2777 High,
2778 Medium,
2779 Low,
2780}
2781
2782#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
2783#[serde(rename_all = "camelCase")]
2784pub struct RateLimitEvidence {
2785 pub title: String,
2786 pub url: String,
2787 pub kind: RateLimitEvidenceKind,
2788 pub notes: String,
2789}
2790
2791#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
2792#[serde(rename_all = "camelCase")]
2793pub struct RegistryRateLimitPolicy {
2794 pub registry: RegistryKind,
2795 pub operation: RateLimitOperation,
2796 pub limit: Option<u32>,
2797 pub window_seconds: Option<u64>,
2798 pub confidence: RateLimitConfidence,
2799 pub notes: String,
2800 pub evidence: Vec<RateLimitEvidence>,
2801}
2802
2803#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
2804#[serde(rename_all = "camelCase")]
2805pub struct RegistryRateLimitWindowPlan {
2806 pub registry: RegistryKind,
2807 pub operation: RateLimitOperation,
2808 pub limit: Option<u32>,
2809 pub window_seconds: Option<u64>,
2810 pub pending: usize,
2811 pub batches_required: usize,
2812 pub fits_single_window: bool,
2813 pub confidence: RateLimitConfidence,
2814 pub notes: String,
2815 pub evidence: Vec<RateLimitEvidence>,
2816}
2817
2818#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
2819#[serde(rename_all = "camelCase")]
2820pub struct PublishRateLimitBatch {
2821 pub registry: RegistryKind,
2822 pub operation: RateLimitOperation,
2823 pub batch_index: usize,
2824 pub total_batches: usize,
2825 pub packages: Vec<String>,
2826 pub recommended_wait_seconds: Option<u64>,
2827}
2828
2829#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
2830#[serde(rename_all = "camelCase")]
2831pub struct PublishRateLimitReport {
2832 pub dry_run: bool,
2833 pub windows: Vec<RegistryRateLimitWindowPlan>,
2834 pub batches: Vec<PublishRateLimitBatch>,
2835 pub warnings: Vec<String>,
2836}
2837
2838#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2839#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
2840#[serde(rename_all = "snake_case")]
2841pub enum HostingProviderKind {
2842 #[default]
2843 #[serde(rename = "generic_git")]
2844 GenericGit,
2845 #[serde(rename = "github")]
2846 GitHub,
2847 #[serde(rename = "gitlab")]
2848 GitLab,
2849 #[serde(rename = "gitea")]
2850 Gitea,
2851 #[serde(rename = "forgejo")]
2852 Forgejo,
2853 #[serde(rename = "bitbucket")]
2854 Bitbucket,
2855}
2856
2857impl HostingProviderKind {
2858 #[must_use]
2860 pub fn as_str(self) -> &'static str {
2861 match self {
2862 Self::GenericGit => "generic_git",
2863 Self::GitHub => "github",
2864 Self::GitLab => "gitlab",
2865 Self::Gitea => "gitea",
2866 Self::Forgejo => "forgejo",
2867 Self::Bitbucket => "bitbucket",
2868 }
2869 }
2870}
2871
2872impl fmt::Display for HostingProviderKind {
2873 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2874 formatter.write_str(self.as_str())
2875 }
2876}
2877
2878#[allow(clippy::struct_excessive_bools)]
2879#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2880#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
2881#[serde(rename_all = "camelCase")]
2882pub struct HostingCapabilities {
2883 pub commit_web_urls: bool,
2884 pub actor_profiles: bool,
2885 pub review_request_lookup: bool,
2886 pub related_issues: bool,
2887 pub issue_comments: bool,
2888}
2889
2890#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2891#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
2892#[serde(rename_all = "snake_case")]
2893pub enum HostedActorSourceKind {
2894 #[default]
2895 CommitAuthor,
2896 CommitCommitter,
2897 ReviewRequestAuthor,
2898}
2899
2900#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2901#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
2902#[serde(rename_all = "camelCase")]
2903pub struct HostedActorRef {
2904 pub provider: HostingProviderKind,
2905 #[serde(default)]
2906 pub host: Option<String>,
2907 #[serde(default)]
2908 pub id: Option<String>,
2909 #[serde(default)]
2910 pub login: Option<String>,
2911 #[serde(default)]
2912 pub display_name: Option<String>,
2913 #[serde(default)]
2914 pub url: Option<String>,
2915 pub source: HostedActorSourceKind,
2916}
2917
2918#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2919#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
2920#[serde(rename_all = "camelCase")]
2921pub struct HostedCommitRef {
2922 pub provider: HostingProviderKind,
2923 #[serde(default)]
2924 pub host: Option<String>,
2925 pub sha: String,
2926 pub short_sha: String,
2927 #[serde(default)]
2928 pub url: Option<String>,
2929 #[serde(default)]
2930 pub authored_at: Option<String>,
2931 #[serde(default)]
2932 pub committed_at: Option<String>,
2933 #[serde(default)]
2934 pub author_name: Option<String>,
2935 #[serde(default)]
2936 pub author_email: Option<String>,
2937}
2938
2939#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2940#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
2941#[serde(rename_all = "snake_case")]
2942pub enum HostedReviewRequestKind {
2943 #[default]
2944 PullRequest,
2945 MergeRequest,
2946}
2947
2948impl HostedReviewRequestKind {
2949 #[must_use]
2951 pub fn as_str(self) -> &'static str {
2952 match self {
2953 Self::PullRequest => "pull_request",
2954 Self::MergeRequest => "merge_request",
2955 }
2956 }
2957}
2958
2959impl fmt::Display for HostedReviewRequestKind {
2960 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2961 formatter.write_str(self.as_str())
2962 }
2963}
2964
2965#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2966#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
2967#[serde(rename_all = "camelCase")]
2968pub struct HostedReviewRequestRef {
2969 pub provider: HostingProviderKind,
2970 #[serde(default)]
2971 pub host: Option<String>,
2972 pub kind: HostedReviewRequestKind,
2973 pub id: String,
2974 #[serde(default)]
2975 pub title: Option<String>,
2976 #[serde(default)]
2977 pub url: Option<String>,
2978 #[serde(default)]
2979 pub author: Option<HostedActorRef>,
2980}
2981
2982#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2983#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
2984#[serde(rename_all = "snake_case")]
2985pub enum HostedIssueRelationshipKind {
2986 #[default]
2987 ClosedByReviewRequest,
2988 ReferencedByReviewRequest,
2989 Mentioned,
2990 Manual,
2991}
2992
2993impl HostedIssueRelationshipKind {
2994 #[must_use]
2996 pub fn as_str(self) -> &'static str {
2997 match self {
2998 Self::ClosedByReviewRequest => "closed_by_review_request",
2999 Self::ReferencedByReviewRequest => "referenced_by_review_request",
3000 Self::Mentioned => "mentioned",
3001 Self::Manual => "manual",
3002 }
3003 }
3004}
3005
3006impl fmt::Display for HostedIssueRelationshipKind {
3007 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3008 formatter.write_str(self.as_str())
3009 }
3010}
3011
3012#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3013#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
3014#[serde(rename_all = "camelCase")]
3015pub struct HostedIssueRef {
3016 pub provider: HostingProviderKind,
3017 #[serde(default)]
3018 pub host: Option<String>,
3019 pub id: String,
3020 #[serde(default)]
3021 pub title: Option<String>,
3022 #[serde(default)]
3023 pub url: Option<String>,
3024 pub relationship: HostedIssueRelationshipKind,
3025}
3026
3027#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3028#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
3029#[serde(rename_all = "camelCase")]
3030pub struct ChangesetRevision {
3031 #[serde(default)]
3032 pub actor: Option<HostedActorRef>,
3033 #[serde(default)]
3034 pub commit: Option<HostedCommitRef>,
3035 #[serde(default)]
3036 pub review_request: Option<HostedReviewRequestRef>,
3037}
3038
3039#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3040#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
3041#[serde(rename_all = "camelCase")]
3042pub struct ChangesetContext {
3043 pub provider: HostingProviderKind,
3044 #[serde(default)]
3045 pub host: Option<String>,
3046 #[serde(default)]
3047 pub capabilities: HostingCapabilities,
3048 #[serde(default)]
3049 pub introduced: Option<ChangesetRevision>,
3050 #[serde(default)]
3051 pub last_updated: Option<ChangesetRevision>,
3052 #[serde(default)]
3053 pub related_issues: Vec<HostedIssueRef>,
3054}
3055
3056#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3057#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
3058#[serde(rename_all = "snake_case")]
3059pub enum ChangesetTargetKind {
3060 Package,
3061 Group,
3062}
3063
3064impl ChangesetTargetKind {
3065 #[must_use]
3067 pub fn as_str(self) -> &'static str {
3068 match self {
3069 Self::Package => "package",
3070 Self::Group => "group",
3071 }
3072 }
3073}
3074
3075impl fmt::Display for ChangesetTargetKind {
3076 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3077 formatter.write_str(self.as_str())
3078 }
3079}
3080
3081#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3082#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3083#[serde(rename_all = "camelCase")]
3084pub struct PreparedChangesetTarget {
3085 pub id: String,
3086 pub kind: ChangesetTargetKind,
3087 #[serde(default)]
3088 pub bump: Option<BumpSeverity>,
3089 pub origin: String,
3090 #[serde(default)]
3091 pub evidence_refs: Vec<String>,
3092 #[serde(default)]
3093 pub change_type: Option<String>,
3094 #[serde(default, skip_serializing_if = "Vec::is_empty")]
3095 pub caused_by: Vec<String>,
3096}
3097
3098#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3099#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3100#[serde(rename_all = "camelCase")]
3101pub struct PreparedChangeset {
3102 pub path: PathBuf,
3103 #[serde(default)]
3104 pub summary: Option<String>,
3105 #[serde(default)]
3106 pub details: Option<String>,
3107 pub targets: Vec<PreparedChangesetTarget>,
3108 #[serde(default, alias = "context")]
3109 pub context: Option<ChangesetContext>,
3110}
3111
3112#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3113#[serde(rename_all = "camelCase")]
3114pub struct ReleaseManifestPlanDecision {
3115 pub package: String,
3116 pub bump: BumpSeverity,
3117 pub trigger: String,
3118 pub planned_version: Option<String>,
3119 pub reasons: Vec<String>,
3120 pub upstream_sources: Vec<String>,
3121}
3122
3123#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3124#[serde(rename_all = "camelCase")]
3125pub struct ReleaseManifestPlanGroup {
3126 pub id: String,
3127 pub planned_version: Option<String>,
3128 pub members: Vec<String>,
3129 pub bump: BumpSeverity,
3130}
3131
3132#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3133#[serde(rename_all = "camelCase")]
3134pub struct ReleaseManifestCompatibilityEvidence {
3135 pub package: String,
3136 pub provider: String,
3137 pub severity: BumpSeverity,
3138 pub summary: String,
3139 pub confidence: String,
3140 pub evidence_location: Option<String>,
3141}
3142
3143#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3144#[serde(rename_all = "camelCase")]
3145pub struct ReleaseManifestPlan {
3146 pub workspace_root: PathBuf,
3147 pub decisions: Vec<ReleaseManifestPlanDecision>,
3148 pub groups: Vec<ReleaseManifestPlanGroup>,
3149 pub warnings: Vec<String>,
3150 pub unresolved_items: Vec<String>,
3151 pub compatibility_evidence: Vec<ReleaseManifestCompatibilityEvidence>,
3152}
3153
3154#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3155#[serde(rename_all = "camelCase")]
3156pub struct ReleaseManifest {
3157 pub command: String,
3158 pub dry_run: bool,
3159 #[serde(default)]
3160 pub version: Option<String>,
3161 #[serde(default)]
3162 pub group_version: Option<String>,
3163 pub release_targets: Vec<ReleaseManifestTarget>,
3164 pub released_packages: Vec<String>,
3165 pub changed_files: Vec<PathBuf>,
3166 pub changelogs: Vec<ReleaseManifestChangelog>,
3167 #[serde(default)]
3168 pub package_publications: Vec<PackagePublicationTarget>,
3169 #[serde(default)]
3170 pub changesets: Vec<PreparedChangeset>,
3171 #[serde(default)]
3172 pub deleted_changesets: Vec<PathBuf>,
3173 pub plan: ReleaseManifestPlan,
3174}
3175
3176pub const RELEASE_RECORD_SCHEMA_VERSION: &str = monochange_schema::CURRENT_SCHEMA_VERSION_TEXT;
3178pub const RELEASE_RECORD_KIND: &str = "monochange.releaseRecord";
3180pub const RELEASE_RECORD_HEADING: &str = "## monochange Release Record";
3182pub const RELEASE_RECORD_START_MARKER: &str = "<!-- monochange:release-record:start -->";
3184pub const RELEASE_RECORD_END_MARKER: &str = "<!-- monochange:release-record:end -->";
3186
3187fn release_record_schema_version() -> String {
3188 monochange_schema::CURRENT_SCHEMA_VERSION_TEXT.to_string()
3189}
3190
3191fn default_release_record_kind() -> String {
3192 RELEASE_RECORD_KIND.to_string()
3193}
3194
3195fn default_true() -> bool {
3196 true
3197}
3198
3199fn default_pull_request_branch_prefix() -> String {
3200 "monochange/release".to_string()
3201}
3202
3203fn default_pull_request_base() -> String {
3204 "main".to_string()
3205}
3206
3207fn default_pull_request_title() -> String {
3208 "chore(release): prepare release".to_string()
3209}
3210
3211fn default_pull_request_labels() -> Vec<String> {
3212 vec!["release".to_string(), "automated".to_string()]
3213}
3214
3215fn normalize_legacy_schema_version(raw: &mut serde_json::Value) {
3221 let Some(object) = raw.as_object_mut() else {
3222 return;
3223 };
3224 if let Some(version) = object.get("schemaVersion")
3225 && version.is_u64()
3226 && version.as_u64() == Some(1)
3227 {
3228 object.insert(
3229 "schemaVersion".to_string(),
3230 serde_json::Value::String(RELEASE_RECORD_SCHEMA_VERSION.to_string()),
3231 );
3232 }
3233 if !object.contains_key("schemaVersion")
3234 && object.contains_key("v")
3235 && let Some(v) = object.remove("v")
3236 {
3237 object.insert("schemaVersion".to_string(), v);
3238 }
3239}
3240
3241#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3242#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3243#[serde(rename_all = "camelCase")]
3244pub struct ReleaseRecordTarget {
3245 pub id: String,
3246 pub kind: ReleaseOwnerKind,
3247 pub version: String,
3248 pub version_format: VersionFormat,
3249 pub tag: bool,
3250 pub release: bool,
3251 pub tag_name: String,
3252 #[serde(default)]
3253 pub members: Vec<String>,
3254}
3255
3256#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3257#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3258#[serde(rename_all = "camelCase")]
3259pub struct ReleaseRecordProvider {
3260 pub kind: SourceProvider,
3261 pub owner: String,
3262 pub repo: String,
3263 #[serde(default)]
3264 pub host: Option<String>,
3265}
3266
3267#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3268#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3269#[serde(rename_all = "camelCase")]
3270pub struct ReleaseRecord {
3271 #[serde(default = "release_record_schema_version")]
3272 pub schema_version: String,
3273 #[serde(default = "default_release_record_kind")]
3274 pub kind: String,
3275 pub created_at: String,
3276 pub command: String,
3277 #[serde(default)]
3278 pub version: Option<String>,
3279 #[serde(default)]
3280 pub versions: BTreeMap<String, String>,
3281 pub release_targets: Vec<ReleaseRecordTarget>,
3282 pub released_packages: Vec<String>,
3283 pub changed_files: Vec<PathBuf>,
3284 #[serde(default)]
3285 pub package_publications: Vec<PackagePublicationTarget>,
3286 #[serde(default)]
3287 pub updated_changelogs: Vec<PathBuf>,
3288 #[serde(default)]
3289 pub deleted_changesets: Vec<PathBuf>,
3290 #[serde(default)]
3291 pub changesets: Vec<PreparedChangeset>,
3292 #[serde(default)]
3293 pub changelogs: Vec<ReleaseManifestChangelog>,
3294 #[serde(default)]
3295 pub provider: Option<ReleaseRecordProvider>,
3296}
3297
3298#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3299#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3300#[serde(rename_all = "camelCase")]
3301pub struct ReleaseRecordDiscovery {
3302 pub input_ref: String,
3303 pub resolved_commit: String,
3304 pub record_commit: String,
3305 pub distance: usize,
3306 pub record: ReleaseRecord,
3307}
3308
3309#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3310#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
3311#[serde(rename_all = "snake_case")]
3312pub enum RetargetOperation {
3313 Planned,
3314 Moved,
3315 AlreadyUpToDate,
3316 Skipped,
3317 Failed,
3318}
3319
3320#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3321#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3322#[serde(rename_all = "camelCase")]
3323pub struct RetargetTagResult {
3324 pub tag_name: String,
3325 pub from_commit: String,
3326 pub to_commit: String,
3327 pub operation: RetargetOperation,
3328 #[serde(default)]
3329 pub message: Option<String>,
3330}
3331
3332#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
3333#[serde(rename_all = "snake_case")]
3334pub enum RetargetProviderOperation {
3335 Planned,
3336 Synced,
3337 AlreadyAligned,
3338 Unsupported,
3339 Skipped,
3340 Failed,
3341}
3342
3343#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3344#[serde(rename_all = "camelCase")]
3345pub struct RetargetProviderResult {
3346 pub provider: SourceProvider,
3347 pub tag_name: String,
3348 pub target_commit: String,
3349 pub operation: RetargetProviderOperation,
3350 #[serde(default)]
3351 pub url: Option<String>,
3352 #[serde(default)]
3353 pub message: Option<String>,
3354}
3355
3356#[allow(clippy::struct_excessive_bools)]
3357#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3358#[serde(rename_all = "camelCase")]
3359pub struct RetargetPlan {
3360 pub record_commit: String,
3361 pub target_commit: String,
3362 pub is_descendant: bool,
3363 pub force: bool,
3364 pub git_tag_updates: Vec<RetargetTagResult>,
3365 pub provider_updates: Vec<RetargetProviderResult>,
3366 pub sync_provider: bool,
3367 pub dry_run: bool,
3368}
3369
3370#[allow(clippy::struct_excessive_bools)]
3371#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3372#[serde(rename_all = "camelCase")]
3373pub struct RetargetResult {
3374 pub record_commit: String,
3375 pub target_commit: String,
3376 pub force: bool,
3377 pub git_tag_results: Vec<RetargetTagResult>,
3378 pub provider_results: Vec<RetargetProviderResult>,
3379 pub sync_provider: bool,
3380 pub dry_run: bool,
3381}
3382
3383#[must_use]
3385pub fn release_record_tag_names(record: &ReleaseRecord) -> Vec<String> {
3386 record
3387 .release_targets
3388 .iter()
3389 .filter(|target| target.tag)
3390 .map(|target| target.tag_name.clone())
3391 .collect::<BTreeSet<_>>()
3392 .into_iter()
3393 .collect()
3394}
3395
3396#[must_use]
3398pub fn release_record_release_tag_names(record: &ReleaseRecord) -> Vec<String> {
3399 record
3400 .release_targets
3401 .iter()
3402 .filter(|target| target.release)
3403 .map(|target| target.tag_name.clone())
3404 .collect::<BTreeSet<_>>()
3405 .into_iter()
3406 .collect()
3407}
3408
3409#[derive(Debug, Error)]
3410pub enum ReleaseRecordError {
3411 #[error("no monochange release record block found")]
3412 NotFound,
3413 #[error("found multiple monochange release record blocks")]
3414 MultipleBlocks,
3415 #[error("found a release record start marker without a matching end marker")]
3416 MissingEndMarker,
3417 #[error("found a malformed release record block without a fenced json payload")]
3418 MissingJsonBlock,
3419 #[error("release record is missing required `kind`")]
3420 MissingKind,
3421 #[error("release record is missing required `schemaVersion`")]
3422 MissingSchemaVersion,
3423 #[error("release record uses unsupported kind `{0}`")]
3424 UnsupportedKind(String),
3425 #[error("release record uses unsupported schema version `{0}`")]
3426 UnsupportedSchemaVersionValue(String),
3427 #[error("release record schema error: {0}")]
3428 Schema(String),
3429 #[error("release record json error: {0}")]
3430 InvalidJson(#[from] serde_json::Error),
3431}
3432
3433pub type ReleaseRecordResult<T> = Result<T, ReleaseRecordError>;
3435
3436fn release_record_schema_error_to_error(
3437 error: monochange_schema::SchemaError,
3438) -> ReleaseRecordError {
3439 match error {
3440 monochange_schema::SchemaError::MissingKind => ReleaseRecordError::MissingKind,
3441 monochange_schema::SchemaError::UnsupportedKind { actual, .. } => {
3442 ReleaseRecordError::UnsupportedKind(actual)
3443 }
3444 monochange_schema::SchemaError::MissingVersion => ReleaseRecordError::MissingSchemaVersion,
3445 monochange_schema::SchemaError::UnsupportedVersion { actual, .. }
3446 | monochange_schema::SchemaError::InvalidVersion {
3447 version: actual, ..
3448 } => ReleaseRecordError::UnsupportedSchemaVersionValue(actual),
3449 other => ReleaseRecordError::Schema(other.to_string()),
3450 }
3451}
3452
3453#[must_use = "the rendered record result must be checked"]
3455pub fn render_release_record_block(record: &ReleaseRecord) -> ReleaseRecordResult<String> {
3456 if record.kind != RELEASE_RECORD_KIND {
3457 return Err(ReleaseRecordError::UnsupportedKind(record.kind.clone()));
3458 }
3459 if record.schema_version != RELEASE_RECORD_SCHEMA_VERSION {
3460 return Err(ReleaseRecordError::UnsupportedSchemaVersionValue(
3461 record.schema_version.clone(),
3462 ));
3463 }
3464 let raw = serde_json::to_value(record)?;
3465 let current = monochange_schema::release_record::render_current_value(raw)
3466 .map_err(release_record_schema_error_to_error)?;
3467 let json = serde_json::to_string_pretty(¤t)?;
3468 Ok(format!(
3469 "{RELEASE_RECORD_HEADING}\n\n{RELEASE_RECORD_START_MARKER}\n```json\n{json}\n```\n{RELEASE_RECORD_END_MARKER}"
3470 ))
3471}
3472
3473#[must_use = "the parsed record result must be checked"]
3475pub fn parse_release_record_json(json_text: &str) -> ReleaseRecordResult<ReleaseRecord> {
3476 let mut raw = serde_json::from_str::<serde_json::Value>(json_text)
3477 .map_err(ReleaseRecordError::InvalidJson)?;
3478 normalize_legacy_schema_version(&mut raw);
3479 let kind = raw
3480 .get("kind")
3481 .and_then(serde_json::Value::as_str)
3482 .ok_or(ReleaseRecordError::MissingKind)?;
3483 if kind != RELEASE_RECORD_KIND {
3484 return Err(ReleaseRecordError::UnsupportedKind(kind.to_string()));
3485 }
3486 let current = monochange_schema::release_record::migrate_value(raw)
3487 .map_err(release_record_schema_error_to_error)?;
3488 serde_json::from_value(current).map_err(ReleaseRecordError::InvalidJson)
3489}
3490
3491pub fn parse_release_record_block(commit_message: &str) -> ReleaseRecordResult<ReleaseRecord> {
3492 let start_matches = commit_message
3493 .match_indices(RELEASE_RECORD_START_MARKER)
3494 .collect::<Vec<_>>();
3495 if start_matches.is_empty() {
3496 return Err(ReleaseRecordError::NotFound);
3497 }
3498 let end_matches = commit_message
3499 .match_indices(RELEASE_RECORD_END_MARKER)
3500 .collect::<Vec<_>>();
3501 if end_matches.is_empty() {
3502 return Err(ReleaseRecordError::MissingEndMarker);
3503 }
3504 if start_matches.len() > 1 || end_matches.len() > 1 {
3505 return Err(ReleaseRecordError::MultipleBlocks);
3506 }
3507 let (start_index, _) = start_matches
3508 .first()
3509 .copied()
3510 .unwrap_or_else(|| unreachable!("start marker count was validated"));
3511 let (end_index, _) = end_matches
3512 .first()
3513 .copied()
3514 .unwrap_or_else(|| unreachable!("end marker count was validated"));
3515 if end_index <= start_index {
3516 return Err(ReleaseRecordError::MissingEndMarker);
3517 }
3518 let block_contents =
3519 &commit_message[start_index + RELEASE_RECORD_START_MARKER.len()..end_index];
3520 let json_text = extract_release_record_json(block_contents)?;
3521 let mut raw = serde_json::from_str::<serde_json::Value>(&json_text)?;
3522 normalize_legacy_schema_version(&mut raw);
3523 normalize_legacy_schema_version(&mut raw);
3524 let kind = raw
3525 .get("kind")
3526 .and_then(serde_json::Value::as_str)
3527 .ok_or(ReleaseRecordError::MissingKind)?;
3528 if kind != RELEASE_RECORD_KIND {
3529 return Err(ReleaseRecordError::UnsupportedKind(kind.to_string()));
3530 }
3531 let current = monochange_schema::release_record::migrate_value(raw)
3532 .map_err(release_record_schema_error_to_error)?;
3533 serde_json::from_value(current).map_err(ReleaseRecordError::InvalidJson)
3534}
3535
3536fn extract_release_record_json(block_contents: &str) -> ReleaseRecordResult<String> {
3537 let lines = block_contents.trim().lines().collect::<Vec<_>>();
3538 if lines.first().map(|line| line.trim_end()) != Some("```json") {
3539 return Err(ReleaseRecordError::MissingJsonBlock);
3540 }
3541 let Some(closing_index) = lines
3542 .iter()
3543 .enumerate()
3544 .skip(1)
3545 .find_map(|(index, line)| (line.trim_end() == "```").then_some(index))
3546 else {
3547 return Err(ReleaseRecordError::MissingJsonBlock);
3548 };
3549 if lines
3550 .iter()
3551 .skip(closing_index + 1)
3552 .any(|line| !line.trim().is_empty())
3553 {
3554 return Err(ReleaseRecordError::MissingJsonBlock);
3555 }
3556 let json = lines
3557 .iter()
3558 .skip(1)
3559 .take(closing_index.saturating_sub(1))
3560 .copied()
3561 .collect::<Vec<_>>()
3562 .join("\n");
3563 if json.trim().is_empty() {
3564 return Err(ReleaseRecordError::MissingJsonBlock);
3565 }
3566 Ok(json)
3567}
3568
3569#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3570#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
3571#[serde(rename_all = "snake_case")]
3572pub enum ProviderReleaseNotesSource {
3573 #[default]
3574 Monochange,
3575 #[serde(rename = "github_generated")]
3576 GitHubGenerated,
3577}
3578
3579#[allow(clippy::struct_excessive_bools)]
3580#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3581#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3582pub struct ProviderReleaseSettings {
3583 #[serde(default = "default_true")]
3584 pub enabled: bool,
3585 #[serde(default)]
3586 pub draft: bool,
3587 #[serde(default)]
3588 pub prerelease: bool,
3589 #[serde(default)]
3590 pub generate_notes: bool,
3591 #[serde(default)]
3592 pub source: ProviderReleaseNotesSource,
3593 #[serde(default = "default_release_branch_patterns")]
3594 pub branches: Vec<String>,
3595 #[serde(default = "default_true")]
3596 pub enforce_for_tags: bool,
3597 #[serde(default = "default_true")]
3598 pub enforce_for_publish: bool,
3599 #[serde(default)]
3600 pub enforce_for_commit: bool,
3601 #[serde(
3602 default,
3603 skip_serializing_if = "ReleaseAttestationSettings::is_default"
3604 )]
3605 pub attestations: ReleaseAttestationSettings,
3606}
3607
3608impl Default for ProviderReleaseSettings {
3609 fn default() -> Self {
3610 Self {
3611 enabled: true,
3612 draft: false,
3613 prerelease: false,
3614 generate_notes: false,
3615 source: ProviderReleaseNotesSource::default(),
3616 branches: default_release_branch_patterns(),
3617 enforce_for_tags: true,
3618 enforce_for_publish: true,
3619 enforce_for_commit: false,
3620 attestations: ReleaseAttestationSettings::default(),
3621 }
3622 }
3623}
3624
3625#[allow(clippy::struct_excessive_bools)]
3626#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3627#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3628pub struct ProviderMergeRequestSettings {
3629 #[serde(default = "default_true")]
3630 pub enabled: bool,
3631 #[serde(default = "default_pull_request_branch_prefix")]
3632 pub branch_prefix: String,
3633 #[serde(default = "default_pull_request_base")]
3634 pub base: String,
3635 #[serde(default = "default_pull_request_title")]
3636 pub title: String,
3637 #[serde(default = "default_pull_request_labels")]
3638 pub labels: Vec<String>,
3639 #[serde(default)]
3640 pub auto_merge: bool,
3641 #[serde(default)]
3642 pub verified_commits: bool,
3643}
3644
3645impl Default for ProviderMergeRequestSettings {
3646 fn default() -> Self {
3647 Self {
3648 enabled: true,
3649 branch_prefix: default_pull_request_branch_prefix(),
3650 base: default_pull_request_base(),
3651 title: default_pull_request_title(),
3652 labels: default_pull_request_labels(),
3653 auto_merge: false,
3654 verified_commits: false,
3655 }
3656 }
3657}
3658
3659#[allow(clippy::struct_excessive_bools)]
3660#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3661#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3662pub struct ChangesetAffectedSettings {
3663 #[serde(default = "default_true")]
3664 pub enabled: bool,
3665 #[serde(default = "default_true")]
3666 pub required: bool,
3667 #[serde(default)]
3668 pub skip_labels: Vec<String>,
3669 #[serde(default = "default_true")]
3670 pub comment_on_failure: bool,
3671 #[serde(default)]
3672 pub changed_paths: Vec<String>,
3673 #[serde(default)]
3674 pub ignored_paths: Vec<String>,
3675}
3676
3677impl Default for ChangesetAffectedSettings {
3678 fn default() -> Self {
3679 Self {
3680 enabled: true,
3681 required: true,
3682 skip_labels: Vec::new(),
3683 comment_on_failure: true,
3684 changed_paths: Vec::new(),
3685 ignored_paths: Vec::new(),
3686 }
3687 }
3688}
3689
3690#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3691#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Default)]
3692pub struct ChangesetSettings {
3693 #[serde(default)]
3694 pub affected: ChangesetAffectedSettings,
3695}
3696
3697#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
3698#[serde(rename_all = "snake_case")]
3699pub enum ChangesetPolicyStatus {
3700 Passed,
3701 Failed,
3702 Skipped,
3703 NotRequired,
3704}
3705
3706impl ChangesetPolicyStatus {
3707 #[must_use]
3709 pub fn as_str(self) -> &'static str {
3710 match self {
3711 Self::Passed => "passed",
3712 Self::Failed => "failed",
3713 Self::Skipped => "skipped",
3714 Self::NotRequired => "not_required",
3715 }
3716 }
3717}
3718
3719impl fmt::Display for ChangesetPolicyStatus {
3720 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3721 formatter.write_str(self.as_str())
3722 }
3723}
3724
3725fn default_release_branch_patterns() -> Vec<String> {
3726 vec!["main".to_string()]
3727}
3728
3729#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3730#[serde(rename_all = "camelCase")]
3731pub struct ChangesetPolicyEvaluation {
3732 pub status: ChangesetPolicyStatus,
3733 pub required: bool,
3734 #[serde(default)]
3735 pub enforce: bool,
3736 pub summary: String,
3737 #[serde(default)]
3738 pub comment: Option<String>,
3739 #[serde(default)]
3740 pub labels: Vec<String>,
3741 #[serde(default)]
3742 pub matched_skip_labels: Vec<String>,
3743 #[serde(default)]
3744 pub changed_paths: Vec<String>,
3745 #[serde(default)]
3746 pub matched_paths: Vec<String>,
3747 #[serde(default)]
3748 pub ignored_paths: Vec<String>,
3749 #[serde(default)]
3750 pub changeset_paths: Vec<String>,
3751 #[serde(default)]
3752 pub affected_package_ids: Vec<String>,
3753 #[serde(default)]
3754 pub covered_package_ids: Vec<String>,
3755 #[serde(default)]
3756 pub uncovered_package_ids: Vec<String>,
3757 #[serde(default)]
3758 pub errors: Vec<String>,
3759}
3760
3761#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3762#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
3763pub enum SourceProvider {
3764 #[default]
3765 #[serde(rename = "github")]
3766 GitHub,
3767 #[serde(rename = "gitlab")]
3768 GitLab,
3769 #[serde(rename = "gitea")]
3770 Gitea,
3771 #[serde(rename = "forgejo")]
3772 Forgejo,
3773}
3774
3775impl SourceProvider {
3776 #[must_use]
3778 pub fn as_str(self) -> &'static str {
3779 match self {
3780 Self::GitHub => "github",
3781 Self::GitLab => "gitlab",
3782 Self::Gitea => "gitea",
3783 Self::Forgejo => "forgejo",
3784 }
3785 }
3786}
3787
3788impl fmt::Display for SourceProvider {
3789 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3790 formatter.write_str(self.as_str())
3791 }
3792}
3793
3794#[allow(clippy::struct_excessive_bools)]
3795#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
3796pub struct SourceCapabilities {
3797 pub draft_releases: bool,
3798 pub prereleases: bool,
3799 pub generated_release_notes: bool,
3800 pub auto_merge_change_requests: bool,
3801 pub released_issue_comments: bool,
3802 pub requires_host: bool,
3803}
3804
3805#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3806#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3807pub struct SourceConfiguration {
3808 #[serde(default)]
3809 pub provider: SourceProvider,
3810 pub owner: String,
3811 pub repo: String,
3812 #[serde(default)]
3813 pub host: Option<String>,
3814 #[serde(default)]
3815 pub api_url: Option<String>,
3816 #[serde(default)]
3817 pub releases: ProviderReleaseSettings,
3818 #[serde(default)]
3819 pub pull_requests: ProviderMergeRequestSettings,
3820}
3821
3822#[allow(clippy::struct_excessive_bools)]
3823#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Default)]
3824#[serde(rename_all = "camelCase")]
3825pub struct HostedSourceFeatures {
3826 pub batched_changeset_context_lookup: bool,
3827 pub released_issue_comments: bool,
3828 pub release_retarget_sync: bool,
3829}
3830
3831#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3832#[serde(rename_all = "camelCase")]
3833pub struct HostedIssueCommentPlan {
3834 pub repository: String,
3835 pub issue_id: String,
3836 pub issue_url: Option<String>,
3837 pub body: String,
3838 #[serde(default)]
3839 pub close: bool,
3840}
3841
3842#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
3843#[serde(rename_all = "snake_case")]
3844pub enum HostedIssueCommentOperation {
3845 Created,
3846 SkippedExisting,
3847 Closed,
3848}
3849
3850#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3851#[serde(rename_all = "camelCase")]
3852pub struct HostedIssueCommentOutcome {
3853 pub repository: String,
3854 pub issue_id: String,
3855 pub operation: HostedIssueCommentOperation,
3856 pub url: Option<String>,
3857}
3858
3859pub trait HostedSourceAdapter: Sync {
3860 fn provider(&self) -> SourceProvider;
3861
3862 fn features(&self) -> HostedSourceFeatures {
3863 HostedSourceFeatures::default()
3864 }
3865
3866 fn annotate_changeset_context(
3867 &self,
3868 source: &SourceConfiguration,
3869 changesets: &mut [PreparedChangeset],
3870 );
3871
3872 fn enrich_changeset_context(
3873 &self,
3874 source: &SourceConfiguration,
3875 changesets: &mut [PreparedChangeset],
3876 ) {
3877 self.annotate_changeset_context(source, changesets);
3878 }
3879
3880 fn plan_released_issue_comments(
3881 &self,
3882 _source: &SourceConfiguration,
3883 _manifest: &ReleaseManifest,
3884 ) -> Vec<HostedIssueCommentPlan> {
3885 Vec::new()
3886 }
3887
3888 fn comment_released_issues(
3889 &self,
3890 source: &SourceConfiguration,
3891 manifest: &ReleaseManifest,
3892 ) -> MonochangeResult<Vec<HostedIssueCommentOutcome>> {
3893 let plans = self.plan_released_issue_comments(source, manifest);
3894 if plans.is_empty() {
3895 return Ok(Vec::new());
3896 }
3897 Err(MonochangeError::Config(format!(
3898 "released issue comments are not yet supported for {}",
3899 self.provider()
3900 )))
3901 }
3902
3903 fn plan_retargeted_releases(
3904 &self,
3905 tag_results: &[RetargetTagResult],
3906 ) -> Vec<RetargetProviderResult> {
3907 let provider = self.provider();
3908 let supports_sync = self.features().release_retarget_sync;
3909 tag_results
3910 .iter()
3911 .map(|update| {
3912 RetargetProviderResult {
3913 provider,
3914 tag_name: update.tag_name.clone(),
3915 target_commit: update.to_commit.clone(),
3916 operation: if supports_sync {
3917 RetargetProviderOperation::Planned
3918 } else {
3919 RetargetProviderOperation::Unsupported
3920 },
3921 url: None,
3922 message: (!supports_sync).then_some(format!(
3923 "provider sync is not yet supported for {provider} release retargeting"
3924 )),
3925 }
3926 })
3927 .collect()
3928 }
3929
3930 fn sync_retargeted_releases(
3931 &self,
3932 source: &SourceConfiguration,
3933 tag_results: &[RetargetTagResult],
3934 dry_run: bool,
3935 ) -> MonochangeResult<Vec<RetargetProviderResult>> {
3936 if dry_run {
3937 return Ok(self.plan_retargeted_releases(tag_results));
3938 }
3939 Err(MonochangeError::Config(format!(
3940 "provider sync is not yet supported for {} release retargeting",
3941 source.provider
3942 )))
3943 }
3944}
3945
3946#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3947#[serde(rename_all = "camelCase")]
3948pub struct SourceReleaseRequest {
3949 pub provider: SourceProvider,
3950 pub repository: String,
3951 pub owner: String,
3952 pub repo: String,
3953 pub target_id: String,
3954 pub target_kind: ReleaseOwnerKind,
3955 pub tag_name: String,
3956 pub name: String,
3957 pub body: Option<String>,
3958 pub draft: bool,
3959 pub prerelease: bool,
3960 pub generate_release_notes: bool,
3961}
3962
3963#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3964#[serde(rename_all = "snake_case")]
3965pub enum SourceReleaseOperation {
3966 Created,
3967 Updated,
3968}
3969
3970#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3971#[serde(rename_all = "camelCase")]
3972pub struct SourceReleaseOutcome {
3973 pub provider: SourceProvider,
3974 pub repository: String,
3975 pub tag_name: String,
3976 pub operation: SourceReleaseOperation,
3977 pub url: Option<String>,
3978}
3979
3980#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3981#[serde(rename_all = "camelCase")]
3982pub struct CommitMessage {
3983 pub subject: String,
3984 #[serde(default)]
3985 pub body: Option<String>,
3986}
3987
3988#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
3989#[serde(rename_all = "camelCase")]
3990pub struct SourceChangeRequest {
3991 pub provider: SourceProvider,
3992 pub repository: String,
3993 pub owner: String,
3994 pub repo: String,
3995 pub base_branch: String,
3996 pub head_branch: String,
3997 pub title: String,
3998 pub body: String,
3999 pub labels: Vec<String>,
4000 pub auto_merge: bool,
4001 pub commit_message: CommitMessage,
4002}
4003
4004#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4005#[serde(rename_all = "snake_case")]
4006pub enum SourceChangeRequestOperation {
4007 Created,
4008 Updated,
4009 Skipped,
4010}
4011
4012#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4013#[serde(rename_all = "camelCase")]
4014pub struct SourceChangeRequestOutcome {
4015 pub provider: SourceProvider,
4016 pub repository: String,
4017 pub number: u64,
4018 pub head_branch: String,
4019 pub operation: SourceChangeRequestOperation,
4020 pub url: Option<String>,
4021}
4022
4023#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4024pub struct EffectiveReleaseIdentity {
4025 pub owner_id: String,
4026 pub owner_kind: ReleaseOwnerKind,
4027 pub group_id: Option<String>,
4028 pub tag: bool,
4029 pub release: bool,
4030 pub version_format: VersionFormat,
4031 pub members: Vec<String>,
4032}
4033
4034#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
4035#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4036pub struct WorkspaceConfiguration {
4037 pub root_path: PathBuf,
4038 pub defaults: WorkspaceDefaults,
4039 pub changelog: ChangelogSettings,
4040 pub packages: Vec<PackageDefinition>,
4041 pub groups: Vec<GroupDefinition>,
4042 pub cli: Vec<CliCommandDefinition>,
4043 pub changesets: ChangesetSettings,
4044 pub source: Option<SourceConfiguration>,
4045 #[cfg_attr(feature = "schema", schemars(skip))]
4046 pub lints: lint::WorkspaceLintSettings,
4047 pub cargo: EcosystemSettings,
4048 pub npm: EcosystemSettings,
4049 pub deno: EcosystemSettings,
4050 pub dart: EcosystemSettings,
4051 pub python: EcosystemSettings,
4052 pub go: EcosystemSettings,
4053}
4054
4055impl WorkspaceConfiguration {
4056 #[must_use]
4058 pub fn package_by_id(&self, package_id: &str) -> Option<&PackageDefinition> {
4059 self.packages
4060 .iter()
4061 .find(|package| package.id == package_id)
4062 }
4063
4064 #[must_use]
4066 pub fn group_by_id(&self, group_id: &str) -> Option<&GroupDefinition> {
4067 self.groups.iter().find(|group| group.id == group_id)
4068 }
4069
4070 #[must_use]
4072 pub fn group_for_package(&self, package_id: &str) -> Option<&GroupDefinition> {
4073 self.groups
4074 .iter()
4075 .find(|group| group.packages.iter().any(|member| member == package_id))
4076 }
4077
4078 #[must_use]
4080 pub fn effective_release_identity(&self, package_id: &str) -> Option<EffectiveReleaseIdentity> {
4081 let package = self.package_by_id(package_id)?;
4082 if let Some(group) = self.group_for_package(package_id) {
4083 return Some(EffectiveReleaseIdentity {
4084 owner_id: group.id.clone(),
4085 owner_kind: ReleaseOwnerKind::Group,
4086 group_id: Some(group.id.clone()),
4087 tag: group.tag,
4088 release: group.release,
4089 version_format: group.version_format,
4090 members: group.packages.clone(),
4091 });
4092 }
4093
4094 Some(EffectiveReleaseIdentity {
4095 owner_id: package.id.clone(),
4096 owner_kind: ReleaseOwnerKind::Package,
4097 group_id: None,
4098 tag: package.tag,
4099 release: package.release,
4100 version_format: package.version_format,
4101 members: vec![package.id.clone()],
4102 })
4103 }
4104}
4105
4106#[must_use]
4108pub fn default_cli_commands() -> Vec<CliCommandDefinition> {
4109 vec![]
4110}
4111
4112#[must_use]
4114pub fn all_step_variants() -> Vec<CliStepDefinition> {
4115 vec![
4116 CliStepDefinition::Config {
4117 name: None,
4118 when: None,
4119 always_run: false,
4120 inputs: BTreeMap::new(),
4121 },
4122 CliStepDefinition::Validate {
4123 name: None,
4124 when: None,
4125 always_run: false,
4126 inputs: BTreeMap::new(),
4127 },
4128 CliStepDefinition::Discover {
4129 name: None,
4130 when: None,
4131 always_run: false,
4132 inputs: BTreeMap::new(),
4133 },
4134 CliStepDefinition::DisplayVersions {
4135 name: None,
4136 when: None,
4137 always_run: false,
4138 inputs: BTreeMap::new(),
4139 },
4140 CliStepDefinition::CreateChangeFile {
4141 name: None,
4142 when: None,
4143 always_run: false,
4144 show_progress: None,
4145 inputs: BTreeMap::new(),
4146 },
4147 CliStepDefinition::PrepareRelease {
4148 name: None,
4149 when: None,
4150 always_run: false,
4151 inputs: BTreeMap::new(),
4152 allow_empty_changesets: false,
4153 },
4154 CliStepDefinition::CommitRelease {
4155 name: None,
4156 when: None,
4157 always_run: false,
4158 no_verify: false,
4159 update_release_json: false,
4160 inputs: BTreeMap::new(),
4161 },
4162 CliStepDefinition::VerifyReleaseBranch {
4163 name: None,
4164 when: None,
4165 always_run: false,
4166 inputs: BTreeMap::new(),
4167 },
4168 CliStepDefinition::PublishRelease {
4169 name: None,
4170 when: None,
4171 always_run: false,
4172 inputs: BTreeMap::new(),
4173 },
4174 CliStepDefinition::PlaceholderPublish {
4175 name: None,
4176 when: None,
4177 always_run: false,
4178 inputs: BTreeMap::new(),
4179 },
4180 CliStepDefinition::PublishPackages {
4181 name: None,
4182 when: None,
4183 always_run: false,
4184 inputs: BTreeMap::new(),
4185 },
4186 CliStepDefinition::PlanPublishRateLimits {
4187 name: None,
4188 when: None,
4189 always_run: false,
4190 inputs: BTreeMap::new(),
4191 },
4192 CliStepDefinition::OpenReleaseRequest {
4193 name: None,
4194 when: None,
4195 always_run: false,
4196 no_verify: false,
4197 inputs: BTreeMap::new(),
4198 },
4199 CliStepDefinition::CommentReleasedIssues {
4200 name: None,
4201 when: None,
4202 always_run: false,
4203 inputs: BTreeMap::new(),
4204 },
4205 CliStepDefinition::AffectedPackages {
4206 name: None,
4207 when: None,
4208 always_run: false,
4209 inputs: BTreeMap::new(),
4210 },
4211 CliStepDefinition::DiagnoseChangesets {
4212 name: None,
4213 when: None,
4214 always_run: false,
4215 inputs: BTreeMap::new(),
4216 },
4217 CliStepDefinition::RetargetRelease {
4218 name: None,
4219 when: None,
4220 always_run: false,
4221 inputs: BTreeMap::new(),
4222 },
4223 ]
4224}
4225
4226#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4227pub struct VersionGroup {
4228 pub group_id: String,
4229 pub display_name: String,
4230 pub members: Vec<String>,
4231 pub mismatch_detected: bool,
4232}
4233
4234#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4235pub struct PlannedVersionGroup {
4236 pub group_id: String,
4237 pub display_name: String,
4238 pub members: Vec<String>,
4239 pub mismatch_detected: bool,
4240 pub planned_version: Option<Version>,
4241 pub recommended_bump: BumpSeverity,
4242}
4243
4244#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4245pub struct ChangeSignal {
4246 pub package_id: String,
4247 pub requested_bump: Option<BumpSeverity>,
4248 pub explicit_version: Option<Version>,
4249 pub change_origin: String,
4250 pub evidence_refs: Vec<String>,
4251 pub notes: Option<String>,
4252 pub details: Option<String>,
4253 pub change_type: Option<String>,
4254 pub caused_by: Vec<String>,
4255 pub source_path: PathBuf,
4256}
4257
4258#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4259pub struct CompatibilityAssessment {
4260 pub package_id: String,
4261 pub provider_id: String,
4262 pub severity: BumpSeverity,
4263 pub confidence: String,
4264 pub summary: String,
4265 pub evidence_location: Option<String>,
4266}
4267
4268#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4269pub struct ReleaseDecision {
4270 pub package_id: String,
4271 pub trigger_type: String,
4272 pub recommended_bump: BumpSeverity,
4273 pub planned_version: Option<Version>,
4274 pub group_id: Option<String>,
4275 pub reasons: Vec<String>,
4276 pub upstream_sources: Vec<String>,
4277 pub warnings: Vec<String>,
4278}
4279
4280#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4281pub struct ReleasePlan {
4282 pub workspace_root: PathBuf,
4283 pub decisions: Vec<ReleaseDecision>,
4284 pub groups: Vec<PlannedVersionGroup>,
4285 pub warnings: Vec<String>,
4286 pub unresolved_items: Vec<String>,
4287 pub compatibility_evidence: Vec<CompatibilityAssessment>,
4288}
4289
4290#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
4291pub struct DiscoveryReport {
4292 pub workspace_root: PathBuf,
4293 pub packages: Vec<PackageRecord>,
4294 pub dependencies: Vec<DependencyEdge>,
4295 pub version_groups: Vec<VersionGroup>,
4296 pub warnings: Vec<String>,
4297}
4298
4299#[derive(Debug, Clone, Eq, PartialEq)]
4300pub struct AdapterDiscovery {
4301 pub packages: Vec<PackageRecord>,
4302 pub warnings: Vec<String>,
4303}
4304
4305pub trait EcosystemAdapter {
4306 fn ecosystem(&self) -> Ecosystem;
4307
4308 fn discover(&self, root: &Path) -> MonochangeResult<AdapterDiscovery>;
4309
4310 fn load_configured(
4311 &self,
4312 root: &Path,
4313 package_path: &Path,
4314 ) -> MonochangeResult<Option<PackageRecord>>;
4315
4316 fn supported_versioned_file_kind(&self, path: &Path) -> bool;
4317
4318 fn validate_versioned_file(
4319 &self,
4320 full_path: &Path,
4321 display_path: &str,
4322 custom_fields: Option<&[String]>,
4323 ) -> MonochangeResult<()>;
4324}
4325
4326#[must_use]
4328#[derive(Default)]
4333pub struct EcosystemRegistry {
4334 adapters: Vec<Box<dyn EcosystemAdapter>>,
4335}
4336
4337impl EcosystemRegistry {
4338 pub fn new() -> Self {
4339 Self::default()
4340 }
4341
4342 pub fn with_adapter(mut self, adapter: Box<dyn EcosystemAdapter>) -> Self {
4343 self.adapters.push(adapter);
4344 self
4345 }
4346
4347 pub fn push_adapter(&mut self, adapter: Box<dyn EcosystemAdapter>) {
4348 self.adapters.push(adapter);
4349 }
4350
4351 pub fn discover_all(&self, root: &Path) -> MonochangeResult<AdapterDiscovery> {
4352 let mut packages = Vec::new();
4353 let mut warnings = Vec::new();
4354 for adapter in &self.adapters {
4355 let mut result = adapter.discover(root)?;
4356 packages.append(&mut result.packages);
4357 warnings.append(&mut result.warnings);
4358 }
4359 Ok(AdapterDiscovery { packages, warnings })
4360 }
4361
4362 pub fn adapter_for_ecosystem(&self, ecosystem: Ecosystem) -> Option<&dyn EcosystemAdapter> {
4363 self.adapters
4364 .iter()
4365 .find(|a| a.ecosystem() == ecosystem)
4366 .map(AsRef::as_ref)
4367 }
4368
4369 pub fn load_configured(
4370 &self,
4371 root: &Path,
4372 package_path: &Path,
4373 ecosystem: Ecosystem,
4374 ) -> MonochangeResult<Option<PackageRecord>> {
4375 match self.adapter_for_ecosystem(ecosystem) {
4376 Some(adapter) => adapter.load_configured(root, package_path),
4377 None => Ok(None),
4378 }
4379 }
4380
4381 pub fn supported_versioned_file_kind(&self, path: &Path, ecosystem: Ecosystem) -> bool {
4382 self.adapter_for_ecosystem(ecosystem)
4383 .is_some_and(|adapter| adapter.supported_versioned_file_kind(path))
4384 }
4385
4386 pub fn validate_versioned_file(
4387 &self,
4388 full_path: &Path,
4389 display_path: &str,
4390 ecosystem: Ecosystem,
4391 custom_fields: Option<&[String]>,
4392 ) -> MonochangeResult<()> {
4393 match self.adapter_for_ecosystem(ecosystem) {
4394 Some(adapter) => {
4395 adapter.validate_versioned_file(full_path, display_path, custom_fields)
4396 }
4397 None => {
4398 Err(MonochangeError::Config(format!(
4399 "no adapter registered for ecosystem `{ecosystem}`"
4400 )))
4401 }
4402 }
4403 }
4404}
4405
4406pub fn materialize_dependency_edges(packages: &[PackageRecord]) -> Vec<DependencyEdge> {
4407 let mut package_ids_by_name = BTreeMap::<String, Vec<String>>::new();
4408 for package in packages {
4409 package_ids_by_name
4410 .entry(package.name.clone())
4411 .or_default()
4412 .push(package.id.clone());
4413 }
4414
4415 let mut edges = Vec::new();
4416 for package in packages {
4417 for dependency in &package.declared_dependencies {
4418 if let Some(target_package_ids) = package_ids_by_name.get(&dependency.name) {
4419 for target_package_id in target_package_ids {
4420 edges.push(DependencyEdge {
4421 from_package_id: package.id.clone(),
4422 to_package_id: target_package_id.clone(),
4423 dependency_kind: dependency.kind,
4424 source_kind: DependencySourceKind::Manifest,
4425 version_constraint: dependency.version_constraint.clone(),
4426 is_optional: dependency.optional,
4427 is_direct: true,
4428 });
4429 }
4430 }
4431 }
4432 }
4433
4434 edges
4435}
4436
4437#[cfg(test)]
4438#[path = "__tests__/proptest_bump_severity_tests.rs"]
4439mod proptest_bump_severity;
4440
4441#[cfg(feature = "schema")]
4442pub mod schema {
4443 pub fn release_record() -> schemars::Schema {
4445 schemars::schema_for!(super::ReleaseRecord)
4446 }
4447}
4448
4449#[cfg(test)]
4450#[path = "__tests__/lib_tests.rs"]
4451mod tests;