From 1eb4c4fef7faae253962cec7348e5c34aa9451f5 Mon Sep 17 00:00:00 2001 From: boban Date: Tue, 23 Jun 2026 06:01:36 +0200 Subject: [PATCH] fix(release): cache public-tags reads + tighten yank tag validation (review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PromotionService::publicTags() is called on every Release render incl. the 5s page poll; the anonymous GitHub tag API is rate-limited (60/h per IP). Cache it 120s so the panel never gets rate-limited to empty. (Test flushes cache in setUp.) - yank.yml: replace the loose shell-glob tag guard with an anchored grep regex so a garbage/space/metachar tag dispatched straight from the GitHub UI is rejected before reaching git push --delete (the panel already validates tag ∈ publicTags). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/yank.yml | 6 ++--- app/Services/PromotionService.php | 35 +++++++++++++++----------- tests/Feature/PromotionServiceTest.php | 2 ++ 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/.github/workflows/yank.yml b/.github/workflows/yank.yml index 8a85736..9e15293 100644 --- a/.github/workflows/yank.yml +++ b/.github/workflows/yank.yml @@ -18,10 +18,8 @@ jobs: env: TAG: ${{ inputs.tag }} run: | - case "$TAG" in - v[0-9]*.[0-9]*.[0-9]*|v[0-9]*.[0-9]*.[0-9]*-beta[0-9]*) ;; - *) echo "refusing to yank an unrecognised tag: $TAG" >&2; exit 1 ;; - esac + echo "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-beta[0-9]+)?$' \ + || { echo "refusing to yank an unrecognised tag: $TAG" >&2; exit 1; } - name: Checkout public repo uses: actions/checkout@v4 with: diff --git a/app/Services/PromotionService.php b/app/Services/PromotionService.php index 81f2ed7..d84db3b 100644 --- a/app/Services/PromotionService.php +++ b/app/Services/PromotionService.php @@ -2,6 +2,7 @@ namespace App\Services; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; /** @@ -39,22 +40,28 @@ class PromotionService if ($slug === '') { return []; } - try { - $res = Http::timeout(5)->acceptJson()->withHeaders(['User-Agent' => 'Clusev-Panel']) - ->get("https://api.github.com/repos/{$slug}/tags", ['per_page' => $limit]); - if (! $res->successful()) { + + // Cached: render() (incl. the 5s page poll) calls this every render, and the anonymous GitHub + // tag API is rate-limited (60/h per IP). 120s keeps it well under the limit; a yank reflects + // once the workflow has actually deleted the tag (~minutes) anyway, so staleness is harmless. + return Cache::remember("clusev:public-tags:{$slug}:{$limit}", 120, function () use ($slug, $limit) { + try { + $res = Http::timeout(5)->acceptJson()->withHeaders(['User-Agent' => 'Clusev-Panel']) + ->get("https://api.github.com/repos/{$slug}/tags", ['per_page' => $limit]); + if (! $res->successful()) { + return []; + } + + return array_values(array_filter( + array_map(static fn ($t): string => is_array($t) ? (string) ($t['name'] ?? '') : '', (array) $res->json()), + static fn (string $n): bool => $n !== '', + )); + } catch (\Throwable $e) { + report($e); + return []; } - - return array_values(array_filter( - array_map(static fn ($t): string => is_array($t) ? (string) ($t['name'] ?? '') : '', (array) $res->json()), - static fn (string $n): bool => $n !== '', - )); - } catch (\Throwable $e) { - report($e); - - return []; - } + }); } /** @param array $inputs */ diff --git a/tests/Feature/PromotionServiceTest.php b/tests/Feature/PromotionServiceTest.php index f7bbb2a..efc9007 100644 --- a/tests/Feature/PromotionServiceTest.php +++ b/tests/Feature/PromotionServiceTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature; use App\Services\PromotionService; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Tests\TestCase; @@ -16,6 +17,7 @@ class PromotionServiceTest extends TestCase protected function setUp(): void { parent::setUp(); + Cache::flush(); // publicTags() is cached — never inherit a cached result between tests config()->set('clusev.github_token', 'tok_secret'); config()->set('clusev.staging_slug', self::STAGING); config()->set('clusev.public_slug', self::PUBLIC_SLUG);