diff --git a/changelog/+minmaxsalt.changed.md b/changelog/+minmaxsalt.changed.md new file mode 100644 index 0000000..57bbb7a --- /dev/null +++ b/changelog/+minmaxsalt.changed.md @@ -0,0 +1 @@ +Ensured minimum and maximum supported Salt versions always at least follow their default values (supported versions at the time of the template release) when updating the template. diff --git a/changelog/+workflows.removed.md b/changelog/+workflows.removed.md new file mode 100644 index 0000000..7f2509a --- /dev/null +++ b/changelog/+workflows.removed.md @@ -0,0 +1 @@ +Dropped `workflows` question and `basic` and `org` workflow variants. All projects use the `enhanced` workflows from now on. diff --git a/copier.yml b/copier.yml index ef92571..22a21ae 100644 --- a/copier.yml +++ b/copier.yml @@ -209,6 +209,9 @@ max_salt_version: {%- endif -%} {%- endif -%} {%- endif -%} + {%- if (max_salt_version | float) < (salt_version | float) -%} + Maximum version needs to be at least {{ salt_version }} + {%- endif -%} no_saltext_namespace: type: bool diff --git a/tasks/migrations.py b/tasks/migrations.py index 5ccbc89..9ff1780 100644 --- a/tasks/migrations.py +++ b/tasks/migrations.py @@ -1,7 +1,48 @@ +""" +Run migrations between template versions during updates. +""" + from packaging.version import Version +from task_helpers.copier import load_data_yaml +from task_helpers.migrate import COPIER_CONF +from task_helpers.migrate import migration from task_helpers.migrate import run_migrations +from task_helpers.migrate import status +from task_helpers.migrate import sync_minimum_version from task_helpers.migrate import var_migration +# 0.5.0 migrates all projects to the enhanced workflows, which +# require accurate Salt versions to generate sensible test matrices. +# The default values always represent the extremes, so sync them +# during all updates from now on. This avoids having to create +# separate migrations for future updates. +sync_minimum_version(None, "max_salt_version") +sync_salt_version = sync_minimum_version(None, "salt_version") + + +@migration(None, "before", desc=False, after=sync_salt_version) +def ensure_minimum_python_requires(answers): + """ + Ensure the minimum Python version is respected during each update. + We cannot use sync_minimum_version for this because the default + value is computed in Jinja. Let's replicate the Jinja here. + """ + if "python_requires" not in answers: + return + + salt_python_support = load_data_yaml("salt_python_support") + selected_salt_version = float( + int(answers.get("salt_version", COPIER_CONF["salt_version"]["default"])) + ) + default = ".".join(str(x) for x in salt_python_support[selected_salt_version]["min"]) + current = answers["python_requires"] + + if Version(str(current)) < Version(str(default)): + new = type(current)(default) + status(f"Answer migration: Updating python_requires from {current!r} to {new!r}") + answers["python_requires"] = new + return answers + @var_migration("0.4.5", "max_salt_version") def migrate_045_max_salt_version(val): @@ -21,14 +62,5 @@ def migrate_037_docs_url(val): return ... -@var_migration("0.3.0", "python_requires") -def migrate_030_python_requires(val): - """ - Raise minimum Python version to 3.8. - """ - if Version(val) < Version("3.8"): - return "3.8" - - if __name__ == "__main__": run_migrations() diff --git a/tasks/task_helpers/copier.py b/tasks/task_helpers/copier.py new file mode 100644 index 0000000..6ca3e32 --- /dev/null +++ b/tasks/task_helpers/copier.py @@ -0,0 +1,24 @@ +from pathlib import Path + +import copier.template +import yaml + +TEMPLATE_ROOT = (Path(__file__).parent.parent.parent).resolve() + + +def load_copier_conf(): + """ + Load the complete Copier configuration. + """ + return copier.template.load_template_config(TEMPLATE_ROOT / "copier.yml") + + +def load_data_yaml(name): + """ + Load a file in the template root `data` directory. + """ + file = (TEMPLATE_ROOT / "data" / name).with_suffix(".yaml") + if not file.exists(): + raise OSError(f"The file '{file}' does not exist") + with open(file, encoding="utf-8") as f: + return yaml.safe_load(f) diff --git a/tasks/task_helpers/migrate.py b/tasks/task_helpers/migrate.py index 425050b..be20adf 100644 --- a/tasks/task_helpers/migrate.py +++ b/tasks/task_helpers/migrate.py @@ -2,6 +2,7 @@ from packaging.version import Version +from .copier import load_copier_conf from .pythonpath import project_tools with project_tools(): @@ -17,6 +18,7 @@ MIGRATIONS = [] +COPIER_CONF = load_copier_conf() def run_migrations(): @@ -51,7 +53,7 @@ def _run_migrations_after(answers): func(answers.copy()) -def migration(trigger, stage="after", desc=None): +def migration(trigger, stage="after", desc=None, after=None): """ Decorator for declaring a general migration. @@ -108,7 +110,12 @@ def wrapper(func): # Other decorators delegate to this one, only create a general # migration if it's not already another subtype if not isinstance(func, Migration): - func = Migration(func, desc=desc or func.__name__.replace("_", " ")) + nonlocal desc + if desc is None: + desc = func.__name__.replace("_", " ") + elif not desc: + desc = None + func = Migration(func, desc=desc) global MIGRATIONS MIGRATIONS.append((trigger_version, func)) return func @@ -116,7 +123,7 @@ def wrapper(func): return wrapper -def var_migration(trigger, varname): +def var_migration(trigger, varname, after=None): """ Decorator for declaring an answer migration, e.g. when raising a minimum version or changing an answer's type. @@ -142,25 +149,68 @@ def migrate_foo_100(val): """ def wrapper(func): - return migration(trigger, "before")(VarMigration(func, varname)) + return migration(trigger, "before")(VarMigration(func, varname, after=after)) return wrapper +def raise_minimum_version(trigger, varname, minimum): + """ + Register a migration to increase the value of an answer representing + a version to a minimum value. + + Examples: + + raise_minimum_version("1.0.0", "python", 3.8) + raise_minimum_version("1.0.0", "python", "3.8") + """ + + def _raise_minimum(var): + if Version(str(var)) < Version(str(minimum)): + return type(var)(minimum) + + return var_migration(trigger, varname)(_raise_minimum) + + +def sync_minimum_version(trigger, varname): + """ + Register a migration to increase the value of an answer representing + a version to a minimum value, determined by its default answer. + """ + default = COPIER_CONF[varname]["default"] + return raise_minimum_version(trigger, varname, default) + + class Migration: - def __init__(self, func, desc=None): + def __init__(self, func, desc=None, after=None): self.func = func self.desc = desc + after = after or [] + if not isinstance(after, list): + after = [after] + self.after = after def __call__(self, answers): if self.desc is not None: status(f"Running migration: {self.desc}") return self.func(answers) + def __lt__(self, other): + """ + Ensure we can sort the list of migrations, even if there + are multiple migrations for a single version. + + This also allows explicit ordering of migrations of the + same version (or those without one). + """ + if not isinstance(other, Migration): + raise TypeError(f"Cannot compare Migration to {other!r}") + return any(migration is self for migration in other.after) + class VarMigration(Migration): - def __init__(self, func, varname): - super().__init__(func) + def __init__(self, func, varname, after=None): + super().__init__(func, after=after) self.varname = varname def __call__(self, answers):