Skip to content

Commit 26477cd

Browse files
committed
cleanup + early private packages support
1 parent cf67869 commit 26477cd

File tree

2 files changed

+165
-107
lines changed

2 files changed

+165
-107
lines changed

src/Commands/BundleCommand.php

Lines changed: 103 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44

55
use Carbon\CarbonInterface;
66
use Illuminate\Console\Command;
7-
use Illuminate\Http\Client\ConnectionException;
7+
use Illuminate\Http\Client\Response;
88
use Illuminate\Support\Facades\Http;
99
use Illuminate\Support\Number;
10+
use Illuminate\Support\Str;
1011
use Native\Laravel\Commands\Traits\CleansEnvFile;
12+
use Native\Laravel\Commands\Traits\HandleApiRequests;
1113
use Symfony\Component\Finder\Finder;
1214
use ZipArchive;
1315

1416
class BundleCommand extends Command
1517
{
16-
use CleansEnvFile;
18+
use CleansEnvFile, HandleApiRequests;
1719

1820
protected $signature = 'native:bundle {--fetch} {--without-cleanup}';
1921

@@ -25,25 +27,28 @@ class BundleCommand extends Command
2527

2628
private string $zipName;
2729

28-
public function handle()
30+
public function handle(): int
2931
{
32+
// Check for ZEPHPYR_KEY
3033
if (! $this->checkForZephpyrKey()) {
3134
return static::FAILURE;
3235
}
3336

37+
// Check for ZEPHPYR_TOKEN
3438
if (! $this->checkForZephpyrToken()) {
3539
return static::FAILURE;
3640
}
3741

42+
// Check if the token is valid
3843
if (! $this->checkAuthenticated()) {
3944
$this->error('Invalid API token: check your ZEPHPYR_TOKEN on '.$this->baseUrl().'user/api-tokens');
4045

4146
return static::FAILURE;
4247
}
4348

49+
// Download the latest bundle if requested
4450
if ($this->option('fetch')) {
4551
if (! $this->fetchLatestBundle()) {
46-
$this->warn('Latest bundle not yet available. Try again soon.');
4752

4853
return static::FAILURE;
4954
}
@@ -53,6 +58,11 @@ public function handle()
5358
return static::SUCCESS;
5459
}
5560

61+
// Check composer.json for symlinked or private packages
62+
if (! $this->checkComposerJson()) {
63+
return static::FAILURE;
64+
}
65+
5666
// Package the app up into a zip
5767
if (! $this->zipApplication()) {
5868
$this->error("Failed to create zip archive at {$this->zipPath}.");
@@ -61,67 +71,19 @@ public function handle()
6171
}
6272

6373
// Send the zip file
64-
try {
65-
$result = $this->sendToZephpyr();
66-
} catch (ConnectionException $e) {
67-
// Timeout, etc.
68-
$this->error('Failed to send to Zephpyr: '.$e->getMessage());
69-
$this->cleanUp();
70-
71-
return static::FAILURE;
72-
}
73-
74-
if ($result->status() === 413) {
75-
$fileSize = Number::fileSize(filesize($this->zipPath));
76-
$this->error('The zip file is too large to upload to Zephpyr ('.$fileSize.'). Please contact support.');
77-
78-
$this->cleanUp();
79-
80-
return static::FAILURE;
81-
} elseif ($result->status() === 422) {
82-
$this->error('Zephpyr returned the following error:');
83-
$this->error(''.$result->json('message'));
84-
$this->cleanUp();
85-
86-
return static::FAILURE;
87-
} elseif ($result->status() === 429) {
88-
$this->error('Zephpyr has a rate limit on builds per hour. Please try again in '.now()->addSeconds(intval($result->header('Retry-After')))->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE).'.');
89-
$this->cleanUp();
90-
91-
return static::FAILURE;
92-
} elseif ($result->failed()) {
93-
$this->error("Failed to upload zip to Zephpyr. Error code: {$result->status()}");
94-
ray($result->body());
95-
$this->cleanUp();
96-
97-
return static::FAILURE;
98-
}
74+
$result = $this->sendToZephpyr();
75+
$this->handleApiErrors($result);
9976

77+
// Success
10078
$this->info('Successfully uploaded to Zephpyr.');
10179
$this->line('Use native:bundle --fetch to retrieve the latest bundle.');
10280

81+
// Clean up temp files
10382
$this->cleanUp();
10483

10584
return static::SUCCESS;
10685
}
10786

