From 4b393082d4d74a11dbfcd07c2ebe2fcdf7778689 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Wed, 1 Jul 2026 21:51:54 +0200 Subject: [PATCH] ci: drop requests dependency and use PowerShell for the Windows release upload The Windows viewer runner has Python but not the 'requests' package, and does not necessarily have bash. So: - rewrite gitea_upload_file.py to use only the Python stdlib (urllib), which works with a bare interpreter on both the Linux package runners and Windows; also drop the file's unused create_release() (gitea_create_release.py owns that); - run the Windows 'Upload installer to release' step in PowerShell (always present) instead of bash, globbing the NSIS .exe with Get-ChildItem. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/build_and_test.yml | 18 ++-- gitea_upload_file.py | 130 ++++++++++++---------------- 2 files changed, 63 insertions(+), 85 deletions(-) diff --git a/.gitea/workflows/build_and_test.yml b/.gitea/workflows/build_and_test.yml index 4ed6179b..864a33d2 100644 --- a/.gitea/workflows/build_and_test.yml +++ b/.gitea/workflows/build_and_test.yml @@ -99,21 +99,17 @@ jobs: cpack - name: Upload installer to release if: github.ref_type == 'tag' - shell: bash + shell: powershell env: TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }} run: | - set -euo pipefail - shopt -s nullglob # NSIS installer named jfjoch--win64-{cuda|cpu}.exe (see CMakeLists.txt). - files=(build/jfjoch-*-win64-*.exe) - if [ ${#files[@]} -eq 0 ]; then - echo "No Windows installer found in build/" - exit 1 - fi - for file in "${files[@]}"; do - python gitea_upload_file.py "$file" - done + $files = Get-ChildItem -Path build -Filter 'jfjoch-*-win64-*.exe' + if ($files.Count -eq 0) { throw 'No Windows installer found in build/' } + foreach ($file in $files) { + python gitea_upload_file.py $file.FullName + if ($LASTEXITCODE -ne 0) { throw "Upload failed for $($file.Name)" } + } build-rpm: name: build:rpm (${{ matrix.distro }}) if: github.ref_type != 'workflow_dispatch' diff --git a/gitea_upload_file.py b/gitea_upload_file.py index 05d3a52f..1fa9201e 100644 --- a/gitea_upload_file.py +++ b/gitea_upload_file.py @@ -1,15 +1,18 @@ #!/usr/bin/env python3 +# Uploads a file as an asset to the Gitea release for the tag in the VERSION file. +# Uses only the standard library (no 'requests'), so it runs with a bare Python +# interpreter on the Linux package runners and on the Windows viewer runner alike. + +import json import os import sys +import uuid +import urllib.request +import urllib.error from typing import Optional -try: - import requests -except ModuleNotFoundError: - print("ERROR: Python 'requests' package is required. Please install it (e.g., pip install requests).") - sys.exit(1) -gitea_api_url = "https://gitea.psi.ch/api/v1/" +gitea_api_url = "https://gitea.psi.ch/api/v1" repo_owner = "mx" repo_name = "jungfraujoch" @@ -18,6 +21,7 @@ if gitea_token is None: print("ERROR: Required environment variables not set") sys.exit(1) + def read_version(): try: with open('VERSION', 'r') as f: @@ -26,57 +30,45 @@ def read_version(): print("ERROR: VERSION file not found") sys.exit(1) -def create_release(version): - headers = { - 'Authorization': f'token {gitea_token}', - 'Content-Type': 'application/json', - } - data = { - 'tag_name': version, - 'name': f'Release {version}', - 'draft': False, - 'prerelease': '-rc.' in version or '-alpha.' in version or '-beta.' in version - } +def _api_request(method: str, url: str, headers: dict, data: Optional[bytes] = None): + req = urllib.request.Request(url, data=data, headers=headers, method=method) + try: + with urllib.request.urlopen(req) as resp: + return resp.status, resp.read() + except urllib.error.HTTPError as e: + return e.code, e.read() - url = f'{gitea_api_url}/repos/{repo_owner}/{repo_name}/releases' - - response = requests.post(url, headers=headers, json=data) - if response.status_code != 201: - print(f"ERROR: Failed to create release. Status code: {response.status_code}") - print(response.text) - sys.exit(1) - - release = response.json() - print(f"Successfully created release {version}") - return release def get_release_by_tag(version: str): - """ - Fetch release information by tag name. - Returns the release object (dict) or exits on error. - """ - headers = { - 'Authorization': f'token {gitea_token}', - 'Content-Type': 'application/json', - } + """Fetch release information by tag name; exits on error.""" url = f'{gitea_api_url}/repos/{repo_owner}/{repo_name}/releases/tags/{version}' - resp = requests.get(url, headers=headers) - if resp.status_code != 200: - print(f"ERROR: Failed to fetch release {version}. Status code: {resp.status_code}") - print(resp.text) + headers = {'Authorization': f'token {gitea_token}'} + status, body = _api_request('GET', url, headers) + if status != 200: + print(f"ERROR: Failed to fetch release {version}. Status code: {status}") + print(body.decode(errors='replace')) sys.exit(1) - return resp.json() + return json.loads(body) + + +def _multipart_body(fields: dict, file_field: str, file_name: str, file_bytes: bytes): + """Encode a multipart/form-data body; returns (body, content_type).""" + boundary = uuid.uuid4().hex + sep = f'--{boundary}'.encode() + parts = [] + for name, value in fields.items(): + parts += [sep, f'Content-Disposition: form-data; name="{name}"'.encode(), + b'', value.encode()] + parts += [sep, + f'Content-Disposition: form-data; name="{file_field}"; filename="{file_name}"'.encode(), + b'Content-Type: application/octet-stream', b'', file_bytes] + parts += [f'--{boundary}--'.encode(), b''] + return b'\r\n'.join(parts), f'multipart/form-data; boundary={boundary}' + def upload_file_to_release(version: str, file_path: str, name: Optional[str] = None, label: Optional[str] = None): - """ - Upload a file as an asset to the release identified by the given version tag. - - :param version: Tag name of the release (e.g., '1.2.3' or '1.2.3-rc.1') - :param file_path: Local file path to upload - :param name: Optional asset name to display in release (defaults to filename) - :param label: Optional asset label (display name) - """ + """Upload a file as an asset to the release identified by the given version tag.""" if not os.path.isfile(file_path): print(f"ERROR: File not found: {file_path}") sys.exit(1) @@ -84,42 +76,32 @@ def upload_file_to_release(version: str, file_path: str, name: Optional[str] = N asset_name = name if name else os.path.basename(file_path) print(f"Uploading file '{file_path}' to release {version} as {asset_name}") - # Ensure the release exists and get its ID - release = get_release_by_tag(version) - release_id = release.get('id') + release_id = get_release_by_tag(version).get('id') if release_id is None: print("ERROR: Release ID not found in response") sys.exit(1) - headers = { - 'Authorization': f'token {gitea_token}', - } - - data = {'name': asset_name} + fields = {'name': asset_name} if label: - data['label'] = label - - url = f'{gitea_api_url}/repos/{repo_owner}/{repo_name}/releases/{release_id}/assets' + fields['label'] = label with open(file_path, 'rb') as f: - files = { - 'attachment': (asset_name, f, 'application/octet-stream'), - } - resp = requests.post(url, headers=headers, data=data, files=files) + body, content_type = _multipart_body(fields, 'attachment', asset_name, f.read()) - if resp.status_code not in (201, 200): - print(f"ERROR: Failed to upload asset. Status code: {resp.status_code}") - print(resp.text) + url = f'{gitea_api_url}/repos/{repo_owner}/{repo_name}/releases/{release_id}/assets' + headers = {'Authorization': f'token {gitea_token}', 'Content-Type': content_type} + status, resp_body = _api_request('POST', url, headers, body) + if status not in (200, 201): + print(f"ERROR: Failed to upload asset. Status code: {status}") + print(resp_body.decode(errors='replace')) sys.exit(1) - asset = resp.json() - browser_download_url = asset.get('browser_download_url') or asset.get('browser_url') or asset.get('url') - print(f"Uploaded asset '{asset_name}' to release {version}: {browser_download_url}") + asset = json.loads(resp_body) + download_url = asset.get('browser_download_url') or asset.get('url') + print(f"Uploaded asset '{asset_name}' to release {version}: {download_url}") + if __name__ == '__main__': - version = read_version() if len(sys.argv) != 2: exit(1) - - upload_file_to_release(version, sys.argv[1]) - + upload_file_to_release(read_version(), sys.argv[1])