From 21a802db6e3481247476d034495ac8bca8ecc8a2 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:20:40 -0400 Subject: [PATCH] Install the pinned reachability engine in the Docker image The image installed @coana-tech/cli unpinned while the launcher asks npx for DEFAULT_COANA_CLI_VERSION. npx reuses the global install only when the versions match, so once the two diverged every scan re-downloaded the engine (~119 MB) and the launcher took roughly three times as long, silently. The Dockerfile now reads the pinned version out of reachability.py, so the image and the runtime cannot drift and the pin stays bumped in exactly one place. Splitting the coana install out of the toolchain RUN keeps a pin bump to a 353 MB layer rather than rebuilding the 2.3 GB combined layer. Marking a release stable now builds that version from its own tag, so an older wheel is no longer paired with the default branch's build recipe and pin. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/docker-stable.yml | 5 +++ .github/workflows/python-tests.yml | 2 + CHANGELOG.md | 12 ++++++ Dockerfile | 23 ++++++++++-- pyproject.toml | 2 +- socketsecurity/__init__.py | 2 +- tests/unit/test_dockerfile_coana_pin.py | 50 +++++++++++++++++++++++++ uv.lock | 2 +- 8 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 tests/unit/test_dockerfile_coana_pin.py diff --git a/.github/workflows/docker-stable.yml b/.github/workflows/docker-stable.yml index f2a07032..d1c1c7d4 100644 --- a/.github/workflows/docker-stable.yml +++ b/.github/workflows/docker-stable.yml @@ -15,6 +15,11 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # Build the requested version from its own tag. The image installs + # socketsecurity==inputs.version from PyPI, and the Dockerfile reads the pinned + # @coana-tech/cli version out of the checked-out source, so building from the + # default branch would pair an old wheel with a newer build recipe and pin. + ref: v${{ inputs.version }} persist-credentials: false - name: Check if version exists in PyPI diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 34717226..d75707fc 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -12,6 +12,7 @@ on: - "tests/core/**/*.py" - "pyproject.toml" - "uv.lock" + - "Dockerfile" - ".github/workflows/python-tests.yml" pull_request: paths: @@ -20,6 +21,7 @@ on: - "tests/core/**/*.py" - "pyproject.toml" - "uv.lock" + - "Dockerfile" - ".github/workflows/python-tests.yml" workflow_dispatch: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1007a3b9..2a5678f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 2.7.2 + +### Fixed: the image now installs the pinned reachability engine + +- The Docker image installed `@coana-tech/cli` unpinned while the CLI asks npx for the + version in `DEFAULT_COANA_CLI_VERSION`. npx reuses the image's global install only when + the versions match, so once they diverged every scan downloaded the engine again. The + Dockerfile now reads the pinned version out of the source, keeping the image and the + runtime aligned without a second place to bump. +- Marking a release stable now builds that version from its own tag instead of the default + branch, so an older release is rebuilt with its own pin and build recipe. + ## 2.7.1 ### Changed: bump pinned @coana-tech/cli to 15.10.36 diff --git a/Dockerfile b/Dockerfile index 06e8b0a0..a06ca94d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -74,13 +74,30 @@ RUN if [ "$TARGETARCH" = "amd64" ]; then \ pypy3 --version; \ fi -# Install additional tools -RUN npm install @coana-tech/cli socket -g && \ - gem install bundler && \ +# Install Ruby bundler and the Rust toolchain +RUN gem install bundler && \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ . ~/.cargo/env && \ rustup component add rustfmt clippy +# Install the reachability engine at exactly the version the CLI will ask for. +# The launcher runs `npx @coana-tech/cli@`, and npx reuses this +# global install only when the versions match; on a mismatch it downloads the engine again +# on every scan. Reading the version out of reachability.py keeps the image and the runtime +# aligned by construction, so the pin stays bumped in exactly one place. +# reachability.py is copied on its own so bumping the pin rebuilds only this layer rather +# than the toolchain layer above it. +COPY socketsecurity/core/tools/reachability.py /tmp/coana-pin/reachability.py +RUN COANA_CLI_VERSION="$(sed -n 's/^DEFAULT_COANA_CLI_VERSION[^"]*"\([^"]*\)".*/\1/p' \ + /tmp/coana-pin/reachability.py)" && \ + if [ -z "$COANA_CLI_VERSION" ]; then \ + echo "Could not read DEFAULT_COANA_CLI_VERSION from reachability.py" >&2; \ + exit 1; \ + fi && \ + echo "Installing @coana-tech/cli@${COANA_CLI_VERSION} (pinned by reachability.py)" && \ + npm install "@coana-tech/cli@${COANA_CLI_VERSION}" socket -g && \ + rm -rf /tmp/coana-pin + # Set environment paths ENV PATH="/usr/local/go/bin:/usr/lib/go/bin:/root/.cargo/bin:${PATH}" ENV GOPATH="/go" diff --git a/pyproject.toml b/pyproject.toml index 3dcd5718..c848f2f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.7.1" +version = "2.7.2" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 78220a1f..9a275469 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.7.1' +__version__ = '2.7.2' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/tests/unit/test_dockerfile_coana_pin.py b/tests/unit/test_dockerfile_coana_pin.py new file mode 100644 index 00000000..5e851e78 --- /dev/null +++ b/tests/unit/test_dockerfile_coana_pin.py @@ -0,0 +1,50 @@ +"""Guards the image's ``@coana-tech/cli`` install against drifting from the runtime pin. + +The launcher asks npx for ``@coana-tech/cli@DEFAULT_COANA_CLI_VERSION``, and npx reuses the +image's global install only when the versions match; on a mismatch it downloads the engine +again on every scan. The Dockerfile therefore derives the version from ``reachability.py`` +with a ``sed`` expression instead of repeating it. These tests run that expression against +the real source, so a reformatted constant or a broken expression fails here rather than +silently producing a mismatched image. +""" + +import re +import subprocess +from pathlib import Path + +from socketsecurity.core.tools.reachability import DEFAULT_COANA_CLI_VERSION + +REPO_ROOT = Path(__file__).resolve().parents[2] +DOCKERFILE = REPO_ROOT / "Dockerfile" +REACHABILITY_SOURCE = ( + REPO_ROOT / "socketsecurity" / "core" / "tools" / "reachability.py" +) + + +def _dockerfile_sed_expression() -> str: + """Return the pin-extraction expression the Dockerfile runs.""" + match = re.search( + r"sed -n '(s/\^DEFAULT_COANA_CLI_VERSION[^']*)'", DOCKERFILE.read_text() + ) + assert match, "Dockerfile no longer extracts DEFAULT_COANA_CLI_VERSION with sed" + return match.group(1) + + +def test_dockerfile_expression_extracts_the_pinned_version(): + extracted = subprocess.run( + ["sed", "-n", _dockerfile_sed_expression(), str(REACHABILITY_SOURCE)], + capture_output=True, + text=True, + check=True, + ).stdout.strip() + assert extracted == DEFAULT_COANA_CLI_VERSION + + +def test_dockerfile_never_installs_coana_unpinned(): + install_lines = [ + line for line in DOCKERFILE.read_text().splitlines() if "npm install" in line + ] + assert install_lines, "Dockerfile no longer installs anything with npm" + for line in install_lines: + if "@coana-tech/cli" in line: + assert "@coana-tech/cli@" in line, f"unpinned coana install: {line.strip()}" diff --git a/uv.lock b/uv.lock index d0338009..03044024 100644 --- a/uv.lock +++ b/uv.lock @@ -1282,7 +1282,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.7.1" +version = "2.7.2" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" },