108-
protected function cleanUp(): void
109-
{
110-
if ($this->option('without-cleanup')) {
111-
return;
112-
}
113-
114-
$this->line('Cleaning up…');
115-
116-
$previousBuilds = glob(base_path('temp/app_*.zip'));
117-
$failedZips = glob(base_path('temp/app_*.part'));
118-
119-
$deleteFiles = array_merge($previousBuilds, $failedZips);
120-
foreach ($deleteFiles as $file) {
121-
@unlink($file);
122-
}
123-
}
124-
12587
private function zipApplication(): bool
12688
{
12789
$this->zipName = 'app_'.str()->random(8).'.zip';
@@ -149,14 +111,42 @@ private function zipApplication(): bool
149111
return true;
150112
}
151113

152-
private function addFilesToZip(ZipArchive $zip): void
114+
private function checkComposerJson(): bool
153115
{
154-
// TODO: Check the composer.json to make sure there are no symlinked
155-
// or private packages as these will be a pain later
116+
$composerJson = json_decode(file_get_contents(base_path('composer.json')), true);
117+
118+
// Fail if there is symlinked packages
119+
foreach ($composerJson['repositories'] ?? [] as $repository) {
120+
if ($repository['type'] === 'path') {
121+
$this->error('Symlinked packages are not supported. Please remove them from your composer.json.');
122+
123+
return false;
124+
} elseif ($repository['type'] === 'composer') {
125+
if (! $this->checkComposerPackageAuth($repository['url'])) {
126+
$this->error('Cannot authenticate with '.$repository['url'].'.');
127+
$this->error('Go to '.$this->baseUrl().' and add your credentials for '.$repository['url'].'.');
128+
129+
return false;
130+
}
131+
}
132+
}
133+
134+
return true;
135+
}
156136

157-
// TODO: Fail if there is symlinked packages
158-
// TODO: For private packages: make an endpoint to check if user gave us their credentials
137+
private function checkComposerPackageAuth(string $repositoryUrl): bool
138+
{
139+
$host = parse_url($repositoryUrl, PHP_URL_HOST);
140+
$this->line('Checking '.$host.' authentication…');
159141

142+
return Http::acceptJson()
143+
->withToken(config('nativephp-internal.zephpyr.token'))
144+
->get($this->baseUrl().'api/v1/project/'.$this->key.'/composer/auth/'.$host)
145+
->successful();
146+
}
147+
148+
private function addFilesToZip(ZipArchive $zip): void
149+
{
160150
$this->line('Creating zip archive…');
161151

162152
$app = (new Finder)->files()
@@ -178,18 +168,22 @@ private function addFilesToZip(ZipArchive $zip): void
178168
// Add .env file
179169
$zip->addFile(base_path('.env'), '.env');
180170

171+
// Custom binaries
172+
$binaryPath = Str::replaceStart(base_path('vendor'), '', config('nativephp.binary_path'));
173+
174+
// Add composer dependencies without unnecessary files
181175
$vendor = (new Finder)->files()
182-
// ->followLinks() // This is causing issues with excluded files
183176
->exclude(array_filter([
184177
'nativephp/php-bin',
185178
'nativephp/electron/resources/js',
186179
'nativephp/*/vendor',
187-
config('nativephp.binary_path'), // User defined binary paths
180+
$binaryPath,
188181
]))
189182
->in(base_path('vendor'));
190183

191184
$this->finderToZip($vendor, $zip, 'vendor');
192185

186+
// Add javascript dependencies
193187
if (file_exists(base_path('node_modules'))) {
194188
$nodeModules = (new Finder)->files()
195189
->in(base_path('node_modules'));
@@ -209,32 +203,18 @@ private function finderToZip(Finder $finder, ZipArchive $zip, ?string $path = nu
209203
}
210204
}
211205

212-
private function baseUrl(): string
213-
{
214-
return str(config('nativephp-internal.zephpyr.host'))->finish('/');
215-
}
216-
217206
private function sendToZephpyr()
218207
{
219208
$this->line('Uploading zip to Zephpyr…');
220209

221210
return Http::acceptJson()
222211
->timeout(300) // 5 minutes
223-
->withoutRedirecting() // Upload won't work if we follow the redirect
212+
->withoutRedirecting() // Upload won't work if we follow redirects (it transform POST to GET)
224213
->withToken(config('nativephp-internal.zephpyr.token'))
225214
->attach('archive', fopen($this->zipPath, 'r'), $this->zipName)
226215
->post($this->baseUrl().'api/v1/project/'.$this->key.'/build/');
227216
}
228217

229-
private function checkAuthenticated()
230-
{
231-
$this->line('Checking authentication…');
232-
233-
return Http::acceptJson()
234-
->withToken(config('nativephp-internal.zephpyr.token'))
235-
->get($this->baseUrl().'api/v1/user')->successful();
236-
}
237-
238218
private function fetchLatestBundle(): bool
239219
{
240220
$this->line('Fetching latest bundle…');
@@ -244,52 +224,68 @@ private function fetchLatestBundle(): bool
244224
->get($this->baseUrl().'api/v1/project/'.$this->key.'/build/download');
245225

246226
if ($response->failed()) {
227+
228+
if ($response->status() === 404) {
229+
$this->error('Project or bundle not found.');
230+
} elseif ($response->status() === 500) {
231+
$this->error('Build failed. Please try again later.');
232+
} elseif ($response->status() === 503) {
233+
$this->warn('Bundle not ready. Please try again in '.now()->addSeconds(intval($response->header('Retry-After')))->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE).'.');
234+
} else {
235+
$this->handleApiErrors($response);
236+
}
237+
247238
return false;
248239
}
249240

241+
// Save the bundle
250242
@mkdir(base_path('build'), recursive: true);
251243
file_put_contents(base_path('build/__nativephp_app_bundle'), $response->body());
252244

253245
return true;
254246
}
255247

