Compare commits

...

27 Commits

Author SHA1 Message Date
github-actions[bot] 780b3674c7 chore: bump version to v1.2 2025-12-05 12:38:45 +00:00
Andras Bacsai 77adbfaebc Merge pull request #46 from coollabsio/update-check-every-cmd
Check for CLI updates on every command
2025-12-05 13:35:52 +01:00
Andras Bacsai 9215fd537e feat: check for CLI updates on every command
Remove the 10-minute check interval so update notifications appear on every command execution. Silent error handling prevents network issues from interrupting commands. Updated message format is more concise and shows the available version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 13:35:05 +01:00
github-actions[bot] 26c0925854 chore: bump version to v1.1 2025-12-05 12:04:57 +00:00
Andras Bacsai 1f1b187ed2 Merge pull request #45 from coollabsio/add-runtime-env-flag
Add runtime env flag and improve service env handling
2025-12-05 13:01:46 +01:00
Andras Bacsai 4af598c213 fix: use is_buildtime JSON tag to match Coolify API response
The Coolify API returns `is_buildtime` (without underscore between
build and time) in responses. Updated service tests to use the correct
field name.

Also simplified application env get command by removing preview filter.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 12:58:18 +01:00
Andras Bacsai 6ca3b700ce fix: resolve lint errors for stuttering type names
Move ServiceBulkUpdateEnvsRequest and ServiceBulkUpdateEnvsResponse
to models package as ServiceEnvBulkUpdateRequest/Response to avoid
the "type name stutters" lint error from revive.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 12:00:41 +01:00
Andras Bacsai 8cf0b71ebf feat: add runtime env flag and improve service env handling
- Add --runtime flag to all env commands (create, update, sync) for both apps and services
- Make --runtime and --build-time flags default to true
- Remove is_preview field from service environment variables (services don't have preview)
- Create ServiceEnvironmentVariable model without preview support
- Wire up service env commands in service.go
- Add --preview filter option to app env list and get commands
- Add --all flag to app env list to show all variables (non-preview first)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 11:54:22 +01:00
github-actions[bot] 99a40bfa1d chore: bump version to v1.0.5 2025-11-27 08:20:33 +00:00
Andras Bacsai 188834fd6d Merge pull request #39 from YaRissi/fix/version
fix: update  release workflow
2025-11-27 09:17:39 +01:00
Andras Bacsai f9c3b9869a Merge pull request #43 from coollabsio/dependabot/go_modules/golang.org/x/crypto-0.45.0
chore(deps): bump golang.org/x/crypto from 0.43.0 to 0.45.0
2025-11-27 09:16:36 +01:00
dependabot[bot] 6044a2107e chore(deps): bump golang.org/x/crypto from 0.43.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.43.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.43.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 08:16:04 +00:00
Andras Bacsai 2f9dc6e8d7 Merge pull request #42 from coollabsio/fix-env-is-build-time-value
Fix is_buildtime JSON tag and add is_runtime, is_shared fields
2025-11-27 09:15:01 +01:00
Andras Bacsai 1e741309cb fix: correct is_buildtime JSON tag and add is_runtime, is_shared fields
Fixed critical bug where is_buildtime field was not unmarshaling from API
responses due to JSON tag mismatch (was expecting 'is_build_time' with
underscore but API returns 'is_buildtime' without underscore). Also added
missing is_runtime and is_shared fields that are present in API responses.

Added comprehensive tests for EnvironmentVariable model and service layer
to ensure proper marshaling/unmarshaling of all fields. Achieved 100%
coverage for EnvironmentVariable struct.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 09:11:35 +01:00
YaRissi 51f759c38f fix: improve error handling in Stop-WithError function and update git tag push command 2025-11-20 02:34:22 +01:00
Andras Bacsai 51e9ec5ec8 Update install.sh 2025-11-18 23:25:17 +01:00
Andras Bacsai e071fd81d4 Merge branch 'v4.x' into fix/version 2025-11-18 23:19:32 +01:00
Andras Bacsai cb0bbfc5cb Merge pull request #41 from ncryptedV1/fix/install-script-release-download-link
fix: leading 'v' for release filename of install script
2025-11-18 23:18:53 +01:00
ncryptedV1 87b6b8fdf7 fix: leading 'v' for release filename of install script 2025-11-18 17:24:25 +01:00
YaRissi 3dbe2507f4 fix deprecated builds tag 2025-11-16 16:39:08 +01:00
YaRissi 1e82217a50 feat: update installation instructions for Windows and add PowerShell script 2025-11-16 16:29:27 +01:00
YaRissi 6f33fa00f1 fix: readd version in binary name 2025-11-16 16:06:03 +01:00
YaRissi d7841b3b5a chore: readd version in binaryname 2025-11-16 15:59:41 +01:00
YaRissi 234f6e9ed6 fix: update binary name in installation script 2025-11-10 21:45:31 +01:00
YaRissi fa86ceb5cc fix: update filename format in download function for consistency 2025-11-10 21:37:22 +01:00
YaRissi 646bf9de36 fix: update version tagging logic in release workflow 2025-11-10 21:07:02 +01:00
github-actions[bot] be29a6e05d chore: bump version to v1.0.4 2025-11-10 13:24:40 +00:00
26 changed files with 1225 additions and 137 deletions
+5
View File
@@ -49,3 +49,8 @@ jobs:
git add internal/version/checker.go
git commit -m "chore: bump version to $TAG"
git push origin v4.x
# Move the tag to point to the new commit with updated version
git tag -d "$TAG" || true
git tag "$TAG"
git push origin "refs/tags/$TAG" --force
+15 -1
View File
@@ -22,4 +22,18 @@ builds:
- amd64
- arm64
env:
- CGO_ENABLED=0
- CGO_ENABLED=0
checksum:
name_template: checksums.txt
algorithm: sha256
archives:
- id: coolify-archive
ids:
- coolify
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
format_overrides:
- goos: windows
formats: [zip]
+20
View File
@@ -4,12 +4,32 @@
### Install script (recommended)
#### Linux/macOS
```bash
curl -fsSL https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.sh | bash
```
It will install the CLI in `/usr/local/bin/coolify` and the configuration file in `~/.config/coolify/config.json`
#### Windows (PowerShell)
```powershell
irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
```
It will install the CLI in `%ProgramFiles%\Coolify\coolify.exe` and the configuration file in `%USERPROFILE%\.config\coolify\config.json`
For user installation (no admin rights required):
```powershell
$env:COOLIFY_USER_INSTALL=1; irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
```
For a specific version:
```powershell
$env:COOLIFY_VERSION='v1.0.0'; irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
```
### Using `go install`
```bash
+6 -1
View File
@@ -31,6 +31,7 @@ func NewCreateEnvCommand() *cobra.Command {
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
isRuntime, _ := cmd.Flags().GetBool("runtime")
if key == "" {
return fmt.Errorf("--key is required")
@@ -56,6 +57,9 @@ func NewCreateEnvCommand() *cobra.Command {
if cmd.Flags().Changed("is-multiline") {
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("runtime") {
req.IsRuntime = &isRuntime
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.CreateEnv(ctx, appUUID, req)
@@ -71,9 +75,10 @@ func NewCreateEnvCommand() *cobra.Command {
cmd.Flags().String("key", "", "Environment variable key (required)")
cmd.Flags().String("value", "", "Environment variable value (required)")
cmd.Flags().Bool("build-time", false, "Available at build time")
cmd.Flags().Bool("build-time", true, "Available at build time (default: true)")
cmd.Flags().Bool("preview", false, "Available in preview deployments")
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
return cmd
}
+5 -1
View File
@@ -11,7 +11,7 @@ import (
)
func NewGetEnvCommand() *cobra.Command {
return &cobra.Command{
cmd := &cobra.Command{
Use: "get <app_uuid> <env_uuid_or_key>",
Short: "Get environment variable details",
Long: `Get detailed information about a specific environment variable by UUID or key name.`,
@@ -27,6 +27,8 @@ func NewGetEnvCommand() *cobra.Command {
}
appSvc := service.NewApplicationService(client)
// First try to get by the identifier directly
env, err := appSvc.GetEnv(ctx, appUUID, envUUIDOrKey)
if err != nil {
return fmt.Errorf("failed to get environment variable: %w", err)
@@ -53,4 +55,6 @@ func NewGetEnvCommand() *cobra.Command {
return formatter.Format(env)
},
}
return cmd
}
+32 -2
View File
@@ -2,19 +2,21 @@ package env
import (
"fmt"
"sort"
"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 NewListEnvCommand() *cobra.Command {
return &cobra.Command{
cmd := &cobra.Command{
Use: "list <app_uuid>",
Short: "List all environment variables for an application",
Long: `List all environment variables for a specific application.`,
Long: `List all environment variables for a specific application. By default, only non-preview environment variables are shown. Use --preview to show preview environment variables instead, or --all to show all variables (non-preview first, then preview).`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
@@ -31,6 +33,29 @@ func NewListEnvCommand() *cobra.Command {
return fmt.Errorf("failed to list environment variables: %w", err)
}
// Filter by preview/all flags
showAll, _ := cmd.Flags().GetBool("all")
showPreview, _ := cmd.Flags().GetBool("preview")
if showAll {
// Sort: non-preview first, then preview
sort.SliceStable(envs, func(i, j int) bool {
if envs[i].IsPreview != envs[j].IsPreview {
return !envs[i].IsPreview // non-preview (false) comes before preview (true)
}
return false // maintain original order within groups
})
} else {
// Filter by preview flag
var filtered []models.EnvironmentVariable
for _, env := range envs {
if env.IsPreview == showPreview {
filtered = append(filtered, env)
}
}
envs = filtered
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
@@ -54,4 +79,9 @@ func NewListEnvCommand() *cobra.Command {
return formatter.Format(envs)
},
}
cmd.Flags().Bool("preview", false, "Show preview environment variables instead of regular ones")
cmd.Flags().Bool("all", false, "Show all environment variables (non-preview first, then preview)")
return cmd
}
+6 -1
View File
@@ -40,6 +40,7 @@ Example: coolify app env sync abc123 --file .env.production`,
isBuildTime, _ := cmd.Flags().GetBool("build-time")
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
isRuntime, _ := cmd.Flags().GetBool("runtime")
// Parse the .env file
envVars, err := parser.ParseEnvFile(filePath)
@@ -87,6 +88,9 @@ Example: coolify app env sync abc123 --file .env.production`,
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("runtime") {
req.IsRuntime = &isRuntime
}
// Auto-detect multiline values
if strings.Contains(envVar.Value, "\n") {
@@ -147,8 +151,9 @@ Example: coolify app env sync abc123 --file .env.production`,
}
syncEnvCmd.Flags().StringP("file", "f", "", "Path to .env file (required)")
syncEnvCmd.Flags().Bool("build-time", false, "Make all variables available at build time")
syncEnvCmd.Flags().Bool("build-time", true, "Make all variables available at build time (default: true)")
syncEnvCmd.Flags().Bool("preview", false, "Make all variables available in preview deployments")
syncEnvCmd.Flags().Bool("is-literal", false, "Treat all values as literal (don't interpolate variables)")
syncEnvCmd.Flags().Bool("runtime", true, "Make all variables available at runtime (default: true)")
return syncEnvCmd
}
+7 -2
View File
@@ -54,8 +54,12 @@ func NewUpdateEnvCommand() *cobra.Command {
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("runtime") {
isRuntime, _ := cmd.Flags().GetBool("runtime")
req.IsRuntime = &isRuntime
}
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsPreview == nil && req.IsLiteral == nil && req.IsMultiline == nil {
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsPreview == nil && req.IsLiteral == nil && req.IsMultiline == nil && req.IsRuntime == nil {
return fmt.Errorf("at least one field must be provided to update")
}
@@ -72,9 +76,10 @@ func NewUpdateEnvCommand() *cobra.Command {
cmd.Flags().String("key", "", "New environment variable key")
cmd.Flags().String("value", "", "New environment variable value")
cmd.Flags().Bool("build-time", false, "Available at build time")
cmd.Flags().Bool("build-time", true, "Available at build time (default: true)")
cmd.Flags().Bool("preview", false, "Available in preview deployments")
cmd.Flags().Bool("is-literal", false, "Treat value as literal")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
return cmd
}
+2 -21
View File
@@ -6,7 +6,6 @@ import (
"log"
"os"
compareVersion "github.com/hashicorp/go-version"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -144,24 +143,6 @@ func initConfig() {
// They are loaded on-demand by getAPIClient() based on --instance or default instance
// This allows --instance flag to work correctly
// Check for updates
latestVersionStr, err := version.CheckLatestVersionOfCli(Debug)
if err != nil {
if Debug {
log.Println("Failed to check for updates:", err)
}
}
// Compare versions properly using semantic versioning
if latestVersionStr != "" {
latestVersion, err := compareVersion.NewVersion(latestVersionStr)
if err == nil {
currentVersion, err := compareVersion.NewVersion(version.GetVersion())
if err == nil && latestVersion.GreaterThan(currentVersion) {
if Debug {
log.Printf("New version of Coolify CLI is available: %s\n", latestVersionStr)
}
}
}
}
// Check for updates (errors are handled silently inside the function)
_, _ = version.CheckLatestVersionOfCli(Debug)
}
+7 -7
View File
@@ -28,9 +28,9 @@ func NewCreateCommand() *cobra.Command {
key, _ := cmd.Flags().GetString("key")
value, _ := cmd.Flags().GetString("value")
isBuildTime, _ := cmd.Flags().GetBool("build-time")
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
isRuntime, _ := cmd.Flags().GetBool("runtime")
if key == "" {
return fmt.Errorf("--key is required")
@@ -39,7 +39,7 @@ func NewCreateCommand() *cobra.Command {
return fmt.Errorf("--value is required")
}
req := &models.EnvironmentVariableCreateRequest{
req := &models.ServiceEnvironmentVariableCreateRequest{
Key: key,
Value: value,
}
@@ -48,15 +48,15 @@ func NewCreateCommand() *cobra.Command {
if cmd.Flags().Changed("build-time") {
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("is-multiline") {
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("runtime") {
req.IsRuntime = &isRuntime
}
serviceSvc := service.NewService(client)
env, err := serviceSvc.CreateEnv(ctx, uuid, req)
@@ -71,10 +71,10 @@ func NewCreateCommand() *cobra.Command {
cmd.Flags().String("key", "", "Environment variable key (required)")
cmd.Flags().String("value", "", "Environment variable value (required)")
cmd.Flags().Bool("build-time", false, "Available at build time")
cmd.Flags().Bool("preview", false, "Available in preview deployments")
cmd.Flags().Bool("build-time", true, "Available at build time (default: true)")
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
return cmd
}
+11 -11
View File
@@ -38,8 +38,8 @@ Example: coolify service env sync abc123 --file .env.production`,
}
isBuildTime, _ := cmd.Flags().GetBool("build-time")
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
isRuntime, _ := cmd.Flags().GetBool("runtime")
// Parse the .env file
envVars, err := parser.ParseEnvFile(filePath)
@@ -62,17 +62,17 @@ Example: coolify service env sync abc123 --file .env.production`,
}
// Build a map of existing env vars by key
existingMap := make(map[string]models.EnvironmentVariable)
existingMap := make(map[string]models.ServiceEnvironmentVariable)
for _, env := range existingEnvs {
existingMap[env.Key] = env
}
// Separate into updates and creates
var toUpdate []models.EnvironmentVariableCreateRequest
var toCreate []models.EnvironmentVariableCreateRequest
var toUpdate []models.ServiceEnvironmentVariableCreateRequest
var toCreate []models.ServiceEnvironmentVariableCreateRequest
for _, envVar := range envVars {
req := models.EnvironmentVariableCreateRequest{
req := models.ServiceEnvironmentVariableCreateRequest{
Key: envVar.Key,
Value: envVar.Value,
}
@@ -81,12 +81,12 @@ Example: coolify service env sync abc123 --file .env.production`,
if cmd.Flags().Changed("build-time") {
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("runtime") {
req.IsRuntime = &isRuntime
}
// Auto-detect multiline values
if strings.Contains(envVar.Value, "\n") {
@@ -108,7 +108,7 @@ Example: coolify service env sync abc123 --file .env.production`,
// Perform bulk update if there are vars to update
if len(toUpdate) > 0 {
fmt.Printf("Updating %d existing variables...\n", len(toUpdate))
bulkReq := &service.BulkUpdateEnvsRequest{
bulkReq := &models.ServiceEnvBulkUpdateRequest{
Data: toUpdate,
}
_, err := serviceSvc.BulkUpdateEnvs(ctx, uuid, bulkReq)
@@ -147,9 +147,9 @@ Example: coolify service env sync abc123 --file .env.production`,
}
cmd.Flags().StringP("file", "f", "", "Path to .env file (required)")
cmd.Flags().Bool("build-time", false, "Make all variables available at build time")
cmd.Flags().Bool("preview", false, "Make all variables available in preview deployments")
cmd.Flags().Bool("build-time", true, "Make all variables available at build time (default: true)")
cmd.Flags().Bool("is-literal", false, "Treat all values as literal (don't interpolate variables)")
cmd.Flags().Bool("runtime", true, "Make all variables available at runtime (default: true)")
return cmd
}
+9 -9
View File
@@ -26,7 +26,7 @@ func NewUpdateCommand() *cobra.Command {
return fmt.Errorf("failed to get API client: %w", err)
}
req := &models.EnvironmentVariableUpdateRequest{
req := &models.ServiceEnvironmentVariableUpdateRequest{
UUID: envUUID,
}
@@ -43,10 +43,6 @@ func NewUpdateCommand() *cobra.Command {
isBuildTime, _ := cmd.Flags().GetBool("build-time")
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
isPreview, _ := cmd.Flags().GetBool("preview")
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
isLiteral, _ := cmd.Flags().GetBool("is-literal")
req.IsLiteral = &isLiteral
@@ -55,10 +51,14 @@ func NewUpdateCommand() *cobra.Command {
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("runtime") {
isRuntime, _ := cmd.Flags().GetBool("runtime")
req.IsRuntime = &isRuntime
}
// Check if at least one field is being updated
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsPreview == nil && req.IsLiteral == nil && req.IsMultiline == nil {
return fmt.Errorf("at least one field must be provided to update (--key, --value, --build-time, --preview, --is-literal, or --is-multiline)")
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsLiteral == nil && req.IsMultiline == nil && req.IsRuntime == nil {
return fmt.Errorf("at least one field must be provided to update (--key, --value, --build-time, --is-literal, --is-multiline, or --runtime)")
}
serviceSvc := service.NewService(client)
@@ -74,10 +74,10 @@ func NewUpdateCommand() *cobra.Command {
cmd.Flags().String("key", "", "New environment variable key")
cmd.Flags().String("value", "", "New environment variable value")
cmd.Flags().Bool("build-time", false, "Available at build time")
cmd.Flags().Bool("preview", false, "Available in preview deployments")
cmd.Flags().Bool("build-time", true, "Available at build time (default: true)")
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
return cmd
}
+17 -10
View File
@@ -1,6 +1,10 @@
package service
import "github.com/spf13/cobra"
import (
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/cmd/service/env"
)
// NewServiceCommand creates the service parent command with all subcommands
func NewServiceCommand() *cobra.Command {
@@ -19,15 +23,18 @@ func NewServiceCommand() *cobra.Command {
cmd.AddCommand(NewRestartCommand())
cmd.AddCommand(NewDeleteCommand())
// Add env subcommand (placeholder for now)
// TODO: Implement env commands
// envCmd := &cobra.Command{
// Use: "env",
// Short: "Manage service environment variables",
// }
// envCmd.AddCommand(env.NewListCommand())
// ... more env commands
// cmd.AddCommand(envCmd)
// Add env subcommand
envCmd := &cobra.Command{
Use: "env",
Short: "Manage service environment variables",
}
envCmd.AddCommand(env.NewListCommand())
envCmd.AddCommand(env.NewGetCommand())
envCmd.AddCommand(env.NewCreateCommand())
envCmd.AddCommand(env.NewUpdateCommand())
envCmd.AddCommand(env.NewDeleteCommand())
envCmd.AddCommand(env.NewSyncCommand())
cmd.AddCommand(envCmd)
return cmd
}
+3 -3
View File
@@ -37,10 +37,10 @@ require (
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/xanzy/go-gitlab v0.115.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+8 -8
View File
@@ -86,8 +86,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -97,15 +97,15 @@ golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+5 -1
View File
@@ -100,10 +100,12 @@ type EnvironmentVariable struct {
UUID string `json:"uuid"`
Key string `json:"key"`
Value string `json:"value" sensitive:"true"`
IsBuildTime bool `json:"is_build_time"`
IsBuildTime bool `json:"is_buildtime"`
IsPreview bool `json:"is_preview"`
IsLiteralValue bool `json:"is_literal"`
IsShownOnce bool `json:"is_shown_once"`
IsRuntime bool `json:"is_runtime"`
IsShared bool `json:"is_shared"`
RealValue *string `json:"real_value,omitempty" sensitive:"true"`
ApplicationID *int `json:"-" table:"-"`
CreatedAt string `json:"-" table:"-"`
@@ -118,6 +120,7 @@ type EnvironmentVariableCreateRequest struct {
IsPreview *bool `json:"is_preview,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
IsRuntime *bool `json:"is_runtime,omitempty"`
}
// EnvironmentVariableUpdateRequest represents the request to update an environment variable
@@ -129,4 +132,5 @@ type EnvironmentVariableUpdateRequest struct {
IsPreview *bool `json:"is_preview,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
IsRuntime *bool `json:"is_runtime,omitempty"`
}
+130
View File
@@ -227,3 +227,133 @@ func TestServerCreateRequest_Marshal(t *testing.T) {
assert.Equal(t, request.Port, unmarshaled.Port)
assert.True(t, unmarshaled.InstantValidate)
}
func TestEnvironmentVariable_IsBuildtimeField(t *testing.T) {
// Test that is_buildtime (without underscore) unmarshals correctly
jsonData := `{
"uuid": "env-123",
"key": "TEST_VAR",
"value": "test_value",
"is_buildtime": true,
"is_preview": false,
"is_literal": false,
"is_shown_once": false,
"is_runtime": true,
"is_shared": false
}`
var env EnvironmentVariable
err := json.Unmarshal([]byte(jsonData), &env)
require.NoError(t, err)
assert.Equal(t, "env-123", env.UUID)
assert.Equal(t, "TEST_VAR", env.Key)
assert.True(t, env.IsBuildTime, "is_buildtime should unmarshal to true")
assert.True(t, env.IsRuntime, "is_runtime should unmarshal to true")
assert.False(t, env.IsShared, "is_shared should unmarshal to false")
}
func TestEnvironmentVariable_MarshalUnmarshal(t *testing.T) {
realValue := "secret_value"
env := EnvironmentVariable{
UUID: "env-uuid-123",
Key: "DATABASE_URL",
Value: "postgres://localhost/db",
IsBuildTime: true,
IsPreview: false,
IsLiteralValue: true,
IsShownOnce: false,
IsRuntime: true,
IsShared: false,
RealValue: &realValue,
}
// Marshal
data, err := json.Marshal(env)
require.NoError(t, err)
// Verify JSON contains is_buildtime (not is_build_time)
assert.Contains(t, string(data), `"is_buildtime":true`)
assert.NotContains(t, string(data), `"is_build_time"`)
// Unmarshal
var unmarshaled EnvironmentVariable
err = json.Unmarshal(data, &unmarshaled)
require.NoError(t, err)
assert.Equal(t, env.UUID, unmarshaled.UUID)
assert.Equal(t, env.Key, unmarshaled.Key)
assert.Equal(t, env.Value, unmarshaled.Value)
assert.True(t, unmarshaled.IsBuildTime)
assert.True(t, unmarshaled.IsLiteralValue)
assert.True(t, unmarshaled.IsRuntime)
assert.False(t, unmarshaled.IsShared)
assert.NotNil(t, unmarshaled.RealValue)
assert.Equal(t, *env.RealValue, *unmarshaled.RealValue)
}
func TestEnvironmentVariable_UnmarshalFromFixture(t *testing.T) {
fixtureData, err := os.ReadFile(filepath.Join("..", "..", "test", "fixtures", "environment_variable_complete.json"))
require.NoError(t, err)
var env EnvironmentVariable
err = json.Unmarshal(fixtureData, &env)
require.NoError(t, err)
assert.Equal(t, "env-test-uuid-123", env.UUID)
assert.Equal(t, "DATABASE_URL", env.Key)
assert.Equal(t, "postgres://localhost/mydb", env.Value)
assert.True(t, env.IsBuildTime, "IsBuildTime should be true from fixture")
assert.True(t, env.IsRuntime, "IsRuntime should be true from fixture")
assert.False(t, env.IsShared, "IsShared should be false from fixture")
assert.False(t, env.IsPreview)
assert.False(t, env.IsLiteralValue)
assert.False(t, env.IsShownOnce)
assert.NotNil(t, env.RealValue)
assert.Equal(t, "postgres://user:pass@localhost/mydb", *env.RealValue)
}
func TestEnvironmentVariable_PartialResponse(t *testing.T) {
// Test backward compatibility with older API responses that might not have all fields
jsonData := `{
"uuid": "env-123",
"key": "OLD_VAR",
"value": "old_value"
}`
var env EnvironmentVariable
err := json.Unmarshal([]byte(jsonData), &env)
require.NoError(t, err)
assert.Equal(t, "env-123", env.UUID)
assert.Equal(t, "OLD_VAR", env.Key)
assert.False(t, env.IsBuildTime, "Missing boolean fields should default to false")
assert.False(t, env.IsRuntime, "Missing boolean fields should default to false")
assert.False(t, env.IsShared, "Missing boolean fields should default to false")
}
func TestEnvironmentVariableCreateRequest_Marshal(t *testing.T) {
isBuildTime := true
isPreview := false
request := EnvironmentVariableCreateRequest{
Key: "NEW_VAR",
Value: "new_value",
IsBuildTime: &isBuildTime,
IsPreview: &isPreview,
}
data, err := json.Marshal(request)
require.NoError(t, err)
// Request models should still use is_build_time (with underscore) per API spec
assert.Contains(t, string(data), `"is_build_time":true`)
var unmarshaled EnvironmentVariableCreateRequest
err = json.Unmarshal(data, &unmarshaled)
require.NoError(t, err)
assert.Equal(t, request.Key, unmarshaled.Key)
assert.Equal(t, request.Value, unmarshaled.Value)
assert.NotNil(t, unmarshaled.IsBuildTime)
assert.True(t, *unmarshaled.IsBuildTime)
}
+49
View File
@@ -68,3 +68,52 @@ type ServiceUpdateRequest struct {
type ServiceLifecycleResponse struct {
Message string `json:"message"`
}
// ServiceEnvironmentVariable represents an environment variable for a service
// Services don't have preview deployments, so IsPreview is excluded from output
type ServiceEnvironmentVariable struct {
ID int `json:"-" table:"-"`
UUID string `json:"uuid"`
Key string `json:"key"`
Value string `json:"value" sensitive:"true"`
IsBuildTime bool `json:"is_buildtime"`
IsLiteralValue bool `json:"is_literal"`
IsShownOnce bool `json:"is_shown_once"`
IsRuntime bool `json:"is_runtime"`
IsShared bool `json:"is_shared"`
RealValue *string `json:"real_value,omitempty" sensitive:"true"`
ServiceID *int `json:"-" table:"-"`
CreatedAt string `json:"-" table:"-"`
UpdatedAt string `json:"-" table:"-"`
}
// ServiceEnvironmentVariableCreateRequest represents the request to create a service environment variable
type ServiceEnvironmentVariableCreateRequest struct {
Key string `json:"key"`
Value string `json:"value"`
IsBuildTime *bool `json:"is_build_time,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
IsRuntime *bool `json:"is_runtime,omitempty"`
}
// ServiceEnvironmentVariableUpdateRequest represents the request to update a service environment variable
type ServiceEnvironmentVariableUpdateRequest struct {
UUID string `json:"uuid"`
Key *string `json:"key,omitempty"`
Value *string `json:"value,omitempty"`
IsBuildTime *bool `json:"is_build_time,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
IsRuntime *bool `json:"is_runtime,omitempty"`
}
// ServiceEnvBulkUpdateRequest represents the request to bulk update service environment variables
type ServiceEnvBulkUpdateRequest struct {
Data []ServiceEnvironmentVariableCreateRequest `json:"data"`
}
// ServiceEnvBulkUpdateResponse represents the response from service bulk update
type ServiceEnvBulkUpdateResponse struct {
Message string `json:"message,omitempty"`
}
+146
View File
@@ -801,3 +801,149 @@ func TestApplicationService_DeleteEnv_Error(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to delete environment variable")
}
func TestApplicationService_ListEnvs_AllFields(t *testing.T) {
// Test that all fields including is_buildtime (without underscore), is_runtime, and is_shared
// are correctly parsed from API response
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-uuid-123/envs", r.URL.Path)
assert.Equal(t, "GET", r.Method)
// Mock API response with all fields
envs := []models.EnvironmentVariable{
{
UUID: "env-1",
Key: "DATABASE_URL",
Value: "postgres://localhost",
IsBuildTime: true,
IsPreview: false,
IsLiteralValue: true,
IsShownOnce: false,
IsRuntime: true,
IsShared: false,
},
{
UUID: "env-2",
Key: "API_KEY",
Value: "secret",
IsBuildTime: false,
IsPreview: true,
IsLiteralValue: false,
IsShownOnce: false,
IsRuntime: false,
IsShared: true,
},
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(envs)
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
result, err := svc.ListEnvs(context.Background(), "app-uuid-123")
require.NoError(t, err)
assert.Len(t, result, 2)
// Verify first env var
assert.Equal(t, "DATABASE_URL", result[0].Key)
assert.True(t, result[0].IsBuildTime, "IsBuildTime should be true for DATABASE_URL")
assert.True(t, result[0].IsRuntime, "IsRuntime should be true for DATABASE_URL")
assert.False(t, result[0].IsShared, "IsShared should be false for DATABASE_URL")
assert.True(t, result[0].IsLiteralValue)
assert.False(t, result[0].IsPreview)
// Verify second env var
assert.Equal(t, "API_KEY", result[1].Key)
assert.False(t, result[1].IsBuildTime, "IsBuildTime should be false for API_KEY")
assert.False(t, result[1].IsRuntime, "IsRuntime should be false for API_KEY")
assert.True(t, result[1].IsShared, "IsShared should be true for API_KEY")
assert.False(t, result[1].IsLiteralValue)
assert.True(t, result[1].IsPreview)
}
func TestApplicationService_EnvBuildtimeFlag(t *testing.T) {
// Test specifically that is_buildtime (without underscore) unmarshals correctly
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-uuid-123/envs", r.URL.Path)
// Directly write JSON with is_buildtime (no underscore) to mimic actual API
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[
{
"uuid": "env-test-1",
"key": "BUILD_VAR",
"value": "build_value",
"is_buildtime": true,
"is_preview": false,
"is_literal": false,
"is_shown_once": false,
"is_runtime": true,
"is_shared": false
}
]`))
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
result, err := svc.ListEnvs(context.Background(), "app-uuid-123")
require.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, "BUILD_VAR", result[0].Key)
assert.True(t, result[0].IsBuildTime, "is_buildtime field should unmarshal correctly to true")
assert.True(t, result[0].IsRuntime, "is_runtime field should unmarshal correctly to true")
}
func TestApplicationService_EnvRuntimeAndShared(t *testing.T) {
// Test is_runtime and is_shared fields specifically
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-uuid-123/envs", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[
{
"uuid": "env-runtime",
"key": "RUNTIME_VAR",
"value": "runtime_value",
"is_buildtime": false,
"is_preview": false,
"is_literal": false,
"is_shown_once": false,
"is_runtime": true,
"is_shared": false
},
{
"uuid": "env-shared",
"key": "SHARED_VAR",
"value": "shared_value",
"is_buildtime": false,
"is_preview": false,
"is_literal": false,
"is_shown_once": false,
"is_runtime": false,
"is_shared": true
}
]`))
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
result, err := svc.ListEnvs(context.Background(), "app-uuid-123")
require.NoError(t, err)
assert.Len(t, result, 2)
// Verify runtime var
assert.Equal(t, "RUNTIME_VAR", result[0].Key)
assert.True(t, result[0].IsRuntime, "IsRuntime should be true")
assert.False(t, result[0].IsShared, "IsShared should be false")
// Verify shared var
assert.Equal(t, "SHARED_VAR", result[1].Key)
assert.False(t, result[1].IsRuntime, "IsRuntime should be false")
assert.True(t, result[1].IsShared, "IsShared should be true")
}
+9 -9
View File
@@ -101,8 +101,8 @@ func (s *Service) Restart(ctx context.Context, uuid string) (*models.ServiceLife
}
// ListEnvs retrieves all environment variables for a service
func (s *Service) ListEnvs(ctx context.Context, uuid string) ([]models.EnvironmentVariable, error) {
var envs []models.EnvironmentVariable
func (s *Service) ListEnvs(ctx context.Context, uuid string) ([]models.ServiceEnvironmentVariable, error) {
var envs []models.ServiceEnvironmentVariable
err := s.client.Get(ctx, fmt.Sprintf("services/%s/envs", uuid), &envs)
if err != nil {
return nil, fmt.Errorf("failed to list environment variables for service %s: %w", uuid, err)
@@ -111,7 +111,7 @@ func (s *Service) ListEnvs(ctx context.Context, uuid string) ([]models.Environme
}
// GetEnv retrieves a single environment variable by UUID or key
func (s *Service) GetEnv(ctx context.Context, serviceUUID, envIdentifier string) (*models.EnvironmentVariable, error) {
func (s *Service) GetEnv(ctx context.Context, serviceUUID, envIdentifier string) (*models.ServiceEnvironmentVariable, error) {
envs, err := s.ListEnvs(ctx, serviceUUID)
if err != nil {
return nil, err
@@ -128,8 +128,8 @@ func (s *Service) GetEnv(ctx context.Context, serviceUUID, envIdentifier string)
}
// CreateEnv creates a new environment variable for a service
func (s *Service) CreateEnv(ctx context.Context, uuid string, req *models.EnvironmentVariableCreateRequest) (*models.EnvironmentVariable, error) {
var env models.EnvironmentVariable
func (s *Service) CreateEnv(ctx context.Context, uuid string, req *models.ServiceEnvironmentVariableCreateRequest) (*models.ServiceEnvironmentVariable, error) {
var env models.ServiceEnvironmentVariable
err := s.client.Post(ctx, fmt.Sprintf("services/%s/envs", uuid), req, &env)
if err != nil {
return nil, fmt.Errorf("failed to create environment variable for service %s: %w", uuid, err)
@@ -138,8 +138,8 @@ func (s *Service) CreateEnv(ctx context.Context, uuid string, req *models.Enviro
}
// UpdateEnv updates an environment variable for a service
func (s *Service) UpdateEnv(ctx context.Context, serviceUUID string, req *models.EnvironmentVariableUpdateRequest) (*models.EnvironmentVariable, error) {
var env models.EnvironmentVariable
func (s *Service) UpdateEnv(ctx context.Context, serviceUUID string, req *models.ServiceEnvironmentVariableUpdateRequest) (*models.ServiceEnvironmentVariable, error) {
var env models.ServiceEnvironmentVariable
err := s.client.Patch(ctx, fmt.Sprintf("services/%s/envs", serviceUUID), req, &env)
if err != nil {
return nil, fmt.Errorf("failed to update environment variable for service %s: %w", serviceUUID, err)
@@ -157,8 +157,8 @@ func (s *Service) DeleteEnv(ctx context.Context, serviceUUID, envUUID string) er
}
// BulkUpdateEnvs updates multiple environment variables in a single request
func (s *Service) BulkUpdateEnvs(ctx context.Context, serviceUUID string, req *BulkUpdateEnvsRequest) (*BulkUpdateEnvsResponse, error) {
var response BulkUpdateEnvsResponse
func (s *Service) BulkUpdateEnvs(ctx context.Context, serviceUUID string, req *models.ServiceEnvBulkUpdateRequest) (*models.ServiceEnvBulkUpdateResponse, error) {
var response models.ServiceEnvBulkUpdateResponse
err := s.client.Patch(ctx, fmt.Sprintf("services/%s/envs/bulk", serviceUUID), req, &response)
if err != nil {
return nil, fmt.Errorf("failed to bulk update environment variables for service %s: %w", serviceUUID, err)
+6 -6
View File
@@ -255,14 +255,14 @@ func TestService_ListEnvs(t *testing.T) {
"uuid": "env-1",
"key": "DATABASE_URL",
"value": "postgres://localhost",
"is_build_time": false,
"is_buildtime": false,
"is_preview": false
},
{
"uuid": "env-2",
"key": "API_KEY",
"value": "secret",
"is_build_time": true,
"is_buildtime": true,
"is_preview": false
}
]`))
@@ -290,7 +290,7 @@ func TestService_CreateEnv(t *testing.T) {
"uuid": "env-new",
"key": "NEW_VAR",
"value": "new_value",
"is_build_time": false,
"is_buildtime": false,
"is_preview": false
}`))
}))
@@ -299,7 +299,7 @@ func TestService_CreateEnv(t *testing.T) {
client := api.NewClient(server.URL, "test-token")
svc := NewService(client)
env, err := svc.CreateEnv(context.Background(), "service-uuid-123", &models.EnvironmentVariableCreateRequest{
env, err := svc.CreateEnv(context.Background(), "service-uuid-123", &models.ServiceEnvironmentVariableCreateRequest{
Key: "NEW_VAR",
Value: "new_value",
})
@@ -319,7 +319,7 @@ func TestService_UpdateEnv(t *testing.T) {
"uuid": "env-123",
"key": "UPDATED_VAR",
"value": "updated_value",
"is_build_time": true,
"is_buildtime": true,
"is_preview": false
}`))
}))
@@ -329,7 +329,7 @@ func TestService_UpdateEnv(t *testing.T) {
svc := NewService(client)
newKey := "UPDATED_VAR"
env, err := svc.UpdateEnv(context.Background(), "service-uuid-123", &models.EnvironmentVariableUpdateRequest{
env, err := svc.UpdateEnv(context.Background(), "service-uuid-123", &models.ServiceEnvironmentVariableUpdateRequest{
UUID: "env-123",
Key: &newKey,
})
+41 -43
View File
@@ -5,92 +5,90 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sort"
"time"
compareVersion "github.com/hashicorp/go-version"
"github.com/spf13/viper"
)
// Version variables injected by GoReleaser at build time via ldflags
var (
version = "v1.0.3"
version = "v1.2"
)
// GitHubAPIURL is the URL for fetching CLI version tags (exported for testing)
var GitHubAPIURL = "https://api.github.com/repos/coollabsio/coolify-cli/git/refs/tags"
func GetVersion() string {
return version
}
// CheckInterval for version checking
const CheckInterval = 10 * time.Minute
// Tag represents a git tag for version checking
type Tag struct {
Ref string `json:"ref"`
}
// CheckLatestVersionOfCli checks for CLI updates
func CheckLatestVersionOfCli(debug bool) (string, error) {
lastCheck := viper.GetString("lastupdatechecktime")
if lastCheck != "" {
lastCheckTime, err := time.Parse(time.RFC3339, lastCheck)
if err == nil && lastCheckTime.Add(CheckInterval).After(time.Now()) {
if debug {
log.Println("Skipping update check. Last check was less than 10 minutes ago.")
}
return GetVersion(), nil
}
}
// CheckLatestVersionOfCli checks for CLI updates on every command.
// Errors are handled silently - the function returns without printing anything
// if the GitHub API call fails.
func CheckLatestVersionOfCli(_ bool) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Update check time
viper.Set("lastupdatechecktime", time.Now().Format(time.RFC3339))
if err := viper.WriteConfig(); err != nil {
log.Printf("Failed to write config: %v\n", err)
}
url := "https://api.github.com/repos/coollabsio/coolify-cli/git/refs/tags"
ctx := context.Background()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
req, err := http.NewRequestWithContext(ctx, "GET", GitHubAPIURL, nil)
if err != nil {
return "", err
return "", nil // Silent fail
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
return "", nil // Silent fail
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
if resp.StatusCode != 200 {
return "", nil // Silent fail
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("%d - Failed to fetch data from %s. Error: %s", resp.StatusCode, url, string(body))
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil // Silent fail
}
var tags []Tag
if err := json.Unmarshal(body, &tags); err != nil {
return "", err
return "", nil // Silent fail
}
if len(tags) == 0 {
return "", nil // Silent fail
}
versionsRaw := make([]string, 0, len(tags))
for _, tag := range tags {
versionStr := tag.Ref[10:]
versionsRaw = append(versionsRaw, versionStr)
if len(tag.Ref) > 10 {
versionStr := tag.Ref[10:]
versionsRaw = append(versionsRaw, versionStr)
}
}
versions := make([]*compareVersion.Version, len(versionsRaw))
for i, raw := range versionsRaw {
if len(versionsRaw) == 0 {
return "", nil // Silent fail
}
versions := make([]*compareVersion.Version, 0, len(versionsRaw))
for _, raw := range versionsRaw {
v, err := compareVersion.NewVersion(raw)
if err != nil {
return "", err
continue // Skip invalid versions
}
versions[i] = v
versions = append(versions, v)
}
if len(versions) == 0 {
return "", nil // Silent fail
}
sort.Sort(compareVersion.Collection(versions))
@@ -99,11 +97,11 @@ func CheckLatestVersionOfCli(debug bool) (string, error) {
// Compare versions properly using semantic versioning
currentVersion, err := compareVersion.NewVersion(GetVersion())
if err != nil {
return latestVersion.String(), err
return "", nil // Silent fail
}
if latestVersion.GreaterThan(currentVersion) {
fmt.Printf("There is a new version of Coolify CLI available.\nPlease update with 'coolify update'.\n\n")
fmt.Printf("A new version (%s) is available. Update with: coolify update\n", latestVersion.String())
}
return latestVersion.String(), nil
}
+262
View File
@@ -0,0 +1,262 @@
package version
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
)
func TestGetVersion(t *testing.T) {
v := GetVersion()
if v == "" {
t.Error("GetVersion() returned empty string")
}
// Version should start with 'v'
if v[0] != 'v' {
t.Errorf("GetVersion() = %q, expected to start with 'v'", v)
}
}
func TestCheckLatestVersionOfCli_UpdateAvailable(t *testing.T) {
// Save original values
originalURL := GitHubAPIURL
originalVersion := version
defer func() {
GitHubAPIURL = originalURL
version = originalVersion
}()
// Set a low version to ensure update is available
version = "v0.0.1"
// Create mock server with newer version
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
// Return tags in GitHub API format
_, _ = w.Write([]byte(`[{"ref":"refs/tags/v1.0.0"},{"ref":"refs/tags/v2.0.0"}]`))
}))
defer server.Close()
GitHubAPIURL = server.URL
// Capture stdout to check for update message
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err)
}
if latestVersion != "2.0.0" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want %q", latestVersion, "2.0.0")
}
// Should print update message
expectedMsg := "A new version (2.0.0) is available. Update with: coolify update\n"
if output != expectedMsg {
t.Errorf("CheckLatestVersionOfCli() output = %q, want %q", output, expectedMsg)
}
}
func TestCheckLatestVersionOfCli_NoUpdate(t *testing.T) {
// Save original values
originalURL := GitHubAPIURL
originalVersion := version
defer func() {
GitHubAPIURL = originalURL
version = originalVersion
}()
// Set a high version to ensure no update is available
version = "v99.99.99"
// Create mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`[{"ref":"refs/tags/v1.0.0"},{"ref":"refs/tags/v2.0.0"}]`))
}))
defer server.Close()
GitHubAPIURL = server.URL
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err)
}
// Function returns the latest version from GitHub (2.0.0), not the current version
if latestVersion != "2.0.0" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want %q", latestVersion, "2.0.0")
}
// Should NOT print any message when already on latest (current v99.99.99 > latest v2.0.0)
if output != "" {
t.Errorf("CheckLatestVersionOfCli() should not print anything when on latest version, got: %q", output)
}
}
func TestCheckLatestVersionOfCli_APIError_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Create mock server that returns error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"error": "internal server error"}`))
}))
defer server.Close()
GitHubAPIURL = server.URL
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on API error", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on API error", latestVersion)
}
// Should NOT print anything on error
if output != "" {
t.Errorf("CheckLatestVersionOfCli() should not print anything on API error, got: %q", output)
}
}
func TestCheckLatestVersionOfCli_NetworkError_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Use invalid URL to cause network error
GitHubAPIURL = "http://localhost:1" // Port 1 should fail to connect
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on network error", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on network error", latestVersion)
}
// Should NOT print anything on error
if output != "" {
t.Errorf("CheckLatestVersionOfCli() should not print anything on network error, got: %q", output)
}
}
func TestCheckLatestVersionOfCli_InvalidJSON_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Create mock server that returns invalid JSON
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`not valid json`))
}))
defer server.Close()
GitHubAPIURL = server.URL
latestVersion, err := CheckLatestVersionOfCli(false)
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on invalid JSON", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on invalid JSON", latestVersion)
}
}
func TestCheckLatestVersionOfCli_EmptyTags_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Create mock server that returns empty array
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`[]`))
}))
defer server.Close()
GitHubAPIURL = server.URL
latestVersion, err := CheckLatestVersionOfCli(false)
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on empty tags", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on empty tags", latestVersion)
}
}
+411
View File
@@ -0,0 +1,411 @@
# Coolify CLI Installer for Windows
# This script installs the coolify-cli from GitHub releases
# Supports Windows on amd64/arm64 architectures
#Requires -Version 5.1
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[string]$Version = "",
[Parameter()]
[switch]$User,
[Parameter()]
[switch]$Help,
[Parameter()]
[string]$InstallDir = ""
)
# Support environment variables for web-based installation
if (-not $Version -and $env:COOLIFY_VERSION) {
$Version = $env:COOLIFY_VERSION
}
if (-not $User.IsPresent -and $env:COOLIFY_USER_INSTALL) {
$User = [bool]($env:COOLIFY_USER_INSTALL -match '^(1|true|yes)$')
}
if (-not $InstallDir -and $env:COOLIFY_INSTALL_DIR) {
$InstallDir = $env:COOLIFY_INSTALL_DIR
}
# Configuration
$Script:REPO = "coollabsio/coolify-cli"
$Script:BINARY_NAME = "coolify.exe"
$Script:GLOBAL_INSTALL_DIR = "$env:ProgramFiles\Coolify"
$Script:USER_INSTALL_DIR = "$env:LOCALAPPDATA\Coolify"
$Script:TEMP_FILE = ""
# Error action preference
$ErrorActionPreference = "Stop"
# Cleanup function
function Cleanup {
if ($Script:TEMP_FILE -and (Test-Path $Script:TEMP_FILE)) {
Remove-Item -Path $Script:TEMP_FILE -Force -ErrorAction SilentlyContinue
}
}
# Register cleanup on exit
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { Cleanup } | Out-Null
# Show help
function Show-Help {
Write-Host @"
Coolify CLI Installer for Windows
Usage: .\install.ps1 [OPTIONS] [VERSION]
OPTIONS:
-User Install to user directory (no admin rights required)
-InstallDir Custom installation directory
-Help Show this help message
ARGUMENTS:
VERSION Specific version to install (e.g., v1.0.0)
If not specified, installs the latest release
EXAMPLES:
.\install.ps1 Install latest version to Program Files
.\install.ps1 -User Install latest version to user directory
.\install.ps1 v1.0.0 Install specific version to Program Files
.\install.ps1 -User v1.0.0 Install specific version to user directory
.\install.ps1 -InstallDir "C:\Tools" Install to custom directory
NOTES:
- Administrator privileges are required for global installation
- User installation does not require admin rights
- The installation directory will be added to PATH if not already present
"@
exit 0
}
# Write colored output
function Write-ColorOutput {
param(
[string]$Message,
[string]$ForegroundColor = "White"
)
Write-Host $Message -ForegroundColor $ForegroundColor
}
function Write-Success {
param([string]$Message)
Write-ColorOutput $Message -ForegroundColor Green
}
function Write-Warning {
param([string]$Message)
Write-ColorOutput $Message -ForegroundColor Yellow
}
function Write-ErrorMessage {
param([string]$Message)
Write-ColorOutput $Message -ForegroundColor Red
}
# Error handler
function Stop-WithError {
param([string]$Message)
Write-ErrorMessage "Error: $Message"
Cleanup
throw $Message
}
# Check if running as administrator
function Test-Administrator {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
# Detect platform and architecture
function Get-PlatformInfo {
$os = "windows"
# Detect architecture
$arch = switch ($env:PROCESSOR_ARCHITECTURE) {
"AMD64" { "amd64" }
"ARM64" { "arm64" }
"x86" { Stop-WithError "32-bit Windows is not supported" }
default { Stop-WithError "Unsupported architecture: $env:PROCESSOR_ARCHITECTURE" }
}
return @{
OS = $os
Arch = $arch
}
}
# Fetch latest release version from GitHub
function Get-LatestVersion {
Write-Host "Fetching latest release version..."
try {
$response = Invoke-RestMethod -Uri "https://api.github.com/repos/$Script:REPO/releases/latest" -Method Get
$latestVersion = $response.tag_name
if (-not $latestVersion) {
Stop-WithError "Failed to fetch latest release version from GitHub"
}
return $latestVersion
}
catch {
Stop-WithError "Failed to fetch latest release version: $($_.Exception.Message)"
}
}
# Validate version format
function Test-VersionFormat {
param([string]$VersionString)
# Check if version matches semantic versioning (with or without 'v' prefix)
if ($VersionString -notmatch '^v?\d+\.\d+\.\d+(-.*)?$') {
Stop-WithError "Invalid version format: $VersionString`nExpected format: v1.0.0 or 1.0.0"
}
# Ensure version starts with 'v' for GitHub releases
if ($VersionString -notmatch '^v') {
$VersionString = "v$VersionString"
}
return $VersionString
}
# Check if coolify is already installed
function Test-ExistingInstallation {
param([string]$NewVersion)
try {
$coolifyCmd = Get-Command coolify -ErrorAction SilentlyContinue
if ($coolifyCmd) {
$currentVersion = & coolify version 2>$null | Select-Object -First 1
if (-not $currentVersion) {
$currentVersion = "unknown"
}
Write-Warning "Coolify CLI is already installed: $currentVersion"
Write-Host "This will upgrade/reinstall to version " -NoNewline
Write-Success $NewVersion
$response = Read-Host "Continue? [y/N]"
if ($response -notmatch '^[Yy]$') {
Write-Host "Installation cancelled."
exit 0
}
}
}
catch {
# Coolify not found, continue with installation
}
}
# Download and install binary from GitHub release
function Install-FromGitHub {
param(
[string]$Repo,
[string]$Release,
[string]$Name,
[string]$InstallDirectory,
[hashtable]$Platform
)
# Clean version (remove 'v' prefix if present)
$cleanVersion = $Release -replace '^v', ''
$filename = "${Name}_${cleanVersion}_$($Platform.OS)_$($Platform.Arch).zip"
$downloadUrl = "https://github.com/${Repo}/releases/download/${Release}/${filename}"
Write-Success "Downloading $Name $Release"
Write-Host "Platform: $($Platform.OS)/$($Platform.Arch)"
Write-Host "URL: $downloadUrl"
# Create temp file
$Script:TEMP_FILE = [System.IO.Path]::GetTempFileName() + ".zip"
# Download file
try {
$ProgressPreference = 'SilentlyContinue' # Speed up download
Invoke-WebRequest -Uri $downloadUrl -OutFile $Script:TEMP_FILE -ErrorAction Stop
$ProgressPreference = 'Continue'
}
catch {
Stop-WithError "Failed to download from ${downloadUrl}`nPlease check if the version exists or try again later.`nError: $($_.Exception.Message)"
}
# Verify downloaded file is not empty
if ((Get-Item $Script:TEMP_FILE).Length -eq 0) {
Stop-WithError "Downloaded file is empty"
}
# Create install directory if it doesn't exist
if (-not (Test-Path $InstallDirectory)) {
Write-Host "Creating directory: $InstallDirectory"
try {
New-Item -ItemType Directory -Path $InstallDirectory -Force | Out-Null
}
catch {
Stop-WithError "Failed to create directory $InstallDirectory : $($_.Exception.Message)"
}
}
# Extract binary
Write-Host "Installing $Name to $InstallDirectory\$Script:BINARY_NAME"
try {
# Remove existing binary if present
$binaryPath = Join-Path $InstallDirectory $Script:BINARY_NAME
if (Test-Path $binaryPath) {
Remove-Item -Path $binaryPath -Force
}
# Extract zip file
Expand-Archive -Path $Script:TEMP_FILE -DestinationPath $InstallDirectory -Force
}
catch {
Stop-WithError "Failed to extract binary: $($_.Exception.Message)"
}
# Verify installation
$installedBinary = Join-Path $InstallDirectory $Script:BINARY_NAME
if (-not (Test-Path $installedBinary)) {
Stop-WithError "Binary was not installed to $installedBinary"
}
Write-Success "$Name installed successfully to $installedBinary"
# Add to PATH if not already present
Add-ToPath -Directory $InstallDirectory
# Show installed version
try {
$installedVersion = & $installedBinary version 2>$null | Select-Object -First 1
if ($installedVersion) {
Write-Host "Installed version: " -NoNewline
Write-Success $installedVersion
}
}
catch {
# Version check failed, but installation was successful
}
}
# Add directory to PATH
function Add-ToPath {
param([string]$Directory)
# Determine which PATH to modify (user or system)
$pathScope = if ($User -or -not (Test-Administrator)) { "User" } else { "Machine" }
# Get current PATH
$currentPath = [Environment]::GetEnvironmentVariable("Path", $pathScope)
# Check if directory is already in PATH
$pathDirs = $currentPath -split ';' | ForEach-Object { $_.Trim() }
if ($pathDirs -contains $Directory) {
Write-Host "Directory is already in PATH"
return
}
Write-Host "Adding $Directory to PATH ($pathScope)..."
try {
# Add to PATH
$newPath = "$currentPath;$Directory"
[Environment]::SetEnvironmentVariable("Path", $newPath, $pathScope)
# Update current session PATH
$env:Path = "$env:Path;$Directory"
Write-Success "✓ Added to PATH. You may need to restart your terminal for changes to take effect."
}
catch {
Write-Warning "Failed to add to PATH automatically: $($_.Exception.Message)"
Write-Host "Please add the following directory to your PATH manually:"
Write-Host " $Directory"
}
}
# Main installation flow
function Install-CoolifyCLI {
Write-Host "Coolify CLI Installer for Windows"
Write-Host "=================================="
Write-Host ""
# Show help if requested
if ($Help) {
Show-Help
}
# Detect platform
$platform = Get-PlatformInfo
# Determine version to install
$versionToInstall = if ($Version) {
Test-VersionFormat -VersionString $Version
} else {
Get-LatestVersion
}
Write-Host "Version to install: $versionToInstall"
Write-Host ""
# Check existing installation
Test-ExistingInstallation -NewVersion $versionToInstall
# Determine install directory
$installDirectory = if ($InstallDir) {
$InstallDir
} elseif ($User) {
$Script:USER_INSTALL_DIR
} else {
$Script:GLOBAL_INSTALL_DIR
}
# Check for admin rights if installing globally
if (-not $User -and -not $InstallDir -and -not (Test-Administrator)) {
Write-Warning "Global installation requires administrator privileges."
Write-Host "Please run this script as Administrator, or use -User flag for user installation."
Write-Host ""
$response = Read-Host "Switch to user installation? [Y/n]"
if ($response -match '^[Nn]$') {
Stop-WithError "Installation cancelled. Please run as Administrator for global installation."
}
$installDirectory = $Script:USER_INSTALL_DIR
}
$installMode = if ($User -or $installDirectory -eq $Script:USER_INSTALL_DIR) {
"User (no admin rights required)"
} else {
"Global (administrator)"
}
Write-Host "Install mode: $installMode"
Write-Host "Install directory: $installDirectory"
Write-Host ""
# Download and install
Install-FromGitHub -Repo $Script:REPO -Release $versionToInstall -Name "coolify-cli" -InstallDirectory $installDirectory -Platform $platform
Write-Host ""
Write-Success "Installation complete!"
Write-Host "Run 'coolify --help' to get started"
Write-Host ""
Write-Host "Note: If 'coolify' command is not found, please restart your terminal."
}
# Run main installation
try {
Install-CoolifyCLI
}
catch {
Write-ErrorMessage "Unexpected error: $($_.Exception.Message)"
Write-ErrorMessage $_.ScriptStackTrace
Cleanup
}
finally {
Cleanup
}
+1 -1
View File
@@ -173,7 +173,7 @@ download_from_github() {
local name=$3
local install_dir=$4
local filename="${name}_${release}_${OS}_${ARCH}.tar.gz"
local filename="${name}_${release#v}_${OS}_${ARCH}.tar.gz"
local download_url="https://github.com/${repo}/releases/download/${release}/${filename}"
echo -e "${GREEN}Downloading ${name} ${release}${NC}"
+12
View File
@@ -0,0 +1,12 @@
{
"uuid": "env-test-uuid-123",
"key": "DATABASE_URL",
"value": "postgres://localhost/mydb",
"real_value": "postgres://user:pass@localhost/mydb",
"is_buildtime": true,
"is_preview": false,
"is_literal": false,
"is_shown_once": false,
"is_runtime": true,
"is_shared": false
}