From 4fd166d953d70121bf39321d8e8e3882ad3de99b Mon Sep 17 00:00:00 2001 From: Release Archive Date: Tue, 22 Sep 2026 11:28:35 +0800 Subject: [PATCH] fix: use non-paginated Gitea release attachment endpoint --- scripts/sync_releases.py | 13 ++++++++++--- tests/test_sync.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/scripts/sync_releases.py b/scripts/sync_releases.py index eb1a2c3..59dec00 100644 --- a/scripts/sync_releases.py +++ b/scripts/sync_releases.py @@ -103,6 +103,13 @@ class API: raise SyncError('Attachment download failed after retries') from None time.sleep(2 ** attempt) + def list_assets(self, release_id): + # Gitea's assets endpoint is NOT paginated; page/limit are ignored. + data = self.request('GET', f'/releases/{release_id}/assets') + if not isinstance(data, list): + raise SyncError('Expected attachment list') + return data + def digest(path): h = hashlib.sha256() @@ -151,7 +158,7 @@ def release_body(source, source_url, hashes, source_sha): def sync_one(src, dst, source, anchor, dry_run, directory): tag = source['tag_name'] key = urllib.parse.quote(tag, safe='') - source_assets = src.pages(f'/releases/{source["id"]}/assets') + source_assets = src.list_assets(source['id']) validate_names(source_assets) source_tag = src.request('GET', '/tags/' + key) if not source_tag or not source_tag.get('commit', {}).get('sha'): @@ -162,7 +169,7 @@ def sync_one(src, dst, source, anchor, dry_run, directory): p = Path(directory) / ('source-' + str(a['id'])) hashes[a['name']] = src.download(a, p) files[a['name']] = p - fresh = src.pages(f'/releases/{source["id"]}/assets') + fresh = src.list_assets(source['id']) identity = lambda rows: sorted((a['id'], a['name'], a['size']) for a in rows) if identity(fresh) != identity(source_assets): raise SyncError('Source attachment list changed during download; retry later') @@ -175,7 +182,7 @@ def sync_one(src, dst, source, anchor, dry_run, directory): result = {'tag': tag, 'source_sha': source_sha, 'created': target is None, 'assets': [], 'metadata_changed': False} if target is None and not dry_run: target = dst.request('POST', '/releases', dict(desired, tag_name=tag, target_commitish=anchor, draft=True)) - assets = dst.pages(f'/releases/{target["id"]}/assets') if target else [] + assets = dst.list_assets(target['id']) if target else [] for source_asset in source_assets: name, sha = source_asset['name'], hashes[source_asset['name']] existing = next((a for a in assets if a['name'] == name), None) diff --git a/tests/test_sync.py b/tests/test_sync.py index 0184eee..af4c84c 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -33,6 +33,8 @@ class Fake: return copy.deepcopy(a) def pages(self, path): return copy.deepcopy(self.assets) + def list_assets(self, release_id): + return copy.deepcopy(self.assets) def download(self, a, p): Path(p).write_bytes(self.data[a['id']]) validate_file(p, a) @@ -118,6 +120,15 @@ class Tests(unittest.TestCase): api = API('https://example.invalid', 'a/b', 'secret') with self.assertRaises(SyncError): api.download({'browser_download_url': 'https://other.invalid/file'}, '/tmp/unused') + def test_assets_endpoint_is_not_paginated(self): + api = API('https://example.invalid', 'a/b', '') + calls = [] + def request(method, path): + calls.append(path) + return [{'id': 1}] + api.request = request + self.assertEqual([{'id': 1}], api.list_assets(2)) + self.assertEqual(['/releases/2/assets'], calls) if __name__ == '__main__': unittest.main()