Skip to content

Commit 463d1d9

Browse files
dc-larsenlelia
andauthored
Log why a new repository couldn't be created (#436)
* 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. * 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. * 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<version> 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. * Use self-repository references for local actions --------- Co-authored-by: lelia <2418071+lelia@users.noreply.github.com>
1 parent 9aff373 commit 463d1d9

12 files changed

Lines changed: 171 additions & 29 deletions

File tree

‎.github/workflows/dependency-review.yml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ jobs:
150150
echo "sha=${{ github.event.pull_request.head.sha }}"
151151
} > sfw-artifacts/context.txt
152152
153-
- uses: ./.github/actions/setup-sfw
153+
- uses: $/.github/actions/setup-sfw
154154
with:
155155
uv: "true"
156156
mode: firewall-free
@@ -241,7 +241,7 @@ jobs:
241241
echo "sha=${{ github.event.pull_request.head.sha }}"
242242
} > sfw-artifacts/context.txt
243243
244-
- uses: ./.github/actions/setup-sfw
244+
- uses: $/.github/actions/setup-sfw
245245
with:
246246
uv: "true"
247247
mode: firewall-enterprise
@@ -332,7 +332,7 @@ jobs:
332332
echo "sha=${{ github.event.pull_request.head.sha }}"
333333
} > sfw-artifacts/context.txt
334334
335-
- uses: ./.github/actions/setup-sfw
335+
- uses: $/.github/actions/setup-sfw
336336
with:
337337
node: "true"
338338
mode: firewall-free
@@ -393,7 +393,7 @@ jobs:
393393
echo "sha=${{ github.event.pull_request.head.sha }}"
394394
} > sfw-artifacts/context.txt
395395
396-
- uses: ./.github/actions/setup-sfw
396+
- uses: $/.github/actions/setup-sfw
397397
with:
398398
node: "true"
399399
mode: firewall-enterprise
@@ -454,7 +454,7 @@ jobs:
454454
echo "sha=${{ github.event.pull_request.head.sha }}"
455455
} > sfw-artifacts/context.txt
456456
457-
- uses: ./.github/actions/setup-sfw
457+
- uses: $/.github/actions/setup-sfw
458458
with:
459459
python: "true"
460460
mode: firewall-free
@@ -518,7 +518,7 @@ jobs:
518518
echo "sha=${{ github.event.pull_request.head.sha }}"
519519
} > sfw-artifacts/context.txt
520520
521-
- uses: ./.github/actions/setup-sfw
521+
- uses: $/.github/actions/setup-sfw
522522
with:
523523
python: "true"
524524
mode: firewall-enterprise

‎.github/workflows/docker-stable.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
echo "Version ${INPUT_VERSION} found on PyPI - proceeding with release"
3030
3131
- name: Set up Docker publishing
32-
uses: ./.github/actions/setup-docker
32+
uses: $/.github/actions/setup-docker
3333
with:
3434
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
3535
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}

‎.github/workflows/package-check.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
python-version: "3.12"
2929

3030
- name: Install build tooling
31-
uses: ./.github/actions/setup-hatch
31+
uses: $/.github/actions/setup-hatch
3232

3333
- name: Install distribution validator
3434
run: python -m pip install "twine>=4.0.0"

‎.github/workflows/pr-preview.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ jobs:
112112
python-version: "3.12"
113113

114114
- name: Install build tooling
115-
uses: ./.github/actions/setup-hatch
115+
uses: $/.github/actions/setup-hatch
116116

117117
- name: Install distribution validator
118118
run: python -m pip install "twine>=4.0.0"
@@ -252,7 +252,7 @@ jobs:
252252
path: dist
253253

254254
- name: Set up Docker publishing
255-
uses: ./.github/actions/setup-docker
255+
uses: $/.github/actions/setup-docker
256256
with:
257257
# QEMU is required to build the arm64 layer on an amd64 runner.
258258
enable-qemu: "true"

‎.github/workflows/release.yml‎

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ jobs:
99
permissions:
1010
id-token: write
1111
contents: read
12+
issues: write
1213
steps:
1314
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
1415
with:
@@ -19,7 +20,7 @@ jobs:
1920
python-version: '3.13'
2021

2122
- name: Install build tooling
22-
uses: ./.github/actions/setup-hatch
23+
uses: $/.github/actions/setup-hatch
2324

2425
- name: Get Version
2526
id: version
@@ -69,7 +70,7 @@ jobs:
6970
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
7071

7172
- name: Set up Docker publishing
72-
uses: ./.github/actions/setup-docker
73+
uses: $/.github/actions/setup-docker
7374
with:
7475
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
7576
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -123,3 +124,44 @@ jobs:
123124
socketdev/cli:${{ env.VERSION }}
124125
build-args: |
125126
CLI_VERSION=${{ env.VERSION }}
127+
128+
# Releases are immutable, so a failed run leaves a published GitHub release whose
129+
# tag can never be reused, with nothing on PyPI or Docker Hub behind it. A failed
130+
# run only notifies whoever published the release, so raise it where the team
131+
# will see it.
132+
- name: Open an issue for the failed release
133+
if: failure()
134+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
135+
env:
136+
VERSION: ${{ env.VERSION }}
137+
with:
138+
script: |
139+
const release = context.payload.release;
140+
const runUrl =
141+
`${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` +
142+
`/actions/runs/${context.runId}`;
143+
const body = [
144+
`@SocketDev/customer-engineering the release workflow for [${release.tag_name}](${release.html_url}) ` +
145+
`(published by @${release.author.login}) failed: ${runUrl}`,
146+
'',
147+
`- Tag: \`${release.tag_name}\``,
148+
`- Package version at that commit: \`${process.env.VERSION || 'unknown'}\``,
149+
'',
150+
'Check the run for which step failed. If it failed before "Publish to PyPI", nothing reached PyPI or ' +
151+
'Docker Hub, but the GitHub release is immutable and its tag can never be reused.',
152+
'',
153+
'Before publishing another release:',
154+
'',
155+
'1. Edit this release\'s notes to say it was not published and name the release that replaces it.',
156+
'2. If the tag and the package version disagree, merge a PR that bumps the version to one with no ' +
157+
'existing tag (the Version Check workflow enforces this), then publish a release whose tag matches ' +
158+
'that version exactly. Do not pick the next tag number from the releases list.',
159+
].join('\n');
160+
161+
const created = await github.rest.issues.create({
162+
owner: context.repo.owner,
163+
repo: context.repo.repo,
164+
title: `Release ${release.tag_name} failed`,
165+
body,
166+
});
167+
core.error(`Release failed; opened #${created.data.number}`);

‎.github/workflows/version-check.yml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ jobs:
4444
python3 <<'PY'
4545
import json
4646
import os
47+
import subprocess
4748
import tomllib
4849
import urllib.request
4950
from packaging import version
@@ -96,6 +97,21 @@ jobs:
9697
)
9798
raise SystemExit(1)
9899
100+
# Releases are immutable: once a v<version> release is published, its tag can't
101+
# be moved or reused, even when that release failed and nothing reached PyPI.
102+
# PyPI alone misses that, so a burned tag would only surface when the release
103+
# run fails after the merge.
104+
tag = f"v{os.environ['PR_VERSION']}"
105+
if subprocess.run(
106+
["git", "rev-parse", "--quiet", "--verify", f"refs/tags/{tag}"],
107+
capture_output=True,
108+
).returncode == 0:
109+
print(
110+
f"❌ Tag {tag} already exists, so {pr_ver} can never be released "
111+
f"(latest PyPI release: {pypi_ver}). Bump to a version with no tag."
112+
)
113+
raise SystemExit(1)
114+
99115
if pr_ver < main_ver:
100116
print(
101117
f"❌ Version moves backwards: main is {main_ver}, PR is {pr_ver}."

‎CHANGELOG.md‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
# Changelog
22

3-
## 2.11.8
3+
## 2.11.10
44

5-
### Changed: bump pinned Coana CLI to 15.14.0
5+
Versions 2.11.7 through 2.11.9 were never published to PyPI or Docker Hub. The
6+
GitHub releases tagged `v2.11.8` and `v2.11.9` have no packages behind them;
7+
their changes ship here.
68

7-
- Bumped the pinned reachability engine (the Coana CLI) from `15.13.3` to
8-
`15.14.0`. See the
9-
[reachability analysis changelog](https://docs.socket.dev/docs/reachability-analysis-changelog)
10-
for engine changes.
9+
### Fixed: report why a new repository couldn't be created
1110

12-
## 2.11.7
11+
- When a repository isn't in Socket yet and the API token can't create it, the
12+
CLI now logs the failure, the HTTP status, and on a 403 a hint about the
13+
`repo:create` scope before exiting `2`. Previously it exited `2` right after
14+
file discovery with nothing logged.
15+
- After the CLI creates a missing repository, its logging is no longer silenced
16+
for the rest of the run.
1317

14-
### Changed: bump pinned Coana CLI to 15.13.3
18+
### Changed: bump pinned Coana CLI to 15.14.0
1519

1620
- Bumped the pinned reachability engine (the Coana CLI) from `15.13.2` to
17-
`15.13.3`. See the
21+
`15.14.0`. See the
1822
[reachability analysis changelog](https://docs.socket.dev/docs/reachability-analysis-changelog)
1923
for engine changes.
2024

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.11.8"
9+
version = "2.11.10"
1010
requires-python = ">= 3.11"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [

‎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.11.8'
2+
__version__ = '2.11.10'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

‎socketsecurity/core/__init__.py‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
from socketsecurity.config import CliConfig
2121
from git import Repo
2222
from socketdev import socketdev
23-
from socketdev.exceptions import APIFailure
23+
from socketdev.exceptions import (
24+
APIFailure,
25+
APIInsufficientQuota,
26+
APIOrganizationNotAllowed,
27+
)
2428
from socketdev.fullscans import DiffArtifacts, FullScanParams, SocketArtifact
2529
from socketdev.org import Organization
2630
from socketdev.repos import RepositoryInfo
@@ -1505,8 +1509,11 @@ def get_repo_info(self, repo_slug: str, default_branch: str = "socket-default-br
15051509
sdk_logger = logging_std.getLogger("socketdev")
15061510
original_level = sdk_logger.level
15071511
sdk_logger.setLevel(logging_std.CRITICAL)
1508-
response = self.sdk.repos.repo(self.config.org_slug, repo_slug, use_types=True)
1509-
sdk_logger.setLevel(original_level)
1512+
try:
1513+
response = self.sdk.repos.repo(self.config.org_slug, repo_slug, use_types=True)
1514+
finally:
1515+
# This module logs through "socketdev" too, so a failed lookup must not leave it muted.
1516+
sdk_logger.setLevel(original_level)
15101517
if not response.success:
15111518
log.error(f"Failed to get repository: {response.status}")
15121519
# log.error(response.message)
@@ -1530,7 +1537,13 @@ def get_repo_info(self, repo_slug: str, default_branch: str = "socket-default-br
15301537
return response.data
15311538

15321539
except APIFailure as e:
1533-
log.error(f"API failure while creating repository: {e}")
1540+
status = f" (HTTP {e.status_code})" if e.status_code else ""
1541+
# The SDK appends the request path and response headers after the first line,
1542+
# and some subclasses carry no message at all.
1543+
reason = str(e).split("\n", 1)[0] or type(e).__name__
1544+
log.error(f"API failure while creating repository {repo_slug}{status}: {reason}")
1545+
if e.status_code == 403 and not isinstance(e, (APIInsufficientQuota, APIOrganizationNotAllowed)):
1546+
log.error("A 403 here usually means the API token is missing the repo:create scope.")
15341547
sys.exit(2) # Exit here with code 2. Code 1 indicates a successfully-detected security issue.
15351548

15361549
return response.data

0 commit comments

Comments
 (0)