From f06e5530f4bc22cb32beec2572a71da76a6ce1dd Mon Sep 17 00:00:00 2001 From: Pablo Caro Revuelta Date: Fri, 10 Jul 2026 11:37:36 +0200 Subject: [PATCH] fix(project): align bootstrapped extension deps with the runner (LITE-33583) Bootstrapping an extension produced an incoherent runner/connect-eaas-core pair, so the generated project could not run without manual fixes. The runner image was selected by the CLI's own major version (26, stale versus the real runner line 43) while the pyproject template hardcoded a connect-eaas-core ">=30" floor, letting `poetry update` resolve a version the runner image does not support. Select the latest published runner instead of filtering by the CLI major, and derive the connect-eaas-core specifier from that runner's PyPI requires_dist so the pair is always coherent and cannot drift again. The template now pins connect-eaas-core to the derived specifier, aligns python to the runner line (>=3.9,<3.13), and refreshes the stale dev-dependencies (pytest 8, flake8 >=6, pytest-asyncio >=0.23; drops the dead mock marker). --- .../plugins/project/extension/constants.py | 1 + .../cli/plugins/project/extension/helpers.py | 5 +- .../${project_slug}/pyproject.toml.j2 | 25 +++--- .../cli/plugins/project/extension/utils.py | 32 +++++-- .../plugins/project/test_extension_helpers.py | 37 +++++++- tests/plugins/project/test_extension_utils.py | 88 +++++++++++++------ 6 files changed, 139 insertions(+), 49 deletions(-) diff --git a/connect/cli/plugins/project/extension/constants.py b/connect/cli/plugins/project/extension/constants.py index a2557d9c..b67a0306 100644 --- a/connect/cli/plugins/project/extension/constants.py +++ b/connect/cli/plugins/project/extension/constants.py @@ -2,6 +2,7 @@ PYPI_EXTENSION_RUNNER_URL = 'https://pypi.org/pypi/connect-extension-runner/json' +PYPI_EXTENSION_RUNNER_RELEASE_URL = 'https://pypi.org/pypi/connect-extension-runner/{version}/json' PRE_COMMIT_HOOK = """#! /bin/sh diff --git a/connect/cli/plugins/project/extension/helpers.py b/connect/cli/plugins/project/extension/helpers.py index 2d98837b..3d7c25a6 100644 --- a/connect/cli/plugins/project/extension/helpers.py +++ b/connect/cli/plugins/project/extension/helpers.py @@ -12,6 +12,7 @@ from connect.cli.core.terminal import console from connect.cli.plugins.project.extension.utils import ( get_event_definitions, + get_pypi_runner_eaas_core_version, get_pypi_runner_version, get_pypi_runner_version_by_connect_version, initialize_git_repository, @@ -79,11 +80,13 @@ def bootstrap_extension_project( # noqa: CCR001 if not answers: raise ClickException('Aborted by user input') + runner_version = get_pypi_runner_version() ctx = { 'statuses_by_event': statuses_by_event[answers['extension_type']], 'interactive': [], 'current_major_version': get_version().split('.')[0], - 'runner_version': get_pypi_runner_version(), + 'runner_version': runner_version, + 'connect_eaas_core_version': get_pypi_runner_eaas_core_version(runner_version), } for var, answer in answers.items(): diff --git a/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 b/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 index 85784b39..f95e0298 100644 --- a/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 +++ b/connect/cli/plugins/project/extension/templates/bootstrap/${project_slug}/pyproject.toml.j2 @@ -24,27 +24,26 @@ readme = "./README.md" {%- endif %} [tool.poetry.dependencies] -python = ">=3.8,<4" -connect-eaas-core = ">=30" +python = ">=3.9,<3.13" +connect-eaas-core = "{{ connect_eaas_core_version }}" [tool.poetry.dev-dependencies] -pytest = ">=6.1.2,<8" -pytest-cov = ">=2.10.1,<5" +pytest = ">=8,<9" +pytest-cov = ">=2.10.1,<7" pytest-mock = "^3.3.1" -mock = { version = "^4.0.3", markers = "python_version < '3.8'" } -coverage = {extras = ["toml"], version = ">=5.3,<7"} -flake8 = ">=3.8,<6" -flake8-bugbear = ">=20,<23" +coverage = {extras = ["toml"], version = ">=5.3,<8"} +flake8 = ">=6" +flake8-bugbear = ">=24" flake8-cognitive-complexity = "^0.1" -flake8-commas = "~2.0" +flake8-commas = "~4" flake8-future-import = "~0.4" flake8-import-order = "~0.18" -flake8-broken-line = ">=0.3,<0.7" -flake8-comprehensions = "^3.3.1" +flake8-broken-line = ">=1.0" +flake8-comprehensions = "^3.10" flake8-debugger = "^4.0.0" -flake8-eradicate = "^1.0.0" +flake8-eradicate = "^1.3.0" flake8-string-format = "^0.3.0" -pytest-asyncio = "^0.15.1" +pytest-asyncio = ">=0.23" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/connect/cli/plugins/project/extension/utils.py b/connect/cli/plugins/project/extension/utils.py index 7a582484..0b9cb129 100644 --- a/connect/cli/plugins/project/extension/utils.py +++ b/connect/cli/plugins/project/extension/utils.py @@ -5,15 +5,15 @@ import requests from click import ClickException from connect.client import ClientError +from packaging.requirements import Requirement -from connect.cli import get_version from connect.cli.core.utils import ( get_connect_version, get_last_version_by_major, - sort_and_filter_tags, ) from connect.cli.plugins.project.extension.constants import ( PRE_COMMIT_HOOK, + PYPI_EXTENSION_RUNNER_RELEASE_URL, PYPI_EXTENSION_RUNNER_URL, ) @@ -31,11 +31,29 @@ def get_pypi_runner_version(): raise ClickException( f'We can not retrieve the current connect-extension-runner version from {PYPI_EXTENSION_RUNNER_URL}.', ) - content = res.json() - tags = sort_and_filter_tags(content['releases'], get_version().split('.', 1)[0]) - if tags: - return tags.popitem()[0] - return content['info']['version'] + return res.json()['info']['version'] + + +def get_pypi_runner_eaas_core_version(runner_version): + """Return the connect-eaas-core version specifier required by the given runner. + + The bootstrapped extension must pin the same connect-eaas-core the runner image + ships, otherwise `poetry update` resolves an incompatible version and the + extension fails to run. + """ + url = PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version=runner_version) + res = requests.get(url) + if res.status_code != 200: + raise ClickException( + f'We can not retrieve the connect-extension-runner {runner_version} metadata from {url}.', + ) + for dist in res.json()['info'].get('requires_dist') or []: + requirement = Requirement(dist) + if requirement.name == 'connect-eaas-core': + return str(requirement.specifier) + raise ClickException( + f'connect-extension-runner {runner_version} does not declare a connect-eaas-core dependency.', + ) def get_pypi_runner_version_by_connect_version(): diff --git a/tests/plugins/project/test_extension_helpers.py b/tests/plugins/project/test_extension_helpers.py index a8e285f0..c4bc5171 100644 --- a/tests/plugins/project/test_extension_helpers.py +++ b/tests/plugins/project/test_extension_helpers.py @@ -49,6 +49,10 @@ def test_bootstrap_extension_project_background( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_vendor.load(config_dir='/tmp') mocked_responses.add( @@ -127,6 +131,8 @@ def test_bootstrap_extension_project_background( pyproject_toml = toml.load(os.path.join(tmpdir, data['project_slug'], 'pyproject.toml')) + assert pyproject_toml['tool']['poetry']['dependencies']['connect-eaas-core'] == '>=37.4,<38' + ext_entrypoint = pyproject_toml['tool']['poetry']['plugins']['connect.eaas.ext'] assert ext_entrypoint == { 'eventsapp': f"{data['package_name']}.events:{classname_prefix}EventsApplication", @@ -224,6 +230,10 @@ def test_bootstrap_extension_project_interactive( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_vendor.load(config_dir='/tmp') mocked_responses.add( @@ -296,6 +306,8 @@ def test_bootstrap_extension_project_interactive( pyproject_toml = toml.load(os.path.join(tmpdir, data['project_slug'], 'pyproject.toml')) + assert pyproject_toml['tool']['poetry']['dependencies']['connect-eaas-core'] == '>=37.4,<38' + ext_entrypoint = pyproject_toml['tool']['poetry']['plugins']['connect.eaas.ext'] assert ext_entrypoint == { 'eventsapp': f"{data['package_name']}.events:{classname_prefix}EventsApplication", @@ -377,6 +389,10 @@ def test_bootstrap_extension_project_multiaccount( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_vendor.load(config_dir='/tmp') mocked_responses.add( @@ -527,6 +543,10 @@ def test_bootstrap_extension_project_webapp( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_provider.load(config_dir='/tmp') mocked_responses.add( @@ -688,6 +708,10 @@ def test_bootstrap_extension_project_tfnapp( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_provider.load(config_dir='/tmp') mocked_responses.add( @@ -893,7 +917,6 @@ def test_bootstrap_extension_project_if_destination_exists(mocker): def test_bump_runner_version(mocker, mocked_responses, capsys): - mocker.patch('connect.cli.plugins.project.extension.utils.get_version', return_value='0.5') mocked_responses.add( 'GET', DEFAULT_ENDPOINT, @@ -1045,6 +1068,10 @@ def test_validate_extension_project(mocker, faker, mocked_responses, config_vend 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_vendor.load(config_dir='/tmp') mocked_responses.add( @@ -1114,6 +1141,10 @@ def test_validate_extension_project_error_exit( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', side_effect=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_vendor.load(config_dir='/tmp') mocked_responses.add( @@ -1184,6 +1215,10 @@ def test_bootstrap_extension_project_corrupted_load_answers( 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value=runner_version, ) + mocker.patch( + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_eaas_core_version', + return_value='>=37.4,<38', + ) config_provider.load(config_dir='/tmp') mocked_responses.add( diff --git a/tests/plugins/project/test_extension_utils.py b/tests/plugins/project/test_extension_utils.py index ffaa2322..2678e8d3 100644 --- a/tests/plugins/project/test_extension_utils.py +++ b/tests/plugins/project/test_extension_utils.py @@ -2,7 +2,10 @@ from click import ClickException from connect.client import ClientError -from connect.cli.plugins.project.extension.constants import PYPI_EXTENSION_RUNNER_URL +from connect.cli.plugins.project.extension.constants import ( + PYPI_EXTENSION_RUNNER_RELEASE_URL, + PYPI_EXTENSION_RUNNER_URL, +) from connect.cli.plugins.project.extension.utils import ( check_eventsapp_feature_not_selected, check_extension_events_applicable, @@ -14,37 +17,32 @@ get_default_application_types, get_event_definitions, get_interactive_events, + get_pypi_runner_eaas_core_version, get_pypi_runner_version, ) -def test_get_pypi_runner_version(mocker, mocked_responses): - mocker.patch( - 'connect.cli.plugins.project.extension.utils.get_version', - return_value='25.0', - ) +def test_get_pypi_runner_version(mocked_responses): + """The bootstrap must pin the latest published runner, not one tied to the CLI major. + + The CLI major (26) diverged from the runner line (43), so filtering runner tags by + CLI major returned a stale, incompatible image. Confidence: a fresh extension always + gets the current runner. + """ mocked_responses.add( method='GET', url=PYPI_EXTENSION_RUNNER_URL, json={ - 'releases': { - '26.0': {}, - '25.1': {}, - '25.0': {}, - '24.5': {}, - }, + 'info': {'version': '43.0'}, + 'releases': {'43.0': {}, '42.0': {}, '26.44': {}}, }, status=200, ) - assert get_pypi_runner_version() == '25.1' + assert get_pypi_runner_version() == '43.0' -def test_get_pypi_runner_version_http_error(mocker, mocked_responses): - mocker.patch( - 'connect.cli.plugins.project.extension.utils.get_version', - return_value='25.0', - ) +def test_get_pypi_runner_version_http_error(mocked_responses): mocked_responses.add( method='GET', url=PYPI_EXTENSION_RUNNER_URL, @@ -56,22 +54,58 @@ def test_get_pypi_runner_version_http_error(mocker, mocked_responses): assert 'We can not retrieve the current connect-extension-runner version' in str(ve.value) -def test_get_pypi_runner_version_no_releases(mocker, mocked_responses): - mocker.patch( - 'connect.cli.plugins.project.extension.utils.get_version', - return_value='25.0', - ) +def test_get_pypi_runner_eaas_core_version(mocked_responses): + """The extension must pin the connect-eaas-core the runner image ships. + + If the generated pyproject allows a newer connect-eaas-core than the runner supports, + `poetry update` resolves an incompatible version and the extension fails to run. + Confidence: the runner↔eaas-core pair is always coherent, so the extension boots. + """ mocked_responses.add( method='GET', - url=PYPI_EXTENSION_RUNNER_URL, + url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'), status=200, json={ - 'info': {'version': '26.0'}, - 'releases': {}, + 'info': { + 'requires_dist': [ + 'logzio-python-handler>=3.1', + 'connect-eaas-core<38,>=37.4', + ], + }, }, ) - assert get_pypi_runner_version() == '26.0' + assert get_pypi_runner_eaas_core_version('43.0') == '<38,>=37.4' + + +def test_get_pypi_runner_eaas_core_version_http_error(mocked_responses): + mocked_responses.add( + method='GET', + url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'), + status=500, + ) + with pytest.raises(ClickException) as ve: + get_pypi_runner_eaas_core_version('43.0') + + assert 'We can not retrieve the connect-extension-runner 43.0 metadata' in str(ve.value) + + +def test_get_pypi_runner_eaas_core_version_missing_dependency(mocked_responses): + """A runner that omits connect-eaas-core must fail loudly, not silently mis-pin. + + Confidence: we never generate a pyproject with an empty/invalid eaas-core constraint. + """ + mocked_responses.add( + method='GET', + url=PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='43.0'), + status=200, + json={'info': {'requires_dist': ['logzio-python-handler>=3.1']}}, + ) + + with pytest.raises(ClickException) as ve: + get_pypi_runner_eaas_core_version('43.0') + + assert 'does not declare a connect-eaas-core dependency' in str(ve.value) def test_get_event_definitions_client_error(mocker):