From f496825145a0e75f7799c1c511932669bbc48511 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Mon, 21 Sep 2026 17:00:19 -0300 Subject: [PATCH] chore: retire Python publication verifier --- .../verify-appstore-publication/action.yml | 43 ------ scripts/verify_appstore_publication.py | 145 ------------------ tests/test_verify_appstore_publication.py | 108 ------------- 3 files changed, 296 deletions(-) delete mode 100644 actions/verify-appstore-publication/action.yml delete mode 100644 scripts/verify_appstore_publication.py delete mode 100644 tests/test_verify_appstore_publication.py diff --git a/actions/verify-appstore-publication/action.yml b/actions/verify-appstore-publication/action.yml deleted file mode 100644 index 9bcf5e3..0000000 --- a/actions/verify-appstore-publication/action.yml +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -name: Verify Nextcloud App Store publication -description: Wait until a published app release is visible in the Nextcloud App Store API - -inputs: - app-name: - description: Nextcloud app id - required: true - version: - description: Release version, with or without the v prefix - required: true - nextcloud-platform: - description: Minimum supported Nextcloud platform version - required: true - attempts: - description: Maximum number of App Store API checks - required: false - default: '12' - delay-seconds: - description: Delay between checks - required: false - default: '10' - -runs: - using: composite - steps: - - name: Verify App Store publication - shell: bash - env: - APP_NAME: ${{ inputs.app-name }} - APP_VERSION: ${{ inputs.version }} - NEXTCLOUD_PLATFORM: ${{ inputs.nextcloud-platform }} - ATTEMPTS: ${{ inputs.attempts }} - DELAY_SECONDS: ${{ inputs.delay-seconds }} - run: | - python3 "$GITHUB_ACTION_PATH/../../scripts/verify_appstore_publication.py" \ - --app-name "$APP_NAME" \ - --version "$APP_VERSION" \ - --platform "$NEXTCLOUD_PLATFORM" \ - --attempts "$ATTEMPTS" \ - --delay-seconds "$DELAY_SECONDS" diff --git a/scripts/verify_appstore_publication.py b/scripts/verify_appstore_publication.py deleted file mode 100644 index c6a5acb..0000000 --- a/scripts/verify_appstore_publication.py +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -from __future__ import annotations - -import argparse -import json -import time -from collections.abc import Callable -from urllib.error import HTTPError, URLError -from urllib.request import Request, urlopen - - -FetchJson = Callable[[str], object] -Sleep = Callable[[float], None] - - -def normalize_version(version: str) -> str: - return version[1:] if version.startswith("v") else version - - -def normalize_platform(platform: str) -> str: - parts = platform.split(".") - if not all(part.isdigit() for part in parts): - raise ValueError(f"invalid Nextcloud platform version: {platform!r}") - if len(parts) > 3: - raise ValueError(f"invalid Nextcloud platform version: {platform!r}") - return ".".join(parts + ["0"] * (3 - len(parts))) - - -def contains_release(payload: object, app_name: str, version: str) -> bool: - if not isinstance(payload, dict): - return False - - data = payload.get("data") - if not isinstance(data, list): - return False - - for app in data: - if not isinstance(app, dict) or app.get("id") != app_name: - continue - releases = app.get("releases") - if not isinstance(releases, list): - continue - for release in releases: - if isinstance(release, dict) and release.get("version") == version: - return True - - return False - - -def fetch_json(url: str) -> object: - request = Request( - url, - headers={ - "Accept": "application/json", - "User-Agent": "LibreCodeCoop/github-workflows", - }, - ) - with urlopen(request, timeout=30) as response: - return json.load(response) - - -def wait_for_publication( - *, - app_name: str, - version: str, - platform: str, - attempts: int, - delay_seconds: float, - fetch: FetchJson = fetch_json, - sleep: Sleep = time.sleep, -) -> None: - if attempts < 1: - raise ValueError("attempts must be at least 1") - if delay_seconds < 0: - raise ValueError("delay seconds must not be negative") - - normalized_version = normalize_version(version) - normalized_platform = normalize_platform(platform) - url = ( - "https://apps.nextcloud.com/api/v1/platform/" - f"{normalized_platform}/apps.json" - ) - - last_error: Exception | None = None - - for attempt in range(1, attempts + 1): - try: - payload = fetch(url) - last_error = None - if contains_release(payload, app_name, normalized_version): - print( - f"Verified {app_name} {normalized_version} " - "in the Nextcloud App Store" - ) - return - except (HTTPError, URLError, TimeoutError, OSError, json.JSONDecodeError) as error: - last_error = error - - if attempt == attempts: - break - - message = ( - f"App Store has not exposed {app_name} {normalized_version} yet " - f"(attempt {attempt}/{attempts})" - ) - if last_error is not None: - message += f": {last_error}" - print(message) - sleep(delay_seconds) - - detail = f": {last_error}" if last_error is not None else "" - raise RuntimeError( - f"App Store publication could not be verified for " - f"{app_name} {normalized_version}{detail}" - ) - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--app-name", required=True) - parser.add_argument("--version", required=True) - parser.add_argument("--platform", required=True) - parser.add_argument("--attempts", type=int, default=12) - parser.add_argument("--delay-seconds", type=float, default=10) - args = parser.parse_args() - - try: - wait_for_publication( - app_name=args.app_name, - version=args.version, - platform=args.platform, - attempts=args.attempts, - delay_seconds=args.delay_seconds, - ) - except (RuntimeError, ValueError) as error: - parser.error(str(error)) - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tests/test_verify_appstore_publication.py b/tests/test_verify_appstore_publication.py deleted file mode 100644 index caf5676..0000000 --- a/tests/test_verify_appstore_publication.py +++ /dev/null @@ -1,108 +0,0 @@ -# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors -# SPDX-License-Identifier: AGPL-3.0-or-later - -import unittest -from urllib.error import URLError - -from scripts.verify_appstore_publication import ( - contains_release, - normalize_platform, - normalize_version, - wait_for_publication, -) - - -class VerifyAppStorePublicationTest(unittest.TestCase): - def test_normalizes_version_prefix(self) -> None: - self.assertEqual(normalize_version("v12.3.4"), "12.3.4") - self.assertEqual(normalize_version("12.3.4"), "12.3.4") - - def test_normalizes_platform(self) -> None: - self.assertEqual(normalize_platform("35"), "35.0.0") - self.assertEqual(normalize_platform("35.1"), "35.1.0") - self.assertEqual(normalize_platform("35.1.2"), "35.1.2") - - def test_rejects_invalid_platform(self) -> None: - with self.assertRaises(ValueError): - normalize_platform("stable35") - - def test_detects_release(self) -> None: - payload = { - "data": [ - { - "id": "example", - "releases": [{"version": "1.2.3"}], - } - ] - } - self.assertTrue(contains_release(payload, "example", "1.2.3")) - self.assertFalse(contains_release(payload, "example", "1.2.4")) - - def test_retries_until_release_is_visible(self) -> None: - responses = iter([ - {"data": [{"id": "example", "releases": []}]}, - {"data": [{"id": "example", "releases": [{"version": "1.2.3"}]}]}, - ]) - sleeps: list[float] = [] - - wait_for_publication( - app_name="example", - version="v1.2.3", - platform="35", - attempts=3, - delay_seconds=7, - fetch=lambda _url: next(responses), - sleep=sleeps.append, - ) - - self.assertEqual(sleeps, [7]) - - def test_retries_transient_request_error(self) -> None: - calls = 0 - sleeps: list[float] = [] - - def fetch(_url: str) -> object: - nonlocal calls - calls += 1 - if calls == 1: - raise URLError("temporary") - return { - "data": [ - { - "id": "example", - "releases": [{"version": "1.2.3"}], - } - ] - } - - wait_for_publication( - app_name="example", - version="1.2.3", - platform="35.0.0", - attempts=2, - delay_seconds=2, - fetch=fetch, - sleep=sleeps.append, - ) - - self.assertEqual(sleeps, [2]) - - def test_fails_after_attempt_limit_without_extra_sleep(self) -> None: - sleeps: list[float] = [] - - with self.assertRaisesRegex(RuntimeError, "could not be verified"): - wait_for_publication( - app_name="example", - version="1.2.3", - platform="35", - attempts=3, - delay_seconds=5, - fetch=lambda _url: {"data": []}, - sleep=sleeps.append, - ) - - self.assertEqual(sleeps, [5, 5]) - - -if __name__ == "__main__": - unittest.main()