Skip to content

Commit 8e143e9

Browse files
committed
Harden opt-in preview publishing
1 parent 73504a6 commit 8e143e9

6 files changed

Lines changed: 156 additions & 55 deletions

File tree

.github/actions/setup-docker/action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ description: >-
55
preview, and stable workflows.
66
77
inputs:
8+
enable-qemu:
9+
description: "Set up QEMU for multi-platform builds"
10+
required: false
11+
default: "true"
812
dockerhub-username:
913
description: "Docker Hub username (pass from secrets)"
1014
required: true
@@ -16,6 +20,7 @@ runs:
1620
using: "composite"
1721
steps:
1822
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
23+
if: inputs.enable-qemu == 'true'
1924
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
2025
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
2126
with:

.github/workflows/pr-preview.yml

Lines changed: 79 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,85 @@ on:
2525
type: string
2626

2727
concurrency:
28-
group: publish-pr-preview-${{ github.event.pull_request.number || inputs.pr_number }}
28+
group: publish-pr-preview-${{ github.event.pull_request.number || github.run_id }}
2929
cancel-in-progress: false
3030

3131
jobs:
32-
build:
32+
context:
3333
if: >-
3434
github.event_name == 'workflow_dispatch' ||
35-
((github.event.label.name == 'publish-preview' ||
35+
(github.event.label.name == 'publish-preview' ||
3636
github.event.label.name == 'publish-docker-preview') &&
37-
github.event.pull_request.head.repo.full_name == github.repository)
37+
github.event.pull_request.head.repo.full_name == github.repository
38+
runs-on: ubuntu-latest
39+
timeout-minutes: 5
40+
permissions:
41+
contents: read
42+
pull-requests: read
43+
outputs:
44+
pr_number: ${{ steps.context.outputs.pr_number }}
45+
head_sha: ${{ steps.context.outputs.head_sha }}
46+
steps:
47+
- name: Validate pull request context
48+
id: context
49+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
50+
env:
51+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
52+
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
53+
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
54+
WORKFLOW_REF: ${{ github.ref }}
55+
with:
56+
script: |
57+
const rawPrNumber = context.eventName === 'workflow_dispatch'
58+
? process.env.INPUT_PR_NUMBER
59+
: process.env.EVENT_PR_NUMBER;
60+
if (!/^[1-9][0-9]*$/.test(rawPrNumber || '')) {
61+
core.setFailed('Pull request number must contain ASCII digits only.');
62+
return;
63+
}
64+
65+
if (context.eventName === 'workflow_dispatch') {
66+
const defaultRef = `refs/heads/${process.env.DEFAULT_BRANCH}`;
67+
if (process.env.WORKFLOW_REF !== defaultRef) {
68+
core.setFailed(`Run manual previews from ${defaultRef}.`);
69+
return;
70+
}
71+
}
72+
73+
const prNumber = Number(rawPrNumber);
74+
if (!Number.isSafeInteger(prNumber)) {
75+
core.setFailed('Pull request number is outside the supported range.');
76+
return;
77+
}
78+
const {data: pullRequest} = await github.rest.pulls.get({
79+
owner: context.repo.owner,
80+
repo: context.repo.repo,
81+
pull_number: prNumber,
82+
});
83+
if (pullRequest.state !== 'open') {
84+
core.setFailed(`Pull request #${prNumber} is not open.`);
85+
return;
86+
}
87+
if (pullRequest.head.repo?.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
88+
core.setFailed('Preview publication is limited to branches in this repository.');
89+
return;
90+
}
91+
92+
core.setOutput('pr_number', String(prNumber));
93+
core.setOutput('head_sha', pullRequest.head.sha);
94+
95+
build:
96+
needs: context
3897
runs-on: ubuntu-latest
3998
timeout-minutes: 10
4099
permissions:
41100
contents: read
42101
outputs:
43102
preview_version: ${{ steps.version.outputs.preview_version }}
44-
pr_number: ${{ steps.context.outputs.pr_number }}
45103
steps:
46104
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
47105
with:
48-
ref: ${{ github.event.pull_request.head.sha || format('refs/pull/{0}/head', inputs.pr_number) }}
106+
ref: ${{ needs.context.outputs.head_sha }}
49107
fetch-depth: 0
50108
persist-credentials: false
51109

@@ -59,13 +117,6 @@ jobs:
59117
- name: Install distribution validator
60118
run: python -m pip install "twine>=4.0.0"
61119

62-
- name: Record preview context
63-
id: context
64-
env:
65-
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
66-
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
67-
run: echo "pr_number=${EVENT_PR_NUMBER:-$INPUT_PR_NUMBER}" >> "$GITHUB_OUTPUT"
68-
69120
- name: Inject deterministic preview version
70121
env:
71122
PREVIEW_ID: ${{ github.run_id }}
@@ -110,7 +161,7 @@ jobs:
110161
retention-days: 14
111162

112163
publish-package:
113-
needs: build
164+
needs: [context, build]
114165
if: >-
115166
github.event.label.name == 'publish-preview' ||
116167
(github.event_name == 'workflow_dispatch' && inputs.publish_test_pypi)
@@ -128,7 +179,7 @@ jobs:
128179
path: dist
129180

130181
- name: Publish to TestPyPI
131-
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
182+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
132183
with:
133184
repository-url: https://test.pypi.org/legacy/
134185
verbose: true
@@ -137,7 +188,7 @@ jobs:
137188
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
138189
env:
139190
PREVIEW_VERSION: ${{ needs.build.outputs.preview_version }}
140-
PR_NUMBER: ${{ needs.build.outputs.pr_number }}
191+
PR_NUMBER: ${{ needs.context.outputs.pr_number }}
141192
with:
142193
script: |
143194
const marker = '<!-- socketsecurity-pr-preview -->';
@@ -176,7 +227,7 @@ jobs:
176227
}
177228
178229
publish-docker:
179-
needs: build
230+
needs: [context, build]
180231
if: >-
181232
github.event.label.name == 'publish-docker-preview' ||
182233
(github.event_name == 'workflow_dispatch' && inputs.publish_docker)
@@ -188,7 +239,9 @@ jobs:
188239
steps:
189240
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
190241
with:
191-
ref: ${{ github.event.pull_request.head.sha || format('refs/pull/{0}/head', inputs.pr_number) }}
242+
# Keep the Dockerfile and credential-handling action on trusted code.
243+
# The pull request enters this job only through the built wheel.
244+
ref: ${{ github.event.repository.default_branch }}
192245
fetch-depth: 1
193246
persist-credentials: false
194247

@@ -198,29 +251,28 @@ jobs:
198251
name: socketsecurity-preview-${{ github.run_id }}-${{ github.run_attempt }}
199252
path: dist
200253

201-
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
202-
203-
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
254+
- name: Set up Docker publishing
255+
uses: ./.github/actions/setup-docker
204256
with:
205-
username: ${{ secrets.DOCKERHUB_USERNAME }}
206-
password: ${{ secrets.DOCKERHUB_TOKEN }}
257+
enable-qemu: "false"
258+
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
259+
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
207260

208261
- name: Build and push Docker preview
209262
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
210263
with:
211264
file: Dockerfile.preview
212265
push: true
266+
pull: true
213267
platforms: linux/amd64
214-
cache-from: type=gha
215-
cache-to: type=gha,mode=max
216-
tags: socketdev/cli:pr-${{ needs.build.outputs.pr_number }}
268+
tags: socketdev/cli:pr-${{ needs.context.outputs.pr_number }}
217269
build-args: |
218270
SDK_PREVIEW_VERSION=${{ inputs.sdk_preview_version }}
219271
220272
- name: Comment on pull request
221273
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
222274
env:
223-
PR_NUMBER: ${{ needs.build.outputs.pr_number }}
275+
PR_NUMBER: ${{ needs.context.outputs.pr_number }}
224276
with:
225277
script: |
226278
const marker = '<!-- socketsecurity-docker-preview -->';

.hooks/sync_version.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ def read_preview_id():
137137
print("❌ `--preview-id` requires a numeric value.")
138138
sys.exit(1)
139139

140-
if not preview_id.isdigit():
141-
print("❌ `--preview-id` must contain digits only.")
140+
if not preview_id.isascii() or not preview_id.isdigit():
141+
print("❌ `--preview-id` must contain ASCII digits only.")
142142
sys.exit(1)
143143
return preview_id
144144

CONTRIBUTING.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ Before opening a pull request, run:
1515

1616
```bash
1717
make test
18-
make lint
1918
uv run hatch build
2019
uv run python -m twine check dist/*
2120
```
@@ -45,18 +44,19 @@ needs testing:
4544
`socketdev/cli:pr-<pull-request-number>` image to Docker Hub and adds or
4645
updates a pull request comment with the image tag.
4746

48-
Label-triggered publication is skipped for pull requests from forks. Each label
49-
is handled as a separate event, so applying both labels starts two workflow
50-
runs. Use manual dispatch instead when both artifacts should be published in a
51-
single run.
47+
Both label-triggered and manually dispatched previews are limited to open pull
48+
requests whose branches belong to this repository. Each label is handled as a
49+
separate event, so applying both labels starts two workflow runs. Use manual
50+
dispatch instead when both artifacts should be published in a single run.
5251

5352
The workflow reacts when a label is added; pushing another commit while the
5453
label remains on the pull request does not publish a new preview. To publish the
5554
new pull request head or retry a failed publication, remove the relevant label
5655
and apply it again.
5756

58-
Maintainers can also open **Actions > Publish PR Preview > Run workflow**, enter
59-
the pull request number, and choose whether to publish to TestPyPI, Docker Hub,
60-
or both. When testing the CLI against an SDK preview, enter the exact TestPyPI
61-
`socketdev` prerelease in `sdk_preview_version`; publish the SDK preview first
62-
and allow time for TestPyPI to expose it before starting the CLI Docker preview.
57+
Maintainers can also open **Actions > Publish PR Preview > Run workflow**, run
58+
it from the repository's default branch, enter the pull request number, and
59+
choose whether to publish to TestPyPI, Docker Hub, or both. When testing the CLI
60+
against an SDK preview, enter the exact TestPyPI `socketdev` prerelease in
61+
`sdk_preview_version`; publish the SDK preview first and allow time for
62+
TestPyPI to expose it before starting the CLI Docker preview.

Dockerfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ COPY . /app
141141
WORKDIR /app
142142
RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
143143
pip install --upgrade -e .; \
144-
pip install --upgrade socketdev; \
145144
fi
146145

147146
# Create workspace directory with proper permissions

Dockerfile.preview

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,68 @@
1+
# syntax=docker/dockerfile:1
2+
13
FROM socketdev/cli:latest
24

35
ARG SDK_PREVIEW_VERSION=""
4-
ARG PIP_INDEX_URL=https://test.pypi.org/simple/
5-
ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple/
6+
ARG PYPI_INDEX_URL=https://pypi.org/simple/
7+
ARG TEST_PYPI_INDEX_URL=https://test.pypi.org/simple/
68

79
COPY dist/socketsecurity-*.whl /tmp/socket-preview/
810

9-
RUN set -eux; \
10-
if [ -n "$SDK_PREVIEW_VERSION" ]; then \
11-
pip install \
12-
--no-cache-dir \
13-
--index-url "$PIP_INDEX_URL" \
14-
--extra-index-url "$PIP_EXTRA_INDEX_URL" \
15-
"socketdev==$SDK_PREVIEW_VERSION"; \
16-
fi; \
17-
pip install \
11+
RUN <<'SH'
12+
set -eux
13+
14+
wheel=$(find /tmp/socket-preview -maxdepth 1 -name 'socketsecurity-*.whl' -print -quit)
15+
16+
# Resolve every wheel dependency from production PyPI. When an exact SDK
17+
# preview is requested, leave socketdev out so the prerelease can intentionally
18+
# override a final-version floor such as socketdev>=3.4.0.
19+
python - "$wheel" > /tmp/socket-preview/requirements.txt <<'PY'
20+
import email
21+
import os
22+
import sys
23+
import zipfile
24+
25+
from packaging.requirements import Requirement
26+
from packaging.utils import canonicalize_name
27+
28+
wheel_path = sys.argv[1]
29+
with zipfile.ZipFile(wheel_path) as archive:
30+
metadata_path = next(
31+
name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
32+
)
33+
metadata = email.message_from_bytes(archive.read(metadata_path))
34+
35+
sdk_preview = os.environ.get("SDK_PREVIEW_VERSION")
36+
for value in metadata.get_all("Requires-Dist", []):
37+
if sdk_preview and canonicalize_name(Requirement(value).name) == "socketdev":
38+
continue
39+
print(value)
40+
PY
41+
42+
python -m pip install \
43+
--no-cache-dir \
44+
--index-url "$PYPI_INDEX_URL" \
45+
--requirement /tmp/socket-preview/requirements.txt
46+
47+
if [ -n "$SDK_PREVIEW_VERSION" ]; then
48+
mkdir /tmp/socket-preview/sdk
49+
python -m pip download \
1850
--no-cache-dir \
1951
--no-deps \
20-
--force-reinstall \
21-
/tmp/socket-preview/socketsecurity-*.whl; \
22-
socketcli --help >/dev/null; \
23-
rm -rf /tmp/socket-preview
52+
--dest /tmp/socket-preview/sdk \
53+
--index-url "$TEST_PYPI_INDEX_URL" \
54+
"socketdev==$SDK_PREVIEW_VERSION"
55+
python -m pip install \
56+
--no-cache-dir \
57+
--index-url "$PYPI_INDEX_URL" \
58+
/tmp/socket-preview/sdk/socketdev-*.whl
59+
fi
60+
61+
python -m pip install \
62+
--no-cache-dir \
63+
--no-deps \
64+
--force-reinstall \
65+
"$wheel"
66+
socketcli --help >/dev/null
67+
rm -rf /tmp/socket-preview
68+
SH

0 commit comments

Comments
 (0)