Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ Commands can use `server` or `servers` interchangeably.
- `--environment-name <name>` - Environment name (or use `--environment-uuid <uuid>`)

#### Application Previews
- `coolify app previews update <app_uuid> <pr_id> --domains <urls>` - Update regular preview domains
- `coolify app previews update <app_uuid> <pr_id> --compose-domain <service=urls>` - Update Docker Compose preview domains
- `coolify app previews delete <app_uuid> <pr_id>` - Delete a preview deployment
- `--force` - Skip confirmation prompt

Expand Down
1 change: 1 addition & 0 deletions cmd/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func NewAppCommand() *cobra.Command {
Long: `Manage preview deployments created from pull requests. Requires the application UUID.`,
}
previewsCmd.AddCommand(previews.NewDeletePreviewCommand())
previewsCmd.AddCommand(previews.NewUpdatePreviewCommand())
cmd.AddCommand(previewsCmd)

return cmd
Expand Down
18 changes: 18 additions & 0 deletions cmd/application/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ func TestNewApplicationCommands_ExposeParityFlags(t *testing.T) {
assert.NotNil(t, NewLogsCommand().Flags().Lookup("service"))
assert.NotNil(t, NewMoveCommand().Flags().Lookup("environment-uuid"))
assert.NotNil(t, NewUpdateCommand().Flags().Lookup("compose-domain"))
previews := NewAppCommand().Commands()
var previewsCommand *cobra.Command
for _, command := range previews {
if command.Name() == "previews" {
previewsCommand = command
}
}
require.NotNil(t, previewsCommand)
var updatePreviewCommand *cobra.Command
for _, command := range previewsCommand.Commands() {
if command.Name() == "update" {
updatePreviewCommand = command
}
}
require.NotNil(t, updatePreviewCommand)
assert.NotNil(t, updatePreviewCommand.Flags().Lookup("domains"))
assert.NotNil(t, updatePreviewCommand.Flags().Lookup("compose-domain"))
assert.NotNil(t, updatePreviewCommand.Flags().Lookup("force-domain-override"))

for _, flag := range []string{
"disable-build-cache", "docker-images-to-keep", "include-source-commit-in-build",
Expand Down
79 changes: 79 additions & 0 deletions cmd/application/previews/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package previews

import (
"fmt"
"strconv"

"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 preview deployment domains",
Long: `Replace the domains for an application preview deployment.

Use --domains for regular applications. Use repeatable --compose-domain flags for Docker Compose applications.
Include an internal port in a URL when needed; Coolify stores the public domain without the port.

Examples:
coolify app previews update <app_uuid> 42 --domains "https://preview.example.com:3000"
coolify app previews update <app_uuid> 42 --compose-domain "web=https://web.example.com:8080" --compose-domain "api=https://api.example.com:3000"`,
Args: cli.ExactArgs(2, "<app_uuid> <pr_id>"),
RunE: func(cmd *cobra.Command, args []string) error {
prID := args[1]
parsedPRID, err := strconv.Atoi(prID)
if err != nil || parsedPRID <= 0 {
return fmt.Errorf("invalid pr_id: must be a positive integer")
}

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

client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := cli.CheckMinimumVersion(cmd.Context(), client, "4.0.0-beta.474"); err != nil {
return err
}

preview, err := service.NewApplicationService(client).UpdatePreview(cmd.Context(), 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(preview)
},
}

cmd.Flags().String("domains", "", "Domains for a regular application preview (comma-separated)")
appflags.BindComposeDomainsFlag(cmd)
cmd.Flags().Bool("force-domain-override", false, "Allow domains already used by another resource")
cmd.MarkFlagsMutuallyExclusive("domains", "compose-domain")

return cmd
}
48 changes: 48 additions & 0 deletions cmd/application/previews/update_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package previews

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestUpdatePreviewCommand_RequiresDomains(t *testing.T) {
cmd := NewUpdatePreviewCommand()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetArgs([]string{"app-uuid", "42"})

err := cmd.Execute()

require.Error(t, err)
assert.Contains(t, err.Error(), "one of --domains or --compose-domain is required")
}

func TestUpdatePreviewCommand_RejectsInvalidPullRequestID(t *testing.T) {
cmd := NewUpdatePreviewCommand()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetArgs([]string{"app-uuid", "1.9", "--domains", "https://preview.example.com"})

err := cmd.Execute()

require.Error(t, err)
assert.Contains(t, err.Error(), "invalid pr_id")
}

func TestUpdatePreviewCommand_RejectsDomainsWithComposeDomain(t *testing.T) {
cmd := NewUpdatePreviewCommand()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetArgs([]string{
"app-uuid", "42",
"--domains", "https://preview.example.com",
"--compose-domain", "web=https://web.example.com",
})

err := cmd.Execute()

require.Error(t, err)
assert.Contains(t, err.Error(), "group [domains compose-domain]")
}
14 changes: 14 additions & 0 deletions internal/models/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,20 @@ type DockerComposeDomain struct {
Domain string `json:"domain"`
}

type ApplicationPreviewUpdateRequest struct {
Domains *string `json:"domains,omitempty"`
DockerComposeDomains []DockerComposeDomain `json:"docker_compose_domains,omitempty"`
ForceDomainOverride bool `json:"force_domain_override,omitempty"`
}

type ApplicationPreviewUpdateResponse struct {
UUID string `json:"uuid"`
PullRequestID int `json:"pull_request_id"`
Domains *string `json:"domains"`
DockerComposeDomains []DockerComposeDomain `json:"docker_compose_domains"`
DomainPortOverrides map[string]int `json:"domain_port_overrides"`
}

// ApplicationLifecycleResponse represents the response from lifecycle operations
type ApplicationLifecycleResponse struct {
Message string `json:"message"`
Expand Down
27 changes: 27 additions & 0 deletions internal/models/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,33 @@ func TestDockerComposeDomains_OmittedWhenUnset(t *testing.T) {
assert.NotContains(t, string(data), "docker_compose_domains")
}

func TestApplicationPreviewUpdateRequest_MarshalComposeDomains(t *testing.T) {
req := ApplicationPreviewUpdateRequest{
DockerComposeDomains: []DockerComposeDomain{{Name: "web", Domain: "https://web.example.com:8080"}},
}

data, err := json.Marshal(req)
require.NoError(t, err)

var body map[string]any
require.NoError(t, json.Unmarshal(data, &body))
assert.NotContains(t, body, "domains")
assert.Equal(t, []any{map[string]any{
"name": "web", "domain": "https://web.example.com:8080",
}}, body["docker_compose_domains"])
}

func TestApplicationUpdateRequest_MarshalsExplicitlyEmptyPortsExposes(t *testing.T) {
empty := ""
data, err := json.Marshal(ApplicationUpdateRequest{PortsExposes: &empty})
require.NoError(t, err)

var body map[string]any
require.NoError(t, json.Unmarshal(data, &body))
assert.Contains(t, body, "ports_exposes")
assert.Equal(t, "", body["ports_exposes"])
}

func TestEnvironmentVariableRequests_MarshalBuildtimeField(t *testing.T) {
buildTime := false
runtime := true
Expand Down
11 changes: 11 additions & 0 deletions internal/service/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ func (s *ApplicationService) DeletePreview(ctx context.Context, appUUID, prID st
return nil
}

// UpdatePreview replaces the domains for an application preview deployment.
func (s *ApplicationService) UpdatePreview(ctx context.Context, appUUID, prID string, req models.ApplicationPreviewUpdateRequest) (*models.ApplicationPreviewUpdateResponse, error) {
var preview models.ApplicationPreviewUpdateResponse
err := s.client.Patch(ctx, fmt.Sprintf("applications/%s/previews/%s", appUUID, prID), req, &preview)
if err != nil {
return nil, fmt.Errorf("failed to update preview %s for application %s: %w", prID, appUUID, err)
}

return &preview, nil
}

// Start starts an application (initiates deployment)
func (s *ApplicationService) Start(ctx context.Context, uuid string, force bool, instantDeploy bool) (*models.ApplicationLifecycleResponse, error) {
var resp models.ApplicationLifecycleResponse
Expand Down
29 changes: 29 additions & 0 deletions internal/service/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,35 @@ func TestApplicationService_DeletePreview_Success(t *testing.T) {
require.NoError(t, err)
}

func TestApplicationService_UpdatePreview_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-uuid-123/previews/42", r.URL.Path)
assert.Equal(t, "PATCH", r.Method)

var body models.ApplicationPreviewUpdateRequest
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
require.NotNil(t, body.Domains)
assert.Equal(t, "https://preview.example.com:3000", *body.Domains)
assert.True(t, body.ForceDomainOverride)

w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"uuid":"preview-uuid","pull_request_id":42,"domains":"https://preview.example.com","docker_compose_domains":null,"domain_port_overrides":{"https://preview.example.com":3000}}`))
}))
defer server.Close()

client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
domains := "https://preview.example.com:3000"
result, err := svc.UpdatePreview(context.Background(), "app-uuid-123", "42", models.ApplicationPreviewUpdateRequest{
Domains: &domains,
ForceDomainOverride: true,
})

require.NoError(t, err)
assert.Equal(t, "preview-uuid", result.UUID)
assert.Equal(t, 3000, result.DomainPortOverrides["https://preview.example.com"])
}

func TestApplicationService_DeletePreview_NotFound(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
Expand Down
18 changes: 17 additions & 1 deletion llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1585,6 +1585,23 @@ Parameters:
required: false
default: false

Command: coolify app previews update <app_uuid> <pr_id>
Description: Replace regular or Docker Compose preview domains, optionally assigning an internal port in each URL.
Parameters:
- name: --domains
type: string
description: Comma-separated domains for a regular application preview
required: false
- name: --compose-domain
type: stringArray
description: Docker Compose domain in service=url format (repeatable)
required: false
- name: --force-domain-override
type: boolean
description: Allow domains already used by another resource
required: false
default: false

Command: coolify app restart <uuid>
Description: Restart a running application.
Parameters: (None)
Expand Down Expand Up @@ -5033,4 +5050,3 @@ Parameters: (None)
Command: coolify version
Description: Current Coolify CLI version
Parameters: (None)

Loading