This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 891e83dce5d Tell contributors when their fork CI workflow stops
running (#75814)
891e83dce5d is described below
commit 891e83dce5dc989e996d0057417bb390537bff4f
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue Oct 27 21:04:47 2026 +0100
Tell contributors when their fork CI workflow stops running (#75814)
The fork CI monitor read the fork's workflow state only until it first saw
the workflow enabled. When the workflow was disabled afterwards, by the
contributor or by GitHub after 60 days of fork inactivity, the pull request
kept waiting for a push that could never run CI, while its comment still
said the workflow was enabled. Contributors now get a new comment telling
them why it stopped and how to turn it back on, starting with syncing their
fork's main from apache/airflow.
breeze ci audit also stops suggesting committers disable the workflow: a
committer trying fork CI needs it, and following the hint is one way a pull
request ends up stuck like this.
Generated-by: Claude Opus 5
---
contributing-docs/18_contribution_workflow.rst | 10 +--
dev/breeze/doc/08_ci_tasks.rst | 4 +-
.../doc/ci/08_fork_ci_for_external_contributors.md | 26 ++++----
dev/breeze/src/airflow_breeze/utils/ci_audit.py | 12 ----
.../src/airflow_breeze/utils/fork_ci_messages.py | 26 ++++++--
.../src/airflow_breeze/utils/fork_ci_reconcile.py | 29 +++++----
dev/breeze/tests/test_ci_audit.py | 4 +-
dev/breeze/tests/test_fork_ci_messages.py | 6 ++
dev/breeze/tests/test_fork_ci_reconcile.py | 74 +++++++++++++++++++++-
9 files changed, 141 insertions(+), 50 deletions(-)
diff --git a/contributing-docs/18_contribution_workflow.rst
b/contributing-docs/18_contribution_workflow.rst
index d864040f24c..7ebe12fb120 100644
--- a/contributing-docs/18_contribution_workflow.rst
+++ b/contributing-docs/18_contribution_workflow.rst
@@ -147,15 +147,17 @@ Set up your checkout and fork once:
2. If fork CI applies to you, enable the ``Tests (AMD)`` workflow in your
fork: today that means
you joined the pilot or a maintainer labelled your pull request ``use fork
ci``; once fork CI is
switched on for everyone, it means you are not a committer, a collaborator
or a member of an
- exempted organisation. GitHub disables workflows in new forks. Otherwise
keep it disabled, so
- your pushes do not run CI twice:
+ exempted organisation. GitHub disables workflows in new forks. Sync your
fork's ``main`` with
+ ``apache/airflow`` first, so it has the current version of the workflow:
.. code-block:: bash
+ gh repo sync <you>/airflow --branch main
gh workflow enable ci-amd.yml -R <you>/airflow
- If the workflow is missing, your fork's ``main`` is older than it; sync it
first with
- ``gh repo sync <you>/airflow --branch main``.
+ Keep the workflow enabled for as long as you have open pull requests. If it
gets disabled, from
+ your fork's Actions tab or by GitHub after 60 days without activity in your
fork, the fork CI
+ monitor comments on your pull requests with the commands to turn it back on.
3. Check everything at once. ``breeze ci audit`` prints one row per check and
the command that fixes
anything missing; ``--fix`` runs those commands after asking:
diff --git a/dev/breeze/doc/08_ci_tasks.rst b/dev/breeze/doc/08_ci_tasks.rst
index 5e1b9311f32..7147b615096 100644
--- a/dev/breeze/doc/08_ci_tasks.rst
+++ b/dev/breeze/doc/08_ci_tasks.rst
@@ -168,8 +168,8 @@ Pull requests from external contributors can run their CI
in the contributor's f
``breeze ci audit`` checks, read-only, whether your checkout and fork are set
up for that: ``gh``
installed and authenticated, the ``upstream`` remote pointing at
``apache/airflow`` and ``origin`` at your
fork, whether you are a committer, collaborator, member of an exempted
organisation, pilot participant or
-external contributor, whether the ``Tests (AMD)`` workflow in your fork is
enabled (or disabled, for those
-whose pull requests run project CI), how far your fork's ``main`` is behind,
and the branch you are on.
+external contributor, whether the ``Tests (AMD)`` workflow in your fork is
enabled (when your pull requests
+use fork CI), how far your fork's ``main`` is behind, and the branch you are
on.
Each failing check prints the command that fixes it; ``--fix`` runs those
commands after asking.
These are all available flags of ``audit`` command:
diff --git a/dev/breeze/doc/ci/08_fork_ci_for_external_contributors.md
b/dev/breeze/doc/ci/08_fork_ci_for_external_contributors.md
index ab33c09aec3..ad06dbc54c5 100644
--- a/dev/breeze/doc/ci/08_fork_ci_for_external_contributors.md
+++ b/dev/breeze/doc/ci/08_fork_ci_for_external_contributors.md
@@ -490,16 +490,19 @@ that matters. It is matched by file, not by the
workflow's display name, which a
change, and only `push` runs count because only they execute the workflow from
the head commit
itself; a `pull_request` run inside the fork, into the fork's own `main`, runs
a merge the
contributor controls. For pull requests
-whose recorded state is `setup_required` (or that have no recorded state yet),
one REST call
-per pull request reads `GET /repos/<fork>/actions/workflows/ci-amd.yml` to
distinguish
-"workflows disabled" from "enabled but not yet pushed". This is bounded by the
number of
-pull requests in that state, not by the total.
+with no fork run for the head commit, one REST call per pull request reads
+`GET /repos/<fork>/actions/workflows/ci-amd.yml` to distinguish "workflows
disabled" from
+"enabled but not yet pushed". It is read on every run, including for pull
requests already
+recorded as `awaiting_push`: a workflow can be disabled after it was enabled,
by the
+contributor or by GitHub after 60 days without activity in the fork, and the
pull request
+would otherwise keep waiting for a push that never runs CI. This is bounded by
the number of
+pull requests without a fork run for their head commit, not by the total.
### State machine
| Observed fork state | Draft | `ready for maintainer review` | Commit status
| Comment |
|---|---|---|---|---|
-| **setup_required**: workflow state is `disabled_fork`, or the fork has no
`ci-amd.yml` | draft | remove | `error` (red), link to the fork's Actions page
for `ci-amd.yml` | create or update: how to enable, with the exact `gh workflow
enable ci-amd.yml -R <fork>` line, and `breeze ci audit` |
+| **setup_required**: workflow state is `disabled_fork`, `disabled_manually`
or `disabled_inactivity`, or the fork has no `ci-amd.yml` | draft | remove |
`error` (red), link to the fork's Actions page for `ci-amd.yml` | create or
update: how to enable, with the exact `gh repo sync <fork> --branch main` and
`gh workflow enable ci-amd.yml -R <fork>` lines, and `breeze ci audit`; on
transition from any other state but `closed` (the workflow stopped running
after it worked): also post a **ne [...]
| **awaiting_push**: enabled, no run for the head SHA | draft | remove |
`pending` (yellow), link to the fork's Actions page | on transition from
setup_required: post a **new** comment asking to push again (an edit would not
notify) |
| **running**: run in progress | draft | remove | `pending` (yellow), link to
the run | update state and run link |
| **failed**: conclusion is failure, cancelled or timed out | draft | remove |
`failure` (red), link to the run | update with the run link |
@@ -872,7 +875,7 @@ Unknown means any of:
that outlasted its retries.
- A check suite for `Tests (AMD)`, in the fork or in `apache/airflow`, with a
status or
conclusion outside the modelled set.
-- A workflow state outside `active` / `disabled_fork` / `disabled_manually` /
not found.
+- A workflow state outside `active` / `disabled_fork` / `disabled_manually` /
`disabled_inactivity` / not found.
- A mutation that returned an error (label, comment, close, status).
- A head repository that no longer exists.
- A state comment whose marker cannot be parsed.
@@ -949,18 +952,17 @@ prints one row: status, finding, and the command that
fixes it.
| 2 | Remote naming | a remote named `upstream` points at `apache/airflow` and
`origin` does not | `git remote rename`/`add` per
`contributing-docs/10_working_with_git.rst` |
| 3 | Fork exists | `origin` is a fork of `apache/airflow` owned by the
authenticated user | `gh repo fork apache/airflow --remote=false` then add
`origin`, unless check 2's fix already adds or repoints it |
| 4 | Who you are | classifies the user in the gate's order: pilot participant
(`[pilot]`, which gates whatever the role), committer (public `apache`
membership, `GET /orgs/apache/public_members/<login>`, since the gate cannot
see private membership), collaborator (`.asf.yaml`), exempted (`[allowlist]`),
external; an external user whose `apache` membership is private is told to make
it public or ask for `[allowlist]`; when the membership call fails for any
reason but a 404, the role is r [...]
-| 5 | Fork workflows | external or pilot participant: `ci-amd.yml` state on
the fork is `active`; committer/collaborator/exempted: informational, with a
hint when it is `active` | `gh workflow enable ci-amd.yml -R <fork>`, or `gh
workflow disable ...` shown but not run by `--fix` |
+| 5 | Fork workflows | external or pilot participant: `ci-amd.yml` state on
the fork is `active`; committer/collaborator/exempted: always OK, whatever the
state | `gh workflow enable ci-amd.yml -R <fork>` |
| 6 | Fork `main` freshness | fork `main` is not behind `upstream/main` by
more than 200 commits (`MAX_FORK_MAIN_BEHIND`) | `gh repo sync <fork> --branch
main` |
| 7 | Current branch | not `main`, has an upstream tracking `origin` |
informational |
The role is determined before checks 2 and 3, though printed as check 4. For a
committer,
collaborator or exempted contributor, whose pull requests run project CI,
checks 2 and 3 are
informational: working from an `apache/airflow` clone without a fork is fine
for them, and
-`--fix` only runs the fixes of failed checks. The same goes for check 5: an
enabled fork
-workflow runs every push twice and attaches a second status, so they are told
they can disable
-it, but it is not a failure, because a committer trying fork CI with the `use
fork ci` label
-needs it enabled. Forks of a public repository cannot be made private on
GitHub, so disabling
-the workflow is the only recommendation available.
+`--fix` only runs the fixes of failed checks. Check 5 passes for them whether
the fork workflow
+is enabled or not. The audit never suggests disabling it: a committer trying
fork CI with the
+`use fork ci` label needs it enabled, and a workflow disabled while a pull
request waits for its
+fork CI leaves that pull request stuck until the workflow is enabled again.
A failed API call is reported as "could not check" rather than as a finding: a
failed fork
lookup is not "not a fork of apache/airflow", and a failed workflow lookup is
not "missing".
diff --git a/dev/breeze/src/airflow_breeze/utils/ci_audit.py
b/dev/breeze/src/airflow_breeze/utils/ci_audit.py
index 3b27ef8eb15..94b0cf7ce9d 100644
--- a/dev/breeze/src/airflow_breeze/utils/ci_audit.py
+++ b/dev/breeze/src/airflow_breeze/utils/ci_audit.py
@@ -327,18 +327,6 @@ def run_audit(*, run: Runner, config: GateConfig,
collaborators: frozenset[str])
enable,
)
)
- elif required is False and state == "active":
- # Not a failure: a committer trying fork CI with the `use fork ci`
label needs it on.
- results.append(
- CheckResult(
- 5,
- "fork workflows",
- CheckStatus.INFO,
- f"`Tests (AMD)` is enabled in {fork}, but your pull
requests run project CI, so each "
- "push runs CI twice; disable it unless you use the `use
fork ci` label",
- (f"gh workflow disable {CI_WORKFLOW_FILE} -R {fork}",),
- )
- )
elif required is None:
results.append(
CheckResult(
diff --git a/dev/breeze/src/airflow_breeze/utils/fork_ci_messages.py
b/dev/breeze/src/airflow_breeze/utils/fork_ci_messages.py
index 48208ae90a1..90f6b6cba51 100644
--- a/dev/breeze/src/airflow_breeze/utils/fork_ci_messages.py
+++ b/dev/breeze/src/airflow_breeze/utils/fork_ci_messages.py
@@ -116,11 +116,12 @@ def fork_actions_url(fork: str) -> str:
def _setup_instructions(fork: str) -> str:
return (
"To get CI for this pull request:\n\n"
- "1. Enable the workflow in your fork (once per fork):\n\n"
+ "1. Sync your fork's `main` with apache/airflow, so that it has the
current version of the "
+ "workflow:\n\n"
+ f" ```bash\n gh repo sync {fork} --branch main\n ```\n\n"
+ "2. Enable the workflow in your fork:\n\n"
f" ```bash\n gh workflow enable ci-amd.yml -R {fork}\n ```\n\n"
- " If your fork's `main` is older than the workflow, sync it first
with "
- f"`gh repo sync {fork} --branch main`.\n"
- "2. Push your branch again. Every push then runs CI in your fork.\n\n"
+ "3. Push your branch again. Every push then runs CI in your fork.\n\n"
"`breeze ci audit` checks your whole local and fork setup and prints
the command that fixes "
f"anything missing. See [the contribution
workflow]({CONTRIBUTING_URL})."
)
@@ -195,6 +196,23 @@ def nudge_comment(*, fork: str) -> str:
)
+_DISABLED_REASONS = {
+ "disabled_manually": "was disabled in your fork, from its Actions tab or
with `gh workflow disable`",
+ "disabled_inactivity": "was disabled by GitHub because your fork had no
activity for 60 days",
+ "disabled_fork": "is disabled in your fork",
+ "missing": "is missing from your fork, for example because its `main` was
reset to an older commit",
+}
+
+
+def workflow_disabled_comment(*, fork: str, workflow_state: str | None) -> str:
+ reason = _DISABLED_REASONS.get(workflow_state or "missing", "is not
enabled in your fork")
+ return (
+ f"**Fork CI stopped running.** The `Tests (AMD)` workflow {reason}, so
pushes to this "
+ "branch no longer run CI, and this pull request stays a draft until
they do.\n\n"
+ f"{_setup_instructions(fork)}" + FOOTER
+ )
+
+
def inactivity_close_comment(*, days: int, fork: str) -> str:
return (
f"Closing this pull request because CI has not run in your fork for
{days} days. {WHY_FORK_CI}\n\n"
diff --git a/dev/breeze/src/airflow_breeze/utils/fork_ci_reconcile.py
b/dev/breeze/src/airflow_breeze/utils/fork_ci_reconcile.py
index 6b54f6c7fac..cd9edf5a44c 100644
--- a/dev/breeze/src/airflow_breeze/utils/fork_ci_reconcile.py
+++ b/dev/breeze/src/airflow_breeze/utils/fork_ci_reconcile.py
@@ -73,6 +73,7 @@ from airflow_breeze.utils.fork_ci_messages import (
status_description,
tamper_close_comment,
ungated_comment,
+ workflow_disabled_comment,
)
CI_WORKFLOW_FILE = "ci-amd.yml"
@@ -144,7 +145,7 @@ JOB_STATUSES_DEFERRED_NOTE = (
RUNNING_STATUSES = frozenset({"QUEUED", "IN_PROGRESS", "PENDING", "WAITING",
"REQUESTED"})
FAILED_CONCLUSIONS = frozenset({"FAILURE", "CANCELLED", "TIMED_OUT",
"STARTUP_FAILURE", "ACTION_REQUIRED"})
-SETUP_WORKFLOW_STATES = frozenset({"disabled_fork", "disabled_manually",
"missing"})
+SETUP_WORKFLOW_STATES = frozenset({"disabled_fork", "disabled_inactivity",
"disabled_manually", "missing"})
class ForkState(str, Enum):
@@ -284,8 +285,6 @@ def observe_fork_state(fork: ForkObservation, marker:
Marker | None) -> tuple[Fo
return ForkState.FAILED, None
return None, f"unexpected fork check suite conclusion
{fork.suite_conclusion!r}"
if fork.workflow_state is None:
- if marker is not None and marker.state ==
ForkState.AWAITING_PUSH.value:
- return ForkState.AWAITING_PUSH, None
return None, "no fork run and the fork workflow state was not read"
if fork.workflow_state in SETUP_WORKFLOW_STATES:
return ForkState.SETUP_REQUIRED, None
@@ -294,10 +293,6 @@ def observe_fork_state(fork: ForkObservation, marker:
Marker | None) -> tuple[Fo
return None, f"unexpected fork workflow state {fork.workflow_state!r}"
-def needs_workflow_state(marker: Marker | None) -> bool:
- return marker is None or marker.state != ForkState.AWAITING_PUSH.value
-
-
def has_job_started(job: dict[str, Any]) -> bool:
# A job that never reached a runner -- queued, or cancelled while queued
-- has no steps and a
# `runner_id` of null or 0, but its `status` can be "completed" and its
`started_at` set.
@@ -587,6 +582,17 @@ def classify(
# tracked at https://github.com/apache/airflow/issues/74908
if marker and marker.state == ForkState.SETUP_REQUIRED.value and state ==
ForkState.AWAITING_PUSH:
plan.new_comments.append(nudge_comment(fork=pr.head_repo))
+ # A new comment, because the edited state comment would not notify the
author that fork CI they
+ # had working stopped running. Like the nudge, it is not retried when
posting it fails; tracked at
+ # https://github.com/apache/airflow/issues/74908
+ if (
+ marker
+ and marker.state not in (ForkState.SETUP_REQUIRED.value,
ForkState.CLOSED.value)
+ and state == ForkState.SETUP_REQUIRED
+ ):
+ plan.new_comments.append(
+ workflow_disabled_comment(fork=pr.head_repo,
workflow_state=fork.workflow_state)
+ )
if state in CLOSABLE_STATES and now - since >=
timedelta(days=config.close_after_days):
plan.state = ForkState.CLOSED
@@ -1501,12 +1507,9 @@ def run_reconcile(
if pr.head_status_unread:
pr = read_head_status(client, repo, pr)
fork = observations.get(pr.number, ForkObservation())
- if (
- pr.head_repo
- and fork.suite_status is None
- and fork.error is None
- and needs_workflow_state(pr.marker)
- ):
+ # Read on every run, not only until the workflow is first seen
enabled: it can be
+ # disabled later, and a pull request waiting for a push would then
wait forever.
+ if pr.head_repo and fork.suite_status is None and fork.error is
None:
fork =
ForkObservation(workflow_state=fetch_workflow_state(client, pr.head_repo))
jobs_reported = None
if config.job_statuses:
diff --git a/dev/breeze/tests/test_ci_audit.py
b/dev/breeze/tests/test_ci_audit.py
index 2dbc0d8a3a5..9697a67acd0 100644
--- a/dev/breeze/tests/test_ci_audit.py
+++ b/dev/breeze/tests/test_ci_audit.py
@@ -260,8 +260,8 @@ def test_fork_problems(repo, message):
frozenset(),
(0, "active"),
Role.COMMITTER,
- CheckStatus.INFO,
- ("gh workflow disable ci-amd.yml -R someone/airflow",),
+ CheckStatus.OK,
+ (),
id="committer-enabled-is-not-a-failure",
),
pytest.param(
diff --git a/dev/breeze/tests/test_fork_ci_messages.py
b/dev/breeze/tests/test_fork_ci_messages.py
index 407065ca4a0..0c238cff949 100644
--- a/dev/breeze/tests/test_fork_ci_messages.py
+++ b/dev/breeze/tests/test_fork_ci_messages.py
@@ -42,6 +42,7 @@ from airflow_breeze.utils.fork_ci_messages import (
render_marker,
state_comment,
tamper_close_comment,
+ workflow_disabled_comment,
)
MARKER = Marker(state="awaiting_push", sha="abc123", since=datetime(2026, 10,
15, 10, 0, tzinfo=UTC))
@@ -114,6 +115,9 @@ def test_parse_marker_malformed(body):
pytest.param(nudge_comment(fork="u/airflow"), id="nudge"),
pytest.param(inactivity_close_comment(days=7, fork="u/airflow"),
id="inactivity"),
pytest.param(tamper_close_comment(run_url="https://x"), id="tamper"),
+ pytest.param(
+ workflow_disabled_comment(fork="u/airflow",
workflow_state="disabled_manually"), id="disabled"
+ ),
],
)
def test_every_comment_ends_with_footer(text):
@@ -125,9 +129,11 @@ def test_every_comment_ends_with_footer(text):
[
pytest.param(state_comment(Marker("setup_required", "a",
MARKER.since), fork="u/fork", run_url=None)),
pytest.param(inactivity_close_comment(days=7, fork="u/fork")),
+ pytest.param(workflow_disabled_comment(fork="u/fork",
workflow_state="disabled_manually")),
],
)
def test_setup_texts_give_the_exact_command(text):
+ assert "gh repo sync u/fork --branch main" in text
assert "gh workflow enable ci-amd.yml -R u/fork" in text
diff --git a/dev/breeze/tests/test_fork_ci_reconcile.py
b/dev/breeze/tests/test_fork_ci_reconcile.py
index 9f3aa61f751..38977df88b4 100644
--- a/dev/breeze/tests/test_fork_ci_reconcile.py
+++ b/dev/breeze/tests/test_fork_ci_reconcile.py
@@ -141,12 +141,19 @@ def _classify(pr, fork=None, *, bypass_run=None,
changes_ci_files=False, config=
id="enabled-no-run",
),
pytest.param(
- ForkObservation(),
+ ForkObservation(workflow_state="active"),
_marker("awaiting_push"),
ForkState.AWAITING_PUSH,
"pending",
id="still-awaiting",
),
+ pytest.param(
+ ForkObservation(workflow_state="disabled_inactivity"),
+ None,
+ ForkState.SETUP_REQUIRED,
+ "error",
+ id="workflow-disabled-for-inactivity",
+ ),
pytest.param(
ForkObservation(suite_status="IN_PROGRESS", run_url=RUN_URL),
None,
@@ -451,6 +458,37 @@ def test_nudge_on_transition_from_setup_required():
assert "Push this branch again" in plan.new_comments[0]
[email protected](
+ ("marker", "workflow_state", "expected_reason"),
+ [
+ pytest.param(
+ _marker("awaiting_push"), "disabled_manually", "was disabled in
your fork", id="awaiting-push"
+ ),
+ pytest.param(_marker("green"), "disabled_inactivity", "no activity for
60 days", id="green"),
+ pytest.param(_marker("failed"), "missing", "is missing from your
fork", id="failed"),
+ ],
+)
+def test_comment_when_workflow_disabled_after_it_worked(marker,
workflow_state, expected_reason):
+ plan, _, _ = _classify(_pr(marker=marker),
ForkObservation(workflow_state=workflow_state))
+
+ assert plan.state == ForkState.SETUP_REQUIRED
+ assert len(plan.new_comments) == 1
+ assert "Fork CI stopped running" in plan.new_comments[0]
+ assert expected_reason in plan.new_comments[0]
+ assert "gh workflow enable ci-amd.yml -R someone/airflow" in
plan.new_comments[0]
+
+
[email protected](
+ "marker",
+ [pytest.param(None, id="first-run"),
pytest.param(_marker("setup_required"), id="still-disabled")],
+)
+def test_no_disabled_comment_when_workflow_never_worked(marker):
+ plan, _, _ = _classify(_pr(marker=marker),
ForkObservation(workflow_state="disabled_fork"))
+
+ assert plan.state == ForkState.SETUP_REQUIRED
+ assert plan.new_comments == []
+
+
@pytest.mark.parametrize(
("marker", "should_close"),
[
@@ -1501,6 +1539,40 @@ def
test_run_reconcile_isolates_failures_and_reports_them(client, tmp_path):
assert client.set_status.call_args.args[:3] == (2, "sha2", "error")
+def test_run_reconcile_rereads_workflow_state_while_awaiting_push(client,
tmp_path):
+ marker_comment = {
+ "databaseId": 5,
+ "body": render_marker(_marker("awaiting_push", sha="sha1")),
+ "author": {"login": "github-actions"},
+ }
+ client.graphql.side_effect = [
+ _search_result(),
+ _search_result(),
+ _search_result(_pr_node(1, comments={"nodes": [marker_comment]})),
+ {"pr0": {"object": None}},
+ ]
+ client.rest_get_pages.return_value = [{"total_count": 0, "workflow_runs":
[]}]
+
+ def rest_get(path, paginate=False, items_key=None):
+ if path == "repos/someone/my-airflow/actions/workflows/ci-amd.yml":
+ return {"state": "disabled_manually"}
+ raise AssertionError(path)
+
+ client.rest_get.side_effect = rest_get
+
+ run_reconcile(
+ client,
+ CONFIG,
+ now=NOW,
+ run_url="https://run",
+ output_file=tmp_path / "s.json",
+ workflows_dir=tmp_path,
+ )
+
+ assert client.set_status.call_args.args[:3] == (1, "sha1", "error")
+ assert "Fork CI stopped running" in client.create_comment.call_args.args[1]
+
+
@pytest.mark.parametrize(
("state", "deletes"),
[