Skip to content

Commit 111883b

Browse files
committed
Make package previews opt-in
1 parent 82cf391 commit 111883b

3 files changed

Lines changed: 187 additions & 92 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Package Check
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: package-check-${{ github.event.pull_request.number || github.ref }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
package-check:
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 10
20+
steps:
21+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22+
with:
23+
fetch-depth: 1
24+
persist-credentials: false
25+
26+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
27+
with:
28+
python-version: "3.12"
29+
30+
- name: Install build tooling
31+
uses: ./.github/actions/setup-hatch
32+
33+
- name: Install test and distribution tooling
34+
run: python -m pip install ".[test]" "twine>=4.0.0"
35+
36+
- name: Run unit tests
37+
run: python -m pytest -q tests/unit
38+
39+
- name: Build distributions
40+
run: hatch build
41+
42+
- name: Validate distributions
43+
run: python -m twine check dist/*
44+
45+
- name: Install and smoke-test wheel
46+
run: |
47+
python -m venv "$RUNNER_TEMP/package-check"
48+
"$RUNNER_TEMP/package-check/bin/pip" install --upgrade pip
49+
"$RUNNER_TEMP/package-check/bin/pip" install dist/*.whl
50+
"$RUNNER_TEMP/package-check/bin/python" -c "import socketdev; from socketdev.version import __version__; print('wheel smoke OK', __version__)"
51+
52+
- name: Upload distributions
53+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
54+
with:
55+
name: socketdev-${{ github.sha }}
56+
path: dist/*
57+
if-no-files-found: error
58+
retention-days: 14

.github/workflows/pr-preview.yml

Lines changed: 98 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,147 @@
1-
name: PR Preview
1+
name: Publish PR Preview
2+
23
on:
34
pull_request:
4-
types: [opened, synchronize, ready_for_review]
5+
types: [labeled]
6+
workflow_dispatch:
7+
inputs:
8+
pr_number:
9+
description: Pull request number to publish
10+
required: true
11+
type: string
512

6-
# Cancel an in-flight preview when the PR is pushed again -- previews publish
7-
# to Test PyPI, so superseded runs shouldn't keep churning.
813
concurrency:
9-
group: pr-preview-${{ github.event.pull_request.number }}
10-
cancel-in-progress: true
14+
group: publish-pr-preview-${{ github.event.pull_request.number || inputs.pr_number }}
15+
cancel-in-progress: false
1116

1217
jobs:
13-
preview:
14-
# Skip on:
15-
# - PRs from forks (no access to publish secrets / OIDC)
16-
# - Dependabot PRs: preview-publishing a dependency bump to Test PyPI is
17-
# pointless (no package version bump) and would fail the version check.
18+
build:
1819
if: >-
19-
github.event.pull_request.head.repo.full_name == github.repository &&
20-
github.event.pull_request.user.login != 'dependabot[bot]'
20+
github.event_name == 'workflow_dispatch' ||
21+
(github.event.label.name == 'publish-preview' &&
22+
github.event.pull_request.head.repo.full_name == github.repository)
2123
runs-on: ubuntu-latest
24+
timeout-minutes: 10
2225
permissions:
23-
id-token: write
2426
contents: read
25-
pull-requests: write
27+
outputs:
28+
preview_version: ${{ steps.version.outputs.preview_version }}
29+
pr_number: ${{ steps.context.outputs.pr_number }}
2630
steps:
2731
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2832
with:
33+
ref: ${{ github.event.pull_request.head.sha || format('refs/pull/{0}/head', inputs.pr_number) }}
2934
fetch-depth: 0
3035
persist-credentials: false
36+
3137
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
3238
with:
33-
python-version: '3.13'
39+
python-version: "3.12"
3440

3541
- name: Install build tooling
3642
uses: ./.github/actions/setup-hatch
3743

38-
- name: Inject full dynamic version
39-
run: python .hooks/sync_version.py --dev
44+
- name: Install distribution validator
45+
run: python -m pip install "twine>=4.0.0"
4046

41-
- name: Check if version exists on Test PyPI
42-
id: version_check
47+
- name: Record preview context
48+
id: context
49+
env:
50+
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
51+
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
52+
run: echo "pr_number=${EVENT_PR_NUMBER:-$INPUT_PR_NUMBER}" >> "$GITHUB_OUTPUT"
53+
54+
- name: Inject deterministic preview version
55+
env:
56+
PREVIEW_ID: ${{ github.run_id }}
57+
RUN_ATTEMPT: ${{ github.run_attempt }}
4358
run: |
44-
VERSION=$(hatch version | cut -d+ -f1)
45-
echo "VERSION=$VERSION" >> $GITHUB_ENV
46-
if curl -s -f https://test.pypi.org/pypi/socketdev/$VERSION/json > /dev/null; then
47-
echo "Version ${VERSION} already exists on Test PyPI"
48-
echo "exists=true" >> $GITHUB_OUTPUT
49-
else
50-
echo "Version ${VERSION} not found on Test PyPI - proceeding with test deployment"
51-
echo "exists=false" >> $GITHUB_OUTPUT
52-
fi
53-
54-
- name: Clean previous builds
55-
run: rm -rf dist/ build/ *.egg-info
56-
57-
- name: Get Hatch version
59+
PREVIEW_ID=$((PREVIEW_ID * 100 + RUN_ATTEMPT))
60+
python .hooks/sync_version.py --dev --preview-id "$PREVIEW_ID" --skip-lock
61+
62+
- name: Read preview version
5863
id: version
59-
run: |
60-
VERSION=$(hatch version | cut -d+ -f1)
61-
echo "VERSION=$VERSION" >> $GITHUB_ENV
64+
run: echo "preview_version=$(hatch version)" >> "$GITHUB_OUTPUT"
6265

63-
- name: Build package
64-
if: steps.version_check.outputs.exists != 'true'
66+
- name: Build and validate distributions
6567
run: |
6668
hatch build
69+
python -m twine check dist/*
70+
71+
- name: Install and smoke-test wheel locally
72+
run: |
73+
python -m venv "$RUNNER_TEMP/preview-check"
74+
"$RUNNER_TEMP/preview-check/bin/pip" install --upgrade pip
75+
"$RUNNER_TEMP/preview-check/bin/pip" install dist/*.whl
76+
"$RUNNER_TEMP/preview-check/bin/python" -c "import socketdev; from socketdev.version import __version__; print('preview wheel smoke OK', __version__)"
77+
78+
- name: Upload preview distributions
79+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
80+
with:
81+
name: socketdev-preview-${{ github.run_id }}-${{ github.run_attempt }}
82+
path: dist/*
83+
if-no-files-found: error
84+
retention-days: 14
85+
86+
publish:
87+
needs: build
88+
runs-on: ubuntu-latest
89+
timeout-minutes: 10
90+
permissions:
91+
contents: read
92+
id-token: write
93+
pull-requests: write
94+
steps:
95+
- name: Download preview distributions
96+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
97+
with:
98+
name: socketdev-preview-${{ github.run_id }}-${{ github.run_attempt }}
99+
path: dist
67100

68-
- name: Publish to Test PyPI
69-
if: steps.version_check.outputs.exists != 'true'
101+
- name: Publish to TestPyPI
70102
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
71103
with:
72104
repository-url: https://test.pypi.org/legacy/
73105
verbose: true
74106

75-
- name: Comment on PR
76-
if: steps.version_check.outputs.exists != 'true'
107+
- name: Comment on pull request
77108
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
78109
env:
79-
VERSION: ${{ env.VERSION }}
110+
PREVIEW_VERSION: ${{ needs.build.outputs.preview_version }}
111+
PR_NUMBER: ${{ needs.build.outputs.pr_number }}
80112
with:
81113
script: |
82-
const version = process.env.VERSION;
83-
const prNumber = context.payload.pull_request.number;
84-
const owner = context.repo.owner;
85-
const repo = context.repo.repo;
86-
// Find existing bot comments
87-
const comments = await github.rest.issues.listComments({
114+
const marker = '<!-- socketdev-pr-preview -->';
115+
const prNumber = Number(process.env.PR_NUMBER);
116+
const version = process.env.PREVIEW_VERSION;
117+
const body = `${marker}
118+
🚀 SDK preview published: \`socketdev==${version}\`
119+
120+
\`\`\`bash
121+
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple socketdev==${version}
122+
\`\`\`
123+
124+
TestPyPI's package index can take several minutes to expose a newly uploaded version.`;
125+
const {data: comments} = await github.rest.issues.listComments({
88126
owner: context.repo.owner,
89127
repo: context.repo.repo,
90128
issue_number: prNumber,
91129
});
92-
93-
const botComment = comments.data.find(comment =>
94-
comment.user.type === 'Bot' &&
95-
comment.body.includes('🚀 Preview package published!')
130+
const existing = comments.find(comment =>
131+
comment.user.type === 'Bot' && comment.body.includes(marker)
96132
);
97-
98-
const comment = `
99-
🚀 Preview package published!
100-
101-
Install with:
102-
\`\`\`bash
103-
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple socketdev==${version}
104-
\`\`\``;
105-
106-
if (botComment) {
107-
// Update existing comment
133+
if (existing) {
108134
await github.rest.issues.updateComment({
109-
owner: owner,
110-
repo: repo,
111-
comment_id: botComment.id,
112-
body: comment
135+
owner: context.repo.owner,
136+
repo: context.repo.repo,
137+
comment_id: existing.id,
138+
body,
113139
});
114140
} else {
115-
// Create new comment
116141
await github.rest.issues.createComment({
117-
owner: owner,
118-
repo: repo,
142+
owner: context.repo.owner,
143+
repo: context.repo.repo,
119144
issue_number: prNumber,
120-
body: comment
145+
body,
121146
});
122147
}
123-
124-
- name: Verify package is available
125-
if: steps.version_check.outputs.exists != 'true'
126-
id: verify_package
127-
env:
128-
VERSION: ${{ env.VERSION }}
129-
run: |
130-
for i in {1..30}; do
131-
if pip install --index-url 'https://test.pypi.org/simple/' --extra-index-url 'https://pypi.org/simple' socketdev==${VERSION}; then
132-
echo "Package ${VERSION} is now available and installable on Test PyPI"
133-
pip uninstall -y socketdev
134-
echo "success=true" >> $GITHUB_OUTPUT
135-
exit 0
136-
fi
137-
echo "Attempt $i: Package not yet installable, waiting 20s... (${i}/30)"
138-
sleep 20
139-
done
140-
echo "success=false" >> $GITHUB_OUTPUT
141-
exit 1

.hooks/sync_version.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,44 @@ def run_uv_lock() -> bool:
124124
return before != after
125125

126126

127+
def read_preview_id():
128+
if "--preview-id" not in sys.argv:
129+
return None
130+
131+
option_index = sys.argv.index("--preview-id")
132+
try:
133+
preview_id = sys.argv[option_index + 1]
134+
except IndexError:
135+
print("❌ `--preview-id` requires a numeric value.")
136+
sys.exit(1)
137+
138+
if not preview_id.isdigit():
139+
print("❌ `--preview-id` must contain digits only.")
140+
sys.exit(1)
141+
return preview_id
142+
143+
127144
def main():
128145
dev_mode = "--dev" in sys.argv
146+
skip_lock = "--skip-lock" in sys.argv
147+
preview_id = read_preview_id()
129148
current_version = read_version_from_version_file(VERSION_FILE)
130149
previous_version = read_version_from_git("socketdev/version.py")
131150

132151
print(f"Current: {current_version}, Previous: {previous_version}")
133152

153+
if preview_id is not None:
154+
if not dev_mode:
155+
print("❌ `--preview-id` can only be used with `--dev`.")
156+
sys.exit(1)
157+
base_version = current_version.split(".dev")[0]
158+
new_version = f"{base_version}.dev{preview_id}"
159+
inject_version(new_version)
160+
if not skip_lock:
161+
run_uv_lock()
162+
print(f"✅ Prepared deterministic preview version {new_version}.")
163+
sys.exit(0)
164+
134165
if current_version == previous_version:
135166
if dev_mode:
136167
base_version = current_version.split(".dev")[0] if ".dev" in current_version else current_version

0 commit comments

Comments
 (0)