256-
private function checkForZephpyrKey()
248+
protected function exitWithMessage(string $message): void
257249
{
258-
$this->key = config('nativephp-internal.zephpyr.key');
259-
260-
if (! $this->key) {
261-
$this->line('');
262-
$this->warn('No ZEPHPYR_KEY found. Cannot bundle!');
263-
$this->line('');
264-
$this->line('Add this app\'s ZEPHPYR_KEY to its .env file:');
265-
$this->line(base_path('.env'));
266-
$this->line('');
267-
$this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!');
268-
$this->info('Check out '.$this->baseUrl().'');
269-
$this->line('');
250+
$this->error($message);
251+
$this->cleanUp();
270252

271-
return false;
272-
}
253+
exit(static::FAILURE);
254+
}
273255

274-
return true;
256+
private function handleApiErrors(Response $result): void
257+
{
258+
if ($result->status() === 413) {
259+
$fileSize = Number::fileSize(filesize($this->zipPath));
260+
$this->exitWithMessage('File is too large to upload ('.$fileSize.'). Please contact support.');
261+
} elseif ($result->status() === 422) {
262+
$this->error('Request refused:'.$result->json('message'));
263+
} elseif ($result->status() === 429) {
264+
$this->exitWithMessage('Too many requests. Please try again in '.now()->addSeconds(intval($result->header('Retry-After')))->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE).'.');
265+
} elseif ($result->failed()) {
266+
$this->exitWithMessage("Request failed. Error code: {$result->status()}");
267+
}
275268
}
276269

277-
private function checkForZephpyrToken()
270+
protected function cleanUp(): void
278271
{
279-
if (! config('nativephp-internal.zephpyr.token')) {
280-
$this->line('');
281-
$this->warn('No ZEPHPYR_TOKEN found. Cannot bundle!');
282-
$this->line('');
283-
$this->line('Add your api ZEPHPYR_TOKEN to its .env file:');
284-
$this->line(base_path('.env'));
285-
$this->line('');
286-
$this->info('Not set up with Zephpyr yet? Secure your NativePHP app builds and more!');
287-
$this->info('Check out '.$this->baseUrl().'');
288-
$this->line('');
272+
if ($this->option('without-cleanup')) {
273+
return;
274+
}
289275

290-
return false;
276+
$previousBuilds = glob(base_path('temp/app_*.zip'));
277+
$failedZips = glob(base_path('temp/app_*.part'));
278+
279+
$deleteFiles = array_merge($previousBuilds, $failedZips);
280+
281+
if (empty($deleteFiles)) {
282+
return;
291283
}
292284

293-
return true;
285+
$this->line('Cleaning up…');
286+
287+
foreach ($deleteFiles as $file) {
288+
@unlink($file);
289+
}
294290
}
295291
}

0 commit comments

Comments
 (0)