From f0e635ebe288d906feffd4b929ff7a39bb541d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D1=8F=D1=82=D0=BA=D0=B8=D0=BD=D0=90=D1=80=D1=82?= =?UTF-8?q?=D1=91=D0=BC?= Date: Wed, 27 May 2026 17:47:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83=20?= =?UTF-8?q?=D1=81=D1=83=D1=89=D0=B5=D1=81=D1=82=D0=B2=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B5=D0=B9=20=D0=B2=20Gitea=20Packages=20=D0=B8=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BE?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D1=85?= =?UTF-8?q?=20=D0=BE=D0=BA=D1=80=D1=83=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 62 ++++++++++++++++++++++++++++++++++++++++- Makefile | 23 ++++++++------- README.md | 17 ++++++----- 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 399ca92..a5c2948 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -93,15 +93,75 @@ jobs: test -n "$PACKAGE_USERNAME" || (echo "PACKAGE_USERNAME secret is required" && exit 1) test -n "$PACKAGE_TOKEN" || (echo "PACKAGE_TOKEN secret is required" && exit 1) + - name: Check whether this package version already exists + id: package_registry + env: + PYPI_REPOSITORY_URL: ${{ secrets.PYPI_REPOSITORY_URL }} + PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} + PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} + PACKAGE_NAME: ${{ steps.package_meta.outputs.name }} + PACKAGE_VERSION: ${{ steps.package_meta.outputs.version }} + run: | + $HOME/.local/bin/uv run python - <<'PY' >> "$GITHUB_OUTPUT" + import base64 + import os + import re + from urllib.error import HTTPError, URLError + from urllib.request import Request, urlopen + + repository_url = os.environ['PYPI_REPOSITORY_URL'].rstrip('/') + package_name = os.environ['PACKAGE_NAME'] + package_version = os.environ['PACKAGE_VERSION'] + normalized_name = re.sub(r'[-_.]+', '-', package_name).lower() + filename_candidates = { + f"{re.sub(r'[-_.]+', '_', package_name)}-{package_version}", + f'{normalized_name}-{package_version}', + } + index_url = f'{repository_url}/simple/{normalized_name}/' + credentials = f"{os.environ['PACKAGE_USERNAME']}:{os.environ['PACKAGE_TOKEN']}".encode() + authorization = base64.b64encode(credentials).decode() + request = Request(index_url, headers={'Authorization': f'Basic {authorization}'}) + + try: + with urlopen(request, timeout=15) as response: + page = response.read().decode('utf-8', 'replace') + except HTTPError as exc: + if exc.code == 404: + print('exists=false') + print(f'index_url={index_url}') + else: + raise + except URLError as exc: + raise SystemExit(f'Unable to query package registry: {exc}') from exc + else: + exists = any(candidate in page for candidate in filename_candidates) + print(f"exists={'true' if exists else 'false'}") + print(f'index_url={index_url}') + PY + + - name: Show package publish decision + run: | + echo "Registry index: ${{ steps.package_registry.outputs.index_url }}" + echo "Version exists: ${{ steps.package_registry.outputs.exists }}" + - name: Build distribution + if: steps.package_registry.outputs.exists != 'true' run: PATH="$HOME/.local/bin:$PATH" make build - name: Check built artifacts + if: steps.package_registry.outputs.exists != 'true' run: PATH="$HOME/.local/bin:$PATH" uv run --with twine twine check dist/* + - name: Skip upload for existing package version + if: steps.package_registry.outputs.exists == 'true' + run: | + echo "Version ${{ steps.package_meta.outputs.version }} already exists in Gitea Packages." + echo "Bump [project].version in pyproject.toml to publish a new release." + - name: Publish to Gitea PyPI registry + if: steps.package_registry.outputs.exists != 'true' env: TWINE_REPOSITORY_URL: ${{ secrets.PYPI_REPOSITORY_URL }} TWINE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} TWINE_PASSWORD: ${{ secrets.PACKAGE_TOKEN }} - run: PATH="$HOME/.local/bin:$PATH" uv run --with twine twine upload --skip-existing dist/* \ No newline at end of file + run: PATH="$HOME/.local/bin:$PATH" uv run --with twine twine upload dist/* \ No newline at end of file diff --git a/Makefile b/Makefile index b0d4b3c..6da18b1 100644 --- a/Makefile +++ b/Makefile @@ -27,16 +27,19 @@ package-version: $(UV) run python -c "import tomllib; from pathlib import Path; print(tomllib.loads(Path('pyproject.toml').read_text(encoding='utf-8'))['project']['version'])" publish-gitea: - @test -n "$$GITEA_PYPI_REPOSITORY_URL" || (echo "GITEA_PYPI_REPOSITORY_URL is required" && exit 1) - @test -n "$$GITEA_PACKAGE_USERNAME" || (echo "GITEA_PACKAGE_USERNAME is required" && exit 1) - @test -n "$$GITEA_PACKAGE_TOKEN" || (echo "GITEA_PACKAGE_TOKEN is required" && exit 1) - @echo "Publishing version $$($(MAKE) --no-print-directory package-version)" - $(UV) build - $(UV) run --with twine twine check dist/* - TWINE_REPOSITORY_URL="$$GITEA_PYPI_REPOSITORY_URL" \ - TWINE_USERNAME="$$GITEA_PACKAGE_USERNAME" \ - TWINE_PASSWORD="$$GITEA_PACKAGE_TOKEN" \ - $(UV) run --with twine twine upload --skip-existing dist/* + @repository_url="$${PYPI_REPOSITORY_URL:-$$GITEA_PYPI_REPOSITORY_URL}"; \ + username="$${PACKAGE_USERNAME:-$$GITEA_PACKAGE_USERNAME}"; \ + token="$${PACKAGE_TOKEN:-$$GITEA_PACKAGE_TOKEN}"; \ + test -n "$$repository_url" || (echo "PYPI_REPOSITORY_URL or GITEA_PYPI_REPOSITORY_URL is required" && exit 1); \ + test -n "$$username" || (echo "PACKAGE_USERNAME or GITEA_PACKAGE_USERNAME is required" && exit 1); \ + test -n "$$token" || (echo "PACKAGE_TOKEN or GITEA_PACKAGE_TOKEN is required" && exit 1); \ + echo "Publishing version $$($(MAKE) --no-print-directory package-version)"; \ + $(UV) build; \ + $(UV) run --with twine twine check dist/*; \ + TWINE_REPOSITORY_URL="$$repository_url" \ + TWINE_USERNAME="$$username" \ + TWINE_PASSWORD="$$token" \ + $(UV) run --with twine twine upload dist/* clean-branches: ifeq ($(OS),Windows_NT) diff --git a/README.md b/README.md index 39536e5..79d264e 100644 --- a/README.md +++ b/README.md @@ -88,23 +88,26 @@ make build Публикация настроена в Gitea PyPI registry по документации Gitea Packages. Версия пакета берётся из секции [project] -> version в pyproject.toml. +Gitea не поддерживает повторную публикацию той же версии, поэтому CI сначала проверяет registry и пропускает upload, если эта версия уже существует. Нужные secrets для CI workflow: -- GITEA_PYPI_REPOSITORY_URL: полный endpoint вида https://gitea.example.com/api/packages//pypi -- GITEA_PACKAGE_USERNAME: пользователь Gitea -- GITEA_PACKAGE_TOKEN: personal access token с правом package write +- PYPI_REPOSITORY_URL: полный endpoint вида https://gitea.example.com/api/packages//pypi +- PACKAGE_USERNAME: пользователь Gitea +- PACKAGE_TOKEN: personal access token с правом package write Публикация идёт автоматически на push в main после успешных lint/typecheck/test jobs. -Если версия пакета уже существует в registry, upload будет пропущен через --skip-existing. +Если версия пакета уже существует в registry, upload будет пропущен до вызова twine upload. Если версия в pyproject.toml увеличена, в Gitea Packages появится новая версия пакета, а старые версии останутся доступными. Локально тот же сценарий можно выполнить так: ```bash -export GITEA_PYPI_REPOSITORY_URL="https://gitea.example.com/api/packages//pypi" -export GITEA_PACKAGE_USERNAME="" -export GITEA_PACKAGE_TOKEN="" +export PYPI_REPOSITORY_URL="https://gitea.example.com/api/packages//pypi" +export PACKAGE_USERNAME="" +export PACKAGE_TOKEN="" make package-version make publish-gitea ``` + +Для обратной совместимости локальная команда publish-gitea также принимает переменные GITEA_PYPI_REPOSITORY_URL, GITEA_PACKAGE_USERNAME и GITEA_PACKAGE_TOKEN.