Skip to content

Commit 4c4037d

Browse files
simonhampclaude
andcommitted
Add license file and release version checks, require support channel on submission
- Add automated checks for license file (LICENSE/LICENSE.md/LICENSE.txt) and release version (GitHub releases/tags) in ReviewPluginRepository - Block plugin approval in Filament admin until required checks pass - Split customer-facing review checks into required and additional sections - Remove automated support email extraction from README - Make support channel required on plugin submission with email/URL validation - Allow users to edit support channel from their plugin dashboard - Update all related tests and notifications Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e77aa06 commit 4c4037d

File tree

13 files changed

+389
-143
lines changed

13 files changed

+389
-143
lines changed

app/Filament/Resources/PluginResource.php

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ public static function form(Schema $schema): Schema
9191
->label('Last Reviewed')
9292
->content(fn (?Plugin $record) => $record?->reviewed_at?->diffForHumans() ?? 'Never'),
9393

94+
Forms\Components\Placeholder::make('review_license')
95+
->label('License File (required)')
96+
->content(fn (?Plugin $record) => ($record?->review_checks['has_license_file'] ?? false) ? '✅ Found' : '❌ Missing'),
97+
98+
Forms\Components\Placeholder::make('review_release')
99+
->label('Release Version (required)')
100+
->content(fn (?Plugin $record) => ($record?->review_checks['has_release_version'] ?? false)
101+
? ''.($record->review_checks['release_version'] ?? '')
102+
: '❌ Missing'),
103+
94104
Forms\Components\Placeholder::make('review_ios')
95105
->label('iOS Support')
96106
->content(fn (?Plugin $record) => ($record?->review_checks['supports_ios'] ?? false) ? '✅ Found' : '❌ Missing'),
@@ -103,12 +113,6 @@ public static function form(Schema $schema): Schema
103113
->label('JS Support')
104114
->content(fn (?Plugin $record) => ($record?->review_checks['supports_js'] ?? false) ? '✅ Found' : '❌ Missing'),
105115

106-
Forms\Components\Placeholder::make('review_email')
107-
->label('Support Email')
108-
->content(fn (?Plugin $record) => ($record?->review_checks['has_support_email'] ?? false)
109-
? ''.($record->review_checks['support_email'] ?? '')
110-
: '❌ Missing'),
111-
112116
Forms\Components\Placeholder::make('review_sdk')
113117
->label('Requires nativephp/mobile')
114118
->content(fn (?Plugin $record) => ($record?->review_checks['requires_mobile_sdk'] ?? false)
@@ -368,10 +372,11 @@ public static function table(Table $table): Table
368372
}
369373

370374
$lines = collect([
375+
['License file *', $checks['has_license_file']],
376+
['Release version *', $checks['has_release_version'] ? $checks['release_version'] : false],
371377
['iOS support', $checks['supports_ios']],
372378
['Android support', $checks['supports_android']],
373379
['JS support', $checks['supports_js']],
374-
['Support email', $checks['has_support_email'] ? $checks['support_email'] : false],
375380
['Requires nativephp/mobile', $checks['requires_mobile_sdk'] ? $checks['mobile_sdk_constraint'] : false],
376381
['iOS min_version', $checks['has_ios_min_version'] ? $checks['ios_min_version'] : false],
377382
['Android min_version', $checks['has_android_min_version'] ? $checks['android_min_version'] : false],
@@ -388,16 +393,17 @@ public static function table(Table $table): Table
388393
})->implode('<br>');
389394

390395
$passed = collect($checks)->only([
396+
'has_license_file', 'has_release_version',
391397
'supports_ios', 'supports_android', 'supports_js',
392-
'has_support_email', 'requires_mobile_sdk',
398+
'requires_mobile_sdk',
393399
'has_ios_min_version', 'has_android_min_version',
394400
])->filter()->count();
395401

396402
Notification::make()
397-
->title("Review checks complete ({$passed}/7 passed)")
403+
->title("Review checks complete ({$passed}/8 passed)")
398404
->body(new HtmlString($lines))
399405
->duration(15000)
400-
->color($passed === 7 ? 'success' : 'warning')
406+
->color($passed === 8 ? 'success' : 'warning')
401407
->send();
402408
}),
403409
])

