name: Publish Package on: push: branches: - "**" workflow_dispatch: jobs: publish: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install build tools run: | 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 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 = 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 - name: Build package run: | python -m build - name: Check package run: | python -m twine check dist/* - name: Publish to Gitea PyPI registry env: TWINE_USERNAME: ${{ secrets.REGISTRY_USER }} TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} run: | python -m twine upload \ --repository-url https://gitea.joseagrc.com/api/packages/tryton-do/pypi \ dist/*