Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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.)
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
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
15 changes: 13 additions & 2 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,6 +99,7 @@ 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}")

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
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
# Changelog

## 2.7.2

### 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.7.1

### Changed: bump pinned @coana-tech/cli to 15.10.36
Expand Down
63 changes: 63 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ dependencies:
uv sync --all-extras
```

Install the git hooks once per clone:

```bash
make hooks
```

Before opening a pull request, run:

```bash
make lint
make test
uv run hatch build
uv run python -m twine check dist/*
Expand All @@ -22,6 +29,62 @@ uv run python -m twine check dist/*
To develop against a local SDK checkout, set `SOCKET_SDK_PATH` if it is not at
`../socketdev`, then run `make first-time-local-setup`.

## Linting

Ruff is the only linter. It runs in three places, all reading the same
configuration from `pyproject.toml` and the same version pinned in
`[project.optional-dependencies].dev`:

- `make lint` locally,
- the `ruff-check` pre-commit hook, on the files a commit touches,
- the `Lint` workflow, on every pull request and every push to `main`.

The pre-commit hook applies ruff's safe fixes and then fails the commit, leaving
the edits unstaged so they get read before they land. CI is the backstop for
commits made with `--no-verify` or without hooks installed.

`ruff format` is enforced the same way. It owns line length (120) and
whitespace, so the linter does not duplicate those checks: `E501` and `W291`/
`W293` are deliberately not selected. Everything the formatter cannot reflow is
a string literal -- argparse help text, log messages, the Markdown used to build
pull request comments -- where rewrapping risks silently changing user-visible
text. The PR-comment markup in particular relies on trailing double-spaces as
Markdown hard line breaks.

### One trap worth knowing

Never run `ruff check --select <narrow-list> --fix` with `RUF100` in the select.
With a narrow select, RUF100 considers every `# noqa` for a *non-selected* rule
to be unused and deletes it -- silently stripping the complexity suppressions
across the repository. Run `make lint-fix`, which uses the full configured rule
set, instead of hand-rolling a `--select`.

### Complexity limits

Two rules bound how large a single function may get:

| Rule | Limit | What it measures |
| --- | --- | --- |
| `C901` | 12 | Cyclomatic complexity: independent paths through a function, which is also the number of tests needed to cover it. |
| `PLR0913` | 8 | Arguments in a function definition. |

Functions that already exceed these limits carry an explicit
`# noqa: C901` / `# noqa: PLR0913` on their `def` line. That list is a backlog,
not a precedent:

- **Do not add a new suppression.** If a function you are writing trips the
limit, split it. This matters most for generated or model-assisted code, where
branches accumulate quickly and nothing pushes back.
- **Suppressions clean themselves up.** `RUF100` fails the build on a `# noqa`
that no longer applies, so refactoring a function back under the limit forces
the marker to be removed. The backlog can only shrink.

To see what is left:

```bash
grep -rn 'noqa: C901\|noqa: PLR0913' socketsecurity/ tests/
```

## Pull request validation

The `Package Check` workflow runs automatically for pull requests. It builds
Expand Down
20 changes: 17 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: setup sync clean test lint update-lock local-dev first-time-setup dev-setup sync-all first-time-local-setup
.PHONY: setup sync clean test lint lint-fix format format-check hooks update-lock local-dev first-time-setup dev-setup sync-all first-time-local-setup

# Environment variable for local SDK path (optional)
SOCKET_SDK_PATH ?= ../socketdev
Expand Down Expand Up @@ -57,6 +57,20 @@ clean:
test:
uv run pytest

# Installs the git pre-commit hooks (ruff + version sync).
hooks:
uv run --extra dev pre-commit install

# Exactly what the Lint workflow runs, so a green `make lint` means a green CI.
lint:
uv run ruff check .
uv run ruff format --check .
uv run --extra dev ruff check
uv run --extra dev ruff format --check

lint-fix:
uv run --extra dev ruff check --fix

format:
uv run --extra dev ruff format

format-check:
uv run --extra dev ruff format --check
9 changes: 3 additions & 6 deletions benchmarks/manifest_discovery.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ def legacy_discover(root: Path) -> set[str]:
insensitive = Core.to_case_insensitive_regex(pattern)
for candidate in root.rglob(insensitive):
if candidate.is_file() and not Core.is_excluded(
str(candidate),
excluded_dirs,
str(candidate),
excluded_dirs,
):
results.add(candidate.as_posix())
return results
Expand Down Expand Up @@ -85,10 +85,7 @@ def main() -> None:
)

if legacy_results != new_results:
raise SystemExit(
"Manifest result mismatch: "
f"legacy={len(legacy_results)}, single_pass={len(new_results)}"
)
raise SystemExit(f"Manifest result mismatch: legacy={len(legacy_results)}, single_pass={len(new_results)}")

speedup = legacy_seconds / new_seconds if new_seconds else float("inf")
print(f"Manifests: {len(new_results)}")
Expand Down
Loading
Loading