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
parent
67a69f3189
commit
1eb4c4fef7
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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<string,string> $inputs */
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue