Skip to main content

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 multi_ecosystem_groups: IndexMap<String, MultiEcosystemGroup>,
22    #[serde(default)]
23    pub registries: IndexMap<String, Registry>,
24    pub updates: Vec<Update>,
25}
26
27/// A multi-ecosystem update group.
28#[derive(Deserialize, Debug)]
29#[serde(rename_all = "kebab-case")]
30pub struct MultiEcosystemGroup {
31    pub schedule: Schedule,
32    #[serde(default = "default_labels")]
33    pub labels: IndexSet<String>,
34    pub milestone: Option<u64>,
35    #[serde(default)]
36    pub assignees: IndexSet<String>,
37    pub target_branch: Option<String>,
38    pub commit_message: Option<CommitMessage>,
39    pub pull_request_branch_name: Option<PullRequestBranchName>,
40}
41
42/// Different registries known to Dependabot.
43#[derive(Deserialize, Debug)]
44#[serde(rename_all = "kebab-case", tag = "type")]
45pub enum Registry {
46    CargoRegistry {
47        url: String,
48        registry: String,
49        token: String,
50    },
51    ComposerRepository {
52        url: String,
53        username: Option<String>,
54        password: Option<String>,
55    },
56    DockerRegistry {
57        url: String,
58        username: Option<String>,
59        password: Option<String>,
60        #[serde(default)]
61        replaces_base: bool,
62    },
63    Git {
64        url: String,
65        username: Option<String>,
66        password: Option<String>,
67    },
68    GoproxyServer {
69        url: String,
70        username: Option<String>,
71        password: Option<String>,
72    },
73    HexOrganization {
74        organization: String,
75        key: Option<String>,
76    },
77    HexRepository {
78        repo: Option<String>,
79        url: String,
80        auth_key: Option<String>,
81        public_key_fingerprint: Option<String>,
82    },
83    MavenRepository {
84        url: String,
85        username: Option<String>,
86        password: Option<String>,
87    },
88    NpmRegistry {
89        url: String,
90        username: Option<String>,
91        password: Option<String>,
92        #[serde(default)]
93        replaces_base: bool,
94    },
95    NugetFeed {
96        url: String,
97        username: Option<String>,
98        password: Option<String>,
99    },
100    PythonIndex {
101        url: String,
102        username: Option<String>,
103        password: Option<String>,
104        #[serde(default)]
105        replaces_base: bool,
106    },
107    RubygemsServer {
108        url: String,
109        username: Option<String>,
110        password: Option<String>,
111        #[serde(default)]
112        replaces_base: bool,
113    },
114    TerraformRegistry {
115        url: String,
116        token: Option<String>,
117    },
118}
119
120/// Cooldown settings for Dependabot updates.
121#[derive(Deserialize, Debug)]
122#[serde(rename_all = "kebab-case")]
123pub struct Cooldown {
124    pub default_days: Option<u64>,
125    pub semver_major_days: Option<u64>,
126    pub semver_minor_days: Option<u64>,
127    pub semver_patch_days: Option<u64>,
128    #[serde(default)]
129    pub include: Vec<String>,
130    #[serde(default)]
131    pub exclude: Vec<String>,
132}
133
134/// A `directory` or `directories` field in a Dependabot `update` directive.
135#[derive(Deserialize, Debug, PartialEq)]
136#[serde(rename_all = "kebab-case")]
137pub enum Directories {
138    Directory(String),
139    Directories(Vec<String>),
140}
141
142/// A single `update` directive.
143#[derive(Deserialize, Debug)]
144#[serde(rename_all = "kebab-case", remote = "Self")]
145pub struct Update {
146    /// Dependency allow rules for this update directive.
147    #[serde(default)]
148    pub allow: Vec<Allow>,
149
150    /// People to assign to this update's pull requests.
151    #[serde(default)]
152    pub assignees: IndexSet<String>,
153
154    /// Commit message settings for this update's pull requests.
155    pub commit_message: Option<CommitMessage>,
156
157    /// Cooldown settings for this update directive.
158    pub cooldown: Option<Cooldown>,
159
160    /// The directory or directories in which to look for manifests
161    /// and dependencies.
162    #[serde(flatten)]
163    pub directories: Directories,
164
165    /// Group settings for batched updates.
166    #[serde(default)]
167    pub groups: IndexMap<String, Group>,
168
169    /// Dependency ignore settings for this update directive.
170    #[serde(default)]
171    pub ignore: Vec<Ignore>,
172
173    /// Whether to allow insecure external code execution during updates.
174    #[serde(default)]
175    pub insecure_external_code_execution: AllowDeny,
176
177    /// Labels to apply to this update group's pull requests.
178    ///
179    /// The default label is `dependencies`.
180    #[serde(default = "default_labels")]
181    pub labels: IndexSet<String>,
182    pub milestone: Option<u64>,
183    /// The maximum number of pull requests to open at a time from this
184    /// update group.
185    ///
186    /// The default maximum is 5.
187    #[serde(default = "default_open_pull_requests_limit")]
188    pub open_pull_requests_limit: u64,
189
190    /// The packaging ecosystem to update.
191    pub package_ecosystem: PackageEcosystem,
192
193    /// The strategy to use when rebasing pull requests.
194    #[serde(default)]
195    pub rebase_strategy: RebaseStrategy,
196    #[serde(default, deserialize_with = "crate::common::scalar_or_vector")]
197    pub registries: Vec<String>,
198    #[serde(default)]
199    pub reviewers: IndexSet<String>,
200    pub schedule: Option<Schedule>,
201    pub target_branch: Option<String>,
202    pub pull_request_branch_name: Option<PullRequestBranchName>,
203    #[serde(default)]
204    pub vendor: bool,
205    pub versioning_strategy: Option<VersioningStrategy>,
206
207    /// If assign, this update directive is assigned to the
208    /// named multi-ecosystem group.
209    ///
210    /// See: <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#multi-ecosystem-group>
211    pub multi_ecosystem_group: Option<String>,
212
213    /// Required if `multi-ecosystem-group` is set.
214    /// A list of glob patterns that determine which dependencies
215    /// are assigned to this group.
216    ///
217    /// See: <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/configuring-multi-ecosystem-updates#2-assign-ecosystems-to-groups-with-patterns>
218    pub patterns: Option<IndexSet<String>>,
219
220    /// Paths that Dependabot will ignore when scanning for manifests
221    /// and dependencies.
222    ///
223    /// See: <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#exclude-paths->
224    #[serde(default)]
225    pub exclude_paths: Option<IndexSet<String>>,
226}
227
228impl<'de> Deserialize<'de> for Update {
229    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
230    where
231        D: serde::Deserializer<'de>,
232    {
233        let update = Self::deserialize(deserializer)?;
234
235        // https://docs.github.com/en/code-security/dependabot/working-with-dependabot/configuring-multi-ecosystem-updates#2-assign-ecosystems-to-groups-with-patterns
236        if update.multi_ecosystem_group.is_some() && update.patterns.is_none() {
237            return Err(custom_error::<D>(
238                "`patterns` must be set when `multi-ecosystem-group` is set",
239            ));
240        }
241
242        // If an update uses `multi-ecosystem-group`, it must
243        // not specify its own `milestone`, `target-branch`, `commit-message`,
244        // or `pull-request-branch-name`.
245        if update.multi_ecosystem_group.is_some() {
246            if update.milestone.is_some() {
247                return Err(custom_error::<D>(
248                    "`milestone` may not be set when `multi-ecosystem-group` is set",
249                ));
250            }
251            if update.target_branch.is_some() {
252                return Err(custom_error::<D>(
253                    "`target-branch` may not be set when `multi-ecosystem-group` is set",
254                ));
255            }
256            if update.commit_message.is_some() {
257                return Err(custom_error::<D>(
258                    "`commit-message` may not be set when `multi-ecosystem-group` is set",
259                ));
260            }
261            if update.pull_request_branch_name.is_some() {
262                return Err(custom_error::<D>(
263                    "`pull-request-branch-name` may not be set when `multi-ecosystem-group` is set",
264                ));
265            }
266        }
267
268        Ok(update)
269    }
270}
271
272#[inline]
273fn default_labels() -> IndexSet<String> {
274    IndexSet::from(["dependencies".to_string()])
275}
276
277#[inline]
278fn default_open_pull_requests_limit() -> u64 {
279    // https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#open-pull-requests-limit
280    5
281}
282
283/// Allow rules for Dependabot updates.
284#[derive(Deserialize, Debug)]
285#[serde(rename_all = "kebab-case")]
286pub struct Allow {
287    pub dependency_name: Option<String>,
288    pub dependency_type: Option<DependencyType>,
289}
290
291/// Dependency types in `allow` rules.
292#[derive(Deserialize, Debug)]
293#[serde(rename_all = "kebab-case")]
294pub enum DependencyType {
295    Direct,
296    Indirect,
297    All,
298    Production,
299    Development,
300}
301
302/// Commit message settings for Dependabot updates.
303#[derive(Deserialize, Debug)]
304#[serde(rename_all = "kebab-case")]
305pub struct CommitMessage {
306    pub prefix: Option<String>,
307    pub prefix_development: Option<String>,
308    /// Invariant: `"scope"`
309    pub include: Option<String>,
310}
311
312/// Group settings for batched updates.
313#[derive(Deserialize, Debug)]
314#[serde(rename_all = "kebab-case")]
315pub struct Group {
316    /// This can only be [`DependencyType::Development`] or
317    /// [`DependencyType::Production`].
318    pub dependency_type: Option<DependencyType>,
319    #[serde(default)]
320    pub patterns: IndexSet<String>,
321    #[serde(default)]
322    pub exclude_patterns: IndexSet<String>,
323    #[serde(default)]
324    pub update_types: IndexSet<UpdateType>,
325}
326
327/// Update types for grouping.
328#[derive(Deserialize, Debug, Hash, Eq, PartialEq)]
329#[serde(rename_all = "kebab-case")]
330pub enum UpdateType {
331    Major,
332    Minor,
333    Patch,
334}
335
336/// Dependency ignore settings for updates.
337#[derive(Deserialize, Debug)]
338#[serde(rename_all = "kebab-case")]
339pub struct Ignore {
340    pub dependency_name: Option<String>,
341    /// These are, inexplicably, not [`UpdateType`] variants.
342    /// Instead, they're strings like `"version-update:semver-{major,minor,patch}"`.
343    #[serde(default)]
344    pub update_types: IndexSet<String>,
345    #[serde(default)]
346    pub versions: IndexSet<String>,
347}
348
349/// An "allow"/"deny" toggle.
350#[derive(Deserialize, Debug, Default)]
351#[serde(rename_all = "kebab-case")]
352pub enum AllowDeny {
353    Allow,
354    #[default]
355    Deny,
356}
357
358/// Supported packaging ecosystems.
359#[derive(Deserialize, Debug, PartialEq)]
360#[serde(rename_all = "kebab-case")]
361pub enum PackageEcosystem {
362    /// `bazel`
363    Bazel,
364    /// `bun`
365    Bun,
366    /// `bundler`
367    Bundler,
368    /// `cargo`
369    Cargo,
370    /// `composer`
371    Composer,
372    /// `conda`
373    Conda,
374    /// `deno`
375    Deno,
376    /// `devcontainers`
377    Devcontainers,
378    /// `docker`
379    Docker,
380    /// `docker-compose`
381    DockerCompose,
382    /// `dotnet-sdk`
383    DotnetSdk,
384    /// `helm`
385    Helm,
386    /// `julia`
387    Julia,
388    /// `elm`
389    Elm,
390    /// `gitsubmodule`
391    Gitsubmodule,
392    /// `github-actions`
393    GithubActions,
394    /// `gomod`
395    Gomod,
396    /// `gradle`
397    Gradle,
398    /// `maven`
399    Maven,
400    /// `mix`
401    Mix,
402    /// `nix`
403    Nix,
404    /// `npm`
405    Npm,
406    /// `nuget`
407    Nuget,
408    /// `opentofu`
409    Opentofu,
410    /// `pip`
411    Pip,
412    /// `pre-commit`
413    PreCommit,
414    /// `pub`
415    Pub,
416    /// `rust-toolchain`
417    RustToolchain,
418    /// `sbt`
419    Sbt,
420    /// `swift`
421    Swift,
422    /// `terraform`
423    Terraform,
424    /// `uv`
425    Uv,
426    /// `vcpkg`
427    Vcpkg,
428}
429
430/// Rebase strategies for Dependabot updates.
431#[derive(Deserialize, Debug, Default, PartialEq)]
432#[serde(rename_all = "kebab-case")]
433pub enum RebaseStrategy {
434    #[default]
435    Auto,
436    Disabled,
437}
438
439/// Scheduling settings for Dependabot updates.
440#[derive(Deserialize, Debug)]
441#[serde(rename_all = "kebab-case", remote = "Self")]
442pub struct Schedule {
443    pub interval: Interval,
444    pub day: Option<Day>,
445    pub time: Option<String>,
446    pub timezone: Option<String>,
447    pub cronjob: Option<String>,
448}
449
450impl<'de> Deserialize<'de> for Schedule {
451    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
452    where
453        D: serde::Deserializer<'de>,
454    {
455        let schedule = Self::deserialize(deserializer)?;
456
457        if schedule.interval == Interval::Cron && schedule.cronjob.is_none() {
458            return Err(custom_error::<D>(
459                "`schedule.cronjob` must be set when `schedule.interval` is `cron`",
460            ));
461        }
462
463        if schedule.interval != Interval::Cron && schedule.cronjob.is_some() {
464            return Err(custom_error::<D>(
465                "`schedule.cronjob` may only be set when `schedule.interval` is `cron`",
466            ));
467        }
468
469        // NOTE(ww): `day` only makes sense with `interval: weekly`, but
470        // Dependabot appears to silently ignore it otherwise. Consequently,
471        // we don't check that for now.
472        // See https://github.com/zizmorcore/zizmor/issues/1305.
473
474        Ok(schedule)
475    }
476}
477
478/// Schedule intervals.
479#[derive(Deserialize, Debug, PartialEq)]
480#[serde(rename_all = "kebab-case")]
481pub enum Interval {
482    Daily,
483    Weekly,
484    Monthly,
485    Quarterly,
486    Semiannually,
487    Yearly,
488    Cron,
489}
490
491/// Days of the week.
492#[derive(Deserialize, Debug, PartialEq)]
493#[serde(rename_all = "kebab-case")]
494pub enum Day {
495    Monday,
496    Tuesday,
497    Wednesday,
498    Thursday,
499    Friday,
500    Saturday,
501    Sunday,
502}
503
504/// Pull request branch name settings.
505#[derive(Deserialize, Debug)]
506#[serde(rename_all = "kebab-case")]
507pub struct PullRequestBranchName {
508    pub separator: Option<String>,
509}
510
511/// Versioning strategies.
512#[derive(Deserialize, Debug, PartialEq)]
513#[serde(rename_all = "kebab-case")]
514pub enum VersioningStrategy {
515    Auto,
516    Increase,
517    IncreaseIfNecessary,
518    LockfileOnly,
519    Widen,
520}