diff --git a/nautobot-app/{{ cookiecutter.project_slug }}/.github/workflows/ci.yml b/nautobot-app/{{ cookiecutter.project_slug }}/.github/workflows/ci.yml index 874537d0..c3e8821a 100644 --- a/nautobot-app/{{ cookiecutter.project_slug }}/.github/workflows/ci.yml +++ b/nautobot-app/{{ cookiecutter.project_slug }}/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: # yamllint disable-line rule:truthy rule:comments pull_request: ~ env: - APP_NAME: "{{ cookiecutter.project_slug }}" + APP_NAME: "{{ cookiecutter.app_slug }}" jobs: ruff-format: @@ -103,6 +103,10 @@ jobs: uses: "actions/checkout@v4" - name: "Setup environment" uses: "networktocode/gh-action-setup-poetry-environment@v6" + - name: "Constrain Nautobot version and regenerate lock file" + env: + INVOKE_NAUTOBOT_DEV_EXAMPLE_LOCAL: "true" + run: "poetry run invoke lock --constrain-nautobot-ver --constrain-python-ver" - name: "Set up Docker Buildx" id: "buildx" uses: "docker/setup-buildx-action@v3" @@ -120,6 +124,7 @@ jobs: build-args: | NAUTOBOT_VER={% raw %}${{ matrix.nautobot-version }}{% endraw %} PYTHON_VER={% raw %}${{ matrix.python-version }}{% endraw %} + CI=true - name: "Copy credentials" run: "cp development/creds.example.env development/creds.env" - name: "Linting: pylint" @@ -134,14 +139,14 @@ jobs: strategy: fail-fast: true matrix: - python-version: ["3.8", "3.11"] + python-version: ["3.8", "3.12"] db-backend: ["postgresql"] nautobot-version: ["stable"] include: - python-version: "3.11" db-backend: "postgresql" nautobot-version: "{{ cookiecutter.min_nautobot_version }}" - - python-version: "3.11" + - python-version: "3.12" db-backend: "mysql" nautobot-version: "stable" runs-on: "ubuntu-22.04" @@ -170,6 +175,7 @@ jobs: build-args: | NAUTOBOT_VER={% raw %}${{ matrix.nautobot-version }}{% endraw %} PYTHON_VER={% raw %}${{ matrix.python-version }}{% endraw %} + CI=true - name: "Copy credentials" run: "cp development/creds.example.env development/creds.env" - name: "Use Mysql invoke settings when needed" @@ -208,7 +214,7 @@ jobs: - name: "Set up Python" uses: "actions/setup-python@v5" with: - python-version: "3.11" + python-version: "3.12" - name: "Install Python Packages" run: "pip install poetry" - name: "Set env" @@ -243,7 +249,7 @@ jobs: - name: "Set up Python" uses: "actions/setup-python@v5" with: - python-version: "3.11" + python-version: "3.12" - name: "Install Python Packages" run: "pip install poetry" - name: "Set env" diff --git a/nautobot-app/{{ cookiecutter.project_slug }}/development/Dockerfile b/nautobot-app/{{ cookiecutter.project_slug }}/development/Dockerfile index ad74c346..a3fc67b7 100644 --- a/nautobot-app/{{ cookiecutter.project_slug }}/development/Dockerfile +++ b/nautobot-app/{{ cookiecutter.project_slug }}/development/Dockerfile @@ -53,29 +53,18 @@ RUN which poetry || curl -sSL https://install.python-poetry.org | python3 - && \ WORKDIR /source COPY . /source -# Get container's installed Nautobot version as a forced constraint -# NAUTOBOT_VER may be a branch name and not a published release therefor we need to get the installed version -# so pip can use it to recognize local constraints. -RUN pip show nautobot | grep "^Version: " | sed -e 's/Version: /nautobot==/' > constraints.txt +# Build args must be declared in each stage +ARG PYTHON_VER -# Use Poetry to grab dev dependencies from the lock file -# Can be improved in Poetry 1.2 which allows `poetry install --only dev` -# -# We can't use the entire freeze as it takes forever to resolve with rigidly fixed non-direct dependencies, -# especially those that are only direct to Nautobot but the container included versions slightly mismatch -RUN poetry export -f requirements.txt --without-hashes --extras all --output poetry_freeze_base.txt -RUN poetry export -f requirements.txt --without-hashes --extras all --with dev --output poetry_freeze_all.txt -RUN sort poetry_freeze_base.txt poetry_freeze_all.txt | uniq -u > poetry_freeze_dev.txt - -# Install all local project as editable, constrained on Nautobot version, to get any additional -# direct dependencies of the app -RUN --mount=type=cache,target="/root/.cache/pip",sharing=locked \ - pip install -c constraints.txt -e .[all] +# Constrain the Nautobot version to NAUTOBOT_VER +# In CI, this should be done outside of the Dockerfile to prevent cross-compile build failures +ARG CI +RUN if [ -z "${CI+x}" ]; then \ + INSTALLED_NAUTOBOT_VER=$(pip show nautobot | grep "^Version" | sed "s/Version: //"); \ + poetry add --lock nautobot@${INSTALLED_NAUTOBOT_VER} --python ${PYTHON_VER}; fi -# Install any dev dependencies frozen from Poetry -# Can be improved in Poetry 1.2 which allows `poetry install --only dev` -RUN --mount=type=cache,target="/root/.cache/pip",sharing=locked \ - pip install -c constraints.txt -r poetry_freeze_dev.txt +# Install the app +RUN poetry install --extras all --with dev COPY development/nautobot_config.py ${NAUTOBOT_ROOT}/nautobot_config.py # !!! USE CAUTION WHEN MODIFYING LINES ABOVE diff --git a/nautobot-app/{{ cookiecutter.project_slug }}/invoke.example.yml b/nautobot-app/{{ cookiecutter.project_slug }}/invoke.example.yml index c76449a9..ff3b38da 100644 --- a/nautobot-app/{{ cookiecutter.project_slug }}/invoke.example.yml +++ b/nautobot-app/{{ cookiecutter.project_slug }}/invoke.example.yml @@ -1,12 +1,15 @@ --- {{ cookiecutter.app_name }}: - project_name: "{{ cookiecutter.app_slug }}" nautobot_ver: "{{ cookiecutter.min_nautobot_version }}" - local: false python_ver: "3.11" - compose_dir: "development" - compose_files: - - "docker-compose.base.yml" - - "docker-compose.redis.yml" - - "docker-compose.postgres.yml" - - "docker-compose.dev.yml" + # local: false + # compose_dir: "/full/path/to/{{ cookiecutter.project_slug }}/development" + +# The following is an example of using MySQL as the database backend +# --- +# {{ cookiecutter.app_name }}: +# compose_files: +# - "docker-compose.base.yml" +# - "docker-compose.redis.yml" +# - "docker-compose.mysql.yml" +# - "docker-compose.dev.yml" diff --git a/nautobot-app/{{ cookiecutter.project_slug }}/pyproject.toml b/nautobot-app/{{ cookiecutter.project_slug }}/pyproject.toml index 812b3dba..4e4095fb 100644 --- a/nautobot-app/{{ cookiecutter.project_slug }}/pyproject.toml +++ b/nautobot-app/{{ cookiecutter.project_slug }}/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] packages = [ { include = "{{ cookiecutter.app_name }}" }, @@ -29,7 +30,7 @@ include = [ ] [tool.poetry.dependencies] -python = ">=3.8,<3.12" +python = ">=3.8,<3.13" # Used for local development nautobot = "^{{ cookiecutter.min_nautobot_version }}" diff --git a/nautobot-app/{{ cookiecutter.project_slug }}/tasks.py b/nautobot-app/{{ cookiecutter.project_slug }}/tasks.py index 56f50250..64efad56 100644 --- a/nautobot-app/{{ cookiecutter.project_slug }}/tasks.py +++ b/nautobot-app/{{ cookiecutter.project_slug }}/tasks.py @@ -13,10 +13,12 @@ """ import os +import re from pathlib import Path from time import sleep from invoke.collection import Collection +from invoke.exceptions import Exit from invoke.tasks import task as invoke_task @@ -48,7 +50,7 @@ def is_truthy(arg): namespace.configure( { "{{ cookiecutter.app_name }}": { - "nautobot_ver": "{{ cookiecutter.min_nautobot_version }}", + "nautobot_ver": "2.3.1", "project_name": "{{ cookiecutter.app_slug }}", "python_ver": "3.11", "local": False, @@ -205,17 +207,51 @@ def generate_packages(context): run_command(context, command) +def _get_docker_nautobot_version(context, nautobot_ver=None, python_ver=None): + """Extract Nautobot version from base docker image.""" + if nautobot_ver is None: + nautobot_ver = context.{{ cookiecutter.app_name }}.nautobot_ver + if python_ver is None: + python_ver = context.{{ cookiecutter.app_name }}.python_ver + dockerfile_path = os.path.join(context.{{ cookiecutter.app_name }}.compose_dir, "Dockerfile") + base_image = context.run(f"grep --max-count=1 '^FROM ' {dockerfile_path}", hide=True).stdout.strip().split(" ")[1] + base_image = base_image.replace(r"${NAUTOBOT_VER}", nautobot_ver).replace(r"${PYTHON_VER}", python_ver) + pip_nautobot_ver = context.run(f"docker run --rm --entrypoint '' {base_image} pip show nautobot", hide=True) + match_version = re.search(r"^Version: (.+)$", pip_nautobot_ver.stdout.strip(), flags=re.MULTILINE) + if match_version: + return match_version.group(1) + else: + raise Exit(f"Nautobot version not found in Docker base image {base_image}.") + + @task( help={ "check": ( "If enabled, check for outdated dependencies in the poetry.lock file, " "instead of generating a new one. (default: disabled)" - ) + ), + "constrain_nautobot_ver": ( + "Run 'poetry add nautobot@[version] --lock' to generate the lockfile, " + "where [version] is the version installed in the Dockerfile's base image. " + "Generally intended to be used in CI and not for local development. (default: disabled)" + ), + "constrain_python_ver": ( + "When using `constrain_nautobot_ver`, further constrain the nautobot version " + "to python_ver so that poetry doesn't complain about python version incompatibilities. " + "Generally intended to be used in CI and not for local development. (default: disabled)" + ), } ) -def lock(context, check=False): - """Generate poetry.lock inside the Nautobot container.""" - run_command(context, f"poetry {'check' if check else 'lock --no-update'}") +def lock(context, check=False, constrain_nautobot_ver=False, constrain_python_ver=False): + """Generate poetry.lock file.""" + if constrain_nautobot_ver: + docker_nautobot_version = _get_docker_nautobot_version(context) + command = f"poetry add --lock nautobot@{docker_nautobot_version}" + if constrain_python_ver: + command += f" --python {context.{{ cookiecutter.app_name }}.python_ver}" + else: + command = f"poetry {'check' if check else 'lock --no-update'}" + run_command(context, command) # ------------------------------------------------------------------------------