diff --git a/conda_forge_tick/auto_tick.py b/conda_forge_tick/auto_tick.py
index df8862d0c..d6a5b8ee5 100644
--- a/conda_forge_tick/auto_tick.py
+++ b/conda_forge_tick/auto_tick.py
@@ -9,7 +9,6 @@
import traceback
import typing
from dataclasses import dataclass
-from textwrap import dedent
from typing import Literal, MutableMapping, cast
from urllib.error import URLError
from uuid import uuid4
@@ -289,6 +288,141 @@ def _run_rerender(
return _RerenderInfo(nontrivial_changes=nontrivial_changes)
+def _should_automerge(migrator: Migrator, context: FeedstockContext) -> bool:
+ """
+ Determine if a migration should be auto merged based on the feedstock and migrator settings.
+
+ :param migrator: The migrator to check.
+ :param context: The feedstock context.
+
+ :return: True if the migrator should be auto merged, False otherwise.
+ """
+ if isinstance(migrator, Version):
+ return context.automerge in [True, "version"]
+ else:
+ return getattr(migrator, "automerge", False) and context.automerge in [
+ True,
+ "migration",
+ ]
+
+
+def _is_solvability_check_needed(
+ migrator: Migrator, context: FeedstockContext, base_branch: str
+) -> bool:
+ migrator_check_solvable = getattr(migrator, "check_solvable", True)
+ pr_attempts = _get_pre_pr_migrator_attempts(
+ context.attrs,
+ migrator_name=get_migrator_name(migrator),
+ is_version=isinstance(migrator, Version),
+ )
+ max_pr_attempts = getattr(
+ migrator, "force_pr_after_solver_attempts", MAX_SOLVER_ATTEMPTS * 2
+ )
+
+ logger.info(
+ textwrap.dedent(
+ f"""
+ automerge and check_solvable status/settings:
+ automerge:
+ feedstock_automerge: {context.automerge}
+ migrator_automerge: {getattr(migrator, 'automerge', False)}
+ has_automerge: {_should_automerge(migrator, context)} (only considers feedstock if version migration)
+ check_solvable:
+ feedstock_check_solvable: {context.check_solvable}
+ migrator_check_solvable: {migrator_check_solvable}
+ pre_pr_migrator_attempts: {pr_attempts}
+ force_pr_after_solver_attempts: {max_pr_attempts}
+ """
+ )
+ )
+
+ return (
+ context.feedstock_name != "conda-forge-pinning"
+ and (base_branch == "master" or base_branch == "main")
+ # feedstocks that have problematic bootstrapping will not always be solvable
+ and context.feedstock_name not in BOOTSTRAP_MAPPINGS
+ # stuff in cycles always goes
+ and context.attrs["name"] not in getattr(migrator, "cycles", set())
+ # stuff at the top always goes
+ and context.attrs["name"] not in getattr(migrator, "top_level", set())
+ # either the migrator or the feedstock has to request solver checks
+ and (migrator_check_solvable or context.check_solvable)
+ # we try up to MAX_SOLVER_ATTEMPTS times, and then we just skip
+ # the solver check and issue the PR if automerge is off
+ and (_should_automerge(migrator, context) or (pr_attempts < max_pr_attempts))
+ )
+
+
+def _handle_solvability_error(
+ errors: list[str], context: FeedstockContext, migrator: Migrator, base_branch: str
+) -> None:
+ ci_url = get_bot_run_url()
+ ci_url = f"(bot CI job)" if ci_url else ""
+ _solver_err_str = textwrap.dedent(
+ f"""
+ not solvable {ci_url} @ {base_branch}
+
+
+
+ {'
'.join(sorted(set(errors)))}
+
+
+
+ """,
+ ).strip()
+
+ _set_pre_pr_migrator_error(
+ context.attrs,
+ get_migrator_name(migrator),
+ _solver_err_str,
+ is_version=isinstance(migrator, Version),
+ )
+
+ # remove part of a try for solver errors to make those slightly
+ # higher priority next time the bot runs
+ if isinstance(migrator, Version):
+ with context.attrs["version_pr_info"] as vpri:
+ _new_ver = vpri["new_version"]
+ vpri["new_version_attempts"][_new_ver] -= 0.8
+
+
+def _check_and_process_solvability(
+ migrator: Migrator, context: ClonedFeedstockContext, base_branch: str
+) -> bool:
+ """
+ If the migration needs a solvability check, perform the check. If the recipe is not solvable, handle the error
+ by setting the corresponding fields in the feedstock attributes.
+ If the recipe is solvable, reset the fields that track the solvability check status.
+
+ :param migrator: The migrator that was run
+ :param context: The current FeedstockContext of the feedstock that was migrated
+ :param base_branch: The branch of the feedstock repository that is the migration target
+
+ :returns: True if the migration can proceed normally, False if a required solvability check failed and the migration
+ needs to be aborted
+ """
+ if not _is_solvability_check_needed(migrator, context, base_branch):
+ return True
+
+ solvable, solvability_errors, _ = is_recipe_solvable(
+ str(context.local_clone_dir),
+ build_platform=context.attrs["conda-forge.yml"].get(
+ "build_platform",
+ None,
+ ),
+ )
+ if solvable:
+ _reset_pre_pr_migrator_fields(
+ context.attrs,
+ get_migrator_name(migrator),
+ is_version=isinstance(migrator, Version),
+ )
+ return True
+
+ _handle_solvability_error(solvability_errors, context, migrator, base_branch)
+ return False
+
+
def run_with_tmpdir(
context: FeedstockContext,
migrator: Migrator,
@@ -416,6 +550,10 @@ def run(
else:
rerender_info = _RerenderInfo(nontrivial_changes=False)
+ if not _check_and_process_solvability(migrator, context, base_branch):
+ logger.warning("Skipping migration due to solvability check failure")
+ return False, False
+
# This is needed because we want to migrate to the new backend step-by-step
repo: github3.repos.Repository | None = github3_client().repository(
context.git_repo_owner, context.git_repo_name
@@ -425,102 +563,6 @@ def run(
feedstock_dir = str(context.local_clone_dir.resolve())
- if isinstance(migrator, Version):
- has_automerge = context.automerge in [True, "version"]
- else:
- has_automerge = getattr(migrator, "automerge", False) and context.automerge in [
- True,
- "migration",
- ]
-
- migrator_check_solvable = getattr(migrator, "check_solvable", True)
- feedstock_check_solvable = get_keys_default(
- context.attrs,
- ["conda-forge.yml", "bot", "check_solvable"],
- {},
- False,
- )
- pr_attempts = _get_pre_pr_migrator_attempts(
- context.attrs,
- migrator_name,
- is_version=is_version_migration,
- )
- max_pr_attempts = getattr(
- migrator, "force_pr_after_solver_attempts", MAX_SOLVER_ATTEMPTS * 2
- )
-
- logger.info(
- f"""automerge and check_solvable status/settings:
- automerge:
- feedstock_automerge: {context.automerge}
- migratror_automerge: {getattr(migrator, 'automerge', False)}
- has_automerge: {has_automerge} (only considers feedstock if version migration)
- check_solvable:
- feedstock_checksolvable: {feedstock_check_solvable}
- migrator_check_solvable: {migrator_check_solvable}
- pre_pr_migrator_attempts: {pr_attempts}
- force_pr_after_solver_attempts: {max_pr_attempts}
-"""
- )
-
- if (
- context.feedstock_name != "conda-forge-pinning"
- and (base_branch == "master" or base_branch == "main")
- # feedstocks that have problematic bootstrapping will not always be solvable
- and context.feedstock_name not in BOOTSTRAP_MAPPINGS
- # stuff in cycles always goes
- and context.attrs["name"] not in getattr(migrator, "cycles", set())
- # stuff at the top always goes
- and context.attrs["name"] not in getattr(migrator, "top_level", set())
- # either the migrator or the feedstock has to request solver checks
- and (migrator_check_solvable or feedstock_check_solvable)
- # we try up to MAX_SOLVER_ATTEMPTS times and then we just skip
- # the solver check and issue the PR if automerge is off
- and (has_automerge or (pr_attempts < max_pr_attempts))
- ):
- solvable, errors, _ = is_recipe_solvable(
- feedstock_dir,
- build_platform=context.attrs["conda-forge.yml"].get(
- "build_platform",
- None,
- ),
- )
- if not solvable:
- ci_url = get_bot_run_url()
- ci_url = f"(bot CI job)" if ci_url else ""
- _solver_err_str = dedent(
- f"""
- not solvable {ci_url} @ {base_branch}
-
-
-
- {'
'.join(sorted(set(errors)))}
-
-
-
- """,
- ).strip()
-
- _set_pre_pr_migrator_error(
- context.attrs,
- migrator_name,
- _solver_err_str,
- is_version=is_version_migration,
- )
-
- # remove part of a try for solver errors to make those slightly
- # higher priority next time the bot runs
- if isinstance(migrator, Version):
- with context.attrs["version_pr_info"] as vpri:
- _new_ver = vpri["new_version"]
- vpri["new_version_attempts"][_new_ver] -= 0.8
-
- return False, False
- else:
- _reset_pre_pr_migrator_fields(
- context.attrs, migrator_name, is_version=is_version_migration
- )
-
# TODO: Better annotation here
pr_json: typing.Union[MutableMapping, None, bool]
if (