Skip to content

Commit 72bf0c2

Browse files
leliaclaude
andauthored
Pin all Python dependencies (#289)
* 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 <noreply@anthropic.com> Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> * 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 <noreply@anthropic.com> Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> * 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 <noreply@anthropic.com> Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> * Drop ticket reference from changelog entry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> * 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 <noreply@anthropic.com> Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> * 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 <noreply@anthropic.com> Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> * 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 <noreply@anthropic.com> * ci(e2e): classify known empty reachability backend result * Drop ticket references from e2e hardening and note it in the changelog Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> --------- Signed-off-by: lelia <2418071+lelia@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 0b577a9 commit 72bf0c2

8 files changed

Lines changed: 156 additions & 46 deletions

File tree

.github/workflows/e2e-test.yml

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ jobs:
4646
--enable-debug
4747
validate: tests/e2e/validate-reachability.sh
4848
setup-node: "true"
49+
# The tier-1 reachability backend intermittently returns empty
50+
# results while the CLI reports success. The probe exits 0 when
51+
# the facts file has alerted components; anything else is
52+
# retried before validation fails the job.
53+
retry-probe: bash tests/e2e/reach-facts-probe.sh tests/e2e/fixtures/simple-npm
4954

5055
- name: gitlab
5156
args: >-
@@ -94,17 +99,59 @@ jobs:
9499
run: pip install uv
95100

96101
- name: Run Socket CLI
102+
id: run-cli
97103
env:
98104
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }}
105+
RETRY_PROBE: ${{ matrix.retry-probe }}
99106
run: |
100107
set -o pipefail
101-
socketcli ${{ matrix.args }} 2>&1 | tee /tmp/e2e-output.log
108+
# Entries with retry-probe get up to 3 attempts: the probe exits 0
109+
# when the scan output looks complete, and a run that fails it is
110+
# re-run on the assumption of a transient backend failure. A
111+
# persistent incomplete result still reaches validation. Validation
112+
# only treats the explicit zero-project backend signature as
113+
# inconclusive; any other empty result remains a failure. Retries are
114+
# surfaced as warning annotations so flake frequency stays visible.
115+
max_attempts=3
116+
attempt=1
117+
while :; do
118+
socketcli ${{ matrix.args }} 2>&1 | tee /tmp/e2e-output.log
119+
[ -z "$RETRY_PROBE" ] && break
120+
if bash -c "$RETRY_PROBE"; then
121+
break
122+
fi
123+
if [ "$attempt" -ge "$max_attempts" ]; then
124+
echo "::warning title=e2e-${{ matrix.name }} incomplete results::output still fails the completeness probe after ${max_attempts} attempts; letting validation classify the result"
125+
echo "diagnostics=true" >> "$GITHUB_OUTPUT"
126+
break
127+
fi
128+
echo "::warning title=e2e-${{ matrix.name }} transient retry::attempt ${attempt} failed the completeness probe (suspected backend transient); retrying"
129+
echo "e2e-${{ matrix.name }}: retry after attempt ${attempt} — completeness probe failed (suspected transient)" >> "$GITHUB_STEP_SUMMARY"
130+
attempt=$((attempt+1))
131+
sleep 30
132+
done
102133
103134
- name: Validate results
104135
env:
105136
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }}
106137
run: bash ${{ matrix.validate }}
107138

