args['type'] ?? ''; $projectId = $this->args['projectId'] ?? ''; switch ($type) { case 'tag': $functionId = $this->args['functionId'] ?? ''; $tagId = $this->args['tagId'] ?? ''; Console::success("Creating build for tag: $tagId"); $this->buildTag($projectId, $functionId, $tagId); break; default: throw new \Exception('Invalid trigger'); break; } } protected function triggerBuildStage(string $projectId, string $buildId) { // TODO: What is a reasonable time to wait for a build to complete? $ch = \curl_init(); \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor:8080/v1/build/$buildId"); \curl_setopt($ch, CURLOPT_POST, true); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); \curl_setopt($ch, CURLOPT_TIMEOUT, 900); \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); \curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'x-appwrite-project: '.$projectId, 'x-appwrite-executor-key: '. App::getEnv('_APP_EXECUTOR_SECRET', '') ]); $response = \curl_exec($ch); $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = \curl_error($ch); if (!empty($error)) { throw new \Exception($error); } \curl_close($ch); if ($responseStatus !== 200) { throw new \Exception("Build failed with status code: $responseStatus"); } $response = json_decode($response, true); if (isset($response['error'])) { throw new \Exception($response['error']); } if (isset($response['success']) && $response['success'] === true) { return; } else { throw new \Exception("Build failed"); } } protected function triggerCreateRuntimeServer(string $projectId, string $functionId, string $tagId) { $ch = \curl_init(); \curl_setopt($ch, CURLOPT_URL, "http://appwrite-executor:8080/v1/create/runtime"); \curl_setopt($ch, CURLOPT_POST, true); \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); \curl_setopt($ch, CURLOPT_TIMEOUT, 900); \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); \curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'x-appwrite-project: '.$projectId, 'x-appwrite-executor-key: '. App::getEnv('_APP_EXECUTOR_SECRET', '') ]); \curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'functionId' => $functionId, 'tagId' => $tagId ])); $response = \curl_exec($ch); $responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = \curl_error($ch); if (!empty($error)) { throw new \Exception($error); } \curl_close($ch); if ($responseStatus !== 200) { throw new \Exception("Build failed with status code: $responseStatus"); } $response = json_decode($response, true); if (isset($response['error'])) { throw new \Exception($response['error']); } if (isset($response['success']) && $response['success'] === true) { return; } else { throw new \Exception("Build failed"); } } protected function buildTag(string $projectId, string $functionId, string $tagId) { $dbForProject = $this->getProjectDB($projectId); // TODO: Why does it need to skip authorization? $function = Authorization::skip(fn() => $dbForProject->getDocument('functions', $functionId)); if ($function->isEmpty()) { throw new Exception('Function not found', 404); } // Get tag document $tag = $dbForProject->getDocument('tags', $tagId); if ($tag->isEmpty()) { throw new Exception('Tag not found', 404); } $runtimes = Config::getParam('runtimes', []); $key = $function->getAttribute('runtime'); $runtime = isset($runtimes[$key]) ? $runtimes[$key] : null; if (\is_null($runtime)) { throw new Exception('Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); } $buildId = $tag->getAttribute('buildId', ''); // If build ID is empty, create a new build if (empty($buildId)) { try { $buildId = $dbForProject->getId(); // TODO : There is no way to associate a build with a tag. So we need to add a tagId attribute to the build document // TODO : What should be the read and write permissions for a build ? $dbForProject->createDocument('builds', new Document([ '$id' => $buildId, '$read' => ['role:all'], '$write' => ['role:all'], 'dateCreated' => time(), 'status' => 'processing', 'runtime' => $function->getAttribute('runtime'), 'outputPath' => '', 'source' => $tag->getAttribute('path'), 'sourceType' => Storage::DEVICE_LOCAL, 'stdout' => '', 'stderr' => '', 'buildTime' => 0, 'envVars' => [ 'ENTRYPOINT_NAME' => $tag->getAttribute('entrypoint'), 'APPWRITE_FUNCTION_ID' => $function->getId(), 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name', ''), 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'], 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'], 'APPWRITE_FUNCTION_PROJECT_ID' => $projectId, ] ])); $tag->setAttribute('buildId', $buildId); $tag = $dbForProject->updateDocument('tags', $tagId, $tag); } catch (\Throwable $th) { Console::error($th->getMessage()); $tag->setAttribute('status', 'failed'); $tag->setAttribute('buildId', ''); $tag = $dbForProject->updateDocument('tags', $tagId, $tag); return; } } // Build the Code try { Console::success("Creating Build with id: $buildId"); $tag->setAttribute('status', 'building'); $tag = $dbForProject->updateDocument('tags', $tagId, $tag); $this->triggerBuildStage($projectId, $buildId); } catch (\Throwable $th) { Console::error($th->getMessage()); $tag->setAttribute('status', 'failed'); $tag = $dbForProject->updateDocument('tags', $tagId, $tag); return; } Console::success("Build id: $buildId completed successfully"); // Update the schedule $schedule = $function->getAttribute('schedule', ''); $cron = (empty($function->getAttribute('tag')) && !empty($schedule)) ? new CronExpression($schedule) : null; $next = (empty($function->getAttribute('tag')) && !empty($schedule)) ? $cron->getNextRunDate()->format('U') : 0; // Grab build $build = $dbForProject->getDocument('builds', $buildId); // If the build failed, it won't be possible to deploy if ($build->getAttribute('status') !== 'ready') { throw new Exception('Build failed', 500); } if ($tag->getAttribute('automaticDeploy') === true) { // Update the function document setting the tag as the active one $function ->setAttribute('tag', $tag->getId()) ->setAttribute('scheduleNext', (int)$next); $function = $dbForProject->updateDocument('functions', $function->getId(), $function); } // Deploy Runtime Server try { Console::success("Creating Runtime Server"); $this->triggerCreateRuntimeServer($functionId, $projectId, $tagId, $dbForProject); } catch (\Throwable $th) { Console::error($th->getMessage()); $tag->setAttribute('status', 'failed'); $tag = $dbForProject->updateDocument('tags', $tagId, $tag); return; } Console::success("Runtime Server created successfully"); } public function shutdown(): void { Console::success("Shutting Down..."); } }