feat(applications): add DELETE endpoint for preview deployments by PR id

Add `DELETE /api/v1/applications/{uuid}/previews/{pull_request_id}` to
cancel active deployments, stop containers, and delete the preview
record via `CleanupPreviewDeployment`. Includes OpenAPI annotations,
input validation, and full feature test coverage.
This commit is contained in:
Andras Bacsai
2026-04-17 13:29:11 +02:00
parent 3a8f52ce16
commit bceb5f28dc
5 changed files with 313 additions and 2 deletions
@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api;
use App\Actions\Application\CleanupPreviewDeployment;
use App\Actions\Application\LoadComposeFile;
use App\Actions\Application\StopApplication;
use App\Actions\Service\StartService;
@@ -9,6 +10,7 @@ use App\Enums\BuildPackTypes;
use App\Http\Controllers\Controller;
use App\Jobs\DeleteResourceJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\EnvironmentVariable;
use App\Models\GithubApp;
use App\Models\LocalFileVolume;
@@ -1058,7 +1060,7 @@ class ApplicationsController extends Controller
$connectToDockerNetwork = $request->connect_to_docker_network;
$customNginxConfiguration = $request->custom_nginx_configuration;
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true);
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled',false);
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled', false);
if (! is_null($customNginxConfiguration)) {
if (! isBase64Encoded($customNginxConfiguration)) {
@@ -4474,4 +4476,73 @@ class ApplicationsController extends Controller
return response()->json(['message' => 'Storage deleted.']);
}
#[OA\Delete(
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.',
path: '/applications/{uuid}/previews/{pull_request_id}',
operationId: 'delete-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 to delete.',
required: true,
schema: new OA\Schema(type: 'integer')
),
],
responses: [
new OA\Response(response: 200, description: 'Preview deletion queued.', content: new OA\JsonContent(
properties: [new OA\Property(property: 'message', type: 'string')],
)),
new OA\Response(response: 401, ref: '#/components/responses/401'),
new OA\Response(response: 400, ref: '#/components/responses/400'),
new OA\Response(response: 404, ref: '#/components/responses/404'),
new OA\Response(response: 422, ref: '#/components/responses/422'),
]
)]
public function delete_preview_by_pull_request_id(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('delete', $application);
$pullRequestIdRaw = $request->route('pull_request_id');
if (! is_numeric($pullRequestIdRaw) || (int) $pullRequestIdRaw <= 0) {
return response()->json(['message' => 'Invalid pull_request_id.'], 422);
}
$pullRequestId = (int) $pullRequestIdRaw;
$preview = ApplicationPreview::where('application_id', $application->id)
->where('pull_request_id', $pullRequestId)
->first();
if (! $preview) {
return response()->json(['message' => 'Preview not found.'], 404);
}
$preview->delete();
CleanupPreviewDeployment::run($application, $pullRequestId, $preview);
return response()->json(['message' => 'Preview deletion request queued.']);
}
}