feat(models): support Dependabot multi-ecosystem groups (#1260)
Some checks are pending
Benchmark baseline / Continuous Benchmarking with Bencher (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Test (push) Waiting to run
CI / Test site build (push) Waiting to run
CI / All tests pass (push) Blocked by required conditions
zizmor wheel builds for PyPI 🐍 / Build source distribution (push) Waiting to run
zizmor wheel builds for PyPI 🐍 / Release (push) Blocked by required conditions
zizmor wheel builds for PyPI 🐍 / Build Linux wheels (manylinux) (push) Waiting to run
zizmor wheel builds for PyPI 🐍 / Build Linux wheels (musllinux) (push) Waiting to run
zizmor wheel builds for PyPI 🐍 / Build Windows wheels (push) Waiting to run
zizmor wheel builds for PyPI 🐍 / Build macOS wheels (push) Waiting to run
Deploy zizmor documentation site 🌐 / Deploy zizmor documentation to GitHub Pages 🌐 (push) Waiting to run
GitHub Actions Security Analysis with zizmor 🌈 / Run zizmor 🌈 (push) Waiting to run

This commit is contained in:
William Woodruff 2025-10-16 23:17:01 -04:00 committed by GitHub
parent e450a597b3
commit 965d9ffccc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 166 additions and 35 deletions

View file

@ -10,6 +10,10 @@ on:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
benchmark_base_branch:
name: Continuous Benchmarking with Bencher

View file

@ -8,6 +8,10 @@ on:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
@ -29,30 +33,30 @@ jobs:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: Test dependencies
run: |
# Don't waste time on man-db updates
sudo apt-get remove --purge man-db
# Needed for tty-tests
sudo apt install -y expect
- name: Test dependencies
run: |
# Don't waste time on man-db updates
sudo apt-get remove --purge man-db
# Needed for tty-tests
sudo apt install -y expect
- name: Test
run: cargo test --features online-tests,tty-tests
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Test
run: cargo test --features online-tests,tty-tests
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Test snippets
run: |
make snippets
git diff --exit-code
- name: Test snippets
run: |
make snippets
git diff --exit-code
test-site:
name: Test site build

2
Cargo.lock generated
View file

@ -907,7 +907,7 @@ dependencies = [
[[package]]
name = "github-actions-models"
version = "0.37.0"
version = "0.38.0"
dependencies = [
"indexmap",
"serde",

View file

@ -20,7 +20,7 @@ rust-version = "1.88.0"
[workspace.dependencies]
anyhow = "1.0.100"
github-actions-expressions = { path = "crates/github-actions-expressions", version = "0.0.10" }
github-actions-models = { path = "crates/github-actions-models", version = "0.37.0" }
github-actions-models = { path = "crates/github-actions-models", version = "0.38.0" }
itertools = "0.14.0"
pest = "2.8.3"
pest_derive = "2.8.3"

View file

@ -1,6 +1,6 @@
[package]
name = "github-actions-models"
version = "0.37.0"
version = "0.38.0"
description = "Unofficial, high-quality data models for GitHub Actions workflows, actions, and related components"
repository = "https://github.com/zizmorcore/zizmor/tree/main/crates/github-actions-models"
keywords = ["github", "ci"]

View file

@ -18,10 +18,27 @@ pub struct Dependabot {
#[serde(default)]
pub enable_beta_ecosystems: bool,
#[serde(default)]
pub multi_ecosystem_groups: IndexMap<String, MultiEcosystemGroup>,
#[serde(default)]
pub registries: IndexMap<String, Registry>,
pub updates: Vec<Update>,
}
/// A multi-ecosystem update group.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct MultiEcosystemGroup {
pub schedule: Schedule,
#[serde(default = "default_labels")]
pub labels: IndexSet<String>,
pub milestone: Option<u64>,
#[serde(default)]
pub assignees: IndexSet<String>,
pub target_branch: Option<String>,
pub commit_message: Option<CommitMessage>,
pub pull_request_branch_name: Option<PullRequestBranchName>,
}
/// Different registries known to Dependabot.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case", tag = "type")]
@ -114,22 +131,39 @@ pub enum Directories {
/// A single `update` directive.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
#[serde(rename_all = "kebab-case", remote = "Self")]
pub struct Update {
/// Dependency allow rules for this update directive.
#[serde(default)]
pub allow: Vec<Allow>,
/// People to assign to this update's pull requests.
#[serde(default)]
pub assignees: IndexSet<String>,
/// Commit message settings for this update's pull requests.
pub commit_message: Option<CommitMessage>,
/// Cooldown settings for this update directive.
pub cooldown: Option<Cooldown>,
/// The directory or directories in which to look for manifests
/// and dependencies.
#[serde(flatten)]
pub directories: Directories,
/// Group settings for batched updates.
#[serde(default)]
pub groups: IndexMap<String, Group>,
/// Dependency ignore settings for this update directive.
#[serde(default)]
pub ignore: Vec<Ignore>,
/// Whether to allow insecure external code execution during updates.
#[serde(default)]
pub insecure_external_code_execution: AllowDeny,
/// Labels to apply to this update group's pull requests.
///
/// The default label is `dependencies`.
@ -142,19 +176,87 @@ pub struct Update {
/// The default maximum is 5.
#[serde(default = "default_open_pull_requests_limit")]
pub open_pull_requests_limit: u64,
/// The packaging ecosystem to update.
pub package_ecosystem: PackageEcosystem,
// TODO: pull-request-branch-name
/// The strategy to use when rebasing pull requests.
#[serde(default)]
pub rebase_strategy: RebaseStrategy,
#[serde(default, deserialize_with = "crate::common::scalar_or_vector")]
pub registries: Vec<String>,
#[serde(default)]
pub reviewers: IndexSet<String>,
pub schedule: Schedule,
pub schedule: Option<Schedule>,
pub target_branch: Option<String>,
pub pull_request_branch_name: Option<PullRequestBranchName>,
#[serde(default)]
pub vendor: bool,
pub versioning_strategy: Option<VersioningStrategy>,
/// If assign, this update directive is assigned to the
/// named multi-ecosystem group.
///
/// See: <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#multi-ecosystem-group>
pub multi_ecosystem_group: Option<String>,
/// Required if `multi-ecosystem-group` is set.
/// A list of glob patterns that determine which dependencies
/// are assigned to this group.
///
/// See: <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/configuring-multi-ecosystem-updates#2-assign-ecosystems-to-groups-with-patterns>
pub patterns: Option<IndexSet<String>>,
/// Paths that Dependabot will ignore when scanning for manifests
/// and dependencies.
///
/// See: <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#exclude-paths->
#[serde(default)]
pub exclude_paths: Option<IndexSet<String>>,
}
impl<'de> Deserialize<'de> for Update {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let update = Self::deserialize(deserializer)?;
// https://docs.github.com/en/code-security/dependabot/working-with-dependabot/configuring-multi-ecosystem-updates#2-assign-ecosystems-to-groups-with-patterns
if update.multi_ecosystem_group.is_some() && update.patterns.is_none() {
return Err(custom_error::<D>(
"`patterns` must be set when `multi-ecosystem-group` is set",
));
}
// If an update uses `multi-ecosystem-group`, it must
// not specify its own `milestone`, `target-branch`, `commit-message`,
// or `pull-request-branch-name`.
if update.multi_ecosystem_group.is_some() {
if update.milestone.is_some() {
return Err(custom_error::<D>(
"`milestone` may not be set when `multi-ecosystem-group` is set",
));
}
if update.target_branch.is_some() {
return Err(custom_error::<D>(
"`target-branch` may not be set when `multi-ecosystem-group` is set",
));
}
if update.commit_message.is_some() {
return Err(custom_error::<D>(
"`commit-message` may not be set when `multi-ecosystem-group` is set",
));
}
if update.pull_request_branch_name.is_some() {
return Err(custom_error::<D>(
"`pull-request-branch-name` may not be set when `multi-ecosystem-group` is set",
));
}
}
Ok(update)
}
}
#[inline]
@ -346,13 +448,7 @@ impl<'de> Deserialize<'de> for Schedule {
));
}
Ok(Self {
interval: schedule.interval,
day: schedule.day,
time: schedule.time,
timezone: schedule.timezone,
cronjob: schedule.cronjob,
})
Ok(schedule)
}
}
@ -382,6 +478,13 @@ pub enum Day {
Sunday,
}
/// Pull request branch name settings.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct PullRequestBranchName {
pub separator: Option<String>,
}
/// Versioning strategies.
#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]

