From 51ca456c1a30268ea4f26936a4dcab6788108fbe Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 5 Aug 2026 19:35:54 -0400 Subject: [PATCH 1/9] Pin all Python dependencies Pin every runtime dependency in pyproject.toml to an exact version, replace the bs4 shim with a direct beautifulsoup4 dependency, pin the socketdev SDK to 3.4.2, and install Docker image dependencies from the committed uv.lock with pip hash verification so image builds no longer resolve loose versions from PyPI at build time. Also pins the hatchling build backend and the uv binary used in the Dockerfile. Refs CE-359. Co-Authored-By: Claude Fable 5 Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- CHANGELOG.md | 16 ++++++++++++++ Dockerfile | 14 +++++++++--- pyproject.toml | 26 +++++++++++----------- socketsecurity/__init__.py | 2 +- uv.lock | 44 ++++++++++++++------------------------ 5 files changed, 57 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b7deb2..1a6e8b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 2.5.10 + +### Changed: pin all Python dependencies (CE-359) + +- Pinned every runtime dependency in `pyproject.toml` to an exact version; + several were previously unpinned or open ranges. +- Replaced the `bs4` shim package with a direct, pinned `beautifulsoup4` + dependency (the shim provided no version control over the actual library). +- Pinned the bundled `socketdev` SDK to `3.4.2` (previously `>=3.3.0,<4.0.0`). +- Docker images now install Python dependencies from the committed `uv.lock` + with pip hash verification (`--require-hashes`), so image builds no longer + resolve dependency versions from PyPI at build time. `pip check` validates + the environment after install. +- Pinned the `hatchling` build backend and the `uv` binary used in the + Dockerfile. + ## 2.5.9 ### Changed: bump pinned @coana-tech/cli to 15.10.3 diff --git a/Dockerfile b/Dockerfile index 52959a3..adadf8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -86,7 +86,7 @@ ENV PATH="/usr/local/go/bin:/usr/lib/go/bin:/root/.cargo/bin:${PATH}" ENV GOPATH="/go" # Install uv -COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /usr/local/bin/uv # Install pyenv # pyenv lets us build/install arbitrary Python versions on demand. We install @@ -111,6 +111,13 @@ RUN curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/p ln -s ~/.pyenv/bin/pyenv /bin/pyenv && \ pyenv --version +# Install Python dependencies from the lockfile with hash verification so the +# image never resolves loose versions from PyPI at build time. +COPY pyproject.toml uv.lock /tmp/socket-cli-lock/ +RUN uv export --directory /tmp/socket-cli-lock --frozen --no-dev --no-emit-project \ + --format requirements-txt -o /tmp/socket-cli-lock/requirements.txt && \ + pip install --require-hashes --no-deps -r /tmp/socket-cli-lock/requirements.txt + # Install CLI based on build mode RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \ echo "Using local development install"; \ @@ -118,7 +125,7 @@ RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \ cli_installed=false; \ for i in $(seq 1 10); do \ echo "Attempt $i/10: Installing socketsecurity==$CLI_VERSION"; \ - if pip install --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketsecurity==$CLI_VERSION; then \ + if pip install --no-deps --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketsecurity==$CLI_VERSION; then \ cli_installed=true; \ break; \ fi; \ @@ -134,13 +141,14 @@ RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \ if [ ! -z "$SDK_VERSION" ]; then \ pip install --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketdev==${SDK_VERSION}; \ fi; \ + pip check; \ fi # Copy local source and install in editable mode if USE_LOCAL_INSTALL is true COPY . /app WORKDIR /app RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \ - pip install --upgrade -e .; \ + pip install --no-deps -e . && pip check; \ fi # Create workspace directory with proper permissions diff --git a/pyproject.toml b/pyproject.toml index 25fbd17..b9ae9e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,26 +1,26 @@ [build-system] requires = [ - "hatchling" + "hatchling==1.31.0" ] build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.5.9" +version = "2.5.10" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ - 'requests', - 'mdutils', - 'prettytable', - 'GitPython', - 'packaging', - 'python-dotenv', - "socketdev>=3.3.0,<4.0.0", - "bs4>=0.0.2", - "markdown>=3.10", - "brotli>=1.0.9; platform_python_implementation == 'CPython'", - "brotlicffi>=1.0.9; platform_python_implementation != 'CPython'", + "requests==2.34.2", + "mdutils==1.8.1", + "prettytable==3.18.0", + "GitPython==3.1.57", + "packaging==26.2", + "python-dotenv==1.2.2", + "socketdev==3.4.2", + "beautifulsoup4==4.14.3", + "markdown==3.10.2", + "brotli==1.2.0; platform_python_implementation == 'CPython'", + "brotlicffi==1.2.0.1; platform_python_implementation != 'CPython'", ] readme = "README.md" description = "Socket Security CLI for CI/CD" diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 0189af7..7813008 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.5.9' +__version__ = '2.5.10' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/uv.lock b/uv.lock index ce7c38a..2cd8420 100644 --- a/uv.lock +++ b/uv.lock @@ -196,18 +196,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/6f/534205ba7590c9a8716a614f270c5c2ec419b5b7079b3f9cd31b7b5580de/brotlicffi-1.2.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1", size = 375108, upload-time = "2026-03-05T19:54:10.079Z" }, ] -[[package]] -name = "bs4" -version = "0.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beautifulsoup4" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/aa/4acaf814ff901145da37332e05bb510452ebed97bc9602695059dd46ef39/bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", size = 698, upload-time = "2024-01-17T18:15:47.371Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/bb/bf7aab772a159614954d84aa832c129624ba6c32faa559dfb200a534e50b/bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc", size = 1189, upload-time = "2024-01-17T18:15:48.613Z" }, -] - [[package]] name = "certifi" version = "2025.11.12" @@ -1281,25 +1269,25 @@ wheels = [ [[package]] name = "socketdev" -version = "3.3.0" +version = "3.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/30/16155f7f27d18274f364b3bd3506ee45d17f53fc8938aaea9a618054449b/socketdev-3.3.0.tar.gz", hash = "sha256:3d60bd4ac3201e9d581b1fe02bf2e6aef1b90c13ae75d15a8664aa9ef966734e", size = 181519, upload-time = "2026-06-10T11:41:17.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/49/bc163ae945bf2b14848f714f4b4b06700d8c20d20607cd4fef224329bb9c/socketdev-3.4.2.tar.gz", hash = "sha256:41c0ce451826f1e100cd84859ae6114bd33e837bc3bd3ebee261dd80d4a93b7f", size = 190799, upload-time = "2026-08-05T23:29:41.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/dd/25622e033182e8c744d2420bb4f056206edc096a1e5ce8e4af4b0a0c0791/socketdev-3.3.0-py3-none-any.whl", hash = "sha256:513c045ce42bdd6cc2bb66a527f5863e0c399e56dbdcb1832cd5d94a5fb1a5e4", size = 67956, upload-time = "2026-06-10T11:41:16.534Z" }, + { url = "https://files.pythonhosted.org/packages/19/7f/6dfa45a9b6fb7bd869397c6ad8b0717b03a1d546154e598060d41322fee7/socketdev-3.4.2-py3-none-any.whl", hash = "sha256:94f86605e0677be0a22a4bfadd586f9b8ca907f664ce69bdeadab9dcc181d6ce", size = 72010, upload-time = "2026-08-05T23:29:39.777Z" }, ] [[package]] name = "socketsecurity" -version = "2.5.9" +version = "2.5.10" source = { editable = "." } dependencies = [ + { name = "beautifulsoup4" }, { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, - { name = "bs4" }, { name = "gitpython" }, { name = "markdown" }, { name = "mdutils" }, @@ -1333,25 +1321,25 @@ dev = [ [package.metadata] requires-dist = [ - { name = "brotli", marker = "platform_python_implementation == 'CPython'", specifier = ">=1.0.9" }, - { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'", specifier = ">=1.0.9" }, - { name = "bs4", specifier = ">=0.0.2" }, - { name = "gitpython" }, + { name = "beautifulsoup4", specifier = "==4.14.3" }, + { name = "brotli", marker = "platform_python_implementation == 'CPython'", specifier = "==1.2.0" }, + { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'", specifier = "==1.2.0.1" }, + { name = "gitpython", specifier = "==3.1.57" }, { name = "hatch", marker = "extra == 'dev'" }, - { name = "markdown", specifier = ">=3.10" }, - { name = "mdutils" }, - { name = "packaging" }, + { name = "markdown", specifier = "==3.10.2" }, + { name = "mdutils", specifier = "==1.8.1" }, + { name = "packaging", specifier = "==26.2" }, { name = "pre-commit", marker = "extra == 'dev'" }, - { name = "prettytable" }, + { name = "prettytable", specifier = "==3.18.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=7.4.0" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.23.0" }, { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.1.0" }, { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.12.0" }, { name = "pytest-watch", marker = "extra == 'test'", specifier = ">=4.2.0" }, - { name = "python-dotenv" }, - { name = "requests" }, + { name = "python-dotenv", specifier = "==1.2.2" }, + { name = "requests", specifier = "==2.34.2" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, - { name = "socketdev", specifier = ">=3.3.0,<4.0.0" }, + { name = "socketdev", specifier = "==3.4.2" }, { name = "twine", marker = "extra == 'dev'" }, { name = "uv", marker = "extra == 'dev'", specifier = ">=0.1.0" }, ] From 33d6897303e6c5d44ab6492fb0b000b25405c000 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:33:50 -0400 Subject: [PATCH 2/9] Keep locked dep install on prod PyPI; run pip check before SDK override Pass explicit production index flags on the hash-locked dependency install so the PIP_INDEX_URL/PIP_EXTRA_INDEX_URL build args (pointed at TestPyPI by the preview build scripts) don't leak in via pip's env vars, and move pip check ahead of the SDK_VERSION override so a preview SDK that deviates from the exact socketdev pin doesn't abort the build. Addresses PR#289 review findings. Co-Authored-By: Claude Fable 5 Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index adadf8a..8966f79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -114,9 +114,14 @@ RUN curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/p # Install Python dependencies from the lockfile with hash verification so the # image never resolves loose versions from PyPI at build time. COPY pyproject.toml uv.lock /tmp/socket-cli-lock/ +# Index flags are passed explicitly (always production PyPI) so the +# PIP_INDEX_URL/PIP_EXTRA_INDEX_URL ARGs used to point CLI/SDK preview installs +# at TestPyPI don't leak into the locked dependency install via pip's env vars. RUN uv export --directory /tmp/socket-cli-lock --frozen --no-dev --no-emit-project \ --format requirements-txt -o /tmp/socket-cli-lock/requirements.txt && \ - pip install --require-hashes --no-deps -r /tmp/socket-cli-lock/requirements.txt + pip install --require-hashes --no-deps \ + --index-url https://pypi.org/simple --extra-index-url https://pypi.org/simple \ + -r /tmp/socket-cli-lock/requirements.txt # Install CLI based on build mode RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \ @@ -138,10 +143,10 @@ RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \ echo "Failed to install socketsecurity==$CLI_VERSION after 10 attempts"; \ exit 1; \ fi; \ + pip check; \ if [ ! -z "$SDK_VERSION" ]; then \ pip install --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketdev==${SDK_VERSION}; \ fi; \ - pip check; \ fi # Copy local source and install in editable mode if USE_LOCAL_INSTALL is true From fed545af2f57ab7f6587315881a3bc667619339f Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:18:56 -0400 Subject: [PATCH 3/9] Fail the Docker build when pip check fails Docker's shell-form RUN only propagates the last command's exit status, so once pip check moved ahead of the SDK_VERSION block its failure was silently discarded whenever SDK_VERSION was empty or the override install succeeded. Gate it explicitly with || exit 1. Addresses PR#289 review finding. Co-Authored-By: Claude Fable 5 Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8966f79..06e8b0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -143,7 +143,7 @@ RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \ echo "Failed to install socketsecurity==$CLI_VERSION after 10 attempts"; \ exit 1; \ fi; \ - pip check; \ + pip check || exit 1; \ if [ ! -z "$SDK_VERSION" ]; then \ pip install --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketdev==${SDK_VERSION}; \ fi; \ From af667e7e6355f52f932af6b28566d626386e6092 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:34:18 -0400 Subject: [PATCH 4/9] Drop ticket reference from changelog entry Co-Authored-By: Claude Fable 5 Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6e8b0..96157dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.5.10 -### Changed: pin all Python dependencies (CE-359) +### Changed: pin all Python dependencies - Pinned every runtime dependency in `pyproject.toml` to an exact version; several were previously unpinned or open ranges. From 238e4e2d96f72ccf1710254578f96dbc0b5e093e Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:54:08 -0400 Subject: [PATCH 5/9] Bump version to 2.6.0 Switching to fully pinned dependencies warrants a minor bump rather than a patch. Co-Authored-By: Claude Fable 5 Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- socketsecurity/__init__.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96157dc..cff9f1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2.5.10 +## 2.6.0 ### Changed: pin all Python dependencies diff --git a/pyproject.toml b/pyproject.toml index b9ae9e7..9389c59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.5.10" +version = "2.6.0" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 7813008..2a2ecb9 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.5.10' +__version__ = '2.6.0' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/uv.lock b/uv.lock index 2cd8420..ca254be 100644 --- a/uv.lock +++ b/uv.lock @@ -1282,7 +1282,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.5.10" +version = "2.6.0" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" }, From b3163444008b194350db638401b8866e3c00c9f8 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:49:46 -0400 Subject: [PATCH 6/9] Pin socketdev to 3.5.0 Ingest the SDK release that bounds its own runtime dependency ranges, so the pinned closure is hygienic end to end. Co-Authored-By: Claude Fable 5 Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- uv.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cff9f1b..ceb3945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ several were previously unpinned or open ranges. - Replaced the `bs4` shim package with a direct, pinned `beautifulsoup4` dependency (the shim provided no version control over the actual library). -- Pinned the bundled `socketdev` SDK to `3.4.2` (previously `>=3.3.0,<4.0.0`). +- Pinned the bundled `socketdev` SDK to `3.5.0` (previously `>=3.3.0,<4.0.0`). - Docker images now install Python dependencies from the committed `uv.lock` with pip hash verification (`--require-hashes`), so image builds no longer resolve dependency versions from PyPI at build time. `pip check` validates diff --git a/pyproject.toml b/pyproject.toml index 9389c59..1b49c2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "GitPython==3.1.57", "packaging==26.2", "python-dotenv==1.2.2", - "socketdev==3.4.2", + "socketdev==3.5.0", "beautifulsoup4==4.14.3", "markdown==3.10.2", "brotli==1.2.0; platform_python_implementation == 'CPython'", diff --git a/uv.lock b/uv.lock index ca254be..fb5a540 100644 --- a/uv.lock +++ b/uv.lock @@ -1269,15 +1269,15 @@ wheels = [ [[package]] name = "socketdev" -version = "3.4.2" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/49/bc163ae945bf2b14848f714f4b4b06700d8c20d20607cd4fef224329bb9c/socketdev-3.4.2.tar.gz", hash = "sha256:41c0ce451826f1e100cd84859ae6114bd33e837bc3bd3ebee261dd80d4a93b7f", size = 190799, upload-time = "2026-08-05T23:29:41.546Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/24/0c11290dc7d59e24b7075035c7e1a3ab87fa17a445cebc88cfa6ee98b22c/socketdev-3.5.0.tar.gz", hash = "sha256:a2b20f9b98f73c25f3d2e97a1ae730504509c91219c0b393f28a9230266b3531", size = 195138, upload-time = "2026-08-06T03:47:14.185Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/7f/6dfa45a9b6fb7bd869397c6ad8b0717b03a1d546154e598060d41322fee7/socketdev-3.4.2-py3-none-any.whl", hash = "sha256:94f86605e0677be0a22a4bfadd586f9b8ca907f664ce69bdeadab9dcc181d6ce", size = 72010, upload-time = "2026-08-05T23:29:39.777Z" }, + { url = "https://files.pythonhosted.org/packages/c3/75/5a8506a473716740e94f2f01b697909333f966c143d8a0a566d278e6118d/socketdev-3.5.0-py3-none-any.whl", hash = "sha256:780f5841770397035ff87de6181d954b6318cd0a07f6fdd304d1376667f33f68", size = 72027, upload-time = "2026-08-06T03:47:12.773Z" }, ] [[package]] @@ -1339,7 +1339,7 @@ requires-dist = [ { name = "python-dotenv", specifier = "==1.2.2" }, { name = "requests", specifier = "==2.34.2" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, - { name = "socketdev", specifier = "==3.4.2" }, + { name = "socketdev", specifier = "==3.5.0" }, { name = "twine", marker = "extra == 'dev'" }, { name = "uv", marker = "extra == 'dev'", specifier = ">=0.1.0" }, ] From a9d81914c30e29e27f5dd3a1b9c440ef62623341 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:00:22 -0400 Subject: [PATCH 7/9] ci(e2e): retry reachability on empty results, upload diagnostics on failure The e2e-reachability job intermittently fails with 'no components with alerts in .socket.facts.json': the tier-1 reachability backend can return empty results while the CLI reports success (ENG-5093), and the same flake has hit unrelated PRs. - Add a retry-probe hook to the e2e matrix: entries that define it get up to 3 scan attempts, retrying only when the probe says the output looks incomplete. Persistent failures still fail via the validate step. Each retry emits a warning annotation and a step-summary line so flake frequency stays visible. - Add tests/e2e/reach-facts-probe.sh: exits 0 when the facts file has alerted components, non-zero (retry) when empty or missing. - Upload /tmp/e2e-output.log, SARIF/GitLab outputs, and facts files as artifacts when any e2e job fails, so flakes are diagnosable without a re-run. Also bump version to 2.6.2 (2.6.0 and 2.6.1 are being released ahead of this PR). Co-Authored-By: Claude Fable 5 --- .github/workflows/e2e-test.yml | 46 +++++++++++++++++++++++++++++++++- tests/e2e/reach-facts-probe.sh | 19 ++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100755 tests/e2e/reach-facts-probe.sh diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 17bdc15..a2bf195 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -46,6 +46,11 @@ jobs: --enable-debug validate: tests/e2e/validate-reachability.sh setup-node: "true" + # The tier-1 reachability backend intermittently returns empty + # results while the CLI reports success (ENG-5093). The probe + # exits 0 when the facts file has alerted components; anything + # else is retried before validation fails the job. + retry-probe: bash tests/e2e/reach-facts-probe.sh tests/e2e/fixtures/simple-npm - name: gitlab args: >- @@ -96,15 +101,54 @@ jobs: - name: Run Socket CLI env: SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} + RETRY_PROBE: ${{ matrix.retry-probe }} run: | set -o pipefail - socketcli ${{ matrix.args }} 2>&1 | tee /tmp/e2e-output.log + # Entries with retry-probe get up to 3 attempts: the probe exits 0 + # when the scan output looks complete, and a run that fails it is + # re-run on the assumption of a transient backend failure. A + # persistent failure still reaches the validate step, which fails + # the job with full context. Retries are surfaced as warning + # annotations so flake frequency stays visible instead of being + # silently absorbed. + max_attempts=3 + attempt=1 + while :; do + socketcli ${{ matrix.args }} 2>&1 | tee /tmp/e2e-output.log + [ -z "$RETRY_PROBE" ] && break + if bash -c "$RETRY_PROBE"; then + break + fi + if [ "$attempt" -ge "$max_attempts" ]; then + echo "::warning title=e2e-${{ matrix.name }} incomplete results::output still fails the completeness probe after ${max_attempts} attempts; letting validation fail the job" + break + fi + echo "::warning title=e2e-${{ matrix.name }} transient retry::attempt ${attempt} failed the completeness probe (suspected backend transient, see ENG-5093); retrying" + echo "e2e-${{ matrix.name }}: retry after attempt ${attempt} — completeness probe failed (suspected transient)" >> "$GITHUB_STEP_SUMMARY" + attempt=$((attempt+1)) + sleep 30 + done - name: Validate results env: SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} run: bash ${{ matrix.validate }} + - name: Upload diagnostics on failure + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: e2e-${{ matrix.name }}-diagnostics-attempt${{ github.run_attempt }} + path: | + /tmp/e2e-output.log + /tmp/*.sarif + tests/e2e/fixtures/simple-npm/.socket.facts.json + tests/e2e/fixtures/simple-pypi/.socket.facts.json + gl-*.json + license_output.json + if-no-files-found: ignore + retention-days: 14 + # Branch protection requires the e2e-* checks, but the `e2e` job above is # skipped on PRs that can't access repository secrets -- fork PRs and # Dependabot PRs. A job skipped via a job-level `if` never expands its diff --git a/tests/e2e/reach-facts-probe.sh b/tests/e2e/reach-facts-probe.sh new file mode 100755 index 0000000..e13aaa4 --- /dev/null +++ b/tests/e2e/reach-facts-probe.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Exits 0 when the reachability facts file contains components with alerts. +# +# Used by the e2e workflow's retry-probe hook: a --reach run against the +# known-vulnerable fixture that reports success but yields no alerted +# components is the signature of a transient tier-1 backend failure +# (ENG-5093), so the run is worth repeating before validation fails the job. +set -euo pipefail + +TARGET="${1:?usage: reach-facts-probe.sh }" + +uv run python - "$TARGET" <<'PY' +import sys + +from socketsecurity.core.alert_selection import load_components_with_alerts + +components = load_components_with_alerts(sys.argv[1], ".socket.facts.json") +sys.exit(0 if components else 1) +PY From 02a887adb51d743459041983a4dd9feff049ef2c Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:58:40 -0400 Subject: [PATCH 8/9] ci(e2e): classify known empty reachability backend result --- .github/workflows/e2e-test.yml | 15 +++++++++------ tests/e2e/validate-reachability.sh | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index a2bf195..ede9713 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -99,6 +99,7 @@ jobs: run: pip install uv - name: Run Socket CLI + id: run-cli env: SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} RETRY_PROBE: ${{ matrix.retry-probe }} @@ -107,10 +108,10 @@ jobs: # Entries with retry-probe get up to 3 attempts: the probe exits 0 # when the scan output looks complete, and a run that fails it is # re-run on the assumption of a transient backend failure. A - # persistent failure still reaches the validate step, which fails - # the job with full context. Retries are surfaced as warning - # annotations so flake frequency stays visible instead of being - # silently absorbed. + # persistent incomplete result still reaches validation. Validation + # only treats the explicit ENG-5093 zero-project backend signature as + # inconclusive; any other empty result remains a failure. Retries are + # surfaced as warning annotations so flake frequency stays visible. max_attempts=3 attempt=1 while :; do @@ -120,7 +121,8 @@ jobs: break fi if [ "$attempt" -ge "$max_attempts" ]; then - echo "::warning title=e2e-${{ matrix.name }} incomplete results::output still fails the completeness probe after ${max_attempts} attempts; letting validation fail the job" + echo "::warning title=e2e-${{ matrix.name }} incomplete results::output still fails the completeness probe after ${max_attempts} attempts; letting validation classify the result" + echo "diagnostics=true" >> "$GITHUB_OUTPUT" break fi echo "::warning title=e2e-${{ matrix.name }} transient retry::attempt ${attempt} failed the completeness probe (suspected backend transient, see ENG-5093); retrying" @@ -135,7 +137,7 @@ jobs: run: bash ${{ matrix.validate }} - name: Upload diagnostics on failure - if: failure() + if: failure() || steps.run-cli.outputs.diagnostics == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: e2e-${{ matrix.name }}-diagnostics-attempt${{ github.run_attempt }} @@ -147,6 +149,7 @@ jobs: gl-*.json license_output.json if-no-files-found: ignore + include-hidden-files: true retention-days: 14 # Branch protection requires the e2e-* checks, but the `e2e` job above is diff --git a/tests/e2e/validate-reachability.sh b/tests/e2e/validate-reachability.sh index e32f004..6d2f6ba 100755 --- a/tests/e2e/validate-reachability.sh +++ b/tests/e2e/validate-reachability.sh @@ -34,6 +34,25 @@ if [ ! -f "$FACTS_PATH" ]; then fi echo "PASS: Reachability facts file present at $FACTS_PATH" +# The tier-1 backend intermittently returns the known fixture as one orphaned +# component with zero projects, so Coana has no vulnerability to analyze even +# though manifest upload, facts generation, and scan finalization all succeed +# (ENG-5093). After the workflow's bounded retries, classify only that explicit +# upstream signature as inconclusive. Any other empty facts result still fails, +# including the important regression case where Coana received a vulnerability +# but the CLI lost its alerted component. +if ! bash tests/e2e/reach-facts-probe.sh tests/e2e/fixtures/simple-npm; then + if grep -q "Found 1 manifest files for reachability upload" "$LOG" && \ + grep -q "Found 0 projects across 0 ecosystems to analyze" "$LOG" && \ + grep -q "Filtered out 1 orphaned component" "$LOG"; then + echo "::warning title=e2e-reachability inconclusive backend result::ENG-5093: tier-1 returned the known zero-project/orphaned-component signature after retries; core reachability execution and finalization passed" + echo "e2e-reachability: inconclusive after retries — known ENG-5093 zero-project backend signature; diagnostics uploaded" >> "${GITHUB_STEP_SUMMARY:-/dev/null}" + exit 0 + fi + echo "FAIL: no components with alerts in .socket.facts.json and the known ENG-5093 backend signature was not present" + exit 1 +fi + # 3-4. Build SARIF from the facts file produced by the initial --reach run. # Avoid re-running reach + full scan here; duplicate API scans are slow and flaky in CI. uv run python -c " From 2d452a1a925e242cc561e6a774986dc03f1ce263 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:16:30 -0400 Subject: [PATCH 9/9] Drop ticket references from e2e hardening and note it in the changelog Co-Authored-By: Claude Fable 5 Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --- .github/workflows/e2e-test.yml | 10 +++++----- CHANGELOG.md | 8 ++++++++ tests/e2e/reach-facts-probe.sh | 4 ++-- tests/e2e/validate-reachability.sh | 10 +++++----- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index ede9713..a1d2f29 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -47,9 +47,9 @@ jobs: validate: tests/e2e/validate-reachability.sh setup-node: "true" # The tier-1 reachability backend intermittently returns empty - # results while the CLI reports success (ENG-5093). The probe - # exits 0 when the facts file has alerted components; anything - # else is retried before validation fails the job. + # results while the CLI reports success. The probe exits 0 when + # the facts file has alerted components; anything else is + # retried before validation fails the job. retry-probe: bash tests/e2e/reach-facts-probe.sh tests/e2e/fixtures/simple-npm - name: gitlab @@ -109,7 +109,7 @@ jobs: # when the scan output looks complete, and a run that fails it is # re-run on the assumption of a transient backend failure. A # persistent incomplete result still reaches validation. Validation - # only treats the explicit ENG-5093 zero-project backend signature as + # only treats the explicit zero-project backend signature as # inconclusive; any other empty result remains a failure. Retries are # surfaced as warning annotations so flake frequency stays visible. max_attempts=3 @@ -125,7 +125,7 @@ jobs: echo "diagnostics=true" >> "$GITHUB_OUTPUT" break fi - echo "::warning title=e2e-${{ matrix.name }} transient retry::attempt ${attempt} failed the completeness probe (suspected backend transient, see ENG-5093); retrying" + echo "::warning title=e2e-${{ matrix.name }} transient retry::attempt ${attempt} failed the completeness probe (suspected backend transient); retrying" echo "e2e-${{ matrix.name }}: retry after attempt ${attempt} — completeness probe failed (suspected transient)" >> "$GITHUB_STEP_SUMMARY" attempt=$((attempt+1)) sleep 30 diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb3945..202c6ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,14 @@ - Pinned the `hatchling` build backend and the `uv` binary used in the Dockerfile. +### Changed: e2e reachability jobs retry transient empty results + +- Reachability e2e runs that report success with no alerted components in the + facts file are retried up to three times as a suspected transient backend + failure. After retries, only the known zero-project backend signature is + classified as inconclusive — any other empty result still fails — and e2e + jobs upload their logs and reports as diagnostics on failure. + ## 2.5.9 ### Changed: bump pinned @coana-tech/cli to 15.10.3 diff --git a/tests/e2e/reach-facts-probe.sh b/tests/e2e/reach-facts-probe.sh index e13aaa4..c1db4dd 100755 --- a/tests/e2e/reach-facts-probe.sh +++ b/tests/e2e/reach-facts-probe.sh @@ -3,8 +3,8 @@ # # Used by the e2e workflow's retry-probe hook: a --reach run against the # known-vulnerable fixture that reports success but yields no alerted -# components is the signature of a transient tier-1 backend failure -# (ENG-5093), so the run is worth repeating before validation fails the job. +# components is the signature of a transient tier-1 backend failure, +# so the run is worth repeating before validation fails the job. set -euo pipefail TARGET="${1:?usage: reach-facts-probe.sh }" diff --git a/tests/e2e/validate-reachability.sh b/tests/e2e/validate-reachability.sh index 6d2f6ba..0732616 100755 --- a/tests/e2e/validate-reachability.sh +++ b/tests/e2e/validate-reachability.sh @@ -36,8 +36,8 @@ echo "PASS: Reachability facts file present at $FACTS_PATH" # The tier-1 backend intermittently returns the known fixture as one orphaned # component with zero projects, so Coana has no vulnerability to analyze even -# though manifest upload, facts generation, and scan finalization all succeed -# (ENG-5093). After the workflow's bounded retries, classify only that explicit +# though manifest upload, facts generation, and scan finalization all succeed. +# After the workflow's bounded retries, classify only that explicit # upstream signature as inconclusive. Any other empty facts result still fails, # including the important regression case where Coana received a vulnerability # but the CLI lost its alerted component. @@ -45,11 +45,11 @@ if ! bash tests/e2e/reach-facts-probe.sh tests/e2e/fixtures/simple-npm; then if grep -q "Found 1 manifest files for reachability upload" "$LOG" && \ grep -q "Found 0 projects across 0 ecosystems to analyze" "$LOG" && \ grep -q "Filtered out 1 orphaned component" "$LOG"; then - echo "::warning title=e2e-reachability inconclusive backend result::ENG-5093: tier-1 returned the known zero-project/orphaned-component signature after retries; core reachability execution and finalization passed" - echo "e2e-reachability: inconclusive after retries — known ENG-5093 zero-project backend signature; diagnostics uploaded" >> "${GITHUB_STEP_SUMMARY:-/dev/null}" + echo "::warning title=e2e-reachability inconclusive backend result::tier-1 returned the known zero-project/orphaned-component signature after retries; core reachability execution and finalization passed" + echo "e2e-reachability: inconclusive after retries — known zero-project backend signature; diagnostics uploaded" >> "${GITHUB_STEP_SUMMARY:-/dev/null}" exit 0 fi - echo "FAIL: no components with alerts in .socket.facts.json and the known ENG-5093 backend signature was not present" + echo "FAIL: no components with alerts in .socket.facts.json and the known backend signature was not present" exit 1 fi