Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,19 @@ Commands can use `server` or `servers` interchangeably.
- `--environment-name <name>` - Environment name (or use `--environment-uuid <uuid>`)

#### Application Previews
- `coolify app previews list <app_uuid>` - List preview deployments
- `coolify app previews get <app_uuid> <pr_id>` - Get a preview deployment
- `coolify app previews create <app_uuid> <pr_id>` - Open a preview deployment and queue its deployment (redeploys an existing preview)
- `--git-type <type>` - Git provider of the pull request: `github`, `gitlab`, `gitea`, or `bitbucket` (required unless the application uses a GitHub App or GitLab App source)
- `--commit <sha>` - Commit SHA to deploy (required for Bitbucket)
- `--docker-tag <tag>` - Docker image tag (Docker Image applications; required for a new preview)
- `--url <url>` - Pull request URL
- `--force` - Rebuild without cache
- `--no-deploy` - Only create the preview, do not queue a deployment
- `coolify app previews update <app_uuid> <pr_id>` - Replace the domains of a preview deployment
- `--domains <domains>` - Domains (comma-separated)
- `--compose-domain <service>=<url>` - Docker Compose service domain (repeatable)
- `--force-domain-override` - Save the domains even when another resource uses them
- `coolify app previews delete <app_uuid> <pr_id>` - Delete a preview deployment
- `--force` - Skip confirmation prompt

Expand Down
4 changes: 4 additions & 0 deletions cmd/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ func NewAppCommand() *cobra.Command {
Short: "Manage application preview deployments",
Long: `Manage preview deployments created from pull requests. Requires the application UUID.`,
}
previewsCmd.AddCommand(previews.NewListPreviewsCommand())
previewsCmd.AddCommand(previews.NewGetPreviewCommand())
previewsCmd.AddCommand(previews.NewCreatePreviewCommand())
previewsCmd.AddCommand(previews.NewUpdatePreviewCommand())
previewsCmd.AddCommand(previews.NewDeletePreviewCommand())
cmd.AddCommand(previewsCmd)

Expand Down
100 changes: 100 additions & 0 deletions cmd/application/previews/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package previews

import (
"fmt"

"github.com/spf13/cobra"

"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
)

func NewCreatePreviewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create <app_uuid> <pr_id>",
Aliases: []string{"open", "deploy"},
Short: "Open or redeploy a preview deployment",
Long: `Open a preview deployment for a pull request and queue its deployment.
When the preview already exists, it is deployed again.

Git based applications need --git-type, unless they use a GitHub App or GitLab App source.
Bitbucket pull requests also need --commit. Docker Image applications need --docker-tag for a new preview.`,
Example: ` coolify app previews create <app_uuid> 42 --git-type github
coolify app previews create <app_uuid> 42 --git-type bitbucket --commit 1a2b3c4d
coolify app previews create <app_uuid> 42 --docker-tag pr-42
coolify app previews create <app_uuid> 42 --git-type gitlab --no-deploy`,
Args: cli.ExactArgs(2, "<app_uuid> <pr_id>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

prID, err := parsePullRequestID(args[1])
if err != nil {
return err
}

req := models.ApplicationPreviewCreateRequest{PullRequestID: prID}
for flag, target := range map[string]**string{
"url": &req.PullRequestHTMLURL,
"git-type": &req.GitType,
"commit": &req.Commit,
"docker-tag": &req.DockerTag,
} {
if cmd.Flags().Changed(flag) {
value, _ := cmd.Flags().GetString(flag)
*target = &value
}
}
if cmd.Flags().Changed("force") {
force, _ := cmd.Flags().GetBool("force")
req.Force = &force
}
if noDeploy, _ := cmd.Flags().GetBool("no-deploy"); noDeploy {
instantDeploy := false
req.InstantDeploy = &instantDeploy
}

client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}

if err := cli.CheckMinimumVersion(ctx, client, minimumVersion); err != nil {
return err
}

appSvc := service.NewApplicationService(client)
resp, err := appSvc.CreatePreview(ctx, args[0], req)
if err != nil {
return fmt.Errorf("failed to create preview deployment: %w", err)
}

