mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(api): add endpoints to list, get, and open preview deployments
- GET /applications/{uuid}/previews and GET /applications/{uuid}/previews/{pull_request_id}
return preview deployments (read ability).
- POST /applications/{uuid}/previews opens a preview for a pull request and queues its
deployment, or redeploys an existing preview (deploy ability). It accepts git_type,
commit (required for Bitbucket), docker_tag (Docker Image apps), force, and
instant_deploy. GitHub App and GitLab App sources set git_type automatically.
- Fix POST /deploy with pull_request_id: pass the preview git_type to the deployment, so
deploy-key and public-repository applications build the pull request instead of the
base branch.
- Regenerate the OpenAPI spec (also adds the missing service_name logs parameter).
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Enums\BuildPackTypes;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\GitlabApp;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class ApplicationPreviewsController extends Controller
|
||||
{
|
||||
private const GIT_TYPES = ['github', 'gitlab', 'gitea', 'bitbucket'];
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Preview Deployments',
|
||||
description: 'List the preview deployments of an application, newest pull request first.',
|
||||
path: '/applications/{uuid}/previews',
|
||||
operationId: 'list-preview-deployments-by-application-uuid',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Applications'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', description: 'UUID of the application.', required: true, schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'List of preview deployments.', content: new OA\JsonContent(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/ApplicationPreview'),
|
||||
)),
|
||||
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||
],
|
||||
)]
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('view', $application);
|
||||
|
||||
$previews = $application->previews()
|
||||
->get()
|
||||
->map(fn (ApplicationPreview $preview): array => $this->formatPreview($preview));
|
||||
|
||||
return response()->json($previews);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Get Preview Deployment',
|
||||
description: 'Get a preview deployment by pull request ID.',
|
||||
path: '/applications/{uuid}/previews/{pull_request_id}',
|
||||
operationId: 'get-preview-deployment-by-pull-request-id',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Applications'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', description: 'UUID of the application.', required: true, schema: new OA\Schema(type: 'string')),
|
||||
new OA\Parameter(name: 'pull_request_id', in: 'path', description: 'Pull request ID of the preview.', required: true, schema: new OA\Schema(type: 'integer')),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Preview deployment.', content: new OA\JsonContent(ref: '#/components/schemas/ApplicationPreview')),
|
||||
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||
],
|
||||
)]
|
||||
public function show(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('view', $application);
|
||||
|
||||
$pullRequestIdRaw = $request->route('pull_request_id');
|
||||
if (! ctype_digit((string) $pullRequestIdRaw) || (int) $pullRequestIdRaw <= 0) {
|
||||
return response()->json(['message' => 'Invalid pull_request_id.'], 422);
|
||||
}
|
||||
|
||||
$preview = $application->previews()->where('pull_request_id', (int) $pullRequestIdRaw)->first();
|
||||
if (! $preview) {
|
||||
return response()->json(['message' => 'Preview not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json($this->formatPreview($preview));
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Preview Deployment',
|
||||
description: 'Open a preview deployment for a pull request and queue its deployment. When the preview already exists, it is redeployed. Git based applications need git_type unless they use a GitHub or GitLab App source; Bitbucket also needs commit. Docker Image applications need docker_tag for a new preview.',
|
||||
path: '/applications/{uuid}/previews',
|
||||
operationId: 'create-preview-deployment-by-application-uuid',
|
||||
security: [['bearerAuth' => []]],
|
||||
tags: ['Applications'],
|
||||
parameters: [
|
||||
new OA\Parameter(name: 'uuid', in: 'path', description: 'UUID of the application.', required: true, schema: new OA\Schema(type: 'string')),
|
||||
],
|
||||
requestBody: new OA\RequestBody(required: true, content: new OA\JsonContent(
|
||||
required: ['pull_request_id'],
|
||||
properties: [
|
||||
new OA\Property(property: 'pull_request_id', type: 'integer', minimum: 1, example: 42),
|
||||
new OA\Property(property: 'pull_request_html_url', type: 'string', nullable: true, example: 'https://github.com/org/repo/pull/42'),
|
||||
new OA\Property(property: 'git_type', type: 'string', nullable: true, enum: self::GIT_TYPES, description: 'Git provider that hosts the pull request. Sets the ref Coolify fetches (pull/{id}/head or merge-requests/{id}/head).'),
|
||||
new OA\Property(property: 'commit', type: 'string', nullable: true, description: 'Commit SHA to deploy. Required for Bitbucket.'),
|
||||
new OA\Property(property: 'docker_tag', type: 'string', nullable: true, description: 'Docker image tag. Docker Image applications only.'),
|
||||
new OA\Property(property: 'force', type: 'boolean', default: false, description: 'Rebuild without cache.'),
|
||||
new OA\Property(property: 'instant_deploy', type: 'boolean', default: true, description: 'Queue a deployment. Set to false to only create the preview, for example to set its domains first.'),
|
||||
],
|
||||
)),
|
||||
responses: [
|
||||
new OA\Response(response: 200, description: 'Existing preview deployment updated.', content: new OA\JsonContent(ref: '#/components/schemas/ApplicationPreviewDeployment')),
|
||||
new OA\Response(response: 201, description: 'Preview deployment created.', content: new OA\JsonContent(ref: '#/components/schemas/ApplicationPreviewDeployment')),
|
||||
new OA\Response(response: 400, ref: '#/components/responses/400'),
|
||||
new OA\Response(response: 401, ref: '#/components/responses/401'),
|
||||
new OA\Response(response: 404, ref: '#/components/responses/404'),
|
||||
new OA\Response(response: 422, ref: '#/components/responses/422'),
|
||||
new OA\Response(response: 429, description: 'Deployment queue is full.'),
|
||||
],
|
||||
)]
|
||||
#[OA\Schema(
|
||||
schema: 'ApplicationPreview',
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'uuid', type: 'string'),
|
||||
new OA\Property(property: 'pull_request_id', type: 'integer'),
|
||||
new OA\Property(property: 'pull_request_html_url', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'git_type', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'status', type: 'string'),
|
||||
new OA\Property(property: 'domains', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'docker_compose_domains', type: 'array', nullable: true, items: new OA\Items(properties: [
|
||||
new OA\Property(property: 'name', type: 'string'),
|
||||
new OA\Property(property: 'domain', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'redirect', type: 'string', nullable: true),
|
||||
], type: 'object')),
|
||||
new OA\Property(property: 'domain_port_overrides', type: 'object', nullable: true),
|
||||
new OA\Property(property: 'docker_registry_image_tag', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'last_online_at', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'created_at', type: 'string'),
|
||||
new OA\Property(property: 'updated_at', type: 'string'),
|
||||
],
|
||||
)]
|
||||
#[OA\Schema(
|
||||
schema: 'ApplicationPreviewDeployment',
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
new OA\Property(property: 'deployment_uuid', type: 'string', nullable: true),
|
||||
new OA\Property(property: 'preview', ref: '#/components/schemas/ApplicationPreview'),
|
||||
],
|
||||
)]
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
$this->authorize('deploy', $application);
|
||||
|
||||
$isDockerImage = $application->build_pack === 'dockerimage';
|
||||
if (! $isDockerImage && ! $application->git_based()) {
|
||||
return response()->json(['message' => 'Preview deployments need a Git based or Docker Image application.'], 400);
|
||||
}
|
||||
|
||||
$rules = [
|
||||
'pull_request_id' => 'required|integer|min:1',
|
||||
'pull_request_html_url' => 'nullable|url|max:2048',
|
||||
'force' => 'boolean',
|
||||
'instant_deploy' => 'boolean',
|
||||
];
|
||||
if ($isDockerImage) {
|
||||
$rules['git_type'] = 'missing';
|
||||
$rules['commit'] = 'missing';
|
||||
$rules['docker_tag'] = ['nullable', 'string', 'regex:/^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/'];
|
||||
} else {
|
||||
$rules['git_type'] = ['nullable', 'string', Rule::in(self::GIT_TYPES)];
|
||||
$rules['commit'] = ['nullable', 'string', 'regex:/^[0-9a-fA-F]{7,40}$/', 'required_if:git_type,bitbucket'];
|
||||
$rules['docker_tag'] = 'missing';
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), $rules);
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['message' => 'Validation failed.', 'errors' => $validator->errors()], 422);
|
||||
}
|
||||
|
||||
$pullRequestId = (int) $request->input('pull_request_id');
|
||||
$dockerTag = $request->string('docker_tag')->trim()->value() ?: null;
|
||||
$preview = $application->previews()->where('pull_request_id', $pullRequestId)->first();
|
||||
$gitType = $request->input('git_type') ?? $preview?->git_type ?? $this->sourceGitType($application);
|
||||
|
||||
if (! $isDockerImage && $gitType === null) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['git_type' => ['The git type field is required for applications without a GitHub or GitLab App source.']],
|
||||
], 422);
|
||||
}
|
||||
if ($isDockerImage && $dockerTag === null && blank($preview?->docker_registry_image_tag)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['docker_tag' => ['The docker tag field is required for a new Docker Image preview.']],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$created = ! $preview;
|
||||
if ($created) {
|
||||
$isCompose = $application->build_pack === BuildPackTypes::DOCKERCOMPOSE->value;
|
||||
$preview = ApplicationPreview::create([
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pullRequestId,
|
||||
'pull_request_html_url' => $request->input('pull_request_html_url') ?? '',
|
||||
'git_type' => $isDockerImage ? null : $gitType,
|
||||
'docker_compose_domains' => $isCompose ? $application->docker_compose_domains : null,
|
||||
'docker_registry_image_tag' => $dockerTag,
|
||||
]);
|
||||
$isCompose
|
||||
? $preview->generate_preview_fqdn_compose()
|
||||
: $preview->generate_preview_fqdn();
|
||||
} else {
|
||||
$preview->fill(array_filter([
|
||||
'pull_request_html_url' => $request->input('pull_request_html_url'),
|
||||
'git_type' => $isDockerImage ? null : $gitType,
|
||||
'docker_registry_image_tag' => $dockerTag,
|
||||
], fn (mixed $value): bool => $value !== null));
|
||||
$preview->save();
|
||||
}
|
||||
|
||||
if ($created) {
|
||||
auditLog('api.application.preview_created', [
|
||||
'team_id' => $teamId,
|
||||
'application_uuid' => $application->uuid,
|
||||
'pull_request_id' => $pullRequestId,
|
||||
]);
|
||||
}
|
||||
|
||||
$status = $created ? 201 : 200;
|
||||
if (! $request->boolean('instant_deploy', true)) {
|
||||
return response()->json([
|
||||
'message' => $created ? 'Preview created.' : 'Preview updated.',
|
||||
'deployment_uuid' => null,
|
||||
'preview' => $this->formatPreview($preview->refresh()),
|
||||
], $status);
|
||||
}
|
||||
|
||||
$deploymentUuid = new_public_id();
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deploymentUuid,
|
||||
pull_request_id: $pullRequestId,
|
||||
commit: $request->input('commit'),
|
||||
force_rebuild: $request->boolean('force'),
|
||||
is_api: true,
|
||||
git_type: $preview->git_type,
|
||||
docker_registry_image_tag: $preview->docker_registry_image_tag,
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
return response()->json(['message' => $result['message']], 429)->header('Retry-After', 60);
|
||||
}
|
||||
|
||||
$queued = $result['status'] !== 'skipped';
|
||||
if ($queued) {
|
||||
auditLog('api.deployment.triggered', [
|
||||
'resource_type' => 'application',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $deploymentUuid,
|
||||
'force_rebuild' => $request->boolean('force'),
|
||||
'pull_request_id' => $pullRequestId,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => $queued ? 'Preview deployment queued.' : $result['message'],
|
||||
'deployment_uuid' => $queued ? $deploymentUuid : null,
|
||||
'preview' => $this->formatPreview($preview->refresh()),
|
||||
], $status);
|
||||
}
|
||||
|
||||
private function sourceGitType(Application $application): ?string
|
||||
{
|
||||
return match ($application->source?->getMorphClass()) {
|
||||
GithubApp::class => 'github',
|
||||
GitlabApp::class => 'gitlab',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatPreview(ApplicationPreview $preview): array
|
||||
{
|
||||
$composeDomains = json_decode($preview->docker_compose_domains ?: 'null', true);
|
||||
|
||||
return [
|
||||
'uuid' => $preview->uuid,
|
||||
'pull_request_id' => $preview->pull_request_id,
|
||||
'pull_request_html_url' => $preview->pull_request_html_url ?: null,
|
||||
'git_type' => $preview->git_type,
|
||||
'status' => $preview->status,
|
||||
'domains' => $preview->fqdn,
|
||||
'docker_compose_domains' => is_array($composeDomains)
|
||||
? collect($composeDomains)
|
||||
->map(fn (mixed $entry, string|int $name): array => ['name' => (string) $name, 'domain' => data_get($entry, 'domain') ?: null, 'redirect' => data_get($entry, 'redirect')])
|
||||
->values()
|
||||
->all()
|
||||
: null,
|
||||
'domain_port_overrides' => $preview->domain_port_overrides,
|
||||
'docker_registry_image_tag' => $preview->docker_registry_image_tag,
|
||||
'last_online_at' => $preview->last_online_at,
|
||||
'created_at' => $preview->created_at,
|
||||
'updated_at' => $preview->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -432,8 +432,8 @@ class DeployController extends Controller
|
||||
$resource = getResourceByUuid($uuid, $teamId);
|
||||
if ($resource) {
|
||||
$dockerTagForResource = $dockerTag;
|
||||
$preview = null;
|
||||
if ($pr !== 0) {
|
||||
$preview = null;
|
||||
if ($resource instanceof Application && $resource->build_pack === 'dockerimage') {
|
||||
$preview = $this->upsertDockerImagePreview($resource, $pr, $dockerTag);
|
||||
$dockerTagForResource = $preview?->docker_registry_image_tag;
|
||||
@@ -446,7 +446,7 @@ class DeployController extends Controller
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$result = $this->deploy_resource($resource, $force, $pr, $dockerTagForResource);
|
||||
$result = $this->deploy_resource($resource, $force, $pr, $dockerTagForResource, $preview?->git_type);
|
||||
if (isset($result['status']) && $result['status'] === 429) {
|
||||
return response()->json(['message' => $result['message']], 429)->header('Retry-After', 60);
|
||||
}
|
||||
@@ -519,7 +519,7 @@ class DeployController extends Controller
|
||||
return response()->json(['message' => 'No resources found with this tag.'], 404);
|
||||
}
|
||||
|
||||
public function deploy_resource($resource, bool $force = false, int $pr = 0, ?string $dockerTag = null): array
|
||||
public function deploy_resource($resource, bool $force = false, int $pr = 0, ?string $dockerTag = null, ?string $gitType = null): array
|
||||
{
|
||||
$message = null;
|
||||
$deployment_uuid = null;
|
||||
@@ -544,6 +544,7 @@ class DeployController extends Controller
|
||||
force_rebuild: $force,
|
||||
pull_request_id: $pr,
|
||||
is_api: true,
|
||||
git_type: $gitType,
|
||||
docker_registry_image_tag: $dockerTag,
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
|
||||
+516
-173
@@ -11,6 +11,401 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"\/applications\/{uuid}\/previews": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "List Preview Deployments",
|
||||
"description": "List the preview deployments of an application, newest pull request first.",
|
||||
"operationId": "list-preview-deployments-by-application-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of preview deployments.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#\/components\/schemas\/ApplicationPreview"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Create Preview Deployment",
|
||||
"description": "Open a preview deployment for a pull request and queue its deployment. When the preview already exists, it is redeployed. Git based applications need git_type unless they use a GitHub or GitLab App source; Bitbucket also needs commit. Docker Image applications need docker_tag for a new preview.",
|
||||
"operationId": "create-preview-deployment-by-application-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"pull_request_id"
|
||||
],
|
||||
"properties": {
|
||||
"pull_request_id": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"example": 42
|
||||
},
|
||||
"pull_request_html_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"example": "https:\/\/github.com\/org\/repo\/pull\/42"
|
||||
},
|
||||
"git_type": {
|
||||
"description": "Git provider that hosts the pull request. Sets the ref Coolify fetches (pull\/{id}\/head or merge-requests\/{id}\/head).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"enum": [
|
||||
"github",
|
||||
"gitlab",
|
||||
"gitea",
|
||||
"bitbucket"
|
||||
]
|
||||
},
|
||||
"commit": {
|
||||
"description": "Commit SHA to deploy. Required for Bitbucket.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"docker_tag": {
|
||||
"description": "Docker image tag. Docker Image applications only.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"force": {
|
||||
"description": "Rebuild without cache.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"instant_deploy": {
|
||||
"description": "Queue a deployment. Set to false to only create the preview, for example to set its domains first.",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Existing preview deployment updated.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"$ref": "#\/components\/schemas\/ApplicationPreviewDeployment"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"201": {
|
||||
"description": "Preview deployment created.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"$ref": "#\/components\/schemas\/ApplicationPreviewDeployment"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
},
|
||||
"429": {
|
||||
"description": "Deployment queue is full."
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}\/previews\/{pull_request_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Get Preview Deployment",
|
||||
"description": "Get a preview deployment by pull request ID.",
|
||||
"operationId": "get-preview-deployment-by-pull-request-id",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pull_request_id",
|
||||
"in": "path",
|
||||
"description": "Pull request ID of the preview.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Preview deployment.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"$ref": "#\/components\/schemas\/ApplicationPreview"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Delete Preview Deployment",
|
||||
"description": "Delete a preview deployment for a pull request. Cancels active deployments, stops containers, removes volumes\/networks, and deletes the preview record.",
|
||||
"operationId": "delete-preview-deployment-by-pull-request-id",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pull_request_id",
|
||||
"in": "path",
|
||||
"description": "Pull request ID of the preview to delete.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Preview deletion queued.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Update Preview Domains",
|
||||
"description": "Replace domains for a preview deployment. Use domains for regular applications or docker_compose_domains for Docker Compose applications. Ports are stored as internal overrides while public domains remain portless.",
|
||||
"operationId": "update-preview-domains-by-pull-request-id",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pull_request_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"domains": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"example": "https:\/\/pr.example.com:3000"
|
||||
},
|
||||
"docker_compose_domains": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"domain": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"redirect": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"enum": [
|
||||
"www",
|
||||
"non-www",
|
||||
"both"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"force_domain_override": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Preview domains updated."
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#\/components\/responses\/403"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"409": {
|
||||
"description": "Domain conflict."
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}\/secret-manager": {
|
||||
"patch": {
|
||||
"tags": [
|
||||
@@ -3575,6 +3970,15 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "service_name",
|
||||
"in": "query",
|
||||
"description": "Return logs only from the container of the Docker Compose service with this name. Returns 404 when no running container matches.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -3670,6 +4074,15 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "service_name",
|
||||
"in": "query",
|
||||
"description": "Return logs only from the container of the Docker Compose service with this name. Returns 404 when no running container matches.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -3708,175 +4121,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}\/previews\/{pull_request_id}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Delete Preview Deployment",
|
||||
"description": "Delete a preview deployment for a pull request. Cancels active deployments, stops containers, removes volumes\/networks, and deletes the preview record.",
|
||||
"operationId": "delete-preview-deployment-by-pull-request-id",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pull_request_id",
|
||||
"in": "path",
|
||||
"description": "Pull request ID of the preview to delete.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Preview deletion queued.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Update Preview Domains",
|
||||
"description": "Replace domains for a preview deployment. Use domains for regular applications or docker_compose_domains for Docker Compose applications. Ports are stored as internal overrides while public domains remain portless.",
|
||||
"operationId": "update-preview-domains-by-pull-request-id",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "pull_request_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"domains": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"example": "https:\/\/pr.example.com:3000"
|
||||
},
|
||||
"docker_compose_domains": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"domain": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"redirect": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"enum": [
|
||||
"www",
|
||||
"non-www",
|
||||
"both"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"force_domain_override": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Preview domains updated."
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#\/components\/responses\/403"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"409": {
|
||||
"description": "Domain conflict."
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}\/envs": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -22592,6 +22836,105 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ApplicationPreview": {
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "string"
|
||||
},
|
||||
"pull_request_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pull_request_html_url": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"git_type": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"domains": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"docker_compose_domains": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"domain": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"redirect": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"domain_port_overrides": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"docker_registry_image_tag": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"last_online_at": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ApplicationPreviewDeployment": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"deployment_uuid": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"preview": {
|
||||
"$ref": "#\/components\/schemas\/ApplicationPreview"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"VolumeBackupScheduleRequest": {
|
||||
"required": [
|
||||
"frequency"
|
||||
@@ -24227,14 +24570,14 @@
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Secret Managers",
|
||||
"description": "Secret Managers"
|
||||
},
|
||||
{
|
||||
"name": "Applications",
|
||||
"description": "Applications"
|
||||
},
|
||||
{
|
||||
"name": "Secret Managers",
|
||||
"description": "Secret Managers"
|
||||
},
|
||||
{
|
||||
"name": "Cloud-init Scripts",
|
||||
"description": "Cloud-init Scripts"
|
||||
|
||||
+317
-96
@@ -7,6 +7,241 @@ servers:
|
||||
url: 'https://app.coolify.io/api/v1'
|
||||
description: 'Coolify Cloud API. Change the host to your own instance if you are self-hosting.'
|
||||
paths:
|
||||
'/applications/{uuid}/previews':
|
||||
get:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'List Preview Deployments'
|
||||
description: 'List the preview deployments of an application, newest pull request first.'
|
||||
operationId: list-preview-deployments-by-application-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'List of preview deployments.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApplicationPreview'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
post:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Create Preview Deployment'
|
||||
description: 'Open a preview deployment for a pull request and queue its deployment. When the preview already exists, it is redeployed. Git based applications need git_type unless they use a GitHub or GitLab App source; Bitbucket also needs commit. Docker Image applications need docker_tag for a new preview.'
|
||||
operationId: create-preview-deployment-by-application-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- pull_request_id
|
||||
properties:
|
||||
pull_request_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
example: 42
|
||||
pull_request_html_url:
|
||||
type: [string, 'null']
|
||||
example: 'https://github.com/org/repo/pull/42'
|
||||
git_type:
|
||||
description: 'Git provider that hosts the pull request. Sets the ref Coolify fetches (pull/{id}/head or merge-requests/{id}/head).'
|
||||
type: [string, 'null']
|
||||
enum: [github, gitlab, gitea, bitbucket]
|
||||
commit:
|
||||
description: 'Commit SHA to deploy. Required for Bitbucket.'
|
||||
type: [string, 'null']
|
||||
docker_tag:
|
||||
description: 'Docker image tag. Docker Image applications only.'
|
||||
type: [string, 'null']
|
||||
force:
|
||||
description: 'Rebuild without cache.'
|
||||
type: boolean
|
||||
default: false
|
||||
instant_deploy:
|
||||
description: 'Queue a deployment. Set to false to only create the preview, for example to set its domains first.'
|
||||
type: boolean
|
||||
default: true
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
description: 'Existing preview deployment updated.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApplicationPreviewDeployment'
|
||||
'201':
|
||||
description: 'Preview deployment created.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApplicationPreviewDeployment'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
'429':
|
||||
description: 'Deployment queue is full.'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}/previews/{pull_request_id}':
|
||||
get:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Get Preview Deployment'
|
||||
description: 'Get a preview deployment by pull request ID.'
|
||||
operationId: get-preview-deployment-by-pull-request-id
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: pull_request_id
|
||||
in: path
|
||||
description: 'Pull request ID of the preview.'
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: 'Preview deployment.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApplicationPreview'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
delete:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Delete Preview Deployment'
|
||||
description: 'Delete a preview deployment for a pull request. Cancels active deployments, stops containers, removes volumes/networks, and deletes the preview record.'
|
||||
operationId: delete-preview-deployment-by-pull-request-id
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: pull_request_id
|
||||
in: path
|
||||
description: 'Pull request ID of the preview to delete.'
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: 'Preview deletion queued.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
patch:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Update Preview Domains'
|
||||
description: 'Replace domains for a preview deployment. Use domains for regular applications or docker_compose_domains for Docker Compose applications. Ports are stored as internal overrides while public domains remain portless.'
|
||||
operationId: update-preview-domains-by-pull-request-id
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: pull_request_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
domains:
|
||||
type: [string, 'null']
|
||||
example: 'https://pr.example.com:3000'
|
||||
docker_compose_domains:
|
||||
type: [array, 'null']
|
||||
items: { properties: { name: { type: string }, domain: { type: [string, 'null'] }, redirect: { type: [string, 'null'], enum: [www, non-www, both] } }, type: object }
|
||||
force_domain_override:
|
||||
type: boolean
|
||||
default: false
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
description: 'Preview domains updated.'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'403':
|
||||
$ref: '#/components/responses/403'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'409':
|
||||
description: 'Domain conflict.'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}/secret-manager':
|
||||
patch:
|
||||
tags:
|
||||
@@ -2360,6 +2595,13 @@ paths:
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
-
|
||||
name: service_name
|
||||
in: query
|
||||
description: 'Return logs only from the container of the Docker Compose service with this name. Returns 404 when no running container matches.'
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Get application logs by UUID.'
|
||||
@@ -2426,6 +2668,13 @@ paths:
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
-
|
||||
name: service_name
|
||||
in: query
|
||||
description: 'Return logs only from the container of the Docker Compose service with this name. Returns 404 when no running container matches.'
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Preview runtime logs.'
|
||||
@@ -2446,99 +2695,6 @@ paths:
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}/previews/{pull_request_id}':
|
||||
delete:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Delete Preview Deployment'
|
||||
description: 'Delete a preview deployment for a pull request. Cancels active deployments, stops containers, removes volumes/networks, and deletes the preview record.'
|
||||
operationId: delete-preview-deployment-by-pull-request-id
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: pull_request_id
|
||||
in: path
|
||||
description: 'Pull request ID of the preview to delete.'
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: 'Preview deletion queued.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
patch:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Update Preview Domains'
|
||||
description: 'Replace domains for a preview deployment. Use domains for regular applications or docker_compose_domains for Docker Compose applications. Ports are stored as internal overrides while public domains remain portless.'
|
||||
operationId: update-preview-domains-by-pull-request-id
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: pull_request_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
domains:
|
||||
type: [string, 'null']
|
||||
example: 'https://pr.example.com:3000'
|
||||
docker_compose_domains:
|
||||
type: [array, 'null']
|
||||
items: { properties: { name: { type: string }, domain: { type: [string, 'null'] }, redirect: { type: [string, 'null'], enum: [www, non-www, both] } }, type: object }
|
||||
force_domain_override:
|
||||
type: boolean
|
||||
default: false
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
description: 'Preview domains updated.'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'403':
|
||||
$ref: '#/components/responses/403'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'409':
|
||||
description: 'Domain conflict.'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}/envs':
|
||||
get:
|
||||
tags:
|
||||
@@ -14347,6 +14503,71 @@ components:
|
||||
- 'null'
|
||||
format: date-time
|
||||
type: object
|
||||
ApplicationPreview:
|
||||
properties:
|
||||
uuid:
|
||||
type: string
|
||||
pull_request_id:
|
||||
type: integer
|
||||
pull_request_html_url:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
git_type:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
status:
|
||||
type: string
|
||||
domains:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
docker_compose_domains:
|
||||
type:
|
||||
- array
|
||||
- 'null'
|
||||
items:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
domain:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
redirect:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
type: object
|
||||
domain_port_overrides:
|
||||
type:
|
||||
- object
|
||||
- 'null'
|
||||
docker_registry_image_tag:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
last_online_at:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
created_at:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
ApplicationPreviewDeployment:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
deployment_uuid:
|
||||
type:
|
||||
- string
|
||||
- 'null'
|
||||
preview:
|
||||
$ref: '#/components/schemas/ApplicationPreview'
|
||||
type: object
|
||||
VolumeBackupScheduleRequest:
|
||||
required:
|
||||
- frequency
|
||||
@@ -15530,12 +15751,12 @@ components:
|
||||
description: 'Go to `Keys & Tokens` / `API tokens` and create a new token. Use the token as the bearer token.'
|
||||
scheme: bearer
|
||||
tags:
|
||||
-
|
||||
name: 'Secret Managers'
|
||||
description: 'Secret Managers'
|
||||
-
|
||||
name: Applications
|
||||
description: Applications
|
||||
-
|
||||
name: 'Secret Managers'
|
||||
description: 'Secret Managers'
|
||||
-
|
||||
name: 'Cloud-init Scripts'
|
||||
description: 'Cloud-init Scripts'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\ApplicationPreviewsController;
|
||||
use App\Http\Controllers\Api\ApplicationsController;
|
||||
use App\Http\Controllers\Api\ApplicationSecretManagerController;
|
||||
use App\Http\Controllers\Api\AuditEventsController;
|
||||
@@ -290,6 +291,9 @@ Route::group([
|
||||
Route::post('/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
||||
Route::post('/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:deploy']);
|
||||
|
||||
Route::get('/applications/{uuid}/previews', [ApplicationPreviewsController::class, 'index'])->middleware(['api.ability:read']);
|
||||
Route::post('/applications/{uuid}/previews', [ApplicationPreviewsController::class, 'store'])->middleware(['api.ability:deploy']);
|
||||
Route::get('/applications/{uuid}/previews/{pull_request_id}', [ApplicationPreviewsController::class, 'show'])->middleware(['api.ability:read']);
|
||||
Route::patch('/applications/{uuid}/previews/{pull_request_id}', [ApplicationsController::class, 'update_preview_by_pull_request_id'])->middleware(['api.ability:write']);
|
||||
Route::delete('/applications/{uuid}/previews/{pull_request_id}', [ApplicationsController::class, 'delete_preview_by_pull_request_id'])->middleware(['api.ability:write']);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Environment;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
@@ -500,3 +501,221 @@ describe('GET /api/v1/applications/{uuid}/logs service_name', function () {
|
||||
->assertJson(['message' => "No running container found for service_name 'work'."]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/applications/{uuid}/previews', function () {
|
||||
test('lists only the previews of the application', function () {
|
||||
createPreview($this->application, 12);
|
||||
createPreview($this->application, 3);
|
||||
$otherApplication = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
createPreview($otherApplication, 99);
|
||||
|
||||
$this->withHeaders(previewAuthHeaders(createTeamApiToken($this->user, $this->team, ['read'])))
|
||||
->getJson("/api/v1/applications/{$this->application->uuid}/previews")
|
||||
->assertOk()
|
||||
->assertJsonCount(2)
|
||||
->assertJsonPath('0.pull_request_id', 12)
|
||||
->assertJsonPath('1.pull_request_id', 3)
|
||||
->assertJsonPath('1.pull_request_html_url', 'https://github.com/example/repo/pull/3');
|
||||
});
|
||||
|
||||
test('returns 404 for an application of another team', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherTeam->members()->attach($otherUser->id, ['role' => 'owner']);
|
||||
createPreview($this->application, 3);
|
||||
|
||||
$this->withHeaders(previewAuthHeaders(createTeamApiToken($otherUser, $otherTeam, ['*'])))
|
||||
->getJson("/api/v1/applications/{$this->application->uuid}/previews")
|
||||
->assertNotFound();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/applications/{uuid}/previews/{pull_request_id}', function () {
|
||||
test('returns the preview', function () {
|
||||
$preview = createPreview($this->application, 42);
|
||||
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->getJson("/api/v1/applications/{$this->application->uuid}/previews/42")
|
||||
->assertOk()
|
||||
->assertJsonPath('uuid', $preview->uuid)
|
||||
->assertJsonPath('pull_request_html_url', 'https://github.com/example/repo/pull/42');
|
||||
});
|
||||
|
||||
test('returns 404 when the preview does not exist', function () {
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->getJson("/api/v1/applications/{$this->application->uuid}/previews/42")
|
||||
->assertNotFound()
|
||||
->assertJson(['message' => 'Preview not found.']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/applications/{uuid}/previews', function () {
|
||||
test('creates a preview and queues a deployment of the merge request ref', function () {
|
||||
$this->application->update(['fqdn' => 'https://app.example.com']);
|
||||
|
||||
$response = $this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->postJson("/api/v1/applications/{$this->application->uuid}/previews", [
|
||||
'pull_request_id' => 42,
|
||||
'pull_request_html_url' => 'https://gitlab.com/example/repo/-/merge_requests/42',
|
||||
'git_type' => 'gitlab',
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('preview.pull_request_id', 42)
|
||||
->assertJsonPath('preview.git_type', 'gitlab');
|
||||
|
||||
expect($response->json('preview.domains'))->toContain('app.example.com');
|
||||
|
||||
$deployment = $this->application->deployment_queue()->latest('id')->first();
|
||||
expect($deployment->deployment_uuid)->toBe($response->json('deployment_uuid'))
|
||||
->and($deployment->pull_request_id)->toBe(42)
|
||||
->and($deployment->git_type)->toBe('gitlab');
|
||||
});
|
||||
|
||||
test('uses the GitHub App source to choose the pull request ref', function () {
|
||||
$githubApp = GithubApp::create([
|
||||
'name' => 'preview-api-app',
|
||||
'team_id' => $this->team->id,
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'is_public' => false,
|
||||
'app_id' => 111,
|
||||
'installation_id' => 222,
|
||||
'client_id' => 'client',
|
||||
'client_secret' => 'secret',
|
||||
'webhook_secret' => 'webhook',
|
||||
]);
|
||||
$this->application->update(['source_id' => $githubApp->id, 'source_type' => GithubApp::class]);
|
||||
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->postJson("/api/v1/applications/{$this->application->uuid}/previews", ['pull_request_id' => 7])
|
||||
->assertCreated()
|
||||
->assertJsonPath('preview.git_type', 'github');
|
||||
|
||||
expect($this->application->deployment_queue()->latest('id')->first()->git_type)->toBe('github');
|
||||
});
|
||||
|
||||
test('requires git_type when the application has no Git App source', function () {
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->postJson("/api/v1/applications/{$this->application->uuid}/previews", ['pull_request_id' => 7])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('git_type');
|
||||
|
||||
expect(ApplicationPreview::where('application_id', $this->application->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('requires a commit for Bitbucket pull requests', function () {
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->postJson("/api/v1/applications/{$this->application->uuid}/previews", [
|
||||
'pull_request_id' => 7,
|
||||
'git_type' => 'bitbucket',
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('commit');
|
||||
});
|
||||
|
||||
test('redeploys an existing preview with its stored git type', function () {
|
||||
$preview = createPreview($this->application, 42);
|
||||
$preview->update(['git_type' => 'gitea']);
|
||||
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->postJson("/api/v1/applications/{$this->application->uuid}/previews", ['pull_request_id' => 42])
|
||||
->assertOk()
|
||||
->assertJsonPath('preview.uuid', $preview->uuid);
|
||||
|
||||
expect(ApplicationPreview::where('application_id', $this->application->id)->count())->toBe(1)
|
||||
->and($this->application->deployment_queue()->latest('id')->first()->git_type)->toBe('gitea');
|
||||
});
|
||||
|
||||
test('creates the preview without a deployment when instant_deploy is false', function () {
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->postJson("/api/v1/applications/{$this->application->uuid}/previews", [
|
||||
'pull_request_id' => 42,
|
||||
'git_type' => 'github',
|
||||
'instant_deploy' => false,
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('deployment_uuid', null);
|
||||
|
||||
expect($this->application->deployment_queue()->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('requires a docker tag for a new Docker Image preview and stores it', function () {
|
||||
$this->application->update([
|
||||
'build_pack' => 'dockerimage',
|
||||
'docker_registry_image_name' => 'ghcr.io/example/app',
|
||||
'docker_registry_image_tag' => 'latest',
|
||||
]);
|
||||
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->postJson("/api/v1/applications/{$this->application->uuid}/previews", ['pull_request_id' => 5])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('docker_tag');
|
||||
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->postJson("/api/v1/applications/{$this->application->uuid}/previews", [
|
||||
'pull_request_id' => 5,
|
||||
'docker_tag' => 'pr-5',
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('preview.docker_registry_image_tag', 'pr-5');
|
||||
|
||||
expect($this->application->deployment_queue()->latest('id')->first()->docker_registry_image_tag)->toBe('pr-5');
|
||||
});
|
||||
|
||||
test('rejects applications without a Git repository or Docker image', function () {
|
||||
$this->application->update(['dockerfile' => 'FROM nginx']);
|
||||
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->postJson("/api/v1/applications/{$this->application->uuid}/previews", [
|
||||
'pull_request_id' => 5,
|
||||
'git_type' => 'github',
|
||||
])
|
||||
->assertBadRequest();
|
||||
});
|
||||
|
||||
test('returns 403 when token lacks deploy ability', function (array $abilities) {
|
||||
$this->withHeaders(previewAuthHeaders(createTeamApiToken($this->user, $this->team, $abilities)))
|
||||
->postJson("/api/v1/applications/{$this->application->uuid}/previews", [
|
||||
'pull_request_id' => 5,
|
||||
'git_type' => 'github',
|
||||
])
|
||||
->assertForbidden();
|
||||
|
||||
expect(ApplicationPreview::where('application_id', $this->application->id)->exists())->toBeFalse();
|
||||
})->with([
|
||||
'read' => [['read']],
|
||||
'write' => [['write']],
|
||||
]);
|
||||
|
||||
test('returns 404 for an application of another team', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherTeam->members()->attach($otherUser->id, ['role' => 'owner']);
|
||||
|
||||
$this->withHeaders(previewAuthHeaders(createTeamApiToken($otherUser, $otherTeam, ['*'])))
|
||||
->postJson("/api/v1/applications/{$this->application->uuid}/previews", [
|
||||
'pull_request_id' => 5,
|
||||
'git_type' => 'github',
|
||||
])
|
||||
->assertNotFound();
|
||||
|
||||
expect(ApplicationPreview::where('application_id', $this->application->id)->exists())->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/deploy with pull_request_id', function () {
|
||||
test('deploys the preview with its stored git type', function () {
|
||||
$preview = createPreview($this->application, 42);
|
||||
$preview->update(['git_type' => 'gitlab']);
|
||||
|
||||
$this->withHeaders(previewAuthHeaders($this->bearerToken))
|
||||
->postJson('/api/v1/deploy', ['uuid' => $this->application->uuid, 'pull_request_id' => 42])
|
||||
->assertOk();
|
||||
|
||||
expect($this->application->deployment_queue()->latest('id')->first()->git_type)->toBe('gitlab');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user