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:
|
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:
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue