Skip to main content

ossctl_core/release/adapters/
node.rs

1//! Node ecosystem adapter: `release-please`, `changesets`, and `npm-publish`.
2//!
3//! Publishes packages to the npm registry. `npm-publish` publishes directly;
4//! `release-please` and `changesets` are release-automation tools whose publish
5//! step is normally CI-driven, so their publish bodies here are clearly-marked
6//! skeletons. `verify` reconciles against npm via the default [`RegistryQuery`]
7//! path.
8
9use std::time::Duration;
10
11use crate::contract::schema::Adapter;
12use crate::protocol::release::{BuildArtifacts, DryRunReport, PlannedCommand, PublishReceipt};
13
14use super::{make_receipt, run_all, AdapterError, AdapterTarget, EffectCtx, ReleaseAdapter};
15
16/// The node release adapter, operating as `release-please`, `changesets`, or
17/// `npm-publish`.
18pub struct NodeAdapter {
19    adapter: Adapter,
20}
21
22impl NodeAdapter {
23    /// Construct for a resolved node adapter identity.
24    #[must_use]
25    pub fn new(adapter: Adapter) -> Self {
26        debug_assert!(matches!(
27            adapter,
28            Adapter::ReleasePlease | Adapter::Changesets | Adapter::NpmPublish
29        ));
30        Self { adapter }
31    }
32}
33
34impl ReleaseAdapter for NodeAdapter {
35    fn adapter(&self) -> Adapter {
36        self.adapter
37    }
38
39    fn dry_run(
40        &self,
41        _ctx: &EffectCtx<'_>,
42        _t: &AdapterTarget,
43    ) -> Result<DryRunReport, AdapterError> {
44        let (planned_commands, notes) = match self.adapter {
45            Adapter::Changesets => (
46                vec![PlannedCommand::new("changeset", &["status", "--verbose"])],
47                vec![],
48            ),
49            Adapter::ReleasePlease => (
50                vec![PlannedCommand::new(
51                    "release-please",
52                    &["release-pr", "--dry-run"],
53                )],
54                vec!["release-please publishes on merge via CI, not from this host".to_string()],
55            ),
56            _ => (
57                vec![PlannedCommand::new("npm", &["publish", "--dry-run"])],
58                vec![],
59            ),
60        };
61        Ok(DryRunReport {
62            adapter: self.adapter,
63            planned_commands,
64            notes,
65        })
66    }
67
68    fn build(
69        &self,
70        ctx: &EffectCtx<'_>,
71        t: &AdapterTarget,
72    ) -> Result<BuildArtifacts, AdapterError> {
73        run_all(ctx, &[PlannedCommand::new("npm", &["pack"])])?;
74        // SKELETON: a production build reads the tarball name npm reports.
75        Ok(BuildArtifacts {
76            adapter: self.adapter,
77            artifacts: vec![format!("{}-{}.tgz", t.package, t.version)],
78            notes: vec![],
79        })
80    }
81
82    fn publish(
83        &self,
84        ctx: &EffectCtx<'_>,
85        t: &AdapterTarget,
86    ) -> Result<PublishReceipt, AdapterError> {
87        // PER-TARGET IRREVERSIBLE.
88        let cmds = match self.adapter {
89            Adapter::Changesets => vec![PlannedCommand::new("changeset", &["publish"])],
90            // SKELETON: release-please's release + npm publish is a CI job; the
91            // representative host command creates the GitHub release it keys off.
92            Adapter::ReleasePlease => {
93                vec![PlannedCommand::new("release-please", &["github-release"])]
94            }
95            _ => vec![PlannedCommand::new("npm", &["publish"])],
96        };
97        run_all(ctx, &cmds)?;
98        let remote_url = Some(format!(
99            "https://www.npmjs.com/package/{}/v/{}",
100            t.package, t.version
101        ));
102        Ok(make_receipt(ctx, t, None, remote_url))
103    }
104
105    fn timeout(&self) -> Duration {
106        Duration::from_secs(600)
107    }
108}