From aa2299bd5ad4e599ea7e8bad1f388fee7c1c1348 Mon Sep 17 00:00:00 2001 From: David Larsen Date: Sun, 1 Nov 2026 16:39:16 -0600 Subject: [PATCH 1/4] Log why a new repository couldn't be created get_repo_info mutes the socketdev logger around the repository lookup and only restored the level after a successful lookup. This module logs through the same logger, so when the lookup raised for a repository that isn't in Socket yet, the "attempting to create it" warning and the creation error were dropped, and the CLI exited 2 right after file discovery with nothing logged. A successful create also left logging muted for the rest of the run. Restore the level in a finally block, and include the repository, the HTTP status and, on a 403, a repo:create hint in the creation error. The SDK keeps only the API's "Insufficient permissions" message and drops the required scope. Bump to 2.11.9. --- CHANGELOG.md | 11 ++++++++++ pyproject.toml | 2 +- socketsecurity/__init__.py | 2 +- socketsecurity/core/__init__.py | 12 +++++++--- tests/core/test_sdk_methods.py | 39 ++++++++++++++++++++++++++++++++- uv.lock | 2 +- 6 files changed, 61 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9554c2c..753b1b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 2.11.9 + +### Fixed: report why a new repository couldn't be created + +- When a repository isn't in Socket yet and the API token can't create it, the + CLI now logs the failure, the HTTP status, and on a 403 a hint about the + `repo:create` scope before exiting `2`. Previously it exited `2` right after + file discovery with nothing logged. +- After the CLI creates a missing repository, its logging is no longer silenced + for the rest of the run. + ## 2.11.8 ### Changed: bump pinned Coana CLI to 15.14.0 diff --git a/pyproject.toml b/pyproject.toml index 6675a80..1ab574a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.11.8" +version = "2.11.9" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index bbf3e24..4a45db6 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.11.8' +__version__ = '2.11.9' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index 06c037e..9eccf63 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -1505,8 +1505,11 @@ def get_repo_info(self, repo_slug: str, default_branch: str = "socket-default-br sdk_logger = logging_std.getLogger("socketdev") original_level = sdk_logger.level sdk_logger.setLevel(logging_std.CRITICAL) - response = self.sdk.repos.repo(self.config.org_slug, repo_slug, use_types=True) - sdk_logger.setLevel(original_level) + try: + response = self.sdk.repos.repo(self.config.org_slug, repo_slug, use_types=True) + finally: + # This module logs through "socketdev" too, so a failed lookup must not leave it muted. + sdk_logger.setLevel(original_level) if not response.success: log.error(f"Failed to get repository: {response.status}") # log.error(response.message) @@ -1530,7 +1533,10 @@ def get_repo_info(self, repo_slug: str, default_branch: str = "socket-default-br return response.data except APIFailure as e: - log.error(f"API failure while creating repository: {e}") + status = f" (HTTP {e.status_code})" if e.status_code else "" + log.error(f"API failure while creating repository {repo_slug}{status}: {e}") + if e.status_code == 403: + log.error("A 403 here usually means the API token is missing the repo:create scope.") sys.exit(2) # Exit here with code 2. Code 1 indicates a successfully-detected security issue. return response.data diff --git a/tests/core/test_sdk_methods.py b/tests/core/test_sdk_methods.py index bf439a3..197241b 100644 --- a/tests/core/test_sdk_methods.py +++ b/tests/core/test_sdk_methods.py @@ -1,5 +1,7 @@ +import logging + import pytest -from socketdev.exceptions import APIFailure +from socketdev.exceptions import APIAccessDenied, APIFailure, APIResourceNotFound from socketdev.fullscans import FullScanParams, FullScanStreamResponse, ScanType from socketsecurity.config import CliConfig @@ -44,6 +46,41 @@ def test_get_repo_info(core, mock_sdk_with_responses): assert repo_info.id == "f639d6c9-acc3-4d8a-9fb5-2090ad651c7e" assert repo_info.head_full_scan_id == "head" + +def test_get_repo_info_logs_why_repo_creation_failed(core, mock_sdk_with_responses, caplog): + """A repo that isn't in Socket yet and can't be created exits 2 with the reason logged""" + mock_sdk_with_responses.repos.repo.side_effect = APIResourceNotFound(status_code=404) + mock_sdk_with_responses.repos.post.side_effect = APIAccessDenied( + "Insufficient permissions", status_code=403 + ) + + with caplog.at_level(logging.INFO, logger="socketdev"), pytest.raises(SystemExit) as exit_info: + core.get_repo_info("new-repo") + + assert exit_info.value.code == 2 + messages = [record.getMessage() for record in caplog.records] + assert "Failed to get repository new-repo, attempting to create it" in messages + assert any("creating repository new-repo (HTTP 403)" in message for message in messages) + assert any("repo:create" in message for message in messages) + + +def test_get_repo_info_restores_log_level_after_creating_repo( + core, mock_sdk_with_responses, repo_info_response, caplog +): + """Creating a missing repo must not leave the socketdev logger muted for the rest of the run""" + mock_sdk_with_responses.repos.repo.side_effect = [ + APIResourceNotFound(status_code=404), + repo_info_response, + ] + mock_sdk_with_responses.repos.post.return_value = {"id": "created"} + + with caplog.at_level(logging.INFO, logger="socketdev"): + repo_info = core.get_repo_info("new-repo") + assert logging.getLogger("socketdev").level == logging.INFO + + assert repo_info.id == "f639d6c9-acc3-4d8a-9fb5-2090ad651c7e" + + def test_get_head_scan_for_repo(core, mock_sdk_with_responses): """Test getting head scan ID for a repository""" head_scan_id = core.get_head_scan_for_repo("test") diff --git a/uv.lock b/uv.lock index 4d8e44e..583a647 100644 --- a/uv.lock +++ b/uv.lock @@ -1332,7 +1332,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.11.8" +version = "2.11.9" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" }, From 7179ede46c7e323eebe851520e4a4c6cff43e38b Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:26:51 -0600 Subject: [PATCH 2/4] Trim repo-creation errors to the API reason; bump to 2.11.10 The SDK appends the request path and the response headers to APIAccessDenied's message, so logging the exception as-is printed a multi-line header dump at the default log level. APIInsufficientPermissions carries no message at all, which left the line ending in ": ". Log the first line, falling back to the exception type. Quota and organization-not-allowed errors are also 403s, so the repo:create hint no longer fires for those. Bump to 2.11.10: v2.11.9 is already taken by an existing tag and GitHub release (#434), even though that release never reached PyPI. --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- socketsecurity/__init__.py | 2 +- socketsecurity/core/__init__.py | 13 +++++++++--- tests/core/test_sdk_methods.py | 36 ++++++++++++++++++++++++++++++--- uv.lock | 2 +- 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 753b1b5..9065949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2.11.9 +## 2.11.10 ### Fixed: report why a new repository couldn't be created diff --git a/pyproject.toml b/pyproject.toml index 1ab574a..c60155d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.11.9" +version = "2.11.10" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 4a45db6..0fae66b 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.11.9' +__version__ = '2.11.10' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index 9eccf63..f78f6c1 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -20,7 +20,11 @@ from socketsecurity.config import CliConfig from git import Repo from socketdev import socketdev -from socketdev.exceptions import APIFailure +from socketdev.exceptions import ( + APIFailure, + APIInsufficientQuota, + APIOrganizationNotAllowed, +) from socketdev.fullscans import DiffArtifacts, FullScanParams, SocketArtifact from socketdev.org import Organization from socketdev.repos import RepositoryInfo @@ -1534,8 +1538,11 @@ def get_repo_info(self, repo_slug: str, default_branch: str = "socket-default-br except APIFailure as e: status = f" (HTTP {e.status_code})" if e.status_code else "" - log.error(f"API failure while creating repository {repo_slug}{status}: {e}") - if e.status_code == 403: + # The SDK appends the request path and response headers after the first line, + # and some subclasses carry no message at all. + reason = str(e).split("\n", 1)[0] or type(e).__name__ + log.error(f"API failure while creating repository {repo_slug}{status}: {reason}") + if e.status_code == 403 and not isinstance(e, (APIInsufficientQuota, APIOrganizationNotAllowed)): log.error("A 403 here usually means the API token is missing the repo:create scope.") sys.exit(2) # Exit here with code 2. Code 1 indicates a successfully-detected security issue. diff --git a/tests/core/test_sdk_methods.py b/tests/core/test_sdk_methods.py index 197241b..9235bc6 100644 --- a/tests/core/test_sdk_methods.py +++ b/tests/core/test_sdk_methods.py @@ -1,7 +1,13 @@ import logging import pytest -from socketdev.exceptions import APIAccessDenied, APIFailure, APIResourceNotFound +from socketdev.exceptions import ( + APIAccessDenied, + APIFailure, + APIInsufficientPermissions, + APIInsufficientQuota, + APIResourceNotFound, +) from socketdev.fullscans import FullScanParams, FullScanStreamResponse, ScanType from socketsecurity.config import CliConfig @@ -50,8 +56,10 @@ def test_get_repo_info(core, mock_sdk_with_responses): def test_get_repo_info_logs_why_repo_creation_failed(core, mock_sdk_with_responses, caplog): """A repo that isn't in Socket yet and can't be created exits 2 with the reason logged""" mock_sdk_with_responses.repos.repo.side_effect = APIResourceNotFound(status_code=404) + # Mirrors the SDK, which appends the request path and response headers to the message mock_sdk_with_responses.repos.post.side_effect = APIAccessDenied( - "Insufficient permissions", status_code=403 + "Insufficient permissions\nPath: https://api.socket.dev/v0/orgs/test/repos\n\nHeaders:\nx-req: 1", + status_code=403, ) with caplog.at_level(logging.INFO, logger="socketdev"), pytest.raises(SystemExit) as exit_info: @@ -60,10 +68,32 @@ def test_get_repo_info_logs_why_repo_creation_failed(core, mock_sdk_with_respons assert exit_info.value.code == 2 messages = [record.getMessage() for record in caplog.records] assert "Failed to get repository new-repo, attempting to create it" in messages - assert any("creating repository new-repo (HTTP 403)" in message for message in messages) + assert "API failure while creating repository new-repo (HTTP 403): Insufficient permissions" in messages assert any("repo:create" in message for message in messages) +@pytest.mark.parametrize( + ("error", "expected_reason", "expects_scope_hint"), + [ + (APIInsufficientPermissions(status_code=403), "APIInsufficientPermissions", True), + (APIInsufficientQuota(status_code=403), "APIInsufficientQuota", False), + ], +) +def test_get_repo_info_repo_creation_failure_reason( + core, mock_sdk_with_responses, caplog, error, expected_reason, expects_scope_hint +): + """Message-less SDK errors still name a reason, and only permission 403s get the scope hint""" + mock_sdk_with_responses.repos.repo.side_effect = APIResourceNotFound(status_code=404) + mock_sdk_with_responses.repos.post.side_effect = error + + with caplog.at_level(logging.INFO, logger="socketdev"), pytest.raises(SystemExit): + core.get_repo_info("new-repo") + + messages = [record.getMessage() for record in caplog.records] + assert f"API failure while creating repository new-repo (HTTP 403): {expected_reason}" in messages + assert any("repo:create" in message for message in messages) is expects_scope_hint + + def test_get_repo_info_restores_log_level_after_creating_repo( core, mock_sdk_with_responses, repo_info_response, caplog ): diff --git a/uv.lock b/uv.lock index 583a647..5890103 100644 --- a/uv.lock +++ b/uv.lock @@ -1332,7 +1332,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.11.9" +version = "2.11.10" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" }, From 12e4e1cdb198711eedf15dab117bdc580a696b3f Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:41:41 -0600 Subject: [PATCH 3/4] Catch burned release tags and report failed releases Releases are immutable, so a published release's tag can't be moved or reused even when its release run fails. v2.11.8 and v2.11.9 were tagged one ahead of the package version at their commits, failed the release workflow's tag/version check, and left nothing on PyPI or Docker Hub. Only the person who published them was notified. - Version Check now fails when v is already a tag, since PyPI alone can't tell that a version has been burned. - The release workflow opens an issue for the team when it fails, with the run, the tag, the package version and what to do next. - Fold the never-published 2.11.7 and 2.11.8 changelog entries into 2.11.10, which is the first release to ship them. --- .github/workflows/release.yml | 42 +++++++++++++++++++++++++++++ .github/workflows/version-check.yml | 16 +++++++++++ CHANGELOG.md | 17 ++++-------- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 878698f..7402621 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,7 @@ jobs: permissions: id-token: write contents: read + issues: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -123,3 +124,44 @@ jobs: socketdev/cli:${{ env.VERSION }} build-args: | CLI_VERSION=${{ env.VERSION }} + + # Releases are immutable, so a failed run leaves a published GitHub release whose + # tag can never be reused, with nothing on PyPI or Docker Hub behind it. A failed + # run only notifies whoever published the release, so raise it where the team + # will see it. + - name: Open an issue for the failed release + if: failure() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + VERSION: ${{ env.VERSION }} + with: + script: | + const release = context.payload.release; + const runUrl = + `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` + + `/actions/runs/${context.runId}`; + const body = [ + `@SocketDev/customer-engineering the release workflow for [${release.tag_name}](${release.html_url}) ` + + `(published by @${release.author.login}) failed: ${runUrl}`, + '', + `- Tag: \`${release.tag_name}\``, + `- Package version at that commit: \`${process.env.VERSION || 'unknown'}\``, + '', + 'Check the run for which step failed. If it failed before "Publish to PyPI", nothing reached PyPI or ' + + 'Docker Hub, but the GitHub release is immutable and its tag can never be reused.', + '', + 'Before publishing another release:', + '', + '1. Edit this release\'s notes to say it was not published and name the release that replaces it.', + '2. If the tag and the package version disagree, merge a PR that bumps the version to one with no ' + + 'existing tag (the Version Check workflow enforces this), then publish a release whose tag matches ' + + 'that version exactly. Do not pick the next tag number from the releases list.', + ].join('\n'); + + const created = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `Release ${release.tag_name} failed`, + body, + }); + core.error(`Release failed; opened #${created.data.number}`); diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml index d505c06..ae18733 100644 --- a/.github/workflows/version-check.yml +++ b/.github/workflows/version-check.yml @@ -44,6 +44,7 @@ jobs: python3 <<'PY' import json import os + import subprocess import tomllib import urllib.request from packaging import version @@ -96,6 +97,21 @@ jobs: ) raise SystemExit(1) + # Releases are immutable: once a v release is published, its tag can't + # be moved or reused, even when that release failed and nothing reached PyPI. + # PyPI alone misses that, so a burned tag would only surface when the release + # run fails after the merge. + tag = f"v{os.environ['PR_VERSION']}" + if subprocess.run( + ["git", "rev-parse", "--quiet", "--verify", f"refs/tags/{tag}"], + capture_output=True, + ).returncode == 0: + print( + f"❌ Tag {tag} already exists, so {pr_ver} can never be released " + f"(latest PyPI release: {pypi_ver}). Bump to a version with no tag." + ) + raise SystemExit(1) + if pr_ver < main_ver: print( f"❌ Version moves backwards: main is {main_ver}, PR is {pr_ver}." diff --git a/CHANGELOG.md b/CHANGELOG.md index 9065949..0d3ef49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 2.11.10 +Versions 2.11.7 through 2.11.9 were never published to PyPI or Docker Hub. The +GitHub releases tagged `v2.11.8` and `v2.11.9` have no packages behind them; +their changes ship here. + ### Fixed: report why a new repository couldn't be created - When a repository isn't in Socket yet and the API token can't create it, the @@ -11,21 +15,10 @@ - After the CLI creates a missing repository, its logging is no longer silenced for the rest of the run. -## 2.11.8 - ### Changed: bump pinned Coana CLI to 15.14.0 -- Bumped the pinned reachability engine (the Coana CLI) from `15.13.3` to - `15.14.0`. See the - [reachability analysis changelog](https://docs.socket.dev/docs/reachability-analysis-changelog) - for engine changes. - -## 2.11.7 - -### Changed: bump pinned Coana CLI to 15.13.3 - - Bumped the pinned reachability engine (the Coana CLI) from `15.13.2` to - `15.13.3`. See the + `15.14.0`. See the [reachability analysis changelog](https://docs.socket.dev/docs/reachability-analysis-changelog) for engine changes. From e9acd8470b5bef6a2e6d9016005db71b62230276 Mon Sep 17 00:00:00 2001 From: lelia <2418071+lelia@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:09:09 -0600 Subject: [PATCH 4/4] Use self-repository references for local actions --- .github/workflows/dependency-review.yml | 12 ++++++------ .github/workflows/docker-stable.yml | 2 +- .github/workflows/package-check.yml | 2 +- .github/workflows/pr-preview.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1e41e14..a609193 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -150,7 +150,7 @@ jobs: echo "sha=${{ github.event.pull_request.head.sha }}" } > sfw-artifacts/context.txt - - uses: ./.github/actions/setup-sfw + - uses: $/.github/actions/setup-sfw with: uv: "true" mode: firewall-free @@ -241,7 +241,7 @@ jobs: echo "sha=${{ github.event.pull_request.head.sha }}" } > sfw-artifacts/context.txt - - uses: ./.github/actions/setup-sfw + - uses: $/.github/actions/setup-sfw with: uv: "true" mode: firewall-enterprise @@ -332,7 +332,7 @@ jobs: echo "sha=${{ github.event.pull_request.head.sha }}" } > sfw-artifacts/context.txt - - uses: ./.github/actions/setup-sfw + - uses: $/.github/actions/setup-sfw with: node: "true" mode: firewall-free @@ -393,7 +393,7 @@ jobs: echo "sha=${{ github.event.pull_request.head.sha }}" } > sfw-artifacts/context.txt - - uses: ./.github/actions/setup-sfw + - uses: $/.github/actions/setup-sfw with: node: "true" mode: firewall-enterprise @@ -454,7 +454,7 @@ jobs: echo "sha=${{ github.event.pull_request.head.sha }}" } > sfw-artifacts/context.txt - - uses: ./.github/actions/setup-sfw + - uses: $/.github/actions/setup-sfw with: python: "true" mode: firewall-free @@ -518,7 +518,7 @@ jobs: echo "sha=${{ github.event.pull_request.head.sha }}" } > sfw-artifacts/context.txt - - uses: ./.github/actions/setup-sfw + - uses: $/.github/actions/setup-sfw with: python: "true" mode: firewall-enterprise diff --git a/.github/workflows/docker-stable.yml b/.github/workflows/docker-stable.yml index f07f45b..de1fe16 100644 --- a/.github/workflows/docker-stable.yml +++ b/.github/workflows/docker-stable.yml @@ -29,7 +29,7 @@ jobs: echo "Version ${INPUT_VERSION} found on PyPI - proceeding with release" - name: Set up Docker publishing - uses: ./.github/actions/setup-docker + uses: $/.github/actions/setup-docker with: dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/package-check.yml b/.github/workflows/package-check.yml index a6f946a..c80a041 100644 --- a/.github/workflows/package-check.yml +++ b/.github/workflows/package-check.yml @@ -28,7 +28,7 @@ jobs: python-version: "3.12" - name: Install build tooling - uses: ./.github/actions/setup-hatch + uses: $/.github/actions/setup-hatch - name: Install distribution validator run: python -m pip install "twine>=4.0.0" diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 1b2086a..7b08eff 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -112,7 +112,7 @@ jobs: python-version: "3.12" - name: Install build tooling - uses: ./.github/actions/setup-hatch + uses: $/.github/actions/setup-hatch - name: Install distribution validator run: python -m pip install "twine>=4.0.0" @@ -252,7 +252,7 @@ jobs: path: dist - name: Set up Docker publishing - uses: ./.github/actions/setup-docker + uses: $/.github/actions/setup-docker with: # QEMU is required to build the arm64 layer on an amd64 runner. enable-qemu: "true" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7402621..ef33417 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: python-version: '3.13' - name: Install build tooling - uses: ./.github/actions/setup-hatch + uses: $/.github/actions/setup-hatch - name: Get Version id: version @@ -70,7 +70,7 @@ jobs: uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3 - name: Set up Docker publishing - uses: ./.github/actions/setup-docker + uses: $/.github/actions/setup-docker with: dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}