configure_semantic_release_assets/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(warnings)]
3
4use std::{
5    collections::HashSet,
6    io::{self, BufWriter, Write},
7    path::PathBuf,
8    str::FromStr,
9};
10use std::{fs::File, io::Read, path::Path};
11
12use indexmap::{map::Entry, IndexMap};
13use log::debug;
14
15mod error;
16
17use crate::error::Error;
18
19#[derive(Debug)]
20pub enum WriteTo {
21    Stdout,
22    InPlace,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub enum ModifiedFlag {
27    Unmodified,
28    Modified,
29}
30
31#[derive(Debug)]
32pub struct SemanticReleaseManifest {
33    inner: IndexMap<String, serde_json::Value>,
34}
35
36impl FromStr for SemanticReleaseManifest {
37    type Err = serde_json::Error;
38
39    fn from_str(s: &str) -> Result<Self, Self::Err> {
40        Ok(Self {
41            inner: serde_json::from_str(s)?,
42        })
43    }
44}
45
46pub struct SemanticReleaseConfiguration {
47    manifest: SemanticReleaseManifest,
48    manifest_path: PathBuf,
49    dirty: ModifiedFlag,
50}
51
52fn plugin_name(plugin: &serde_json::Value) -> Option<&str> {
53    match plugin {
54        serde_json::Value::String(name) => Some(name.as_str()),
55        serde_json::Value::Array(array) => array.get(0).and_then(|value| value.as_str()),
56        _ => None,
57    }
58}
59
60fn plugin_configuration(
61    plugin: &mut serde_json::Value,
62) -> Option<&mut serde_json::Map<String, serde_json::Value>> {
63    match plugin {
64        serde_json::Value::Array(array) => array.get_mut(1).and_then(|value| value.as_object_mut()),
65        _ => None,
66    }
67}
68
69impl SemanticReleaseManifest {
70    pub fn apply_whitelist(&mut self, whitelist: HashSet<String>) -> ModifiedFlag {
71        let mut dirty = ModifiedFlag::Unmodified;
72
73        if let Entry::Occupied(mut entry) = self.inner.entry("plugins".to_owned()) {
74            if let Some(plugins) = entry.get_mut().as_array_mut() {
75                for plugin in plugins {
76                    if plugin_name(plugin) != Some("@semantic-release/github") {
77                        continue;
78                    }
79
80                    if let Some(assets) = plugin_configuration(plugin)
81                        .and_then(|settings| settings.get_mut("assets"))
82                        .and_then(|assets| assets.as_array_mut())
83                    {
84                        assets.retain(|asset| {
85                            let label = asset
86                                .as_object()
87                                .and_then(|asset| asset.get("label"))
88                                .and_then(|label| label.as_str());
89                            match label {
90                                Some(label) => {
91                                    let keep = whitelist.contains(label);
92                                    if !keep {
93                                        dirty = ModifiedFlag::Modified;
94                                    }
95                                    keep
96                                }
97                                // Not sure what this is, so pass it through unchanged
98                                None => true,
99                            }
100                        });
101                    };
102                }
103            }
104        };
105
106        dirty
107    }
108}
109
110impl std::fmt::Display for SemanticReleaseManifest {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        write!(f, "{}", serde_json::to_string_pretty(&self.inner).unwrap())
113    }
114}
115
116impl SemanticReleaseConfiguration {
117    // FIXME: use find-semantic-release-manifest.
118    // For now, assume the semantic-release configuration is a .releaserc.json and document the limitation
119    pub fn read_from_file(semantic_release_manifest_path: &Path) -> Result<Self, Error> {
120        debug!(
121            "Reading semantic-release configuration from file {:?}",
122            semantic_release_manifest_path
123        );
124
125        if !semantic_release_manifest_path.exists() {
126            return Err(Error::configuration_file_not_found_error(
127                semantic_release_manifest_path,
128            ));
129        }
130
131        // Reading a file into a string before invoking Serde is faster than
132        // invoking Serde from a BufReader, see
133        // https://github.com/serde-rs/json/issues/160
134        let mut string = String::new();
135        File::open(semantic_release_manifest_path)
136            .map_err(|err| Error::file_open_error(err, semantic_release_manifest_path))?
137            .read_to_string(&mut string)
138            .map_err(|err| Error::file_read_error(err, semantic_release_manifest_path))?;
139
140        Ok(Self {
141            manifest: SemanticReleaseManifest::from_str(&string)
142                .map_err(|err| Error::file_parse_error(err, semantic_release_manifest_path))?,
143            manifest_path: semantic_release_manifest_path.to_owned(),
144            dirty: ModifiedFlag::Unmodified,
145        })
146    }
147
148    fn write(&mut self, mut w: impl Write) -> Result<(), Error> {
149        debug!(
150            "Writing semantic-release configuration to file {:?}",
151            self.manifest_path
152        );
153        serde_json::to_writer_pretty(&mut w, &self.manifest.inner)
154            .map_err(Error::file_serialize_error)?;
155        w.write_all(b"\n")
156            .map_err(|err| Error::file_write_error(err, &self.manifest_path))?;
157        w.flush()
158            .map_err(|err| Error::file_write_error(err, &self.manifest_path))?;
159
160        Ok(())
161    }
162
163    pub fn write_if_modified(&mut self, write_to: WriteTo) -> Result<(), Error> {
164        match self.dirty {
165            ModifiedFlag::Unmodified => Ok(()),
166            ModifiedFlag::Modified => match write_to {
167                WriteTo::Stdout => self.write(io::stdout()),
168                WriteTo::InPlace => {
169                    let file = File::create(&self.manifest_path)
170                        .map_err(|err| Error::file_open_error(err, &self.manifest_path))?;
171                    self.write(BufWriter::new(file))
172                }
173            },
174        }
175    }
176
177    pub fn apply_whitelist(&mut self, to_remove: HashSet<String>) {
178        let modified = self.manifest.apply_whitelist(to_remove);
179        if modified == ModifiedFlag::Modified {
180            self.dirty = ModifiedFlag::Modified;
181        }
182    }
183}