abandon_old_changes: Initial commit
Abandon old changes owned by the current service account. This will help
clean up rolls that were orphaned by rollers.
Cap the number of abandoned changes at 10. This should be enough to get
through all the past changes without taking too long, but it's still low
enough that it won't delay the rest of the builder more than a few
seconds.
Bug: b/368181572
Change-Id: Ib243b38a3b67eb8b1d10b8c004931b9a8b23478e
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/236838
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Ted Pudlik <tpudlik@google.com>
Commit-Queue: Rob Mohr <mohrr@google.com>
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
diff --git a/recipe_modules/abandon_old_changes/__init__.py b/recipe_modules/abandon_old_changes/__init__.py
new file mode 100644
index 0000000..17722ab
--- /dev/null
+++ b/recipe_modules/abandon_old_changes/__init__.py
@@ -0,0 +1,25 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# pylint: disable=missing-docstring
+
+from __future__ import annotations
+
+DEPS = [
+ 'fuchsia/gerrit',
+ 'recipe_engine/buildbucket',
+ 'recipe_engine/futures',
+ 'recipe_engine/json',
+ 'recipe_engine/step',
+]
diff --git a/recipe_modules/abandon_old_changes/api.py b/recipe_modules/abandon_old_changes/api.py
new file mode 100644
index 0000000..2c0c12d
--- /dev/null
+++ b/recipe_modules/abandon_old_changes/api.py
@@ -0,0 +1,87 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Abandon old changes owned by the current service account."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from recipe_engine import recipe_api
+
+if TYPE_CHECKING: # pragma: no cover
+ from recipe_engine import config_types, engine_types
+
+
+class AbandonOldChangesApi(recipe_api.RecipeApi):
+ """Abandon old changes owned by the current service account."""
+
+ def __call__(
+ self,
+ *,
+ host: str,
+ age_days: int = 30,
+ continue_on_failure: bool = True,
+ max_to_abandon: int = 10,
+ ):
+ with self.m.step.nest('abandon old changes'):
+ try:
+ self._abandon_old_changes(
+ host=self.m.gerrit.host_from_remote_url(host),
+ age_days=age_days,
+ max_to_abandon=max_to_abandon,
+ )
+ self.m.step.empty('success')
+
+ except self.m.step.StepFailure:
+ self.m.step.empty('failure')
+ if not continue_on_failure:
+ raise # pragma: no cover
+
+ def _abandon_old_changes(
+ self,
+ *,
+ host: str,
+ age_days: int,
+ max_to_abandon: int,
+ ):
+ query_string = (
+ f'is:open '
+ f'owner:{self.m.buildbucket.build.infra.swarming.task_service_account} '
+ f'age:{age_days}d '
+ )
+
+ changes = self.m.gerrit.change_query(
+ name='get old changes',
+ query_string=query_string,
+ host=host,
+ max_attempts=1,
+ timeout=30,
+ test_data=self.m.json.test_api.output(
+ [
+ {'_number': 1001},
+ {'_number': 1002},
+ ],
+ ),
+ ).json.output
+
+ futures = []
+ for change in changes[:max_to_abandon]:
+ self.m.futures.spawn(
+ self.m.gerrit.abandon,
+ f'abandon {change["_number"]}',
+ change['_number'],
+ 'Abandoning orphaned roll change.',
+ )
+
+ self.m.futures.wait(futures)
diff --git a/recipe_modules/abandon_old_changes/tests/full.py b/recipe_modules/abandon_old_changes/tests/full.py
new file mode 100644
index 0000000..d0137c9
--- /dev/null
+++ b/recipe_modules/abandon_old_changes/tests/full.py
@@ -0,0 +1,72 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Full test of abandon_old_changes module."""
+
+from __future__ import annotations
+
+from typing import Generator, TYPE_CHECKING
+
+from recipe_engine import post_process
+
+if TYPE_CHECKING: # pragma: no cover
+ from recipe_engine import recipe_test_api
+
+DEPS = [
+ 'fuchsia/buildbucket_util',
+ 'pigweed/abandon_old_changes',
+]
+
+
+def RunSteps(api):
+ api.abandon_old_changes(host='pigweed')
+
+
+def GenTests(api) -> Generator[recipe_test_api.TestData, None, None]:
+ yield api.buildbucket_util.test(
+ 'success',
+ api.post_process(
+ post_process.MustRun,
+ 'abandon old changes.abandon 1001',
+ ),
+ api.post_process(
+ post_process.MustRun,
+ 'abandon old changes.abandon 1002',
+ ),
+ api.post_process(
+ post_process.MustRun,
+ 'abandon old changes.success',
+ ),
+ api.post_process(
+ post_process.DoesNotRun,
+ 'abandon old changes.failure',
+ ),
+ api.post_process(post_process.DropExpectation),
+ )
+
+ yield api.buildbucket_util.test(
+ 'failure',
+ api.override_step_data(
+ 'abandon old changes.get old changes',
+ retcode=1,
+ ),
+ api.post_process(
+ post_process.DoesNotRun,
+ 'abandon old changes.success',
+ ),
+ api.post_process(
+ post_process.MustRun,
+ 'abandon old changes.failure',
+ ),
+ api.post_process(post_process.DropExpectation),
+ )