From 9bdd886d187e3954118adc6aece9d17704972dd2 Mon Sep 17 00:00:00 2001 From: Daniel Dolezal Date: Tue, 20 Oct 2026 12:56:24 +0200 Subject: [PATCH] build: harden just publish with token lookup and dry run - Move token lookup into scripts/pypi_token.sh, shared by both recipes - Resolve UV_PUBLISH_TOKEN, then .env, then pass api/packages/pypi - Fail with a clear hint instead of dropping into an interactive prompt - Never print the token, only its length - Gate publish behind version-check so version drift stops the build - Add publish-dry, same path as publish but with uv publish --dry-run - Pass --check-url so re-runs skip files already on PyPI - Hoist dist dir, token entry, and check URL into justfile variables - Add .env and .env.* to .gitignore The old recipe sourced .env for credentials, but .env was never ignored, so a token file dropped there would have been committed and shipped inside the sdist. Verified token precedence for env, .env and password-store, the missing-token exit code, the version-check gate, and a dry run against an unpublished version. --- .gitignore | 2 ++ justfile | 33 ++++++++++++++++++++++++--------- scripts/pypi_token.sh | 30 ++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 scripts/pypi_token.sh diff --git a/.gitignore b/.gitignore index 210d49a..3a1c54f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ dist/ secrets/ *.pem *.pem.gpg +.env +.env.* # Python __pycache__/ diff --git a/justfile b/justfile index df56b19..764c515 100644 --- a/justfile +++ b/justfile @@ -4,6 +4,11 @@ # Default browser profile for `install` / quick checks browser := "brave" +# Where `publish` builds, and where it looks up the PyPI token +dist_dir := "/tmp/dist-browser-cli" +pypi_token_entry := "api/packages/pypi" +pypi_check_url := "https://pypi.org/simple/" + # Show available recipes default: @just --list @@ -74,17 +79,27 @@ version-check: ext=$(grep -m1 '"version"' extension/manifest.json | cut -d'"' -f4); \ if [ "$py" = "$ext" ]; then echo "ok: $py"; else echo "MISMATCH pyproject=$py manifest=$ext"; exit 1; fi -# Build into /tmp/dist-browser-cli and publish using credentials from .env -publish: +# Build and upload to PyPI. Token: UV_PUBLISH_TOKEN, else .env, else password-store. +publish: version-check #!/usr/bin/env bash set -euo pipefail - set -a - [ ! -f .env ] || source .env - set +a - rm -rf /tmp/dist-browser-cli - mkdir -p /tmp/dist-browser-cli - uv build --out-dir /tmp/dist-browser-cli - uv publish /tmp/dist-browser-cli/* + source scripts/pypi_token.sh + load_pypi_token "{{pypi_token_entry}}" || exit 1 + rm -rf "{{dist_dir}}" + mkdir -p "{{dist_dir}}" + uv build --out-dir "{{dist_dir}}" + uv publish --check-url "{{pypi_check_url}}" "{{dist_dir}}"/* + +# Same build and token path as `publish`, but uploads nothing +publish-dry: version-check + #!/usr/bin/env bash + set -euo pipefail + source scripts/pypi_token.sh + load_pypi_token "{{pypi_token_entry}}" || exit 1 + rm -rf "{{dist_dir}}" + mkdir -p "{{dist_dir}}" + uv build --out-dir "{{dist_dir}}" + uv publish --dry-run --check-url "{{pypi_check_url}}" "{{dist_dir}}"/* # ── Demos ────────────────────────────────────────────────────────────── diff --git a/scripts/pypi_token.sh b/scripts/pypi_token.sh new file mode 100644 index 0000000..b3b9bb9 --- /dev/null +++ b/scripts/pypi_token.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Resolve the PyPI token for `uv publish` without ever printing it. +# +# Order: an already exported UV_PUBLISH_TOKEN, then .env in the repo root, then +# the password-store entry passed as $1. The repo intentionally ships no .env, +# so the password-store path is the normal one on a dev machine. + +load_pypi_token() { + local entry="${1:-}" + + if [ -z "${UV_PUBLISH_TOKEN:-}" ] && [ -f .env ]; then + set -a + # shellcheck disable=SC1091 + source .env + set +a + fi + + if [ -z "${UV_PUBLISH_TOKEN:-}" ] && [ -n "$entry" ] && command -v pass >/dev/null 2>&1; then + UV_PUBLISH_TOKEN="$(pass "$entry" 2>/dev/null | head -1 || true)" + fi + + if [ -z "${UV_PUBLISH_TOKEN:-}" ]; then + echo "error: no PyPI token found." >&2 + echo " export UV_PUBLISH_TOKEN=..., put it in .env, or store it in: pass ${entry:-}" >&2 + return 1 + fi + + export UV_PUBLISH_TOKEN + echo "Using PyPI token (${#UV_PUBLISH_TOKEN} chars) for upload." +}