diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index 28efef1..3cf352e 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -24,23 +24,127 @@ jobs: python -m pip install --upgrade pip python -m pip install build twine + - name: Check publishing credentials + env: + TWINE_USERNAME: ${{ secrets.REGISTRY_USER }} + TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + test -n "$TWINE_USERNAME" || { echo "Missing REGISTRY_USER secret"; exit 1; } + test -n "$TWINE_PASSWORD" || { echo "Missing REGISTRY_PASSWORD secret"; exit 1; } + - name: Set CI package version + env: + TWINE_USERNAME: ${{ secrets.REGISTRY_USER }} + TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} run: | python - <<'PY' from pathlib import Path + from urllib.error import HTTPError + from urllib.parse import urlencode + from urllib.request import Request, urlopen + import base64 + import json import os import re + import tomllib - path = Path('tryton.cfg') - text = path.read_text() - match = re.search(r'(?m)^version=(.+)$', text) - if not match: + OWNER = 'tryton-do' + PACKAGE_TYPE = 'pypi' + API_ROOT = 'https://gitea.joseagrc.com/api/v1' + + def normalize(name): + return re.sub(r'[-_.]+', '-', name).lower() + + cfg_path = Path('tryton.cfg') + cfg_text = cfg_path.read_text() + cfg_match = re.search(r'(?m)^version=(.+)$', cfg_text) + if not cfg_match: raise SystemExit('tryton.cfg has no version entry') - base_version = match.group(1).strip() - run_number = os.environ.get('GITHUB_RUN_NUMBER', '0') - ci_version = f'{base_version}.post{run_number}' - text = re.sub(r'(?m)^version=.+$', f'version={ci_version}', text) - path.write_text(text) + base_version = cfg_match.group(1).strip() + version_match = re.fullmatch(r'(\d+)\.(\d+)\.(\d+)', base_version) + if not version_match: + raise SystemExit( + f'tryton.cfg version must be X.Y.Z, got {base_version!r}') + major, minor, patch = map(int, version_match.groups()) + + pyproject = tomllib.loads(Path('pyproject.toml').read_text()) + package_name = pyproject['project']['name'] + normalized_name = normalize(package_name) + + username = os.environ['TWINE_USERNAME'] + password = os.environ['TWINE_PASSWORD'] + token = base64.b64encode( + f'{username}:{password}'.encode()).decode() + + def request_json(path, params=None, missing_ok=False): + url = f'{API_ROOT}{path}' + if params: + url = f'{url}?{urlencode(params)}' + request = Request(url, headers={ + 'Accept': 'application/json', + 'Authorization': f'Basic {token}', + }) + try: + with urlopen(request, timeout=30) as response: + return json.load(response) + except HTTPError as error: + if missing_ok and error.code == 404: + return [] + raise + + def collect_versions(payload, package_specific=False): + versions = [] + if isinstance(payload, list): + for item in payload: + versions.extend(collect_versions(item, package_specific)) + elif isinstance(payload, dict): + item_name = payload.get('name') or payload.get('package_name') + item_type = payload.get('type') or payload.get('package_type') + item_version = payload.get('version') + if item_version and ( + package_specific + or (item_type == PACKAGE_TYPE + and normalize(item_name or '') == normalized_name)): + versions.append(str(item_version)) + for value in payload.values(): + if isinstance(value, (list, dict)): + versions.extend(collect_versions(value, package_specific)) + return versions + + versions = [] + for page in range(1, 11): + payload = request_json( + f'/packages/{OWNER}/{PACKAGE_TYPE}/{package_name}', + {'page': page, 'limit': 100}, + missing_ok=True) + page_versions = collect_versions(payload, package_specific=True) + versions.extend(page_versions) + if not page_versions: + break + + for page in range(1, 11): + payload = request_json( + f'/packages/{OWNER}', + {'page': page, 'limit': 100, 'type': PACKAGE_TYPE, + 'q': package_name}) + page_versions = collect_versions(payload) + versions.extend(page_versions) + if not page_versions: + break + + patch_pattern = re.compile(rf'^{major}\.{minor}\.(\d+)$') + patches = [patch] + for version in set(versions): + match = patch_pattern.fullmatch(version) + if match: + patches.append(int(match.group(1))) + + ci_version = f'{major}.{minor}.{max(patches) + 1}' + cfg_text = re.sub( + r'(?m)^version=.+$', f'version={ci_version}', cfg_text) + cfg_path.write_text(cfg_text) + print(f'Package name: {package_name}') + print(f'Published versions found: {sorted(set(versions))}') print(f'Package version: {ci_version}') PY @@ -52,14 +156,6 @@ jobs: run: | python -m twine check dist/* - - name: Check publishing credentials - env: - TWINE_USERNAME: ${{ secrets.REGISTRY_USER }} - TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} - run: | - test -n "$TWINE_USERNAME" || { echo "Missing REGISTRY_USER secret"; exit 1; } - test -n "$TWINE_PASSWORD" || { echo "Missing REGISTRY_PASSWORD secret"; exit 1; } - - name: Publish to Gitea PyPI registry env: TWINE_USERNAME: ${{ secrets.REGISTRY_USER }}