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