Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Revisions that only reformat or mechanically re-lint code.
# Configure once per clone:
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# (GitHub applies this file automatically in its blame view.)
9 changes: 8 additions & 1 deletion .github/actions/setup-hatch/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ description: >-
runs:
using: "composite"
steps:
# Kept in step with pyproject.toml: `hatch` matches the dev extra and
# `hatchling` matches [build-system].requires. hatchling is pinned here only
# for consistency -- `hatch build` resolves the backend in an isolated
# environment from build-system.requires, so that pin is what actually
# produces the wheel (the artifact records `Generator: hatchling 1.32.0`).
# Three different values for the same tool across three files was only ever
# going to mislead someone debugging a build.
- shell: bash
run: |
python -m pip install --upgrade pip
pip install "virtualenv<20.36"
pip install hatchling==1.27.0 hatch==1.14.0
pip install hatchling==1.32.0 hatch==1.18.0
48 changes: 48 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Lint

env:
PYTHON_VERSION: "3.12"

# Deliberately not path-filtered. Ruff finishes in well under a minute, and its
# trigger surface is every Python file in the repository -- including the ones
# outside the Unit Tests filters (.hooks/, benchmarks/, tests/e2e/). Running
# unconditionally also keeps this usable as a required status check: a
# path-filtered workflow reports as "not run" rather than "passed", which blocks
# any pull request that does not happen to touch the filtered paths.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: lint-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
ruff:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
persist-credentials: false
- name: 🐍 setup python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: 🛠️ install deps
run: |
python -m pip install --upgrade pip
pip install uv
uv sync --extra dev
# Same ruff version the pre-commit hook uses (pinned in pyproject.toml,
# locked in uv.lock), so a clean commit locally stays clean here.
- name: 🧹 ruff check
run: uv run ruff check
- name: 🎨 ruff format
run: uv run ruff format --check
12 changes: 12 additions & 0 deletions .github/workflows/pr-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ jobs:
core.setOutput('head_sha', pullRequest.head.sha);

build:
# This job checks out and executes untrusted pull request code. Under
# workflow_dispatch it runs in the default branch's context, which means its
# Actions cache scope is main's. Do not add caching here -- no `actions/cache`
# step, and no `cache:` input on setup-python -- or a preview build of a
# malicious branch could plant an entry that every workflow on main then
# restores. CodeQL's actions/cache-poisoning/poisonable-step alerts point at
# these steps for exactly that reason; they are inert only while nothing in
# this job writes a cache.
#
# The privilege split is what keeps this safe: this job holds `contents: read`
# and no secrets, and publish-package (which holds `id-token: write`) never
# checks out code -- it only downloads the built artifact.
needs: context
runs-on: ubuntu-latest
timeout-minutes: 10
Expand Down
20 changes: 0 additions & 20 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,26 +67,6 @@ jobs:
uv export --no-hashes --no-emit-project --format requirements-txt > /tmp/req-audit.txt
uvx pip-audit --strict --progress-spinner off --disable-pip --no-deps -r /tmp/req-audit.txt

ruff:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
persist-credentials: false
- name: 🐍 setup python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: 🛠️ install deps
run: |
python -m pip install --upgrade pip
pip install uv
uv sync --extra dev
- name: 🧹 run ruff
run: uv run ruff check

unsupported-python-install:
runs-on: ubuntu-latest
timeout-minutes: 10
Expand Down
37 changes: 29 additions & 8 deletions .github/workflows/version-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,28 @@ jobs:
pip install packaging

# Get version from current PR
PR_VERSION=$(grep -o "__version__.*" socketsecurity/__init__.py | awk '{print $3}' | tr -d "'")
PR_VERSION=$(grep -o "__version__.*" socketsecurity/__init__.py | awk '{print $3}' | tr -d "\"'")
echo "PR_VERSION=$PR_VERSION" >> $GITHUB_ENV

# Get version from main branch
MAIN_VERSION=$(git show origin/main:socketsecurity/__init__.py | grep -o "__version__.*" | awk '{print $3}' | tr -d "'")
MAIN_VERSION=$(git show origin/main:socketsecurity/__init__.py | grep -o "__version__.*" | awk '{print $3}' | tr -d "\"'")
echo "MAIN_VERSION=$MAIN_VERSION" >> $GITHUB_ENV

