diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..24c099119 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,2 @@ +reviews: + review_status: false diff --git a/.cursor/rules/frontend-patterns.mdc b/.cursor/rules/frontend-patterns.mdc index 663490d3b..4730160b2 100644 --- a/.cursor/rules/frontend-patterns.mdc +++ b/.cursor/rules/frontend-patterns.mdc @@ -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 +
+
+ + + + + + + + + + Save Changes + + +
+``` + +**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(); diff --git a/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml index d00853964..365842254 100644 --- a/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml +++ b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml @@ -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' diff --git a/.github/workflows/chore-manage-stale-issues-and-prs.yml b/.github/workflows/chore-manage-stale-issues-and-prs.yml index 58a2b7d7e..d61005549 100644 --- a/.github/workflows/chore-manage-stale-issues-and-prs.yml +++ b/.github/workflows/chore-manage-stale-issues-and-prs.yml @@ -4,6 +4,10 @@ on: schedule: - cron: '0 2 * * *' +permissions: + issues: write + pull-requests: write + jobs: manage-stale: runs-on: ubuntu-latest diff --git a/.github/workflows/chore-pr-comments.yml b/.github/workflows/chore-pr-comments.yml index 8836c6632..1d94bec81 100644 --- a/.github/workflows/chore-pr-comments.yml +++ b/.github/workflows/chore-pr-comments.yml @@ -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: diff --git a/.github/workflows/chore-remove-labels-and-assignees-on-close.yml b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml index 194984ddc..8ac199a08 100644 --- a/.github/workflows/chore-remove-labels-and-assignees-on-close.yml +++ b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml @@ -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 diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index a2c92df59..000000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -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]') - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 9daf0e90e..000000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/cleanup-ghcr-untagged.yml b/.github/workflows/cleanup-ghcr-untagged.yml index 394fba68f..a86cedcb0 100644 --- a/.github/workflows/cleanup-ghcr-untagged.yml +++ b/.github/workflows/cleanup-ghcr-untagged.yml @@ -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'] diff --git a/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml index a4a2a21f6..fec54d54a 100644 --- a/.github/workflows/coolify-helper-next.yml +++ b/.github/workflows/coolify-helper-next.yml @@ -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 diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index 56c3eaa17..0c9996ec8 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -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 diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml index cd1f002b8..21871b103 100644 --- a/.github/workflows/coolify-production-build.yml +++ b/.github/workflows/coolify-production-build.yml @@ -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 diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml index ad590146b..7ab4dcc42 100644 --- a/.github/workflows/coolify-realtime-next.yml +++ b/.github/workflows/coolify-realtime-next.yml @@ -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 diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml index d00621cc2..5efe445c5 100644 --- a/.github/workflows/coolify-realtime.yml +++ b/.github/workflows/coolify-realtime.yml @@ -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 diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml index df737c9c3..67b7b03e8 100644 --- a/.github/workflows/coolify-staging-build.yml +++ b/.github/workflows/coolify-staging-build.yml @@ -17,6 +17,10 @@ on: - templates/** - CHANGELOG.md +permissions: + contents: read + packages: write + env: GITHUB_REGISTRY: ghcr.io DOCKER_REGISTRY: docker.io @@ -34,11 +38,10 @@ jobs: platform: linux/aarch64 runner: ubuntu-24.04-arm runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write steps: - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Sanitize branch name for Docker tag id: sanitize @@ -82,11 +85,10 @@ jobs: merge-manifest: runs-on: ubuntu-24.04 needs: build-push - permissions: - contents: read - packages: write steps: - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Sanitize branch name for Docker tag id: sanitize diff --git a/.github/workflows/coolify-testing-host.yml b/.github/workflows/coolify-testing-host.yml index 95a228114..24133887a 100644 --- a/.github/workflows/coolify-testing-host.yml +++ b/.github/workflows/coolify-testing-host.yml @@ -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 diff --git a/.gitignore b/.gitignore index 65b7faa1b..935ea548e 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ scripts/load-test/* docker/coolify-realtime/node_modules .DS_Store CHANGELOG.md +/.workspaces diff --git a/CHANGELOG.md b/CHANGELOG.md index 6696cfba0..2980c7401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ # 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.440] - 2025-11-04 + +### 🐛 Bug Fixes + +- 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 @@ -13,6 +46,12 @@ ### 🚀 Features - 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 @@ -50,6 +89,13 @@ ### 🐛 Bug Fixes - *(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 @@ -64,6 +110,10 @@ ### 🚜 Refactor - 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 @@ -74,6 +124,7 @@ ### 📚 Documentation - Update changelog - Update changelog - Update changelog +- Add service & database deployment logging plan ### 🧪 Testing @@ -85,6 +136,8 @@ ### ⚙️ Miscellaneous Tasks - 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 diff --git a/README.md b/README.md index f159cde89..456a1268e 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,6 @@ ## Big Sponsors * [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 diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index 38f6d7bc8..7fdfe9aeb 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -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.'"; diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 300221d24..d1bb119af 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -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.'"; diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index 3a2ceebb3..128469e24 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -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.'"; diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index 8a936c8ae..29dd7b8fe 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -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.'"; diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 19699d684..5982b68be 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -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) { diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 25546fa9d..c1df8d6db 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -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"; diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index ac011acbe..1ae0d56a0 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -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'); } diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index 8a7ae42a4..4c99a0213 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -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.'"; diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 63f5b1979..5c881e743 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -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); } diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 392562167..6bf094c32 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -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'; diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index dfef6a566..50011c74f 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -22,6 +22,10 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s $service->isConfigurationChanged(save: true); $commands[] = 'cd '.$service->workdir(); $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; + // Ensure .env file exists before docker compose tries to load it + // This is defensive programming - saveComposeConfigs() already creates it, + // but we guarantee it here in case of any edge cases or manual deployments + $commands[] = 'touch .env'; if ($pullLatestImages) { $commands[] = "echo 'Pulling images.'"; $commands[] = 'docker compose pull'; diff --git a/app/Console/Commands/Cloud/RestoreDatabase.php b/app/Console/Commands/Cloud/RestoreDatabase.php new file mode 100644 index 000000000..7c6c0d4c6 --- /dev/null +++ b/app/Console/Commands/Cloud/RestoreDatabase.php @@ -0,0 +1,219 @@ +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('Executing drop command:'); + $this->line($command); + } + + $output = shell_exec($command.' 2>&1'); + + if ($this->debug) { + $this->line("Output: {$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('Executing decompress command:'); + $this->line($decompressCommand); + } + + $decompressOutput = shell_exec($decompressCommand.' 2>&1'); + if ($this->debug && $decompressOutput) { + $this->line("Decompress output: {$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('Executing restore command:'); + $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('Output:'); + $this->line($output); + } + if ($error) { + $this->line('Error output:'); + $this->line($error); + } + $this->line("Exit code: {$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']); + } +} diff --git a/app/Console/Commands/SyncBunny.php b/app/Console/Commands/SyncBunny.php index b0cd24715..e634feadb 100644 --- a/app/Console/Commands/SyncBunny.php +++ b/app/Console/Commands/SyncBunny.php @@ -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; } diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 73690a05b..0d38b7363 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -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); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 971c1d806..0fd007e9a 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1780,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); @@ -2322,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' => [ @@ -3030,6 +3029,12 @@ private function stop_running_container(bool $force = false) private function start_by_compose_file() { + // Ensure .env file exists before docker compose tries to load it (defensive programming) + $this->execute_remote_command( + ["touch {$this->workdir}/.env", 'hidden' => true], + ["touch {$this->configuration_dir}/.env", 'hidden' => true], + ); + if ($this->application->build_pack === 'dockerimage') { $this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.'); $this->execute_remote_command( @@ -3227,6 +3232,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) { @@ -3239,6 +3258,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() @@ -3247,9 +3278,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 @@ -3259,9 +3290,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 @@ -3271,9 +3300,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 @@ -3283,15 +3312,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")); diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 11da6fac1..45586f0d0 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -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}"; } diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index b92886d38..7cdf1b81a 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -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); } } diff --git a/app/Livewire/Concerns/SynchronizesModelData.php b/app/Livewire/Concerns/SynchronizesModelData.php deleted file mode 100644 index f8218c715..000000000 --- a/app/Livewire/Concerns/SynchronizesModelData.php +++ /dev/null @@ -1,35 +0,0 @@ - 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); - } - } -} diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 8e8add430..a83e6f70a 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -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) { @@ -441,7 +542,7 @@ public function loadComposeFile($isInit = false, $showToast = true) // Sync the docker_compose_raw from the model to the component property // This ensures the Monaco editor displays the loaded compose file - $this->syncFromModel(); + $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 @@ -507,7 +608,7 @@ public function generateDomain(string $serviceName) public function updatedBaseDirectory() { - if ($this->build_pack === 'dockercompose') { + if ($this->buildPack === 'dockercompose') { $this->loadComposeFile(); } } @@ -527,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); @@ -567,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(); } @@ -586,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.'); } @@ -603,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); @@ -617,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'); @@ -722,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(); @@ -742,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) { @@ -809,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 { diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index f759dd71e..a9a7de878 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -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.

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); diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 40539b13e..2ce4374a0 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -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); } diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 20358218f..1d8d8b247 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -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.

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); diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 85cd21a7f..8a7b6e090 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -5,6 +5,7 @@ use App\Models\Service; use App\Support\ValidationPatterns; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Livewire\Component; class StackForm extends Component @@ -22,7 +23,7 @@ class StackForm extends Component public string $dockerComposeRaw; - public string $dockerCompose; + public ?string $dockerCompose = null; public ?bool $connectToDockerNetwork = null; @@ -30,7 +31,7 @@ protected function rules(): array { $baseRules = [ 'dockerComposeRaw' => 'required', - 'dockerCompose' => 'required', + 'dockerCompose' => 'nullable', 'name' => ValidationPatterns::nameRules(), 'description' => ValidationPatterns::descriptionRules(), 'connectToDockerNetwork' => 'nullable', @@ -140,18 +141,26 @@ public function submit($notify = true) $this->validate(); $this->syncData(true); - // Validate for command injection BEFORE saving to database + // Validate for command injection BEFORE any database operations validateDockerComposeForInjection($this->service->docker_compose_raw); - $this->service->save(); - $this->service->saveExtraFields($this->fields); - $this->service->parse(); - $this->service->refresh(); - $this->service->saveComposeConfigs(); + // Use transaction to ensure atomicity - if parse fails, save is rolled back + DB::transaction(function () { + $this->service->save(); + $this->service->saveExtraFields($this->fields); + $this->service->parse(); + $this->service->refresh(); + $this->service->saveComposeConfigs(); + }); + $this->dispatch('refreshEnvs'); $this->dispatch('refreshServices'); $notify && $this->dispatch('success', 'Service saved.'); } catch (\Throwable $e) { + // On error, refresh from database to restore clean state + $this->service->refresh(); + $this->syncData(false); + return handleError($e, $this); } finally { if (is_null($this->service->config_hash)) { diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php index c8029761d..05f786690 100644 --- a/app/Livewire/Project/Shared/HealthChecks.php +++ b/app/Livewire/Project/Shared/HealthChecks.php @@ -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()) { diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 13d690352..96f13b173 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -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!'); diff --git a/app/Models/Application.php b/app/Models/Application.php index 32459f752..615e35f68 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -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 { diff --git a/app/Models/ApplicationSetting.php b/app/Models/ApplicationSetting.php index 4b03c69e1..26cb937b3 100644 --- a/app/Models/ApplicationSetting.php +++ b/app/Models/ApplicationSetting.php @@ -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', diff --git a/app/Models/Service.php b/app/Models/Service.php index c4b8623e0..ef755d105 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -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); @@ -1262,6 +1287,11 @@ public function workdir() public function saveComposeConfigs() { + // Guard against null or empty docker_compose + if (! $this->docker_compose) { + return; + } + $workdir = $this->workdir(); instant_remote_process([ diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index 5cafc9042..49bd56206 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -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); diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 5d0f9a2a7..488653fb1 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -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', diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index d6c9b5bdf..5bccb50f1 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -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']); diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 01ae50f6b..9b17e6810 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -59,11 +59,13 @@ function validateDockerComposeForInjection(string $composeYaml): void if (isset($volume['source'])) { $source = $volume['source']; if (is_string($source)) { - // Allow simple env vars and env vars with defaults (validated in parseDockerVolumeString) + // Allow env vars and env vars with defaults (validated in parseDockerVolumeString) + // Also allow env vars followed by safe path concatenation (e.g., ${VAR}/path) $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $source); $isEnvVarWithDefault = preg_match('/^\$\{[^}]+:-[^}]*\}$/', $source); + $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $source); - if (! $isSimpleEnvVar && ! $isEnvVarWithDefault) { + if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { try { validateShellSafePath($source, 'volume source'); } catch (\Exception $e) { @@ -310,15 +312,17 @@ function parseDockerVolumeString(string $volumeString): array // Validate source path for command injection attempts // We validate the final source value after environment variable processing if ($source !== null) { - // Allow simple environment variables like ${VAR_NAME} or ${VAR} - // but validate everything else for shell metacharacters + // Allow environment variables like ${VAR_NAME} or ${VAR} + // Also allow env vars followed by safe path concatenation (e.g., ${VAR}/path) $sourceStr = is_string($source) ? $source : $source; // Skip validation for simple environment variable references - // Pattern: ${WORD_CHARS} with no special characters inside + // Pattern 1: ${WORD_CHARS} with no special characters inside + // Pattern 2: ${WORD_CHARS}/path/to/file (env var with path concatenation) $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceStr); + $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $sourceStr); - if (! $isSimpleEnvVar) { + if (! $isSimpleEnvVar && ! $isEnvVarWithPath) { try { validateShellSafePath($sourceStr, 'volume source'); } catch (\Exception $e) { @@ -711,9 +715,12 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int // Validate source and target for command injection (array/long syntax) if ($source !== null && ! empty($source->value())) { $sourceValue = $source->value(); - // Allow simple environment variable references + // Allow environment variable references and env vars with path concatenation $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceValue); - if (! $isSimpleEnvVar) { + $isEnvVarWithDefault = preg_match('/^\$\{[^}]+:-[^}]*\}$/', $sourceValue); + $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $sourceValue); + + if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { try { validateShellSafePath($sourceValue, 'volume source'); } catch (\Exception $e) { @@ -1164,13 +1171,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; @@ -1285,6 +1300,9 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int if ($depends_on->count() > 0) { $payload['depends_on'] = $depends_on; } + // Auto-inject .env file so Coolify environment variables are available inside containers + // This makes Applications behave consistently with manual .env file usage + $payload['env_file'] = ['.env']; if ($isPullRequest) { $serviceName = addPreviewDeploymentSuffix($serviceName, $pullRequestId); } @@ -1299,6 +1317,18 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int return array_search($key, $customOrder); }); + // 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; @@ -1589,21 +1619,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, ]); } @@ -1791,9 +1822,12 @@ function serviceParser(Service $resource): Collection // Validate source and target for command injection (array/long syntax) if ($source !== null && ! empty($source->value())) { $sourceValue = $source->value(); - // Allow simple environment variable references + // Allow environment variable references and env vars with path concatenation $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceValue); - if (! $isSimpleEnvVar) { + $isEnvVarWithDefault = preg_match('/^\$\{[^}]+:-[^}]*\}$/', $sourceValue); + $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $sourceValue); + + if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { try { validateShellSafePath($sourceValue, 'volume source'); } catch (\Exception $e) { @@ -2122,13 +2156,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; @@ -2240,6 +2282,9 @@ function serviceParser(Service $resource): Collection if ($depends_on->count() > 0) { $payload['depends_on'] = $depends_on; } + // Auto-inject .env file so Coolify environment variables are available inside containers + // This makes Services behave consistently with Applications + $payload['env_file'] = ['.env']; $parsedServices->put($serviceName, $payload); } @@ -2251,6 +2296,18 @@ function serviceParser(Service $resource): Collection return array_search($key, $customOrder); }); + // 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; diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 0f5b6f553..effde712a 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -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(); diff --git a/config/constants.php b/config/constants.php index 503fe3808..770e00ffe 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,8 +2,8 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.439', - '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' => [ diff --git a/database/migrations/2025_11_02_161923_add_dev_helper_version_to_instance_settings.php b/database/migrations/2025_11_02_161923_add_dev_helper_version_to_instance_settings.php new file mode 100644 index 000000000..56ed2239a --- /dev/null +++ b/database/migrations/2025_11_02_161923_add_dev_helper_version_to_instance_settings.php @@ -0,0 +1,28 @@ +string('dev_helper_version')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('dev_helper_version'); + }); + } +}; diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index 212703798..14879eb96 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -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 diff --git a/jean.json b/jean.json new file mode 100644 index 000000000..4e5c788ed --- /dev/null +++ b/jean.json @@ -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" + } +} diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 2e5cc5e84..0d9519bf8 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.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" diff --git a/package-lock.json b/package-lock.json index 9e8fe7328..f8ef518d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", @@ -1431,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", @@ -1595,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", @@ -1609,6 +1610,7 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -1627,6 +1629,7 @@ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" } @@ -2388,7 +2391,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2465,7 +2467,6 @@ "integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tweetnacl": "^1.0.3" } @@ -2550,6 +2551,7 @@ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", @@ -2566,6 +2568,7 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2584,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" @@ -2598,6 +2602,7 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2646,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", @@ -2664,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", @@ -2716,7 +2720,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -2816,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", @@ -2839,6 +2841,7 @@ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -2860,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" } diff --git a/resources/css/app.css b/resources/css/app.css index fa1e61cb2..70759e542 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -82,7 +82,7 @@ @keyframes lds-heart { */ html, body { - @apply w-full min-h-full bg-neutral-50 dark:bg-base dark:text-neutral-400; + @apply w-full min-h-full bg-gray-50 dark:bg-base dark:text-neutral-400; } body { diff --git a/resources/css/utilities.css b/resources/css/utilities.css index f819280d5..5d8a6bfa1 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -152,7 +152,7 @@ @utility custom-modal { } @utility navbar-main { - @apply flex flex-col gap-4 justify-items-start pb-2 border-b-2 border-solid h-fit md:flex-row sm:justify-between dark:border-coolgray-200 border-neutral-200 md:items-center; + @apply flex flex-col gap-4 justify-items-start pb-2 border-b-2 border-solid h-fit md:flex-row sm:justify-between dark:border-coolgray-200 border-neutral-200 md:items-center text-neutral-700 dark:text-neutral-400; } @utility loading { diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index f85dc268e..ede49117a 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -61,7 +61,7 @@ class="text-sm dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning
- + Don't have an account?
@@ -82,7 +82,7 @@ class="block w-full text-center py-3 px-4 rounded-lg border border-neutral-300 d
- or + or continue with
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 3db943726..cdfa52a98 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -33,7 +33,8 @@ function getOldOrLocal($key, $localValue)

Root User Setup

-

This user will be the root user with full admin access.

+

This user will be the root user with full + admin access.

@@ -58,13 +59,16 @@ function getOldOrLocal($key, $localValue) -
+

- Your password should be min 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one symbol. + Your password should be min 8 characters long and contain at least one uppercase letter, + one lowercase letter, one number, and one symbol.

- + Create Account @@ -74,17 +78,18 @@ function getOldOrLocal($key, $localValue)
- + Already have an account?
- + {{ __('auth.already_registered') }} - + \ No newline at end of file diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php index a4a07ebd6..3e0c237b4 100644 --- a/resources/views/auth/reset-password.blade.php +++ b/resources/views/auth/reset-password.blade.php @@ -47,16 +47,19 @@ label="{{ __('input.email') }}" /> - + -
+

- Your password should be min 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one symbol. + Your password should be min 8 characters long and contain at least one uppercase letter, + one lowercase letter, one number, and one symbol.

- + {{ __('auth.reset_password') }} @@ -66,17 +69,18 @@
- + Remember your password?
- + Back to Login - + \ No newline at end of file diff --git a/resources/views/auth/two-factor-challenge.blade.php b/resources/views/auth/two-factor-challenge.blade.php index d4531cbe8..05dbcc90c 100644 --- a/resources/views/auth/two-factor-challenge.blade.php +++ b/resources/views/auth/two-factor-challenge.blade.php @@ -120,7 +120,7 @@ class="mt-2 text-sm dark:text-neutral-400 hover:text-black dark:hover:text-white
- + Need help?
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 0a7909761..0ef021458 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -17,7 +17,8 @@ localStorage.setItem('pageWidth', 'full'); } } - }" x-cloak class="mx-auto" :class="pageWidth === 'full' ? '' : 'max-w-7xl'"> + }" x-cloak class="mx-auto dark:text-inherit text-black" + :class="pageWidth === 'full' ? '' : 'max-w-7xl'"> -
+