mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(api): add Gitea App endpoints for sources and applications
Add GET /gitea-apps, POST /applications/private-gitea-app, and
gitea_app_uuid in PATCH /applications/{uuid}, with OpenAPI docs and MCP tool support.
Closes #12459
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4bdf03db69
commit
3bc82eb71d
@@ -15,6 +15,7 @@ use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GiteaApp;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
@@ -36,6 +37,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Spatie\Url\Url;
|
||||
@@ -789,6 +791,218 @@ class ApplicationsController extends Controller
|
||||
return $this->create_application($request, 'private-gh-app');
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create (Private - Gitea App)',
|
||||
description: 'Create new application based on a private repository through a Gitea App.',
|
||||
path: '/applications/private-gitea-app',
|
||||
operationId: 'create-private-gitea-app-application',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Applications'],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Application object that needs to be created.',
|
||||
required: true,
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['project_uuid', 'server_uuid', 'gitea_app_uuid', 'git_repository', 'git_branch', 'build_pack'],
|
||||
properties: [
|
||||
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
|
||||
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
|
||||
'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
|
||||
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
|
||||
'gitea_app_uuid' => ['type' => 'string', 'description' => 'The Gitea App UUID. The source must be connected.'],
|
||||
'git_repository' => ['type' => 'string', 'description' => 'The repository as owner/name, or its URL on the Gitea instance.'],
|
||||
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
|
||||
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'railpack', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The application name.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The application description.'],
|
||||
'domains' => ['type' => 'string', 'description' => 'The application URLs in a comma-separated list.'],
|
||||
'noindex_domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored.'],
|
||||
'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'],
|
||||
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
|
||||
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
|
||||
'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'],
|
||||
'is_spa' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'],
|
||||
'is_auto_deploy_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'],
|
||||
'is_force_https_enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if HTTPS is forced. Defaults to true.'],
|
||||
'is_preview_deployments_enabled' => ['type' => 'boolean', 'description' => 'Enable preview deployments for pull requests.'],
|
||||
'static_image' => ['type' => 'string', 'enum' => ['nginx:alpine'], 'description' => 'The static image.'],
|
||||
'install_command' => ['type' => 'string', 'description' => 'The install command.'],
|
||||
'build_command' => ['type' => 'string', 'description' => 'The build command.'],
|
||||
'start_command' => ['type' => 'string', 'description' => 'The start command.'],
|
||||
'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'],
|
||||
'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'],
|
||||
'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'],
|
||||
'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'],
|
||||
'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'],
|
||||
'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'],
|
||||
'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'],
|
||||
'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'],
|
||||
'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'],
|
||||
'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'],
|
||||
'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'],
|
||||
'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'],
|
||||
'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
|
||||
'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
|
||||
'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
|
||||
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
|
||||
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
|
||||
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
|
||||
'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'],
|
||||
'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'],
|
||||
'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'],
|
||||
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'],
|
||||
'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'],
|
||||
'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'],
|
||||
'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'],
|
||||
'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'],
|
||||
'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'],
|
||||
'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'],
|
||||
'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github. Needs the deploy ability.'],
|
||||
'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab. Needs the deploy ability.'],
|
||||
'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket. Needs the deploy ability.'],
|
||||
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea. Needs the deploy ability.'],
|
||||
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
|
||||
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
|
||||
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
|
||||
'dockerfile_location' => ['type' => 'string', 'description' => 'The Dockerfile location in the repository'],
|
||||
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'The Docker build target (stage) in the Dockerfile.'],
|
||||
'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'],
|
||||
'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'],
|
||||
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
|
||||
'docker_compose_domains' => [
|
||||
'type' => 'array',
|
||||
'description' => 'Array of URLs to be applied to containers of a dockercompose application.',
|
||||
'items' => new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The service name as defined in docker-compose.'],
|
||||
'domain' => ['type' => 'string', 'description' => 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io")'],
|
||||
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'Per-service www/non-www redirect for this compose service.', 'enum' => ['www', 'non-www', 'both']],
|
||||
],
|
||||
),
|
||||
],
|
||||
'watch_paths' => ['type' => 'string', 'description' => 'Watch path patterns, one per line (newline-separated). Commas do not separate patterns.'],
|
||||
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
|
||||
'use_build_secrets' => ['type' => 'boolean', 'default' => false, 'description' => 'Use Docker Build Secrets for build-time environment variables.'],
|
||||
'is_git_submodules_enabled' => ['type' => 'boolean', 'description' => 'Clone Git submodules.'],
|
||||
'is_git_lfs_enabled' => ['type' => 'boolean', 'description' => 'Enable Git LFS.'],
|
||||
'is_git_shallow_clone_enabled' => ['type' => 'boolean', 'description' => 'Use a shallow Git clone.'],
|
||||
'is_auto_cancel_queued_deployments_enabled' => ['type' => 'boolean', 'description' => 'Cancel older queued webhook deployments when a newer push arrives. Default: true.'],
|
||||
'is_auto_cancel_building_deployments_enabled' => ['type' => 'boolean', 'description' => 'Also stop an older webhook deployment that is still building when a newer push arrives. A deployment that already started to replace containers always finishes. This also cancels queued webhook deployments. Default: false.'],
|
||||
'disable_build_cache' => ['type' => 'boolean', 'description' => 'Disable the build cache.'],
|
||||
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
|
||||
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
|
||||
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
|
||||
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
|
||||
'include_coolify_default_variables' => ['type' => 'boolean', 'default' => true, 'description' => 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'],
|
||||
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
|
||||
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
|
||||
'rolling_update_drain_seconds' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 300, 'description' => 'Seconds to wait after the new container is healthy before old containers stop during a rolling update. 0 turns the wait off. Default: 5.'],
|
||||
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
|
||||
'is_gzip_enabled' => ['type' => 'boolean', 'description' => 'Enable gzip compression.'],
|
||||
'is_stripprefix_enabled' => ['type' => 'boolean', 'description' => 'Enable path prefix stripping.'],
|
||||
'is_permanent_redirect_enabled' => ['type' => 'boolean', 'description' => 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'],
|
||||
'traefik_certificate_resolver' => ['type' => 'string', 'nullable' => true, 'description' => 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'],
|
||||
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'],
|
||||
'is_raw_compose_deployment_enabled' => ['type' => 'boolean', 'description' => 'Deploy the raw Docker Compose definition.'],
|
||||
'is_log_drain_enabled' => ['type' => 'boolean', 'description' => 'Enable log drain for the application.'],
|
||||
'is_gpu_enabled' => ['type' => 'boolean', 'description' => 'Enable GPU support.'],
|
||||
'gpu_driver' => ['type' => 'string', 'nullable' => true, 'description' => 'GPU driver name.'],
|
||||
'gpu_count' => ['type' => 'string', 'nullable' => true, 'description' => 'Number of GPUs to allocate.'],
|
||||
'gpu_device_ids' => ['type' => 'string', 'nullable' => true, 'description' => 'Comma-separated GPU device IDs.'],
|
||||
'gpu_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Additional GPU options.'],
|
||||
'is_consistent_container_name_enabled' => ['type' => 'boolean', 'description' => 'Use a consistent container name across deployments.'],
|
||||
'custom_internal_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'],
|
||||
'custom_container_name_prefix' => ['type' => 'string', 'nullable' => true, 'description' => 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'],
|
||||
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
|
||||
'max_restart_count' => ['type' => 'integer', 'minimum' => 0, 'description' => 'Maximum container restart count before stopping.'],
|
||||
'is_http_basic_auth_enabled' => ['type' => 'boolean', 'description' => 'HTTP Basic Authentication enabled.'],
|
||||
'http_basic_auth_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTP Basic Authentication'],
|
||||
'http_basic_auth_password' => ['type' => 'string', 'nullable' => true, 'description' => 'Password for HTTP Basic Authentication'],
|
||||
'http_basic_auth_hash_algorithm' => ['type' => 'string', 'enum' => ['bcrypt', 'argon2id'], 'default' => 'bcrypt', 'description' => 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'],
|
||||
'http_basic_auth_bcrypt_cost' => ['type' => 'integer', 'minimum' => 4, 'maximum' => 14, 'default' => 10, 'description' => 'Bcrypt cost for the HTTP Basic Authentication password.'],
|
||||
'http_basic_auth_argon2id_memory_cost' => ['type' => 'integer', 'minimum' => 8192, 'maximum' => 262144, 'default' => 65536, 'description' => 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'],
|
||||
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
|
||||
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Ignored for the dockercompose build pack, which uses docker_compose_domains. Default: true.'],
|
||||
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
|
||||
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
|
||||
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
|
||||
],
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Application created successfully.',
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 403,
|
||||
description: 'The token needs the deploy ability to set a manual webhook secret.',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 409,
|
||||
description: 'Domain conflicts detected.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Domain conflicts detected. Use force_domain_override=true to proceed.'],
|
||||
'warning' => ['type' => 'string', 'example' => 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.'],
|
||||
'conflicts' => [
|
||||
'type' => 'array',
|
||||
'items' => new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'domain' => ['type' => 'string', 'example' => 'example.com'],
|
||||
'resource_name' => ['type' => 'string', 'example' => 'My Application'],
|
||||
'resource_uuid' => ['type' => 'string', 'nullable' => true, 'example' => 'abc123-def456'],
|
||||
'resource_type' => ['type' => 'string', 'enum' => ['application', 'service', 'instance'], 'example' => 'application'],
|
||||
'message' => ['type' => 'string', 'example' => 'Domain example.com is already in use by application \'My Application\''],
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_private_gitea_app_application(Request $request)
|
||||
{
|
||||
return $this->create_application($request, 'private-gitea-app');
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create (Private - Deploy Key)',
|
||||
description: 'Create new application based on a private repository through a Deploy Key.',
|
||||
@@ -1396,7 +1610,7 @@ class ApplicationsController extends Controller
|
||||
'autogenerate_domain' => 'boolean',
|
||||
'tags' => 'array|nullable',
|
||||
'tags.*' => 'string|min:2',
|
||||
]);
|
||||
], ValidationPatterns::combinedMessages());
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
@@ -1458,7 +1672,6 @@ class ApplicationsController extends Controller
|
||||
$autogenerateDomain = $request->boolean('autogenerate_domain', true) && $request->build_pack !== 'dockercompose';
|
||||
$forceDomainOverride = $request->boolean('force_domain_override');
|
||||
$instantDeploy = $request->instant_deploy;
|
||||
$githubAppUuid = $request->github_app_uuid;
|
||||
$useBuildServer = $request->use_build_server;
|
||||
$useBuildSecrets = $request->use_build_secrets;
|
||||
$isStatic = $request->is_static;
|
||||
@@ -1472,7 +1685,7 @@ class ApplicationsController extends Controller
|
||||
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled', false);
|
||||
$applicationSettings = $this->applicationSettingsFromRequest($request);
|
||||
|
||||
$requestedBuildPack = in_array($type, ['public', 'private-gh-app', 'private-deploy-key'], true)
|
||||
$requestedBuildPack = in_array($type, ['public', 'private-gh-app', 'private-gitea-app', 'private-deploy-key'], true)
|
||||
? $request->input('build_pack')
|
||||
: $type;
|
||||
if (($applicationSettings['is_raw_compose_deployment_enabled'] ?? false) && $requestedBuildPack !== 'dockercompose') {
|
||||
@@ -1804,13 +2017,14 @@ class ApplicationsController extends Controller
|
||||
'uuid' => data_get($application, 'uuid'),
|
||||
'domains' => data_get($application, 'fqdn'),
|
||||
]))->setStatusCode(201);
|
||||
} elseif ($type === 'private-gh-app') {
|
||||
} elseif (in_array($type, ['private-gh-app', 'private-gitea-app'], true)) {
|
||||
$sourceUuidField = $type === 'private-gh-app' ? 'github_app_uuid' : 'gitea_app_uuid';
|
||||
$validationRules = [
|
||||
'git_repository' => 'string|required',
|
||||
'git_branch' => ['string', 'required', new ValidGitBranch],
|
||||
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
|
||||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|nullable',
|
||||
'github_app_uuid' => 'string|required',
|
||||
$sourceUuidField => 'string|required',
|
||||
'watch_paths' => 'string|nullable',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
'docker_compose_domains.*' => 'array:name,domain,redirect',
|
||||
@@ -1851,39 +2065,13 @@ class ApplicationsController extends Controller
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$githubApp = GithubApp::where('uuid', $githubAppUuid)
|
||||
->where(function ($query) use ($teamId) {
|
||||
$query->where('team_id', $teamId)
|
||||
->orWhere('is_system_wide', true);
|
||||
})
|
||||
->first();
|
||||
if (! $githubApp) {
|
||||
return response()->json(['message' => 'Github App not found.'], 404);
|
||||
$sourceRepository = $type === 'private-gh-app'
|
||||
? $this->githubAppRepository($request->github_app_uuid, $request->git_repository, $teamId)
|
||||
: $this->giteaAppRepository($this->teamSourceByUuid(GiteaApp::class, $request->gitea_app_uuid, $teamId), $request->git_repository);
|
||||
if ($sourceRepository instanceof JsonResponse) {
|
||||
return $sourceRepository;
|
||||
}
|
||||
$token = generateGithubInstallationToken($githubApp);
|
||||
if (! $token) {
|
||||
return response()->json(['message' => 'Failed to generate Github App token.'], 400);
|
||||
}
|
||||
|
||||
$gitRepository = gitRepositorySlug($request->git_repository);
|
||||
|
||||
// Use direct API call to verify repository access instead of loading all repositories
|
||||
// This is much faster and avoids timeouts for GitHub Apps with many repositories
|
||||
$response = Http::GitHub($githubApp->api_url, $token)
|
||||
->timeout(20)
|
||||
->retry(3, 200, throw: false)
|
||||
->get("/repos/{$gitRepository}");
|
||||
|
||||
if ($response->status() === 404 || $response->status() === 403) {
|
||||
return response()->json(['message' => 'Repository not found or not accessible by the GitHub App.'], 404);
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
return response()->json(['message' => 'Failed to verify repository access: '.($response->json()['message'] ?? 'Unknown error')], 400);
|
||||
}
|
||||
|
||||
$gitRepositoryFound = $response->json();
|
||||
$repository_project_id = data_get($gitRepositoryFound, 'id');
|
||||
['source' => $source, 'repository' => $gitRepository, 'repository_project_id' => $repository_project_id] = $sourceRepository;
|
||||
|
||||
$application = new Application;
|
||||
removeUnnecessaryFieldsFromRequest($request);
|
||||
@@ -1970,8 +2158,8 @@ class ApplicationsController extends Controller
|
||||
$application->destination_id = $destination->id;
|
||||
$application->destination_type = $destination->getMorphClass();
|
||||
$application->environment_id = $environment->id;
|
||||
$application->source_type = $githubApp->getMorphClass();
|
||||
$application->source_id = $githubApp->id;
|
||||
$application->source_type = $source->getMorphClass();
|
||||
$application->source_id = $source->id;
|
||||
$application->repository_project_id = $repository_project_id;
|
||||
|
||||
if ($imageNameValidation = $this->sharedImageNameValidationResponse($application)) {
|
||||
@@ -3230,6 +3418,7 @@ class ApplicationsController extends Controller
|
||||
type: 'object',
|
||||
properties: [
|
||||
'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'],
|
||||
'gitea_app_uuid' => ['type' => 'string', 'description' => 'The Gitea App UUID. The source must be connected. Do not send it together with github_app_uuid.'],
|
||||
'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
|
||||
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
|
||||
'git_https_username' => ['type' => 'string', 'nullable' => true, 'description' => 'Username for HTTPS access to a private repository on a custom Git host. Send null to remove it.'],
|
||||
@@ -3471,6 +3660,7 @@ class ApplicationsController extends Controller
|
||||
];
|
||||
$validationRules = array_merge(sharedDataApplications($application), $validationRules);
|
||||
$validationMessages = [
|
||||
...ValidationPatterns::combinedMessages(),
|
||||
'docker_compose_domains.*.array' => 'An item in the docker_compose_domains array has invalid fields. Only name, domain, and redirect fields are supported.',
|
||||
];
|
||||
$validator = Validator::make($request->all(), $validationRules, $validationMessages);
|
||||
@@ -3551,39 +3741,36 @@ class ApplicationsController extends Controller
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
$githubApp = null;
|
||||
if ($request->has('github_app_uuid')) {
|
||||
$newSource = null;
|
||||
if ($request->has('github_app_uuid') && $request->has('gitea_app_uuid')) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['gitea_app_uuid' => 'Send only one of github_app_uuid or gitea_app_uuid.'],
|
||||
], 422);
|
||||
}
|
||||
foreach (['github_app_uuid' => GithubApp::class, 'gitea_app_uuid' => GiteaApp::class] as $sourceUuidField => $sourceClass) {
|
||||
if (! $request->has($sourceUuidField)) {
|
||||
continue;
|
||||
}
|
||||
if ($application->build_pack === 'dockerimage' || filled($application->dockerfile) || $application->deploymentType() === 'deploy_key') {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'github_app_uuid' => 'The github_app_uuid can only be changed for applications deployed from a GitHub App or public repository.',
|
||||
$sourceUuidField => "The {$sourceUuidField} can only be changed for applications deployed from a Git source or public repository.",
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$githubApp = GithubApp::where('uuid', $request->github_app_uuid)
|
||||
->where(fn ($query) => $query->where('team_id', $teamId)->orWhere('is_system_wide', true))
|
||||
->first();
|
||||
if (! $githubApp?->isConnected()) {
|
||||
return response()->json(['message' => 'Github App not found.'], 404);
|
||||
$newSource = $this->teamSourceByUuid($sourceClass, $request->input($sourceUuidField), $teamId);
|
||||
if (! $newSource?->isConnected()) {
|
||||
return response()->json(['message' => $sourceClass === GithubApp::class ? 'Github App not found.' : 'Gitea App not found.'], 404);
|
||||
}
|
||||
}
|
||||
|
||||
$githubApp = null;
|
||||
if ($request->has('github_app_uuid')) {
|
||||
if ($application->build_pack === 'dockerimage' || filled($application->dockerfile) || $application->deploymentType() === 'deploy_key') {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'github_app_uuid' => 'The github_app_uuid can only be changed for applications deployed from a GitHub App or public repository.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$githubApp = GithubApp::where('uuid', $request->github_app_uuid)
|
||||
->where(fn ($query) => $query->where('team_id', $teamId)->orWhere('is_system_wide', true))
|
||||
->first();
|
||||
if (! $githubApp?->isConnected()) {
|
||||
return response()->json(['message' => 'Github App not found.'], 404);
|
||||
$giteaRepository = null;
|
||||
if ($newSource instanceof GiteaApp) {
|
||||
// Gitea clones and webhooks need owner/name and the repository ID of the new source.
|
||||
$giteaRepository = $this->giteaAppRepository($newSource, (string) $request->input('git_repository', $application->git_repository));
|
||||
if ($giteaRepository instanceof JsonResponse) {
|
||||
return $giteaRepository;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3871,12 +4058,16 @@ class ApplicationsController extends Controller
|
||||
$application->git_https_username = null;
|
||||
}
|
||||
}
|
||||
if ($githubApp && ($application->source_id !== $githubApp->id || $application->source_type !== $githubApp->getMorphClass())) {
|
||||
$application->source_id = $githubApp->id;
|
||||
$application->source_type = $githubApp->getMorphClass();
|
||||
// The next deployment fetches the repository ID from the new source.
|
||||
if ($newSource && ($application->source_id !== $newSource->id || $application->source_type !== $newSource->getMorphClass())) {
|
||||
$application->source_id = $newSource->id;
|
||||
$application->source_type = $newSource->getMorphClass();
|
||||
// The next deployment fetches the repository ID from a GitHub App.
|
||||
$application->repository_project_id = null;
|
||||
}
|
||||
if ($giteaRepository) {
|
||||
$application->git_repository = $giteaRepository['repository'];
|
||||
$application->repository_project_id = $giteaRepository['repository_project_id'];
|
||||
}
|
||||
if ($requestHasNoindexDomains) {
|
||||
// Must run after fqdn is filled: flags are kept only for domains the app still has.
|
||||
$application->setNoindexDomains($request->input('noindex_domains') ?? []);
|
||||
@@ -5092,6 +5283,120 @@ class ApplicationsController extends Controller
|
||||
return migrateResourceToDestination($request, $application, 'Application', $teamId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a Git source of the team, or a system-wide one, by UUID.
|
||||
*
|
||||
* @template TSource of GithubApp|GiteaApp
|
||||
*
|
||||
* @param class-string<TSource> $sourceClass
|
||||
* @return TSource|null
|
||||
*/
|
||||
private function teamSourceByUuid(string $sourceClass, string $uuid, int|string $teamId): GithubApp|GiteaApp|null
|
||||
{
|
||||
return $sourceClass::where('uuid', $uuid)
|
||||
->where(fn ($query) => $query->where('team_id', $teamId)->orWhere('is_system_wide', true))
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a GitHub App of the team and check that it can read the repository.
|
||||
*
|
||||
* @return array{source: GithubApp, repository: string, repository_project_id: mixed}|JsonResponse
|
||||
*/
|
||||
private function githubAppRepository(string $githubAppUuid, string $gitRepository, int|string $teamId): array|JsonResponse
|
||||
{
|
||||
$githubApp = $this->teamSourceByUuid(GithubApp::class, $githubAppUuid, $teamId);
|
||||
if (! $githubApp) {
|
||||
return response()->json(['message' => 'Github App not found.'], 404);
|
||||
}
|
||||
$token = generateGithubInstallationToken($githubApp);
|
||||
if (! $token) {
|
||||
return response()->json(['message' => 'Failed to generate Github App token.'], 400);
|
||||
}
|
||||
|
||||
$gitRepository = gitRepositorySlug($gitRepository);
|
||||
|
||||
// Use direct API call to verify repository access instead of loading all repositories
|
||||
// This is much faster and avoids timeouts for GitHub Apps with many repositories
|
||||
$response = Http::GitHub($githubApp->api_url, $token)
|
||||
->timeout(20)
|
||||
->retry(3, 200, throw: false)
|
||||
->get("/repos/{$gitRepository}");
|
||||
|
||||
if ($response->status() === 404 || $response->status() === 403) {
|
||||
return response()->json(['message' => 'Repository not found or not accessible by the GitHub App.'], 404);
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
return response()->json(['message' => 'Failed to verify repository access: '.($response->json()['message'] ?? 'Unknown error')], 400);
|
||||
}
|
||||
|
||||
return ['source' => $githubApp, 'repository' => $gitRepository, 'repository_project_id' => data_get($response->json(), 'id')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a connected Gitea App can read the repository.
|
||||
*
|
||||
* @return array{source: GiteaApp, repository: string, repository_project_id: mixed}|JsonResponse
|
||||
*/
|
||||
private function giteaAppRepository(?GiteaApp $giteaApp, string $gitRepository): array|JsonResponse
|
||||
{
|
||||
if (! $giteaApp?->isConnected()) {
|
||||
return response()->json(['message' => 'Gitea App not found.'], 404);
|
||||
}
|
||||
|
||||
$repositoryPath = $this->giteaRepositoryPath($giteaApp, $gitRepository);
|
||||
if ($repositoryPath === null) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['git_repository' => 'The git_repository must be owner/name or a repository URL on the Gitea instance of the source.'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
refreshGiteaToken($giteaApp);
|
||||
} catch (\RuntimeException $e) {
|
||||
return response()->json(['message' => $e->getMessage()], 400);
|
||||
}
|
||||
|
||||
$response = Http::Gitea($giteaApp->apiUrlBase(), $giteaApp->access_token)
|
||||
->timeout(20)
|
||||
->retry(3, 200, throw: false)
|
||||
->get('/repos/'.collect(explode('/', $repositoryPath))->map(fn (string $part) => rawurlencode($part))->implode('/'));
|
||||
|
||||
if ($response->status() === 404 || $response->status() === 403
|
||||
|| ($response->successful() && ! $giteaApp->allowsRepositoryOwner(data_get($response->json(), 'owner.login')))) {
|
||||
return response()->json(['message' => 'Repository not found or not accessible by the Gitea App.'], 404);
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
return response()->json(['message' => 'Failed to verify repository access: '.data_get($response->json(), 'message', 'Unknown error')], 400);
|
||||
}
|
||||
|
||||
return ['source' => $giteaApp, 'repository' => $repositoryPath, 'repository_project_id' => data_get($response->json(), 'id')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get owner/name from owner/name, or from an HTTPS or SSH URL on the Gitea instance of the source.
|
||||
*/
|
||||
private function giteaRepositoryPath(GiteaApp $giteaApp, string $gitRepository): ?string
|
||||
{
|
||||
$repository = sshGitUrlToHttps($gitRepository) ?? $gitRepository;
|
||||
if (str_contains($repository, '://')) {
|
||||
if (strtolower((string) parse_url($repository, PHP_URL_HOST)) !== strtolower((string) parse_url($giteaApp->html_url, PHP_URL_HOST))) {
|
||||
return null;
|
||||
}
|
||||
$repository = Str::chopStart((string) parse_url($repository, PHP_URL_PATH), rtrim((string) parse_url($giteaApp->html_url, PHP_URL_PATH), '/'));
|
||||
}
|
||||
$repository = str($repository)->trim('/')->replaceEnd('.git', '')->toString();
|
||||
|
||||
if (! preg_match('#^[\w.-]+/[\w.-]+$#', $repository) || array_intersect(explode('/', $repository), ['.', '..']) !== []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $repository;
|
||||
}
|
||||
|
||||
private function validateDataApplications(Request $request, Server $server)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
@@ -6105,9 +6410,9 @@ class ApplicationsController extends Controller
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'destination_uuid' => 'required|string',
|
||||
'name' => 'string|max:255|nullable',
|
||||
'name' => ValidationPatterns::nameRules(required: false),
|
||||
'clone_volumes' => 'boolean',
|
||||
]);
|
||||
], ValidationPatterns::combinedMessages());
|
||||
$allowedFields = ['destination_uuid', 'name', 'clone_volumes'];
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GiteaApp;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class GiteaController extends Controller
|
||||
{
|
||||
private function removeSensitiveData(GiteaApp $giteaApp, int $teamId): Collection
|
||||
{
|
||||
$sensitiveFields = ['client_secret', 'access_token', 'refresh_token', 'webhook_secret'];
|
||||
|
||||
if (request()->attributes->get('can_read_sensitive', false) === true && $giteaApp->team_id === $teamId) {
|
||||
$giteaApp->makeVisible($sensitiveFields);
|
||||
} else {
|
||||
$giteaApp->makeHidden($sensitiveFields);
|
||||
}
|
||||
|
||||
return serializeApiResponse($giteaApp);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List',
|
||||
description: 'List all Gitea apps for the current team (and system-wide sources).',
|
||||
path: '/gitea-apps',
|
||||
operationId: 'list-gitea-apps',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Gitea Apps'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'List of Gitea apps.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer'],
|
||||
'uuid' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'organization' => ['type' => 'string', 'nullable' => true],
|
||||
'api_url' => ['type' => 'string'],
|
||||
'html_url' => ['type' => 'string'],
|
||||
'custom_user' => ['type' => 'string'],
|
||||
'custom_port' => ['type' => 'integer'],
|
||||
'client_id' => ['type' => 'string', 'nullable' => true],
|
||||
'redirect_uri' => ['type' => 'string', 'nullable' => true],
|
||||
'is_system_wide' => ['type' => 'boolean'],
|
||||
'is_public' => ['type' => 'boolean'],
|
||||
'is_connected' => ['type' => 'boolean', 'description' => 'True when the OAuth connection is complete and the source can deploy private repositories.'],
|
||||
'team_id' => ['type' => 'integer'],
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
ref: '#/components/responses/400',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function list_gitea_apps(Request $request): JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$giteaApps = GiteaApp::where(function ($query) use ($teamId) {
|
||||
$query->where('team_id', $teamId)
|
||||
->orWhere('is_system_wide', true);
|
||||
})->get();
|
||||
|
||||
return response()->json($giteaApps->map(
|
||||
fn (GiteaApp $giteaApp) => $this->removeSensitiveData($giteaApp, $teamId)->put('is_connected', $giteaApp->isConnected())
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,14 @@ class CreateApplication extends RestActionTool
|
||||
{
|
||||
protected string $name = 'create_application';
|
||||
|
||||
protected string $description = 'Create an application in a project of the authenticated team, from a public Git repository, a private repository (GitHub App or deploy key), a Dockerfile, or a Docker image. Requires write ability (instant_deploy and manual webhook secrets also need deploy). Same validation and permissions as the REST API.';
|
||||
protected string $description = 'Create an application in a project of the authenticated team, from a public Git repository, a private repository (GitHub App, Gitea App, or deploy key), a Dockerfile, or a Docker image. Requires write ability (instant_deploy and manual webhook secrets also need deploy). Same validation and permissions as the REST API.';
|
||||
|
||||
protected function actions(): array
|
||||
{
|
||||
return [
|
||||
'public' => ['method' => 'POST', 'path' => 'applications/public', 'ability' => 'write', 'description' => 'create from a public Git repository (data: git_repository, git_branch, build_pack, ports_exposes)'],
|
||||
'github_app' => ['method' => 'POST', 'path' => 'applications/private-github-app', 'ability' => 'write', 'description' => 'create from a private repository through a GitHub App (data: github_app_uuid, git_repository, git_branch, build_pack, ports_exposes)'],
|
||||
'gitea_app' => ['method' => 'POST', 'path' => 'applications/private-gitea-app', 'ability' => 'write', 'description' => 'create from a private repository through a connected Gitea App (data: gitea_app_uuid, git_repository as owner/name, git_branch, build_pack, ports_exposes)'],
|
||||
'deploy_key' => ['method' => 'POST', 'path' => 'applications/private-deploy-key', 'ability' => 'write', 'description' => 'create from a private repository with a deploy key (data: private_key_uuid, git_repository, git_branch, build_pack, ports_exposes)'],
|
||||
'dockerfile' => ['method' => 'POST', 'path' => 'applications/dockerfile', 'ability' => 'write', 'description' => 'create from a base64 encoded Dockerfile (data: dockerfile, ports_exposes)'],
|
||||
'docker_image' => ['method' => 'POST', 'path' => 'applications/dockerimage', 'ability' => 'write', 'description' => 'create from a Docker image (data: docker_registry_image_name, docker_registry_image_tag, ports_exposes)'],
|
||||
@@ -32,7 +33,7 @@ class CreateApplication extends RestActionTool
|
||||
protected function dataDescription(): string
|
||||
{
|
||||
return 'Request body, same fields as the REST API. Always: project_uuid, environment_name or environment_uuid, server_uuid, destination_uuid (required when the server has several destinations). '
|
||||
.'public / github_app / deploy_key: git_repository, git_branch, build_pack (nixpacks | railpack | static | dockerfile | dockercompose), ports_exposes (not for dockercompose); github_app also needs github_app_uuid, deploy_key needs private_key_uuid. '
|
||||
.'public / github_app / gitea_app / deploy_key: git_repository, git_branch, build_pack (nixpacks | railpack | static | dockerfile | dockercompose), ports_exposes (not for dockercompose); github_app also needs github_app_uuid, gitea_app needs gitea_app_uuid, deploy_key needs private_key_uuid. '
|
||||
.'dockerfile: dockerfile (base64). docker_image: docker_registry_image_name, docker_registry_image_tag, ports_exposes. '
|
||||
.'Optional: name, description, domains, autogenerate_domain, git_commit_sha, base_directory, publish_directory, install_command, build_command, start_command, docker_compose_location, docker_compose_domains, is_static, is_spa, health_check_*, limits_*, tags, instant_deploy (needs deploy ability).';
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ class ManageGitSource extends RestActionTool
|
||||
{
|
||||
protected string $name = 'manage_git_source';
|
||||
|
||||
protected string $description = 'List, add, update, or delete GitHub apps (resource=github) and GitLab apps (resource=gitlab) of the authenticated team. Secrets and keys are never returned. Requires read ability for list, write ability for changes. Same validation and permissions as the REST API.';
|
||||
protected string $description = 'List, add, update, or delete GitHub apps (resource=github) and GitLab apps (resource=gitlab) of the authenticated team. Gitea apps (resource=gitea) can only be listed; add and connect them in the UI. Secrets and keys are never returned. Requires read ability for list, write ability for changes. Same validation and permissions as the REST API.';
|
||||
|
||||
protected function actions(): array
|
||||
{
|
||||
return [
|
||||
'list' => ['method' => 'GET', 'paths' => ['github' => 'github-apps', 'gitlab' => 'gitlab-apps'], 'ability' => 'read', 'description' => 'git apps of the team and system-wide apps'],
|
||||
'list' => ['method' => 'GET', 'paths' => ['github' => 'github-apps', 'gitlab' => 'gitlab-apps', 'gitea' => 'gitea-apps'], 'ability' => 'read', 'description' => 'git apps of the team and system-wide apps'],
|
||||
'create' => ['method' => 'POST', 'paths' => ['github' => 'github-apps', 'gitlab' => 'gitlab-apps'], 'ability' => 'write', 'description' => 'add a git app (github data: name, html_url, api_url, app_id, installation_id, client_id, client_secret, webhook_secret, private_key_uuid, organization, custom_user, custom_port; gitlab data: name, html_url, api_url, group_name, client_id, client_secret, webhook_token, redirect_uri, custom_user, custom_port)'],
|
||||
'update' => ['method' => 'PATCH', 'paths' => ['github' => 'github-apps/{source_id}', 'gitlab' => 'gitlab-apps/{source_id}'], 'ability' => 'write', 'description' => 'update a git app of the team (data: same fields as create, all optional)'],
|
||||
'delete' => ['method' => 'DELETE', 'paths' => ['github' => 'github-apps/{source_id}', 'gitlab' => 'gitlab-apps/{source_id}'], 'ability' => 'delete', 'confirm' => true, 'description' => 'delete a git app of the team that no application uses'],
|
||||
|
||||
@@ -107,6 +107,18 @@ class GiteaApp extends BaseModel
|
||||
return ! empty($this->access_token) && ! empty($this->refresh_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* The organization field limits the source to repositories of these owners. Empty allows all owners.
|
||||
*/
|
||||
public function allowsRepositoryOwner(?string $owner): bool
|
||||
{
|
||||
$organizations = collect(explode(',', (string) $this->organization))
|
||||
->map(fn ($organization) => strtolower(trim($organization)))
|
||||
->filter();
|
||||
|
||||
return $organizations->isEmpty() || $organizations->contains(strtolower((string) $owner));
|
||||
}
|
||||
|
||||
public function apiUrlBase(): string
|
||||
{
|
||||
$apiUrl = rtrim($this->api_url, '/');
|
||||
|
||||
@@ -507,6 +507,7 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
|
||||
$request->offsetUnset('domains');
|
||||
$request->offsetUnset('instant_deploy');
|
||||
$request->offsetUnset('github_app_uuid');
|
||||
$request->offsetUnset('gitea_app_uuid');
|
||||
$request->offsetUnset('private_key_uuid');
|
||||
$request->offsetUnset('use_build_server');
|
||||
$request->offsetUnset('use_build_secrets');
|
||||
|
||||
@@ -119,14 +119,9 @@ function loadGiteaRepositories(GiteaApp $source, int $page = 1): array
|
||||
$repositories = collect($response->json());
|
||||
$hasMore = count($response->json()) === GITEA_PAGE_SIZE;
|
||||
|
||||
$organizations = collect(explode(',', (string) $source->organization))
|
||||
->map(fn ($organization) => strtolower(trim($organization)))
|
||||
->filter();
|
||||
if ($organizations->isNotEmpty()) {
|
||||
$repositories = $repositories->filter(
|
||||
fn ($repository) => $organizations->contains(strtolower((string) data_get($repository, 'owner.login')))
|
||||
);
|
||||
}
|
||||
$repositories = $repositories->filter(
|
||||
fn ($repository) => $source->allowsRepositoryOwner(data_get($repository, 'owner.login'))
|
||||
);
|
||||
|
||||
return [
|
||||
'total_count' => $repositories->count(),
|
||||
|
||||
+756
@@ -1790,6 +1790,670 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/private-gitea-app": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Applications"
|
||||
],
|
||||
"summary": "Create (Private - Gitea App)",
|
||||
"description": "Create new application based on a private repository through a Gitea App.",
|
||||
"operationId": "create-private-gitea-app-application",
|
||||
"requestBody": {
|
||||
"description": "Application object that needs to be created.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"project_uuid",
|
||||
"server_uuid",
|
||||
"gitea_app_uuid",
|
||||
"git_repository",
|
||||
"git_branch",
|
||||
"build_pack"
|
||||
],
|
||||
"properties": {
|
||||
"project_uuid": {
|
||||
"type": "string",
|
||||
"description": "The project UUID."
|
||||
},
|
||||
"server_uuid": {
|
||||
"type": "string",
|
||||
"description": "The server UUID."
|
||||
},
|
||||
"environment_name": {
|
||||
"type": "string",
|
||||
"description": "The environment name. You need to provide at least one of environment_name or environment_uuid."
|
||||
},
|
||||
"environment_uuid": {
|
||||
"type": "string",
|
||||
"description": "The environment UUID. You need to provide at least one of environment_name or environment_uuid."
|
||||
},
|
||||
"gitea_app_uuid": {
|
||||
"type": "string",
|
||||
"description": "The Gitea App UUID. The source must be connected."
|
||||
},
|
||||
"git_repository": {
|
||||
"type": "string",
|
||||
"description": "The repository as owner\/name, or its URL on the Gitea instance."
|
||||
},
|
||||
"git_branch": {
|
||||
"type": "string",
|
||||
"description": "The git branch."
|
||||
},
|
||||
"ports_exposes": {
|
||||
"type": "string",
|
||||
"description": "The ports to expose."
|
||||
},
|
||||
"destination_uuid": {
|
||||
"type": "string",
|
||||
"description": "The destination UUID."
|
||||
},
|
||||
"build_pack": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"nixpacks",
|
||||
"railpack",
|
||||
"static",
|
||||
"dockerfile",
|
||||
"dockercompose"
|
||||
],
|
||||
"description": "The build pack type."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The application name."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "The application description."
|
||||
},
|
||||
"domains": {
|
||||
"type": "string",
|
||||
"description": "The application URLs in a comma-separated list."
|
||||
},
|
||||
"noindex_domains": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored."
|
||||
},
|
||||
"git_commit_sha": {
|
||||
"type": "string",
|
||||
"description": "The git commit SHA."
|
||||
},
|
||||
"docker_registry_image_name": {
|
||||
"type": "string",
|
||||
"description": "The docker registry image name."
|
||||
},
|
||||
"docker_registry_image_tag": {
|
||||
"type": "string",
|
||||
"description": "The docker registry image tag."
|
||||
},
|
||||
"is_static": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to indicate if the application is static."
|
||||
},
|
||||
"is_spa": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true."
|
||||
},
|
||||
"is_auto_deploy_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to indicate if auto-deploy is enabled on git push. Defaults to true."
|
||||
},
|
||||
"is_force_https_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to indicate if HTTPS is forced. Defaults to true."
|
||||
},
|
||||
"is_preview_deployments_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable preview deployments for pull requests."
|
||||
},
|
||||
"static_image": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"nginx:alpine"
|
||||
],
|
||||
"description": "The static image."
|
||||
},
|
||||
"install_command": {
|
||||
"type": "string",
|
||||
"description": "The install command."
|
||||
},
|
||||
"build_command": {
|
||||
"type": "string",
|
||||
"description": "The build command."
|
||||
},
|
||||
"start_command": {
|
||||
"type": "string",
|
||||
"description": "The start command."
|
||||
},
|
||||
"ports_mappings": {
|
||||
"type": "string",
|
||||
"description": "The ports mappings."
|
||||
},
|
||||
"base_directory": {
|
||||
"type": "string",
|
||||
"description": "The base directory for all commands."
|
||||
},
|
||||
"publish_directory": {
|
||||
"type": "string",
|
||||
"description": "The publish directory."
|
||||
},
|
||||
"health_check_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Health check enabled."
|
||||
},
|
||||
"health_check_path": {
|
||||
"type": "string",
|
||||
"description": "Health check path."
|
||||
},
|
||||
"health_check_port": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Health check port."
|
||||
},
|
||||
"health_check_host": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Health check host."
|
||||
},
|
||||
"health_check_method": {
|
||||
"type": "string",
|
||||
"description": "Health check method."
|
||||
},
|
||||
"health_check_return_code": {
|
||||
"type": "integer",
|
||||
"description": "Health check return code."
|
||||
},
|
||||
"health_check_scheme": {
|
||||
"type": "string",
|
||||
"description": "Health check scheme."
|
||||
},
|
||||
"health_check_response_text": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Health check response text."
|
||||
},
|
||||
"health_check_interval": {
|
||||
"type": "integer",
|
||||
"description": "Health check interval in seconds."
|
||||
},
|
||||
"health_check_timeout": {
|
||||
"type": "integer",
|
||||
"description": "Health check timeout in seconds."
|
||||
},
|
||||
"health_check_retries": {
|
||||
"type": "integer",
|
||||
"description": "Health check retries count."
|
||||
},
|
||||
"health_check_start_period": {
|
||||
"type": "integer",
|
||||
"description": "Health check start period in seconds."
|
||||
},
|
||||
"limits_memory": {
|
||||
"type": "string",
|
||||
"description": "Memory limit."
|
||||
},
|
||||
"limits_memory_swap": {
|
||||
"type": "string",
|
||||
"description": "Memory swap limit."
|
||||
},
|
||||
"limits_memory_swappiness": {
|
||||
"type": "integer",
|
||||
"description": "Memory swappiness."
|
||||
},
|
||||
"limits_memory_reservation": {
|
||||
"type": "string",
|
||||
"description": "Memory reservation."
|
||||
},
|
||||
"limits_cpus": {
|
||||
"type": "string",
|
||||
"description": "CPU limit."
|
||||
},
|
||||
"limits_cpuset": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "CPU set."
|
||||
},
|
||||
"limits_cpu_shares": {
|
||||
"type": "integer",
|
||||
"description": "CPU shares."
|
||||
},
|
||||
"custom_labels": {
|
||||
"type": "string",
|
||||
"description": "Custom labels."
|
||||
},
|
||||
"custom_docker_run_options": {
|
||||
"type": "string",
|
||||
"description": "Custom docker run options."
|
||||
},
|
||||
"post_deployment_command": {
|
||||
"type": "string",
|
||||
"description": "Post deployment command."
|
||||
},
|
||||
"post_deployment_command_container": {
|
||||
"type": "string",
|
||||
"description": "Post deployment command container."
|
||||
},
|
||||
"pre_deployment_command": {
|
||||
"type": "string",
|
||||
"description": "Pre deployment command."
|
||||
},
|
||||
"pre_deployment_command_container": {
|
||||
"type": "string",
|
||||
"description": "Pre deployment command container."
|
||||
},
|
||||
"manual_webhook_secret_github": {
|
||||
"type": "string",
|
||||
"description": "Manual webhook secret for Github. Needs the deploy ability."
|
||||
},
|
||||
"manual_webhook_secret_gitlab": {
|
||||
"type": "string",
|
||||
"description": "Manual webhook secret for Gitlab. Needs the deploy ability."
|
||||
},
|
||||
"manual_webhook_secret_bitbucket": {
|
||||
"type": "string",
|
||||
"description": "Manual webhook secret for Bitbucket. Needs the deploy ability."
|
||||
},
|
||||
"manual_webhook_secret_gitea": {
|
||||
"type": "string",
|
||||
"description": "Manual webhook secret for Gitea. Needs the deploy ability."
|
||||
},
|
||||
"redirect": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "How to set redirect with Traefik \/ Caddy. www<->non-www.",
|
||||
"enum": [
|
||||
"www",
|
||||
"non-www",
|
||||
"both"
|
||||
]
|
||||
},
|
||||
"instant_deploy": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to indicate if the application should be deployed instantly."
|
||||
},
|
||||
"dockerfile": {
|
||||
"type": "string",
|
||||
"description": "The Dockerfile content."
|
||||
},
|
||||
"dockerfile_location": {
|
||||
"type": "string",
|
||||
"description": "The Dockerfile location in the repository"
|
||||
},
|
||||
"dockerfile_target_build": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The Docker build target (stage) in the Dockerfile."
|
||||
},
|
||||
"docker_compose_location": {
|
||||
"type": "string",
|
||||
"description": "The Docker Compose location."
|
||||
},
|
||||
"docker_compose_custom_start_command": {
|
||||
"type": "string",
|
||||
"description": "The Docker Compose custom start command."
|
||||
},
|
||||
"docker_compose_custom_build_command": {
|
||||
"type": "string",
|
||||
"description": "The Docker Compose custom build command."
|
||||
},
|
||||
"docker_compose_domains": {
|
||||
"type": "array",
|
||||
"description": "Array of URLs to be applied to containers of a dockercompose application.",
|
||||
"items": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The service name as defined in docker-compose."
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "Comma-separated list of URLs (e.g. \"https:\/\/app.coolify.io,https:\/\/app2.coolify.io\")"
|
||||
},
|
||||
"redirect": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Per-service www\/non-www redirect for this compose service.",
|
||||
"enum": [
|
||||
"www",
|
||||
"non-www",
|
||||
"both"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"watch_paths": {
|
||||
"type": "string",
|
||||
"description": "Watch path patterns, one per line (newline-separated). Commas do not separate patterns."
|
||||
},
|
||||
"use_build_server": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Use build server."
|
||||
},
|
||||
"use_build_secrets": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Use Docker Build Secrets for build-time environment variables."
|
||||
},
|
||||
"is_git_submodules_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Clone Git submodules."
|
||||
},
|
||||
"is_git_lfs_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable Git LFS."
|
||||
},
|
||||
"is_git_shallow_clone_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use a shallow Git clone."
|
||||
},
|
||||
"is_auto_cancel_queued_deployments_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Cancel older queued webhook deployments when a newer push arrives. Default: true."
|
||||
},
|
||||
"is_auto_cancel_building_deployments_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Also stop an older webhook deployment that is still building when a newer push arrives. A deployment that already started to replace containers always finishes. This also cancels queued webhook deployments. Default: false."
|
||||
},
|
||||
"disable_build_cache": {
|
||||
"type": "boolean",
|
||||
"description": "Disable the build cache."
|
||||
},
|
||||
"inject_build_args_to_dockerfile": {
|
||||
"type": "boolean",
|
||||
"description": "Inject build arguments into the Dockerfile build."
|
||||
},
|
||||
"include_source_commit_in_build": {
|
||||
"type": "boolean",
|
||||
"description": "Include the source commit in the build."
|
||||
},
|
||||
"is_env_sorting_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Sort environment variables."
|
||||
},
|
||||
"redact_all_env_values_in_logs": {
|
||||
"type": "boolean",
|
||||
"description": "Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions."
|
||||
},
|
||||
"include_coolify_default_variables": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it."
|
||||
},
|
||||
"is_pr_deployments_public_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Make pull request deployments public."
|
||||
},
|
||||
"stop_grace_period": {
|
||||
"type": "integer",
|
||||
"nullable": true,
|
||||
"minimum": 1,
|
||||
"maximum": 3600,
|
||||
"description": "Container stop grace period in seconds."
|
||||
},
|
||||
"rolling_update_drain_seconds": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 300,
|
||||
"description": "Seconds to wait after the new container is healthy before old containers stop during a rolling update. 0 turns the wait off. Default: 5."
|
||||
},
|
||||
"docker_images_to_keep": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"description": "Number of Docker images to retain."
|
||||
},
|
||||
"is_gzip_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable gzip compression."
|
||||
},
|
||||
"is_stripprefix_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable path prefix stripping."
|
||||
},
|
||||
"is_permanent_redirect_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use permanent redirects (301\/308) instead of temporary (302\/307) for the HTTP to HTTPS and www redirects. Defaults to false."
|
||||
},
|
||||
"traefik_certificate_resolver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Traefik certificate resolver for this application. Null uses the server setting, \"none\" uses no resolver."
|
||||
},
|
||||
"is_container_label_readonly_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels."
|
||||
},
|
||||
"is_raw_compose_deployment_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Deploy the raw Docker Compose definition."
|
||||
},
|
||||
"is_log_drain_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable log drain for the application."
|
||||
},
|
||||
"is_gpu_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable GPU support."
|
||||
},
|
||||
"gpu_driver": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "GPU driver name."
|
||||
},
|
||||
"gpu_count": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Number of GPUs to allocate."
|
||||
},
|
||||
"gpu_device_ids": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Comma-separated GPU device IDs."
|
||||
},
|
||||
"gpu_options": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Additional GPU options."
|
||||
},
|
||||
"is_consistent_container_name_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Use a consistent container name across deployments."
|
||||
},
|
||||
"custom_internal_name": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422."
|
||||
},
|
||||
"custom_container_name_prefix": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance."
|
||||
},
|
||||
"preview_url_template": {
|
||||
"type": "string",
|
||||
"description": "Preview URL template."
|
||||
},
|
||||
"max_restart_count": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "Maximum container restart count before stopping."
|
||||
},
|
||||
"is_http_basic_auth_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "HTTP Basic Authentication enabled."
|
||||
},
|
||||
"http_basic_auth_username": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Username for HTTP Basic Authentication"
|
||||
},
|
||||
"http_basic_auth_password": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Password for HTTP Basic Authentication"
|
||||
},
|
||||
"http_basic_auth_hash_algorithm": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"bcrypt",
|
||||
"argon2id"
|
||||
],
|
||||
"default": "bcrypt",
|
||||
"description": "Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer."
|
||||
},
|
||||
"http_basic_auth_bcrypt_cost": {
|
||||
"type": "integer",
|
||||
"minimum": 4,
|
||||
"maximum": 14,
|
||||
"default": 10,
|
||||
"description": "Bcrypt cost for the HTTP Basic Authentication password."
|
||||
},
|
||||
"http_basic_auth_argon2id_memory_cost": {
|
||||
"type": "integer",
|
||||
"minimum": 8192,
|
||||
"maximum": 262144,
|
||||
"default": 65536,
|
||||
"description": "Argon2id memory cost in KiB for the HTTP Basic Authentication password."
|
||||
},
|
||||
"http_basic_auth_argon2id_time_cost": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 12,
|
||||
"default": 4,
|
||||
"description": "Argon2id time cost (iterations) for the HTTP Basic Authentication password."
|
||||
},
|
||||
"connect_to_docker_network": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to connect the service to the predefined Docker network."
|
||||
},
|
||||
"force_domain_override": {
|
||||
"type": "boolean",
|
||||
"description": "Force domain usage even if conflicts are detected. Default is false."
|
||||
},
|
||||
"autogenerate_domain": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Ignored for the dockercompose build pack, which uses docker_compose_domains. Default: true."
|
||||
},
|
||||
"is_container_label_escape_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off."
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Tags to assign to the application."
|
||||
},
|
||||
"is_preserve_repository_enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Preserve repository during deployment."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Application created successfully.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"403": {
|
||||
"description": "The token needs the deploy ability to set a manual webhook secret."
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
},
|
||||
"409": {
|
||||
"description": "Domain conflicts detected.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Domain conflicts detected. Use force_domain_override=true to proceed."
|
||||
},
|
||||
"warning": {
|
||||
"type": "string",
|
||||
"example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior."
|
||||
},
|
||||
"conflicts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"example": "example.com"
|
||||
},
|
||||
"resource_name": {
|
||||
"type": "string",
|
||||
"example": "My Application"
|
||||
},
|
||||
"resource_uuid": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"example": "abc123-def456"
|
||||
},
|
||||
"resource_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"application",
|
||||
"service",
|
||||
"instance"
|
||||
],
|
||||
"example": "application"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Domain example.com is already in use by application 'My Application'"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/private-deploy-key": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -3706,6 +4370,10 @@
|
||||
"type": "string",
|
||||
"description": "The Github App UUID."
|
||||
},
|
||||
"gitea_app_uuid": {
|
||||
"type": "string",
|
||||
"description": "The Gitea App UUID. The source must be connected. Do not send it together with github_app_uuid."
|
||||
},
|
||||
"git_repository": {
|
||||
"type": "string",
|
||||
"description": "The git repository URL."
|
||||
@@ -12191,6 +12859,90 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/gitea-apps": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Gitea Apps"
|
||||
],
|
||||
"summary": "List",
|
||||
"description": "List all Gitea apps for the current team (and system-wide sources).",
|
||||
"operationId": "list-gitea-apps",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of Gitea apps.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"api_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"html_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"custom_user": {
|
||||
"type": "string"
|
||||
},
|
||||
"custom_port": {
|
||||
"type": "integer"
|
||||
},
|
||||
"client_id": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"redirect_uri": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"is_system_wide": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_public": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_connected": {
|
||||
"type": "boolean",
|
||||
"description": "True when the OAuth connection is complete and the source can deploy private repositories."
|
||||
},
|
||||
"team_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#\/components\/responses\/400"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/github-apps": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -28431,6 +29183,10 @@
|
||||
"name": "DNS",
|
||||
"description": "DNS"
|
||||
},
|
||||
{
|
||||
"name": "Gitea Apps",
|
||||
"description": "Gitea Apps"
|
||||
},
|
||||
{
|
||||
"name": "GitHub Apps",
|
||||
"description": "GitHub Apps"
|
||||
|
||||
+472
@@ -1164,6 +1164,448 @@ paths:
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/applications/private-gitea-app:
|
||||
post:
|
||||
tags:
|
||||
- Applications
|
||||
summary: 'Create (Private - Gitea App)'
|
||||
description: 'Create new application based on a private repository through a Gitea App.'
|
||||
operationId: create-private-gitea-app-application
|
||||
requestBody:
|
||||
description: 'Application object that needs to be created.'
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- project_uuid
|
||||
- server_uuid
|
||||
- gitea_app_uuid
|
||||
- git_repository
|
||||
- git_branch
|
||||
- build_pack
|
||||
properties:
|
||||
project_uuid:
|
||||
type: string
|
||||
description: 'The project UUID.'
|
||||
server_uuid:
|
||||
type: string
|
||||
description: 'The server UUID.'
|
||||
environment_name:
|
||||
type: string
|
||||
description: 'The environment name. You need to provide at least one of environment_name or environment_uuid.'
|
||||
environment_uuid:
|
||||
type: string
|
||||
description: 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'
|
||||
gitea_app_uuid:
|
||||
type: string
|
||||
description: 'The Gitea App UUID. The source must be connected.'
|
||||
git_repository:
|
||||
type: string
|
||||
description: 'The repository as owner/name, or its URL on the Gitea instance.'
|
||||
git_branch:
|
||||
type: string
|
||||
description: 'The git branch.'
|
||||
ports_exposes:
|
||||
type: string
|
||||
description: 'The ports to expose.'
|
||||
destination_uuid:
|
||||
type: string
|
||||
description: 'The destination UUID.'
|
||||
build_pack:
|
||||
type: string
|
||||
enum: [nixpacks, railpack, static, dockerfile, dockercompose]
|
||||
description: 'The build pack type.'
|
||||
name:
|
||||
type: string
|
||||
description: 'The application name.'
|
||||
description:
|
||||
type: string
|
||||
description: 'The application description.'
|
||||
domains:
|
||||
type: string
|
||||
description: 'The application URLs in a comma-separated list.'
|
||||
noindex_domains:
|
||||
type: array
|
||||
items: { type: string }
|
||||
description: 'The subset of the application domains served with an X-Robots-Tag: noindex, nofollow response header, keeping them out of search engines. Entries that are not among the application domains are ignored.'
|
||||
git_commit_sha:
|
||||
type: string
|
||||
description: 'The git commit SHA.'
|
||||
docker_registry_image_name:
|
||||
type: string
|
||||
description: 'The docker registry image name.'
|
||||
docker_registry_image_tag:
|
||||
type: string
|
||||
description: 'The docker registry image tag.'
|
||||
is_static:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the application is static.'
|
||||
is_spa:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the application is a single-page application (SPA). Only relevant when is_static is true.'
|
||||
is_auto_deploy_enabled:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if auto-deploy is enabled on git push. Defaults to true.'
|
||||
is_force_https_enabled:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if HTTPS is forced. Defaults to true.'
|
||||
is_preview_deployments_enabled:
|
||||
type: boolean
|
||||
description: 'Enable preview deployments for pull requests.'
|
||||
static_image:
|
||||
type: string
|
||||
enum: ['nginx:alpine']
|
||||
description: 'The static image.'
|
||||
install_command:
|
||||
type: string
|
||||
description: 'The install command.'
|
||||
build_command:
|
||||
type: string
|
||||
description: 'The build command.'
|
||||
start_command:
|
||||
type: string
|
||||
description: 'The start command.'
|
||||
ports_mappings:
|
||||
type: string
|
||||
description: 'The ports mappings.'
|
||||
base_directory:
|
||||
type: string
|
||||
description: 'The base directory for all commands.'
|
||||
publish_directory:
|
||||
type: string
|
||||
description: 'The publish directory.'
|
||||
health_check_enabled:
|
||||
type: boolean
|
||||
description: 'Health check enabled.'
|
||||
health_check_path:
|
||||
type: string
|
||||
description: 'Health check path.'
|
||||
health_check_port:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Health check port.'
|
||||
health_check_host:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Health check host.'
|
||||
health_check_method:
|
||||
type: string
|
||||
description: 'Health check method.'
|
||||
health_check_return_code:
|
||||
type: integer
|
||||
description: 'Health check return code.'
|
||||
health_check_scheme:
|
||||
type: string
|
||||
description: 'Health check scheme.'
|
||||
health_check_response_text:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Health check response text.'
|
||||
health_check_interval:
|
||||
type: integer
|
||||
description: 'Health check interval in seconds.'
|
||||
health_check_timeout:
|
||||
type: integer
|
||||
description: 'Health check timeout in seconds.'
|
||||
health_check_retries:
|
||||
type: integer
|
||||
description: 'Health check retries count.'
|
||||
health_check_start_period:
|
||||
type: integer
|
||||
description: 'Health check start period in seconds.'
|
||||
limits_memory:
|
||||
type: string
|
||||
description: 'Memory limit.'
|
||||
limits_memory_swap:
|
||||
type: string
|
||||
description: 'Memory swap limit.'
|
||||
limits_memory_swappiness:
|
||||
type: integer
|
||||
description: 'Memory swappiness.'
|
||||
limits_memory_reservation:
|
||||
type: string
|
||||
description: 'Memory reservation.'
|
||||
limits_cpus:
|
||||
type: string
|
||||
description: 'CPU limit.'
|
||||
limits_cpuset:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'CPU set.'
|
||||
limits_cpu_shares:
|
||||
type: integer
|
||||
description: 'CPU shares.'
|
||||
custom_labels:
|
||||
type: string
|
||||
description: 'Custom labels.'
|
||||
custom_docker_run_options:
|
||||
type: string
|
||||
description: 'Custom docker run options.'
|
||||
post_deployment_command:
|
||||
type: string
|
||||
description: 'Post deployment command.'
|
||||
post_deployment_command_container:
|
||||
type: string
|
||||
description: 'Post deployment command container.'
|
||||
pre_deployment_command:
|
||||
type: string
|
||||
description: 'Pre deployment command.'
|
||||
pre_deployment_command_container:
|
||||
type: string
|
||||
description: 'Pre deployment command container.'
|
||||
manual_webhook_secret_github:
|
||||
type: string
|
||||
description: 'Manual webhook secret for Github. Needs the deploy ability.'
|
||||
manual_webhook_secret_gitlab:
|
||||
type: string
|
||||
description: 'Manual webhook secret for Gitlab. Needs the deploy ability.'
|
||||
manual_webhook_secret_bitbucket:
|
||||
type: string
|
||||
description: 'Manual webhook secret for Bitbucket. Needs the deploy ability.'
|
||||
manual_webhook_secret_gitea:
|
||||
type: string
|
||||
description: 'Manual webhook secret for Gitea. Needs the deploy ability.'
|
||||
redirect:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'How to set redirect with Traefik / Caddy. www<->non-www.'
|
||||
enum: [www, non-www, both]
|
||||
instant_deploy:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the application should be deployed instantly.'
|
||||
dockerfile:
|
||||
type: string
|
||||
description: 'The Dockerfile content.'
|
||||
dockerfile_location:
|
||||
type: string
|
||||
description: 'The Dockerfile location in the repository'
|
||||
dockerfile_target_build:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The Docker build target (stage) in the Dockerfile.'
|
||||
docker_compose_location:
|
||||
type: string
|
||||
description: 'The Docker Compose location.'
|
||||
docker_compose_custom_start_command:
|
||||
type: string
|
||||
description: 'The Docker Compose custom start command.'
|
||||
docker_compose_custom_build_command:
|
||||
type: string
|
||||
description: 'The Docker Compose custom build command.'
|
||||
docker_compose_domains:
|
||||
type: array
|
||||
description: 'Array of URLs to be applied to containers of a dockercompose application.'
|
||||
items: { properties: { name: { type: string, description: 'The service name as defined in docker-compose.' }, domain: { type: string, description: 'Comma-separated list of URLs (e.g. "https://app.coolify.io,https://app2.coolify.io")' }, redirect: { type: string, nullable: true, description: 'Per-service www/non-www redirect for this compose service.', enum: [www, non-www, both] } }, type: object }
|
||||
watch_paths:
|
||||
type: string
|
||||
description: 'Watch path patterns, one per line (newline-separated). Commas do not separate patterns.'
|
||||
use_build_server:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: 'Use build server.'
|
||||
use_build_secrets:
|
||||
type: boolean
|
||||
default: false
|
||||
description: 'Use Docker Build Secrets for build-time environment variables.'
|
||||
is_git_submodules_enabled:
|
||||
type: boolean
|
||||
description: 'Clone Git submodules.'
|
||||
is_git_lfs_enabled:
|
||||
type: boolean
|
||||
description: 'Enable Git LFS.'
|
||||
is_git_shallow_clone_enabled:
|
||||
type: boolean
|
||||
description: 'Use a shallow Git clone.'
|
||||
is_auto_cancel_queued_deployments_enabled:
|
||||
type: boolean
|
||||
description: 'Cancel older queued webhook deployments when a newer push arrives. Default: true.'
|
||||
is_auto_cancel_building_deployments_enabled:
|
||||
type: boolean
|
||||
description: 'Also stop an older webhook deployment that is still building when a newer push arrives. A deployment that already started to replace containers always finishes. This also cancels queued webhook deployments. Default: false.'
|
||||
disable_build_cache:
|
||||
type: boolean
|
||||
description: 'Disable the build cache.'
|
||||
inject_build_args_to_dockerfile:
|
||||
type: boolean
|
||||
description: 'Inject build arguments into the Dockerfile build.'
|
||||
include_source_commit_in_build:
|
||||
type: boolean
|
||||
description: 'Include the source commit in the build.'
|
||||
is_env_sorting_enabled:
|
||||
type: boolean
|
||||
description: 'Sort environment variables.'
|
||||
redact_all_env_values_in_logs:
|
||||
type: boolean
|
||||
description: 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'
|
||||
include_coolify_default_variables:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Add the predefined COOLIFY_* variables (COOLIFY_URL, COOLIFY_FQDN, ...) to the environment. Requires team admin permissions. Redeploy to apply it.'
|
||||
is_pr_deployments_public_enabled:
|
||||
type: boolean
|
||||
description: 'Make pull request deployments public.'
|
||||
stop_grace_period:
|
||||
type: integer
|
||||
nullable: true
|
||||
minimum: 1
|
||||
maximum: 3600
|
||||
description: 'Container stop grace period in seconds.'
|
||||
rolling_update_drain_seconds:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 300
|
||||
description: 'Seconds to wait after the new container is healthy before old containers stop during a rolling update. 0 turns the wait off. Default: 5.'
|
||||
docker_images_to_keep:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
description: 'Number of Docker images to retain.'
|
||||
is_gzip_enabled:
|
||||
type: boolean
|
||||
description: 'Enable gzip compression.'
|
||||
is_stripprefix_enabled:
|
||||
type: boolean
|
||||
description: 'Enable path prefix stripping.'
|
||||
is_permanent_redirect_enabled:
|
||||
type: boolean
|
||||
description: 'Use permanent redirects (301/308) instead of temporary (302/307) for the HTTP to HTTPS and www redirects. Defaults to false.'
|
||||
traefik_certificate_resolver:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Traefik certificate resolver for this application. Null uses the server setting, "none" uses no resolver.'
|
||||
is_container_label_readonly_enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Let Coolify manage the container labels. True regenerates custom_labels from domains, redirect, and proxy settings. False keeps custom_labels as you set them: domains, redirect, and basic auth changes then do not change the labels.'
|
||||
is_raw_compose_deployment_enabled:
|
||||
type: boolean
|
||||
description: 'Deploy the raw Docker Compose definition.'
|
||||
is_log_drain_enabled:
|
||||
type: boolean
|
||||
description: 'Enable log drain for the application.'
|
||||
is_gpu_enabled:
|
||||
type: boolean
|
||||
description: 'Enable GPU support.'
|
||||
gpu_driver:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'GPU driver name.'
|
||||
gpu_count:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Number of GPUs to allocate.'
|
||||
gpu_device_ids:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Comma-separated GPU device IDs.'
|
||||
gpu_options:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Additional GPU options.'
|
||||
is_consistent_container_name_enabled:
|
||||
type: boolean
|
||||
description: 'Use a consistent container name across deployments.'
|
||||
custom_internal_name:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Custom internal container name. Turns is_consistent_container_name_enabled on when that field is not sent; sending it as false together with a name returns 422.'
|
||||
custom_container_name_prefix:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Prefix for generated container names (prefix-20260908T141530). Slugified and unique across the instance.'
|
||||
preview_url_template:
|
||||
type: string
|
||||
description: 'Preview URL template.'
|
||||
max_restart_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: 'Maximum container restart count before stopping.'
|
||||
is_http_basic_auth_enabled:
|
||||
type: boolean
|
||||
description: 'HTTP Basic Authentication enabled.'
|
||||
http_basic_auth_username:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Username for HTTP Basic Authentication'
|
||||
http_basic_auth_password:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Password for HTTP Basic Authentication'
|
||||
http_basic_auth_hash_algorithm:
|
||||
type: string
|
||||
enum: [bcrypt, argon2id]
|
||||
default: bcrypt
|
||||
description: 'Hash algorithm for the HTTP Basic Authentication password. Argon2id needs the Caddy proxy, version 2.11 or newer.'
|
||||
http_basic_auth_bcrypt_cost:
|
||||
type: integer
|
||||
minimum: 4
|
||||
maximum: 14
|
||||
default: 10
|
||||
description: 'Bcrypt cost for the HTTP Basic Authentication password.'
|
||||
http_basic_auth_argon2id_memory_cost:
|
||||
type: integer
|
||||
minimum: 8192
|
||||
maximum: 262144
|
||||
default: 65536
|
||||
description: 'Argon2id memory cost in KiB for the HTTP Basic Authentication password.'
|
||||
http_basic_auth_argon2id_time_cost:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 12
|
||||
default: 4
|
||||
description: 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'
|
||||
connect_to_docker_network:
|
||||
type: boolean
|
||||
description: 'The flag to connect the service to the predefined Docker network.'
|
||||
force_domain_override:
|
||||
type: boolean
|
||||
description: 'Force domain usage even if conflicts are detected. Default is false.'
|
||||
autogenerate_domain:
|
||||
type: boolean
|
||||
default: true
|
||||
description: "If true and domains is empty, auto-generate a domain using the server's wildcard domain or sslip.io fallback. Ignored for the dockercompose build pack, which uses docker_compose_domains. Default: true."
|
||||
is_container_label_escape_enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'
|
||||
tags:
|
||||
type: array
|
||||
items: { type: string }
|
||||
description: 'Tags to assign to the application.'
|
||||
is_preserve_repository_enabled:
|
||||
type: boolean
|
||||
default: false
|
||||
description: 'Preserve repository during deployment.'
|
||||
type: object
|
||||
responses:
|
||||
'201':
|
||||
description: 'Application created successfully.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
uuid: { type: string }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'403':
|
||||
description: 'The token needs the deploy ability to set a manual webhook secret.'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
'409':
|
||||
description: 'Domain conflicts detected.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string, example: 'Domain conflicts detected. Use force_domain_override=true to proceed.' }
|
||||
warning: { type: string, example: 'Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior.' }
|
||||
conflicts: { type: array, items: { properties: { domain: { type: string, example: example.com }, resource_name: { type: string, example: 'My Application' }, resource_uuid: { type: string, nullable: true, example: abc123-def456 }, resource_type: { type: string, enum: [application, service, instance], example: application }, message: { type: string, example: "Domain example.com is already in use by application 'My Application'" } }, type: object } }
|
||||
type: object
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/applications/private-deploy-key:
|
||||
post:
|
||||
tags:
|
||||
@@ -2467,6 +2909,9 @@ paths:
|
||||
github_app_uuid:
|
||||
type: string
|
||||
description: 'The Github App UUID.'
|
||||
gitea_app_uuid:
|
||||
type: string
|
||||
description: 'The Gitea App UUID. The source must be connected. Do not send it together with github_app_uuid.'
|
||||
git_repository:
|
||||
type: string
|
||||
description: 'The git repository URL.'
|
||||
@@ -7949,6 +8394,30 @@ paths:
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/gitea-apps:
|
||||
get:
|
||||
tags:
|
||||
- 'Gitea Apps'
|
||||
summary: List
|
||||
description: 'List all Gitea apps for the current team (and system-wide sources).'
|
||||
operationId: list-gitea-apps
|
||||
responses:
|
||||
'200':
|
||||
description: 'List of Gitea apps.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
properties: { id: { type: integer }, uuid: { type: string }, name: { type: string }, organization: { type: string, nullable: true }, api_url: { type: string }, html_url: { type: string }, custom_user: { type: string }, custom_port: { type: integer }, client_id: { type: string, nullable: true }, redirect_uri: { type: string, nullable: true }, is_system_wide: { type: boolean }, is_public: { type: boolean }, is_connected: { type: boolean, description: 'True when the OAuth connection is complete and the source can deploy private repositories.' }, team_id: { type: integer } }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'400':
|
||||
$ref: '#/components/responses/400'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/github-apps:
|
||||
get:
|
||||
tags:
|
||||
@@ -18235,6 +18704,9 @@ tags:
|
||||
-
|
||||
name: DNS
|
||||
description: DNS
|
||||
-
|
||||
name: 'Gitea Apps'
|
||||
description: 'Gitea Apps'
|
||||
-
|
||||
name: 'GitHub Apps'
|
||||
description: 'GitHub Apps'
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Http\Controllers\Api\DeployController;
|
||||
use App\Http\Controllers\Api\DestinationsController;
|
||||
use App\Http\Controllers\Api\DigitalOceanController;
|
||||
use App\Http\Controllers\Api\DnsController;
|
||||
use App\Http\Controllers\Api\GiteaController;
|
||||
use App\Http\Controllers\Api\GithubController;
|
||||
use App\Http\Controllers\Api\GitlabController;
|
||||
use App\Http\Controllers\Api\HetznerController;
|
||||
@@ -255,6 +256,7 @@ Route::group([
|
||||
Route::get('/applications', [ApplicationsController::class, 'applications'])->middleware(['api.ability:read']);
|
||||
Route::post('/applications/public', [ApplicationsController::class, 'create_public_application'])->middleware(['api.ability:write']);
|
||||
Route::post('/applications/private-github-app', [ApplicationsController::class, 'create_private_gh_app_application'])->middleware(['api.ability:write']);
|
||||
Route::post('/applications/private-gitea-app', [ApplicationsController::class, 'create_private_gitea_app_application'])->middleware(['api.ability:write']);
|
||||
Route::post('/applications/private-deploy-key', [ApplicationsController::class, 'create_private_deploy_key_application'])->middleware(['api.ability:write']);
|
||||
Route::post('/applications/dockerfile', [ApplicationsController::class, 'create_dockerfile_application'])->middleware(['api.ability:write']);
|
||||
Route::post('/applications/dockerimage', [ApplicationsController::class, 'create_dockerimage_application'])->middleware(['api.ability:write']);
|
||||
@@ -335,6 +337,8 @@ Route::group([
|
||||
Route::patch('/gitlab-apps/{gitlab_app_id}', [GitlabController::class, 'update_gitlab_app'])->middleware(['api.ability:write']);
|
||||
Route::delete('/gitlab-apps/{gitlab_app_id}', [GitlabController::class, 'delete_gitlab_app'])->middleware(['api.ability:delete']);
|
||||
|
||||
Route::get('/gitea-apps', [GiteaController::class, 'list_gitea_apps'])->middleware(['api.ability:read']);
|
||||
|
||||
Route::get('/databases', [DatabasesController::class, 'databases'])->middleware(['api.ability:read']);
|
||||
Route::post('/databases/postgresql', [DatabasesController::class, 'create_database_postgresql'])->middleware(['api.ability:write']);
|
||||
Route::post('/databases/mysql', [DatabasesController::class, 'create_database_mysql'])->middleware(['api.ability:write']);
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\GiteaApp;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
/**
|
||||
* Http::GitSource() pins DNS before Http::fake() answers, so use a public IP
|
||||
* literal that needs no DNS lookup. No request leaves the test.
|
||||
*/
|
||||
const GITEA_API_TEST_URL = 'https://1.1.1.1';
|
||||
|
||||
function giteaApiTestSource(Team $team, array $attributes = []): GiteaApp
|
||||
{
|
||||
return GiteaApp::create([
|
||||
'name' => 'Team Gitea',
|
||||
'api_url' => GITEA_API_TEST_URL.'/api/v1',
|
||||
'html_url' => GITEA_API_TEST_URL,
|
||||
'client_id' => 'client-id',
|
||||
'client_secret' => 'client-secret',
|
||||
'access_token' => 'gitea-access-token',
|
||||
'refresh_token' => 'gitea-refresh-token',
|
||||
'webhook_secret' => 'gitea-webhook-secret',
|
||||
'expires_at' => time() + 3600,
|
||||
'team_id' => $team->id,
|
||||
'is_system_wide' => false,
|
||||
'is_public' => false,
|
||||
...$attributes,
|
||||
]);
|
||||
}
|
||||
|
||||
function fakeGiteaRepository(string $path = 'acme/app', string $owner = 'acme', int $id = 4242): void
|
||||
{
|
||||
Http::fake([
|
||||
GITEA_API_TEST_URL."/api/v1/repos/{$path}" => Http::response(['id' => $id, 'full_name' => $path, 'owner' => ['login' => $owner]]),
|
||||
'*' => Http::response(['message' => 'Not Found'], 404),
|
||||
]);
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
Queue::fake();
|
||||
Server::flushIdentityMap();
|
||||
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
session(['currentTeam' => $this->team]);
|
||||
$this->headers = ['Authorization' => 'Bearer '.$this->user->createToken('gitea-api-test', ['*'])->plainTextToken];
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
StandaloneDocker::where('server_id', $this->server->id)->firstOrFail();
|
||||
|
||||
$this->giteaApp = giteaApiTestSource($this->team);
|
||||
});
|
||||
|
||||
function createGiteaApplicationPayload(object $test, array $overrides = []): array
|
||||
{
|
||||
return [
|
||||
'project_uuid' => $test->project->uuid,
|
||||
'environment_uuid' => $test->environment->uuid,
|
||||
'server_uuid' => $test->server->uuid,
|
||||
'gitea_app_uuid' => $test->giteaApp->uuid,
|
||||
'git_repository' => 'acme/app',
|
||||
'git_branch' => 'main',
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => '3000',
|
||||
'autogenerate_domain' => false,
|
||||
...$overrides,
|
||||
];
|
||||
}
|
||||
|
||||
describe('GET /api/v1/gitea-apps', function () {
|
||||
test('lists team and system-wide sources without secrets for read tokens', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
giteaApiTestSource($otherTeam, ['name' => 'Foreign System Gitea', 'is_system_wide' => true]);
|
||||
giteaApiTestSource($otherTeam, ['name' => 'Foreign Private Gitea']);
|
||||
giteaApiTestSource($this->team, ['name' => 'Unconnected Gitea', 'access_token' => null, 'refresh_token' => null]);
|
||||
$readToken = $this->user->createToken('read-token', ['read'])->plainTextToken;
|
||||
|
||||
$response = $this->withHeaders(['Authorization' => 'Bearer '.$readToken])
|
||||
->getJson('/api/v1/gitea-apps')
|
||||
->assertOk()
|
||||
->assertJsonCount(3);
|
||||
|
||||
$apps = collect($response->json())->keyBy('name');
|
||||
expect($apps->keys()->sort()->values()->all())->toBe(['Foreign System Gitea', 'Team Gitea', 'Unconnected Gitea'])
|
||||
->and($apps['Team Gitea']['is_connected'])->toBeTrue()
|
||||
->and($apps['Unconnected Gitea']['is_connected'])->toBeFalse();
|
||||
foreach ($apps as $app) {
|
||||
expect($app)->not->toHaveKeys(['client_secret', 'access_token', 'refresh_token', 'webhook_secret']);
|
||||
}
|
||||
});
|
||||
|
||||
test('shows secrets of own sources to root tokens only', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
giteaApiTestSource($otherTeam, ['name' => 'Foreign System Gitea', 'is_system_wide' => true]);
|
||||
|
||||
$apps = collect($this->withHeaders($this->headers)->getJson('/api/v1/gitea-apps')->assertOk()->json())->keyBy('name');
|
||||
|
||||
expect($apps['Team Gitea']['client_secret'])->toBe('client-secret')
|
||||
->and($apps['Foreign System Gitea'])->not->toHaveKey('client_secret');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/applications/private-gitea-app', function () {
|
||||
test('creates an application from a repository URL on the Gitea instance', function () {
|
||||
fakeGiteaRepository();
|
||||
|
||||
$response = $this->withHeaders($this->headers)
|
||||
->postJson('/api/v1/applications/private-gitea-app', createGiteaApplicationPayload($this, [
|
||||
'git_repository' => GITEA_API_TEST_URL.'/acme/app.git',
|
||||
]))
|
||||
->assertCreated();
|
||||
|
||||
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
|
||||
expect($application->source_type)->toBe(GiteaApp::class)
|
||||
->and($application->source_id)->toBe($this->giteaApp->id)
|
||||
->and($application->git_repository)->toBe('acme/app')
|
||||
->and($application->repository_project_id)->toBe(4242);
|
||||
Http::assertSent(fn ($request) => $request->url() === GITEA_API_TEST_URL.'/api/v1/repos/acme/app'
|
||||
&& $request->hasHeader('Authorization', 'Bearer gitea-access-token'));
|
||||
});
|
||||
|
||||
test('returns 404 for a Gitea App of another team or one that is not connected', function (string $case) {
|
||||
$source = $case === 'other team'
|
||||
? giteaApiTestSource(Team::factory()->create())
|
||||
: giteaApiTestSource($this->team, ['access_token' => null, 'refresh_token' => null]);
|
||||
fakeGiteaRepository();
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->postJson('/api/v1/applications/private-gitea-app', createGiteaApplicationPayload($this, ['gitea_app_uuid' => $source->uuid]))
|
||||
->assertNotFound()
|
||||
->assertJson(['message' => 'Gitea App not found.']);
|
||||
|
||||
expect(Application::count())->toBe(0);
|
||||
Http::assertNothingSent();
|
||||
})->with(['other team', 'not connected']);
|
||||
|
||||
test('rejects a repository of an owner outside the source organizations', function () {
|
||||
$this->giteaApp->update(['organization' => 'acme']);
|
||||
fakeGiteaRepository('intruder/app', 'intruder');
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->postJson('/api/v1/applications/private-gitea-app', createGiteaApplicationPayload($this, ['git_repository' => 'intruder/app']))
|
||||
->assertNotFound()
|
||||
->assertJson(['message' => 'Repository not found or not accessible by the Gitea App.']);
|
||||
|
||||
expect(Application::count())->toBe(0);
|
||||
});
|
||||
|
||||
test('rejects a repository that is not on the Gitea instance of the source', function (string $repository) {
|
||||
fakeGiteaRepository();
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->postJson('/api/v1/applications/private-gitea-app', createGiteaApplicationPayload($this, ['git_repository' => $repository]))
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('git_repository');
|
||||
|
||||
Http::assertNothingSent();
|
||||
})->with([
|
||||
'other host' => 'https://gitea.example.org/acme/app',
|
||||
'path traversal' => '../admin',
|
||||
'nested path' => 'acme/app/extra',
|
||||
]);
|
||||
|
||||
test('requires gitea_app_uuid', function () {
|
||||
$this->withHeaders($this->headers)
|
||||
->postJson('/api/v1/applications/private-gitea-app', createGiteaApplicationPayload($this, ['gitea_app_uuid' => null]))
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('gitea_app_uuid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /api/v1/applications/{uuid} gitea_app_uuid', function () {
|
||||
beforeEach(function () {
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => StandaloneDocker::where('server_id', $this->server->id)->firstOrFail()->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'git_repository' => GITEA_API_TEST_URL.'/acme/app',
|
||||
'repository_project_id' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
test('moves a public repository application to a Gitea App source', function () {
|
||||
fakeGiteaRepository();
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/applications/{$this->application->uuid}", ['gitea_app_uuid' => $this->giteaApp->uuid])
|
||||
->assertOk();
|
||||
|
||||
$this->application->refresh();
|
||||
expect($this->application->source_type)->toBe(GiteaApp::class)
|
||||
->and($this->application->source_id)->toBe($this->giteaApp->id)
|
||||
->and($this->application->git_repository)->toBe('acme/app')
|
||||
->and($this->application->repository_project_id)->toBe(4242);
|
||||
});
|
||||
|
||||
test('does not move the application to a Gitea App of another team', function () {
|
||||
$foreignSource = giteaApiTestSource(Team::factory()->create());
|
||||
$originalSource = [$this->application->source_type, $this->application->source_id];
|
||||
fakeGiteaRepository();
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/applications/{$this->application->uuid}", ['gitea_app_uuid' => $foreignSource->uuid])
|
||||
->assertNotFound();
|
||||
|
||||
$this->application->refresh();
|
||||
expect([$this->application->source_type, $this->application->source_id])->toBe($originalSource);
|
||||
});
|
||||
|
||||
test('rejects github_app_uuid and gitea_app_uuid together', function () {
|
||||
$this->withHeaders($this->headers)
|
||||
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||
'gitea_app_uuid' => $this->giteaApp->uuid,
|
||||
'github_app_uuid' => 'any-github-app',
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('gitea_app_uuid');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user