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