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" },