From b8c06a8622cfffb21d2a3bac1e11c08d55efbfbd Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 14 Aug 2026 21:09:41 +0200 Subject: [PATCH] feat(dev): support direct environment overrides --- .../command-line/development/index.mdx | 4 ++- tools/homarr-dev/README.md | 4 +++ tools/homarr-dev/internal/run/run_test.go | 18 ++++++++++++ tools/homarr-dev/main.go | 29 ++++++++++++++----- tools/homarr-dev/main_test.go | 22 +++++++++++++- 5 files changed, 68 insertions(+), 9 deletions(-) diff --git a/apps/docs/docs/advanced/command-line/development/index.mdx b/apps/docs/docs/advanced/command-line/development/index.mdx index cdbffc29a..ee5036a72 100644 --- a/apps/docs/docs/advanced/command-line/development/index.mdx +++ b/apps/docs/docs/advanced/command-line/development/index.mdx @@ -34,8 +34,10 @@ Images built by this CLI record their source checkout, revision, and optional pu ## Run images ```bash +homarr --pr --env HOMARR_WEBSITE_URL=https://app-v2.preview.homarr.dev/ homarr run homarr run --pr ``` -Add `--detach` to run in the background, `--demo` to enable demo data, or repeat `--env KEY=VALUE` to provide additional environment variables. +The direct `homarr --pr ` form is shorthand for `homarr run --pr `. Add `--detach` to run in the +background, `--demo` to enable demo data, or repeat `--env KEY=VALUE` to provide additional environment variables. diff --git a/tools/homarr-dev/README.md b/tools/homarr-dev/README.md index 99fe89640..01dcbb686 100644 --- a/tools/homarr-dev/README.md +++ b/tools/homarr-dev/README.md @@ -31,6 +31,7 @@ Checkout builds are tagged as `homarr:`. The source checkout and revision ## Launch ```sh +homarr --pr 6441 --env WORKSHOP_WEB_URL=https://app-v2.preview.homarr.dev/ pnpm dev:cli -- run dev pnpm dev:cli -- run --detach dev pnpm dev:cli -- run --pr 6441 @@ -39,6 +40,8 @@ pnpm dev:cli -- run --env FOO=bar --env FEATURE=true dev ``` PR launches always pull the latest GHCR tag before starting. Local tags remain local. +The installed binary accepts `--pr`, `--demo`, `--detach`, and repeatable `--env KEY=VALUE` flags directly, so +`homarr --pr --env KEY=VALUE` is shorthand for `homarr run --pr --env KEY=VALUE`. The first launch generates a per-user encryption key in the OS config directory and reuses it for later launches. Set `HOMARR_DEV_SECRET_ENCRYPTION_KEY` to a 64-character hexadecimal key to override it. The command reference below assumes the optional `homarr` installation. Without it, prefix each command with `pnpm dev:cli --` (for example, `pnpm dev:cli -- dev`). @@ -47,6 +50,7 @@ The command reference below assumes the optional `homarr` installation. Without ```text homarr Interactive local and remote image browser +homarr --pr Pull and launch a PR image, with optional --env overrides homarr dev Explicit alias for the main browser homarr dash Start on the instance dashboard homarr build Build this checkout as homarr: diff --git a/tools/homarr-dev/internal/run/run_test.go b/tools/homarr-dev/internal/run/run_test.go index 5bed7eac9..5b3681705 100644 --- a/tools/homarr-dev/internal/run/run_test.go +++ b/tools/homarr-dev/internal/run/run_test.go @@ -33,6 +33,24 @@ func TestBuildPlanFailsWhenNoPortIsAvailable(t *testing.T) { } } +func TestBuildPlanPreservesEnvironmentOverrides(t *testing.T) { + want := []string{ + "WORKSHOP_WEB_URL=https://app-v2.preview.homarr.dev/", + "FEATURE_OPTIONS=one=two", + } + plan, err := BuildPlan(Options{ + PR: 1, + Env: want, + FindPort: func(int) int { return 7575 }, + }) + if err != nil { + t.Fatal(err) + } + if !slices.Equal(plan.Env, want) { + t.Fatalf("environment = %v, want %v", plan.Env, want) + } +} + func TestStartPRIntegration(t *testing.T) { if os.Getenv("HOMARR_DOCKER_INTEGRATION_TEST") != "1" { t.Skip("set HOMARR_DOCKER_INTEGRATION_TEST=1 and HOMARR_TEST_PR=") diff --git a/tools/homarr-dev/main.go b/tools/homarr-dev/main.go index 2380e41bd..b4c868296 100644 --- a/tools/homarr-dev/main.go +++ b/tools/homarr-dev/main.go @@ -21,6 +21,7 @@ var ( flagDetach bool flagBuildPR int runMain = tui.RunDev + runRootMain = runLaunch ) func main() { @@ -39,6 +40,8 @@ Start a local image, a PR test build from ghcr.io, browse open PRs, or manage running instances from an interactive dashboard. Examples: + homarr --pr 6390 # launch remote PR test image directly + homarr --pr 6390 -e FOO=bar # PR image with environment overrides homarr run dev # launch local homarr:dev image homarr run --pr 6390 # launch remote PR test image homarr run --pr 6390 --demo # PR + demo mode @@ -52,9 +55,7 @@ Examples: homarr logs homarr_pr_6390 # follow logs homarr doctor # check Docker and optional integrations`, Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runMain() - }, + RunE: runRoot, } var dashCmd = &cobra.Command{ @@ -240,15 +241,29 @@ var doctorCmd = &cobra.Command{ } func init() { - runCmd.Flags().BoolVarP(&flagDemo, "demo", "m", false, "Demo mode (mock integrations)") - runCmd.Flags().IntVarP(&flagPR, "pr", "p", 0, "Launch PR test image from ghcr.io") - runCmd.Flags().StringArrayVarP(&flagEnv, "env", "e", nil, "Extra env vars (repeatable, KEY=VALUE)") - runCmd.Flags().BoolVar(&flagDetach, "detach", false, "Start in the background") + addRunFlags(rootCmd) + addRunFlags(runCmd) buildCmd.Flags().IntVarP(&flagBuildPR, "pr", "p", 0, "Build a pull request in a temporary checkout") rootCmd.AddCommand(runCmd, dashCmd, devCmd, buildCmd, rebuildCmd, listCmd, logsCmd, stopCmd, restartCmd, removeCmd, openCmd, doctorCmd) } +func addRunFlags(cmd *cobra.Command) { + cmd.Flags().BoolVarP(&flagDemo, "demo", "m", false, "Demo mode (mock integrations)") + cmd.Flags().IntVarP(&flagPR, "pr", "p", 0, "Launch PR test image from ghcr.io") + cmd.Flags().StringArrayVarP(&flagEnv, "env", "e", nil, "Extra env vars (repeatable, KEY=VALUE)") + cmd.Flags().BoolVar(&flagDetach, "detach", false, "Start in the background") +} + +func runRoot(cmd *cobra.Command, args []string) error { + for _, name := range []string{"pr", "env", "demo", "detach"} { + if cmd.Flags().Changed(name) { + return runRootMain(cmd, args) + } + } + return runMain() +} + func runLaunch(cmd *cobra.Command, args []string) error { tag := "" if len(args) > 0 { diff --git a/tools/homarr-dev/main_test.go b/tools/homarr-dev/main_test.go index 1736d98b3..45cae5ae2 100644 --- a/tools/homarr-dev/main_test.go +++ b/tools/homarr-dev/main_test.go @@ -4,6 +4,8 @@ import ( "errors" "strings" "testing" + + "github.com/spf13/cobra" ) func TestRootCommandLaunchesDevelopmentDashboard(t *testing.T) { @@ -12,7 +14,25 @@ func TestRootCommandLaunchesDevelopmentDashboard(t *testing.T) { t.Cleanup(func() { runMain = previous }) runMain = func() error { return want } - if err := rootCmd.RunE(rootCmd, nil); !errors.Is(err, want) { + cmd := &cobra.Command{} + if err := runRoot(cmd, nil); !errors.Is(err, want) { + t.Fatalf("root command error = %v", err) + } +} + +func TestRootCommandLaunchesPRWhenRunFlagIsProvided(t *testing.T) { + want := errors.New("PR launched") + previous := runRootMain + t.Cleanup(func() { runRootMain = previous }) + runRootMain = func(cmd *cobra.Command, args []string) error { return want } + + cmd := &cobra.Command{} + cmd.Flags().Bool("pr", false, "") + if err := cmd.Flags().Set("pr", "true"); err != nil { + t.Fatal(err) + } + + if err := runRoot(cmd, nil); !errors.Is(err, want) { t.Fatalf("root command error = %v", err) } }