From 838efddf1bcc44c1b1af5a18f6f0edf3aa889726 Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Wed, 19 Aug 2026 12:26:11 +0200 Subject: [PATCH 1/3] fix: use registry API to list PHP image tags Docker Hub v2 API refuses to paginate past 1000 results for anonymous requests, which broke the nightly backlog sync: "pagination offset too large for anonymous requests; sign in to page further" The registry API returns the full tag list in a single response using an anonymous pull token, so no pagination or account is needed. --- prestashop_docker/docker_api.py | 78 ++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/prestashop_docker/docker_api.py b/prestashop_docker/docker_api.py index 6ac081ff..801d4aa6 100644 --- a/prestashop_docker/docker_api.py +++ b/prestashop_docker/docker_api.py @@ -4,6 +4,7 @@ import requests import ssl import time +from urllib.parse import urljoin logger = logging.getLogger(__name__) ssl._create_default_https_context = ssl._create_unverified_context @@ -21,7 +22,8 @@ def __init__(self, cache, debug): @type debug: bool """ self.sleep_time = 1 - self.url = 'https://hub.docker.com/v2/repositories/' + self.auth_url = 'https://auth.docker.io/token' + self.registry_url = 'https://registry-1.docker.io/v2/' self.cache = cache self.is_debug = debug @@ -31,37 +33,68 @@ def __init__(self, cache, debug): def get_tags(self, image_name): """Generate return tags - @return: The json content - @rtype: dict + The registry API is used instead of the Docker Hub one because + Docker Hub refuses to paginate large tag lists for anonymous + requests, while the registry returns them in a single response. + @param image_name: Name of the image (e.g. library/php) + @type image_name: str + @return: The tags, as a list of {'name': } dicts + @rtype: list """ logger.debug( 'Processing request for tags' ) - data = self.execute( - self.url + image_name + '/tags?page_size=100' - ) + headers = {'Authorization': 'Bearer ' + self.get_token(image_name)} + request_url = self.registry_url + image_name + '/tags/list' + + tags = [] + while request_url is not None: + resp = self.execute(request_url, headers) + tags += resp.json()['tags'] + if 'next' in resp.links: + request_url = urljoin(request_url, resp.links['next']['url']) + else: + request_url = None + + return [{'name': name} for name in tags] + + def get_token(self, image_name): + """Get an anonymous pull token for the registry API + + @param image_name: Name of the image the token grants access to + @type image_name: str + @return: The token + @rtype: str + """ + # Tokens are short-lived, never serve one from the cache + with requests_cache.disabled(): + resp = self.execute( + self.auth_url + + '?service=registry.docker.io' + + '&scope=repository:' + image_name + ':pull' + ) - return data['results'] + return resp.json()['token'] - def execute(self, request_url): + def execute(self, request_url, headers=None): """Execute url @param request_url: The url to execute + @param headers: Optional HTTP headers @return: The HTTP Response - @rtype: dict + @rtype: requests.Response """ logger.debug( 'Execute URL: ' + request_url ) resp = requests.get( - request_url + request_url, + headers=headers ) - data = resp.json() - if resp.status_code != 200: # Something went wrong, retry time.sleep(self.sleep_time) @@ -69,16 +102,11 @@ def execute(self, request_url): if DockerApi.retries >= 10: raise requests.HTTPError(resp.text) - return self.execute(request_url) - else: - DockerApi.retries = 0 - # Data not in cache - if not hasattr(resp, 'from_cache') or not resp.from_cache: - time.sleep(self.sleep_time) - - if 'next' in data and data['next'] is not None: - # Compute items if there is a next url - data['results'] += self.execute( - data['next'] - )['results'] - return data + return self.execute(request_url, headers) + + DockerApi.retries = 0 + # Data not in cache + if not hasattr(resp, 'from_cache') or not resp.from_cache: + time.sleep(self.sleep_time) + + return resp From d79722af8eac63765c224bab27ea4ca73af7007d Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Wed, 19 Aug 2026 12:26:11 +0200 Subject: [PATCH 2/3] ci: gate manual sync-releases runs on prestashop-sa Manual workflow_dispatch runs now require the actor to be an active member of the prestashop-sa team, checked through the shared team-guard action from PrestaShop/.github. Scheduled runs are unaffected. Bump checkout and setup-python to v5 to clear the Node 20 deprecation warnings. --- .github/workflows/sync-releases.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sync-releases.yml b/.github/workflows/sync-releases.yml index 55cf1cf6..f8f8d762 100644 --- a/.github/workflows/sync-releases.yml +++ b/.github/workflows/sync-releases.yml @@ -9,10 +9,17 @@ jobs: sync-releases: runs-on: ubuntu-latest steps: + - name: Check team membership + if: github.event_name == 'workflow_dispatch' + uses: PrestaShop/.github/.github/actions/team-guard@master + with: + team_slug: prestashop-sa + token: ${{ secrets.JARVIS_TOKEN }} + - name: Checkout the repo - uses: actions/checkout@v2 + uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install dependencies From 82c033c4501b10e753ed5850d4ddb4859563d749 Mon Sep 17 00:00:00 2001 From: Jonathan LELIEVRE Date: Wed, 19 Aug 2026 12:39:58 +0200 Subject: [PATCH 3/3] style: fix W503 flagged by repo flake8 config --- prestashop_docker/docker_api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/prestashop_docker/docker_api.py b/prestashop_docker/docker_api.py index 801d4aa6..89b5c597 100644 --- a/prestashop_docker/docker_api.py +++ b/prestashop_docker/docker_api.py @@ -71,9 +71,7 @@ def get_token(self, image_name): # Tokens are short-lived, never serve one from the cache with requests_cache.disabled(): resp = self.execute( - self.auth_url - + '?service=registry.docker.io' - + '&scope=repository:' + image_name + ':pull' + self.auth_url + '?service=registry.docker.io&scope=repository:' + image_name + ':pull' ) return resp.json()['token']