Skip to content

Make package and Docker previews opt-in - #287

Merged
lelia merged 4 commits into
mainfrom
agent/opt-in-pr-previews
Aug 5, 2026
Merged

Make package and Docker previews opt-in#287
lelia merged 4 commits into
mainfrom
agent/opt-in-pr-previews

Conversation

@lelia

@lelia lelia commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Replace the always-on TestPyPI and Docker preview pipeline with deterministic local package validation plus explicitly requested publication:

  • Package Check runs on every PR and on main. It builds the wheel and sdist, validates them with Twine, installs the wheel without resolving dependencies, verifies its entry point and metadata, compiles the package, and uploads the distributions as workflow artifacts.
  • Publish PR Preview publishes to TestPyPI only through the publish-preview label or manual workflow dispatch.
  • Docker previews publish only through the separate publish-docker-preview label or the manual publish_docker input.

This follows the same pattern as SocketDev/socket-sdk-python#100.

Why

The previous required-on-every-push pipeline mixed package validation, TestPyPI propagation, and a heavyweight Docker deployment. TestPyPI's cached Simple API could make the job wait for ten minutes and fail after the upload had already succeeded. Every PR also pushed a mutable pr-<number> Docker Hub tag, creating repository clutter even when nobody needed an image.

Implementation

  • Preview versions use a unique .dev<run-id-and-attempt> suffix instead of querying TestPyPI for the next free number.
  • Preview version injection can skip uv lock, so artifact construction is not coupled to dependency resolution.
  • The TestPyPI verification loop is removed; distributions are built, checked, and inspected locally before the trusted upload.
  • Docker previews use a small Dockerfile.preview overlay based on socketdev/cli:latest, installing the locally built PR wheel rather than waiting for TestPyPI and rebuilding Go, Java, .NET, Rust, and other toolchains.
  • Manual Docker previews accept an optional exact socketdev TestPyPI prerelease for cross-repository integration testing.
  • The production Dockerfile now exits nonzero if all ten CLI installation attempts fail instead of continuing after the final sleep.
  • The dependency-review Dockerfile smoke uses the intended local-install mode instead of relying on an empty CLI_VERSION install being swallowed.

Opt-in usage

Use these existing repository labels:

  • publish-preview — publish the Python package to TestPyPI.
  • publish-docker-preview — publish socketdev/cli:pr-<number> to Docker Hub.

Maintainers can alternatively run Publish PR Preview manually, choose TestPyPI and/or Docker publication, and optionally supply an SDK preview version.

Review follow-up

  • Manual dispatch now validates an ASCII numeric PR number, requires the default branch, rejects closed and fork PRs, and pins the exact PR head SHA before any checkout.
  • Docker publishing checks out the trusted default branch for the overlay and credential-handling action; PR code enters that job only as the already-built wheel artifact.
  • The Docker overlay resolves runtime dependencies from production PyPI, downloads only an explicitly requested SDK prerelease from TestPyPI, always refreshes the base image, and avoids exporting a low-value GHA cache.
  • The preview ID guard now accepts ASCII digits only, the PyPI publisher action is aligned with the SDK, and the unconstrained Docker SDK upgrade was removed.
  • CONTRIBUTING documents the same-repository and default-branch requirements.

Validation

  • actionlint .github/workflows/package-check.yml .github/workflows/pr-preview.yml
  • ruff check .hooks/sync_version.py
  • Wheel and sdist built successfully and passed twine check
  • Wheel installed without dependencies and passed metadata, entry-point, and bytecode checks
  • Deterministic preview version injection verified without changing uv.lock
  • Dockerfile.preview built successfully against socketdev/cli:latest; after the initial base pull, the PR-specific install and CLI smoke layer completed in about one second

The existing unit and E2E workflows remain responsible for dependency resolution and behavioral testing. After this merges, Package Check can be added to the branch-protection required checks.


Note

Medium Risk
Changes CI publishing gates and release-preview mechanics (TestPyPI/Docker secrets on label/dispatch only); runtime product code is largely untouched aside from Dockerfile install failure handling.

Overview
Every PR now gets a Package Check workflow that builds wheel/sdist, runs twine check, smoke-installs the wheel (entry point + bytecode), and uploads artifacts—without publishing.

TestPyPI and Docker previews move to Publish PR Preview, triggered only by publish-preview / publish-docker-preview labels (in-repo PRs) or manual dispatch—not on every push. Preview versions use a deterministic .dev<run-id> suffix via new --preview-id / --skip-lock flags on sync_version.py, dropping TestPyPI “next free version” queries and the post-upload install wait loop.

Docker previews use a new Dockerfile.preview overlay on socketdev/cli:latest that installs the built PR wheel locally (optional socketdev TestPyPI prerelease), instead of rebuilding the full image from TestPyPI.

The production Dockerfile now fails the build if all ten PyPI install attempts fail; dependency-review’s Dockerfile smoke passes USE_LOCAL_INSTALL=true. CONTRIBUTING.md documents validation vs opt-in preview labels.

Reviewed by Cursor Bugbot for commit 73504a6. Configure here.

@socket-security

socket-security Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgithub/​actions/​download-artifact@​3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c10010010010080
Addedgithub/​pypa/​gh-action-pypi-publish@​dc37677b2e1c63e2034f94d8a5b11f265b73ba33100100100100100

View full report

@lelia
lelia marked this pull request as ready for review August 5, 2026 17:23
@lelia
lelia requested a review from a team as a code owner August 5, 2026 17:23
@lelia

lelia commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 73504a6. Configure here.

@flowstate Eric Hibbs (flowstate) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ai@cursor: review of the workflow restructuring, the Dockerfile change, and the preview overlay. The split into a credential-free build job that produces an artifact and separate publish jobs that consume it is the right shape, and it's a clear improvement over holding id-token: write through a ten-minute TestPyPI polling loop. Most of what follows is about the manual-dispatch path and the Docker preview.

Manual dispatch bypasses the fork check

The fork check sits inside the label branch of the ||, so workflow_dispatch short-circuits it:

if: >-
  github.event_name == 'workflow_dispatch' ||
  ((github.event.label.name == 'publish-preview' ||
  github.event.label.name == 'publish-docker-preview') &&
  github.event.pull_request.head.repo.full_name == github.repository)

Once github.event_name == 'workflow_dispatch' is true, head.repo.full_name is never consulted, and the checkout resolves to format('refs/pull/{0}/head', inputs.pr_number) for any PR number including fork PRs. The build job then runs code from that ref: .hooks/sync_version.py, the local composite action ./.github/actions/setup-hatch, and the hatchling build hooks. build itself is well isolated with contents: read and no secrets, but its artifact is published to TestPyPI under the org's trusted publisher.

publish-docker is more direct than that. It checks out the PR ref, runs docker/login-action with DOCKERHUB_TOKEN, and then builds a Dockerfile.preview taken from that same ref and pushes to socketdev/cli. A dispatched fork PR gets to author a Dockerfile that runs with Docker Hub credentials already on the runner.

I assume this is intentional and the maintainer running the dispatch is vouching for the code. Two things would make that safe to rely on. First, say so explicitly in CONTRIBUTING — the current text puts "Label-triggered publication is skipped for pull requests from forks" directly above the dispatch paragraph, which reads as though the protection carries over to dispatch. Second, put a GitHub environment: on publish-package and publish-docker so the gate is enforced by GitHub with a required reviewer rather than by an if: expression, and scope the TestPyPI trusted publisher to that environment. As things stand, any workflow in the repo with id-token: write can mint a TestPyPI token; release.yml has the same property, so this is pre-existing rather than a regression, but this PR is the natural place to close it.

inputs.pr_number is unvalidated

It's type: string and reaches echo "pr_number=$INPUT_PR_NUMBER" >> "$GITHUB_OUTPUT", so a multi-line value can set arbitrary additional job outputs. It also becomes the Docker tag socketdev/cli:pr-<value> and Number(process.env.PR_NUMBER) in github-script, where a non-numeric value produces NaN and an opaque API error rather than a clear failure. Dispatch is maintainer-only so the severity is low, but a numeric guard in the "Record preview context" step is one line.

The format('refs/pull/{0}/head', inputs.pr_number) usage is fine — that's an action input rather than a shell.

Dockerfile.preview

--no-deps on the wheel install. If a PR adds or bumps a runtime dependency, the preview image silently won't have it. socketcli --help catches top-level import failures, so many cases fail at build time, but a lazily imported new dependency ships a broken preview. "I added a dependency" is close to the most likely reason someone asks for a preview image in the first place, so this is worth either fixing or documenting in CONTRIBUTING.

ARG PIP_INDEX_URL=https://test.pypi.org/simple/. BuildKit exposes build args to RUN as environment variables and pip reads PIP_INDEX_URL from the environment, so TestPyPI is the implicit index for that entire layer. Harmless today because the wheel install is --no-deps from a local path, but if --no-deps ever comes off, dependency resolution silently moves to TestPyPI. Defaulting the ARG to real PyPI and passing the TestPyPI URL only on the SDK_PREVIEW_VERSION install removes the trap.

FROM socketdev/cli:latest without pull: true. docker/build-push-action doesn't pull by default, so a warm builder can resolve a stale base and nobody would know. Either set pull: true or include the resolved base digest in the PR comment so the tester knows what they're on top of.

The GHA cache isn't buying anything. COPY dist/socketsecurity-*.whl changes on every run, so no layer after it can be reused, and cache-to: mode=max then exports the whole base image's layers on each build. For a two-instruction overlay I'd drop cache-from and cache-to entirely.

sync_version.py

--preview-id validates with str.isdigit(), which returns True for characters like ² and ٣. Those produce an invalid PEP 440 version that fails later in twine check or at upload with a much worse error than the guard was meant to give. CI only ever passes $((run_id * 100 + attempt)) so it can't happen today, but if the check is worth having it may as well be correct — int() in a try, or isascii() and isdigit().

Separately, the file now has four flags parsed three different ways: membership tests for --dev and --skip-lock, and sys.argv.index with manual IndexError handling for --preview-id. argparse would be shorter and would give the type and arity checks for free. Not blocking.

The Dockerfile fix should probably be its own PR

The retry-loop change is a real bug fix and it's correct. The old for ... done && if [ -n "$SDK_VERSION" ] chain always exited 0, because a shell for loop's status is that of its last command — sleep 30 — so ten consecutive failed installs produced a "successful" image with no CLI in it. Skipping the final sleep is a nice touch too.

That said, it's the only change in this PR that affects a released artifact, and it's the change most likely to need an independent revert. Splitting it out would make both halves easier to reason about.

The dependency-review.yml change follows from it: with the fix in place, the smoke build's empty CLI_VERSION would hard-fail, so USE_LOCAL_INSTALL=true is necessary, and it's a better test for a dependency bump anyway. Worth noting the side effect, though — no CI job builds the Dockerfile's published-package install path anymore, so a regression in that branch first surfaces at release. Also, the local-install branch runs pip install --upgrade socketdev unpinned, which can install an SDK outside this project's own socketdev>=3.3.0,<4.0.0 constraint. Pre-existing, but it's now on the smoke path.

Package Check as a required check