app/Filament/Resources/PluginResource/Pages/EditPlugin.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ protected function getHeaderActions(): array
2929
->icon('heroicon-o-check')
3030
->color('success')
3131
->visible(fn () => $this->record->isPending())
32+
->disabled(fn () => ! $this->record->passesRequiredReviewChecks())
3233
->action(fn () => $this->record->approve(auth()->id()))
3334
->requiresConfirmation()
3435
->modalHeading('Approve Plugin')
35-
->modalDescription(fn () => "Are you sure you want to approve '{$this->record->name}'?"),
36+
->modalDescription(fn () => ! $this->record->passesRequiredReviewChecks()
37+
? "Cannot approve '{$this->record->name}' — required checks are failing: ".implode(', ', $this->record->getFailingRequiredChecks())
38+
: "Are you sure you want to approve '{$this->record->name}'?"),
3639

3740
Actions\Action::make('reject')
3841
->icon('heroicon-o-x-mark')
@@ -193,10 +196,11 @@ protected function getHeaderActions(): array
193196
}
194197

195198
$lines = collect([
199+
['License file *', $checks['has_license_file']],
200+
['Release version *', $checks['has_release_version'] ? $checks['release_version'] : false],
196201
['iOS support', $checks['supports_ios']],
197202
['Android support', $checks['supports_android']],
198203
['JS support', $checks['supports_js']],
199-
['Support email', $checks['has_support_email'] ? $checks['support_email'] : false],
200204
['Requires nativephp/mobile', $checks['requires_mobile_sdk'] ? $checks['mobile_sdk_constraint'] : false],
201205
['iOS min_version', $checks['has_ios_min_version'] ? $checks['ios_min_version'] : false],
202206
['Android min_version', $checks['has_android_min_version'] ? $checks['android_min_version'] : false],
@@ -213,16 +217,17 @@ protected function getHeaderActions(): array
213217
})->implode('<br>');
214218

215219
$passed = collect($checks)->only([
220+
'has_license_file', 'has_release_version',
216221
'supports_ios', 'supports_android', 'supports_js',
217-
'has_support_email', 'requires_mobile_sdk',
222+
'requires_mobile_sdk',
218223
'has_ios_min_version', 'has_android_min_version',
219224
])->filter()->count();
220225

221226
Notification::make()
222-
->title("Review checks complete ({$passed}/7 passed)")
227+
->title("Review checks complete ({$passed}/8 passed)")
223228
->body(new HtmlString($lines))
224229
->duration(15000)
225-
->color($passed === 7 ? 'success' : 'warning')
230+
->color($passed === 8 ? 'success' : 'warning')
226231
->send();
227232
}),
228233

app/Jobs/ReviewPluginRepository.php

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,16 @@ public function handle(): array
4848
}
4949

5050
$tree = $this->fetchRepoTree($owner, $repoName, $defaultBranch, $token);
51-
$readme = $this->fetchReadme($owner, $repoName, $token);
5251
$composerJson = $this->fetchComposerJson($owner, $repoName, $token);
5352
$nativephpJson = $this->fetchNativephpJson($owner, $repoName, $token);
5453

5554
$checks = [
55+
'has_license_file' => $this->checkHasLicenseFile($tree),
56+
'has_release_version' => false,
57+
'release_version' => null,
5658
'supports_ios' => $this->checkDirectoryHasFiles($tree, 'resources/ios/'),
5759
'supports_android' => $this->checkDirectoryHasFiles($tree, 'resources/android/'),
5860
'supports_js' => $this->checkDirectoryHasFiles($tree, 'resources/js/'),
59-
'has_support_email' => false,
60-
'support_email' => null,
6161
'requires_mobile_sdk' => false,
6262
'mobile_sdk_constraint' => null,
6363
'has_ios_min_version' => false,
@@ -66,10 +66,10 @@ public function handle(): array
6666
'android_min_version' => null,
6767
];
6868

