github_actions_models/dependabot/
v2.rs

1//! "v2" Dependabot models.
2//!
3//! Resources:
4//! * [Configuration options for the `dependabot.yml` file](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file)
5//! * [JSON Schema for Dependabot v2](https://json.schemastore.org/dependabot-2.0.json)
6
7use indexmap::{IndexMap, IndexSet};
8use serde::Deserialize;
9
10use crate::common::custom_error;
11
12/// A `dependabot.yml` configuration file.
13#[derive(Deserialize, Debug)]
14#[serde(rename_all = "kebab-case")]
15pub struct Dependabot {
16    /// Invariant: `2`
17    pub version: u64,
18    #[serde(default)]
19    pub enable_beta_ecosystems: bool,
20    #[serde(default)]
21    pub registries: IndexMap<String, Registry>,
22    pub updates: Vec<Update>,
23}
24
25/// Different registries known to Dependabot.
26#[derive(Deserialize, Debug)]
27#[serde(rename_all = "kebab-case", tag = "type")]
28pub enum Registry {
29    ComposerRepository {
30        url: String,
31        username: Option<String>,
32        password: Option<String>,
33    },
34    DockerRegistry {
35        url: String,
36        username: Option<String>,
37        password: Option<String>,
38        #[serde(default)]
39        replaces_base: bool,
40    },
41    Git {
42        url: String,
43        username: Option<String>,
44        password: Option<String>,
45    },
46    HexOrganization {
47        organization: String,
48        key: Option<String>,
49    },
50    HexRepository {
51        repo: Option<String>,
52        url: String,
53        auth_key: Option<String>,
54        public_key_fingerprint: Option<String>,
55    },
56    MavenRepository {
57        url: String,
58        username: Option<String>,
59        password: Option<String>,
60    },
61    NpmRegistry {
62        url: String,
63        username: Option<String>,
64        password: Option<String>,
65        #[serde(default)]
66        replaces_base: bool,
67    },
68    NugetFeed {
69        url: String,
70        username: Option<String>,
71        password: Option<String>,
72    },
73    PythonIndex {
74        url: String,
75        username: Option<String>,
76        password: Option<String>,
77        #[serde(default)]
78        replaces_base: bool,
79    },
80    RubygemsServer {
81        url: String,
82        username: Option<String>,
83        password: Option<String>,
84        #[serde(default)]
85        replaces_base: bool,
86    },
87    TerraformRegistry {
88        url: String,
89        token: Option<String>,
90    },
91}
92
93/// Cooldown settings for Dependabot updates.
94#[derive(Deserialize, Debug)]
95#[serde(rename_all = "kebab-case")]
96pub struct Cooldown {
97    pub default_days: Option<u64>,
98    pub semver_major_days: Option<u64>,
99    pub semver_minor_days: Option<u64>,
100    pub semver_patch_days: Option<u64>,
101    #[serde(default)]
102    pub include: Vec<String>,
103    #[serde(default)]
104    pub exclude: Vec<String>,
105}
106
107/// A `directory` or `directories` field in a Dependabot `update` directive.
108#[derive(Deserialize, Debug, PartialEq)]
109#[serde(rename_all = "kebab-case")]
110pub enum Directories {
111    Directory(String),
112    Directories(Vec<String>),
113}
114
115/// A single `update` directive.
116#[derive(Deserialize, Debug)]
117#[serde(rename_all = "kebab-case")]
118pub struct Update {
119    #[serde(default)]
120    pub allow: Vec<Allow>,
121    #[serde(default)]
122    pub assignees: IndexSet<String>,
123    pub commit_message: Option<CommitMessage>,
124    pub cooldown: Option<Cooldown>,
125    #[serde(flatten)]
126    pub directories: Directories,
127    #[serde(default)]
128    pub groups: IndexMap<String, Group>,
129    #[serde(default)]
130    pub ignore: Vec<Ignore>,
131    #[serde(default)]
132    pub insecure_external_code_execution: AllowDeny,
133    /// Labels to apply to this update group's pull requests.
134    ///
135    /// The default label is `dependencies`.
136    #[serde(default = "default_labels")]
137    pub labels: IndexSet<String>,
138    pub milestone: Option<u64>,
139    /// The maximum number of pull requests to open at a time from this
140    /// update group.
141    ///
142    /// The default maximum is 5.
143    #[serde(default = "default_open_pull_requests_limit")]
144    pub open_pull_requests_limit: u64,
145    pub package_ecosystem: PackageEcosystem,
146    // TODO: pull-request-branch-name
147    #[serde(default)]
148    pub rebase_strategy: RebaseStrategy,
149    #[serde(default, deserialize_with = "crate::common::scalar_or_vector")]
150    pub registries: Vec<String>,
151    #[serde(default)]
152    pub reviewers: IndexSet<String>,
153    pub schedule: Schedule,
154    pub target_branch: Option<String>,
155    #[serde(default)]
156    pub vendor: bool,
157    pub versioning_strategy: Option<VersioningStrategy>,
158}
159
160#[inline]
161fn default_labels() -> IndexSet<String> {
162    IndexSet::from(["dependencies".to_string()])
163}
164
165#[inline]
166fn default_open_pull_requests_limit() -> u64 {
167    // https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#open-pull-requests-limit
168    5
169}
170
171/// Allow rules for Dependabot updates.
172#[derive(Deserialize, Debug)]
173#[serde(rename_all = "kebab-case")]
174pub struct Allow {
175    pub dependency_name: Option<String>,
176    pub dependency_type: Option<DependencyType>,
177}
178
179/// Dependency types in `allow` rules.
180#[derive(Deserialize, Debug)]
181#[serde(rename_all = "kebab-case")]
182pub enum DependencyType {
183    Direct,
184    Indirect,
185    All,
186    Production,
187    Development,
188}
189
190/// Commit message settings for Dependabot updates.
191#[derive(Deserialize, Debug)]
192#[serde(rename_all = "kebab-case")]
193pub struct CommitMessage {
194    pub prefix: Option<String>,
195    pub prefix_development: Option<String>,
196    /// Invariant: `"scope"`
197    pub include: Option<String>,
198}
199
200/// Group settings for batched updates.
201#[derive(Deserialize, Debug)]
202#[serde(rename_all = "kebab-case")]
203pub struct Group {
204    /// This can only be [`DependencyType::Development`] or
205    /// [`DependencyType::Production`].
206    pub dependency_type: Option<DependencyType>,
207    #[serde(default)]
208    pub patterns: IndexSet<String>,
209    #[serde(default)]
210    pub exclude_patterns: IndexSet<String>,
211    #[serde(default)]
212    pub update_types: IndexSet<UpdateType>,
213}
214
215/// Update types for grouping.
216#[derive(Deserialize, Debug, Hash, Eq, PartialEq)]
217#[serde(rename_all = "kebab-case")]
218pub enum UpdateType {
219    Major,
220    Minor,
221    Patch,
222}
223
224/// Dependency ignore settings for updates.
225#[derive(Deserialize, Debug)]
226#[serde(rename_all = "kebab-case")]
227pub struct Ignore {
228    pub dependency_name: Option<String>,
229    /// These are, inexplicably, not [`UpdateType`] variants.
230    /// Instead, they're strings like `"version-update:semver-{major,minor,patch}"`.
231    #[serde(default)]
232    pub update_types: IndexSet<String>,
233    #[serde(default)]
234    pub versions: IndexSet<String>,
235}
236
237/// An "allow"/"deny" toggle.
238#[derive(Deserialize, Debug, Default)]
239#[serde(rename_all = "kebab-case")]
240pub enum AllowDeny {
241    Allow,
242    #[default]
243    Deny,
244}
245
246/// Supported packaging ecosystems.
247#[derive(Deserialize, Debug, PartialEq)]
248#[serde(rename_all = "kebab-case")]
249pub enum PackageEcosystem {
250    /// `bun`
251    Bun,
252    /// `bundler`
253    Bundler,
254    /// `cargo`
255    Cargo,
256    /// `composer`
257    Composer,
258    /// `conda`
259    Conda,
260    /// `devcontainers`
261    Devcontainers,
262    /// `docker`
263    Docker,
264    /// `docker-compose`
265    DockerCompose,
266    /// `dotnet-sdk`
267    DotnetSdk,
268    /// `helm`
269    Helm,
270    /// `elm`
271    Elm,
272    /// `gitsubmodule`
273    Gitsubmodule,
274    /// `github-actions`
275    GithubActions,
276    /// `gomod`
277    Gomod,
278    /// `gradle`
279    Gradle,
280    /// `maven`
281    Maven,
282    /// `mix`
283    Mix,
284    /// `npm`
285    Npm,
286    /// `nuget`
287    Nuget,
288    /// `pip`
289    Pip,
290    /// `pub`
291    Pub,
292    /// `rust-toolchain`
293    RustToolchain,
294    /// `swift`
295    Swift,
296    /// `terraform`
297    Terraform,
298    /// `uv`
299    Uv,
300    /// `vcpkg`
301    Vcpkg,
302}
303
304/// Rebase strategies for Dependabot updates.
305#[derive(Deserialize, Debug, Default, PartialEq)]
306#[serde(rename_all = "kebab-case")]
307pub enum RebaseStrategy {
308    #[default]
309    Auto,
310    Disabled,
311}
312
313/// Scheduling settings for Dependabot updates.
314#[derive(Deserialize, Debug)]
315#[serde(rename_all = "kebab-case", remote = "Self")]
316pub struct Schedule {
317    pub interval: Interval,
318    pub day: Option<Day>,
319    pub time: Option<String>,
320    pub timezone: Option<String>,
321    pub cronjob: Option<String>,
322}
323
324impl<'de> Deserialize<'de> for Schedule {
325    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
326    where
327        D: serde::Deserializer<'de>,
328    {
329        let schedule = Self::deserialize(deserializer)?;
330
331        if schedule.interval == Interval::Cron && schedule.cronjob.is_none() {
332            return Err(custom_error::<D>(
333                "`schedule.cronjob` must be set when `schedule.interval` is `cron`",
334            ));
335        }
336
337        if schedule.interval != Interval::Cron && schedule.cronjob.is_some() {
338            return Err(custom_error::<D>(
339                "`schedule.cronjob` may only be set when `schedule.interval` is `cron`",
340            ));
341        }
342
343        if schedule.interval != Interval::Weekly && schedule.day.is_some() {
344            return Err(custom_error::<D>(
345                "`schedule.day` is only valid when `schedule.interval` is `weekly`",
346            ));
347        }
348
349        Ok(Self {
350            interval: schedule.interval,
351            day: schedule.day,
352            time: schedule.time,
353            timezone: schedule.timezone,
354            cronjob: schedule.cronjob,
355        })
356    }
357}
358
359/// Schedule intervals.
360#[derive(Deserialize, Debug, PartialEq)]
361#[serde(rename_all = "kebab-case")]
362pub enum Interval {
363    Daily,
364    Weekly,
365    Monthly,
366    Quarterly,
367    Semiannually,
368    Yearly,
369    Cron,
370}
371
372/// Days of the week.
373#[derive(Deserialize, Debug, PartialEq)]
374#[serde(rename_all = "kebab-case")]
375pub enum Day {
376    Monday,
377    Tuesday,
378    Wednesday,
379    Thursday,
380    Friday,
381    Saturday,
382    Sunday,
383}
384
385/// Versioning strategies.
386#[derive(Deserialize, Debug, PartialEq)]
387#[serde(rename_all = "kebab-case")]
388pub enum VersioningStrategy {
389    Auto,
390    Increase,
391    IncreaseIfNecessary,
392    LockfileOnly,
393    Widen,
394}