fix(release): cache public-tags reads + tighten yank tag validation (review)

- 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 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-23 06:01:36 +02:00
parent 67a69f3189
commit 1eb4c4fef7
3 changed files with 25 additions and 18 deletions

View File

@ -18,10 +18,8 @@ jobs:
env: env:
TAG: ${{ inputs.tag }} TAG: ${{ inputs.tag }}
run: | run: |
case "$TAG" in echo "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-beta[0-9]+)?$' \
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; }
*) echo "refusing to yank an unrecognised tag: $TAG" >&2; exit 1 ;;
esac
- name: Checkout public repo - name: Checkout public repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:

View File

@ -2,6 +2,7 @@
namespace App\Services; namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
/** /**
@ -39,22 +40,28 @@ class PromotionService
if ($slug === '') { if ($slug === '') {
return []; return [];
} }
try {
$res = Http::timeout(5)->acceptJson()->withHeaders(['User-Agent' => 'Clusev-Panel']) // Cached: render() (incl. the 5s page poll) calls this every render, and the anonymous GitHub
->get("https://api.github.com/repos/{$slug}/tags", ['per_page' => $limit]); // tag API is rate-limited (60/h per IP). 120s keeps it well under the limit; a yank reflects
if (! $res->successful()) { // 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 [];
} }
});
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<string,string> $inputs */ /** @param array<string,string> $inputs */

View File

@ -3,6 +3,7 @@
namespace Tests\Feature; namespace Tests\Feature;
use App\Services\PromotionService; use App\Services\PromotionService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Tests\TestCase; use Tests\TestCase;
@ -16,6 +17,7 @@ class PromotionServiceTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
Cache::flush(); // publicTags() is cached — never inherit a cached result between tests
config()->set('clusev.github_token', 'tok_secret'); config()->set('clusev.github_token', 'tok_secret');
config()->set('clusev.staging_slug', self::STAGING); config()->set('clusev.staging_slug', self::STAGING);
config()->set('clusev.public_slug', self::PUBLIC_SLUG); config()->set('clusev.public_slug', self::PUBLIC_SLUG);