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..ba8a69d28 100644 --- a/.github/workflows/coolify-helper-next.yml +++ b/.github/workflows/coolify-helper-next.yml @@ -7,6 +7,10 @@ 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 @@ -15,11 +19,10 @@ env: jobs: amd64: runs-on: ubuntu-latest - permissions: - contents: read - packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -54,11 +57,10 @@ jobs: coolify.managed=true aarch64: runs-on: [ self-hosted, arm64 ] - permissions: - contents: read - packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -94,12 +96,12 @@ jobs: merge-manifest: runs-on: ubuntu-latest - permissions: - contents: read - packages: write needs: [ amd64, aarch64 ] 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 }} diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index 56c3eaa17..738a3480c 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -7,6 +7,10 @@ on: - .github/workflows/coolify-helper.yml - docker/coolify-helper/Dockerfile +permissions: + contents: read + packages: write + env: GITHUB_REGISTRY: ghcr.io DOCKER_REGISTRY: docker.io @@ -15,11 +19,10 @@ env: jobs: amd64: runs-on: ubuntu-latest - permissions: - contents: read - packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -54,11 +57,10 @@ jobs: coolify.managed=true aarch64: runs-on: [ self-hosted, arm64 ] - permissions: - contents: read - packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -93,12 +95,11 @@ jobs: coolify.managed=true merge-manifest: runs-on: ubuntu-latest - permissions: - contents: read - packages: write needs: [ amd64, aarch64 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml index cd1f002b8..b6cfd34ae 100644 --- a/.github/workflows/coolify-production-build.yml +++ b/.github/workflows/coolify-production-build.yml @@ -14,6 +14,10 @@ on: - templates/** - CHANGELOG.md +permissions: + contents: read + packages: write + env: GITHUB_REGISTRY: ghcr.io DOCKER_REGISTRY: docker.io @@ -23,7 +27,9 @@ jobs: amd64: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -58,7 +64,9 @@ jobs: aarch64: runs-on: [self-hosted, arm64] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -92,12 +100,11 @@ jobs: merge-manifest: runs-on: ubuntu-latest - permissions: - contents: read - packages: write needs: [amd64, aarch64] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml index ad590146b..7a6071bde 100644 --- a/.github/workflows/coolify-realtime-next.yml +++ b/.github/workflows/coolify-realtime-next.yml @@ -11,6 +11,10 @@ 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 @@ -19,11 +23,10 @@ env: jobs: amd64: runs-on: ubuntu-latest - permissions: - contents: read - packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -59,11 +62,11 @@ jobs: aarch64: runs-on: [ self-hosted, arm64 ] - permissions: - contents: read - packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -99,12 +102,11 @@ jobs: merge-manifest: runs-on: ubuntu-latest - permissions: - contents: read - packages: write needs: [ amd64, aarch64 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml index d00621cc2..1074af3ee 100644 --- a/.github/workflows/coolify-realtime.yml +++ b/.github/workflows/coolify-realtime.yml @@ -11,6 +11,10 @@ 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 @@ -19,11 +23,10 @@ env: jobs: amd64: runs-on: ubuntu-latest - permissions: - contents: read - packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -59,11 +62,10 @@ jobs: aarch64: runs-on: [ self-hosted, arm64 ] - permissions: - contents: read - packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -99,12 +101,11 @@ jobs: merge-manifest: runs-on: ubuntu-latest - permissions: - contents: read - packages: write needs: [ amd64, aarch64 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: docker/setup-buildx-action@v3 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..c4aecd85e 100644 --- a/.github/workflows/coolify-testing-host.yml +++ b/.github/workflows/coolify-testing-host.yml @@ -7,6 +7,10 @@ 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 @@ -15,11 +19,10 @@ env: jobs: amd64: runs-on: ubuntu-latest - permissions: - contents: read - packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -50,11 +53,10 @@ jobs: aarch64: runs-on: [ self-hosted, arm64 ] - permissions: - contents: read - packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 @@ -85,12 +87,11 @@ jobs: merge-manifest: runs-on: ubuntu-latest - permissions: - contents: read - packages: write needs: [ amd64, aarch64 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 + with: + persist-credentials: false - uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml index 935a88721..f62b41736 100644 --- a/.github/workflows/generate-changelog.yml +++ b/.github/workflows/generate-changelog.yml @@ -16,6 +16,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: + persist-credentials: false fetch-depth: 0 - name: Generate changelog 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/Console/Commands/Cloud/RestoreDatabase.php b/app/Console/Commands/Cloud/RestoreDatabase.php index 6c60d1c6c..7c6c0d4c6 100644 --- a/app/Console/Commands/Cloud/RestoreDatabase.php +++ b/app/Console/Commands/Cloud/RestoreDatabase.php @@ -1,6 +1,6 @@ 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/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index a240a759a..ea8cdff95 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -3226,6 +3226,20 @@ private function generate_secrets_hash($variables) return hash_hmac('sha256', $secrets_string, $this->secrets_hash_key); } + protected function findFromInstructionLines($dockerfile): array + { + $fromLines = []; + foreach ($dockerfile as $index => $line) { + $trimmedLine = trim($line); + // Check if line starts with FROM (case-insensitive) + if (preg_match('/^FROM\s+/i', $trimmedLine)) { + $fromLines[] = $index; + } + } + + return $fromLines; + } + private function add_build_env_variables_to_dockerfile() { if ($this->dockerBuildkitSupported) { @@ -3238,6 +3252,18 @@ private function add_build_env_variables_to_dockerfile() 'ignore_errors' => true, ]); $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); + + // Find all FROM instruction positions + $fromLines = $this->findFromInstructionLines($dockerfile); + + // If no FROM instructions found, skip ARG insertion + if (empty($fromLines)) { + return; + } + + // Collect all ARG statements to insert + $argsToInsert = collect(); + if ($this->pull_request_id === 0) { // Only add environment variables that are available during build $envs = $this->application->environment_variables() @@ -3246,9 +3272,9 @@ private function add_build_env_variables_to_dockerfile() ->get(); foreach ($envs as $env) { if (data_get($env, 'is_multiline') === true) { - $dockerfile->splice(1, 0, ["ARG {$env->key}"]); + $argsToInsert->push("ARG {$env->key}"); } else { - $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); + $argsToInsert->push("ARG {$env->key}={$env->real_value}"); } } // Add Coolify variables as ARGs @@ -3258,9 +3284,7 @@ private function add_build_env_variables_to_dockerfile() ->map(function ($var) { return "ARG {$var}"; }); - foreach ($coolify_vars as $arg) { - $dockerfile->splice(1, 0, [$arg]); - } + $argsToInsert = $argsToInsert->merge($coolify_vars); } } else { // Only add preview environment variables that are available during build @@ -3270,9 +3294,9 @@ private function add_build_env_variables_to_dockerfile() ->get(); foreach ($envs as $env) { if (data_get($env, 'is_multiline') === true) { - $dockerfile->splice(1, 0, ["ARG {$env->key}"]); + $argsToInsert->push("ARG {$env->key}"); } else { - $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); + $argsToInsert->push("ARG {$env->key}={$env->real_value}"); } } // Add Coolify variables as ARGs @@ -3282,15 +3306,23 @@ private function add_build_env_variables_to_dockerfile() ->map(function ($var) { return "ARG {$var}"; }); - foreach ($coolify_vars as $arg) { - $dockerfile->splice(1, 0, [$arg]); - } + $argsToInsert = $argsToInsert->merge($coolify_vars); } } - if ($envs->isNotEmpty()) { - $secrets_hash = $this->generate_secrets_hash($envs); - $dockerfile->splice(1, 0, ["ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"]); + // Insert ARGs after each FROM instruction (in reverse order to maintain correct line numbers) + if ($argsToInsert->isNotEmpty()) { + foreach (array_reverse($fromLines) as $fromLineIndex) { + // Insert all ARGs after this FROM instruction + foreach ($argsToInsert->reverse() as $arg) { + $dockerfile->splice($fromLineIndex + 1, 0, [$arg]); + } + } + $envs_mapped = $envs->mapWithKeys(function ($env) { + return [$env->key => $env->real_value]; + }); + $secrets_hash = $this->generate_secrets_hash($envs_mapped); + $argsToInsert->push("ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"); } $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index 371c860ca..a9a7de878 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -22,6 +22,12 @@ class EditDomain extends Component public $forceSaveDomains = false; + public $showPortWarningModal = false; + + public $forceRemovePort = false; + + public $requiredPort = null; + #[Validate(['nullable'])] public ?string $fqdn = null; @@ -33,6 +39,7 @@ public function mount() { $this->application = ServiceApplication::ownedByCurrentTeam()->findOrFail($this->applicationId); $this->authorize('view', $this->application); + $this->requiredPort = $this->application->service->getRequiredPort(); $this->syncData(); } @@ -58,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 { @@ -91,6 +111,41 @@ 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(); diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index 09392ab09..1d8d8b247 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -30,6 +30,12 @@ class ServiceApplicationView extends Component public $forceSaveDomains = false; + public $showPortWarningModal = false; + + public $forceRemovePort = false; + + public $requiredPort = null; + #[Validate(['nullable'])] public ?string $humanName = null; @@ -129,12 +135,26 @@ public function mount() try { $this->parameters = get_route_parameters(); $this->authorize('view', $this->application); + $this->requiredPort = $this->application->service->getRequiredPort(); $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } + 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 syncData(bool $toModel = false): void { if ($toModel) { @@ -246,6 +266,41 @@ 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(); diff --git a/app/Models/Service.php b/app/Models/Service.php index c4b8623e0..12d3d6a11 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); 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/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..1deec45d7 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -1164,13 +1164,21 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $environment = $environment->filter(function ($value, $key) { return ! str($key)->startsWith('SERVICE_FQDN_'); })->map(function ($value, $key) use ($resource) { - // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used - if (str($value)->isEmpty()) { - if ($resource->environment_variables()->where('key', $key)->exists()) { - $value = $resource->environment_variables()->where('key', $key)->first()->value; - } else { - $value = null; + // Preserve empty strings and null values with correct Docker Compose semantics: + // - Empty string: Variable is set to "" (e.g., HTTP_PROXY="" means "no proxy") + // - Null: Variable is unset/removed from container environment (may inherit from host) + if ($value === null) { + // User explicitly wants variable unset - respect that + // NEVER override from database - null means "inherit from environment" + // Keep as null (will be excluded from container environment) + } elseif ($value === '') { + // Empty string - allow database override for backward compatibility + $dbEnv = $resource->environment_variables()->where('key', $key)->first(); + // Only use database override if it exists AND has a non-empty value + if ($dbEnv && str($dbEnv->value)->isNotEmpty()) { + $value = $dbEnv->value; } + // Otherwise keep empty string as-is } return $value; @@ -1299,6 +1307,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 +1609,22 @@ function serviceParser(Service $resource): Collection ]); } if (substr_count(str($key)->value(), '_') === 3) { - $newKey = str($key)->beforeLast('_'); + // For port-specific variables (e.g., SERVICE_FQDN_UMAMI_3000), + // keep the port suffix in the key and use the URL with port $resource->environment_variables()->updateOrCreate([ - 'key' => $newKey->value(), + 'key' => $key->value(), 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, ], [ - 'value' => $fqdn, + 'value' => $fqdnWithPort, 'is_preview' => false, ]); $resource->environment_variables()->updateOrCreate([ - 'key' => $newKey->value(), + 'key' => $key->value(), 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, ], [ - 'value' => $url, + 'value' => $urlWithPort, 'is_preview' => false, ]); } @@ -2122,13 +2143,21 @@ function serviceParser(Service $resource): Collection $environment = $environment->filter(function ($value, $key) { return ! str($key)->startsWith('SERVICE_FQDN_'); })->map(function ($value, $key) use ($resource) { - // if value is empty, set it to null so if you set the environment variable in the .env file (Coolify's UI), it will used - if (str($value)->isEmpty()) { - if ($resource->environment_variables()->where('key', $key)->exists()) { - $value = $resource->environment_variables()->where('key', $key)->first()->value; - } else { - $value = null; + // Preserve empty strings and null values with correct Docker Compose semantics: + // - Empty string: Variable is set to "" (e.g., HTTP_PROXY="" means "no proxy") + // - Null: Variable is unset/removed from container environment (may inherit from host) + if ($value === null) { + // User explicitly wants variable unset - respect that + // NEVER override from database - null means "inherit from environment" + // Keep as null (will be excluded from container environment) + } elseif ($value === '') { + // Empty string - allow database override for backward compatibility + $dbEnv = $resource->environment_variables()->where('key', $key)->first(); + // Only use database override if it exists AND has a non-empty value + if ($dbEnv && str($dbEnv->value)->isNotEmpty()) { + $value = $dbEnv->value; } + // Otherwise keep empty string as-is } return $value; @@ -2251,6 +2280,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/config/constants.php b/config/constants.php index fd2adb860..02a1eaae6 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,8 +2,8 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.441', - 'helper_version' => '1.0.11', + 'version' => '4.0.0-beta.442', + '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/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/gcool.json b/jean.json similarity index 53% rename from gcool.json rename to jean.json index 629d8569a..c625e08c0 100644 --- a/gcool.json +++ b/jean.json @@ -1,6 +1,6 @@ { "scripts": { - "onWorktreeCreate": "cp $GCOOL_ROOT_PATH/.env .", + "onWorktreeCreate": "cp $JEAN_ROOT_PATH/.env . && mkdir -p .claude && cp $JEAN_ROOT_PATH/.claude/settings.local.json .claude/settings.local.json", "run": "docker rm -f coolify coolify-realtime coolify-minio coolify-testing-host coolify-redis coolify-db coolify-mail coolify-vite && spin up" } } diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 5d070a6bb..a83b4c8ce 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.441" + "version": "4.0.0-beta.442" }, "nightly": { - "version": "4.0.0-beta.442" + "version": "4.0.0-beta.443" }, "helper": { "version": "1.0.11" 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/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
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.
- 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.
- 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.