View file

@ -0,0 +1,20 @@
# https://github.com/Homebrew/homebrew-core/blob/6f327cc644648871b1c086e949688c244b5ca917/.github/dependabot.yml
# This file is synced from the `.github` repository, do not modify it directly.
---
version: 2
multi-ecosystem-groups:
all:
schedule:
interval: weekly
day: friday
time: "08:00"
timezone: Etc/UTC
updates:
- package-ecosystem: github-actions
directory: "/"
multi-ecosystem-group: all
patterns:
- "*"
allow:
- dependency-type: all

View file

@ -59,7 +59,7 @@ fn test_contents() {
let pip = &dependabot.updates[0];
assert_eq!(pip.package_ecosystem, PackageEcosystem::Pip);
assert_eq!(pip.directories, Directories::Directory("/".into()));
assert_eq!(pip.schedule.interval, Interval::Daily);
assert_eq!(pip.schedule.as_ref().unwrap().interval, Interval::Daily);
assert_eq!(pip.open_pull_requests_limit, 5); // default
let github_actions = &dependabot.updates[1];
@ -129,6 +129,6 @@ fn test_schedule_weekly_accepts_day() {
let dependabot = load_dependabot("weekly-with-day.yml");
assert_eq!(dependabot.updates.len(), 1);
let schedule = &dependabot.updates[0].schedule;
assert_eq!(schedule.interval, Interval::Weekly);
assert_eq!(schedule.day, Some(Day::Friday));
assert_eq!(schedule.as_ref().unwrap().interval, Interval::Weekly);
assert_eq!(schedule.as_ref().unwrap().day, Some(Day::Friday));
}