From 76d4c3a649a82931d68a4bc63bf33d8f03572ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Arturo=20Garci=CC=81a?= Date: Wed, 12 Aug 2026 18:10:18 -0400 Subject: [PATCH] ci: add package publishing workflow --- .gitea/workflows/publish.yml | 192 +++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 .gitea/workflows/publish.yml diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..02a3cc2 --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,192 @@ +name: Publish Package + +on: + push: + branches: + - "**" + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + + 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://code.unpaismejor.org.do/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://code.unpaismejor.org.do/api/packages/tryton-do/pypi \ + dist/*