69-
if ($readme) {
70-
$email = $this->extractEmail($readme);
71-
$checks['has_support_email'] = $email !== null;
72-
$checks['support_email'] = $email;
69+
$latestTag = $this->fetchLatestTag($owner, $repoName, $token);
70+
if ($latestTag) {
71+
$checks['has_release_version'] = true;
72+
$checks['release_version'] = $latestTag;
7373
}
7474

7575
if ($composerJson) {
@@ -151,30 +151,6 @@ protected function fetchRepoTree(string $owner, string $repo, string $branch, ?s
151151
return $response->json('tree', []);
152152
}
153153

154-
protected function fetchReadme(string $owner, string $repo, ?string $token): ?string
155-
{
156-
$request = Http::timeout(30);
157-
158-
if ($token) {
159-
$request = $request->withToken($token);
160-
}
161-
162-
$response = $request->get("https://api.github.com/repos/{$owner}/{$repo}/readme");
163-
164-
if ($response->failed()) {
165-
return null;
166-
}
167-
168-
$content = $response->json('content');
169-
$encoding = $response->json('encoding');
170-
171-
if ($encoding === 'base64' && $content) {
172-
return base64_decode($content);
173-
}
174-
175-
return null;
176-
}
177-
178154
protected function fetchComposerJson(string $owner, string $repo, ?string $token): ?array
179155
{
180156
$request = Http::timeout(30);
@@ -241,20 +217,50 @@ protected function checkDirectoryHasFiles(array $tree, string $prefix): bool
241217
return false;
242218
}
243219

244-
protected function extractEmail(string $content): ?string
220+
protected function checkHasLicenseFile(array $tree): bool
245221
{
246-
$excludedDomains = ['example.com', 'example.org', 'example.net', 'test.com', 'localhost'];
222+
$licenseNames = ['LICENSE', 'LICENSE.md', 'LICENSE.txt', 'license', 'license.md', 'license.txt'];
247223

248-
if (preg_match_all('/[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/', $content, $matches)) {
249-
foreach ($matches[0] as $email) {
250-
$domain = strtolower(substr($email, strpos($email, '@') + 1));
224+
foreach ($tree as $item) {
225+
$path = $item['path'] ?? '';
226+
$type = $item['type'] ?? '';
251227

252-
if (! in_array($domain, $excludedDomains, true)) {
253-
return $email;
254-
}
228+
if ($type === 'blob' && in_array($path, $licenseNames, true)) {
229+
return true;
255230
}
256231
}
257232

233+
return false;
234+
}
235+
236+
protected function fetchLatestTag(string $owner, string $repo, ?string $token): ?string
237+
{
238+
$request = Http::timeout(10);
239+
240+
if ($token) {
241+
$request = $request->withToken($token);
242+
}
243+
244+
$response = $request->get("https://api.github.com/repos/{$owner}/{$repo}/releases/latest");
245+
246+
if ($response->successful()) {
247+
return $response->json('tag_name');
248+
}
249+
250+
$tagsResponse = Http::timeout(10);
251+
252+
if ($token) {
253+
$tagsResponse = $tagsResponse->withToken($token);
254+
}
255+
256+
$tagsResponse = $tagsResponse->get("https://api.github.com/repos/{$owner}/{$repo}/tags", [
257+
'per_page' => 1,
258+
]);
259+
260+
if ($tagsResponse->successful() && count($tagsResponse->json()) > 0) {
261+
return $tagsResponse->json()[0]['name'];
262+
}
263+
258264
return null;
259265
}
260266
}

app/Livewire/Customer/Plugins/Create.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,20 @@ function ($attribute, $value, $fail): void {
9595
],
9696
'pluginType' => ['required', 'string', 'in:free,paid'],
9797
'notes' => ['nullable', 'string', 'max:5000'],
98-
'supportChannel' => ['nullable', 'string', 'max:255'],
98+
'supportChannel' => [
99+
'required',
100+
'string',
101+
'max:255',
102+
function (string $attribute, mixed $value, \Closure $fail) {
103+
if (! filter_var($value, FILTER_VALIDATE_EMAIL) && ! filter_var($value, FILTER_VALIDATE_URL)) {
104+
$fail('The support channel must be a valid email address or URL.');
105+
}
106+
},
107+
],
99108
], [
100109
'repository.required' => 'Please select a repository for your plugin.',
101110
'repository.regex' => 'Please enter a valid repository in the format vendor/repo-name.',
111+
'supportChannel.required' => 'Please provide a support channel (email or URL) for your plugin.',
102112
]);
103113

104114
if ($this->pluginType === 'paid' && ! Feature::active(AllowPaidPlugins::class)) {

app/Livewire/Customer/Plugins/Show.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class Show extends Component
3131
#[Validate('nullable|image|max:1024')]
3232
public $logo = null;
3333

34+
public ?string $supportChannel = null;
35+
3436
public function mount(string $vendor, string $package): void
3537
{
3638
$this->plugin = Plugin::findByVendorPackageOrFail($vendor, $package);
@@ -43,6 +45,7 @@ public function mount(string $vendor, string $package): void
4345
$this->iconName = $this->plugin->icon_name ?? 'cube';
4446
$this->iconGradient = $this->plugin->icon_gradient;
4547
$this->iconMode = $this->plugin->hasLogo() ? 'upload' : 'gradient';
48+
$this->supportChannel = $this->plugin->support_channel;
4649
}
4750

4851
public function updateDescription(): void
@@ -122,6 +125,32 @@ public function deleteIcon(): void
122125
session()->flash('success', 'Plugin icon removed successfully!');
123126
}
124127

128+
public function updateSupportChannel(): void
129+
{
130+
$this->validate([
131+
'supportChannel' => [
132+
'required',
133+
'string',
134+
'max:255',
135+
function (string $attribute, mixed $value, \Closure $fail) {
136+
if (! filter_var($value, FILTER_VALIDATE_EMAIL) && ! filter_var($value, FILTER_VALIDATE_URL)) {
137+
$fail('The support channel must be a valid email address or URL.');
138+
}
139+
},
140+
],
141+
], [
142+
'supportChannel.required' => 'Please provide a support channel (email or URL) for your plugin.',
143+
]);
144+
145+
$this->plugin->update([
146+
'support_channel' => $this->supportChannel,
147+
]);
148+
149+
$this->plugin->refresh();
150+
151+
session()->flash('success', 'Support channel updated successfully!');
152+
}
153+
125154
public function resubmit(): void
126155
{
127156
if (! $this->plugin->isRejected()) {

app/Models/Plugin.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,42 @@ public function isSatisSynced(): bool
287287
return $this->satis_synced_at !== null;
288288
}
289289

290+
/**
291+
* Check if all required review checks have passed.
292+
* A plugin cannot be approved until these checks pass.
293+
*/
294+
public function passesRequiredReviewChecks(): bool
295+
{
296+
$checks = $this->review_checks;
297+
298+
if (! $checks) {
299+
return false;
300+
}
301+
302+
return ! empty($checks['has_license_file']) && ! empty($checks['has_release_version']);
303+
}
304+
305+
/**
306+
* Get the list of failing required review checks.
307+
*
308+
* @return array<int, string>
309+
*/
310+
public function getFailingRequiredChecks(): array
311+
{
312+
$checks = $this->review_checks;
313+
$failing = [];
314+
315+
if (empty($checks['has_license_file'])) {
316+
$failing[] = 'License file (LICENSE or LICENSE.md)';
317+
}
318+
319+
if (empty($checks['has_release_version'])) {
320+
$failing[] = 'Release version (GitHub release or tag)';
321+
}
322+
323+
return $failing;
324+
}
325+
290326
/**
291327
* @param Builder<Plugin> $query
292328
* @return Builder<Plugin>

app/Notifications/PluginReviewChecksIncomplete.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ class PluginReviewChecksIncomplete extends Notification implements ShouldQueue
1818
* @var array<string, array{label: string, passing_label: string, docs_url: string, docs_label: string}>
1919
*/
2020
private const CHECK_DEFINITIONS = [
21+
'has_license_file' => [
22+
'label' => 'LICENSE or LICENSE.md file in your repository (required for approval)',
23+
'passing_label' => 'License file',
24+
'docs_url' => self::DOCS_BASE.'/best-practices',
25+
'docs_label' => 'Best Practices guide',
26+
],
27+
'has_release_version' => [
28+
'label' => 'Release version or tag on GitHub (required for approval)',
29+
'passing_label' => 'Release version',
30+
'docs_url' => self::DOCS_BASE.'/best-practices',
31+
'docs_label' => 'Best Practices guide',
32+
],
2133
'supports_ios' => [
2234
'label' => 'iOS native code in `resources/ios/Sources/`',
2335
'passing_label' => 'iOS native code',
@@ -36,12 +48,6 @@ class PluginReviewChecksIncomplete extends Notification implements ShouldQueue
3648
'docs_url' => self::DOCS_BASE.'/creating-plugins',
3749
'docs_label' => 'Creating Plugins guide',
3850
],
39-
'has_support_email' => [
40-
'label' => 'Support email in your README',
41-
'passing_label' => 'Support email',
42-
'docs_url' => self::DOCS_BASE.'/best-practices',
43-
'docs_label' => 'Best Practices guide',
44-
],
4551
'requires_mobile_sdk' => [
4652
'label' => '`nativephp/mobile` required in `composer.json`',
4753
'passing_label' => 'Requires nativephp/mobile',

app/Notifications/PluginSubmitted.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,11 @@ protected function getFailingChecks(): array
7474
}
7575

7676
$labels = [
77+
'has_license_file' => 'Add a LICENSE or LICENSE.md file to your repository (required)',
78+
'has_release_version' => 'Create a release version or tag on GitHub (required)',
7779
'supports_ios' => 'Add iOS support (resources/ios/)',
7880
'supports_android' => 'Add Android support (resources/android/)',
7981
'supports_js' => 'Add JavaScript support (resources/js/)',
80-
'has_support_email' => 'Add a support email to your README',
8182
'requires_mobile_sdk' => 'Require the nativephp/mobile SDK in composer.json',
8283
'has_ios_min_version' => 'Set iOS min_version in nativephp.json',
8384
'has_android_min_version' => 'Set Android min_version in nativephp.json',

0 commit comments

Comments
 (0)