loadMissing('environment.application', 'builtByService.server', 'builtByService.replicas.server'); $application = $artifact->environment->application; $strategy = BuildStrategy::tryFrom($artifact->metadata['build_strategy'] ?? BuildStrategy::TARGET_SERVER->value) ?? BuildStrategy::TARGET_SERVER; $server = $this->buildServer($artifact); $artifact->update([ 'status' => BuildArtifactStatus::BUILDING, 'built_by_operation_id' => $operation?->id, ]); try { $output = $this->remoteCommandRunner->run( $server, $strategy === BuildStrategy::EXTERNAL_REGISTRY ? $this->manifestDigestScript($artifact) : $this->buildScript($artifact, $strategy) ); $artifact->update([ 'image_digest' => $this->digestFromOutput($output), 'status' => BuildArtifactStatus::AVAILABLE, ]); return $artifact->refresh(); } catch (\Throwable $exception) { $artifact->update([ 'status' => BuildArtifactStatus::FAILED, 'metadata' => [ ...($artifact->metadata ?? []), 'error' => $exception->getMessage(), ], ]); throw $exception; } } private function buildServer(BuildArtifact $artifact): Server { if ($artifact->builtByService instanceof Service) { $server = $artifact->builtByService->replicas->first()?->server ?: $artifact->builtByService->server; if ($server instanceof Server) { return $server; } } if (($artifact->metadata['build_strategy'] ?? null) === BuildStrategy::DEDICATED_BUILDER->value) { throw new RuntimeException('Dedicated builder strategy requires a builder service.'); } $services = $artifact->environment->services() ->with(['server', 'replicas.server']) ->get(); $server = $services ->flatMap(fn (Service $service) => $service->replicas->pluck('server')->filter()) ->first() ?: $services->pluck('server')->filter()->first(); if (! $server instanceof Server) { $serverId = $services ->flatMap(fn (Service $service) => collect($service->config['server_ids'] ?? [])) ->filter() ->first(); $server = $serverId ? Server::find($serverId) : null; } if (! $server instanceof Server) { throw new RuntimeException('A target server is required to build this artifact over SSH.'); } return $server; } private function buildScript(BuildArtifact $artifact, BuildStrategy $strategy): string { $application = $artifact->environment->application; if (! $application->deploy_key_private) { throw new RuntimeException('Application does not have a deploy key.'); } $operationDirectory = '/home/keystone/operations/build-'.$artifact->id.'-'.str()->random(8); $imageReference = $artifact->registry_ref ?: $artifact->image_tag; $pushCommand = $strategy === BuildStrategy::DEDICATED_BUILDER && $artifact->registry_ref ? "\ndocker push ".escapeshellarg($imageReference) : ''; return implode("\n", [ 'set -euo pipefail', 'operation_dir='.escapeshellarg($operationDirectory), 'source_dir="$operation_dir/source"', 'rm -rf "$operation_dir"', 'mkdir -p "$operation_dir"', 'chmod 700 "$operation_dir"', 'cleanup() { rm -rf "$operation_dir"; }', 'trap cleanup EXIT', $this->writeFileCommand('$operation_dir/deploy_key', $application->deploy_key_private), 'chmod 600 "$operation_dir/deploy_key"', 'export GIT_SSH_COMMAND="ssh -i $operation_dir/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=no"', 'git clone --depth 1 --branch '.escapeshellarg($artifact->environment->branch).' '.escapeshellarg($application->repository_url).' "$source_dir"', $this->writeFileCommand('$source_dir/Dockerfile.keystone', $this->dockerfile($artifact)), 'cd "$source_dir"', 'docker build --file Dockerfile.keystone --tag '.escapeshellarg($imageReference).' .'.$pushCommand, 'digest=$(docker image inspect --format '.escapeshellarg('{{if .RepoDigests}}{{index .RepoDigests 0}}{{else}}{{.Id}}{{end}}').' '.escapeshellarg($imageReference).')', 'printf "image_digest=%s\n" "$digest"', ]); } private function manifestDigestScript(BuildArtifact $artifact): string { $imageReference = $artifact->registry_ref ?: $artifact->image_tag; return implode("\n", [ 'set -euo pipefail', 'manifest=$(docker manifest inspect '.escapeshellarg($imageReference).')', 'digest=$(printf "%s" "$manifest" | sed -n '.escapeshellarg('s/.*"digest": "\(sha256:[^"]*\)".*/\1/p').' | head -n 1)', 'test -n "$digest"', 'printf "image_digest=%s\n" "$digest"', ]); } private function dockerfile(BuildArtifact $artifact): string { $service = $artifact->environment->services() ->where('type', \App\Enums\ServiceType::LARAVEL) ->first(); if ($service && method_exists($service->driver(), 'dockerfileTemplate')) { return $service->driver()->dockerfileTemplate(); } return <<<'DOCKERFILE' FROM serversideup/php:8.4-frankenphp WORKDIR /var/www/html COPY --chown=www-data:www-data . . RUN composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader ENV SERVER_DOCUMENT_ROOT=/var/www/html/public DOCKERFILE; } private function writeFileCommand(string $path, string $contents): string { return implode("\n", [ 'cat > '.$path." <<'KEYSTONE_FILE'", rtrim($contents), 'KEYSTONE_FILE', ]); } private function digestFromOutput(string $output): string { if (preg_match('/image_digest=(?\S+)/', $output, $matches)) { return $this->digestFromOutput($matches['digest']); } if (str_contains($output, '@')) { return str($output)->after('@')->trim()->value(); } if (str_starts_with($output, 'sha256:')) { return $output; } throw new RuntimeException('Unable to resolve built image digest.'); } }