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    /// `pip`
391    Pip,
392    /// `pub`
393    Pub,
394    /// `rust-toolchain`
395    RustToolchain,
396    /// `swift`
397    Swift,
398    /// `terraform`
399    Terraform,
400    /// `uv`
401    Uv,
402    /// `vcpkg`
403    Vcpkg,
404}
405
406/// Rebase strategies for Dependabot updates.
407#[derive(Deserialize, Debug, Default, PartialEq)]
408#[serde(rename_all = "kebab-case")]
409pub enum RebaseStrategy {
410    #[default]
411    Auto,
412    Disabled,
413}
414
415/// Scheduling settings for Dependabot updates.
416#[derive(Deserialize, Debug)]
417#[serde(rename_all = "kebab-case", remote = "Self")]
418pub struct Schedule {
419    pub interval: Interval,
420    pub day: Option<Day>,
421    pub time: Option<String>,
422    pub timezone: Option<String>,
423    pub cronjob: Option<String>,
424}
425
426impl<'de> Deserialize<'de> for Schedule {
427    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
428    where
429        D: serde::Deserializer<'de>,
430    {
431        let schedule = Self::deserialize(deserializer)?;
432
433        if schedule.interval == Interval::Cron && schedule.cronjob.is_none() {
434            return Err(custom_error::<D>(
435                "`schedule.cronjob` must be set when `schedule.interval` is `cron`",
436            ));
437        }
438
439        if schedule.interval != Interval::Cron && schedule.cronjob.is_some() {
440            return Err(custom_error::<D>(
441                "`schedule.cronjob` may only be set when `schedule.interval` is `cron`",
442            ));
443        }
444
445        // NOTE(ww): `day` only makes sense with `interval: weekly`, but
446        // Dependabot appears to silently ignore it otherwise. Consequently,
447        // we don't check that for now.
448        // See https://github.com/zizmorcore/zizmor/issues/1305.
449
450        Ok(schedule)
451    }
452}
453
454/// Schedule intervals.
455#[derive(Deserialize, Debug, PartialEq)]
456#[serde(rename_all = "kebab-case")]
457pub enum Interval {
458    Daily,
459    Weekly,
460    Monthly,
461    Quarterly,
462    Semiannually,
463    Yearly,
464    Cron,
465}
466
467/// Days of the week.
468#[derive(Deserialize, Debug, PartialEq)]
469#[serde(rename_all = "kebab-case")]
470pub enum Day {
471    Monday,
472    Tuesday,
473    Wednesday,
474    Thursday,
475    Friday,
476    Saturday,
477    Sunday,
478}
479
480/// Pull request branch name settings.
481#[derive(Deserialize, Debug)]
482#[serde(rename_all = "kebab-case")]
483pub struct PullRequestBranchName {
484    pub separator: Option<String>,
485}
486
487/// Versioning strategies.
488#[derive(Deserialize, Debug, PartialEq)]
489#[serde(rename_all = "kebab-case")]
490pub enum VersioningStrategy {
491    Auto,
492    Increase,
493    IncreaseIfNecessary,
494    LockfileOnly,
495    Widen,
496}