format, _ := cmd.Flags().GetString("format")
if format != output.FormatTable {
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(resp)
}

fmt.Println(resp.Message)
if resp.DeploymentUUID != nil && *resp.DeploymentUUID != "" {
fmt.Printf("Deployment UUID: %s\n", *resp.DeploymentUUID)
}
if resp.Preview.Domains != nil && *resp.Preview.Domains != "" {
fmt.Printf("Domains: %s\n", *resp.Preview.Domains)
}
return nil
},
}

cmd.Flags().String("url", "", "Pull request URL")
cmd.Flags().String("git-type", "", "Git provider of the pull request: github, gitlab, gitea, or bitbucket")
cmd.Flags().String("commit", "", "Commit SHA to deploy (required for Bitbucket)")
cmd.Flags().String("docker-tag", "", "Docker image tag (Docker Image applications only)")
cmd.Flags().Bool("force", false, "Rebuild without cache")
cmd.Flags().Bool("no-deploy", false, "Only create the preview, do not queue a deployment")
return cmd
}
51 changes: 51 additions & 0 deletions cmd/application/previews/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package previews

import (
"fmt"

"github.com/spf13/cobra"

"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
)

func NewGetPreviewCommand() *cobra.Command {
return &cobra.Command{
Use: "get <app_uuid> <pr_id>",
Short: "Get a preview deployment",
Long: `Get a preview deployment of an application by pull request ID.`,
Args: cli.ExactArgs(2, "<app_uuid> <pr_id>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

prID, err := parsePullRequestID(args[1])
if err != nil {
return err
}

client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}

if err := cli.CheckMinimumVersion(ctx, client, minimumVersion); err != nil {
return err
}

appSvc := service.NewApplicationService(client)
preview, err := appSvc.GetPreview(ctx, args[0], prID)
if err != nil {
return fmt.Errorf("failed to get preview deployment: %w", err)
}

format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}

return formatter.Format(preview)
},
}
}
17 changes: 17 additions & 0 deletions cmd/application/previews/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package previews

import (
"fmt"
"strconv"
)

// minimumVersion is the first Coolify version with the preview list, get, and create endpoints.
const minimumVersion = "4.4.0"

func parsePullRequestID(value string) (int, error) {
prID, err := strconv.Atoi(value)
if err != nil || prID <= 0 {
return 0, fmt.Errorf("invalid pr_id: must be a positive integer")
}
return prID, nil
}
46 changes: 46 additions & 0 deletions cmd/application/previews/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package previews

import (
"fmt"

"github.com/spf13/cobra"

"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
)

func NewListPreviewsCommand() *cobra.Command {
return &cobra.Command{
Use: "list <app_uuid>",
Short: "List preview deployments",
Long: `List the preview deployments of an application, newest pull request first.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}

if err := cli.CheckMinimumVersion(ctx, client, minimumVersion); err != nil {
return err
}

appSvc := service.NewApplicationService(client)
previews, err := appSvc.ListPreviews(ctx, args[0])
if err != nil {
return fmt.Errorf("failed to list preview deployments: %w", err)
}

format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}

return formatter.Format(previews)
},
}
}
81 changes: 81 additions & 0 deletions cmd/application/previews/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package previews

import (
"fmt"

"github.com/spf13/cobra"

"github.com/coollabsio/coolify-cli/cmd/application/appflags"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
)

func NewUpdatePreviewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <app_uuid> <pr_id>",
Short: "Update the domains of a preview deployment",
Long: `Replace the domains of a preview deployment.
Use --domains for regular applications and --compose-domain for Docker Compose applications.
Docker Compose domains are supplied as the complete service-to-domain mapping.
Redeploy the preview to apply the new domains.`,
Example: ` coolify app previews update <app_uuid> 42 --domains https://pr-42.example.com
coolify app previews update <app_uuid> 42 --compose-domain web=https://pr-42.example.com`,
Args: cli.ExactArgs(2, "<app_uuid> <pr_id>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

prID, err := parsePullRequestID(args[1])
if err != nil {
return err
}

req := models.ApplicationPreviewDomainsUpdateRequest{}
if cmd.Flags().Changed("domains") {
domains, _ := cmd.Flags().GetString("domains")
req.Domains = &domains
}
composeChanged, err := appflags.ApplyComposeDomainsFlag(cmd, &req.DockerComposeDomains)
if err != nil {
return err
}
if req.Domains == nil && !composeChanged {
return fmt.Errorf("set --domains or --compose-domain")
}
if cmd.Flags().Changed("force-domain-override") {
force, _ := cmd.Flags().GetBool("force-domain-override")
req.ForceDomainOverride = &force
}

client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}

if err := cli.CheckMinimumVersion(ctx, client, "4.3.15"); err != nil {
return err
}

appSvc := service.NewApplicationService(client)
resp, err := appSvc.UpdatePreviewDomains(ctx, args[0], prID, req)
if err != nil {
return fmt.Errorf("failed to update preview deployment: %w", err)
}

format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}

return formatter.Format(resp)
},
}

