diff --git a/connect/cli/plugins/project/extension/helpers.py b/connect/cli/plugins/project/extension/helpers.py index 9bb9618..94246fe 100644 --- a/connect/cli/plugins/project/extension/helpers.py +++ b/connect/cli/plugins/project/extension/helpers.py @@ -14,7 +14,6 @@ get_event_definitions, get_pypi_runner_requirements, get_pypi_runner_version, - get_pypi_runner_version_by_connect_version, initialize_git_repository, ) from connect.cli.plugins.project.extension.wizard import ( @@ -246,11 +245,37 @@ def _update_docker_file_runner_from(dockerfile: str, latest_version: str): return to_be_replaced +def _update_pyproject_runner_deps(pyproject_file, latest_version): + connect_eaas_core_version, python_version = get_pypi_runner_requirements(latest_version) + new_values = {'python': python_version, 'connect-eaas-core': connect_eaas_core_version} + to_be_replaced = False + section = None + lines = [] + with open(pyproject_file, 'r') as f: + for line in f.readlines(): + stripped = line.strip() + if stripped.startswith('['): + section = stripped + elif section == '[tool.poetry.dependencies]': + # ponytail: handles the plain `name = "spec"` form bootstrap emits; a + # table form ({ version = ... }) gets rewritten to the plain form + key = stripped.split('=', 1)[0].strip() + new_value = new_values.get(key) + if new_value and stripped != f'{key} = "{new_value}"': + line = f'{key} = "{new_value}"\n' + to_be_replaced = True + lines.append(line) + if to_be_replaced: + with open(pyproject_file, 'w') as f: + f.writelines(lines) + return to_be_replaced + + def bump_runner_extension_project(project_dir: str): # noqa: CCR001 console.secho(f'Bumping runner version on project {project_dir}...\n', fg='blue') updated_files = set() - latest_version = get_pypi_runner_version_by_connect_version() + latest_version = get_pypi_runner_version() latest_runner_version = f'cloudblueconnect/connect-extension-runner:{latest_version}' docker_compose_file = os.path.join(project_dir, 'docker-compose.yml') if not os.path.isfile(docker_compose_file): @@ -294,6 +319,13 @@ def bump_runner_extension_project(project_dir: str): # noqa: CCR001 f'Error: {error}', ) + pyproject_file = os.path.join(project_dir, 'pyproject.toml') + if os.path.isfile(pyproject_file) and _update_pyproject_runner_deps( + pyproject_file, + latest_version, + ): + updated_files.add(pyproject_file) + if updated_files: console.secho( f'Runner version has been successfully updated to {latest_version}. The following ' diff --git a/connect/cli/plugins/project/extension/utils.py b/connect/cli/plugins/project/extension/utils.py index 93dd9a0..f3a592c 100644 --- a/connect/cli/plugins/project/extension/utils.py +++ b/connect/cli/plugins/project/extension/utils.py @@ -7,10 +7,6 @@ from connect.client import ClientError from packaging.requirements import Requirement -from connect.cli.core.utils import ( - get_connect_version, - get_last_version_by_major, -) from connect.cli.plugins.project.extension.constants import ( PRE_COMMIT_HOOK, PYPI_EXTENSION_RUNNER_RELEASE_URL, @@ -62,18 +58,6 @@ def get_pypi_runner_requirements(runner_version): ) -def get_pypi_runner_version_by_connect_version(): - connect_version = get_connect_version() - res = requests.get(PYPI_EXTENSION_RUNNER_URL) - if res.status_code != 200 or not connect_version: - raise ClickException( - f'We can not retrieve the current connect-extension-runner version from {PYPI_EXTENSION_RUNNER_URL}.', - ) - content = res.json() - version = get_last_version_by_major(content['releases'], connect_version.split('.', 1)[0]) - return version or content['info']['version'] - - def get_extension_types(config): if config.active.is_provider(): extension_types = [('hub', 'Hub integration')] diff --git a/tests/plugins/project/test_extension_helpers.py b/tests/plugins/project/test_extension_helpers.py index 2b925d0..e8ec698 100644 --- a/tests/plugins/project/test_extension_helpers.py +++ b/tests/plugins/project/test_extension_helpers.py @@ -11,8 +11,10 @@ from click import ClickException from flake8.api import legacy as flake8 -from connect.cli.core.constants import DEFAULT_ENDPOINT -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.helpers import ( bootstrap_extension_project, bump_runner_extension_project, @@ -918,17 +920,28 @@ def test_bootstrap_extension_project_if_destination_exists(mocker): assert 'Answers cannot be saved' in str(cv.value) -def test_bump_runner_version(mocker, mocked_responses, capsys): +def test_bump_runner_version(mocked_responses, capsys): + """`bump` must move the project to the latest published runner and keep the + pyproject pins (connect-eaas-core, python) in lockstep with it. + + Bumping only the docker image would recreate the bootstrap mismatch: a new + runner with stale pyproject pins fails to resolve/run after `poetry update`. + Confidence: one bump leaves the whole project coherent with the runner. + """ mocked_responses.add( 'GET', - DEFAULT_ENDPOINT, - status=401, - headers={'Connect-Version': '1.0.1-abc'}, + PYPI_EXTENSION_RUNNER_URL, + json={'info': {'version': '1.0'}}, ) mocked_responses.add( 'GET', - PYPI_EXTENSION_RUNNER_URL, - json={'releases': {'1.0': ['release info']}}, + PYPI_EXTENSION_RUNNER_RELEASE_URL.format(version='1.0'), + json={ + 'info': { + 'requires_python': '<3.13,>=3.9', + 'requires_dist': ['connect-eaas-core<38,>=37.4'], + }, + }, ) with tempfile.TemporaryDirectory() as tmp_data: @@ -957,6 +970,18 @@ def test_bump_runner_version(mocker, mocked_responses, capsys): fp.write('FROM cloudblueconnect/connect-extension-runner:0.5') with open(f'{project_dir}/OtherDockerfile', 'w') as fp: fp.write('FROM cloudblueconnect/connect-extension-runner:0.5') + with open(f'{project_dir}/pyproject.toml', 'w') as fp: + fp.write( + '[tool.poetry]\n' + 'name = "myext"\n' + '\n' + '[tool.poetry.dependencies]\n' + 'python = ">=3.8,<4"\n' + 'connect-eaas-core = ">=30"\n' + '\n' + '[tool.poetry.dev-dependencies]\n' + 'pytest = ">=6.1.2,<8"\n', + ) bump_runner_extension_project(project_dir) captured = capsys.readouterr() captured_out = ''.join(captured.out.split('\n')) @@ -964,6 +989,13 @@ def test_bump_runner_version(mocker, mocked_responses, capsys): assert f'{os.path.join(project_dir, "docker-compose.yml")}' in captured_out assert f'{os.path.join(project_dir, "OtherDockerfile")}' in captured_out assert f'{os.path.join(project_dir, "Dockerfile")}' in captured_out + assert f'{os.path.join(project_dir, "pyproject.toml")}' in captured_out + + pyproject = open(f'{project_dir}/pyproject.toml').read() + assert 'python = "<3.13,>=3.9"' in pyproject + assert 'connect-eaas-core = "<38,>=37.4"' in pyproject + # dev-dependencies section stays untouched + assert 'pytest = ">=6.1.2,<8"' in pyproject def test_bump_runner_version_no_update_required(mocker, capsys): @@ -1047,8 +1079,7 @@ def test_bump_runner_docker_yaml_error(mocker): assert 'not properly formatted' in str(error.value) -def test_bump_runner_get_version_error(mocker, mocked_responses): - mocker.patch('connect.cli.plugins.project.extension.utils.get_connect_version') +def test_bump_runner_get_version_error(mocked_responses): mocked_responses.add('GET', PYPI_EXTENSION_RUNNER_URL, status=400) with pytest.raises(ClickException) as error: bump_runner_extension_project('project_dir') @@ -1057,7 +1088,7 @@ def test_bump_runner_get_version_error(mocker, mocked_responses): def _mock_pypi_version(mocker): mocker.patch( - 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version_by_connect_version', + 'connect.cli.plugins.project.extension.helpers.get_pypi_runner_version', return_value='1.0', )