Hatchling cannot build editable installs when project sources rewrite a prefix. Install the package normally with test extras in Gitea Actions and keep the workflow coverage test aligned with that supported install path.
231 lines
7.6 KiB
YAML
231 lines
7.6 KiB
YAML
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 '.[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/*
|