export PR_VERSION
export MAIN_VERSION

# Only enforce a bump when the PR actually changes shipped content.
# This workflow sits in its own paths filter so edits to it are
# exercised, but a CI-only change ships nothing and must not be told
# to cut a release. The comparison still runs and reports either way.
if git diff --name-only origin/main...HEAD \
| grep -qE '^(socketsecurity/|pyproject\.toml$|uv\.lock$)'; then
PACKAGE_CHANGED=true
else
PACKAGE_CHANGED=false
fi
export PACKAGE_CHANGED

# Compare against the latest published PyPI release.
python3 <<'PY'
import json
Expand All @@ -55,6 +67,18 @@ jobs:
pr_ver = version.parse(os.environ["PR_VERSION"])
main_ver = version.parse(os.environ["MAIN_VERSION"])

enforced = os.environ["PACKAGE_CHANGED"] == "true"

def reject(message):
print(message)
if enforced:
raise SystemExit(1)
print(
"ℹ️ Not enforced: this PR changes no packaged files, "
"so it ships nothing that needs a new version."
)
raise SystemExit(0)

with open("pyproject.toml", "rb") as fh:
pyproject_ver = version.parse(tomllib.load(fh)["project"]["version"])

Expand All @@ -68,12 +92,11 @@ jobs:
# copy, but uv derives it and `uv lock --locked` in python-tests
# already fails when it drifts.)
if pr_ver != pyproject_ver:
print(
reject(
f"❌ Version mismatch inside the PR: pyproject.toml is "
f"{pyproject_ver}, socketsecurity/__init__.py is {pr_ver}. "
f"Bump both."
)
raise SystemExit(1)

with urllib.request.urlopen("https://pypi.org/pypi/socketsecurity/json") as response:
pypi_data = json.load(response)
Expand All @@ -94,17 +117,15 @@ jobs:
# header. Main is therefore only a floor when this PR moves the
# version -- a change to it must go forwards, never backwards.
if pr_ver <= pypi_ver:
print(
reject(
f"❌ Version {pr_ver} is already published on PyPI "
f"(latest release: {pypi_ver}). Bump it."
)
raise SystemExit(1)

if pr_ver < main_ver:
print(
reject(
f"❌ Version moves backwards: main is {main_ver}, PR is {pr_ver}."
)
raise SystemExit(1)

if pr_ver == main_ver:
print(
Expand Down
17 changes: 14 additions & 3 deletions .hooks/sync_version.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
PYPI_PROD_API = "https://pypi.org/pypi/socketsecurity/json"
PYPI_TEST_API = "https://test.pypi.org/pypi/socketsecurity/json"


def read_version_from_init(path: pathlib.Path) -> str:
content = path.read_text()
match = VERSION_PATTERN.search(content)
Expand All @@ -24,6 +25,7 @@ def read_version_from_init(path: pathlib.Path) -> str:
sys.exit(1)
return match.group(1)


def read_version_from_git(path: str) -> str:
try:
output = subprocess.check_output(["git", "show", f"HEAD:{path}"], text=True)
Expand All @@ -34,13 +36,15 @@ def read_version_from_git(path: str) -> str:
except subprocess.CalledProcessError:
return None


def bump_patch_version(version: str) -> str:
if ".dev" in version:
version = version.split(".dev")[0]
parts = version.split(".")
parts[-1] = str(int(parts[-1]) + 1)
return ".".join(parts)


def parse_stable_version(version: str):
if not STABLE_VERSION_PATTERN.fullmatch(version):
return None
Expand Down Expand Up @@ -72,6 +76,7 @@ def fetch_latest_stable_pypi_version():
return None
return max(stable_versions)


def find_next_available_dev_version(base_version: str) -> str:
existing_versions = fetch_existing_versions(PYPI_TEST_API)
for i in range(1, 100):
Expand All @@ -94,12 +99,13 @@ def find_next_stable_patch_version(current_version: str) -> str:
next_parts = (base_parts[0], base_parts[1], base_parts[2] + 1)
return format_stable_version(next_parts)


def inject_version(version: str):
print(f"🔁 Updating version to: {version}")

# Update __init__.py
init_content = INIT_FILE.read_text()
new_init_content = VERSION_PATTERN.sub(f"__version__ = '{version}'", init_content)
new_init_content = VERSION_PATTERN.sub(f'__version__ = "{version}"', init_content)
INIT_FILE.write_text(new_init_content)

# Update pyproject.toml
Expand Down Expand Up @@ -190,16 +196,21 @@ def main():
inject_version(new_version)
uv_lock_changed = run_uv_lock()
lock_hint = " and uv.lock" if uv_lock_changed else ""
print(f"⚠️ Version {current_version} is already published on PyPI — auto-bumped to {new_version}. Please git add{lock_hint} + commit again.")
print(
f"⚠️ Version {current_version} is already published on PyPI — auto-bumped to {new_version}. Please git add{lock_hint} + commit again."
)
sys.exit(1)

uv_lock_changed = run_uv_lock()
if uv_lock_changed:
print("⚠️ Version already bumped, but uv.lock was out of date and has been updated. Please git add uv.lock + commit again.")
print(
"⚠️ Version already bumped, but uv.lock was out of date and has been updated. Please git add uv.lock + commit again."
)
sys.exit(1)

print("✅ Version already bumped and uv.lock is up to date — proceeding.")
sys.exit(0)


if __name__ == "__main__":
main()
27 changes: 26 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,29 @@ repos:
entry: python .hooks/sync_version.py
language: python
always_run: true
pass_filenames: false
pass_filenames: false

# Ruff runs out of the project environment rather than the upstream
# astral-sh/ruff-pre-commit mirror so its version is pinned in exactly one
# place: `ruff==0.16.4` under [project.optional-dependencies].dev, locked
# in uv.lock and used verbatim by the Lint workflow. Dependabot has no
# pre-commit ecosystem and will not touch a mirror's `rev:`, so a mirror
# would drift out of step with CI and produce the worst failure mode for a
# hook -- clean locally, red on the pull request.
#
# `--fix` applies only ruff's fixes marked safe. When it changes a file
# pre-commit aborts the commit and leaves the edit in the working tree, so
# nothing lands without being looked at.
- id: ruff-check
name: ruff check
entry: uv run --extra dev ruff check --force-exclude --fix
language: system
types_or: [python, pyi]
require_serial: true

- id: ruff-format
name: ruff format
entry: uv run --extra dev ruff format --force-exclude
language: system
types_or: [python, pyi]
require_serial: true
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
# Changelog

## 2.8.1

### Fixed: debug logging could include sensitive configuration values

- Debug-level output could include configuration values and integration URLs
verbatim. Both are now redacted in log output.

### Fixed: manifest links used the wrong host for some organizations

- The source-control type was partly inferred by searching the Socket report URL
for "github", "gitlab" or "bitbucket". That URL is always a Socket dashboard
link, so the only part that could match was the organization slug: an org whose
slug contained one of those words got manifest links pointing at a repository
host it may not use. The type now comes from `--scm` alone.

### Fixed: package timestamps were truncated

- `Package.created_at` stripped its `" (Coordinated Universal Time)"` suffix with
`str.strip()`, which treats its argument as a set of characters rather than a
suffix. Timestamps beginning with `Tue` lost their leading `T`, and timestamps
that carried no such suffix lost a trailing `T`. The suffix is now removed with
`str.removesuffix()`.

### Fixed: notification delivery could hang a pipeline indefinitely

- Slack, Teams, Jira, generic webhook and GitLab commit-status requests were sent
without a timeout. `requests` blocks forever by default, so an unresponsive
endpoint could hold a run open until the CI job itself timed out. All of these
calls now use an explicit 30 second timeout.

### Fixed: two internal guards did nothing under `python -O`

- A manifest upload checked its organization slug with `assert`, which the
interpreter removes entirely in optimised mode. It is now an explicit check that
raises with a readable message. A second, redundant `assert` was removed.

### Fixed: a debug message was written to stdout

- Duplicate packages in a scan's SBOM artifacts printed to stdout, which also
carries machine-readable output such as SARIF. The message is now logged at
debug level.

### Changed: configuration messages follow the CLI logger

- `config.py` logged through the root logger, so its warnings and errors ignored
the configured log level and format. They now use the `socketcli` logger like
the rest of the CLI.
## 2.8.0

### Changed: improve monorepo scan diagnostics and guidance
Expand Down
Loading