name: Developer Meta-package Release on: push: tags: - "v*" workflow_dispatch: inputs: version: description: Existing protected release version without leading v required: true type: string jobs: publish-package: runs-on: ubuntu-latest env: GITEA_REPOSITORY: ${{ gitea.repository }} TRIGGER_TAG: ${{ gitea.ref_name }} REQUESTED_VERSION: ${{ inputs.version }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: fetch-depth: 0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: python-version: "3.12" - name: Validate protected release tag and package version run: | python - <<'PY' import os from pathlib import Path import subprocess import tomllib requested_version = os.environ.get("REQUESTED_VERSION", "").strip() tag = f"v{requested_version}" if requested_version else os.environ["TRIGGER_TAG"] if not tag.startswith("v") or not tag[1:]: raise SystemExit("release tag is missing") project_text = subprocess.check_output( ["git", "show", f"{tag}:packages/govoplan-meta/pyproject.toml"], text=True, ) project = tomllib.loads(project_text)["project"] if tag != f"v{project['version']}": raise SystemExit("meta-package version does not match the release tag") tag_commit = subprocess.check_output( ["git", "rev-parse", f"refs/tags/{tag}^{{commit}}"], text=True ).strip() if subprocess.run( ["git", "merge-base", "--is-ancestor", tag_commit, "origin/main"] ).returncode: raise SystemExit("release tag is not contained in main") if not requested_version: head_commit = subprocess.check_output( ["git", "rev-parse", "HEAD"], text=True ).strip() if head_commit != tag_commit: raise SystemExit("tag-triggered checkout does not match the release tag") with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file: env_file.write(f"RELEASE_TAG={tag}\n") subprocess.run(["git", "checkout", "--detach", tag_commit], check=True) PY - name: Build developer package run: | set -euo pipefail python -m pip install --disable-pip-version-check build==1.5.0 twine==7.0.0 python -m build --wheel --outdir dist packages/govoplan-meta python -m twine check dist/*.whl python - <<'PY' import hashlib import json import os from pathlib import Path import subprocess wheels = tuple(Path("dist").glob("*.whl")) if len(wheels) != 1: raise SystemExit("meta release must contain exactly one wheel") wheel = wheels[0] evidence = { "schema_version": "1", "repository": os.environ["GITEA_REPOSITORY"], "tag": os.environ["RELEASE_TAG"], "commit": subprocess.check_output( ["git", "rev-parse", "HEAD"], text=True ).strip(), "artifacts": [ { "filename": wheel.name, "sha256": hashlib.sha256(wheel.read_bytes()).hexdigest(), "size": wheel.stat().st_size, } ], } Path("dist/package-artifacts.json").write_text( json.dumps(evidence, indent=2, sort_keys=True) + "\n", encoding="utf-8" ) PY - name: Retain package hash evidence uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 with: name: developer-meta-package path: dist/package-artifacts.json - name: Check immutable registry state env: PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }} run: | set -euo pipefail test -n "$PACKAGE_TOKEN" python - <<'PY' import hashlib import json import os from pathlib import Path import tomllib from urllib.error import HTTPError from urllib.parse import quote from urllib.request import Request, urlopen project = tomllib.loads( Path("packages/govoplan-meta/pyproject.toml").read_text(encoding="utf-8") )["project"] wheels = tuple(Path("dist").glob("*.whl")) if len(wheels) != 1: raise SystemExit("meta release must contain exactly one wheel") wheel = wheels[0] digest = hashlib.sha256(wheel.read_bytes()).hexdigest() package_url = "/".join( ( "https://git.add-ideas.de/api/v1/packages/GovOPlaN", "pypi", quote(str(project["name"]), safe=""), quote(str(project["version"]), safe=""), "files", ) ) request = Request( package_url, headers={ "Accept": "application/json", "Authorization": f"token {os.environ['PACKAGE_TOKEN']}", }, ) publish = True try: with urlopen(request, timeout=30) as response: files = json.load(response) except HTTPError as exc: if exc.code != 404: raise else: if not isinstance(files, list) or len(files) != 1: raise SystemExit("immutable meta-package has an unexpected file set") if files[0].get("sha256") != digest: raise SystemExit( "immutable meta-package already exists with a different SHA-256" ) publish = False with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file: env_file.write(f"PUBLISH_PYPI={int(publish)}\n") PY - name: Publish developer package env: PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }} PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }} run: | set -euo pipefail test -n "$PACKAGE_USERNAME" test -n "$PACKAGE_TOKEN" if [[ "$PUBLISH_PYPI" == 1 ]]; then TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \ python -m twine upload --non-interactive \ --repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \ dist/*.whl else echo "Exact developer meta-package is already present; skipping immutable retry." fi