1616 composite action under :file:`.github`, since YAML drops the comment.
1717* :class:`TestDependabotUpdatesTheActions` checks :file:`.github/dependabot.yml`
1818 watches the ``github-actions`` ecosystem from the repository root.
19+ * :class:`TestDependabotGroupsTheCodeQLSteps` checks that entry groups the
20+ ``github/codeql-action`` steps, which refuse to run at mixed versions
21+ (#2384-#2386), and groups nothing else.
1922* :class:`TestDependabotCoversEveryManifest` checks the owner's ruling on #1968:
2023 ``pip`` over the root, :file:`docs` and :file:`examples/benchmark`, and
2124 neither :file:`conda` nor a ``docker`` entry, as Dependabot cannot update
6265PYUP = ROOT / '.pyup.yml'
6366#: The directories the ``pip`` entry watches, in the owner's ruling on #1968.
6467PIP_DIRECTORIES = ['/' , '/docs' , '/examples/benchmark' ]
68+ #: The ecosystem whose dependencies are the ``uses:`` references.
69+ ACTIONS = 'github-actions'
70+ #: The group holding the CodeQL Action's steps (#2384-#2386), under that ecosystem.
71+ CODEQL_GROUP = 'codeql-action'
72+ #: The action whose steps that group holds, without the step's sub-path.
73+ CODEQL = 'github/codeql-action'
6574
6675
6776def updates () -> 'dict[str, dict[str, Any]]' :
@@ -83,6 +92,45 @@ def files() -> 'list[pathlib.Path]':
8392 return found
8493
8594
95+ def groups (ecosystem : 'str' ) -> 'dict[str, Any]' :
96+ """The ``groups`` an entry declares, by group name, empty where it declares none."""
97+ return updates ()[ecosystem ].get ('groups' ) or {}
98+
99+
100+ def references () -> 'set[str]' :
101+ """Every non-local action a ``uses:`` under :file:`.github` names, without its commit.
102+
103+ These are the dependency names Dependabot's ``github-actions`` ecosystem
104+ matches a group's ``patterns`` against, the step's sub-path included:
105+ ``github/codeql-action/init`` rather than ``github/codeql-action``.
106+
107+ """
108+ found = set ()
109+ for path in files ():
110+ for match in USES .finditer (path .read_text (encoding = 'utf-8' )):
111+ ref = match ['ref' ].strip ('\' "' )
112+ if not ref .startswith (('./' , 'docker://' )):
113+ found .add (ref .partition ('@' )[0 ])
114+ return found
115+
116+
117+ def grouped (name : 'str' , patterns : 'list[str]' ) -> 'list[str]' :
118+ """Which of ``patterns`` claim the dependency ``name``.
119+
120+ dependabot-core matches a group's pattern with ``WildcardMatcher.match?``,
121+ which folds case on both sides and turns each ``*`` into ``.*``, treating
122+ every other character -- ``?`` and ``[...]`` included -- as literal:
123+ ``github/codeql-action/*`` claims ``github/codeql-action/init`` and a bare
124+ ``*`` claims every action in the tree, but ``github/codeql-action/[a-z]*``
125+ claims nothing, since dependabot-core reads the brackets as a literal
126+ substring rather than a character class.
127+
128+ """
129+ return [pattern for pattern in patterns
130+ if re .fullmatch ('.*' .join (re .escape (part ) for part in pattern .lower ().split ('*' )),
131+ name .lower (), re .S )]
132+
133+
86134def unpinned (text : 'str' ) -> 'list[str]' :
87135 """Each non-local ``uses:`` line of ``text`` not pinned to a commit with a version comment."""
88136 bad = []
@@ -158,6 +206,64 @@ def test_it_reads_the_workflows_from_the_root(self) -> None:
158206 self .assertRegex (entry .group (0 ), r'(?m)^\s*interval:\s*"\w+"' ) # type: ignore[union-attr]
159207
160208
209+ class TestDependabotGroupsTheCodeQLSteps (unittest .TestCase ):
210+ """The ``github/codeql-action`` steps bump in one pull request (#2384-#2386).
211+
212+ ``Initialize CodeQL`` persists a configuration tagged with the version that
213+ wrote it, and a later step of the action refuses to read one a different
214+ version wrote: ``Loaded a configuration file for version '4.39.0', but
215+ running version '4.39.1'.`` So an ungrouped bump of a single step leaves the
216+ ``Analyze`` job red whichever step moved -- measured on #2384 and #2386,
217+ where ``Autobuild`` refused and ``Perform CodeQL Analysis`` never ran, and on
218+ #2385, where ``Perform CodeQL Analysis`` refused. The error says so itself:
219+ *If you are using Dependabot to manage dependency updates, you can configure
220+ a dependency group to update all* ``github/codeql-action`` *steps at the same
221+ time.*
222+
223+ The group has to sit under ``github-actions``, since no other ecosystem sees
224+ an action, and its patterns have to claim every step the workflows use and no
225+ other action: a wider pattern batches unrelated bumps into the same pull
226+ request, which is what reviewing one action at a time avoids.
227+
228+ """
229+
230+ def test_the_actions_entry_declares_the_group (self ) -> None :
231+ self .assertIn (CODEQL_GROUP , groups (ACTIONS ))
232+
233+ def test_no_other_ecosystem_declares_it (self ) -> None :
234+ """``pip`` reads a manifest, never a ``uses:``, so a group there groups nothing."""
235+ for ecosystem in updates ():
236+ if ecosystem != ACTIONS :
237+ with self .subTest (ecosystem = ecosystem ):
238+ self .assertNotIn (CODEQL_GROUP , groups (ecosystem ))
239+
240+ def test_the_patterns_claim_every_step_the_workflows_use (self ) -> None :
241+ """A pattern that has gone stale, by a typo or by a step added beside it."""
242+ patterns = groups (ACTIONS )[CODEQL_GROUP ]['patterns' ]
243+ steps = sorted (name for name in references () if name .startswith (f'{ CODEQL } /' ))
244+ self .assertTrue (steps , f'no workflow uses { CODEQL } , so the group holds nothing' )
245+ for name in steps :
246+ with self .subTest (action = name ):
247+ self .assertTrue (grouped (name , patterns ))
248+
249+ def test_the_patterns_claim_nothing_else (self ) -> None :
250+ patterns = groups (ACTIONS )[CODEQL_GROUP ]['patterns' ]
251+ for name in sorted (references ()):
252+ if not name .startswith (f'{ CODEQL } /' ):
253+ with self .subTest (action = name ):
254+ self .assertEqual (grouped (name , patterns ), [])
255+
256+ def test_every_step_is_pinned_to_one_release (self ) -> None :
257+ """What the group is there to keep true, read from the pins themselves."""
258+ pins = {}
259+ for path in files ():
260+ for match in _workflows .PINNED .finditer (path .read_text (encoding = 'utf-8' )):
261+ if match ['action' ].startswith (f'{ CODEQL } /' ):
262+ pins [match ['action' ]] = (match ['sha' ], match ['version' ])
263+ self .assertEqual (sorted (pins ), sorted (name for name in references ()
264+ if name .startswith (f'{ CODEQL } /' )))
265+ self .assertEqual (len (set (pins .values ())), 1 , f'the steps disagree on a release: { pins } ' )
266+
161267
162268class TestDependabotCoversEveryManifest (unittest .TestCase ):
163269 """Dependabot watches the docs and benchmark pins, and nothing it cannot update."""
0 commit comments