name: Test and Publish Package on: push: branches: - "**" pull_request: branches: - "**" workflow_dispatch: jobs: test: 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 package and test dependencies run: | python -m pip install --upgrade pip python -m pip install -e '.[test]' - name: Compile Python sources run: | python -m compileall . - name: Run module tests run: | python -m unittest discover -s tests -p 'test*.py' - name: Build package run: | python -m pip install build twine python -m build - name: Check package run: | python -m twine check dist/* publish: runs-on: ubuntu-latest needs: test if: github.event_name != 'pull_request' steps: - name: Checkout uses: actions/checkout@v4 - name: Detect package branch run: | python - <<'PY' import os import re branch = ( os.environ.get('GITHUB_REF_NAME') or os.environ.get('GITHUB_HEAD_REF') or '') publish = bool(re.fullmatch(r'\d+\.\d+', branch)) with open(os.environ['GITHUB_ENV'], 'a') as env: env.write(f'PUBLISH_PACKAGE={"true" if publish else "false"}\n') if publish: env.write(f'PACKAGE_SERIES={branch}\n') if publish: print(f'Publishing package series: {branch}') else: print(f'Skipping package publish for branch: {branch}') PY - name: Set up Python if: env.PUBLISH_PACKAGE == 'true' uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install build tools if: env.PUBLISH_PACKAGE == 'true' run: | python -m pip install --upgrade pip python -m pip install build twine - name: Check publishing credentials if: env.PUBLISH_PACKAGE == 'true' 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 if: env.PUBLISH_PACKAGE == 'true' 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') series = os.environ['PACKAGE_SERIES'] major, minor = map(int, series.split('.')) 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 = [] for version in set(versions): match = patch_pattern.fullmatch(version) if match: patches.append(int(match.group(1))) next_patch = max(patches) + 1 if patches else 0 ci_version = f'{major}.{minor}.{next_patch}' 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 if: env.PUBLISH_PACKAGE == 'true' run: | python -m build - name: Check package if: env.PUBLISH_PACKAGE == 'true' run: | python -m twine check dist/* - name: Publish to Gitea PyPI registry if: env.PUBLISH_PACKAGE == 'true' 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/*