139+
- name: Upload diagnostics on failure
140+
if: failure() || steps.run-cli.outputs.diagnostics == 'true'
141+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
142+
with:
143+
name: e2e-${{ matrix.name }}-diagnostics-attempt${{ github.run_attempt }}
144+
path: |
145+
/tmp/e2e-output.log
146+
/tmp/*.sarif
147+
tests/e2e/fixtures/simple-npm/.socket.facts.json
148+
tests/e2e/fixtures/simple-pypi/.socket.facts.json
149+
gl-*.json
150+
license_output.json
151+
if-no-files-found: ignore
152+
include-hidden-files: true
153+
retention-days: 14
154+
108155
# Branch protection requires the e2e-* checks, but the `e2e` job above is
109156
# skipped on PRs that can't access repository secrets -- fork PRs and
110157
# Dependabot PRs. A job skipped via a job-level `if` never expands its

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# Changelog
22

3+
## 2.6.0
4+
5+
### Changed: pin all Python dependencies
6+
7+
- Pinned every runtime dependency in `pyproject.toml` to an exact version;
8+
several were previously unpinned or open ranges.
9+
- Replaced the `bs4` shim package with a direct, pinned `beautifulsoup4`
10+
dependency (the shim provided no version control over the actual library).
11+
- Pinned the bundled `socketdev` SDK to `3.5.0` (previously `>=3.3.0,<4.0.0`).
12+
- Docker images now install Python dependencies from the committed `uv.lock`
13+
with pip hash verification (`--require-hashes`), so image builds no longer
14+
resolve dependency versions from PyPI at build time. `pip check` validates
15+
the environment after install.
16+
- Pinned the `hatchling` build backend and the `uv` binary used in the
17+
Dockerfile.
18+
19+
### Changed: e2e reachability jobs retry transient empty results
20+
21+
- Reachability e2e runs that report success with no alerted components in the
22+
facts file are retried up to three times as a suspected transient backend
23+
failure. After retries, only the known zero-project backend signature is
24+
classified as inconclusive — any other empty result still fails — and e2e
25+
jobs upload their logs and reports as diagnostics on failure.
26+
327
## 2.5.9
428

529
### Changed: bump pinned @coana-tech/cli to 15.10.3

Dockerfile

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ ENV PATH="/usr/local/go/bin:/usr/lib/go/bin:/root/.cargo/bin:${PATH}"
8686
ENV GOPATH="/go"
8787

8888
# Install uv
89-
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
89+
COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /usr/local/bin/uv
9090

9191
# Install pyenv
9292
# pyenv lets us build/install arbitrary Python versions on demand. We install
@@ -111,14 +111,26 @@ RUN curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/p
111111
ln -s ~/.pyenv/bin/pyenv /bin/pyenv && \
112112
pyenv --version
113113

114+
# Install Python dependencies from the lockfile with hash verification so the
115+
# image never resolves loose versions from PyPI at build time.
116+
COPY pyproject.toml uv.lock /tmp/socket-cli-lock/
117+
# Index flags are passed explicitly (always production PyPI) so the
118+
# PIP_INDEX_URL/PIP_EXTRA_INDEX_URL ARGs used to point CLI/SDK preview installs
119+
# at TestPyPI don't leak into the locked dependency install via pip's env vars.
120+
RUN uv export --directory /tmp/socket-cli-lock --frozen --no-dev --no-emit-project \
121+
--format requirements-txt -o /tmp/socket-cli-lock/requirements.txt && \
122+
pip install --require-hashes --no-deps \
123+
--index-url https://pypi.org/simple --extra-index-url https://pypi.org/simple \
124+
-r /tmp/socket-cli-lock/requirements.txt
125+
114126
# Install CLI based on build mode
115127
RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
116128
echo "Using local development install"; \
117129
else \
118130
cli_installed=false; \
119131
for i in $(seq 1 10); do \
120132
echo "Attempt $i/10: Installing socketsecurity==$CLI_VERSION"; \
121-
if pip install --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketsecurity==$CLI_VERSION; then \
133+
if pip install --no-deps --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketsecurity==$CLI_VERSION; then \
122134
cli_installed=true; \
123135
break; \
124136
fi; \
@@ -131,6 +143,7 @@ RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
131143
echo "Failed to install socketsecurity==$CLI_VERSION after 10 attempts"; \
132144
exit 1; \
133145
fi; \
146+
pip check || exit 1; \
134147
if [ ! -z "$SDK_VERSION" ]; then \
135148
pip install --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketdev==${SDK_VERSION}; \
136149
fi; \
@@ -140,7 +153,7 @@ RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
140153
COPY . /app
141154
WORKDIR /app
142155
RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
143-
pip install --upgrade -e .; \
156+
pip install --no-deps -e . && pip check; \
144157
fi
145158

146159
# Create workspace directory with proper permissions

pyproject.toml

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
[build-system]
22
requires = [
3-
"hatchling"
3+
"hatchling==1.31.0"
44
]
55
build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.5.9"
9+
version = "2.6.0"
1010
requires-python = ">= 3.11"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [
13-
'requests',
14-
'mdutils',
15-
'prettytable',
16-
'GitPython',
17-
'packaging',
18-
'python-dotenv',
19-
"socketdev>=3.3.0,<4.0.0",
20-
"bs4>=0.0.2",
21-
"markdown>=3.10",
22-
"brotli>=1.0.9; platform_python_implementation == 'CPython'",
23-
"brotlicffi>=1.0.9; platform_python_implementation != 'CPython'",
13+
"requests==2.34.2",
14+
"mdutils==1.8.1",
15+
"prettytable==3.18.0",
16+
"GitPython==3.1.57",
17+
"packaging==26.2",
18+
"python-dotenv==1.2.2",
19+
"socketdev==3.5.0",
20+
"beautifulsoup4==4.14.3",
21+
"markdown==3.10.2",
22+
"brotli==1.2.0; platform_python_implementation == 'CPython'",
23+
"brotlicffi==1.2.0.1; platform_python_implementation != 'CPython'",
2424
]
2525
readme = "README.md"
2626
description = "Socket Security CLI for CI/CD"

socketsecurity/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.5.9'
2+
__version__ = '2.6.0'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

tests/e2e/reach-facts-probe.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
# Exits 0 when the reachability facts file contains components with alerts.
3+
#
4+
# Used by the e2e workflow's retry-probe hook: a --reach run against the
5+
# known-vulnerable fixture that reports success but yields no alerted
6+
# components is the signature of a transient tier-1 backend failure,
7+
# so the run is worth repeating before validation fails the job.
8+
set -euo pipefail
9+
10+
TARGET="${1:?usage: reach-facts-probe.sh <target-path>}"
11+
12+
uv run python - "$TARGET" <<'PY'
13+
import sys
14+
15+
from socketsecurity.core.alert_selection import load_components_with_alerts
16+
17+
components = load_components_with_alerts(sys.argv[1], ".socket.facts.json")
18+
sys.exit(0 if components else 1)
19+
PY

tests/e2e/validate-reachability.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,25 @@ if [ ! -f "$FACTS_PATH" ]; then
3434
fi
3535
echo "PASS: Reachability facts file present at $FACTS_PATH"
3636

37+
# The tier-1 backend intermittently returns the known fixture as one orphaned
38+
# component with zero projects, so Coana has no vulnerability to analyze even
39+
# though manifest upload, facts generation, and scan finalization all succeed.
40+
# After the workflow's bounded retries, classify only that explicit
41+
# upstream signature as inconclusive. Any other empty facts result still fails,
42+
# including the important regression case where Coana received a vulnerability
43+
# but the CLI lost its alerted component.
44+
if ! bash tests/e2e/reach-facts-probe.sh tests/e2e/fixtures/simple-npm; then
45+
if grep -q "Found 1 manifest files for reachability upload" "$LOG" && \
46+
grep -q "Found 0 projects across 0 ecosystems to analyze" "$LOG" && \
47+
grep -q "Filtered out 1 orphaned component" "$LOG"; then
48+
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"
49+
echo "e2e-reachability: inconclusive after retries — known zero-project backend signature; diagnostics uploaded" >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
50+
exit 0
51+
fi
52+
echo "FAIL: no components with alerts in .socket.facts.json and the known backend signature was not present"
53+
exit 1
54+
fi
55+
3756
# 3-4. Build SARIF from the facts file produced by the initial --reach run.
3857
# Avoid re-running reach + full scan here; duplicate API scans are slow and flaky in CI.
3958
uv run python -c "

uv.lock

Lines changed: 16 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)