cmd.Flags().String("domains", "", "Domains (comma-separated)")
appflags.BindComposeDomainsFlag(cmd)
cmd.Flags().Bool("force-domain-override", false, "Save the domains even when another resource uses them")
cmd.MarkFlagsMutuallyExclusive("domains", "compose-domain")
return cmd
}
57 changes: 57 additions & 0 deletions internal/models/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,63 @@ type DockerComposeDomain struct {
Domain string `json:"domain"`
}

// ApplicationPreview represents a preview deployment of an application.
type ApplicationPreview struct {
UUID string `json:"uuid"`
PullRequestID int `json:"pull_request_id"`
Status string `json:"status"`
Domains *string `json:"domains"`
GitType *string `json:"git_type"`
DockerRegistryImageTag *string `json:"docker_registry_image_tag"`
PullRequestHTMLURL *string `json:"pull_request_html_url"`
DockerComposeDomains []PreviewComposeDomain `json:"docker_compose_domains" table:"-"`
DomainPortOverrides map[string]int `json:"domain_port_overrides" table:"-"`
LastOnlineAt *string `json:"last_online_at" table:"-"`
CreatedAt string `json:"created_at" table:"-"`
UpdatedAt string `json:"updated_at" table:"-"`
}

// PreviewComposeDomain is the domain of one Docker Compose service in a preview deployment.
type PreviewComposeDomain struct {
Name string `json:"name"`
Domain *string `json:"domain"`
Redirect *string `json:"redirect,omitempty"`
}

// ApplicationPreviewCreateRequest opens (or redeploys) a preview deployment.
type ApplicationPreviewCreateRequest struct {
PullRequestID int `json:"pull_request_id"`
PullRequestHTMLURL *string `json:"pull_request_html_url,omitempty"`
GitType *string `json:"git_type,omitempty"`
Commit *string `json:"commit,omitempty"`
DockerTag *string `json:"docker_tag,omitempty"`
Force *bool `json:"force,omitempty"`
InstantDeploy *bool `json:"instant_deploy,omitempty"`
}

// ApplicationPreviewDeploymentResponse is returned when a preview deployment is opened.
type ApplicationPreviewDeploymentResponse struct {
Message string `json:"message"`
DeploymentUUID *string `json:"deployment_uuid"`
Preview ApplicationPreview `json:"preview"`
}

// ApplicationPreviewDomainsUpdateRequest replaces the domains of a preview deployment.
type ApplicationPreviewDomainsUpdateRequest struct {
Domains *string `json:"domains,omitempty"`
DockerComposeDomains []DockerComposeDomain `json:"docker_compose_domains,omitempty"`
ForceDomainOverride *bool `json:"force_domain_override,omitempty"`
}

// ApplicationPreviewDomainsUpdateResponse is returned after preview domains are replaced.
type ApplicationPreviewDomainsUpdateResponse struct {
UUID string `json:"uuid"`
PullRequestID int `json:"pull_request_id"`
Domains *string `json:"domains"`
DockerComposeDomains []PreviewComposeDomain `json:"docker_compose_domains" table:"-"`
DomainPortOverrides map[string]int `json:"domain_port_overrides" table:"-"`
}

// ApplicationLifecycleResponse represents the response from lifecycle operations
type ApplicationLifecycleResponse struct {
Message string `json:"message"`
Expand Down
Loading
Loading