The description proposes making this the required branch-protection check. Note that Unit Tests is path-filtered on socketsecurity/**, tests/**, pyproject.toml, and uv.lock, so it didn't run on this PR at all — this branch has had no Python test coverage in CI. If Package Check becomes the gate, it's a required check that runs no tests, on exactly the PRs the path filter excludes. Either run the unit tests inside Package Check (as the SDK's version of this workflow does) or make both required.

While on the subject: CONTRIBUTING now tells contributors to run make lint before opening a PR, and no workflow runs ruff. If Package Check is becoming the gate, lint is the obvious thing to add to it.

Smaller notes

pr-preview.yml now inlines setup-buildx-action and login-action instead of using ./.github/actions/setup-docker, which still exists and is used by release.yml and docker-stable.yml. The reason is clear — the composite pulls in QEMU and previews are amd64-only — but the pinned SHAs now live in two places and will drift. A qemu: false input on the composite would keep them in one place.

pypa/gh-action-pypi-publish is pinned to v1.14.1 here and v1.14.2 in SocketDev/socket-sdk-python#100. Worth aligning while the two are being changed together.

Checked and fine

The needs: build plus explicit if: combination on both publish jobs is correct. Neither condition uses always(), failure(), or cancelled(), so the implicit needs-success gate still applies and a failed build skips both. Worth a comment in the file, since it's a well-known footgun and the next person to add always() for a notification step would quietly open the gate.

Dropping | cut -d+ -f1 from the version read is safe: inject_version writes an exact version into both socketsecurity/__init__.py and the static version = in pyproject.toml, so no local segment can appear and get rejected at upload. Removing the rm -rf dist/ step is likewise fine on a fresh runner, and with no .dockerignore in the repo the downloaded dist/ artifact reaches the build context despite being gitignored. Both labels exist in the repo.

@lelia

lelia commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Eric Hibbs (@flowstate) Thank you for the detailed review. I pushed the applicable follow-up in 8e143e9, with the shared SDK changes mirrored in SocketDev/socket-sdk-python#100 at 9ea2bb3.

Addressed:

  • Added a credential-free context job that validates a positive ASCII PR number, requires manual dispatch from the default branch, fetches the PR through the API, rejects closed and fork PRs, and outputs the exact head SHA. Package construction now checks out that immutable SHA.
  • Docker publication now checks out the trusted default branch for Dockerfile.preview and the Docker credential-handling action. PR code enters the credentialed job only through the wheel artifact produced by the unprivileged build job.
  • Docker setup now reuses the composite action with QEMU disabled; the PyPI publisher pin is aligned with the SDK.
  • Added pull: true and removed the GHA cache from the two-layer preview overlay.
  • Reworked preview dependency installation. All CLI wheel dependencies resolve from production PyPI. If an SDK preview is supplied, only the exact socketdev wheel is downloaded from TestPyPI and its dependencies still resolve from production PyPI. The CLI wheel remains a no-deps install only after those requirements are handled explicitly. This preserves the intentional case where 3.4.0.devN needs to override a CLI floor of >=3.4.0.
  • Tightened --preview-id to ASCII digits in both repositories.
  • Removed the redundant unconstrained pip install --upgrade socketdev from the production Dockerfile local-install path.
  • Updated both contribution guides to document same-repository previews and default-branch manual dispatch.

Validated locally with actionlint, 420 CLI tests (2 skipped), 122 SDK tests (1 skipped), wheel/sdist builds plus twine checks, the full production Dockerfile local-install build, and both Docker overlay modes. The SDK-preview build installed socketsecurity 2.5.9 with exact TestPyPI socketdev 3.4.0.dev8; the normal build retained stable socketdev 3.3.0.

Intentional non-changes / pushback:

  • We are keeping the Dockerfile retry fix in this PR. With limited review capacity, splitting closely related correctness work into another PR is not practical.
  • We did not add required-reviewer environments here. The manual fork path is now closed, the exact same-repository SHA is pinned, and the Dockerfile/action used with Docker credentials comes from the trusted default branch. PyPI trusted publishers are also scoped to the configured workflow filename; an environment would be additional defense-in-depth, not the absence of all publisher scoping. Adopting one requires matching TestPyPI publisher configuration and Docker secret migration, and would add a second approval after the opt-in label/dispatch. That can be considered separately if two-person preview publication is desired.
  • Package Check remains an artifact-integrity check without dependency resolution. Adding CLI unit tests to it would recreate the unpublished-SDK dependency catch-22 this PR is designed to decouple. The existing required E2E scan/reachability checks remain the behavioral gates. The PR description now treats Package Check as an additional required check, not a replacement.
  • We did not add repo-wide Ruff to Package Check. The existing baselines currently produce 63 CLI and 28 SDK findings, so enabling it here would turn these PRs into unrelated cleanup work. The contribution guides no longer claim those whole-repo lint commands are clean prerequisites.
  • We are not restoring a full published-package Docker build on every PR; that would restore the heavyweight behavior this change removes. The old empty-CLI_VERSION smoke was not actually validating that path because the retry-loop bug swallowed the failed install. The local build still validates the Dockerfile on PRs, and release remains the real published-package path.
  • The argparse refactor and a comment explaining standard needs-success semantics do not materially improve this change, so those remain unchanged.

@lelia
lelia merged commit 142449b into main Aug 5, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants