Skip to content

Commit 10579fb

Browse files
authored
Merge branch 'main' into lelia/diff-scan-polling
2 parents d99a122 + 4d66c3b commit 10579fb

4 files changed

Lines changed: 287 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: 149 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,198 @@
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 || github.run_id }}
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+
context:
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: 5
2225
permissions:
23-
id-token: write
2426
contents: read
25-
pull-requests: write
27+
pull-requests: read
28+
outputs:
29+
pr_number: ${{ steps.context.outputs.pr_number }}
30+
head_sha: ${{ steps.context.outputs.head_sha }}
31+
steps:
32+
- name: Validate pull request context
33+
id: context
34+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
35+
env:
36+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
37+
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
38+
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
39+
WORKFLOW_REF: ${{ github.ref }}
40+
with:
41+
script: |
42+
const rawPrNumber = context.eventName === 'workflow_dispatch'
43+
? process.env.INPUT_PR_NUMBER
44+
: process.env.EVENT_PR_NUMBER;
45+
if (!/^[1-9][0-9]*$/.test(rawPrNumber || '')) {
46+
core.setFailed('Pull request number must contain ASCII digits only.');
47+
return;
48+
}
49+
50+
if (context.eventName === 'workflow_dispatch') {
51+
const defaultRef = `refs/heads/${process.env.DEFAULT_BRANCH}`;
52+
if (process.env.WORKFLOW_REF !== defaultRef) {
53+
core.setFailed(`Run manual previews from ${defaultRef}.`);
54+
return;
55+
}
56+
}
57+
58+
const prNumber = Number(rawPrNumber);
59+
if (!Number.isSafeInteger(prNumber)) {
60+
core.setFailed('Pull request number is outside the supported range.');
61+
return;
62+
}
63+
const {data: pullRequest} = await github.rest.pulls.get({
64+
owner: context.repo.owner,
65+
repo: context.repo.repo,
66+
pull_number: prNumber,
67+
});
68+
if (pullRequest.state !== 'open') {
69+
core.setFailed(`Pull request #${prNumber} is not open.`);
70+
return;
71+
}
72+
if (pullRequest.head.repo?.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
73+
core.setFailed('Preview publication is limited to branches in this repository.');
74+
return;
75+
}
76+
77+
core.setOutput('pr_number', String(prNumber));
78+
core.setOutput('head_sha', pullRequest.head.sha);
79+
80+
build:
81+
needs: context
82+
runs-on: ubuntu-latest
83+
timeout-minutes: 10
84+
permissions:
85+
contents: read
86+
outputs:
87+
preview_version: ${{ steps.version.outputs.preview_version }}
2688
steps:
2789
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2890
with:
91+
ref: ${{ needs.context.outputs.head_sha }}
2992
fetch-depth: 0
3093
persist-credentials: false
94+
3195
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
3296
with:
33-
python-version: '3.13'
97+
python-version: "3.12"
3498

3599
- name: Install build tooling
36100
uses: ./.github/actions/setup-hatch
37101

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

41-
- name: Check if version exists on Test PyPI
42-
id: version_check
105+
- name: Inject deterministic preview version
106+
env:
107+
PREVIEW_ID: ${{ github.run_id }}
108+
RUN_ATTEMPT: ${{ github.run_attempt }}
43109
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
110+
PREVIEW_ID=$((PREVIEW_ID * 100 + RUN_ATTEMPT))
111+
python .hooks/sync_version.py --dev --preview-id "$PREVIEW_ID" --skip-lock
112+
113+
- name: Read preview version
58114
id: version
59-
run: |
60-
VERSION=$(hatch version | cut -d+ -f1)
61-
echo "VERSION=$VERSION" >> $GITHUB_ENV
115+
run: echo "preview_version=$(hatch version)" >> "$GITHUB_OUTPUT"
62116

63-
- name: Build package
64-
if: steps.version_check.outputs.exists != 'true'
117+
- name: Build and validate distributions
65118
run: |
66119
hatch build
120+
python -m twine check dist/*
121+
122+
- name: Install and smoke-test wheel locally
123+
run: |
124+
python -m venv "$RUNNER_TEMP/preview-check"
125+
"$RUNNER_TEMP/preview-check/bin/pip" install --upgrade pip
126+
"$RUNNER_TEMP/preview-check/bin/pip" install dist/*.whl
127+
"$RUNNER_TEMP/preview-check/bin/python" -c "import socketdev; from socketdev.version import __version__; print('preview wheel smoke OK', __version__)"
128+
129+
- name: Upload preview distributions
130+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
131+
with:
132+
name: socketdev-preview-${{ github.run_id }}-${{ github.run_attempt }}
133+
path: dist/*
134+
if-no-files-found: error
135+
retention-days: 14
136+
137+
publish:
138+
needs: [context, build]
139+
runs-on: ubuntu-latest
140+
timeout-minutes: 10
141+
permissions:
142+
contents: read
143+
id-token: write
144+
pull-requests: write
145+
steps:
146+
- name: Download preview distributions
147+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
148+
with:
149+
name: socketdev-preview-${{ github.run_id }}-${{ github.run_attempt }}
150+
path: dist
67151

68-
- name: Publish to Test PyPI
69-
if: steps.version_check.outputs.exists != 'true'
152+
- name: Publish to TestPyPI
70153
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
71154
with:
72155
repository-url: https://test.pypi.org/legacy/
73156
verbose: true
74157

75-
- name: Comment on PR
76-
if: steps.version_check.outputs.exists != 'true'
158+
- name: Comment on pull request
77159
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
78160
env:
79-
VERSION: ${{ env.VERSION }}
161+
PREVIEW_VERSION: ${{ needs.build.outputs.preview_version }}
162+
PR_NUMBER: ${{ needs.context.outputs.pr_number }}
80163
with:
81164
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({
165+
const marker = '<!-- socketdev-pr-preview -->';
166+
const prNumber = Number(process.env.PR_NUMBER);
167+
const version = process.env.PREVIEW_VERSION;
168+
const body = `${marker}
169+
🚀 SDK preview published: \`socketdev==${version}\`
170+
171+
\`\`\`bash
172+
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple socketdev==${version}
173+
\`\`\`
174+
175+
TestPyPI's package index can take several minutes to expose a newly uploaded version.`;
176+
const {data: comments} = await github.rest.issues.listComments({
88177
owner: context.repo.owner,
89178
repo: context.repo.repo,
90179
issue_number: prNumber,
91180
});
92-
93-
const botComment = comments.data.find(comment =>
94-
comment.user.type === 'Bot' &&
95-
comment.body.includes('🚀 Preview package published!')
181+
const existing = comments.find(comment =>
182+
comment.user.type === 'Bot' && comment.body.includes(marker)
96183
);
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
184+
if (existing) {
108185
await github.rest.issues.updateComment({
109-
owner: owner,
110-
repo: repo,
111-
comment_id: botComment.id,
112-
body: comment
186+
owner: context.repo.owner,
187+
repo: context.repo.repo,
188+
comment_id: existing.id,
189+
body,
113190
});
114191
} else {
115-
// Create new comment
116192
await github.rest.issues.createComment({
117-
owner: owner,
118-
repo: repo,
193+
owner: context.repo.owner,
194+
repo: context.repo.repo,
119195
issue_number: prNumber,
120-
body: comment
196+
body,
121197
});
122198
}
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.isascii() or not preview_id.isdigit():
139+
print("❌ `--preview-id` must contain ASCII 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)