Skip to content

Commit 4975cfc

Browse files
drernieclaude
andauthored
2025 09 20 uv package (#174)
## Summary - add Phase 1 design/episodes/checklist documents for uv packaging in release.sh - implement credential-free `python-dist` build command in `bin/release.sh` with matching make target - add behavioral tests for the build command; document future `python-publish` path in specs and CLAUDE notes ## Testing - PYTHONPATH=src uv run pytest tests/unit/test_release_uv.py -k python -v - make test-unit --------- Co-authored-by: Claude <[email protected]>
1 parent 39d4344 commit 4975cfc

27 files changed

+1054
-18
lines changed

.github/actions/create-release/action.yml

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
name: 'Create Release'
2-
description: 'Build DXT package and create GitHub release with bundle'
2+
description: 'Build Python package, DXT package, and create GitHub release'
33
inputs:
4-
tag-version:
4+
package-version:
55
description: 'Version from git tag (e.g., 0.5.9-dev-20250904075318)'
66
required: true
7+
pypi-repository-url:
8+
description: 'PyPI repository URL (empty for PyPI, https://test.pypi.org/legacy/ for TestPyPI)'
9+
required: false
10+
default: ''
11+
skip-existing:
12+
description: 'Skip existing packages during upload'
13+
required: false
14+
default: 'false'
715

816
outputs:
917
release-url:
@@ -13,10 +21,26 @@ outputs:
1321
runs:
1422
using: 'composite'
1523
steps:
24+
- name: Install build dependencies
25+
shell: bash
26+
run: uv sync --group dev
27+
28+
- name: Build python distributions
29+
shell: bash
30+
run: make python-dist
31+
32+
- name: Publish to PyPI/TestPyPI
33+
uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0
34+
with:
35+
repository-url: ${{ inputs.pypi-repository-url }}
36+
packages-dir: dist/
37+
skip-existing: ${{ inputs.skip-existing }}
38+
verbose: true
39+
1640
- name: Build DXT package
17-
shell: bash
41+
shell: bash
1842
run: make dxt
19-
43+
2044
- name: Validate DXT package
2145
shell: bash
2246
run: make dxt-validate
@@ -29,13 +53,13 @@ runs:
2953
id: create-release
3054
uses: softprops/action-gh-release@v2
3155
with:
32-
name: "Quilt MCP DXT v${{ inputs.tag-version }}"
56+
name: "Quilt MCP DXT v${{ inputs.package-version }}"
3357
files: |
3458
dist/*-release.zip
3559
draft: false
36-
prerelease: ${{ contains(inputs.tag-version, '-') }}
60+
prerelease: ${{ contains(inputs.package-version, '-') }}
3761
generate_release_notes: true
38-
62+
3963
- name: Upload DXT artifacts
4064
uses: actions/upload-artifact@v4
4165
with:

.github/workflows/pr.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ jobs:
6060
runs-on: ubuntu-latest
6161
needs: test
6262
timeout-minutes: 30
63+
environment: testpypi
64+
permissions:
65+
contents: write
66+
id-token: write
6367
if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-dev-')
6468

6569
steps:
@@ -93,4 +97,6 @@ jobs:
9397
- name: Create dev release
9498
uses: ./.github/actions/create-release
9599
with:
96-
tag-version: ${{ steps.version.outputs.tag_version }}
100+
package-version: ${{ steps.version.outputs.tag_version }}
101+
pypi-repository-url: https://test.pypi.org/legacy/
102+
skip-existing: true

.github/workflows/push.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ jobs:
5353
runs-on: ubuntu-latest
5454
timeout-minutes: 30
5555
needs: [test]
56+
environment: pypi
57+
permissions:
58+
contents: write
59+
id-token: write
5660
# Only run for production tags (v* but not v*-dev-*) and only after tests pass
5761
if: startsWith(github.ref, 'refs/tags/v') && (!contains(github.ref, '-dev-')) && contains(needs.test.result, 'success')
5862

@@ -76,4 +80,5 @@ jobs:
7680
- name: Create production release
7781
uses: ./.github/actions/create-release
7882
with:
79-
tag-version: ${{ steps.version.outputs.tag_version }}
83+
package-version: ${{ steps.version.outputs.tag_version }}
84+
# pypi-repository-url defaults to '' for PyPI

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,11 @@ The following permissions are granted for this repository:
406406
- `TelemetryCollector.cleanup_old_sessions` clears `current_session_id` when an aged session is evicted—tests that probe cleanup should confirm the pointer resets.
407407
- Workflow orchestration APIs reject blank workflow IDs; trim identifiers in tests when constructing fixtures to avoid silent acceptance.
408408

409+
### 2025-09-20 uv packaging notes
410+
- DXT packaging currently runs through `make.deploy` using `uv pip install`; the UV PyPI build flow lives in `bin/release.sh python-dist` with `make python-dist`, mirroring how `make dxt` exposes DXT packaging.
411+
- `python-dist` builds local artifacts without credentials. `bin/release.sh python-publish` (via `make python-publish`) requires either `UV_PUBLISH_TOKEN` or `UV_PUBLISH_USERNAME`/`UV_PUBLISH_PASSWORD`, defaults to TestPyPI (`PYPI_PUBLISH_URL`/`PYPI_REPOSITORY_URL` override), and respects `DIST_DIR`.
412+
- GitHub Actions builds dist artifacts via `python-dist`, publishes them with `pypa/gh-action-pypi-publish`, then runs `make dxt`, `make dxt-validate`, and `make release-zip` to keep DXT parity. Secrets supply the PyPI/TestPyPI token (`secrets.PYPI_TOKEN`).
413+
409414
## important-instruction-reminders
410415

411416
Do what has been asked; nothing more, nothing less.

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ All notable changes to this project will be documented in this file.
66
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
88

9+
## [0.6.9] - 2025-09-21
10+
11+
### Added
12+
13+
- **uv-based Python Packaging Pipeline**: Complete integration for PyPI/TestPyPI publishing
14+
- `make python-dist` - Build wheel and sdist artifacts using uv build system
15+
- `make python-publish` - Publish artifacts to PyPI/TestPyPI with credential validation
16+
- GitHub Actions integration with Trusted Publishing support via PyPA action
17+
- Full compatibility with existing DXT packaging system
18+
919
## [0.6.8] - 2025-09-20
1020

1121
### Added

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ help:
3131
@echo "📦 Production Workflow (make.deploy):"
3232
@echo " make deploy-build - Prepare production build environment"
3333
@echo " make dxt - Create DXT package"
34+
@echo " make python-dist - Build wheel + sdist into dist/ using uv (no publish)"
35+
@echo " make python-publish - Publish dist/ artifacts via uv (requires credentials)"
3436
@echo " make dxt-validate - Validate DXT package"
3537
@echo " make release-zip - Create release bundle with documentation"
3638
@echo " make release - Create and push release tag"

bin/release.sh

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,117 @@ set -e
88
REPO_URL=$(git config --get remote.origin.url | sed 's/.*github.com[:/]\(.*\)\.git/\1/')
99
DRY_RUN=${DRY_RUN:-0}
1010

11+
12+
ensure_publish_env() {
13+
if [ -n "$UV_PUBLISH_TOKEN" ]; then
14+
PUBLISH_AUTH_MODE="token"
15+
return 0
16+
fi
17+
18+
if [ -n "$UV_PUBLISH_USERNAME" ] && [ -n "$UV_PUBLISH_PASSWORD" ]; then
19+
PUBLISH_AUTH_MODE="userpass"
20+
return 0
21+
fi
22+
23+
echo "❌ Missing publish credentials. Set UV_PUBLISH_TOKEN or UV_PUBLISH_USERNAME and UV_PUBLISH_PASSWORD."
24+
return 1
25+
}
26+
27+
python_publish() {
28+
echo "🚀 Starting python-publish workflow"
29+
30+
ensure_publish_env || return 1
31+
32+
if ! command -v uv >/dev/null 2>&1; then
33+
echo "❌ uv not found - install uv package manager"
34+
return 1
35+
fi
36+
37+
local dist_dir="${DIST_DIR:-dist}"
38+
if [ ! -d "$dist_dir" ]; then
39+
echo "❌ Distribution directory '$dist_dir' does not exist. Run 'make python-dist' to build artifacts first."
40+
return 1
41+
fi
42+
43+
local artifact_count
44+
artifact_count=$(find "$dist_dir" -maxdepth 1 -type f \( -name "*.whl" -o -name "*.tar.gz" \) | wc -l | tr -d ' ')
45+
if [ "$artifact_count" = "0" ]; then
46+
echo "❌ No artifacts found in '$dist_dir'. Run 'make python-dist' to build artifacts before publishing."
47+
return 1
48+
fi
49+
50+
local publish_url="${PYPI_PUBLISH_URL:-${PYPI_REPOSITORY_URL:-https://test.pypi.org/legacy/}}"
51+
local -a artifacts
52+
while IFS= read -r artifact; do
53+
artifacts+=("$artifact")
54+
done < <(find "$dist_dir" -maxdepth 1 -type f \( -name "*.whl" -o -name "*.tar.gz" \) | sort)
55+
56+
local log_cmd="uv publish"
57+
local -a publish_cmd
58+
publish_cmd=(uv publish)
59+
60+
if [ -n "$publish_url" ]; then
61+
log_cmd="$log_cmd --publish-url $publish_url"
62+
publish_cmd+=(--publish-url "$publish_url")
63+
fi
64+
65+
if [ ${#artifacts[@]} -eq 0 ]; then
66+
echo "❌ No artifacts found in '$dist_dir'. Run 'make python-dist' to build artifacts before publishing."
67+
return 1
68+
fi
69+
70+
for artifact in "${artifacts[@]}"; do
71+
log_cmd="$log_cmd $artifact"
72+
publish_cmd+=("$artifact")
73+
done
74+
75+
if [ "$PUBLISH_AUTH_MODE" = "token" ]; then
76+
log_cmd="$log_cmd --token ****"
77+
publish_cmd+=(--token "$UV_PUBLISH_TOKEN")
78+
else
79+
log_cmd="$log_cmd --username $UV_PUBLISH_USERNAME --password ****"
80+
publish_cmd+=(--username "$UV_PUBLISH_USERNAME" --password "$UV_PUBLISH_PASSWORD")
81+
fi
82+
83+
if [ "$DRY_RUN" = "1" ]; then
84+
echo "🔍 DRY RUN: Would run: $log_cmd"
85+
return 0
86+
fi
87+
88+
echo "📦 Publishing artifacts from $dist_dir to ${publish_url:-https://test.pypi.org/legacy/}"
89+
"${publish_cmd[@]}"
90+
echo "✅ python-publish completed"
91+
92+
local package_name
93+
package_name=$(python3 - <<'PY'
94+
import tomllib
95+
with open('pyproject.toml', 'rb') as f:
96+
data = tomllib.load(f)
97+
print(data["project"]["name"])
98+
PY
99+
)
100+
101+
local package_version
102+
package_version=$(python3 - <<'PY'
103+
import tomllib
104+
with open('pyproject.toml', 'rb') as f:
105+
data = tomllib.load(f)
106+
print(data["project"]["version"])
107+
PY
108+
)
109+
110+
local project_url=""
111+
if [[ "${publish_url:-https://test.pypi.org/legacy/}" == *"test.pypi.org"* ]]; then
112+
project_url="https://test.pypi.org/project/${package_name}/${package_version}/"
113+
elif [[ "${publish_url}" == *"pypi.org"* ]]; then
114+
project_url="https://pypi.org/project/${package_name}/${package_version}/"
115+
fi
116+
117+
if [ -n "$project_url" ]; then
118+
echo "🔗 View package at $project_url"
119+
fi
120+
}
121+
11122
check_clean_repo() {
12123
echo "🔍 Checking repository state..."
13124
if [ -n "$(git status --porcelain)" ]; then
@@ -232,18 +343,25 @@ case "${1:-}" in
232343
fi
233344
tag_release
234345
;;
346+
"python-publish")
347+
if [ "${2:-}" = "--dry-run" ]; then
348+
DRY_RUN=1
349+
fi
350+
python_publish
351+
;;
235352
"bump")
236353
if [ "${3:-}" = "--dry-run" ]; then
237354
DRY_RUN=1
238355
fi
239356
bump_version "${2:-}"
240357
;;
241358
*)
242-
echo "Usage: $0 {dev|release|bump} [options]"
359+
echo "Usage: $0 {dev|release|python-publish|bump} [options]"
243360
echo ""
244361
echo "Commands:"
245362
echo " dev - Create development tag with timestamp"
246-
echo " release - Create release tag from pyproject.toml version"
363+
echo " release - Create release tag from pyproject.toml version"
364+
echo " python-publish - Publish artifacts to PyPI/TestPyPI via uv"
247365
echo " bump {type} - Bump version in pyproject.toml"
248366
echo ""
249367
echo "Bump types:"
@@ -256,6 +374,11 @@ case "${1:-}" in
256374
echo ""
257375
echo "Environment Variables:"
258376
echo " DRY_RUN=1 - Enable dry-run mode"
377+
echo " DIST_DIR - Override packaging output directory (default: dist)"
378+
echo " PYPI_PUBLISH_URL - Override publish endpoint (default: https://test.pypi.org/legacy/)"
379+
echo " PYPI_REPOSITORY_URL - Deprecated alias for PYPI_PUBLISH_URL"
380+
echo " UV_PUBLISH_TOKEN or UV_PUBLISH_USERNAME/UV_PUBLISH_PASSWORD"
381+
echo " - Credentials required before running python-publish"
259382
echo ""
260383
echo "Examples:"
261384
echo " $0 bump patch # Bump patch version"
@@ -264,4 +387,4 @@ case "${1:-}" in
264387
echo " $0 release # Create release tag"
265388
exit 1
266389
;;
267-
esac
390+
esac

env.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,16 @@ FASTMCP_TRANSPORT=stdio
2222

2323
# ngrok Configuration for remote tunneling (optional)
2424
NGROK_DOMAIN=my-free-domain.ngrok-free.app
25+
26+
# Python packaging (uv)
27+
# Required for publishing: set either UV_PUBLISH_TOKEN or both UV_PUBLISH_USERNAME/UV_PUBLISH_PASSWORD.
28+
# Leave unset to build locally without publishing.
29+
UV_PUBLISH_TOKEN=your-testpypi-token
30+
UV_PUBLISH_USERNAME=testpypi-username
31+
UV_PUBLISH_PASSWORD=testpypi-password
32+
33+
# Optional: Override publish endpoint (defaults to TestPyPI)
34+
PYPI_PUBLISH_URL=https://test.pypi.org/legacy/
35+
36+
# Optional: Override dist output directory (defaults to dist/)
37+
DIST_DIR=dist

make.deploy

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ DEPS_MARKER := $(BUILD_DIR)/.deps-installed
2626
ASSET_FILES := $(wildcard $(ASSETS_DIR)/*)
2727
APP_FILES := $(shell find $(APP_DIR)/quilt_mcp -name "*.py" 2>/dev/null || true)
2828

29-
.PHONY: deploy-build dxt dxt-validate release-zip deploy-clean check-tools
29+
.PHONY: deploy-build dxt dxt-validate python-dist python-publish release-zip deploy-clean check-tools
3030

3131
# Check for required tools
3232
check-tools:
@@ -87,6 +87,17 @@ $(DXT_PACKAGE): build-contents
8787
dxt: $(DXT_PACKAGE)
8888
@echo "✅ DXT package created: $(DXT_PACKAGE)"
8989

90+
# Python Package Distribution
91+
python-dist: check-tools
92+
@echo "🚀 Building Python artifacts..."
93+
@mkdir -p $(DIST_DIR)
94+
@uv sync --group dev
95+
@uv run python -m build --wheel --sdist --outdir $(DIST_DIR)
96+
@echo "✅ Python packaging complete"
97+
98+
python-publish:
99+
@./bin/release.sh python-publish
100+
90101
# DXT Package Validation
91102
dxt-validate: check-tools dxt
92103
@echo "Validating DXT package..."
@@ -133,4 +144,4 @@ release-dev-tag:
133144
deploy-clean:
134145
@echo "Cleaning build artifacts..."
135146
@rm -rf $(BUILD_DIR) $(DIST_DIR)
136-
@echo "✅ Deploy cleanup completed"
147+
@echo "✅ Deploy cleanup completed"

0 commit comments

Comments
 (0)