Files
govoplan/.gitea/workflows/publish-developer-meta-package.yml
T

179 lines
6.8 KiB
YAML

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