Merge branch 'next' into ts-template
This commit is contained in:
commit
6effccd1fb
204 changed files with 10343 additions and 2614 deletions
2
.coderabbit.yaml
Normal file
2
.coderabbit.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
reviews:
|
||||
review_status: false
|
||||
|
|
@ -267,18 +267,365 @@ For complete documentation, see **[form-components.mdc](mdc:.cursor/rules/form-c
|
|||
|
||||
## Form Handling Patterns
|
||||
|
||||
### Livewire Component Data Synchronization Pattern
|
||||
|
||||
**IMPORTANT**: All Livewire components must use the **manual `syncData()` pattern** for synchronizing component properties with Eloquent models.
|
||||
|
||||
#### Property Naming Convention
|
||||
- **Component properties**: Use camelCase (e.g., `$gitRepository`, `$isStatic`)
|
||||
- **Database columns**: Use snake_case (e.g., `git_repository`, `is_static`)
|
||||
- **View bindings**: Use camelCase matching component properties (e.g., `id="gitRepository"`)
|
||||
|
||||
#### The syncData() Method Pattern
|
||||
|
||||
```php
|
||||
use Livewire\Attributes\Validate;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
|
||||
class MyComponent extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Application $application;
|
||||
|
||||
// Properties with validation attributes
|
||||
#[Validate(['required'])]
|
||||
public string $name;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $description = null;
|
||||
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isStatic = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('view', $this->application);
|
||||
$this->syncData(); // Load from model
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync TO model (camelCase → snake_case)
|
||||
$this->application->name = $this->name;
|
||||
$this->application->description = $this->description;
|
||||
$this->application->is_static = $this->isStatic;
|
||||
|
||||
$this->application->save();
|
||||
} else {
|
||||
// Sync FROM model (snake_case → camelCase)
|
||||
$this->name = $this->application->name;
|
||||
$this->description = $this->application->description;
|
||||
$this->isStatic = $this->application->is_static;
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->authorize('update', $this->application);
|
||||
$this->syncData(toModel: true); // Save to model
|
||||
$this->dispatch('success', 'Saved successfully.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Validation with #[Validate] Attributes
|
||||
|
||||
All component properties should have `#[Validate]` attributes:
|
||||
|
||||
```php
|
||||
// Boolean properties
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isEnabled = false;
|
||||
|
||||
// Required strings
|
||||
#[Validate(['string', 'required'])]
|
||||
public string $name;
|
||||
|
||||
// Nullable strings
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $description = null;
|
||||
|
||||
// With constraints
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $timeout;
|
||||
```
|
||||
|
||||
#### Benefits of syncData() Pattern
|
||||
|
||||
- **Explicit Control**: Clear visibility of what's being synchronized
|
||||
- **Type Safety**: #[Validate] attributes provide compile-time validation info
|
||||
- **Easy Debugging**: Single method to check for data flow issues
|
||||
- **Maintainability**: All sync logic in one place
|
||||
- **Flexibility**: Can add custom logic (encoding, transformations, etc.)
|
||||
|
||||
#### Creating New Form Components with syncData()
|
||||
|
||||
#### Step-by-Step Component Creation Guide
|
||||
|
||||
**Step 1: Define properties in camelCase with #[Validate] attributes**
|
||||
```php
|
||||
use Livewire\Attributes\Validate;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class MyFormComponent extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
// The model we're syncing with
|
||||
public Application $application;
|
||||
|
||||
// Component properties in camelCase with validation
|
||||
#[Validate(['string', 'required'])]
|
||||
public string $name;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $gitRepository = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $installCommand = null;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isStatic = false;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Implement syncData() method**
|
||||
```php
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync TO model (component camelCase → database snake_case)
|
||||
$this->application->name = $this->name;
|
||||
$this->application->git_repository = $this->gitRepository;
|
||||
$this->application->install_command = $this->installCommand;
|
||||
$this->application->is_static = $this->isStatic;
|
||||
|
||||
$this->application->save();
|
||||
} else {
|
||||
// Sync FROM model (database snake_case → component camelCase)
|
||||
$this->name = $this->application->name;
|
||||
$this->gitRepository = $this->application->git_repository;
|
||||
$this->installCommand = $this->application->install_command;
|
||||
$this->isStatic = $this->application->is_static;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Implement mount() to load initial data**
|
||||
```php
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('view', $this->application);
|
||||
$this->syncData(); // Load data from model to component properties
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Implement action methods with authorization**
|
||||
```php
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->syncData(toModel: true); // Save component properties to model
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->syncData(toModel: true); // Save component properties to model
|
||||
$this->dispatch('success', 'Changes saved successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Create Blade view with camelCase bindings**
|
||||
```blade
|
||||
<div>
|
||||
<form wire:submit="submit">
|
||||
<x-forms.input
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
id="name"
|
||||
label="Name"
|
||||
required />
|
||||
|
||||
<x-forms.input
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
id="gitRepository"
|
||||
label="Git Repository" />
|
||||
|
||||
<x-forms.input
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
id="installCommand"
|
||||
label="Install Command" />
|
||||
|
||||
<x-forms.checkbox
|
||||
instantSave
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
id="isStatic"
|
||||
label="Static Site" />
|
||||
|
||||
<x-forms.button
|
||||
canGate="update"
|
||||
:canResource="$application"
|
||||
type="submit">
|
||||
Save Changes
|
||||
</x-forms.button>
|
||||
</form>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- Use `wire:model="camelCase"` and `id="camelCase"` in Blade views
|
||||
- Component properties are camelCase, database columns are snake_case
|
||||
- Always include authorization checks (`authorize()`, `canGate`, `canResource`)
|
||||
- Use `instantSave` for checkboxes that save immediately without form submission
|
||||
|
||||
#### Special Patterns
|
||||
|
||||
**Pattern 1: Related Models (e.g., Application → Settings)**
|
||||
```php
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync main model
|
||||
$this->application->name = $this->name;
|
||||
$this->application->save();
|
||||
|
||||
// Sync related model
|
||||
$this->application->settings->is_static = $this->isStatic;
|
||||
$this->application->settings->save();
|
||||
} else {
|
||||
// From main model
|
||||
$this->name = $this->application->name;
|
||||
|
||||
// From related model
|
||||
$this->isStatic = $this->application->settings->is_static;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 2: Custom Encoding/Decoding**
|
||||
```php
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Encode before saving
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
} else {
|
||||
// Decode when loading
|
||||
$this->customLabels = $this->application->parseContainerLabels();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern 3: Error Rollback**
|
||||
```php
|
||||
public function submit()
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
$original = $this->model->getOriginal();
|
||||
|
||||
try {
|
||||
$this->syncData(toModel: true);
|
||||
$this->dispatch('success', 'Saved successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
// Rollback on error
|
||||
$this->model->setRawAttributes($original);
|
||||
$this->model->save();
|
||||
$this->syncData(); // Reload from model
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Property Type Patterns
|
||||
|
||||
**Required Strings**
|
||||
```php
|
||||
#[Validate(['string', 'required'])]
|
||||
public string $name; // No ?, no default, always has value
|
||||
```
|
||||
|
||||
**Nullable Strings**
|
||||
```php
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $description = null; // ?, = null, can be empty
|
||||
```
|
||||
|
||||
**Booleans**
|
||||
```php
|
||||
#[Validate(['boolean'])]
|
||||
public bool $isEnabled = false; // Always has default value
|
||||
```
|
||||
|
||||
**Integers with Constraints**
|
||||
```php
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $timeout; // Required
|
||||
|
||||
#[Validate(['integer', 'min:1', 'nullable'])]
|
||||
public ?int $port = null; // Nullable
|
||||
```
|
||||
|
||||
#### Testing Checklist
|
||||
|
||||
After creating a new component with syncData(), verify:
|
||||
|
||||
- [ ] All checkboxes save correctly (especially `instantSave` ones)
|
||||
- [ ] All form inputs persist to database
|
||||
- [ ] Custom encoded fields (like labels) display correctly if applicable
|
||||
- [ ] Form validation works for all fields
|
||||
- [ ] No console errors in browser
|
||||
- [ ] Authorization checks work (`@can` directives and `authorize()` calls)
|
||||
- [ ] Error rollback works if exceptions occur
|
||||
- [ ] Related models save correctly if applicable (e.g., Application + ApplicationSetting)
|
||||
|
||||
#### Common Pitfalls to Avoid
|
||||
|
||||
1. **snake_case in component properties**: Always use camelCase for component properties (e.g., `$gitRepository` not `$git_repository`)
|
||||
2. **Missing #[Validate] attributes**: Every property should have validation attributes for type safety
|
||||
3. **Forgetting to call syncData()**: Must call `syncData()` in `mount()` to load initial data
|
||||
4. **Missing authorization**: Always use `authorize()` in methods and `canGate`/`canResource` in views
|
||||
5. **View binding mismatch**: Use camelCase in Blade (e.g., `id="gitRepository"` not `id="git_repository"`)
|
||||
6. **wire:model vs wire:model.live**: Use `.live` for `instantSave` checkboxes to avoid timing issues
|
||||
7. **Validation sync**: If using `rules()` method, keep it in sync with `#[Validate]` attributes
|
||||
8. **Related models**: Don't forget to save both main and related models in syncData() method
|
||||
|
||||
### Livewire Forms
|
||||
```php
|
||||
class ServerCreateForm extends Component
|
||||
{
|
||||
public $name;
|
||||
public $ip;
|
||||
|
||||
|
||||
protected $rules = [
|
||||
'name' => 'required|min:3',
|
||||
'ip' => 'required|ip',
|
||||
];
|
||||
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ on:
|
|||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
lock-threads:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -13,5 +18,5 @@ jobs:
|
|||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-inactive-days: '30'
|
||||
pr-inactive-days: '30'
|
||||
discussion-inactive-days: '30'
|
||||
pr-inactive-days: '30'
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ on:
|
|||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
manage-stale:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
15
.github/workflows/chore-pr-comments.yml
vendored
15
.github/workflows/chore-pr-comments.yml
vendored
|
|
@ -3,20 +3,13 @@ on:
|
|||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
add-comment:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
actions: none
|
||||
checks: none
|
||||
deployments: none
|
||||
issues: none
|
||||
packages: none
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: none
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ on:
|
|||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
remove-labels-and-assignees:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
79
.github/workflows/claude-code-review.yml
vendored
79
.github/workflows/claude-code-review.yml
vendored
|
|
@ -1,79 +0,0 @@
|
|||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
if: false
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
|
||||
# model: "claude-opus-4-1-20250805"
|
||||
|
||||
# Direct prompt for automated review (no @claude mention needed)
|
||||
direct_prompt: |
|
||||
Please review this pull request and provide feedback on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Performance considerations
|
||||
- Security concerns
|
||||
- Test coverage
|
||||
|
||||
Be constructive and helpful in your feedback.
|
||||
|
||||
# Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
|
||||
# use_sticky_comment: true
|
||||
|
||||
# Optional: Customize review based on file types
|
||||
# direct_prompt: |
|
||||
# Review this PR focusing on:
|
||||
# - For TypeScript files: Type safety and proper interface usage
|
||||
# - For API endpoints: Security, input validation, and error handling
|
||||
# - For React components: Performance, accessibility, and best practices
|
||||
# - For tests: Coverage, edge cases, and test quality
|
||||
|
||||
# Optional: Different prompts for different authors
|
||||
# direct_prompt: |
|
||||
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
|
||||
# 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
|
||||
# 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
|
||||
|
||||
# Optional: Add specific tools for running tests or linting
|
||||
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
|
||||
|
||||
# Optional: Skip review for certain conditions
|
||||
# if: |
|
||||
# !contains(github.event.pull_request.title, '[skip-review]') &&
|
||||
# !contains(github.event.pull_request.title, '[WIP]')
|
||||
|
||||
65
.github/workflows/claude.yml
vendored
65
.github/workflows/claude.yml
vendored
|
|
@ -1,65 +0,0 @@
|
|||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'Claude') ||
|
||||
(github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'Claude') ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
|
||||
# model: "claude-opus-4-1-20250805"
|
||||
|
||||
# Optional: Customize the trigger phrase (default: @claude)
|
||||
# trigger_phrase: "/claude"
|
||||
|
||||
# Optional: Trigger when specific user is assigned to an issue
|
||||
# assignee_trigger: "claude-bot"
|
||||
|
||||
# Optional: Allow Claude to run specific commands
|
||||
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
|
||||
|
||||
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
||||
# custom_instructions: |
|
||||
# Follow our coding standards
|
||||
# Ensure all new code has tests
|
||||
# Use TypeScript for new files
|
||||
|
||||
# Optional: Custom environment variables for Claude
|
||||
# claude_env: |
|
||||
# NODE_ENV: test
|
||||
9
.github/workflows/cleanup-ghcr-untagged.yml
vendored
9
.github/workflows/cleanup-ghcr-untagged.yml
vendored
|
|
@ -1,17 +1,14 @@
|
|||
name: Cleanup Untagged GHCR Images
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Manual trigger only
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
cleanup-all-packages:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
package: ['coolify', 'coolify-helper', 'coolify-realtime', 'coolify-testing-host']
|
||||
|
|
|
|||
89
.github/workflows/coolify-helper-next.yml
vendored
89
.github/workflows/coolify-helper-next.yml
vendored
|
|
@ -7,19 +7,31 @@ on:
|
|||
- .github/workflows/coolify-helper-next.yml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-helper"
|
||||
|
||||
jobs:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
|
|
@ -40,66 +52,27 @@ jobs:
|
|||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-helper/Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
aarch64:
|
||||
runs-on: [ self-hosted, arm64 ]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-helper/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: [ amd64, aarch64 ]
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
|
|
@ -124,14 +97,16 @@ jobs:
|
|||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
|
|
|
|||
88
.github/workflows/coolify-helper.yml
vendored
88
.github/workflows/coolify-helper.yml
vendored
|
|
@ -7,19 +7,31 @@ on:
|
|||
- .github/workflows/coolify-helper.yml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-helper"
|
||||
|
||||
jobs:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
|
|
@ -40,65 +52,25 @@ jobs:
|
|||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-helper/Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
aarch64:
|
||||
runs-on: [ self-hosted, arm64 ]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-helper/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: [ amd64, aarch64 ]
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
|
@ -124,14 +96,16 @@ jobs:
|
|||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
|
|
|
|||
81
.github/workflows/coolify-production-build.yml
vendored
81
.github/workflows/coolify-production-build.yml
vendored
|
|
@ -14,16 +14,31 @@ on:
|
|||
- templates/**
|
||||
- CHANGELOG.md
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify"
|
||||
|
||||
jobs:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
|
|
@ -44,60 +59,24 @@ jobs:
|
|||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
|
||||
|
||||
aarch64:
|
||||
runs-on: [self-hosted, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: [amd64, aarch64]
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
|
@ -123,14 +102,16 @@ jobs:
|
|||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
|
|
|
|||
89
.github/workflows/coolify-realtime-next.yml
vendored
89
.github/workflows/coolify-realtime-next.yml
vendored
|
|
@ -11,19 +11,31 @@ on:
|
|||
- docker/coolify-realtime/package-lock.json
|
||||
- docker/coolify-realtime/soketi-entrypoint.sh
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-realtime"
|
||||
|
||||
jobs:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
|
|
@ -44,67 +56,26 @@ jobs:
|
|||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-realtime/Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
aarch64:
|
||||
runs-on: [ self-hosted, arm64 ]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-realtime/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: [ amd64, aarch64 ]
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
|
@ -130,14 +101,16 @@ jobs:
|
|||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next
|
||||
|
||||
|
|
|
|||
89
.github/workflows/coolify-realtime.yml
vendored
89
.github/workflows/coolify-realtime.yml
vendored
|
|
@ -11,19 +11,31 @@ on:
|
|||
- docker/coolify-realtime/package-lock.json
|
||||
- docker/coolify-realtime/soketi-entrypoint.sh
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-realtime"
|
||||
|
||||
jobs:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
|
|
@ -44,67 +56,26 @@ jobs:
|
|||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-realtime/Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
aarch64:
|
||||
runs-on: [ self-hosted, arm64 ]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: version
|
||||
run: |
|
||||
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/coolify-realtime/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: [ amd64, aarch64 ]
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
|
@ -130,14 +101,16 @@ jobs:
|
|||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
|
|
|
|||
93
.github/workflows/coolify-staging-build.yml
vendored
93
.github/workflows/coolify-staging-build.yml
vendored
|
|
@ -17,16 +17,31 @@ on:
|
|||
- templates/**
|
||||
- CHANGELOG.md
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify"
|
||||
|
||||
jobs:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Sanitize branch name for Docker tag
|
||||
id: sanitize
|
||||
|
|
@ -35,6 +50,9 @@ jobs:
|
|||
SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g')
|
||||
echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
|
|
@ -49,65 +67,28 @@ jobs:
|
|||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build and Push Image
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}
|
||||
|
||||
aarch64:
|
||||
runs-on: [self-hosted, arm64]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Sanitize branch name for Docker tag
|
||||
id: sanitize
|
||||
run: |
|
||||
# Replace slashes and other invalid characters with dashes
|
||||
SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g')
|
||||
echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build and Push Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-${{ matrix.arch }}
|
||||
cache-from: |
|
||||
type=gha,scope=build-${{ matrix.arch }}
|
||||
type=registry,ref=${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=build-${{ matrix.arch }}
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: [amd64, aarch64]
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Sanitize branch name for Docker tag
|
||||
id: sanitize
|
||||
|
|
@ -135,13 +116,15 @@ jobs:
|
|||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
|
|
|
|||
84
.github/workflows/coolify-testing-host.yml
vendored
84
.github/workflows/coolify-testing-host.yml
vendored
|
|
@ -7,19 +7,31 @@ on:
|
|||
- .github/workflows/coolify-testing-host.yml
|
||||
- docker/testing-host/Dockerfile
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
GITHUB_REGISTRY: ghcr.io
|
||||
DOCKER_REGISTRY: docker.io
|
||||
IMAGE_NAME: "coollabsio/coolify-testing-host"
|
||||
|
||||
jobs:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
|
|
@ -35,62 +47,26 @@ jobs:
|
|||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build and Push Image
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/testing-host/Dockerfile
|
||||
platforms: linux/amd64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
aarch64:
|
||||
runs-on: [ self-hosted, arm64 ]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build and Push Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/testing-host/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }}
|
||||
labels: |
|
||||
coolify.managed=true
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: [ amd64, aarch64 ]
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
|
|
@ -111,13 +87,15 @@ jobs:
|
|||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -37,3 +37,4 @@ scripts/load-test/*
|
|||
docker/coolify-realtime/node_modules
|
||||
.DS_Store
|
||||
CHANGELOG.md
|
||||
/.workspaces
|
||||
|
|
|
|||
258
CHANGELOG.md
258
CHANGELOG.md
|
|
@ -4,80 +4,164 @@ # Changelog
|
|||
|
||||
## [unreleased]
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Update syncData method to use data_get for safer property access
|
||||
- Update version numbers to 4.0.0-beta.441 and 4.0.0-beta.442
|
||||
- Enhance menu item styles and update theme color meta tag
|
||||
- Clean up input attributes for PostgreSQL settings in general.blade.php
|
||||
- Update docker stop command to use --time instead of --timeout
|
||||
- Clean up utility classes and improve readability in Blade templates
|
||||
- Enhance styling for page width component in Blade template
|
||||
- Remove debugging output from StartPostgresql command handling
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update changelog
|
||||
|
||||
## [4.0.0-beta.436] - 2025-10-17
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Implement TrustHosts middleware to handle FQDN and IP address trust logic
|
||||
- Implement TrustHosts middleware to handle FQDN and IP address trust logic
|
||||
- Allow safe environment variable defaults in array-format volumes
|
||||
- Add signoz template
|
||||
- *(signoz)* Replace png icon by svg icon
|
||||
- *(signoz)* Remove explicit 'networks' setting
|
||||
- *(signoz)* Add predefined environment variables to configure Telemetry, SMTP and email sending for Alert Manager
|
||||
- *(signoz)* Generate URLs for `otel-collector` service
|
||||
- *(signoz)* Update documentation link
|
||||
- *(signoz)* Add healthcheck to otel-collector service
|
||||
- *(signoz)* Use latest tag instead of hardcoded versions
|
||||
- *(signoz)* Remove redundant users.xml volume from clickhouse container
|
||||
- *(signoz)* Replace clickhouse' config.xml volume with simpler configuration
|
||||
- *(signoz)* Remove deprecated parameters of signoz container
|
||||
- *(signoz)* Remove volumes from signoz.yaml
|
||||
- *(signoz)* Assume there is a single zookeeper container
|
||||
- *(signoz)* Update Clickhouse config to include all settings required by Signoz
|
||||
- *(signoz)* Update config.xml and users.xml to ensure clickhouse boots correctly
|
||||
- *(signoz)* Update otel-collector configuration to match upstream
|
||||
- *(signoz)* Fix otel-collector config for version v0.128.0
|
||||
- *(signoz)* Remove unecessary port mapping for otel-collector
|
||||
- *(signoz)* Add SIGNOZ_JWT_SECRET env var generation
|
||||
- *(signoz)* Upgrade clickhouse image to 25.5.6
|
||||
- *(signoz)* Use latest tag for signoz/zookeeper
|
||||
- *(signoz)* Update variables for SMTP configuration
|
||||
- *(signoz)* Replace deprecated `TELEMETRY_ENABLED` by `SIGNOZ_STATSREPORTER_ENABLED`
|
||||
- *(signoz)* Pin service image tags and `exclude_from_hc` flag to services excluded from health checks
|
||||
- *(templates)* Add SMTP configuration to ente-photos compose templates
|
||||
- *(templates)* Add SMTP encryption configuration to ente-photos compose templates
|
||||
## [4.0.0-beta.440] - 2025-11-04
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Use wasChanged() instead of isDirty() in updated hooks
|
||||
- Prevent command injection in git ls-remote operations
|
||||
- Handle null environment variable values in bash escaping
|
||||
- Critical privilege escalation in team invitation system
|
||||
- Add authentication context to TeamPolicyTest
|
||||
- Ensure negative cache results are stored in TrustHosts middleware
|
||||
- Use wasChanged() instead of isDirty() in updated hook
|
||||
- Prevent command injection in Docker Compose parsing - add pre-save validation
|
||||
- Use canonical parser for Windows path validation
|
||||
- Correct variable name typo in generateGitLsRemoteCommands method
|
||||
- Update version numbers to 4.0.0-beta.436 and 4.0.0-beta.437
|
||||
- Ensure authorization checks are in place for viewing and updating the application
|
||||
- Ensure authorization check is performed during component mount
|
||||
- *(signoz)* Remove example secrets to avoid triggering GitGuardian
|
||||
- *(signoz)* Remove hardcoded container names
|
||||
- *(signoz)* Remove HTTP collector FQDN in otel-collector
|
||||
- *(n8n)* Add DB_SQLITE_POOL_SIZE environment variable for configuration
|
||||
- Fix SPA toggle nginx regeneration and add confirmation modal
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update changelog
|
||||
|
||||
## [4.0.0-beta.439] - 2025-11-03
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update changelog
|
||||
|
||||
## [4.0.0-beta.438] - 2025-10-29
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Display service logos in original colors with consistent sizing
|
||||
- Add warnings for system-wide GitHub Apps
|
||||
- Show message when no resources use GitHub App
|
||||
- Add dynamic viewport-based height for compose editor
|
||||
- Add funding information for Coollabs including sponsorship plans and channels
|
||||
- Update Evolution API slogan to better reflect its capabilities
|
||||
- *(templates)* Update plane compose to v1.0.0
|
||||
- Add token validation functionality for Hetzner and DigitalOcean providers
|
||||
- Add dev_helper_version to instance settings and update related functionality
|
||||
- Add RestoreDatabase command for PostgreSQL dump restoration
|
||||
- Update ApplicationSetting model to include additional boolean casts
|
||||
- Enhance General component with additional properties and validation rules
|
||||
- Update version numbers to 4.0.0-beta.440 and 4.0.0-beta.441
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Handle redis_password in API database creation
|
||||
- Make modals scrollable on small screens
|
||||
- Resolve Livewire wire:model binding error in domains input
|
||||
- Make environment variable forms responsive
|
||||
- Make proxy logs page responsive
|
||||
- Improve proxy logs form layout for better responsive behavior
|
||||
- Prevent horizontal overflow in log text
|
||||
- Use break-all to force line wrapping in logs
|
||||
- Ensure deployment failure notifications are sent reliably
|
||||
- GitHub source creation and configuration issues
|
||||
- Make system-wide warning reactive in Create view
|
||||
- Prevent system-wide warning callout from making modal too wide
|
||||
- Constrain callout width with max-w-2xl and wrap text properly
|
||||
- Center system-wide warning callout in modal
|
||||
- Left-align callout on regular view, keep centered in modal
|
||||
- Allow callout to take full width in regular view
|
||||
- Change app_id and installation_id to integer values in createGithubAppManually method
|
||||
- Use x-cloak instead of inline style to prevent FOUC
|
||||
- Clarify warning message for allowed IPs configuration
|
||||
- Server URL generation in ServerPatchCheck notification
|
||||
- Monaco editor empty for docker compose applications
|
||||
- Update sponsor link from Darweb to Dade2 in README
|
||||
- *(database)* Prevent malformed URLs when server IP is empty
|
||||
- Optimize caching in Dockerfile and GitHub Actions workflow
|
||||
- Remove wire:ignore from modal and add wire:key to EditCompose component
|
||||
- Add wire:ignore directive to modal component for improved functionality
|
||||
- Clean up formatting and remove unnecessary key binding in stack form component
|
||||
- Add null checks and validation to OAuth bulk update method
|
||||
- *(docs)* Update documentation URL to version 2 in evolution-api.yaml
|
||||
- *(templates)* Remove volumes from Plane's compose
|
||||
- *(templates)* Add redis env to live service in Plane
|
||||
- *(templates)* Update minio image to use coollabsio fork in Plane
|
||||
- Prevent login rate limit bypass via spoofed headers
|
||||
- Correct login rate limiter key format to include IP address
|
||||
- Change SMTP port input type to number for better validation
|
||||
- Remove unnecessary step attribute from maximum storage input fields
|
||||
- Update boarding flow logic to complete onboarding when server is created
|
||||
- Convert network aliases to string for display
|
||||
- Improve custom_network_aliases handling and testing
|
||||
- Remove duplicate custom_labels from config hash calculation
|
||||
- Improve run script and enhance sticky header style
|
||||
|
||||
### 💼 Other
|
||||
|
||||
- *(deps-dev)* Bump vite from 6.3.6 to 6.4.1
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- Improve validation error handling and coding standards
|
||||
- Preserve exception chain in validation error handling
|
||||
- Harden and deduplicate validateShellSafePath
|
||||
- Replace random ID generation with Cuid2 for unique HTML IDs in form components
|
||||
- Remove deprecated next() method
|
||||
- Replace allowed IPs validation logic with regex
|
||||
- Remove redundant
|
||||
- Streamline allowed IPs validation and enhance UI warnings for API access
|
||||
- Remove staging URL logic from ServerPatchCheck constructor
|
||||
- Streamline Docker build process with matrix strategy for multi-architecture support
|
||||
- Simplify project data retrieval and enhance OAuth settings handling
|
||||
- Improve handling of custom network aliases
|
||||
- Remove unused submodules
|
||||
- Update subproject commit hashes
|
||||
- Remove SynchronizesModelData trait and implement syncData method for model synchronization
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update changelog
|
||||
- Update changelog
|
||||
- Update changelog
|
||||
- Update changelog
|
||||
- Update changelog
|
||||
- Update changelog
|
||||
- Update changelog
|
||||
- Add service & database deployment logging plan
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
- Add coverage for newline and tab rejection in volume strings
|
||||
- Add unit tests for ServerPatchCheck notification URL generation
|
||||
- Fix ServerPatchCheckNotification tests to avoid global state pollution
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(signoz)* Remove unused ports
|
||||
- *(signoz)* Bump version to 0.77.0
|
||||
- *(signoz)* Bump version to 0.78.1
|
||||
- Add category field to siyuan.yaml
|
||||
- Update siyuan category in service templates
|
||||
- Add spacing and format callout text in modal
|
||||
- Update version numbers to 4.0.0-beta.439 and 4.0.0-beta.440
|
||||
- Add .workspaces to .gitignore
|
||||
|
||||
## [4.0.0-beta.437] - 2025-10-21
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(templates)* Add sparkyfitness compose template and logo
|
||||
- *(servide)* Add siyuan template
|
||||
- Add onboarding guide link to global search no results state
|
||||
- Add category filter dropdown to service selection
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(service)* Update image version & healthcheck start period
|
||||
- Filter deprecated server types for Hetzner
|
||||
- Eliminate dark mode white screen flicker on page transitions
|
||||
|
||||
### 💼 Other
|
||||
|
||||
- Preserve clean docker_compose_raw without Coolify additions
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update changelog
|
||||
- Update changelog
|
||||
|
||||
## [4.0.0-beta.435] - 2025-10-15
|
||||
|
||||
|
|
@ -143,6 +227,35 @@ ### 🚀 Features
|
|||
- Add Hetzner affiliate link to token form
|
||||
- Update Hetzner affiliate link text and URL
|
||||
- Add CPU vendor information to server types in Hetzner integration
|
||||
- Implement TrustHosts middleware to handle FQDN and IP address trust logic
|
||||
- Implement TrustHosts middleware to handle FQDN and IP address trust logic
|
||||
- Allow safe environment variable defaults in array-format volumes
|
||||
- Add signoz template
|
||||
- *(signoz)* Replace png icon by svg icon
|
||||
- *(signoz)* Remove explicit 'networks' setting
|
||||
- *(signoz)* Add predefined environment variables to configure Telemetry, SMTP and email sending for Alert Manager
|
||||
- *(signoz)* Generate URLs for `otel-collector` service
|
||||
- *(signoz)* Update documentation link
|
||||
- *(signoz)* Add healthcheck to otel-collector service
|
||||
- *(signoz)* Use latest tag instead of hardcoded versions
|
||||
- *(signoz)* Remove redundant users.xml volume from clickhouse container
|
||||
- *(signoz)* Replace clickhouse' config.xml volume with simpler configuration
|
||||
- *(signoz)* Remove deprecated parameters of signoz container
|
||||
- *(signoz)* Remove volumes from signoz.yaml
|
||||
- *(signoz)* Assume there is a single zookeeper container
|
||||
- *(signoz)* Update Clickhouse config to include all settings required by Signoz
|
||||
- *(signoz)* Update config.xml and users.xml to ensure clickhouse boots correctly
|
||||
- *(signoz)* Update otel-collector configuration to match upstream
|
||||
- *(signoz)* Fix otel-collector config for version v0.128.0
|
||||
- *(signoz)* Remove unecessary port mapping for otel-collector
|
||||
- *(signoz)* Add SIGNOZ_JWT_SECRET env var generation
|
||||
- *(signoz)* Upgrade clickhouse image to 25.5.6
|
||||
- *(signoz)* Use latest tag for signoz/zookeeper
|
||||
- *(signoz)* Update variables for SMTP configuration
|
||||
- *(signoz)* Replace deprecated `TELEMETRY_ENABLED` by `SIGNOZ_STATSREPORTER_ENABLED`
|
||||
- *(signoz)* Pin service image tags and `exclude_from_hc` flag to services excluded from health checks
|
||||
- *(templates)* Add SMTP configuration to ente-photos compose templates
|
||||
- *(templates)* Add SMTP encryption configuration to ente-photos compose templates
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
|
@ -230,6 +343,25 @@ ### 🐛 Bug Fixes
|
|||
- Use computed imageTag variable for digest-based Docker images
|
||||
- Improve Docker image digest handling and add auto-parse feature
|
||||
- 'new image' quick action not progressing to resource selection
|
||||
- Use wasChanged() instead of isDirty() in updated hooks
|
||||
- Prevent command injection in git ls-remote operations
|
||||
- Handle null environment variable values in bash escaping
|
||||
- Critical privilege escalation in team invitation system
|
||||
- Add authentication context to TeamPolicyTest
|
||||
- Ensure negative cache results are stored in TrustHosts middleware
|
||||
- Use wasChanged() instead of isDirty() in updated hook
|
||||
- Prevent command injection in Docker Compose parsing - add pre-save validation
|
||||
- Use canonical parser for Windows path validation
|
||||
- Correct variable name typo in generateGitLsRemoteCommands method
|
||||
- Update version numbers to 4.0.0-beta.436 and 4.0.0-beta.437
|
||||
- Ensure authorization checks are in place for viewing and updating the application
|
||||
- Ensure authorization check is performed during component mount
|
||||
- *(signoz)* Remove example secrets to avoid triggering GitGuardian
|
||||
- *(signoz)* Remove hardcoded container names
|
||||
- *(signoz)* Remove HTTP collector FQDN in otel-collector
|
||||
- *(n8n)* Add DB_SQLITE_POOL_SIZE environment variable for configuration
|
||||
- *(template)* Remove default values for environment variables
|
||||
- Update metamcp image version and clean up environment variable syntax
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
|
@ -242,6 +374,8 @@ ### 💼 Other
|
|||
- Remove volumes
|
||||
- Add ray logging for Hetzner createServer API request/response
|
||||
- Escape all shell directory paths in Git deployment commands
|
||||
- Remove content from docker_compose_raw to prevent file overwrites
|
||||
- *(templates)* Metamcp app
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
|
|
@ -269,6 +403,10 @@ ### 🚜 Refactor
|
|||
- Migrate database components from legacy model binding to explicit properties
|
||||
- Volumes set back to ./pds-data:/pds
|
||||
- *(campfire)* Streamline environment variable definitions in Docker Compose file
|
||||
- Improve validation error handling and coding standards
|
||||
- Preserve exception chain in validation error handling
|
||||
- Harden and deduplicate validateShellSafePath
|
||||
- Replace random ID generation with Cuid2 for unique HTML IDs in form components
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
|
|
@ -292,12 +430,16 @@ ### 🎨 Styling
|
|||
### 🧪 Testing
|
||||
|
||||
- Improve Git ls-remote parsing tests with uppercase SHA and negative cases
|
||||
- Add coverage for newline and tab rejection in volume strings
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(versions)* Update Coolify version numbers to 4.0.0-beta.435 and 4.0.0-beta.436
|
||||
- Update package-lock.json
|
||||
- *(service)* Update convex template and image
|
||||
- *(signoz)* Remove unused ports
|
||||
- *(signoz)* Bump version to 0.77.0
|
||||
- *(signoz)* Bump version to 0.78.1
|
||||
|
||||
## [4.0.0-beta.434] - 2025-10-03
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ ### Livewire Component Structure
|
|||
- State management handled on the server
|
||||
- Use wire:model for two-way data binding
|
||||
- Dispatch events for component communication
|
||||
- **CRITICAL**: Livewire component views **MUST** have exactly ONE root element. ALL content must be contained within this single root element. Placing ANY elements (`<style>`, `<script>`, `<div>`, comments, or any other HTML) outside the root element will break Livewire's component tracking and cause `wire:click` and other directives to fail silently.
|
||||
|
||||
### Code Organization Patterns
|
||||
- **Actions Pattern**: Use Actions for complex business logic (`app/Actions/`)
|
||||
|
|
|
|||
|
|
@ -66,10 +66,9 @@ ## Big Sponsors
|
|||
* [CompAI](https://www.trycomp.ai?ref=coolify.io) - Open source compliance automation platform
|
||||
* [Convex](https://convex.link/coolify.io) - Open-source reactive database for web app developers
|
||||
* [CubePath](https://cubepath.com/?ref=coolify.io) - Dedicated Servers & Instant Deploy
|
||||
* [Darweb](https://darweb.nl/?ref=coolify.io) - Design. Develop. Deliver. Specialized in 3D CPQ Solutions
|
||||
* [Dade2](https://dade2.net/?ref=coolify.io) - IT Consulting, Cloud Solutions & System Integration
|
||||
* [Formbricks](https://formbricks.com?ref=coolify.io) - The open source feedback platform
|
||||
* [GoldenVM](https://billing.goldenvm.com?ref=coolify.io) - Premium virtual machine hosting solutions
|
||||
* [Gozunga](https://gozunga.com?ref=coolify.io) - Seriously Simple Cloud Infrastructure
|
||||
* [Hetzner](http://htznr.li/CoolifyXHetzner) - Server, cloud, hosting, and data center solutions
|
||||
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - Web hosting and VPS solutions
|
||||
* [JobsCollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - 30,000+ remote jobs for developers
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ public function handle(StandaloneClickhouse $database)
|
|||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ public function handle(StandaloneDragonfly $database)
|
|||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||
}
|
||||
$this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ public function handle(StandaloneKeydb $database)
|
|||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||
}
|
||||
$this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ public function handle(StandaloneMariadb $database)
|
|||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ public function handle(StandaloneMongodb $database)
|
|||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
if ($this->database->enable_ssl) {
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ public function handle(StandaloneMysql $database)
|
|||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ public function handle(StandalonePostgresql $database)
|
|||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
if ($this->database->enable_ssl) {
|
||||
|
|
@ -231,8 +231,6 @@ public function handle(StandalonePostgresql $database)
|
|||
}
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
||||
ray($this->commands);
|
||||
|
||||
return remote_process($this->commands, $database->destination->server, callEventOnFinish: 'DatabaseStatusChanged');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ public function handle(StandaloneRedis $database)
|
|||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||
}
|
||||
$this->commands[] = "docker stop --timeout=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker stop --time=10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo 'Database started.'";
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ private function stopContainer($database, string $containerName, int $timeout =
|
|||
{
|
||||
$server = $database->destination->server;
|
||||
instant_remote_process(command: [
|
||||
"docker stop --timeout=$timeout $containerName",
|
||||
"docker stop --time=$timeout $containerName",
|
||||
"docker rm -f $containerName",
|
||||
], server: $server, throwError: false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $
|
|||
$realtimeImageWithoutPrefix = 'coollabsio/coolify-realtime';
|
||||
$realtimeImageWithoutPrefixVersion = "coollabsio/coolify-realtime:$realtimeImageVersion";
|
||||
|
||||
$helperImageVersion = data_get($settings, 'helper_version');
|
||||
$helperImageVersion = getHelperVersion();
|
||||
$helperImage = config('constants.coolify.helper_image');
|
||||
$helperImageWithVersion = "$helperImage:$helperImageVersion";
|
||||
$helperImageWithoutPrefix = 'coollabsio/coolify-helper';
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ class InstallDocker
|
|||
{
|
||||
use AsAction;
|
||||
|
||||
private string $dockerVersion;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
$dockerVersion = config('constants.docker.minimum_required_version');
|
||||
$this->dockerVersion = config('constants.docker.minimum_required_version');
|
||||
$supported_os_type = $server->validateOS();
|
||||
if (! $supported_os_type) {
|
||||
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');
|
||||
|
|
@ -99,7 +101,19 @@ public function handle(Server $server)
|
|||
}
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Docker Engine...'",
|
||||
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$dockerVersion}",
|
||||
]);
|
||||
|
||||
if ($supported_os_type->contains('debian')) {
|
||||
$command = $command->merge([$this->getDebianDockerInstallCommand()]);
|
||||
} elseif ($supported_os_type->contains('rhel')) {
|
||||
$command = $command->merge([$this->getRhelDockerInstallCommand()]);
|
||||
} elseif ($supported_os_type->contains('sles')) {
|
||||
$command = $command->merge([$this->getSuseDockerInstallCommand()]);
|
||||
} else {
|
||||
$command = $command->merge([$this->getGenericDockerInstallCommand()]);
|
||||
}
|
||||
|
||||
$command = $command->merge([
|
||||
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
|
||||
'test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json "/etc/docker/daemon.json.original-$(date +"%Y%m%d-%H%M%S")"',
|
||||
"test ! -s /etc/docker/daemon.json && echo '{$config}' | base64 -d | tee /etc/docker/daemon.json > /dev/null",
|
||||
|
|
@ -128,4 +142,43 @@ public function handle(Server $server)
|
|||
return remote_process($command, $server);
|
||||
}
|
||||
}
|
||||
|
||||
private function getDebianDockerInstallCommand(): string
|
||||
{
|
||||
return "curl --max-time 300 --retry 3 https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl --max-time 300 --retry 3 https://get.docker.com | sh -s -- --version {$this->dockerVersion} || (".
|
||||
'install -m 0755 -d /etc/apt/keyrings && '.
|
||||
'curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && '.
|
||||
'chmod a+r /etc/apt/keyrings/docker.asc && '.
|
||||
'. /etc/os-release && '.
|
||||
'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list && '.
|
||||
'apt-get update && '.
|
||||
'apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin'.
|
||||
')';
|
||||
}
|
||||
|
||||
private function getRhelDockerInstallCommand(): string
|
||||
{
|
||||
return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion} || (".
|
||||
'dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo && '.
|
||||
'dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin && '.
|
||||
'systemctl start docker && '.
|
||||
'systemctl enable docker'.
|
||||
')';
|
||||
}
|
||||
|
||||
private function getSuseDockerInstallCommand(): string
|
||||
{
|
||||
return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion} || (".
|
||||
'zypper addrepo https://download.docker.com/linux/sles/docker-ce.repo && '.
|
||||
'zypper refresh && '.
|
||||
'zypper install -y --no-confirm docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin && '.
|
||||
'systemctl start docker && '.
|
||||
'systemctl enable docker'.
|
||||
')';
|
||||
}
|
||||
|
||||
private function getGenericDockerInstallCommand(): string
|
||||
{
|
||||
return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
class CleanupRedis extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup}';
|
||||
protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup} {--clear-locks : Clear stale WithoutOverlapping locks}';
|
||||
|
||||
protected $description = 'Cleanup Redis (Horizon jobs, metrics, overlapping queues, and related data)';
|
||||
protected $description = 'Cleanup Redis (Horizon jobs, metrics, overlapping queues, cache locks, and related data)';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
|
|
@ -56,6 +56,13 @@ public function handle()
|
|||
$deletedCount += $overlappingCleaned;
|
||||
}
|
||||
|
||||
// Clean up stale cache locks (WithoutOverlapping middleware)
|
||||
if ($this->option('clear-locks')) {
|
||||
$this->info('Cleaning up stale cache locks...');
|
||||
$locksCleaned = $this->cleanupCacheLocks($dryRun);
|
||||
$deletedCount += $locksCleaned;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info("DRY RUN: Would delete {$deletedCount} out of {$totalKeys} keys");
|
||||
} else {
|
||||
|
|
@ -273,4 +280,56 @@ private function deduplicateQueueContents($redis, $queueKey, $dryRun)
|
|||
|
||||
return $cleanedCount;
|
||||
}
|
||||
|
||||
private function cleanupCacheLocks(bool $dryRun): int
|
||||
{
|
||||
$cleanedCount = 0;
|
||||
|
||||
// Use the default Redis connection (database 0) where cache locks are stored
|
||||
$redis = Redis::connection('default');
|
||||
|
||||
// Get all keys matching WithoutOverlapping lock pattern
|
||||
$allKeys = $redis->keys('*');
|
||||
$lockKeys = [];
|
||||
|
||||
foreach ($allKeys as $key) {
|
||||
// Match cache lock keys: they contain 'laravel-queue-overlap'
|
||||
if (preg_match('/overlap/i', $key)) {
|
||||
$lockKeys[] = $key;
|
||||
}
|
||||
}
|
||||
if (empty($lockKeys)) {
|
||||
$this->info(' No cache locks found.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info(' Found '.count($lockKeys).' cache lock(s)');
|
||||
|
||||
foreach ($lockKeys as $lockKey) {
|
||||
// Check TTL to identify stale locks
|
||||
$ttl = $redis->ttl($lockKey);
|
||||
|
||||
// TTL = -1 means no expiration (stale lock!)
|
||||
// TTL = -2 means key doesn't exist
|
||||
// TTL > 0 means lock is valid and will expire
|
||||
if ($ttl === -1) {
|
||||
if ($dryRun) {
|
||||
$this->warn(" Would delete STALE lock (no expiration): {$lockKey}");
|
||||
} else {
|
||||
$redis->del($lockKey);
|
||||
$this->info(" ✓ Deleted STALE lock: {$lockKey}");
|
||||
}
|
||||
$cleanedCount++;
|
||||
} elseif ($ttl > 0) {
|
||||
$this->line(" Skipping active lock (expires in {$ttl}s): {$lockKey}");
|
||||
}
|
||||
}
|
||||
|
||||
if ($cleanedCount === 0) {
|
||||
$this->info(' No stale locks found (all locks have expiration set)');
|
||||
}
|
||||
|
||||
return $cleanedCount;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
219
app/Console/Commands/Cloud/RestoreDatabase.php
Normal file
219
app/Console/Commands/Cloud/RestoreDatabase.php
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands\Cloud;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RestoreDatabase extends Command
|
||||
{
|
||||
protected $signature = 'cloud:restore-database {file : Path to the database dump file} {--debug : Show detailed debug output}';
|
||||
|
||||
protected $description = 'Restore a PostgreSQL database from a dump file (development mode only)';
|
||||
|
||||
private bool $debug = false;
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->debug = $this->option('debug');
|
||||
|
||||
if (! $this->isDevelopment()) {
|
||||
$this->error('This command can only be run in development mode.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$filePath = $this->argument('file');
|
||||
|
||||
if (! file_exists($filePath)) {
|
||||
$this->error("File not found: {$filePath}");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (! is_readable($filePath)) {
|
||||
$this->error("File is not readable: {$filePath}");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->info('Starting database restoration...');
|
||||
|
||||
$database = config('database.connections.pgsql.database');
|
||||
$host = config('database.connections.pgsql.host');
|
||||
$port = config('database.connections.pgsql.port');
|
||||
$username = config('database.connections.pgsql.username');
|
||||
$password = config('database.connections.pgsql.password');
|
||||
|
||||
if (! $database || ! $username) {
|
||||
$this->error('Database configuration is incomplete.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info("Restoring to database: {$database}");
|
||||
|
||||
// Drop all tables
|
||||
if (! $this->dropAllTables($database, $host, $port, $username, $password)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Restore the database dump
|
||||
if (! $this->restoreDatabaseDump($filePath, $database, $host, $port, $username, $password)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info('Database restoration completed successfully!');
|
||||
|
||||
return 0;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("An error occurred: {$e->getMessage()}");
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private function dropAllTables(string $database, string $host, string $port, string $username, string $password): bool
|
||||
{
|
||||
$this->info('Dropping all tables...');
|
||||
|
||||
// SQL to drop all tables
|
||||
$dropTablesSQL = <<<'SQL'
|
||||
DO $$ DECLARE
|
||||
r RECORD;
|
||||
BEGIN
|
||||
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP
|
||||
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.tablename) || ' CASCADE';
|
||||
END LOOP;
|
||||
END $$;
|
||||
SQL;
|
||||
|
||||
// Build the psql command to drop all tables
|
||||
$command = sprintf(
|
||||
'PGPASSWORD=%s psql -h %s -p %s -U %s -d %s -c %s',
|
||||
escapeshellarg($password),
|
||||
escapeshellarg($host),
|
||||
escapeshellarg($port),
|
||||
escapeshellarg($username),
|
||||
escapeshellarg($database),
|
||||
escapeshellarg($dropTablesSQL)
|
||||
);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->line('<comment>Executing drop command:</comment>');
|
||||
$this->line($command);
|
||||
}
|
||||
|
||||
$output = shell_exec($command.' 2>&1');
|
||||
|
||||
if ($this->debug) {
|
||||
$this->line("<comment>Output:</comment> {$output}");
|
||||
}
|
||||
|
||||
$this->info('All tables dropped successfully.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function restoreDatabaseDump(string $filePath, string $database, string $host, string $port, string $username, string $password): bool
|
||||
{
|
||||
$this->info('Restoring database from dump file...');
|
||||
|
||||
// Handle gzipped files by decompressing first
|
||||
$actualFile = $filePath;
|
||||
if (str_ends_with($filePath, '.gz')) {
|
||||
$actualFile = rtrim($filePath, '.gz');
|
||||
$this->info('Decompressing gzipped dump file...');
|
||||
|
||||
$decompressCommand = sprintf(
|
||||
'gunzip -c %s > %s',
|
||||
escapeshellarg($filePath),
|
||||
escapeshellarg($actualFile)
|
||||
);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->line('<comment>Executing decompress command:</comment>');
|
||||
$this->line($decompressCommand);
|
||||
}
|
||||
|
||||
$decompressOutput = shell_exec($decompressCommand.' 2>&1');
|
||||
if ($this->debug && $decompressOutput) {
|
||||
$this->line("<comment>Decompress output:</comment> {$decompressOutput}");
|
||||
}
|
||||
}
|
||||
|
||||
// Use pg_restore for custom format dumps
|
||||
$command = sprintf(
|
||||
'PGPASSWORD=%s pg_restore -h %s -p %s -U %s -d %s -v %s',
|
||||
escapeshellarg($password),
|
||||
escapeshellarg($host),
|
||||
escapeshellarg($port),
|
||||
escapeshellarg($username),
|
||||
escapeshellarg($database),
|
||||
escapeshellarg($actualFile)
|
||||
);
|
||||
|
||||
if ($this->debug) {
|
||||
$this->line('<comment>Executing restore command:</comment>');
|
||||
$this->line($command);
|
||||
}
|
||||
|
||||
// Execute the restore command
|
||||
$process = proc_open(
|
||||
$command,
|
||||
[
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
],
|
||||
$pipes
|
||||
);
|
||||
|
||||
if (! is_resource($process)) {
|
||||
$this->error('Failed to start restoration process.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$output = stream_get_contents($pipes[1]);
|
||||
$error = stream_get_contents($pipes[2]);
|
||||
$exitCode = proc_close($process);
|
||||
|
||||
// Clean up decompressed file if we created one
|
||||
if ($actualFile !== $filePath && file_exists($actualFile)) {
|
||||
unlink($actualFile);
|
||||
}
|
||||
|
||||
if ($this->debug) {
|
||||
if ($output) {
|
||||
$this->line('<comment>Output:</comment>');
|
||||
$this->line($output);
|
||||
}
|
||||
if ($error) {
|
||||
$this->line('<comment>Error output:</comment>');
|
||||
$this->line($error);
|
||||
}
|
||||
$this->line("<comment>Exit code:</comment> {$exitCode}");
|
||||
}
|
||||
|
||||
if ($exitCode !== 0) {
|
||||
$this->error("Restoration failed with exit code: {$exitCode}");
|
||||
if ($error) {
|
||||
$this->error('Error details:');
|
||||
$this->error($error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($output && ! $this->debug) {
|
||||
$this->line($output);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function isDevelopment(): bool
|
||||
{
|
||||
return app()->environment(['local', 'development', 'dev']);
|
||||
}
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ public function handle()
|
|||
$this->cleanupUnusedNetworkFromCoolifyProxy();
|
||||
|
||||
try {
|
||||
$this->call('cleanup:redis');
|
||||
$this->call('cleanup:redis', ['--clear-locks' => true]);
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleanup:redis command: {$e->getMessage()}\n";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ class SyncBunny extends Command
|
|||
protected $description = 'Sync files to BunnyCDN';
|
||||
|
||||
/**
|
||||
* Fetch GitHub releases and sync to CDN
|
||||
* Fetch GitHub releases and sync to GitHub repository
|
||||
*/
|
||||
private function syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn)
|
||||
private function syncReleasesToGitHubRepo(): bool
|
||||
{
|
||||
$this->info('Fetching releases from GitHub...');
|
||||
try {
|
||||
|
|
@ -37,33 +37,122 @@ private function syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny
|
|||
'per_page' => 30, // Fetch more releases for better changelog
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$releases = $response->json();
|
||||
|
||||
// Save releases to a temporary file
|
||||
$releases_file = "$parent_dir/releases.json";
|
||||
file_put_contents($releases_file, json_encode($releases, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
// Upload to CDN
|
||||
Http::pool(fn (Pool $pool) => [
|
||||
$pool->storage(fileName: $releases_file)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/releases.json"),
|
||||
$pool->purge("$bunny_cdn/coolify/releases.json"),
|
||||
]);
|
||||
|
||||
// Clean up temporary file
|
||||
unlink($releases_file);
|
||||
|
||||
$this->info('releases.json uploaded & purged...');
|
||||
$this->info('Total releases synced: '.count($releases));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (! $response->successful()) {
|
||||
$this->error('Failed to fetch releases from GitHub: '.$response->status());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$releases = $response->json();
|
||||
$timestamp = time();
|
||||
$tmpDir = sys_get_temp_dir().'/coolify-cdn-'.$timestamp;
|
||||
$branchName = 'update-releases-'.$timestamp;
|
||||
|
||||
// Clone the repository
|
||||
$this->info('Cloning coolify-cdn repository...');
|
||||
exec('gh repo clone coollabsio/coolify-cdn '.escapeshellarg($tmpDir).' 2>&1', $output, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to clone repository: '.implode("\n", $output));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create feature branch
|
||||
$this->info('Creating feature branch...');
|
||||
exec('cd '.escapeshellarg($tmpDir).' && git checkout -b '.escapeshellarg($branchName).' 2>&1', $output, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to create branch: '.implode("\n", $output));
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write releases.json
|
||||
$this->info('Writing releases.json...');
|
||||
$releasesPath = "$tmpDir/json/releases.json";
|
||||
$jsonContent = json_encode($releases, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
$bytesWritten = file_put_contents($releasesPath, $jsonContent);
|
||||
|
||||
if ($bytesWritten === false) {
|
||||
$this->error("Failed to write releases.json to: $releasesPath");
|
||||
$this->error('Possible reasons: directory does not exist, permission denied, or disk full.');
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stage and commit
|
||||
$this->info('Committing changes...');
|
||||
exec('cd '.escapeshellarg($tmpDir).' && git add json/releases.json 2>&1', $output, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to stage changes: '.implode("\n", $output));
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info('Checking for changes...');
|
||||
$statusOutput = [];
|
||||
exec('cd '.escapeshellarg($tmpDir).' && git status --porcelain json/releases.json 2>&1', $statusOutput, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to check repository status: '.implode("\n", $statusOutput));
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty(array_filter($statusOutput))) {
|
||||
$this->info('Releases are already up to date. No changes to commit.');
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$commitMessage = 'Update releases.json with latest releases - '.date('Y-m-d H:i:s');
|
||||
$output = [];
|
||||
exec('cd '.escapeshellarg($tmpDir).' && git commit -m '.escapeshellarg($commitMessage).' 2>&1', $output, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to commit changes: '.implode("\n", $output));
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Push to remote
|
||||
$this->info('Pushing branch to remote...');
|
||||
exec('cd '.escapeshellarg($tmpDir).' && git push origin '.escapeshellarg($branchName).' 2>&1', $output, $returnCode);
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to push branch: '.implode("\n", $output));
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create pull request
|
||||
$this->info('Creating pull request...');
|
||||
$prTitle = 'Update releases.json - '.date('Y-m-d H:i:s');
|
||||
$prBody = 'Automated update of releases.json with latest '.count($releases).' releases from GitHub API';
|
||||
$prCommand = 'gh pr create --repo coollabsio/coolify-cdn --title '.escapeshellarg($prTitle).' --body '.escapeshellarg($prBody).' --base main --head '.escapeshellarg($branchName).' 2>&1';
|
||||
exec($prCommand, $output, $returnCode);
|
||||
|
||||
// Clean up
|
||||
exec('rm -rf '.escapeshellarg($tmpDir));
|
||||
|
||||
if ($returnCode !== 0) {
|
||||
$this->error('Failed to create PR: '.implode("\n", $output));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info('Pull request created successfully!');
|
||||
if (! empty($output)) {
|
||||
$this->info('PR Output: '.implode("\n", $output));
|
||||
}
|
||||
$this->info('Total releases synced: '.count($releases));
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('Error fetching releases: '.$e->getMessage());
|
||||
$this->error('Error syncing releases: '.$e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -174,11 +263,7 @@ public function handle()
|
|||
return;
|
||||
}
|
||||
|
||||
// First sync GitHub releases
|
||||
$this->info('Syncing GitHub releases first...');
|
||||
$this->syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn);
|
||||
|
||||
// Then sync versions.json
|
||||
// Sync versions.json to BunnyCDN
|
||||
Http::pool(fn (Pool $pool) => [
|
||||
$pool->storage(fileName: $versions_location)->put("/$bunny_cdn_storage_name/$bunny_cdn_path/$versions"),
|
||||
$pool->purge("$bunny_cdn/$bunny_cdn_path/$versions"),
|
||||
|
|
@ -187,14 +272,14 @@ public function handle()
|
|||
|
||||
return;
|
||||
} elseif ($only_github_releases) {
|
||||
$this->info('About to sync GitHub releases to BunnyCDN.');
|
||||
$this->info('About to sync GitHub releases to GitHub repository.');
|
||||
$confirmed = confirm('Are you sure you want to sync GitHub releases?');
|
||||
if (! $confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the reusable function
|
||||
$this->syncGitHubReleases($parent_dir, $bunny_cdn_storage_name, $bunny_cdn_path, $bunny_cdn);
|
||||
// Sync releases to GitHub repository
|
||||
$this->syncReleasesToGitHubRepo();
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
791
app/Console/Commands/UpdateServiceVersions.php
Normal file
791
app/Console/Commands/UpdateServiceVersions.php
Normal file
|
|
@ -0,0 +1,791 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class UpdateServiceVersions extends Command
|
||||
{
|
||||
protected $signature = 'services:update-versions
|
||||
{--service= : Update specific service template}
|
||||
{--dry-run : Show what would be updated without making changes}
|
||||
{--registry= : Filter by registry (dockerhub, ghcr, quay, codeberg)}';
|
||||
|
||||
protected $description = 'Update service template files with latest Docker image versions from registries';
|
||||
|
||||
protected array $stats = [
|
||||
'total' => 0,
|
||||
'updated' => 0,
|
||||
'failed' => 0,
|
||||
'skipped' => 0,
|
||||
];
|
||||
|
||||
protected array $registryCache = [];
|
||||
|
||||
protected array $majorVersionUpdates = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Starting service version update...');
|
||||
|
||||
$templateFiles = $this->getTemplateFiles();
|
||||
|
||||
$this->stats['total'] = count($templateFiles);
|
||||
|
||||
foreach ($templateFiles as $file) {
|
||||
$this->processTemplate($file);
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->displayStats();
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
protected function getTemplateFiles(): array
|
||||
{
|
||||
$pattern = base_path('templates/compose/*.yaml');
|
||||
$files = glob($pattern);
|
||||
|
||||
if ($service = $this->option('service')) {
|
||||
$files = array_filter($files, fn ($file) => basename($file) === "$service.yaml");
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
protected function processTemplate(string $filePath): void
|
||||
{
|
||||
$filename = basename($filePath);
|
||||
$this->info("Processing: {$filename}");
|
||||
|
||||
try {
|
||||
$content = file_get_contents($filePath);
|
||||
$yaml = Yaml::parse($content);
|
||||
|
||||
if (! isset($yaml['services'])) {
|
||||
$this->warn(" No services found in {$filename}");
|
||||
$this->stats['skipped']++;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$updated = false;
|
||||
$updatedYaml = $yaml;
|
||||
|
||||
foreach ($yaml['services'] as $serviceName => $serviceConfig) {
|
||||
if (! isset($serviceConfig['image'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentImage = $serviceConfig['image'];
|
||||
|
||||
// Check if using 'latest' tag and log for manual review
|
||||
if (str_contains($currentImage, ':latest')) {
|
||||
$registryUrl = $this->getRegistryUrl($currentImage);
|
||||
$this->warn(" {$serviceName}: {$currentImage} (using 'latest' tag)");
|
||||
if ($registryUrl) {
|
||||
$this->line(" → Manual review: {$registryUrl}");
|
||||
}
|
||||
}
|
||||
|
||||
$latestVersion = $this->getLatestVersion($currentImage);
|
||||
|
||||
if ($latestVersion && $latestVersion !== $currentImage) {
|
||||
$this->line(" {$serviceName}: {$currentImage} → {$latestVersion}");
|
||||
$updatedYaml['services'][$serviceName]['image'] = $latestVersion;
|
||||
$updated = true;
|
||||
} else {
|
||||
$this->line(" {$serviceName}: {$currentImage} (up to date)");
|
||||
}
|
||||
}
|
||||
|
||||
if ($updated) {
|
||||
if (! $this->option('dry-run')) {
|
||||
$this->updateYamlFile($filePath, $content, $updatedYaml);
|
||||
$this->stats['updated']++;
|
||||
} else {
|
||||
$this->warn(' [DRY RUN] Would update this file');
|
||||
$this->stats['updated']++;
|
||||
}
|
||||
} else {
|
||||
$this->stats['skipped']++;
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->error(" Failed: {$e->getMessage()}");
|
||||
$this->stats['failed']++;
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
protected function getLatestVersion(string $image): ?string
|
||||
{
|
||||
// Parse the image string
|
||||
[$repository, $currentTag] = $this->parseImage($image);
|
||||
|
||||
// Determine registry and fetch latest version
|
||||
$result = null;
|
||||
if (str_starts_with($repository, 'ghcr.io/')) {
|
||||
$result = $this->getGhcrLatestVersion($repository, $currentTag);
|
||||
} elseif (str_starts_with($repository, 'quay.io/')) {
|
||||
$result = $this->getQuayLatestVersion($repository, $currentTag);
|
||||
} elseif (str_starts_with($repository, 'codeberg.org/')) {
|
||||
$result = $this->getCodebergLatestVersion($repository, $currentTag);
|
||||
} elseif (str_starts_with($repository, 'lscr.io/')) {
|
||||
$result = $this->getDockerHubLatestVersion($repository, $currentTag);
|
||||
} elseif ($this->isCustomRegistry($repository)) {
|
||||
// Custom registries - skip for now, log warning
|
||||
$this->warn(" Skipping custom registry: {$repository}");
|
||||
$result = null;
|
||||
} else {
|
||||
// DockerHub (default registry - no prefix or docker.io/index.docker.io)
|
||||
$result = $this->getDockerHubLatestVersion($repository, $currentTag);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function isCustomRegistry(string $repository): bool
|
||||
{
|
||||
// List of custom/private registries that we can't query
|
||||
$customRegistries = [
|
||||
'docker.elastic.co/',
|
||||
'docker.n8n.io/',
|
||||
'docker.flipt.io/',
|
||||
'docker.getoutline.com/',
|
||||
'cr.weaviate.io/',
|
||||
'downloads.unstructured.io/',
|
||||
'budibase.docker.scarf.sh/',
|
||||
'calcom.docker.scarf.sh/',
|
||||
'code.forgejo.org/',
|
||||
'registry.supertokens.io/',
|
||||
'registry.rocket.chat/',
|
||||
'nabo.codimd.dev/',
|
||||
'gcr.io/',
|
||||
];
|
||||
|
||||
foreach ($customRegistries as $registry) {
|
||||
if (str_starts_with($repository, $registry)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function getRegistryUrl(string $image): ?string
|
||||
{
|
||||
[$repository] = $this->parseImage($image);
|
||||
|
||||
// GitHub Container Registry
|
||||
if (str_starts_with($repository, 'ghcr.io/')) {
|
||||
$parts = explode('/', str_replace('ghcr.io/', '', $repository));
|
||||
if (count($parts) >= 2) {
|
||||
return "https://github.com/{$parts[0]}/{$parts[1]}/pkgs/container/{$parts[1]}";
|
||||
}
|
||||
}
|
||||
|
||||
// Quay.io
|
||||
if (str_starts_with($repository, 'quay.io/')) {
|
||||
$repo = str_replace('quay.io/', '', $repository);
|
||||
|
||||
return "https://quay.io/repository/{$repo}?tab=tags";
|
||||
}
|
||||
|
||||
// Codeberg
|
||||
if (str_starts_with($repository, 'codeberg.org/')) {
|
||||
$parts = explode('/', str_replace('codeberg.org/', '', $repository));
|
||||
if (count($parts) >= 2) {
|
||||
return "https://codeberg.org/{$parts[0]}/-/packages/container/{$parts[1]}";
|
||||
}
|
||||
}
|
||||
|
||||
// Docker Hub
|
||||
$cleanRepo = str_replace(['index.docker.io/', 'docker.io/', 'lscr.io/'], '', $repository);
|
||||
if (! str_contains($cleanRepo, '/')) {
|
||||
// Official image
|
||||
return "https://hub.docker.com/_/{$cleanRepo}/tags";
|
||||
} else {
|
||||
// User/org image
|
||||
return "https://hub.docker.com/r/{$cleanRepo}/tags";
|
||||
}
|
||||
}
|
||||
|
||||
protected function parseImage(string $image): array
|
||||
{
|
||||
if (str_contains($image, ':')) {
|
||||
[$repo, $tag] = explode(':', $image, 2);
|
||||
} else {
|
||||
$repo = $image;
|
||||
$tag = 'latest';
|
||||
}
|
||||
|
||||
// Handle variables in tags
|
||||
if (str_contains($tag, '$')) {
|
||||
$tag = 'latest'; // Default to latest for variable tags
|
||||
}
|
||||
|
||||
return [$repo, $tag];
|
||||
}
|
||||
|
||||
protected function getDockerHubLatestVersion(string $repository, string $currentTag): ?string
|
||||
{
|
||||
try {
|
||||
// Check if we've already fetched tags for this repository
|
||||
if (! isset($this->registryCache[$repository.'_tags'])) {
|
||||
// Remove various registry prefixes
|
||||
$cleanRepo = $repository;
|
||||
$cleanRepo = str_replace('index.docker.io/', '', $cleanRepo);
|
||||
$cleanRepo = str_replace('docker.io/', '', $cleanRepo);
|
||||
$cleanRepo = str_replace('lscr.io/', '', $cleanRepo);
|
||||
|
||||
// For official images (no /) add library prefix
|
||||
if (! str_contains($cleanRepo, '/')) {
|
||||
$cleanRepo = "library/{$cleanRepo}";
|
||||
}
|
||||
|
||||
$url = "https://hub.docker.com/v2/repositories/{$cleanRepo}/tags";
|
||||
|
||||
$response = Http::timeout(10)->get($url, [
|
||||
'page_size' => 100,
|
||||
'ordering' => 'last_updated',
|
||||
]);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$tags = $data['results'] ?? [];
|
||||
|
||||
// Cache the tags for this repository
|
||||
$this->registryCache[$repository.'_tags'] = $tags;
|
||||
} else {
|
||||
$this->line(" [cached] Using cached tags for {$repository}");
|
||||
$tags = $this->registryCache[$repository.'_tags'];
|
||||
}
|
||||
|
||||
// Find the best matching tag
|
||||
return $this->findBestTag($tags, $currentTag, $repository);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn(" DockerHub API error for {$repository}: {$e->getMessage()}");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function findLatestTagDigest(array $tags, string $targetTag = 'latest'): ?string
|
||||
{
|
||||
// Find the digest/sha for the target tag (usually 'latest')
|
||||
foreach ($tags as $tag) {
|
||||
if ($tag['name'] === $targetTag) {
|
||||
return $tag['digest'] ?? $tag['images'][0]['digest'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function findVersionTagsForDigest(array $tags, string $digest): array
|
||||
{
|
||||
// Find all semantic version tags that share the same digest
|
||||
$versionTags = [];
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
$tagDigest = $tag['digest'] ?? $tag['images'][0]['digest'] ?? null;
|
||||
|
||||
if ($tagDigest === $digest) {
|
||||
$tagName = $tag['name'];
|
||||
// Only include semantic version tags
|
||||
if (preg_match('/^\d+\.\d+(\.\d+)?$/', $tagName)) {
|
||||
$versionTags[] = $tagName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $versionTags;
|
||||
}
|
||||
|
||||
protected function getGhcrLatestVersion(string $repository, string $currentTag): ?string
|
||||
{
|
||||
try {
|
||||
// GHCR doesn't have a public API for listing tags without auth
|
||||
// We'll try to fetch the package metadata via GitHub API
|
||||
$parts = explode('/', str_replace('ghcr.io/', '', $repository));
|
||||
|
||||
if (count($parts) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$owner = $parts[0];
|
||||
$package = $parts[1];
|
||||
|
||||
// Try GitHub Container Registry API
|
||||
$url = "https://api.github.com/users/{$owner}/packages/container/{$package}/versions";
|
||||
|
||||
$response = Http::timeout(10)
|
||||
->withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
])
|
||||
->get($url, ['per_page' => 100]);
|
||||
|
||||
if (! $response->successful()) {
|
||||
// Most GHCR packages require authentication
|
||||
if ($currentTag === 'latest') {
|
||||
$this->warn(' ⚠ GHCR requires authentication - manual review needed');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$versions = $response->json();
|
||||
$tags = [];
|
||||
|
||||
// Build tags array with digest information
|
||||
foreach ($versions as $version) {
|
||||
$digest = $version['name'] ?? null; // This is the SHA digest
|
||||
|
||||
if (isset($version['metadata']['container']['tags'])) {
|
||||
foreach ($version['metadata']['container']['tags'] as $tag) {
|
||||
$tags[] = [
|
||||
'name' => $tag,
|
||||
'digest' => $digest,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->findBestTag($tags, $currentTag, $repository);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn(" GHCR API error for {$repository}: {$e->getMessage()}");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getQuayLatestVersion(string $repository, string $currentTag): ?string
|
||||
{
|
||||
try {
|
||||
// Check if we've already fetched tags for this repository
|
||||
if (! isset($this->registryCache[$repository.'_tags'])) {
|
||||
$cleanRepo = str_replace('quay.io/', '', $repository);
|
||||
|
||||
$url = "https://quay.io/api/v1/repository/{$cleanRepo}/tag/";
|
||||
|
||||
$response = Http::timeout(10)->get($url, ['limit' => 100]);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$tags = array_map(fn ($tag) => ['name' => $tag['name']], $data['tags'] ?? []);
|
||||
|
||||
// Cache the tags for this repository
|
||||
$this->registryCache[$repository.'_tags'] = $tags;
|
||||
} else {
|
||||
$this->line(" [cached] Using cached tags for {$repository}");
|
||||
$tags = $this->registryCache[$repository.'_tags'];
|
||||
}
|
||||
|
||||
return $this->findBestTag($tags, $currentTag, $repository);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn(" Quay API error for {$repository}: {$e->getMessage()}");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getCodebergLatestVersion(string $repository, string $currentTag): ?string
|
||||
{
|
||||
try {
|
||||
// Check if we've already fetched tags for this repository
|
||||
if (! isset($this->registryCache[$repository.'_tags'])) {
|
||||
// Codeberg uses Forgejo/Gitea, which has a container registry API
|
||||
$cleanRepo = str_replace('codeberg.org/', '', $repository);
|
||||
$parts = explode('/', $cleanRepo);
|
||||
|
||||
if (count($parts) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$owner = $parts[0];
|
||||
$package = $parts[1];
|
||||
|
||||
// Codeberg API endpoint for packages
|
||||
$url = "https://codeberg.org/api/packages/{$owner}/container/{$package}";
|
||||
|
||||
$response = Http::timeout(10)->get($url);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$tags = [];
|
||||
|
||||
if (isset($data['versions'])) {
|
||||
foreach ($data['versions'] as $version) {
|
||||
if (isset($version['name'])) {
|
||||
$tags[] = ['name' => $version['name']];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the tags for this repository
|
||||
$this->registryCache[$repository.'_tags'] = $tags;
|
||||
} else {
|
||||
$this->line(" [cached] Using cached tags for {$repository}");
|
||||
$tags = $this->registryCache[$repository.'_tags'];
|
||||
}
|
||||
|
||||
return $this->findBestTag($tags, $currentTag, $repository);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn(" Codeberg API error for {$repository}: {$e->getMessage()}");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function findBestTag(array $tags, string $currentTag, string $repository): ?string
|
||||
{
|
||||
if (empty($tags)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If current tag is 'latest', find what version it actually points to
|
||||
if ($currentTag === 'latest') {
|
||||
// First, try to find the digest for 'latest' tag
|
||||
$latestDigest = $this->findLatestTagDigest($tags, 'latest');
|
||||
|
||||
if ($latestDigest) {
|
||||
// Find all semantic version tags that share the same digest
|
||||
$versionTags = $this->findVersionTagsForDigest($tags, $latestDigest);
|
||||
|
||||
if (! empty($versionTags)) {
|
||||
// Prefer shorter version tags (1.8 over 1.8.1)
|
||||
$bestVersion = $this->preferShorterVersion($versionTags);
|
||||
$this->info(" ✓ Found 'latest' points to: {$bestVersion}");
|
||||
|
||||
return $repository.':'.$bestVersion;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: get the latest semantic version available (prefer shorter)
|
||||
$semverTags = $this->filterSemanticVersionTags($tags);
|
||||
if (! empty($semverTags)) {
|
||||
$bestVersion = $this->preferShorterVersion($semverTags);
|
||||
|
||||
return $repository.':'.$bestVersion;
|
||||
}
|
||||
|
||||
// If no semantic versions found, keep 'latest'
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for major version updates for reporting
|
||||
$this->checkForMajorVersionUpdate($tags, $currentTag, $repository);
|
||||
|
||||
// If current tag is a major version (e.g., "8", "5", "16")
|
||||
if (preg_match('/^\d+$/', $currentTag)) {
|
||||
$majorVersion = (int) $currentTag;
|
||||
$matchingTags = array_filter($tags, function ($tag) use ($majorVersion) {
|
||||
$name = $tag['name'];
|
||||
|
||||
// Match tags that start with the major version
|
||||
return preg_match("/^{$majorVersion}(\.\d+)?(\.\d+)?$/", $name);
|
||||
});
|
||||
|
||||
if (! empty($matchingTags)) {
|
||||
$versions = array_column($matchingTags, 'name');
|
||||
$bestVersion = $this->preferShorterVersion($versions);
|
||||
if ($bestVersion !== $currentTag) {
|
||||
return $repository.':'.$bestVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If current tag is date-based version (e.g., "2025.06.02-sha-xxx")
|
||||
if (preg_match('/^\d{4}\.\d{2}\.\d{2}/', $currentTag)) {
|
||||
// Get all date-based tags
|
||||
$dateTags = array_filter($tags, function ($tag) {
|
||||
return preg_match('/^\d{4}\.\d{2}\.\d{2}/', $tag['name']);
|
||||
});
|
||||
|
||||
if (! empty($dateTags)) {
|
||||
$versions = array_column($dateTags, 'name');
|
||||
$sorted = $this->sortSemanticVersions($versions);
|
||||
$latestDate = $sorted[0];
|
||||
|
||||
// Compare dates
|
||||
if ($latestDate !== $currentTag) {
|
||||
return $repository.':'.$latestDate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// If current tag is semantic version (e.g., "1.7.4", "8.0")
|
||||
if (preg_match('/^\d+\.\d+(\.\d+)?$/', $currentTag)) {
|
||||
$parts = explode('.', $currentTag);
|
||||
$majorMinor = $parts[0].'.'.$parts[1];
|
||||
|
||||
$matchingTags = array_filter($tags, function ($tag) use ($majorMinor) {
|
||||
$name = $tag['name'];
|
||||
|
||||
return str_starts_with($name, $majorMinor);
|
||||
});
|
||||
|
||||
if (! empty($matchingTags)) {
|
||||
$versions = array_column($matchingTags, 'name');
|
||||
$bestVersion = $this->preferShorterVersion($versions);
|
||||
if (version_compare($bestVersion, $currentTag, '>') || version_compare($bestVersion, $currentTag, '=')) {
|
||||
// Only update if it's newer or if we can simplify (1.8.1 -> 1.8)
|
||||
if ($bestVersion !== $currentTag) {
|
||||
return $repository.':'.$bestVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If current tag is a named version (e.g., "stable")
|
||||
if (in_array($currentTag, ['stable', 'lts', 'edge'])) {
|
||||
// Check if the same tag exists in the list (it's up to date)
|
||||
$exists = array_filter($tags, fn ($tag) => $tag['name'] === $currentTag);
|
||||
if (! empty($exists)) {
|
||||
return null; // Tag exists and is current
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function filterSemanticVersionTags(array $tags): array
|
||||
{
|
||||
$semverTags = array_filter($tags, function ($tag) {
|
||||
$name = $tag['name'];
|
||||
|
||||
// Accept semantic versions (1.2.3, v1.2.3)
|
||||
if (preg_match('/^v?\d+\.\d+(\.\d+)?(\.\d+)?$/', $name)) {
|
||||
// Exclude versions with suffixes like -rc, -beta, -alpha
|
||||
if (preg_match('/-(rc|beta|alpha|dev|test|pre|snapshot)/i', $name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Accept date-based versions (2025.06.02, 2025.10.0, 2025.06.02-sha-xxx, RELEASE.2025-10-15T17-29-55Z)
|
||||
if (preg_match('/^\d{4}\.\d{2}\.(\d{2}|\d)/', $name) || preg_match('/^RELEASE\.\d{4}-\d{2}-\d{2}/', $name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return $this->sortSemanticVersions(array_column($semverTags, 'name'));
|
||||
}
|
||||
|
||||
protected function sortSemanticVersions(array $versions): array
|
||||
{
|
||||
usort($versions, function ($a, $b) {
|
||||
// Check if these are date-based versions (YYYY.MM.DD or YYYY.MM.D format)
|
||||
$isDateA = preg_match('/^(\d{4})\.(\d{2})\.(\d{1,2})/', $a, $matchesA);
|
||||
$isDateB = preg_match('/^(\d{4})\.(\d{2})\.(\d{1,2})/', $b, $matchesB);
|
||||
|
||||
if ($isDateA && $isDateB) {
|
||||
// Both are date-based (YYYY.MM.DD), compare as dates
|
||||
$dateA = $matchesA[1].$matchesA[2].str_pad($matchesA[3], 2, '0', STR_PAD_LEFT); // YYYYMMDD
|
||||
$dateB = $matchesB[1].$matchesB[2].str_pad($matchesB[3], 2, '0', STR_PAD_LEFT); // YYYYMMDD
|
||||
|
||||
return strcmp($dateB, $dateA); // Descending order (newest first)
|
||||
}
|
||||
|
||||
// Check if these are RELEASE date versions (RELEASE.YYYY-MM-DDTHH-MM-SSZ)
|
||||
$isReleaseA = preg_match('/^RELEASE\.(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})Z/', $a, $matchesA);
|
||||
$isReleaseB = preg_match('/^RELEASE\.(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})Z/', $b, $matchesB);
|
||||
|
||||
if ($isReleaseA && $isReleaseB) {
|
||||
// Both are RELEASE format, compare as datetime
|
||||
$dateTimeA = $matchesA[1].$matchesA[2].$matchesA[3].$matchesA[4].$matchesA[5].$matchesA[6]; // YYYYMMDDHHMMSS
|
||||
$dateTimeB = $matchesB[1].$matchesB[2].$matchesB[3].$matchesB[4].$matchesB[5].$matchesB[6]; // YYYYMMDDHHMMSS
|
||||
|
||||
return strcmp($dateTimeB, $dateTimeA); // Descending order (newest first)
|
||||
}
|
||||
|
||||
// Strip 'v' prefix for version comparison
|
||||
$cleanA = ltrim($a, 'v');
|
||||
$cleanB = ltrim($b, 'v');
|
||||
|
||||
// Fall back to semantic version comparison
|
||||
return version_compare($cleanB, $cleanA); // Descending order
|
||||
});
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
protected function preferShorterVersion(array $versions): string
|
||||
{
|
||||
if (empty($versions)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Sort by version (highest first)
|
||||
$sorted = $this->sortSemanticVersions($versions);
|
||||
$highest = $sorted[0];
|
||||
|
||||
// Parse the highest version
|
||||
$parts = explode('.', $highest);
|
||||
|
||||
// Look for shorter versions that match
|
||||
// Priority: major (8) > major.minor (8.0) > major.minor.patch (8.0.39)
|
||||
|
||||
// Try to find just major.minor (e.g., 1.8 instead of 1.8.1)
|
||||
if (count($parts) === 3) {
|
||||
$majorMinor = $parts[0].'.'.$parts[1];
|
||||
if (in_array($majorMinor, $versions)) {
|
||||
return $majorMinor;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find just major (e.g., 8 instead of 8.0.39)
|
||||
if (count($parts) >= 2) {
|
||||
$major = $parts[0];
|
||||
if (in_array($major, $versions)) {
|
||||
return $major;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the highest version we found
|
||||
return $highest;
|
||||
}
|
||||
|
||||
protected function updateYamlFile(string $filePath, string $originalContent, array $updatedYaml): void
|
||||
{
|
||||
// Preserve comments and formatting by updating the YAML content
|
||||
$lines = explode("\n", $originalContent);
|
||||
$updatedLines = [];
|
||||
$inServices = false;
|
||||
$currentService = null;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Detect if we're in the services section
|
||||
if (preg_match('/^services:/', $line)) {
|
||||
$inServices = true;
|
||||
$updatedLines[] = $line;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Detect service name (allow hyphens and underscores)
|
||||
if ($inServices && preg_match('/^ ([\w-]+):/', $line, $matches)) {
|
||||
$currentService = $matches[1];
|
||||
$updatedLines[] = $line;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update image line
|
||||
if ($currentService && preg_match('/^(\s+)image:\s*(.+)$/', $line, $matches)) {
|
||||
$indent = $matches[1];
|
||||
$newImage = $updatedYaml['services'][$currentService]['image'] ?? $matches[2];
|
||||
$updatedLines[] = "{$indent}image: {$newImage}";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we hit a non-indented line, we're out of services
|
||||
if ($inServices && preg_match('/^\S/', $line) && ! preg_match('/^services:/', $line)) {
|
||||
$inServices = false;
|
||||
$currentService = null;
|
||||
}
|
||||
|
||||
$updatedLines[] = $line;
|
||||
}
|
||||
|
||||
file_put_contents($filePath, implode("\n", $updatedLines));
|
||||
}
|
||||
|
||||
protected function checkForMajorVersionUpdate(array $tags, string $currentTag, string $repository): void
|
||||
{
|
||||
// Only check semantic versions
|
||||
if (! preg_match('/^v?(\d+)\./', $currentTag, $currentMatches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentMajor = (int) $currentMatches[1];
|
||||
|
||||
// Get all semantic version tags
|
||||
$semverTags = $this->filterSemanticVersionTags($tags);
|
||||
|
||||
// Find the highest major version available
|
||||
$highestMajor = $currentMajor;
|
||||
foreach ($semverTags as $version) {
|
||||
if (preg_match('/^v?(\d+)\./', $version, $matches)) {
|
||||
$major = (int) $matches[1];
|
||||
if ($major > $highestMajor) {
|
||||
$highestMajor = $major;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a higher major version available, record it
|
||||
if ($highestMajor > $currentMajor) {
|
||||
$this->majorVersionUpdates[] = [
|
||||
'repository' => $repository,
|
||||
'current' => $currentTag,
|
||||
'current_major' => $currentMajor,
|
||||
'available_major' => $highestMajor,
|
||||
'registry_url' => $this->getRegistryUrl($repository.':'.$currentTag),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
protected function displayStats(): void
|
||||
{
|
||||
$this->info('Summary:');
|
||||
$this->table(
|
||||
['Metric', 'Count'],
|
||||
[
|
||||
['Total Templates', $this->stats['total']],
|
||||
['Updated', $this->stats['updated']],
|
||||
['Skipped (up to date)', $this->stats['skipped']],
|
||||
['Failed', $this->stats['failed']],
|
||||
]
|
||||
);
|
||||
|
||||
// Display major version updates if any
|
||||
if (! empty($this->majorVersionUpdates)) {
|
||||
$this->newLine();
|
||||
$this->warn('⚠ Services with available MAJOR version updates:');
|
||||
$this->newLine();
|
||||
|
||||
$tableData = [];
|
||||
foreach ($this->majorVersionUpdates as $update) {
|
||||
$tableData[] = [
|
||||
$update['repository'],
|
||||
"v{$update['current_major']}.x",
|
||||
"v{$update['available_major']}.x",
|
||||
$update['registry_url'],
|
||||
];
|
||||
}
|
||||
|
||||
$this->table(
|
||||
['Repository', 'Current', 'Available', 'Registry URL'],
|
||||
$tableData
|
||||
);
|
||||
|
||||
$this->newLine();
|
||||
$this->comment('💡 Major version updates may include breaking changes. Review before upgrading.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1893,7 +1893,6 @@ public function logs_by_uuid(Request $request)
|
|||
public function delete_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
$cleanup = filter_var($request->query->get('cleanup', true), FILTER_VALIDATE_BOOLEAN);
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
|
@ -1912,10 +1911,10 @@ public function delete_by_uuid(Request $request)
|
|||
|
||||
DeleteResourceJob::dispatch(
|
||||
resource: $application,
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true)
|
||||
deleteVolumes: $request->boolean('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->boolean('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->boolean('delete_configurations', true),
|
||||
dockerCleanup: $request->boolean('docker_cleanup', true)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -3155,8 +3154,8 @@ public function action_deploy(Request $request)
|
|||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$force = $request->query->get('force') ?? false;
|
||||
$instant_deploy = $request->query->get('instant_deploy') ?? false;
|
||||
$force = $request->boolean('force', false);
|
||||
$instant_deploy = $request->boolean('instant_deploy', false);
|
||||
$uuid = $request->route('uuid');
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'UUID is required.'], 400);
|
||||
|
|
|
|||
|
|
@ -1619,6 +1619,18 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
|||
return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
|
||||
}
|
||||
$destination = $destinations->first();
|
||||
if ($destinations->count() > 1 && $request->has('destination_uuid')) {
|
||||
$destination = $destinations->where('uuid', $request->destination_uuid)->first();
|
||||
if (! $destination) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'destination_uuid' => 'Provided destination_uuid does not belong to the specified server.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('public_port') && $request->is_public) {
|
||||
if (isPublicPortAlreadyUsed($server, $request->public_port)) {
|
||||
return response()->json(['message' => 'Public port already used by another database.'], 400);
|
||||
|
|
@ -2133,7 +2145,6 @@ public function create_database(Request $request, NewDatabaseTypes $type)
|
|||
public function delete_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
$cleanup = filter_var($request->query->get('cleanup', true), FILTER_VALIDATE_BOOLEAN);
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
|
@ -2149,10 +2160,10 @@ public function delete_by_uuid(Request $request)
|
|||
|
||||
DeleteResourceJob::dispatch(
|
||||
resource: $database,
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true)
|
||||
deleteVolumes: $request->boolean('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->boolean('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->boolean('delete_configurations', true),
|
||||
dockerCleanup: $request->boolean('docker_cleanup', true)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -2243,7 +2254,7 @@ public function delete_backup_by_uuid(Request $request)
|
|||
return response()->json(['message' => 'Backup configuration not found.'], 404);
|
||||
}
|
||||
|
||||
$deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN);
|
||||
$deleteS3 = $request->boolean('delete_s3', false);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
|
@ -2376,7 +2387,7 @@ public function delete_execution_by_uuid(Request $request)
|
|||
return response()->json(['message' => 'Backup execution not found.'], 404);
|
||||
}
|
||||
|
||||
$deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN);
|
||||
$deleteS3 = $request->boolean('delete_s3', false);
|
||||
|
||||
try {
|
||||
if ($execution->filename) {
|
||||
|
|
|
|||
|
|
@ -649,10 +649,10 @@ public function delete_by_uuid(Request $request)
|
|||
|
||||
DeleteResourceJob::dispatch(
|
||||
resource: $service,
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true)
|
||||
deleteVolumes: $request->boolean('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->boolean('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->boolean('delete_configurations', true),
|
||||
dockerCleanup: $request->boolean('docker_cleanup', true)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
|
|
|
|||
|
|
@ -459,7 +459,7 @@ private function decide_what_to_do()
|
|||
private function post_deployment()
|
||||
{
|
||||
GetContainersStatus::dispatch($this->server);
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
$this->completeDeployment();
|
||||
if ($this->pull_request_id !== 0) {
|
||||
if ($this->application->is_github_based()) {
|
||||
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED);
|
||||
|
|
@ -517,6 +517,10 @@ private function deploy_dockerimage_buildpack()
|
|||
$this->generate_image_names();
|
||||
$this->prepare_builder_image();
|
||||
$this->generate_compose_file();
|
||||
|
||||
// Save runtime environment variables (including empty .env file if no variables defined)
|
||||
$this->save_runtime_environment_variables();
|
||||
|
||||
$this->rolling_update();
|
||||
}
|
||||
|
||||
|
|
@ -1004,7 +1008,7 @@ private function just_restart()
|
|||
$this->generate_image_names();
|
||||
$this->check_image_locally_or_remotely();
|
||||
$this->should_skip_build();
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
$this->completeDeployment();
|
||||
}
|
||||
|
||||
private function should_skip_build()
|
||||
|
|
@ -1222,9 +1226,9 @@ private function save_runtime_environment_variables()
|
|||
|
||||
// Handle empty environment variables
|
||||
if ($environment_variables->isEmpty()) {
|
||||
// For Docker Compose, we need to create an empty .env file
|
||||
// For Docker Compose and Docker Image, we need to create an empty .env file
|
||||
// because we always reference it in the compose file
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if ($this->build_pack === 'dockercompose' || $this->build_pack === 'dockerimage') {
|
||||
$this->application_deployment_queue->addLogEntry('Creating empty .env file (no environment variables defined).');
|
||||
|
||||
// Create empty .env file
|
||||
|
|
@ -1628,7 +1632,7 @@ private function health_check()
|
|||
return;
|
||||
}
|
||||
if ($this->application->custom_healthcheck_found) {
|
||||
$this->application_deployment_queue->addLogEntry('Custom healthcheck found, skipping default healthcheck.');
|
||||
$this->application_deployment_queue->addLogEntry('Custom healthcheck found in Dockerfile.');
|
||||
}
|
||||
if ($this->container_name) {
|
||||
$counter = 1;
|
||||
|
|
@ -1776,9 +1780,8 @@ private function create_workdir()
|
|||
private function prepare_builder_image(bool $firstTry = true)
|
||||
{
|
||||
$this->checkForCancellation();
|
||||
$settings = instanceSettings();
|
||||
$helperImage = config('constants.coolify.helper_image');
|
||||
$helperImage = "{$helperImage}:{$settings->helper_version}";
|
||||
$helperImage = "{$helperImage}:".getHelperVersion();
|
||||
// Get user home directory
|
||||
$this->serverUserHomeDir = instant_remote_process(['echo $HOME'], $this->server);
|
||||
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
|
||||
|
|
@ -2318,8 +2321,8 @@ private function generate_compose_file()
|
|||
$this->application->parseHealthcheckFromDockerfile($this->saved_outputs->get('dockerfile_from_repo'));
|
||||
}
|
||||
$custom_network_aliases = [];
|
||||
if (is_array($this->application->custom_network_aliases) && count($this->application->custom_network_aliases) > 0) {
|
||||
$custom_network_aliases = $this->application->custom_network_aliases;
|
||||
if (! empty($this->application->custom_network_aliases_array)) {
|
||||
$custom_network_aliases = $this->application->custom_network_aliases_array;
|
||||
}
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
|
|
@ -2354,16 +2357,22 @@ private function generate_compose_file()
|
|||
];
|
||||
// Always use .env file
|
||||
$docker_compose['services'][$this->container_name]['env_file'] = ['.env'];
|
||||
$docker_compose['services'][$this->container_name]['healthcheck'] = [
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
$this->generate_healthcheck_commands(),
|
||||
],
|
||||
'interval' => $this->application->health_check_interval.'s',
|
||||
'timeout' => $this->application->health_check_timeout.'s',
|
||||
'retries' => $this->application->health_check_retries,
|
||||
'start_period' => $this->application->health_check_start_period.'s',
|
||||
];
|
||||
|
||||
// Only add Coolify healthcheck if no custom HEALTHCHECK found in Dockerfile
|
||||
// If custom_healthcheck_found is true, the Dockerfile's HEALTHCHECK will be used
|
||||
// If healthcheck is disabled, no healthcheck will be added
|
||||
if (! $this->application->custom_healthcheck_found && ! $this->application->isHealthcheckDisabled()) {
|
||||
$docker_compose['services'][$this->container_name]['healthcheck'] = [
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
$this->generate_healthcheck_commands(),
|
||||
],
|
||||
'interval' => $this->application->health_check_interval.'s',
|
||||
'timeout' => $this->application->health_check_timeout.'s',
|
||||
'retries' => $this->application->health_check_retries,
|
||||
'start_period' => $this->application->health_check_start_period.'s',
|
||||
];
|
||||
}
|
||||
|
||||
if (! is_null($this->application->limits_cpuset)) {
|
||||
data_set($docker_compose, 'services.'.$this->container_name.'.cpuset', $this->application->limits_cpuset);
|
||||
|
|
@ -3013,9 +3022,7 @@ private function stop_running_container(bool $force = false)
|
|||
$this->application_deployment_queue->addLogEntry('----------------------------------------');
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry('New container is not healthy, rolling back to the old container.');
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => ApplicationDeploymentStatus::FAILED->value,
|
||||
]);
|
||||
$this->failDeployment();
|
||||
$this->graceful_shutdown_container($this->container_name);
|
||||
}
|
||||
}
|
||||
|
|
@ -3219,6 +3226,20 @@ private function generate_secrets_hash($variables)
|
|||
return hash_hmac('sha256', $secrets_string, $this->secrets_hash_key);
|
||||
}
|
||||
|
||||
protected function findFromInstructionLines($dockerfile): array
|
||||
{
|
||||
$fromLines = [];
|
||||
foreach ($dockerfile as $index => $line) {
|
||||
$trimmedLine = trim($line);
|
||||
// Check if line starts with FROM (case-insensitive)
|
||||
if (preg_match('/^FROM\s+/i', $trimmedLine)) {
|
||||
$fromLines[] = $index;
|
||||
}
|
||||
}
|
||||
|
||||
return $fromLines;
|
||||
}
|
||||
|
||||
private function add_build_env_variables_to_dockerfile()
|
||||
{
|
||||
if ($this->dockerBuildkitSupported) {
|
||||
|
|
@ -3231,6 +3252,18 @@ private function add_build_env_variables_to_dockerfile()
|
|||
'ignore_errors' => true,
|
||||
]);
|
||||
$dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
|
||||
|
||||
// Find all FROM instruction positions
|
||||
$fromLines = $this->findFromInstructionLines($dockerfile);
|
||||
|
||||
// If no FROM instructions found, skip ARG insertion
|
||||
if (empty($fromLines)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect all ARG statements to insert
|
||||
$argsToInsert = collect();
|
||||
|
||||
if ($this->pull_request_id === 0) {
|
||||
// Only add environment variables that are available during build
|
||||
$envs = $this->application->environment_variables()
|
||||
|
|
@ -3239,9 +3272,9 @@ private function add_build_env_variables_to_dockerfile()
|
|||
->get();
|
||||
foreach ($envs as $env) {
|
||||
if (data_get($env, 'is_multiline') === true) {
|
||||
$dockerfile->splice(1, 0, ["ARG {$env->key}"]);
|
||||
$argsToInsert->push("ARG {$env->key}");
|
||||
} else {
|
||||
$dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]);
|
||||
$argsToInsert->push("ARG {$env->key}={$env->real_value}");
|
||||
}
|
||||
}
|
||||
// Add Coolify variables as ARGs
|
||||
|
|
@ -3251,9 +3284,7 @@ private function add_build_env_variables_to_dockerfile()
|
|||
->map(function ($var) {
|
||||
return "ARG {$var}";
|
||||
});
|
||||
foreach ($coolify_vars as $arg) {
|
||||
$dockerfile->splice(1, 0, [$arg]);
|
||||
}
|
||||
$argsToInsert = $argsToInsert->merge($coolify_vars);
|
||||
}
|
||||
} else {
|
||||
// Only add preview environment variables that are available during build
|
||||
|
|
@ -3263,9 +3294,9 @@ private function add_build_env_variables_to_dockerfile()
|
|||
->get();
|
||||
foreach ($envs as $env) {
|
||||
if (data_get($env, 'is_multiline') === true) {
|
||||
$dockerfile->splice(1, 0, ["ARG {$env->key}"]);
|
||||
$argsToInsert->push("ARG {$env->key}");
|
||||
} else {
|
||||
$dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]);
|
||||
$argsToInsert->push("ARG {$env->key}={$env->real_value}");
|
||||
}
|
||||
}
|
||||
// Add Coolify variables as ARGs
|
||||
|
|
@ -3275,15 +3306,23 @@ private function add_build_env_variables_to_dockerfile()
|
|||
->map(function ($var) {
|
||||
return "ARG {$var}";
|
||||
});
|
||||
foreach ($coolify_vars as $arg) {
|
||||
$dockerfile->splice(1, 0, [$arg]);
|
||||
}
|
||||
$argsToInsert = $argsToInsert->merge($coolify_vars);
|
||||
}
|
||||
}
|
||||
|
||||
if ($envs->isNotEmpty()) {
|
||||
$secrets_hash = $this->generate_secrets_hash($envs);
|
||||
$dockerfile->splice(1, 0, ["ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"]);
|
||||
// Insert ARGs after each FROM instruction (in reverse order to maintain correct line numbers)
|
||||
if ($argsToInsert->isNotEmpty()) {
|
||||
foreach (array_reverse($fromLines) as $fromLineIndex) {
|
||||
// Insert all ARGs after this FROM instruction
|
||||
foreach ($argsToInsert->reverse() as $arg) {
|
||||
$dockerfile->splice($fromLineIndex + 1, 0, [$arg]);
|
||||
}
|
||||
}
|
||||
$envs_mapped = $envs->mapWithKeys(function ($env) {
|
||||
return [$env->key => $env->real_value];
|
||||
});
|
||||
$secrets_hash = $this->generate_secrets_hash($envs_mapped);
|
||||
$argsToInsert->push("ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}");
|
||||
}
|
||||
|
||||
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
|
||||
|
|
@ -3649,42 +3688,116 @@ private function checkForCancellation(): void
|
|||
}
|
||||
}
|
||||
|
||||
private function next(string $status)
|
||||
/**
|
||||
* Transition deployment to a new status with proper validation and side effects.
|
||||
* This is the single source of truth for status transitions.
|
||||
*/
|
||||
private function transitionToStatus(ApplicationDeploymentStatus $status): void
|
||||
{
|
||||
// Refresh to get latest status
|
||||
$this->application_deployment_queue->refresh();
|
||||
|
||||
// Never allow changing status from FAILED or CANCELLED_BY_USER to anything else
|
||||
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) {
|
||||
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
|
||||
|
||||
if ($this->isInTerminalState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->updateDeploymentStatus($status);
|
||||
$this->handleStatusTransition($status);
|
||||
queue_next_deployment($this->application);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if deployment is in a terminal state (FAILED or CANCELLED).
|
||||
* Terminal states cannot be changed.
|
||||
*/
|
||||
private function isInTerminalState(): bool
|
||||
{
|
||||
$this->application_deployment_queue->refresh();
|
||||
|
||||
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
|
||||
// Job was cancelled, stop execution
|
||||
$this->application_deployment_queue->addLogEntry('Deployment cancelled by user, stopping execution.');
|
||||
throw new \RuntimeException('Deployment cancelled by user', 69420);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the deployment status in the database.
|
||||
*/
|
||||
private function updateDeploymentStatus(ApplicationDeploymentStatus $status): void
|
||||
{
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => $status,
|
||||
'status' => $status->value,
|
||||
]);
|
||||
}
|
||||
|
||||
queue_next_deployment($this->application);
|
||||
/**
|
||||
* Execute status-specific side effects (events, notifications, additional deployments).
|
||||
*/
|
||||
private function handleStatusTransition(ApplicationDeploymentStatus $status): void
|
||||
{
|
||||
match ($status) {
|
||||
ApplicationDeploymentStatus::FINISHED => $this->handleSuccessfulDeployment(),
|
||||
ApplicationDeploymentStatus::FAILED => $this->handleFailedDeployment(),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
||||
event(new ApplicationConfigurationChanged($this->application->team()->id));
|
||||
/**
|
||||
* Handle side effects when deployment succeeds.
|
||||
*/
|
||||
private function handleSuccessfulDeployment(): void
|
||||
{
|
||||
event(new ApplicationConfigurationChanged($this->application->team()->id));
|
||||
|
||||
if (! $this->only_this_server) {
|
||||
$this->deploy_to_additional_destinations();
|
||||
}
|
||||
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
|
||||
if (! $this->only_this_server) {
|
||||
$this->deploy_to_additional_destinations();
|
||||
}
|
||||
|
||||
$this->sendDeploymentNotification(DeploymentSuccess::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle side effects when deployment fails.
|
||||
*/
|
||||
private function handleFailedDeployment(): void
|
||||
{
|
||||
$this->sendDeploymentNotification(DeploymentFailed::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send deployment status notification to the team.
|
||||
*/
|
||||
private function sendDeploymentNotification(string $notificationClass): void
|
||||
{
|
||||
$this->application->environment->project->team?->notify(
|
||||
new $notificationClass($this->application, $this->deployment_uuid, $this->preview)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete deployment successfully.
|
||||
* Sends success notification and triggers additional deployments if needed.
|
||||
*/
|
||||
private function completeDeployment(): void
|
||||
{
|
||||
$this->transitionToStatus(ApplicationDeploymentStatus::FINISHED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail the deployment.
|
||||
* Sends failure notification and queues next deployment.
|
||||
*/
|
||||
private function failDeployment(): void
|
||||
{
|
||||
$this->transitionToStatus(ApplicationDeploymentStatus::FAILED);
|
||||
}
|
||||
|
||||
public function failed(Throwable $exception): void
|
||||
{
|
||||
$this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
$this->failDeployment();
|
||||
$this->application_deployment_queue->addLogEntry('Oops something is not okay, are you okay? 😢', 'stderr');
|
||||
if (str($exception->getMessage())->isNotEmpty()) {
|
||||
$this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
|
||||
|
|
|
|||
|
|
@ -653,9 +653,8 @@ private function upload_to_s3(): void
|
|||
|
||||
private function getFullImageName(): string
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
$helperImage = config('constants.coolify.helper_image');
|
||||
$latestVersion = $settings->helper_version;
|
||||
$latestVersion = getHelperVersion();
|
||||
|
||||
return "{$helperImage}:{$latestVersion}";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public function __construct(public Server $server)
|
|||
public function handle(): void
|
||||
{
|
||||
$helperImage = config('constants.coolify.helper_image');
|
||||
$latest_version = instanceSettings()->helper_version;
|
||||
$latest_version = getHelperVersion();
|
||||
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ public function middleware(): array
|
|||
{
|
||||
return [
|
||||
(new WithoutOverlapping('scheduled-job-manager'))
|
||||
->releaseAfter(60), // Release the lock after 60 seconds if job fails
|
||||
->expireAfter(60) // Lock expires after 1 minute to prevent stale locks
|
||||
->dontRelease(), // Don't re-queue on lock conflict
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ public function mount()
|
|||
|
||||
if ($this->selectedServerType === 'remote') {
|
||||
if ($this->privateKeys->isEmpty()) {
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
$this->privateKeys = PrivateKey::ownedAndOnlySShKeys(['name'])->where('id', '!=', 0)->get();
|
||||
}
|
||||
if ($this->servers->isEmpty()) {
|
||||
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
|
|
@ -186,7 +186,7 @@ public function setServerType(string $type)
|
|||
|
||||
return $this->validateServer('localhost');
|
||||
} elseif ($this->selectedServerType === 'remote') {
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
$this->privateKeys = PrivateKey::ownedAndOnlySShKeys(['name'])->where('id', '!=', 0)->get();
|
||||
// Auto-select first key if available for better UX
|
||||
if ($this->privateKeys->count() > 0) {
|
||||
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Concerns;
|
||||
|
||||
trait SynchronizesModelData
|
||||
{
|
||||
/**
|
||||
* Define the mapping between component properties and model keys.
|
||||
*
|
||||
* @return array<string, string> Array mapping property names to model keys (e.g., ['content' => 'fileStorage.content'])
|
||||
*/
|
||||
abstract protected function getModelBindings(): array;
|
||||
|
||||
/**
|
||||
* Synchronize component properties TO the model.
|
||||
* Copies values from component properties to the model.
|
||||
*/
|
||||
protected function syncToModel(): void
|
||||
{
|
||||
foreach ($this->getModelBindings() as $property => $modelKey) {
|
||||
data_set($this, $modelKey, $this->{$property});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize component properties FROM the model.
|
||||
* Copies values from the model to component properties.
|
||||
*/
|
||||
protected function syncFromModel(): void
|
||||
{
|
||||
foreach ($this->getModelBindings() as $property => $modelKey) {
|
||||
$this->{$property} = data_get($this, $modelKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,11 @@
|
|||
namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Actions\Application\GenerateConfig;
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\Application;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
class General extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use SynchronizesModelData;
|
||||
|
||||
public string $applicationId;
|
||||
|
||||
|
|
@ -23,94 +22,136 @@ class General extends Component
|
|||
|
||||
public Collection $services;
|
||||
|
||||
#[Validate('required|regex:/^[a-zA-Z0-9\s\-_.\/:()]+$/')]
|
||||
public string $name;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $description = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $fqdn = null;
|
||||
|
||||
public string $git_repository;
|
||||
#[Validate(['required'])]
|
||||
public string $gitRepository;
|
||||
|
||||
public string $git_branch;
|
||||
#[Validate(['required'])]
|
||||
public string $gitBranch;
|
||||
|
||||
public ?string $git_commit_sha = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $gitCommitSha = null;
|
||||
|
||||
public ?string $install_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $installCommand = null;
|
||||
|
||||
public ?string $build_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $buildCommand = null;
|
||||
|
||||
public ?string $start_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $startCommand = null;
|
||||
|
||||
public string $build_pack;
|
||||
#[Validate(['required'])]
|
||||
public string $buildPack;
|
||||
|
||||
public string $static_image;
|
||||
#[Validate(['required'])]
|
||||
public string $staticImage;
|
||||
|
||||
public string $base_directory;
|
||||
#[Validate(['required'])]
|
||||
public string $baseDirectory;
|
||||
|
||||
public ?string $publish_directory = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $publishDirectory = null;
|
||||
|
||||
public ?string $ports_exposes = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $portsExposes = null;
|
||||
|
||||
public ?string $ports_mappings = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $portsMappings = null;
|
||||
|
||||
public ?string $custom_network_aliases = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $customNetworkAliases = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerfile = null;
|
||||
|
||||
public ?string $dockerfile_location = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerfileLocation = null;
|
||||
|
||||
public ?string $dockerfile_target_build = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerfileTargetBuild = null;
|
||||
|
||||
public ?string $docker_registry_image_name = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerRegistryImageName = null;
|
||||
|
||||
public ?string $docker_registry_image_tag = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerRegistryImageTag = null;
|
||||
|
||||
public ?string $docker_compose_location = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeLocation = null;
|
||||
|
||||
public ?string $docker_compose = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerCompose = null;
|
||||
|
||||
public ?string $docker_compose_raw = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeRaw = null;
|
||||
|
||||
public ?string $docker_compose_custom_start_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeCustomStartCommand = null;
|
||||
|
||||
public ?string $docker_compose_custom_build_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerComposeCustomBuildCommand = null;
|
||||
|
||||
public ?string $custom_labels = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
public ?string $custom_docker_run_options = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $preDeploymentCommand = null;
|
||||
|
||||
public ?string $pre_deployment_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $preDeploymentCommandContainer = null;
|
||||
|
||||
public ?string $pre_deployment_command_container = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $postDeploymentCommand = null;
|
||||
|
||||
public ?string $post_deployment_command = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $postDeploymentCommandContainer = null;
|
||||
|
||||
public ?string $post_deployment_command_container = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $customNginxConfiguration = null;
|
||||
|
||||
public ?string $custom_nginx_configuration = null;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isStatic = false;
|
||||
|
||||
public bool $is_static = false;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isSpa = false;
|
||||
|
||||
public bool $is_spa = false;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isBuildServerEnabled = false;
|
||||
|
||||
public bool $is_build_server_enabled = false;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isPreserveRepositoryEnabled = false;
|
||||
|
||||
public bool $is_preserve_repository_enabled = false;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isContainerLabelEscapeEnabled = true;
|
||||
|
||||
public bool $is_container_label_escape_enabled = true;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isContainerLabelReadonlyEnabled = false;
|
||||
|
||||
public bool $is_container_label_readonly_enabled = false;
|
||||
#[Validate(['boolean', 'required'])]
|
||||
public bool $isHttpBasicAuthEnabled = false;
|
||||
|
||||
public bool $is_http_basic_auth_enabled = false;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $httpBasicAuthUsername = null;
|
||||
|
||||
public ?string $http_basic_auth_username = null;
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $httpBasicAuthPassword = null;
|
||||
|
||||
public ?string $http_basic_auth_password = null;
|
||||
|
||||
public ?string $watch_paths = null;
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $watchPaths = null;
|
||||
|
||||
#[Validate(['string', 'required'])]
|
||||
public string $redirect;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public $customLabels;
|
||||
|
||||
public bool $labelsChanged = false;
|
||||
|
|
@ -141,46 +182,46 @@ protected function rules(): array
|
|||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'fqdn' => 'nullable',
|
||||
'git_repository' => 'required',
|
||||
'git_branch' => 'required',
|
||||
'git_commit_sha' => 'nullable',
|
||||
'install_command' => 'nullable',
|
||||
'build_command' => 'nullable',
|
||||
'start_command' => 'nullable',
|
||||
'build_pack' => 'required',
|
||||
'static_image' => 'required',
|
||||
'base_directory' => 'required',
|
||||
'publish_directory' => 'nullable',
|
||||
'ports_exposes' => 'required',
|
||||
'ports_mappings' => 'nullable',
|
||||
'custom_network_aliases' => 'nullable',
|
||||
'gitRepository' => 'required',
|
||||
'gitBranch' => 'required',
|
||||
'gitCommitSha' => 'nullable',
|
||||
'installCommand' => 'nullable',
|
||||
'buildCommand' => 'nullable',
|
||||
'startCommand' => 'nullable',
|
||||
'buildPack' => 'required',
|
||||
'staticImage' => 'required',
|
||||
'baseDirectory' => 'required',
|
||||
'publishDirectory' => 'nullable',
|
||||
'portsExposes' => 'required',
|
||||
'portsMappings' => 'nullable',
|
||||
'customNetworkAliases' => 'nullable',
|
||||
'dockerfile' => 'nullable',
|
||||
'docker_registry_image_name' => 'nullable',
|
||||
'docker_registry_image_tag' => 'nullable',
|
||||
'dockerfile_location' => 'nullable',
|
||||
'docker_compose_location' => 'nullable',
|
||||
'docker_compose' => 'nullable',
|
||||
'docker_compose_raw' => 'nullable',
|
||||
'dockerfile_target_build' => 'nullable',
|
||||
'docker_compose_custom_start_command' => 'nullable',
|
||||
'docker_compose_custom_build_command' => 'nullable',
|
||||
'custom_labels' => 'nullable',
|
||||
'custom_docker_run_options' => 'nullable',
|
||||
'pre_deployment_command' => 'nullable',
|
||||
'pre_deployment_command_container' => 'nullable',
|
||||
'post_deployment_command' => 'nullable',
|
||||
'post_deployment_command_container' => 'nullable',
|
||||
'custom_nginx_configuration' => 'nullable',
|
||||
'is_static' => 'boolean|required',
|
||||
'is_spa' => 'boolean|required',
|
||||
'is_build_server_enabled' => 'boolean|required',
|
||||
'is_container_label_escape_enabled' => 'boolean|required',
|
||||
'is_container_label_readonly_enabled' => 'boolean|required',
|
||||
'is_preserve_repository_enabled' => 'boolean|required',
|
||||
'is_http_basic_auth_enabled' => 'boolean|required',
|
||||
'http_basic_auth_username' => 'string|nullable',
|
||||
'http_basic_auth_password' => 'string|nullable',
|
||||
'watch_paths' => 'nullable',
|
||||
'dockerRegistryImageName' => 'nullable',
|
||||
'dockerRegistryImageTag' => 'nullable',
|
||||
'dockerfileLocation' => 'nullable',
|
||||
'dockerComposeLocation' => 'nullable',
|
||||
'dockerCompose' => 'nullable',
|
||||
'dockerComposeRaw' => 'nullable',
|
||||
'dockerfileTargetBuild' => 'nullable',
|
||||
'dockerComposeCustomStartCommand' => 'nullable',
|
||||
'dockerComposeCustomBuildCommand' => 'nullable',
|
||||
'customLabels' => 'nullable',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'preDeploymentCommand' => 'nullable',
|
||||
'preDeploymentCommandContainer' => 'nullable',
|
||||
'postDeploymentCommand' => 'nullable',
|
||||
'postDeploymentCommandContainer' => 'nullable',
|
||||
'customNginxConfiguration' => 'nullable',
|
||||
'isStatic' => 'boolean|required',
|
||||
'isSpa' => 'boolean|required',
|
||||
'isBuildServerEnabled' => 'boolean|required',
|
||||
'isContainerLabelEscapeEnabled' => 'boolean|required',
|
||||
'isContainerLabelReadonlyEnabled' => 'boolean|required',
|
||||
'isPreserveRepositoryEnabled' => 'boolean|required',
|
||||
'isHttpBasicAuthEnabled' => 'boolean|required',
|
||||
'httpBasicAuthUsername' => 'string|nullable',
|
||||
'httpBasicAuthPassword' => 'string|nullable',
|
||||
'watchPaths' => 'nullable',
|
||||
'redirect' => 'string|required',
|
||||
];
|
||||
}
|
||||
|
|
@ -193,26 +234,26 @@ protected function messages(): array
|
|||
'name.required' => 'The Name field is required.',
|
||||
'name.regex' => 'The Name may only contain letters, numbers, spaces, dashes (-), underscores (_), dots (.), slashes (/), colons (:), and parentheses ().',
|
||||
'description.regex' => 'The Description contains invalid characters. Only letters, numbers, spaces, and common punctuation (- _ . : / () \' " , ! ? @ # % & + = [] {} | ~ ` *) are allowed.',
|
||||
'git_repository.required' => 'The Git Repository field is required.',
|
||||
'git_branch.required' => 'The Git Branch field is required.',
|
||||
'build_pack.required' => 'The Build Pack field is required.',
|
||||
'static_image.required' => 'The Static Image field is required.',
|
||||
'base_directory.required' => 'The Base Directory field is required.',
|
||||
'ports_exposes.required' => 'The Exposed Ports field is required.',
|
||||
'is_static.required' => 'The Static setting is required.',
|
||||
'is_static.boolean' => 'The Static setting must be true or false.',
|
||||
'is_spa.required' => 'The SPA setting is required.',
|
||||
'is_spa.boolean' => 'The SPA setting must be true or false.',
|
||||
'is_build_server_enabled.required' => 'The Build Server setting is required.',
|
||||
'is_build_server_enabled.boolean' => 'The Build Server setting must be true or false.',
|
||||
'is_container_label_escape_enabled.required' => 'The Container Label Escape setting is required.',
|
||||
'is_container_label_escape_enabled.boolean' => 'The Container Label Escape setting must be true or false.',
|
||||
'is_container_label_readonly_enabled.required' => 'The Container Label Readonly setting is required.',
|
||||
'is_container_label_readonly_enabled.boolean' => 'The Container Label Readonly setting must be true or false.',
|
||||
'is_preserve_repository_enabled.required' => 'The Preserve Repository setting is required.',
|
||||
'is_preserve_repository_enabled.boolean' => 'The Preserve Repository setting must be true or false.',
|
||||
'is_http_basic_auth_enabled.required' => 'The HTTP Basic Auth setting is required.',
|
||||
'is_http_basic_auth_enabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
|
||||
'gitRepository.required' => 'The Git Repository field is required.',
|
||||
'gitBranch.required' => 'The Git Branch field is required.',
|
||||
'buildPack.required' => 'The Build Pack field is required.',
|
||||
'staticImage.required' => 'The Static Image field is required.',
|
||||
'baseDirectory.required' => 'The Base Directory field is required.',
|
||||
'portsExposes.required' => 'The Exposed Ports field is required.',
|
||||
'isStatic.required' => 'The Static setting is required.',
|
||||
'isStatic.boolean' => 'The Static setting must be true or false.',
|
||||
'isSpa.required' => 'The SPA setting is required.',
|
||||
'isSpa.boolean' => 'The SPA setting must be true or false.',
|
||||
'isBuildServerEnabled.required' => 'The Build Server setting is required.',
|
||||
'isBuildServerEnabled.boolean' => 'The Build Server setting must be true or false.',
|
||||
'isContainerLabelEscapeEnabled.required' => 'The Container Label Escape setting is required.',
|
||||
'isContainerLabelEscapeEnabled.boolean' => 'The Container Label Escape setting must be true or false.',
|
||||
'isContainerLabelReadonlyEnabled.required' => 'The Container Label Readonly setting is required.',
|
||||
'isContainerLabelReadonlyEnabled.boolean' => 'The Container Label Readonly setting must be true or false.',
|
||||
'isPreserveRepositoryEnabled.required' => 'The Preserve Repository setting is required.',
|
||||
'isPreserveRepositoryEnabled.boolean' => 'The Preserve Repository setting must be true or false.',
|
||||
'isHttpBasicAuthEnabled.required' => 'The HTTP Basic Auth setting is required.',
|
||||
'isHttpBasicAuthEnabled.boolean' => 'The HTTP Basic Auth setting must be true or false.',
|
||||
'redirect.required' => 'The Redirect setting is required.',
|
||||
'redirect.string' => 'The Redirect setting must be a string.',
|
||||
]
|
||||
|
|
@ -220,43 +261,43 @@ protected function messages(): array
|
|||
}
|
||||
|
||||
protected $validationAttributes = [
|
||||
'application.name' => 'name',
|
||||
'application.description' => 'description',
|
||||
'application.fqdn' => 'FQDN',
|
||||
'application.git_repository' => 'Git repository',
|
||||
'application.git_branch' => 'Git branch',
|
||||
'application.git_commit_sha' => 'Git commit SHA',
|
||||
'application.install_command' => 'Install command',
|
||||
'application.build_command' => 'Build command',
|
||||
'application.start_command' => 'Start command',
|
||||
'application.build_pack' => 'Build pack',
|
||||
'application.static_image' => 'Static image',
|
||||
'application.base_directory' => 'Base directory',
|
||||
'application.publish_directory' => 'Publish directory',
|
||||
'application.ports_exposes' => 'Ports exposes',
|
||||
'application.ports_mappings' => 'Ports mappings',
|
||||
'application.dockerfile' => 'Dockerfile',
|
||||
'application.docker_registry_image_name' => 'Docker registry image name',
|
||||
'application.docker_registry_image_tag' => 'Docker registry image tag',
|
||||
'application.dockerfile_location' => 'Dockerfile location',
|
||||
'application.docker_compose_location' => 'Docker compose location',
|
||||
'application.docker_compose' => 'Docker compose',
|
||||
'application.docker_compose_raw' => 'Docker compose raw',
|
||||
'application.custom_labels' => 'Custom labels',
|
||||
'application.dockerfile_target_build' => 'Dockerfile target build',
|
||||
'application.custom_docker_run_options' => 'Custom docker run commands',
|
||||
'application.custom_network_aliases' => 'Custom docker network aliases',
|
||||
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
|
||||
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
|
||||
'application.custom_nginx_configuration' => 'Custom Nginx configuration',
|
||||
'application.settings.is_static' => 'Is static',
|
||||
'application.settings.is_spa' => 'Is SPA',
|
||||
'application.settings.is_build_server_enabled' => 'Is build server enabled',
|
||||
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
|
||||
'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly',
|
||||
'application.settings.is_preserve_repository_enabled' => 'Is preserve repository enabled',
|
||||
'application.watch_paths' => 'Watch paths',
|
||||
'application.redirect' => 'Redirect',
|
||||
'name' => 'name',
|
||||
'description' => 'description',
|
||||
'fqdn' => 'FQDN',
|
||||
'gitRepository' => 'Git repository',
|
||||
'gitBranch' => 'Git branch',
|
||||
'gitCommitSha' => 'Git commit SHA',
|
||||
'installCommand' => 'Install command',
|
||||
'buildCommand' => 'Build command',
|
||||
'startCommand' => 'Start command',
|
||||
'buildPack' => 'Build pack',
|
||||
'staticImage' => 'Static image',
|
||||
'baseDirectory' => 'Base directory',
|
||||
'publishDirectory' => 'Publish directory',
|
||||
'portsExposes' => 'Ports exposes',
|
||||
'portsMappings' => 'Ports mappings',
|
||||
'dockerfile' => 'Dockerfile',
|
||||
'dockerRegistryImageName' => 'Docker registry image name',
|
||||
'dockerRegistryImageTag' => 'Docker registry image tag',
|
||||
'dockerfileLocation' => 'Dockerfile location',
|
||||
'dockerComposeLocation' => 'Docker compose location',
|
||||
'dockerCompose' => 'Docker compose',
|
||||
'dockerComposeRaw' => 'Docker compose raw',
|
||||
'customLabels' => 'Custom labels',
|
||||
'dockerfileTargetBuild' => 'Dockerfile target build',
|
||||
'customDockerRunOptions' => 'Custom docker run commands',
|
||||
'customNetworkAliases' => 'Custom docker network aliases',
|
||||
'dockerComposeCustomStartCommand' => 'Docker compose custom start command',
|
||||
'dockerComposeCustomBuildCommand' => 'Docker compose custom build command',
|
||||
'customNginxConfiguration' => 'Custom Nginx configuration',
|
||||
'isStatic' => 'Is static',
|
||||
'isSpa' => 'Is SPA',
|
||||
'isBuildServerEnabled' => 'Is build server enabled',
|
||||
'isContainerLabelEscapeEnabled' => 'Is container label escape enabled',
|
||||
'isContainerLabelReadonlyEnabled' => 'Is container label readonly',
|
||||
'isPreserveRepositoryEnabled' => 'Is preserve repository enabled',
|
||||
'watchPaths' => 'Watch paths',
|
||||
'redirect' => 'Redirect',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
|
@ -266,14 +307,14 @@ public function mount()
|
|||
if (is_null($this->parsedServices) || empty($this->parsedServices)) {
|
||||
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
||||
// Still sync data even if parse fails, so form fields are populated
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
// Still sync data even on error, so form fields are populated
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
}
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
// Only update if user has permission
|
||||
|
|
@ -325,57 +366,114 @@ public function mount()
|
|||
|
||||
// Sync data from model to properties at the END, after all business logic
|
||||
// This ensures any modifications to $this->application during mount() are reflected in properties
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
return [
|
||||
'name' => 'application.name',
|
||||
'description' => 'application.description',
|
||||
'fqdn' => 'application.fqdn',
|
||||
'git_repository' => 'application.git_repository',
|
||||
'git_branch' => 'application.git_branch',
|
||||
'git_commit_sha' => 'application.git_commit_sha',
|
||||
'install_command' => 'application.install_command',
|
||||
'build_command' => 'application.build_command',
|
||||
'start_command' => 'application.start_command',
|
||||
'build_pack' => 'application.build_pack',
|
||||
'static_image' => 'application.static_image',
|
||||
'base_directory' => 'application.base_directory',
|
||||
'publish_directory' => 'application.publish_directory',
|
||||
'ports_exposes' => 'application.ports_exposes',
|
||||
'ports_mappings' => 'application.ports_mappings',
|
||||
'custom_network_aliases' => 'application.custom_network_aliases',
|
||||
'dockerfile' => 'application.dockerfile',
|
||||
'dockerfile_location' => 'application.dockerfile_location',
|
||||
'dockerfile_target_build' => 'application.dockerfile_target_build',
|
||||
'docker_registry_image_name' => 'application.docker_registry_image_name',
|
||||
'docker_registry_image_tag' => 'application.docker_registry_image_tag',
|
||||
'docker_compose_location' => 'application.docker_compose_location',
|
||||
'docker_compose' => 'application.docker_compose',
|
||||
'docker_compose_raw' => 'application.docker_compose_raw',
|
||||
'docker_compose_custom_start_command' => 'application.docker_compose_custom_start_command',
|
||||
'docker_compose_custom_build_command' => 'application.docker_compose_custom_build_command',
|
||||
'custom_labels' => 'application.custom_labels',
|
||||
'custom_docker_run_options' => 'application.custom_docker_run_options',
|
||||
'pre_deployment_command' => 'application.pre_deployment_command',
|
||||
'pre_deployment_command_container' => 'application.pre_deployment_command_container',
|
||||
'post_deployment_command' => 'application.post_deployment_command',
|
||||
'post_deployment_command_container' => 'application.post_deployment_command_container',
|
||||
'custom_nginx_configuration' => 'application.custom_nginx_configuration',
|
||||
'is_static' => 'application.settings.is_static',
|
||||
'is_spa' => 'application.settings.is_spa',
|
||||
'is_build_server_enabled' => 'application.settings.is_build_server_enabled',
|
||||
'is_preserve_repository_enabled' => 'application.settings.is_preserve_repository_enabled',
|
||||
'is_container_label_escape_enabled' => 'application.settings.is_container_label_escape_enabled',
|
||||
'is_container_label_readonly_enabled' => 'application.settings.is_container_label_readonly_enabled',
|
||||
'is_http_basic_auth_enabled' => 'application.is_http_basic_auth_enabled',
|
||||
'http_basic_auth_username' => 'application.http_basic_auth_username',
|
||||
'http_basic_auth_password' => 'application.http_basic_auth_password',
|
||||
'watch_paths' => 'application.watch_paths',
|
||||
'redirect' => 'application.redirect',
|
||||
];
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Application properties
|
||||
$this->application->name = $this->name;
|
||||
$this->application->description = $this->description;
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
$this->application->git_repository = $this->gitRepository;
|
||||
$this->application->git_branch = $this->gitBranch;
|
||||
$this->application->git_commit_sha = $this->gitCommitSha;
|
||||
$this->application->install_command = $this->installCommand;
|
||||
$this->application->build_command = $this->buildCommand;
|
||||
$this->application->start_command = $this->startCommand;
|
||||
$this->application->build_pack = $this->buildPack;
|
||||
$this->application->static_image = $this->staticImage;
|
||||
$this->application->base_directory = $this->baseDirectory;
|
||||
$this->application->publish_directory = $this->publishDirectory;
|
||||
$this->application->ports_exposes = $this->portsExposes;
|
||||
$this->application->ports_mappings = $this->portsMappings;
|
||||
$this->application->custom_network_aliases = $this->customNetworkAliases;
|
||||
$this->application->dockerfile = $this->dockerfile;
|
||||
$this->application->dockerfile_location = $this->dockerfileLocation;
|
||||
$this->application->dockerfile_target_build = $this->dockerfileTargetBuild;
|
||||
$this->application->docker_registry_image_name = $this->dockerRegistryImageName;
|
||||
$this->application->docker_registry_image_tag = $this->dockerRegistryImageTag;
|
||||
$this->application->docker_compose_location = $this->dockerComposeLocation;
|
||||
$this->application->docker_compose = $this->dockerCompose;
|
||||
$this->application->docker_compose_raw = $this->dockerComposeRaw;
|
||||
$this->application->docker_compose_custom_start_command = $this->dockerComposeCustomStartCommand;
|
||||
$this->application->docker_compose_custom_build_command = $this->dockerComposeCustomBuildCommand;
|
||||
$this->application->custom_labels = is_null($this->customLabels)
|
||||
? null
|
||||
: base64_encode($this->customLabels);
|
||||
$this->application->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
$this->application->pre_deployment_command = $this->preDeploymentCommand;
|
||||
$this->application->pre_deployment_command_container = $this->preDeploymentCommandContainer;
|
||||
$this->application->post_deployment_command = $this->postDeploymentCommand;
|
||||
$this->application->post_deployment_command_container = $this->postDeploymentCommandContainer;
|
||||
$this->application->custom_nginx_configuration = $this->customNginxConfiguration;
|
||||
$this->application->is_http_basic_auth_enabled = $this->isHttpBasicAuthEnabled;
|
||||
$this->application->http_basic_auth_username = $this->httpBasicAuthUsername;
|
||||
$this->application->http_basic_auth_password = $this->httpBasicAuthPassword;
|
||||
$this->application->watch_paths = $this->watchPaths;
|
||||
$this->application->redirect = $this->redirect;
|
||||
|
||||
// Application settings properties
|
||||
$this->application->settings->is_static = $this->isStatic;
|
||||
$this->application->settings->is_spa = $this->isSpa;
|
||||
$this->application->settings->is_build_server_enabled = $this->isBuildServerEnabled;
|
||||
$this->application->settings->is_preserve_repository_enabled = $this->isPreserveRepositoryEnabled;
|
||||
$this->application->settings->is_container_label_escape_enabled = $this->isContainerLabelEscapeEnabled;
|
||||
$this->application->settings->is_container_label_readonly_enabled = $this->isContainerLabelReadonlyEnabled;
|
||||
|
||||
$this->application->settings->save();
|
||||
} else {
|
||||
// From model to properties
|
||||
$this->name = $this->application->name;
|
||||
$this->description = $this->application->description;
|
||||
$this->fqdn = $this->application->fqdn;
|
||||
$this->gitRepository = $this->application->git_repository;
|
||||
$this->gitBranch = $this->application->git_branch;
|
||||
$this->gitCommitSha = $this->application->git_commit_sha;
|
||||
$this->installCommand = $this->application->install_command;
|
||||
$this->buildCommand = $this->application->build_command;
|
||||
$this->startCommand = $this->application->start_command;
|
||||
$this->buildPack = $this->application->build_pack;
|
||||
$this->staticImage = $this->application->static_image;
|
||||
$this->baseDirectory = $this->application->base_directory;
|
||||
$this->publishDirectory = $this->application->publish_directory;
|
||||
$this->portsExposes = $this->application->ports_exposes;
|
||||
$this->portsMappings = $this->application->ports_mappings;
|
||||
$this->customNetworkAliases = $this->application->custom_network_aliases;
|
||||
$this->dockerfile = $this->application->dockerfile;
|
||||
$this->dockerfileLocation = $this->application->dockerfile_location;
|
||||
$this->dockerfileTargetBuild = $this->application->dockerfile_target_build;
|
||||
$this->dockerRegistryImageName = $this->application->docker_registry_image_name;
|
||||
$this->dockerRegistryImageTag = $this->application->docker_registry_image_tag;
|
||||
$this->dockerComposeLocation = $this->application->docker_compose_location;
|
||||
$this->dockerCompose = $this->application->docker_compose;
|
||||
$this->dockerComposeRaw = $this->application->docker_compose_raw;
|
||||
$this->dockerComposeCustomStartCommand = $this->application->docker_compose_custom_start_command;
|
||||
$this->dockerComposeCustomBuildCommand = $this->application->docker_compose_custom_build_command;
|
||||
$this->customLabels = $this->application->parseContainerLabels();
|
||||
$this->customDockerRunOptions = $this->application->custom_docker_run_options;
|
||||
$this->preDeploymentCommand = $this->application->pre_deployment_command;
|
||||
$this->preDeploymentCommandContainer = $this->application->pre_deployment_command_container;
|
||||
$this->postDeploymentCommand = $this->application->post_deployment_command;
|
||||
$this->postDeploymentCommandContainer = $this->application->post_deployment_command_container;
|
||||
$this->customNginxConfiguration = $this->application->custom_nginx_configuration;
|
||||
$this->isHttpBasicAuthEnabled = $this->application->is_http_basic_auth_enabled;
|
||||
$this->httpBasicAuthUsername = $this->application->http_basic_auth_username;
|
||||
$this->httpBasicAuthPassword = $this->application->http_basic_auth_password;
|
||||
$this->watchPaths = $this->application->watch_paths;
|
||||
$this->redirect = $this->application->redirect;
|
||||
|
||||
// Application settings properties
|
||||
$this->isStatic = $this->application->settings->is_static;
|
||||
$this->isSpa = $this->application->settings->is_spa;
|
||||
$this->isBuildServerEnabled = $this->application->settings->is_build_server_enabled;
|
||||
$this->isPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled;
|
||||
$this->isContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$this->isContainerLabelReadonlyEnabled = $this->application->settings->is_container_label_readonly_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
|
|
@ -386,33 +484,36 @@ public function instantSave()
|
|||
$oldPortsExposes = $this->application->ports_exposes;
|
||||
$oldIsContainerLabelEscapeEnabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
$oldIsPreserveRepositoryEnabled = $this->application->settings->is_preserve_repository_enabled;
|
||||
$oldIsSpa = $this->application->settings->is_spa;
|
||||
$oldIsHttpBasicAuthEnabled = $this->application->is_http_basic_auth_enabled;
|
||||
|
||||
$this->syncToModel();
|
||||
$this->syncData(toModel: true);
|
||||
|
||||
if ($this->application->settings->isDirty('is_spa')) {
|
||||
$this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
|
||||
if ($oldIsSpa !== $this->isSpa) {
|
||||
$this->generateNginxConfiguration($this->isSpa ? 'spa' : 'static');
|
||||
}
|
||||
if ($this->application->isDirty('is_http_basic_auth_enabled')) {
|
||||
if ($oldIsHttpBasicAuthEnabled !== $this->isHttpBasicAuthEnabled) {
|
||||
$this->application->save();
|
||||
}
|
||||
$this->application->settings->save();
|
||||
|
||||
$this->dispatch('success', 'Settings saved.');
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
|
||||
$this->syncData();
|
||||
|
||||
// If port_exposes changed, reset default labels
|
||||
if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
|
||||
if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
if ($oldIsPreserveRepositoryEnabled !== $this->is_preserve_repository_enabled) {
|
||||
if ($this->is_preserve_repository_enabled === false) {
|
||||
if ($oldIsPreserveRepositoryEnabled !== $this->isPreserveRepositoryEnabled) {
|
||||
if ($this->isPreserveRepositoryEnabled === false) {
|
||||
$this->application->fileStorages->each(function ($storage) {
|
||||
$storage->is_based_on_git = $this->is_preserve_repository_enabled;
|
||||
$storage->is_based_on_git = $this->isPreserveRepositoryEnabled;
|
||||
$storage->save();
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($this->is_container_label_readonly_enabled) {
|
||||
if ($this->isContainerLabelReadonlyEnabled) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -438,6 +539,11 @@ public function loadComposeFile($isInit = false, $showToast = true)
|
|||
|
||||
// Refresh parsedServiceDomains to reflect any changes in docker_compose_domains
|
||||
$this->application->refresh();
|
||||
|
||||
// Sync the docker_compose_raw from the model to the component property
|
||||
// This ensures the Monaco editor displays the loaded compose file
|
||||
$this->syncData();
|
||||
|
||||
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
|
||||
// Convert service names with dots and dashes to use underscores for HTML form binding
|
||||
$sanitizedDomains = [];
|
||||
|
|
@ -502,7 +608,7 @@ public function generateDomain(string $serviceName)
|
|||
|
||||
public function updatedBaseDirectory()
|
||||
{
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
$this->loadComposeFile();
|
||||
}
|
||||
}
|
||||
|
|
@ -522,24 +628,24 @@ public function updatedBuildPack()
|
|||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
// User doesn't have permission, revert the change and return
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync property to model before checking/modifying
|
||||
$this->syncToModel();
|
||||
$this->syncData(toModel: true);
|
||||
|
||||
if ($this->build_pack !== 'nixpacks') {
|
||||
$this->is_static = false;
|
||||
if ($this->buildPack !== 'nixpacks') {
|
||||
$this->isStatic = false;
|
||||
$this->application->settings->is_static = false;
|
||||
$this->application->settings->save();
|
||||
} else {
|
||||
$this->ports_exposes = 3000;
|
||||
$this->application->ports_exposes = 3000;
|
||||
$this->portsExposes = '3000';
|
||||
$this->application->ports_exposes = '3000';
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
// Only update if user has permission
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
|
@ -562,9 +668,9 @@ public function updatedBuildPack()
|
|||
$this->application->environment_variables_preview()->where('key', 'LIKE', 'SERVICE_URL_%')->delete();
|
||||
}
|
||||
}
|
||||
if ($this->build_pack === 'static') {
|
||||
$this->ports_exposes = 80;
|
||||
$this->application->ports_exposes = 80;
|
||||
if ($this->buildPack === 'static') {
|
||||
$this->portsExposes = '80';
|
||||
$this->application->ports_exposes = '80';
|
||||
$this->resetDefaultLabels(false);
|
||||
$this->generateNginxConfiguration();
|
||||
}
|
||||
|
|
@ -581,10 +687,10 @@ public function getWildcardDomain()
|
|||
if ($server) {
|
||||
$fqdn = generateUrl(server: $server, random: $this->application->uuid);
|
||||
$this->fqdn = $fqdn;
|
||||
$this->syncToModel();
|
||||
$this->syncData(toModel: true);
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
$this->resetDefaultLabels();
|
||||
$this->dispatch('success', 'Wildcard domain generated.');
|
||||
}
|
||||
|
|
@ -598,11 +704,11 @@ public function generateNginxConfiguration($type = 'static')
|
|||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
$this->custom_nginx_configuration = defaultNginxConfiguration($type);
|
||||
$this->syncToModel();
|
||||
$this->customNginxConfiguration = defaultNginxConfiguration($type);
|
||||
$this->syncData(toModel: true);
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
$this->dispatch('success', 'Nginx configuration generated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
|
@ -612,16 +718,15 @@ public function generateNginxConfiguration($type = 'static')
|
|||
public function resetDefaultLabels($manualReset = false)
|
||||
{
|
||||
try {
|
||||
if (! $this->is_container_label_readonly_enabled && ! $manualReset) {
|
||||
if (! $this->isContainerLabelReadonlyEnabled && ! $manualReset) {
|
||||
return;
|
||||
}
|
||||
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
|
||||
$this->custom_labels = base64_encode($this->customLabels);
|
||||
$this->syncToModel();
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$this->syncData();
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
$this->loadComposeFile(showToast: false);
|
||||
}
|
||||
$this->dispatch('configurationChanged');
|
||||
|
|
@ -717,7 +822,7 @@ public function submit($showToaster = true)
|
|||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
|
||||
$this->syncToModel();
|
||||
$this->syncData(toModel: true);
|
||||
|
||||
if ($this->application->isDirty('redirect')) {
|
||||
$this->setRedirect();
|
||||
|
|
@ -737,42 +842,42 @@ public function submit($showToaster = true)
|
|||
$this->application->save();
|
||||
}
|
||||
|
||||
if ($this->build_pack === 'dockercompose' && $oldDockerComposeLocation !== $this->docker_compose_location) {
|
||||
if ($this->buildPack === 'dockercompose' && $oldDockerComposeLocation !== $this->dockerComposeLocation) {
|
||||
$compose_return = $this->loadComposeFile(showToast: false);
|
||||
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($oldPortsExposes !== $this->ports_exposes || $oldIsContainerLabelEscapeEnabled !== $this->is_container_label_escape_enabled) {
|
||||
if ($oldPortsExposes !== $this->portsExposes || $oldIsContainerLabelEscapeEnabled !== $this->isContainerLabelEscapeEnabled) {
|
||||
$this->resetDefaultLabels();
|
||||
}
|
||||
if ($this->build_pack === 'dockerimage') {
|
||||
if ($this->buildPack === 'dockerimage') {
|
||||
$this->validate([
|
||||
'docker_registry_image_name' => 'required',
|
||||
'dockerRegistryImageName' => 'required',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->custom_docker_run_options) {
|
||||
$this->custom_docker_run_options = str($this->custom_docker_run_options)->trim()->toString();
|
||||
$this->application->custom_docker_run_options = $this->custom_docker_run_options;
|
||||
if ($this->customDockerRunOptions) {
|
||||
$this->customDockerRunOptions = str($this->customDockerRunOptions)->trim()->toString();
|
||||
$this->application->custom_docker_run_options = $this->customDockerRunOptions;
|
||||
}
|
||||
if ($this->dockerfile) {
|
||||
$port = get_port_from_dockerfile($this->dockerfile);
|
||||
if ($port && ! $this->ports_exposes) {
|
||||
$this->ports_exposes = $port;
|
||||
if ($port && ! $this->portsExposes) {
|
||||
$this->portsExposes = $port;
|
||||
$this->application->ports_exposes = $port;
|
||||
}
|
||||
}
|
||||
if ($this->base_directory && $this->base_directory !== '/') {
|
||||
$this->base_directory = rtrim($this->base_directory, '/');
|
||||
$this->application->base_directory = $this->base_directory;
|
||||
if ($this->baseDirectory && $this->baseDirectory !== '/') {
|
||||
$this->baseDirectory = rtrim($this->baseDirectory, '/');
|
||||
$this->application->base_directory = $this->baseDirectory;
|
||||
}
|
||||
if ($this->publish_directory && $this->publish_directory !== '/') {
|
||||
$this->publish_directory = rtrim($this->publish_directory, '/');
|
||||
$this->application->publish_directory = $this->publish_directory;
|
||||
if ($this->publishDirectory && $this->publishDirectory !== '/') {
|
||||
$this->publishDirectory = rtrim($this->publishDirectory, '/');
|
||||
$this->application->publish_directory = $this->publishDirectory;
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if ($this->buildPack === 'dockercompose') {
|
||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||
if ($this->application->isDirty('docker_compose_domains')) {
|
||||
foreach ($this->parsedServiceDomains as $service) {
|
||||
|
|
@ -804,11 +909,11 @@ public function submit($showToaster = true)
|
|||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
|
||||
} catch (\Throwable $e) {
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,11 @@ public function checkStatus()
|
|||
}
|
||||
}
|
||||
|
||||
public function manualCheckStatus()
|
||||
{
|
||||
$this->checkStatus();
|
||||
}
|
||||
|
||||
public function force_deploy_without_cache()
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
|
|
|||
|
|
@ -62,6 +62,11 @@ public function checkStatus()
|
|||
}
|
||||
}
|
||||
|
||||
public function manualCheckStatus()
|
||||
{
|
||||
$this->checkStatus();
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
|
|
|
|||
|
|
@ -18,20 +18,7 @@ class Index extends Component
|
|||
public function mount()
|
||||
{
|
||||
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeam()->get()->map(function ($project) {
|
||||
$project->settingsRoute = route('project.edit', ['project_uuid' => $project->uuid]);
|
||||
$project->canUpdate = auth()->user()->can('update', $project);
|
||||
$project->canCreateResource = auth()->user()->can('createAnyResource');
|
||||
$firstEnvironment = $project->environments->first();
|
||||
$project->addResourceRoute = $firstEnvironment
|
||||
? route('project.resource.create', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $firstEnvironment->uuid,
|
||||
])
|
||||
: null;
|
||||
|
||||
return $project;
|
||||
});
|
||||
$this->projects = Project::ownedByCurrentTeam()->get();
|
||||
$this->servers = Server::ownedByCurrentTeam()->count();
|
||||
}
|
||||
|
||||
|
|
@ -39,11 +26,4 @@ public function render()
|
|||
{
|
||||
return view('livewire.project.index');
|
||||
}
|
||||
|
||||
public function navigateToProject($projectUuid)
|
||||
{
|
||||
$project = collect($this->projects)->firstWhere('uuid', $projectUuid);
|
||||
|
||||
return $this->redirect($project->navigateTo(), navigate: false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -2,14 +2,16 @@
|
|||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class EditDomain extends Component
|
||||
{
|
||||
use SynchronizesModelData;
|
||||
use AuthorizesRequests;
|
||||
|
||||
public $applicationId;
|
||||
|
||||
public ServiceApplication $application;
|
||||
|
|
@ -20,6 +22,13 @@ class EditDomain extends Component
|
|||
|
||||
public $forceSaveDomains = false;
|
||||
|
||||
public $showPortWarningModal = false;
|
||||
|
||||
public $forceRemovePort = false;
|
||||
|
||||
public $requiredPort = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $fqdn = null;
|
||||
|
||||
protected $rules = [
|
||||
|
|
@ -28,16 +37,25 @@ class EditDomain extends Component
|
|||
|
||||
public function mount()
|
||||
{
|
||||
$this->application = ServiceApplication::query()->findOrFail($this->applicationId);
|
||||
$this->application = ServiceApplication::ownedByCurrentTeam()->findOrFail($this->applicationId);
|
||||
$this->authorize('view', $this->application);
|
||||
$this->syncFromModel();
|
||||
$this->requiredPort = $this->application->service->getRequiredPort();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
return [
|
||||
'fqdn' => 'application.fqdn',
|
||||
];
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync to model
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
|
||||
$this->application->save();
|
||||
} else {
|
||||
// Sync from model
|
||||
$this->fqdn = $this->application->fqdn;
|
||||
}
|
||||
}
|
||||
|
||||
public function confirmDomainUsage()
|
||||
|
|
@ -47,6 +65,19 @@ public function confirmDomainUsage()
|
|||
$this->submit();
|
||||
}
|
||||
|
||||
public function confirmRemovePort()
|
||||
{
|
||||
$this->forceRemovePort = true;
|
||||
$this->showPortWarningModal = false;
|
||||
$this->submit();
|
||||
}
|
||||
|
||||
public function cancelRemovePort()
|
||||
{
|
||||
$this->showPortWarningModal = false;
|
||||
$this->syncData(); // Reset to original FQDN
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
|
|
@ -64,8 +95,8 @@ public function submit()
|
|||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
// Sync to model for domain conflict check
|
||||
$this->syncToModel();
|
||||
// Sync to model for domain conflict check (without validation)
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
// Check for domain conflicts if not forcing save
|
||||
if (! $this->forceSaveDomains) {
|
||||
$result = checkDomainUsage(resource: $this->application);
|
||||
|
|
@ -80,10 +111,45 @@ public function submit()
|
|||
$this->forceSaveDomains = false;
|
||||
}
|
||||
|
||||
// Check for required port
|
||||
if (! $this->forceRemovePort) {
|
||||
$service = $this->application->service;
|
||||
$requiredPort = $service->getRequiredPort();
|
||||
|
||||
if ($requiredPort !== null) {
|
||||
// Check if all FQDNs have a port
|
||||
$fqdns = str($this->fqdn)->trim()->explode(',');
|
||||
$missingPort = false;
|
||||
|
||||
foreach ($fqdns as $fqdn) {
|
||||
$fqdn = trim($fqdn);
|
||||
if (empty($fqdn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$port = ServiceApplication::extractPortFromUrl($fqdn);
|
||||
if ($port === null) {
|
||||
$missingPort = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($missingPort) {
|
||||
$this->requiredPort = $requiredPort;
|
||||
$this->showPortWarningModal = true;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Reset the force flag after using it
|
||||
$this->forceRemovePort = false;
|
||||
}
|
||||
|
||||
$this->validate();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
updateCompose($this->application);
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||
|
|
@ -96,7 +162,7 @@ public function submit()
|
|||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\LocalFileVolume;
|
||||
|
|
@ -19,11 +18,12 @@
|
|||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class FileStorage extends Component
|
||||
{
|
||||
use AuthorizesRequests, SynchronizesModelData;
|
||||
use AuthorizesRequests;
|
||||
|
||||
public LocalFileVolume $fileStorage;
|
||||
|
||||
|
|
@ -37,8 +37,10 @@ class FileStorage extends Component
|
|||
|
||||
public bool $isReadOnly = false;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $content = null;
|
||||
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $isBasedOnGit = false;
|
||||
|
||||
protected $rules = [
|
||||
|
|
@ -61,15 +63,24 @@ public function mount()
|
|||
}
|
||||
|
||||
$this->isReadOnly = $this->fileStorage->isReadOnlyVolume();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
return [
|
||||
'content' => 'fileStorage.content',
|
||||
'isBasedOnGit' => 'fileStorage.is_based_on_git',
|
||||
];
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync to model
|
||||
$this->fileStorage->content = $this->content;
|
||||
$this->fileStorage->is_based_on_git = $this->isBasedOnGit;
|
||||
|
||||
$this->fileStorage->save();
|
||||
} else {
|
||||
// Sync from model
|
||||
$this->content = $this->fileStorage->content;
|
||||
$this->isBasedOnGit = $this->fileStorage->is_based_on_git;
|
||||
}
|
||||
}
|
||||
|
||||
public function convertToDirectory()
|
||||
|
|
@ -96,7 +107,7 @@ public function loadStorageOnServer()
|
|||
$this->authorize('update', $this->resource);
|
||||
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
$this->dispatch('success', 'File storage loaded from server.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
|
@ -165,14 +176,16 @@ public function submit()
|
|||
if ($this->fileStorage->is_directory) {
|
||||
$this->content = null;
|
||||
}
|
||||
$this->syncToModel();
|
||||
// Sync component properties to model
|
||||
$this->fileStorage->content = $this->content;
|
||||
$this->fileStorage->is_based_on_git = $this->isBasedOnGit;
|
||||
$this->fileStorage->save();
|
||||
$this->fileStorage->saveStorageOnServer();
|
||||
$this->dispatch('success', 'File updated.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->fileStorage->setRawAttributes($original);
|
||||
$this->fileStorage->save();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ public function checkStatus()
|
|||
}
|
||||
}
|
||||
|
||||
public function manualCheckStatus()
|
||||
{
|
||||
$this->checkStatus();
|
||||
}
|
||||
|
||||
public function serviceChecked()
|
||||
{
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2,20 +2,19 @@
|
|||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class ServiceApplicationView extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use SynchronizesModelData;
|
||||
|
||||
public ServiceApplication $application;
|
||||
|
||||
|
|
@ -31,20 +30,34 @@ class ServiceApplicationView extends Component
|
|||
|
||||
public $forceSaveDomains = false;
|
||||
|
||||
public $showPortWarningModal = false;
|
||||
|
||||
public $forceRemovePort = false;
|
||||
|
||||
public $requiredPort = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $humanName = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $description = null;
|
||||
|
||||
#[Validate(['nullable'])]
|
||||
public ?string $fqdn = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $image = null;
|
||||
|
||||
#[Validate(['required', 'boolean'])]
|
||||
public bool $excludeFromStatus = false;
|
||||
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $isGzipEnabled = false;
|
||||
|
||||
#[Validate(['nullable', 'boolean'])]
|
||||
public bool $isStripprefixEnabled = false;
|
||||
|
||||
protected $rules = [
|
||||
|
|
@ -79,7 +92,15 @@ public function instantSaveAdvanced()
|
|||
|
||||
return;
|
||||
}
|
||||
$this->syncToModel();
|
||||
// Sync component properties to model
|
||||
$this->application->human_name = $this->humanName;
|
||||
$this->application->description = $this->description;
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
$this->application->image = $this->image;
|
||||
$this->application->exclude_from_status = $this->excludeFromStatus;
|
||||
$this->application->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->application->is_gzip_enabled = $this->isGzipEnabled;
|
||||
$this->application->is_stripprefix_enabled = $this->isStripprefixEnabled;
|
||||
$this->application->save();
|
||||
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -114,24 +135,53 @@ public function mount()
|
|||
try {
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->authorize('view', $this->application);
|
||||
$this->syncFromModel();
|
||||
$this->requiredPort = $this->application->service->getRequiredPort();
|
||||
$this->syncData();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getModelBindings(): array
|
||||
public function confirmRemovePort()
|
||||
{
|
||||
return [
|
||||
'humanName' => 'application.human_name',
|
||||
'description' => 'application.description',
|
||||
'fqdn' => 'application.fqdn',
|
||||
'image' => 'application.image',
|
||||
'excludeFromStatus' => 'application.exclude_from_status',
|
||||
'isLogDrainEnabled' => 'application.is_log_drain_enabled',
|
||||
'isGzipEnabled' => 'application.is_gzip_enabled',
|
||||
'isStripprefixEnabled' => 'application.is_stripprefix_enabled',
|
||||
];
|
||||
$this->forceRemovePort = true;
|
||||
$this->showPortWarningModal = false;
|
||||
$this->submit();
|
||||
}
|
||||
|
||||
public function cancelRemovePort()
|
||||
{
|
||||
$this->showPortWarningModal = false;
|
||||
$this->syncData(); // Reset to original FQDN
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync to model
|
||||
$this->application->human_name = $this->humanName;
|
||||
$this->application->description = $this->description;
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
$this->application->image = $this->image;
|
||||
$this->application->exclude_from_status = $this->excludeFromStatus;
|
||||
$this->application->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->application->is_gzip_enabled = $this->isGzipEnabled;
|
||||
$this->application->is_stripprefix_enabled = $this->isStripprefixEnabled;
|
||||
|
||||
$this->application->save();
|
||||
} else {
|
||||
// Sync from model
|
||||
$this->humanName = $this->application->human_name;
|
||||
$this->description = $this->application->description;
|
||||
$this->fqdn = $this->application->fqdn;
|
||||
$this->image = $this->application->image;
|
||||
$this->excludeFromStatus = data_get($this->application, 'exclude_from_status', false);
|
||||
$this->isLogDrainEnabled = data_get($this->application, 'is_log_drain_enabled', false);
|
||||
$this->isGzipEnabled = data_get($this->application, 'is_gzip_enabled', true);
|
||||
$this->isStripprefixEnabled = data_get($this->application, 'is_stripprefix_enabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
public function convertToDatabase()
|
||||
|
|
@ -193,8 +243,15 @@ public function submit()
|
|||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
// Sync to model for domain conflict check
|
||||
$this->syncToModel();
|
||||
// Sync to model for domain conflict check (without validation)
|
||||
$this->application->human_name = $this->humanName;
|
||||
$this->application->description = $this->description;
|
||||
$this->application->fqdn = $this->fqdn;
|
||||
$this->application->image = $this->image;
|
||||
$this->application->exclude_from_status = $this->excludeFromStatus;
|
||||
$this->application->is_log_drain_enabled = $this->isLogDrainEnabled;
|
||||
$this->application->is_gzip_enabled = $this->isGzipEnabled;
|
||||
$this->application->is_stripprefix_enabled = $this->isStripprefixEnabled;
|
||||
// Check for domain conflicts if not forcing save
|
||||
if (! $this->forceSaveDomains) {
|
||||
$result = checkDomainUsage(resource: $this->application);
|
||||
|
|
@ -209,10 +266,45 @@ public function submit()
|
|||
$this->forceSaveDomains = false;
|
||||
}
|
||||
|
||||
// Check for required port
|
||||
if (! $this->forceRemovePort) {
|
||||
$service = $this->application->service;
|
||||
$requiredPort = $service->getRequiredPort();
|
||||
|
||||
if ($requiredPort !== null) {
|
||||
// Check if all FQDNs have a port
|
||||
$fqdns = str($this->fqdn)->trim()->explode(',');
|
||||
$missingPort = false;
|
||||
|
||||
foreach ($fqdns as $fqdn) {
|
||||
$fqdn = trim($fqdn);
|
||||
if (empty($fqdn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$port = ServiceApplication::extractPortFromUrl($fqdn);
|
||||
if ($port === null) {
|
||||
$missingPort = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($missingPort) {
|
||||
$this->requiredPort = $requiredPort;
|
||||
$this->showPortWarningModal = true;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Reset the force flag after using it
|
||||
$this->forceRemovePort = false;
|
||||
}
|
||||
|
||||
$this->validate();
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
updateCompose($this->application);
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||
|
|
@ -224,7 +316,7 @@ public function submit()
|
|||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ class GetLogs extends Component
|
|||
|
||||
public ?string $container = null;
|
||||
|
||||
public ?string $displayName = null;
|
||||
|
||||
public ?string $pull_request = null;
|
||||
|
||||
public ?bool $streamLogs = false;
|
||||
|
|
|
|||
|
|
@ -2,42 +2,54 @@
|
|||
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Livewire\Concerns\SynchronizesModelData;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class HealthChecks extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use SynchronizesModelData;
|
||||
|
||||
public $resource;
|
||||
|
||||
// Explicit properties
|
||||
#[Validate(['boolean'])]
|
||||
public bool $healthCheckEnabled = false;
|
||||
|
||||
#[Validate(['string'])]
|
||||
public string $healthCheckMethod;
|
||||
|
||||
#[Validate(['string'])]
|
||||
public string $healthCheckScheme;
|
||||
|
||||
#[Validate(['string'])]
|
||||
public string $healthCheckHost;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $healthCheckPort = null;
|
||||
|
||||
#[Validate(['string'])]
|
||||
public string $healthCheckPath;
|
||||
|
||||
#[Validate(['integer'])]
|
||||
public int $healthCheckReturnCode;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
public ?string $healthCheckResponseText = null;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $healthCheckInterval;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $healthCheckTimeout;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $healthCheckRetries;
|
||||
|
||||
#[Validate(['integer'])]
|
||||
public int $healthCheckStartPeriod;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $customHealthcheckFound = false;
|
||||
|
||||
protected $rules = [
|
||||
|
|
@ -56,36 +68,69 @@ class HealthChecks extends Component
|
|||
'customHealthcheckFound' => 'boolean',
|
||||
];
|
||||
|
||||
protected function getModelBindings(): array
|
||||
{
|
||||
return [
|
||||
'healthCheckEnabled' => 'resource.health_check_enabled',
|
||||
'healthCheckMethod' => 'resource.health_check_method',
|
||||
'healthCheckScheme' => 'resource.health_check_scheme',
|
||||
'healthCheckHost' => 'resource.health_check_host',
|
||||
'healthCheckPort' => 'resource.health_check_port',
|
||||
'healthCheckPath' => 'resource.health_check_path',
|
||||
'healthCheckReturnCode' => 'resource.health_check_return_code',
|
||||
'healthCheckResponseText' => 'resource.health_check_response_text',
|
||||
'healthCheckInterval' => 'resource.health_check_interval',
|
||||
'healthCheckTimeout' => 'resource.health_check_timeout',
|
||||
'healthCheckRetries' => 'resource.health_check_retries',
|
||||
'healthCheckStartPeriod' => 'resource.health_check_start_period',
|
||||
'customHealthcheckFound' => 'resource.custom_healthcheck_found',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('view', $this->resource);
|
||||
$this->syncFromModel();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
|
||||
// Sync to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
$this->resource->health_check_port = $this->healthCheckPort;
|
||||
$this->resource->health_check_path = $this->healthCheckPath;
|
||||
$this->resource->health_check_return_code = $this->healthCheckReturnCode;
|
||||
$this->resource->health_check_response_text = $this->healthCheckResponseText;
|
||||
$this->resource->health_check_interval = $this->healthCheckInterval;
|
||||
$this->resource->health_check_timeout = $this->healthCheckTimeout;
|
||||
$this->resource->health_check_retries = $this->healthCheckRetries;
|
||||
$this->resource->health_check_start_period = $this->healthCheckStartPeriod;
|
||||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
|
||||
$this->resource->save();
|
||||
} else {
|
||||
// Sync from model
|
||||
$this->healthCheckEnabled = $this->resource->health_check_enabled;
|
||||
$this->healthCheckMethod = $this->resource->health_check_method;
|
||||
$this->healthCheckScheme = $this->resource->health_check_scheme;
|
||||
$this->healthCheckHost = $this->resource->health_check_host;
|
||||
$this->healthCheckPort = $this->resource->health_check_port;
|
||||
$this->healthCheckPath = $this->resource->health_check_path;
|
||||
$this->healthCheckReturnCode = $this->resource->health_check_return_code;
|
||||
$this->healthCheckResponseText = $this->resource->health_check_response_text;
|
||||
$this->healthCheckInterval = $this->resource->health_check_interval;
|
||||
$this->healthCheckTimeout = $this->resource->health_check_timeout;
|
||||
$this->healthCheckRetries = $this->resource->health_check_retries;
|
||||
$this->healthCheckStartPeriod = $this->resource->health_check_start_period;
|
||||
$this->customHealthcheckFound = $this->resource->custom_healthcheck_found;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$this->syncToModel();
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
$this->resource->health_check_port = $this->healthCheckPort;
|
||||
$this->resource->health_check_path = $this->healthCheckPath;
|
||||
$this->resource->health_check_return_code = $this->healthCheckReturnCode;
|
||||
$this->resource->health_check_response_text = $this->healthCheckResponseText;
|
||||
$this->resource->health_check_interval = $this->healthCheckInterval;
|
||||
$this->resource->health_check_timeout = $this->healthCheckTimeout;
|
||||
$this->resource->health_check_retries = $this->healthCheckRetries;
|
||||
$this->resource->health_check_start_period = $this->healthCheckStartPeriod;
|
||||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Health check updated.');
|
||||
}
|
||||
|
|
@ -96,7 +141,20 @@ public function submit()
|
|||
$this->authorize('update', $this->resource);
|
||||
$this->validate();
|
||||
|
||||
$this->syncToModel();
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
$this->resource->health_check_port = $this->healthCheckPort;
|
||||
$this->resource->health_check_path = $this->healthCheckPath;
|
||||
$this->resource->health_check_return_code = $this->healthCheckReturnCode;
|
||||
$this->resource->health_check_response_text = $this->healthCheckResponseText;
|
||||
$this->resource->health_check_interval = $this->healthCheckInterval;
|
||||
$this->resource->health_check_timeout = $this->healthCheckTimeout;
|
||||
$this->resource->health_check_retries = $this->healthCheckRetries;
|
||||
$this->resource->health_check_start_period = $this->healthCheckStartPeriod;
|
||||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
$this->resource->save();
|
||||
$this->dispatch('success', 'Health check updated.');
|
||||
} catch (\Throwable $e) {
|
||||
|
|
@ -111,7 +169,20 @@ public function toggleHealthcheck()
|
|||
$wasEnabled = $this->healthCheckEnabled;
|
||||
$this->healthCheckEnabled = ! $this->healthCheckEnabled;
|
||||
|
||||
$this->syncToModel();
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
$this->resource->health_check_port = $this->healthCheckPort;
|
||||
$this->resource->health_check_path = $this->healthCheckPath;
|
||||
$this->resource->health_check_return_code = $this->healthCheckReturnCode;
|
||||
$this->resource->health_check_response_text = $this->healthCheckResponseText;
|
||||
$this->resource->health_check_interval = $this->healthCheckInterval;
|
||||
$this->resource->health_check_timeout = $this->healthCheckTimeout;
|
||||
$this->resource->health_check_retries = $this->healthCheckRetries;
|
||||
$this->resource->health_check_start_period = $this->healthCheckStartPeriod;
|
||||
$this->resource->custom_healthcheck_found = $this->customHealthcheckFound;
|
||||
$this->resource->save();
|
||||
|
||||
if ($this->healthCheckEnabled && ! $wasEnabled && $this->resource->isRunning()) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,60 @@ public function loadTokens()
|
|||
$this->tokens = CloudProviderToken::ownedByCurrentTeam()->get();
|
||||
}
|
||||
|
||||
public function validateToken(int $tokenId)
|
||||
{
|
||||
try {
|
||||
$token = CloudProviderToken::ownedByCurrentTeam()->findOrFail($tokenId);
|
||||
$this->authorize('view', $token);
|
||||
|
||||
if ($token->provider === 'hetzner') {
|
||||
$isValid = $this->validateHetznerToken($token->token);
|
||||
if ($isValid) {
|
||||
$this->dispatch('success', 'Hetzner token is valid.');
|
||||
} else {
|
||||
$this->dispatch('error', 'Hetzner token validation failed. Please check the token.');
|
||||
}
|
||||
} elseif ($token->provider === 'digitalocean') {
|
||||
$isValid = $this->validateDigitalOceanToken($token->token);
|
||||
if ($isValid) {
|
||||
$this->dispatch('success', 'DigitalOcean token is valid.');
|
||||
} else {
|
||||
$this->dispatch('error', 'DigitalOcean token validation failed. Please check the token.');
|
||||
}
|
||||
} else {
|
||||
$this->dispatch('error', 'Unknown provider.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateHetznerToken(string $token): bool
|
||||
{
|
||||
try {
|
||||
$response = \Illuminate\Support\Facades\Http::withToken($token)
|
||||
->timeout(10)
|
||||
->get('https://api.hetzner.cloud/v1/servers?per_page=1');
|
||||
|
||||
return $response->successful();
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function validateDigitalOceanToken(string $token): bool
|
||||
{
|
||||
try {
|
||||
$response = \Illuminate\Support\Facades\Http::withToken($token)
|
||||
->timeout(10)
|
||||
->get('https://api.digitalocean.com/v2/account');
|
||||
|
||||
return $response->successful();
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteToken(int $tokenId)
|
||||
{
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -74,12 +74,16 @@ class ByHetzner extends Component
|
|||
#[Locked]
|
||||
public Collection $saved_cloud_init_scripts;
|
||||
|
||||
public bool $from_onboarding = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('viewAny', CloudProviderToken::class);
|
||||
$this->loadTokens();
|
||||
$this->loadSavedCloudInitScripts();
|
||||
$this->server_name = generate_random_name();
|
||||
$this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get();
|
||||
|
||||
if ($this->private_keys->count() > 0) {
|
||||
$this->private_key_id = $this->private_keys->first()->id;
|
||||
}
|
||||
|
|
@ -131,7 +135,7 @@ public function handleTokenAdded($tokenId)
|
|||
public function handlePrivateKeyCreated($keyId)
|
||||
{
|
||||
// Refresh private keys list
|
||||
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
$this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get();
|
||||
|
||||
// Auto-select the new key
|
||||
$this->private_key_id = $keyId;
|
||||
|
|
@ -246,12 +250,6 @@ private function loadHetznerData(string $token)
|
|||
// Get images and sort by name
|
||||
$images = $hetznerService->getImages();
|
||||
|
||||
ray('Raw images from Hetzner API', [
|
||||
'total_count' => count($images),
|
||||
'types' => collect($images)->pluck('type')->unique()->values(),
|
||||
'sample' => array_slice($images, 0, 3),
|
||||
]);
|
||||
|
||||
$this->images = collect($images)
|
||||
->filter(function ($image) {
|
||||
// Only system images
|
||||
|
|
@ -269,20 +267,8 @@ private function loadHetznerData(string $token)
|
|||
->sortBy('name')
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
ray('Filtered images', [
|
||||
'filtered_count' => count($this->images),
|
||||
'debian_images' => collect($this->images)->filter(fn ($img) => str_contains($img['name'] ?? '', 'debian'))->values(),
|
||||
]);
|
||||
|
||||
// Load SSH keys from Hetzner
|
||||
$this->hetznerSshKeys = $hetznerService->getSshKeys();
|
||||
|
||||
ray('Hetzner SSH Keys', [
|
||||
'total_count' => count($this->hetznerSshKeys),
|
||||
'keys' => $this->hetznerSshKeys,
|
||||
]);
|
||||
|
||||
$this->loading_data = false;
|
||||
} catch (\Throwable $e) {
|
||||
$this->loading_data = false;
|
||||
|
|
@ -299,9 +285,9 @@ private function getCpuVendorInfo(array $serverType): ?string
|
|||
} elseif (str_starts_with($name, 'cpx')) {
|
||||
return 'AMD EPYC™';
|
||||
} elseif (str_starts_with($name, 'cx')) {
|
||||
return 'Intel® Xeon®';
|
||||
return 'Intel®/AMD';
|
||||
} elseif (str_starts_with($name, 'cax')) {
|
||||
return 'Ampere® Altra®';
|
||||
return 'Ampere®';
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -574,6 +560,16 @@ public function submit()
|
|||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
|
||||
if ($this->from_onboarding) {
|
||||
// Complete the boarding when server is successfully created via Hetzner
|
||||
currentTeam()->update([
|
||||
'show_boarding' => false,
|
||||
]);
|
||||
refreshSession();
|
||||
|
||||
return $this->redirect(route('server.show', $server->uuid));
|
||||
}
|
||||
|
||||
return redirect()->route('server.show', $server->uuid);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
use App\Models\Team;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
|
|
@ -39,25 +38,12 @@ class ByIp extends Component
|
|||
|
||||
public int $port = 22;
|
||||
|
||||
public bool $is_swarm_manager = false;
|
||||
|
||||
public bool $is_swarm_worker = false;
|
||||
|
||||
public $selected_swarm_cluster = null;
|
||||
|
||||
public bool $is_build_server = false;
|
||||
|
||||
#[Locked]
|
||||
public Collection $swarm_managers;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->name = generate_random_name();
|
||||
$this->private_key_id = $this->private_keys->first()?->id;
|
||||
$this->swarm_managers = Server::isUsable()->get()->where('settings.is_swarm_manager', true);
|
||||
if ($this->swarm_managers->count() > 0) {
|
||||
$this->selected_swarm_cluster = $this->swarm_managers->first()->id;
|
||||
}
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
|
|
@ -72,9 +58,6 @@ protected function rules(): array
|
|||
'ip' => 'required|string',
|
||||
'user' => 'required|string',
|
||||
'port' => 'required|integer|between:1,65535',
|
||||
'is_swarm_manager' => 'required|boolean',
|
||||
'is_swarm_worker' => 'required|boolean',
|
||||
'selected_swarm_cluster' => 'nullable|integer',
|
||||
'is_build_server' => 'required|boolean',
|
||||
];
|
||||
}
|
||||
|
|
@ -94,11 +77,6 @@ protected function messages(): array
|
|||
'port.required' => 'The Port field is required.',
|
||||
'port.integer' => 'The Port field must be an integer.',
|
||||
'port.between' => 'The Port field must be between 1 and 65535.',
|
||||
'is_swarm_manager.required' => 'The Swarm Manager field is required.',
|
||||
'is_swarm_manager.boolean' => 'The Swarm Manager field must be true or false.',
|
||||
'is_swarm_worker.required' => 'The Swarm Worker field is required.',
|
||||
'is_swarm_worker.boolean' => 'The Swarm Worker field must be true or false.',
|
||||
'selected_swarm_cluster.integer' => 'The Swarm Cluster field must be an integer.',
|
||||
'is_build_server.required' => 'The Build Server field is required.',
|
||||
'is_build_server.boolean' => 'The Build Server field must be true or false.',
|
||||
]);
|
||||
|
|
@ -140,9 +118,6 @@ public function submit()
|
|||
'team_id' => currentTeam()->id,
|
||||
'private_key_id' => $this->private_key_id,
|
||||
];
|
||||
if ($this->is_swarm_worker) {
|
||||
$payload['swarm_cluster'] = $this->selected_swarm_cluster;
|
||||
}
|
||||
if ($this->is_build_server) {
|
||||
data_forget($payload, 'proxy');
|
||||
}
|
||||
|
|
@ -150,13 +125,6 @@ public function submit()
|
|||
$server->proxy->set('status', 'exited');
|
||||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
if ($this->is_build_server) {
|
||||
$this->is_swarm_manager = false;
|
||||
$this->is_swarm_worker = false;
|
||||
} else {
|
||||
$server->settings->is_swarm_manager = $this->is_swarm_manager;
|
||||
$server->settings->is_swarm_worker = $this->is_swarm_worker;
|
||||
}
|
||||
$server->settings->is_build_server = $this->is_build_server;
|
||||
$server->settings->save();
|
||||
|
||||
|
|
|
|||
|
|
@ -85,19 +85,8 @@ public function submit()
|
|||
// Handle allowed IPs with subnet support and 0.0.0.0 special case
|
||||
$this->allowed_ips = str($this->allowed_ips)->replaceEnd(',', '')->trim();
|
||||
|
||||
// Check if user entered 0.0.0.0 or left field empty (both allow access from anywhere)
|
||||
$allowsFromAnywhere = false;
|
||||
if (empty($this->allowed_ips)) {
|
||||
$allowsFromAnywhere = true;
|
||||
} elseif ($this->allowed_ips === '0.0.0.0' || str_contains($this->allowed_ips, '0.0.0.0')) {
|
||||
$allowsFromAnywhere = true;
|
||||
}
|
||||
|
||||
// Check if it's 0.0.0.0 (allow all) or empty
|
||||
if ($this->allowed_ips === '0.0.0.0' || empty($this->allowed_ips)) {
|
||||
// Keep as is - empty means no restriction, 0.0.0.0 means allow all
|
||||
} else {
|
||||
// Validate and clean up the entries
|
||||
// Only validate and clean up if we have IPs and it's not 0.0.0.0 (allow all)
|
||||
if (! empty($this->allowed_ips) && ! in_array('0.0.0.0', array_map('trim', explode(',', $this->allowed_ips)))) {
|
||||
$invalidEntries = [];
|
||||
$validEntries = str($this->allowed_ips)->trim()->explode(',')->map(function ($entry) use (&$invalidEntries) {
|
||||
$entry = str($entry)->trim()->toString();
|
||||
|
|
@ -133,7 +122,6 @@ public function submit()
|
|||
return;
|
||||
}
|
||||
|
||||
// Also check if we have no valid entries after filtering
|
||||
if ($validEntries->isEmpty()) {
|
||||
$this->dispatch('error', 'No valid IP addresses or subnets provided');
|
||||
|
||||
|
|
@ -144,14 +132,6 @@ public function submit()
|
|||
}
|
||||
|
||||
$this->instantSave();
|
||||
|
||||
// Show security warning if allowing access from anywhere
|
||||
if ($allowsFromAnywhere) {
|
||||
$message = empty($this->allowed_ips)
|
||||
? 'Empty IP allowlist allows API access from anywhere.<br><br>This is not recommended for production environments!'
|
||||
: 'Using 0.0.0.0 allows API access from anywhere.<br><br>This is not recommended for production environments!';
|
||||
$this->dispatch('warning', $message);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ class Index extends Component
|
|||
#[Validate('required|string|timezone')]
|
||||
public string $instance_timezone;
|
||||
|
||||
#[Validate('nullable|string|max:50')]
|
||||
public ?string $dev_helper_version = null;
|
||||
|
||||
public array $domainConflicts = [];
|
||||
|
||||
public bool $showDomainConflictModal = false;
|
||||
|
|
@ -60,6 +63,7 @@ public function mount()
|
|||
$this->public_ipv4 = $this->settings->public_ipv4;
|
||||
$this->public_ipv6 = $this->settings->public_ipv6;
|
||||
$this->instance_timezone = $this->settings->instance_timezone;
|
||||
$this->dev_helper_version = $this->settings->dev_helper_version;
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
|
|
@ -81,6 +85,7 @@ public function instantSave($isSave = true)
|
|||
$this->settings->public_ipv4 = $this->public_ipv4;
|
||||
$this->settings->public_ipv6 = $this->public_ipv6;
|
||||
$this->settings->instance_timezone = $this->instance_timezone;
|
||||
$this->settings->dev_helper_version = $this->dev_helper_version;
|
||||
if ($isSave) {
|
||||
$this->settings->save();
|
||||
$this->dispatch('success', 'Settings updated!');
|
||||
|
|
|
|||
|
|
@ -29,7 +29,16 @@ public function mount()
|
|||
return redirect()->route('home');
|
||||
}
|
||||
$this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) {
|
||||
$carry[$setting->provider] = $setting;
|
||||
$carry[$setting->provider] = [
|
||||
'id' => $setting->id,
|
||||
'provider' => $setting->provider,
|
||||
'enabled' => $setting->enabled,
|
||||
'client_id' => $setting->client_id,
|
||||
'client_secret' => $setting->client_secret,
|
||||
'redirect_uri' => $setting->redirect_uri,
|
||||
'tenant' => $setting->tenant,
|
||||
'base_url' => $setting->base_url,
|
||||
];
|
||||
|
||||
return $carry;
|
||||
}, []);
|
||||
|
|
@ -38,16 +47,83 @@ public function mount()
|
|||
private function updateOauthSettings(?string $provider = null)
|
||||
{
|
||||
if ($provider) {
|
||||
$oauth = $this->oauth_settings_map[$provider];
|
||||
$oauthData = $this->oauth_settings_map[$provider];
|
||||
$oauth = OauthSetting::find($oauthData['id']);
|
||||
|
||||
if (! $oauth) {
|
||||
throw new \Exception('OAuth setting for '.$provider.' not found. It may have been deleted.');
|
||||
}
|
||||
|
||||
$oauth->fill([
|
||||
'enabled' => $oauthData['enabled'],
|
||||
'client_id' => $oauthData['client_id'],
|
||||
'client_secret' => $oauthData['client_secret'],
|
||||
'redirect_uri' => $oauthData['redirect_uri'],
|
||||
'tenant' => $oauthData['tenant'],
|
||||
'base_url' => $oauthData['base_url'],
|
||||
]);
|
||||
|
||||
if (! $oauth->couldBeEnabled()) {
|
||||
$oauth->update(['enabled' => false]);
|
||||
throw new \Exception('OAuth settings are not complete for '.$oauth->provider.'.<br/>Please fill in all required fields.');
|
||||
}
|
||||
$oauth->save();
|
||||
|
||||
// Update the array with fresh data
|
||||
$this->oauth_settings_map[$provider] = [
|
||||
'id' => $oauth->id,
|
||||
'provider' => $oauth->provider,
|
||||
'enabled' => $oauth->enabled,
|
||||
'client_id' => $oauth->client_id,
|
||||
'client_secret' => $oauth->client_secret,
|
||||
'redirect_uri' => $oauth->redirect_uri,
|
||||
'tenant' => $oauth->tenant,
|
||||
'base_url' => $oauth->base_url,
|
||||
];
|
||||
|
||||
$this->dispatch('success', 'OAuth settings for '.$oauth->provider.' updated successfully!');
|
||||
} else {
|
||||
foreach (array_values($this->oauth_settings_map) as &$setting) {
|
||||
$setting->save();
|
||||
$errors = [];
|
||||
foreach (array_values($this->oauth_settings_map) as $settingData) {
|
||||
$oauth = OauthSetting::find($settingData['id']);
|
||||
|
||||
if (! $oauth) {
|
||||
$errors[] = "OAuth setting for provider '{$settingData['provider']}' not found. It may have been deleted.";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$oauth->fill([
|
||||
'enabled' => $settingData['enabled'],
|
||||
'client_id' => $settingData['client_id'],
|
||||
'client_secret' => $settingData['client_secret'],
|
||||
'redirect_uri' => $settingData['redirect_uri'],
|
||||
'tenant' => $settingData['tenant'],
|
||||
'base_url' => $settingData['base_url'],
|
||||
]);
|
||||
|
||||
if ($settingData['enabled'] && ! $oauth->couldBeEnabled()) {
|
||||
$oauth->enabled = false;
|
||||
$errors[] = "OAuth settings are incomplete for '{$oauth->provider}'. Required fields are missing. The provider has been disabled.";
|
||||
}
|
||||
|
||||
$oauth->save();
|
||||
|
||||
// Update the array with fresh data
|
||||
$this->oauth_settings_map[$oauth->provider] = [
|
||||
'id' => $oauth->id,
|
||||
'provider' => $oauth->provider,
|
||||
'enabled' => $oauth->enabled,
|
||||
'client_id' => $oauth->client_id,
|
||||
'client_secret' => $oauth->client_secret,
|
||||
'redirect_uri' => $oauth->redirect_uri,
|
||||
'tenant' => $oauth->tenant,
|
||||
'base_url' => $oauth->base_url,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($errors)) {
|
||||
$this->dispatch('error', implode('<br/>', $errors));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,19 +47,19 @@ class Change extends Component
|
|||
|
||||
public int $customPort;
|
||||
|
||||
public int $appId;
|
||||
public ?int $appId = null;
|
||||
|
||||
public int $installationId;
|
||||
public ?int $installationId = null;
|
||||
|
||||
public string $clientId;
|
||||
public ?string $clientId = null;
|
||||
|
||||
public string $clientSecret;
|
||||
public ?string $clientSecret = null;
|
||||
|
||||
public string $webhookSecret;
|
||||
public ?string $webhookSecret = null;
|
||||
|
||||
public bool $isSystemWide;
|
||||
|
||||
public int $privateKeyId;
|
||||
public ?int $privateKeyId = null;
|
||||
|
||||
public ?string $contents = null;
|
||||
|
||||
|
|
@ -78,16 +78,16 @@ class Change extends Component
|
|||
'htmlUrl' => 'required|string',
|
||||
'customUser' => 'required|string',
|
||||
'customPort' => 'required|int',
|
||||
'appId' => 'required|int',
|
||||
'installationId' => 'required|int',
|
||||
'clientId' => 'required|string',
|
||||
'clientSecret' => 'required|string',
|
||||
'webhookSecret' => 'required|string',
|
||||
'appId' => 'nullable|int',
|
||||
'installationId' => 'nullable|int',
|
||||
'clientId' => 'nullable|string',
|
||||
'clientSecret' => 'nullable|string',
|
||||
'webhookSecret' => 'nullable|string',
|
||||
'isSystemWide' => 'required|bool',
|
||||
'contents' => 'nullable|string',
|
||||
'metadata' => 'nullable|string',
|
||||
'pullRequests' => 'nullable|string',
|
||||
'privateKeyId' => 'required|int',
|
||||
'privateKeyId' => 'nullable|int',
|
||||
];
|
||||
|
||||
public function boot()
|
||||
|
|
@ -148,47 +148,48 @@ public function checkPermissions()
|
|||
try {
|
||||
$this->authorize('view', $this->github_app);
|
||||
|
||||
// Validate required fields before attempting to fetch permissions
|
||||
$missingFields = [];
|
||||
|
||||
if (! $this->github_app->app_id) {
|
||||
$missingFields[] = 'App ID';
|
||||
}
|
||||
|
||||
if (! $this->github_app->private_key_id) {
|
||||
$missingFields[] = 'Private Key';
|
||||
}
|
||||
|
||||
if (! empty($missingFields)) {
|
||||
$fieldsList = implode(', ', $missingFields);
|
||||
$this->dispatch('error', "Cannot fetch permissions. Please set the following required fields first: {$fieldsList}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the private key exists and is accessible
|
||||
if (! $this->github_app->privateKey) {
|
||||
$this->dispatch('error', 'Private Key not found. Please select a valid private key.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
GithubAppPermissionJob::dispatchSync($this->github_app);
|
||||
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||
$this->dispatch('success', 'Github App permissions updated.');
|
||||
} catch (\Throwable $e) {
|
||||
// Provide better error message for unsupported key formats
|
||||
$errorMessage = $e->getMessage();
|
||||
if (str_contains($errorMessage, 'DECODER routines::unsupported') ||
|
||||
str_contains($errorMessage, 'parse your key')) {
|
||||
$this->dispatch('error', 'The selected private key format is not supported for GitHub Apps. <br><br>Please use an RSA private key in PEM format (BEGIN RSA PRIVATE KEY). <br><br>OpenSSH format keys (BEGIN OPENSSH PRIVATE KEY) are not supported.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
// public function check()
|
||||
// {
|
||||
|
||||
// Need administration:read:write permission
|
||||
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#list-self-hosted-runners-for-a-repository
|
||||
|
||||
// $github_access_token = generateGithubInstallationToken($this->github_app);
|
||||
// $repositories = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100");
|
||||
// $runners_by_repository = collect([]);
|
||||
// $repositories = $repositories->json()['repositories'];
|
||||
// foreach ($repositories as $repository) {
|
||||
// $runners_downloads = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/downloads");
|
||||
// $runners = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners");
|
||||
// $token = Http::withHeaders([
|
||||
// 'Authorization' => "Bearer $github_access_token",
|
||||
// 'Accept' => 'application/vnd.github+json'
|
||||
// ])->withBody(null)->post("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/registration-token");
|
||||
// $token = $token->json();
|
||||
// $remove_token = Http::withHeaders([
|
||||
// 'Authorization' => "Bearer $github_access_token",
|
||||
// 'Accept' => 'application/vnd.github+json'
|
||||
// ])->withBody(null)->post("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/remove-token");
|
||||
// $remove_token = $remove_token->json();
|
||||
// $runners_by_repository->put($repository['full_name'], [
|
||||
// 'token' => $token,
|
||||
// 'remove_token' => $remove_token,
|
||||
// 'runners' => $runners->json(),
|
||||
// 'runners_downloads' => $runners_downloads->json()
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
|
|
@ -340,10 +341,13 @@ public function createGithubAppManually()
|
|||
$this->authorize('update', $this->github_app);
|
||||
|
||||
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||
$this->github_app->app_id = '1234567890';
|
||||
$this->github_app->installation_id = '1234567890';
|
||||
$this->github_app->app_id = 1234567890;
|
||||
$this->github_app->installation_id = 1234567890;
|
||||
$this->github_app->save();
|
||||
$this->dispatch('success', 'Github App updated.');
|
||||
|
||||
// Redirect to avoid Livewire morphing issues when view structure changes
|
||||
return redirect()->route('source.github.show', ['github_app_uuid' => $this->github_app->uuid])
|
||||
->with('success', 'Github App updated. You can now configure the details.');
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
|
|
|
|||
|
|
@ -50,11 +50,9 @@ public function createGitHubApp()
|
|||
'html_url' => $this->html_url,
|
||||
'custom_user' => $this->custom_user,
|
||||
'custom_port' => $this->custom_port,
|
||||
'is_system_wide' => $this->is_system_wide,
|
||||
'team_id' => currentTeam()->id,
|
||||
];
|
||||
if (isCloud()) {
|
||||
$payload['is_system_wide'] = $this->is_system_wide;
|
||||
}
|
||||
$github_app = GithubApp::create($payload);
|
||||
if (session('from')) {
|
||||
session(['from' => session('from') + ['source_id' => $github_app->id]]);
|
||||
|
|
|
|||
|
|
@ -120,7 +120,6 @@ class Application extends BaseModel
|
|||
protected $appends = ['server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'custom_network_aliases' => 'array',
|
||||
'http_basic_auth_password' => 'encrypted',
|
||||
];
|
||||
|
||||
|
|
@ -253,6 +252,30 @@ public function customNetworkAliases(): Attribute
|
|||
return null;
|
||||
}
|
||||
|
||||
if (is_string($value) && $this->isJson($value)) {
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
// Return as comma-separated string, not array
|
||||
return is_array($decoded) ? implode(',', $decoded) : $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom_network_aliases as an array
|
||||
*/
|
||||
public function customNetworkAliasesArray(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$value = $this->getRawOriginal('custom_network_aliases');
|
||||
if (is_null($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($value) && $this->isJson($value)) {
|
||||
return json_decode($value, true);
|
||||
}
|
||||
|
|
@ -957,7 +980,7 @@ public function isLogDrainEnabled()
|
|||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->custom_labels.$this->settings->use_build_secrets);
|
||||
$newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->custom_network_aliases.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->settings->use_build_secrets);
|
||||
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->sort());
|
||||
} else {
|
||||
|
|
@ -1804,7 +1827,22 @@ public function getFilesFromServer(bool $isInit = false)
|
|||
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
|
||||
{
|
||||
$dockerfile = str($dockerfile)->trim()->explode("\n");
|
||||
if (str($dockerfile)->contains('HEALTHCHECK') && ($this->isHealthcheckDisabled() || $isInit)) {
|
||||
$hasHealthcheck = str($dockerfile)->contains('HEALTHCHECK');
|
||||
|
||||
// Always check if healthcheck was removed, regardless of health_check_enabled setting
|
||||
if (! $hasHealthcheck && $this->custom_healthcheck_found) {
|
||||
// HEALTHCHECK was removed from Dockerfile, reset to defaults
|
||||
$this->custom_healthcheck_found = false;
|
||||
$this->health_check_interval = 5;
|
||||
$this->health_check_timeout = 5;
|
||||
$this->health_check_retries = 10;
|
||||
$this->health_check_start_period = 5;
|
||||
$this->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($hasHealthcheck && ($this->isHealthcheckDisabled() || $isInit)) {
|
||||
$healthcheckCommand = null;
|
||||
$lines = $dockerfile->toArray();
|
||||
foreach ($lines as $line) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,14 @@
|
|||
|
||||
class ApplicationSetting extends Model
|
||||
{
|
||||
protected $cast = [
|
||||
protected $casts = [
|
||||
'is_static' => 'boolean',
|
||||
'is_spa' => 'boolean',
|
||||
'is_build_server_enabled' => 'boolean',
|
||||
'is_preserve_repository_enabled' => 'boolean',
|
||||
'is_container_label_escape_enabled' => 'boolean',
|
||||
'is_container_label_readonly_enabled' => 'boolean',
|
||||
'use_build_secrets' => 'boolean',
|
||||
'is_auto_deploy_enabled' => 'boolean',
|
||||
'is_force_https_enabled' => 'boolean',
|
||||
'is_debug_enabled' => 'boolean',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class GithubApp extends BaseModel
|
|||
|
||||
protected $casts = [
|
||||
'is_public' => 'boolean',
|
||||
'is_system_wide' => 'boolean',
|
||||
'type' => 'string',
|
||||
];
|
||||
|
||||
|
|
@ -27,7 +28,20 @@ protected static function booted(): void
|
|||
if ($applications_count > 0) {
|
||||
throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.');
|
||||
}
|
||||
$github_app->privateKey()->delete();
|
||||
|
||||
$privateKey = $github_app->privateKey;
|
||||
if ($privateKey) {
|
||||
// Check if key is used by anything EXCEPT this GitHub app
|
||||
$isUsedElsewhere = $privateKey->servers()->exists()
|
||||
|| $privateKey->applications()->exists()
|
||||
|| $privateKey->githubApps()->where('id', '!=', $github_app->id)->exists()
|
||||
|| $privateKey->gitlabApps()->exists();
|
||||
|
||||
if (! $isUsedElsewhere) {
|
||||
$privateKey->delete();
|
||||
} else {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,6 +88,16 @@ public static function ownedByCurrentTeam(array $select = ['*'])
|
|||
return self::whereTeamId($teamId)->select($selectArray->all());
|
||||
}
|
||||
|
||||
public static function ownedAndOnlySShKeys(array $select = ['*'])
|
||||
{
|
||||
$teamId = currentTeam()->id;
|
||||
$selectArray = collect($select)->concat(['id']);
|
||||
|
||||
return self::whereTeamId($teamId)
|
||||
->where('is_git_related', false)
|
||||
->select($selectArray->all());
|
||||
}
|
||||
|
||||
public static function validatePrivateKey($privateKey)
|
||||
{
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1184,6 +1184,31 @@ public function documentation()
|
|||
return data_get($service, 'documentation', config('constants.urls.docs'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required port for this service from the template definition.
|
||||
*/
|
||||
public function getRequiredPort(): ?int
|
||||
{
|
||||
try {
|
||||
$services = get_service_templates();
|
||||
$serviceName = str($this->name)->beforeLast('-')->value();
|
||||
$service = data_get($services, $serviceName, []);
|
||||
$port = data_get($service, 'port');
|
||||
|
||||
return $port ? (int) $port : null;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this service requires a port to function correctly.
|
||||
*/
|
||||
public function requiresPort(): bool
|
||||
{
|
||||
return $this->getRequiredPort() !== null;
|
||||
}
|
||||
|
||||
public function applications()
|
||||
{
|
||||
return $this->hasMany(ServiceApplication::class);
|
||||
|
|
|
|||
|
|
@ -118,6 +118,53 @@ public function fqdns(): Attribute
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract port number from a given FQDN URL.
|
||||
* Returns null if no port is specified.
|
||||
*/
|
||||
public static function extractPortFromUrl(string $url): ?int
|
||||
{
|
||||
try {
|
||||
// Ensure URL has a scheme for proper parsing
|
||||
if (! str_starts_with($url, 'http://') && ! str_starts_with($url, 'https://')) {
|
||||
$url = 'http://'.$url;
|
||||
}
|
||||
|
||||
$parsed = parse_url($url);
|
||||
$port = $parsed['port'] ?? null;
|
||||
|
||||
return $port ? (int) $port : null;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all FQDNs have a port specified.
|
||||
*/
|
||||
public function allFqdnsHavePort(): bool
|
||||
{
|
||||
if (is_null($this->fqdn) || $this->fqdn === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fqdns = explode(',', $this->fqdn);
|
||||
|
||||
foreach ($fqdns as $fqdn) {
|
||||
$fqdn = trim($fqdn);
|
||||
if (empty($fqdn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$port = self::extractPortFromUrl($fqdn);
|
||||
if ($port === null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getFilesFromServer(bool $isInit = false)
|
||||
{
|
||||
getFilesystemVolumesFromServer($this, $isInit);
|
||||
|
|
|
|||
|
|
@ -243,10 +243,14 @@ protected function externalDbUrl(): Attribute
|
|||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$encodedUser = rawurlencode($this->clickhouse_admin_user);
|
||||
$encodedPass = rawurlencode($this->clickhouse_admin_password);
|
||||
|
||||
return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
|
||||
return "clickhouse://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->clickhouse_db}";
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -249,9 +249,13 @@ protected function externalDbUrl(): Attribute
|
|||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||
$encodedPass = rawurlencode($this->dragonfly_password);
|
||||
$url = "{$scheme}://:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
$url = "{$scheme}://:{$encodedPass}@{$serverIp}:{$this->public_port}/0";
|
||||
|
||||
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||
|
|
|
|||
|
|
@ -249,9 +249,13 @@ protected function externalDbUrl(): Attribute
|
|||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||
$encodedPass = rawurlencode($this->keydb_password);
|
||||
$url = "{$scheme}://:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
$url = "{$scheme}://:{$encodedPass}@{$serverIp}:{$this->public_port}/0";
|
||||
|
||||
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||
|
|
|
|||
|
|
@ -239,10 +239,14 @@ protected function externalDbUrl(): Attribute
|
|||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$encodedUser = rawurlencode($this->mariadb_user);
|
||||
$encodedPass = rawurlencode($this->mariadb_password);
|
||||
|
||||
return "mysql://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
|
||||
return "mysql://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->mariadb_database}";
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -269,9 +269,13 @@ protected function externalDbUrl(): Attribute
|
|||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$encodedUser = rawurlencode($this->mongo_initdb_root_username);
|
||||
$encodedPass = rawurlencode($this->mongo_initdb_root_password);
|
||||
$url = "mongodb://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
|
||||
$url = "mongodb://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/?directConnection=true";
|
||||
if ($this->enable_ssl) {
|
||||
$url .= '&tls=true&tlsCAFile=/etc/mongo/certs/ca.pem';
|
||||
if (in_array($this->ssl_mode, ['verify-full'])) {
|
||||
|
|
|
|||
|
|
@ -251,9 +251,13 @@ protected function externalDbUrl(): Attribute
|
|||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$encodedUser = rawurlencode($this->mysql_user);
|
||||
$encodedPass = rawurlencode($this->mysql_password);
|
||||
$url = "mysql://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
|
||||
$url = "mysql://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->mysql_database}";
|
||||
if ($this->enable_ssl) {
|
||||
$url .= "?ssl-mode={$this->ssl_mode}";
|
||||
if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) {
|
||||
|
|
|
|||
|
|
@ -246,9 +246,13 @@ protected function externalDbUrl(): Attribute
|
|||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$encodedUser = rawurlencode($this->postgres_user);
|
||||
$encodedPass = rawurlencode($this->postgres_password);
|
||||
$url = "postgres://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
|
||||
$url = "postgres://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->postgres_db}";
|
||||
if ($this->enable_ssl) {
|
||||
$url .= "?sslmode={$this->ssl_mode}";
|
||||
if (in_array($this->ssl_mode, ['verify-ca', 'verify-full'])) {
|
||||
|
|
|
|||
|
|
@ -253,11 +253,15 @@ protected function externalDbUrl(): Attribute
|
|||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$redis_version = $this->getRedisVersion();
|
||||
$username_part = version_compare($redis_version, '6.0', '>=') ? rawurlencode($this->redis_username).':' : '';
|
||||
$encodedPass = rawurlencode($this->redis_password);
|
||||
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||
$url = "{$scheme}://{$username_part}{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
$url = "{$scheme}://{$username_part}{$encodedPass}@{$serverIp}:{$this->public_port}/0";
|
||||
|
||||
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||
|
|
|
|||
|
|
@ -16,10 +16,7 @@ class ServerPatchCheck extends CustomEmailNotification
|
|||
public function __construct(public Server $server, public array $patchData)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->serverUrl = route('server.security.patches', ['server_uuid' => $this->server->uuid]);
|
||||
if (isDev()) {
|
||||
$this->serverUrl = 'https://staging-but-dev.coolify.io/server/'.$this->server->uuid.'/security/patches';
|
||||
}
|
||||
$this->serverUrl = base_url().'/server/'.$this->server->uuid.'/security/patches';
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
|
|
|||
|
|
@ -127,13 +127,19 @@ public function boot(): void
|
|||
});
|
||||
|
||||
RateLimiter::for('forgot-password', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->ip());
|
||||
// Use real client IP (not spoofable forwarded headers)
|
||||
$realIp = $request->server('REMOTE_ADDR') ?? $request->ip();
|
||||
|
||||
return Limit::perMinute(5)->by($realIp);
|
||||
});
|
||||
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
$email = (string) $request->email;
|
||||
// Use email + real client IP (not spoofable forwarded headers)
|
||||
// server('REMOTE_ADDR') gives the actual connecting IP before proxy headers
|
||||
$realIp = $request->server('REMOTE_ADDR') ?? $request->ip();
|
||||
|
||||
return Limit::perMinute(5)->by($email.$request->ip());
|
||||
return Limit::perMinute(5)->by($email.'|'.$realIp);
|
||||
});
|
||||
|
||||
RateLimiter::for('two-factor', function (Request $request) {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,14 @@ public function getImages(): array
|
|||
|
||||
public function getServerTypes(): array
|
||||
{
|
||||
return $this->requestPaginated('get', '/server_types', 'server_types');
|
||||
$types = $this->requestPaginated('get', '/server_types', 'server_types');
|
||||
|
||||
// Filter out entries where "deprecated" is explicitly true
|
||||
$filtered = array_filter($types, function ($type) {
|
||||
return ! (isset($type['deprecated']) && $type['deprecated'] === true);
|
||||
});
|
||||
|
||||
return array_values($filtered);
|
||||
}
|
||||
|
||||
public function getSshKeys(): array
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ function sharedDataApplications()
|
|||
'start_command' => 'string|nullable',
|
||||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/',
|
||||
'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable',
|
||||
'custom_network_aliases' => 'string|nullable',
|
||||
'base_directory' => 'string|nullable',
|
||||
'publish_directory' => 'string|nullable',
|
||||
'health_check_enabled' => 'boolean',
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@
|
|||
const SPECIFIC_SERVICES = [
|
||||
'quay.io/minio/minio',
|
||||
'minio/minio',
|
||||
'ghcr.io/coollabsio/minio',
|
||||
'coollabsio/minio',
|
||||
'svhd/logto',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,13 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth
|
|||
$database = new StandaloneRedis;
|
||||
$database->uuid = (new Cuid2);
|
||||
$database->name = 'redis-database-'.$database->uuid;
|
||||
|
||||
$redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||
if ($otherData && isset($otherData['redis_password'])) {
|
||||
$redis_password = $otherData['redis_password'];
|
||||
unset($otherData['redis_password']);
|
||||
}
|
||||
|
||||
$database->environment_id = $environment_id;
|
||||
$database->destination_id = $destination->id;
|
||||
$database->destination_type = $destination->getMorphClass();
|
||||
|
|
|
|||
|
|
@ -1073,6 +1073,9 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
|
|||
}
|
||||
$yaml_compose = Yaml::parse($compose);
|
||||
foreach ($yaml_compose['services'] as $service_name => $service) {
|
||||
if (! isset($service['volumes'])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($service['volumes'] as $volume_name => $volume) {
|
||||
if (data_get($volume, 'type') === 'bind' && data_get($volume, 'content')) {
|
||||
unset($yaml_compose['services'][$service_name]['volumes'][$volume_name]['content']);
|
||||
|
|
|
|||
|
|
@ -358,6 +358,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
{
|
||||
$uuid = data_get($resource, 'uuid');
|
||||
$compose = data_get($resource, 'docker_compose_raw');
|
||||
// Store original compose for later use to update docker_compose_raw with content removed
|
||||
$originalCompose = $compose;
|
||||
if (! $compose) {
|
||||
return collect([]);
|
||||
}
|
||||
|
|
@ -1162,13 +1164,21 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
$environment = $environment->filter(function ($value, $key) {
|
||||
return ! str($key)->startsWith('SERVICE_FQDN_');
|
||||
})->map(function ($value, $key) use ($resource) {
|
||||
// if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used
|
||||
if (str($value)->isEmpty()) {
|
||||
if ($resource->environment_variables()->where('key', $key)->exists()) {
|
||||
$value = $resource->environment_variables()->where('key', $key)->first()->value;
|
||||
} else {
|
||||
$value = null;
|
||||
// Preserve empty strings and null values with correct Docker Compose semantics:
|
||||
// - Empty string: Variable is set to "" (e.g., HTTP_PROXY="" means "no proxy")
|
||||
// - Null: Variable is unset/removed from container environment (may inherit from host)
|
||||
if ($value === null) {
|
||||
// User explicitly wants variable unset - respect that
|
||||
// NEVER override from database - null means "inherit from environment"
|
||||
// Keep as null (will be excluded from container environment)
|
||||
} elseif ($value === '') {
|
||||
// Empty string - allow database override for backward compatibility
|
||||
$dbEnv = $resource->environment_variables()->where('key', $key)->first();
|
||||
// Only use database override if it exists AND has a non-empty value
|
||||
if ($dbEnv && str($dbEnv->value)->isNotEmpty()) {
|
||||
$value = $dbEnv->value;
|
||||
}
|
||||
// Otherwise keep empty string as-is
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
|
@ -1297,7 +1307,46 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
return array_search($key, $customOrder);
|
||||
});
|
||||
|
||||
$resource->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2);
|
||||
// Remove empty top-level sections (volumes, networks, configs, secrets)
|
||||
// Keep only non-empty sections to match Docker Compose best practices
|
||||
$topLevel = $topLevel->filter(function ($value, $key) {
|
||||
// Always keep 'services' section
|
||||
if ($key === 'services') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keep section only if it has content
|
||||
return $value instanceof Collection ? $value->isNotEmpty() : ! empty($value);
|
||||
});
|
||||
|
||||
$cleanedCompose = Yaml::dump(convertToArray($topLevel), 10, 2);
|
||||
$resource->docker_compose = $cleanedCompose;
|
||||
|
||||
// Update docker_compose_raw to remove content: from volumes only
|
||||
// This keeps the original user input clean while preventing content reapplication
|
||||
// Parse the original compose again to create a clean version without Coolify additions
|
||||
try {
|
||||
$originalYaml = Yaml::parse($originalCompose);
|
||||
// Remove content, isDirectory, and is_directory from all volume definitions
|
||||
if (isset($originalYaml['services'])) {
|
||||
foreach ($originalYaml['services'] as $serviceName => &$service) {
|
||||
if (isset($service['volumes'])) {
|
||||
foreach ($service['volumes'] as $key => &$volume) {
|
||||
if (is_array($volume)) {
|
||||
unset($volume['content']);
|
||||
unset($volume['isDirectory']);
|
||||
unset($volume['is_directory']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
|
||||
} catch (\Exception $e) {
|
||||
// If parsing fails, keep the original docker_compose_raw unchanged
|
||||
ray('Failed to update docker_compose_raw in applicationParser: '.$e->getMessage());
|
||||
}
|
||||
|
||||
data_forget($resource, 'environment_variables');
|
||||
data_forget($resource, 'environment_variables_preview');
|
||||
$resource->save();
|
||||
|
|
@ -1309,6 +1358,8 @@ function serviceParser(Service $resource): Collection
|
|||
{
|
||||
$uuid = data_get($resource, 'uuid');
|
||||
$compose = data_get($resource, 'docker_compose_raw');
|
||||
// Store original compose for later use to update docker_compose_raw with content removed
|
||||
$originalCompose = $compose;
|
||||
if (! $compose) {
|
||||
return collect([]);
|
||||
}
|
||||
|
|
@ -1558,21 +1609,22 @@ function serviceParser(Service $resource): Collection
|
|||
]);
|
||||
}
|
||||
if (substr_count(str($key)->value(), '_') === 3) {
|
||||
$newKey = str($key)->beforeLast('_');
|
||||
// For port-specific variables (e.g., SERVICE_FQDN_UMAMI_3000),
|
||||
// keep the port suffix in the key and use the URL with port
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => $newKey->value(),
|
||||
'key' => $key->value(),
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $fqdn,
|
||||
'value' => $fqdnWithPort,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => $newKey->value(),
|
||||
'key' => $key->value(),
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
], [
|
||||
'value' => $url,
|
||||
'value' => $urlWithPort,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
|
|
@ -2091,13 +2143,21 @@ function serviceParser(Service $resource): Collection
|
|||
$environment = $environment->filter(function ($value, $key) {
|
||||
return ! str($key)->startsWith('SERVICE_FQDN_');
|
||||
})->map(function ($value, $key) use ($resource) {
|
||||
// if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used
|
||||
if (str($value)->isEmpty()) {
|
||||
if ($resource->environment_variables()->where('key', $key)->exists()) {
|
||||
$value = $resource->environment_variables()->where('key', $key)->first()->value;
|
||||
} else {
|
||||
$value = null;
|
||||
// Preserve empty strings and null values with correct Docker Compose semantics:
|
||||
// - Empty string: Variable is set to "" (e.g., HTTP_PROXY="" means "no proxy")
|
||||
// - Null: Variable is unset/removed from container environment (may inherit from host)
|
||||
if ($value === null) {
|
||||
// User explicitly wants variable unset - respect that
|
||||
// NEVER override from database - null means "inherit from environment"
|
||||
// Keep as null (will be excluded from container environment)
|
||||
} elseif ($value === '') {
|
||||
// Empty string - allow database override for backward compatibility
|
||||
$dbEnv = $resource->environment_variables()->where('key', $key)->first();
|
||||
// Only use database override if it exists AND has a non-empty value
|
||||
if ($dbEnv && str($dbEnv->value)->isNotEmpty()) {
|
||||
$value = $dbEnv->value;
|
||||
}
|
||||
// Otherwise keep empty string as-is
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
|
@ -2220,7 +2280,46 @@ function serviceParser(Service $resource): Collection
|
|||
return array_search($key, $customOrder);
|
||||
});
|
||||
|
||||
$resource->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2);
|
||||
// Remove empty top-level sections (volumes, networks, configs, secrets)
|
||||
// Keep only non-empty sections to match Docker Compose best practices
|
||||
$topLevel = $topLevel->filter(function ($value, $key) {
|
||||
// Always keep 'services' section
|
||||
if ($key === 'services') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keep section only if it has content
|
||||
return $value instanceof Collection ? $value->isNotEmpty() : ! empty($value);
|
||||
});
|
||||
|
||||
$cleanedCompose = Yaml::dump(convertToArray($topLevel), 10, 2);
|
||||
$resource->docker_compose = $cleanedCompose;
|
||||
|
||||
// Update docker_compose_raw to remove content: from volumes only
|
||||
// This keeps the original user input clean while preventing content reapplication
|
||||
// Parse the original compose again to create a clean version without Coolify additions
|
||||
try {
|
||||
$originalYaml = Yaml::parse($originalCompose);
|
||||
// Remove content, isDirectory, and is_directory from all volume definitions
|
||||
if (isset($originalYaml['services'])) {
|
||||
foreach ($originalYaml['services'] as $serviceName => &$service) {
|
||||
if (isset($service['volumes'])) {
|
||||
foreach ($service['volumes'] as $key => &$volume) {
|
||||
if (is_array($volume)) {
|
||||
unset($volume['content']);
|
||||
unset($volume['isDirectory']);
|
||||
unset($volume['is_directory']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
|
||||
} catch (\Exception $e) {
|
||||
// If parsing fails, keep the original docker_compose_raw unchanged
|
||||
ray('Failed to update docker_compose_raw in serviceParser: '.$e->getMessage());
|
||||
}
|
||||
|
||||
data_forget($resource, 'environment_variables');
|
||||
data_forget($resource, 'environment_variables_preview');
|
||||
$resource->save();
|
||||
|
|
|
|||
|
|
@ -2879,6 +2879,18 @@ function instanceSettings()
|
|||
return InstanceSettings::get();
|
||||
}
|
||||
|
||||
function getHelperVersion(): string
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
|
||||
// In development mode, use the dev_helper_version if set, otherwise fallback to config
|
||||
if (isDev() && ! empty($settings->dev_helper_version)) {
|
||||
return $settings->dev_helper_version;
|
||||
}
|
||||
|
||||
return config('constants.coolify.helper_version');
|
||||
}
|
||||
|
||||
function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id)
|
||||
{
|
||||
$server = Server::find($server_id)->where('team_id', $team_id)->first();
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.437',
|
||||
'helper_version' => '1.0.11',
|
||||
'version' => '4.0.0-beta.443',
|
||||
'helper_version' => '1.0.12',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
'autoupdate' => env('AUTOUPDATE'),
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
'helper_image' => env('HELPER_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-helper'),
|
||||
'realtime_image' => env('REALTIME_IMAGE', env('REGISTRY_URL', 'ghcr.io').'/coollabsio/coolify-realtime'),
|
||||
'is_windows_docker_desktop' => env('IS_WINDOWS_DOCKER_DESKTOP', false),
|
||||
'releases_url' => 'https://cdn.coollabs.io/coolify/releases.json',
|
||||
'releases_url' => 'https://cdn.coolify.io/releases.json',
|
||||
],
|
||||
|
||||
'urls' => [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->string('dev_helper_version')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('dev_helper_version');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -104,7 +104,7 @@ services:
|
|||
networks:
|
||||
- coolify
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
image: ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z # Released on 15 October 2025
|
||||
pull_policy: always
|
||||
container_name: coolify-minio
|
||||
command: server /data --console-address ":9001"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ ARG DOCKER_BUILDX_VERSION=0.25.0
|
|||
# https://github.com/buildpacks/pack/releases
|
||||
ARG PACK_VERSION=0.38.2
|
||||
# https://github.com/railwayapp/nixpacks/releases
|
||||
ARG NIXPACKS_VERSION=1.40.0
|
||||
ARG NIXPACKS_VERSION=1.41.0
|
||||
# https://github.com/minio/mc/releases
|
||||
ARG MINIO_VERSION=RELEASE.2025-08-13T08-35-41Z
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID && \
|
|||
|
||||
WORKDIR /var/www/html
|
||||
COPY --chown=www-data:www-data composer.json composer.lock ./
|
||||
RUN composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist
|
||||
RUN --mount=type=cache,target=/tmp/cache \
|
||||
COMPOSER_CACHE_DIR=/tmp/cache composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist
|
||||
|
||||
USER www-data
|
||||
|
||||
|
|
@ -38,7 +39,8 @@ FROM node:24-alpine AS static-assets
|
|||
|
||||
WORKDIR /app
|
||||
COPY package*.json vite.config.js postcss.config.cjs ./
|
||||
RUN npm ci
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
|
|
@ -72,8 +74,9 @@ RUN apk add --no-cache gnupg && \
|
|||
curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor > /usr/share/keyrings/postgresql.gpg
|
||||
|
||||
# Install system dependencies
|
||||
RUN apk upgrade
|
||||
RUN apk add --no-cache \
|
||||
RUN --mount=type=cache,target=/var/cache/apk \
|
||||
apk upgrade && \
|
||||
apk add --no-cache \
|
||||
postgresql${POSTGRES_VERSION}-client \
|
||||
openssh-client \
|
||||
git \
|
||||
|
|
|
|||
5
jean.json
Normal file
5
jean.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"scripts": {
|
||||
"setup": "cp $JEAN_ROOT_PATH/.env . && mkdir -p .claude && cp $JEAN_ROOT_PATH/.claude/settings.local.json .claude/settings.local.json"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.435"
|
||||
"version": "4.0.0-beta.443"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.436"
|
||||
"version": "4.0.0-beta.444"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.11"
|
||||
|
|
|
|||
98
package-lock.json
generated
98
package-lock.json
generated
|
|
@ -22,7 +22,7 @@
|
|||
"pusher-js": "8.4.0",
|
||||
"tailwind-scrollbar": "4.0.2",
|
||||
"tailwindcss": "4.1.10",
|
||||
"vite": "6.3.6",
|
||||
"vite": "6.4.1",
|
||||
"vue": "3.5.16"
|
||||
}
|
||||
},
|
||||
|
|
@ -916,7 +916,8 @@
|
|||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.5.10",
|
||||
|
|
@ -1158,6 +1159,66 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.4.3",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.0.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.3",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.10",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.3",
|
||||
"@emnapi/runtime": "^1.4.3",
|
||||
"@tybys/wasm-util": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.9.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz",
|
||||
|
|
@ -1371,8 +1432,7 @@
|
|||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
|
|
@ -1535,6 +1595,7 @@
|
|||
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
|
|
@ -1549,6 +1610,7 @@
|
|||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
|
|
@ -1567,6 +1629,7 @@
|
|||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
|
|
@ -2328,7 +2391,6 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -2405,7 +2467,6 @@
|
|||
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
|
|
@ -2490,6 +2551,7 @@
|
|||
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
|
|
@ -2506,6 +2568,7 @@
|
|||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
|
|
@ -2524,6 +2587,7 @@
|
|||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
|
|
@ -2538,6 +2602,7 @@
|
|||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
|
|
@ -2586,8 +2651,7 @@
|
|||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz",
|
||||
"integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
|
|
@ -2604,11 +2668,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz",
|
||||
"integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
|
||||
"integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
|
|
@ -2651,12 +2715,11 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
|
|
@ -2756,7 +2819,6 @@
|
|||
"integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.16",
|
||||
"@vue/compiler-sfc": "3.5.16",
|
||||
|
|
@ -2779,6 +2841,7 @@
|
|||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
|
@ -2800,6 +2863,7 @@
|
|||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"pusher-js": "8.4.0",
|
||||
"tailwind-scrollbar": "4.0.2",
|
||||
"tailwindcss": "4.1.10",
|
||||
"vite": "6.3.6",
|
||||
"vite": "6.4.1",
|
||||
"vue": "3.5.16"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
4
public/svgs/home-assistant.svg
Normal file
4
public/svgs/home-assistant.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="240" height="240" viewBox="0 0 240 240" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M240 224.762C240 233.012 233.25 239.762 225 239.762H15C6.75 239.762 0 233.012 0 224.762V134.762C0 126.512 4.77 114.993 10.61 109.153L109.39 10.3725C115.22 4.5425 124.77 4.5425 130.6 10.3725L229.39 109.162C235.22 114.992 240 126.522 240 134.772V224.772V224.762Z" fill="#F2F4F9"/>
|
||||
<path d="M229.39 109.153L130.61 10.3725C124.78 4.5425 115.23 4.5425 109.4 10.3725L10.61 109.153C4.78 114.983 0 126.512 0 134.762V224.762C0 233.012 6.75 239.762 15 239.762H107.27L66.64 199.132C64.55 199.852 62.32 200.262 60 200.262C48.7 200.262 39.5 191.062 39.5 179.762C39.5 168.462 48.7 159.262 60 159.262C71.3 159.262 80.5 168.462 80.5 179.762C80.5 182.092 80.09 184.322 79.37 186.412L111 218.042V102.162C104.2 98.8225 99.5 91.8425 99.5 83.7725C99.5 72.4725 108.7 63.2725 120 63.2725C131.3 63.2725 140.5 72.4725 140.5 83.7725C140.5 91.8425 135.8 98.8225 129 102.162V183.432L160.46 151.972C159.84 150.012 159.5 147.932 159.5 145.772C159.5 134.472 168.7 125.272 180 125.272C191.3 125.272 200.5 134.472 200.5 145.772C200.5 157.072 191.3 166.272 180 166.272C177.5 166.272 175.12 165.802 172.91 164.982L129 208.892V239.772H225C233.25 239.772 240 233.022 240 224.772V134.772C240 126.522 235.23 115.002 229.39 109.162V109.153Z" fill="#18BCF2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/svgs/metamcp.png
Normal file
BIN
public/svgs/metamcp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
public/svgs/pocketid-logo.png
Normal file
BIN
public/svgs/pocketid-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
public/svgs/redisinsight.png
Normal file
BIN
public/svgs/redisinsight.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue