diff --git a/.github/command-registry.json b/.github/command-registry.json index ec0cfcfe7..811d3a57e 100644 --- a/.github/command-registry.json +++ b/.github/command-registry.json @@ -18946,7 +18946,7 @@ "shorthand": null, "type": "bool", "default": "false", - "usage": "Delete plugin data (schema/tables). A bundle remove asks for confirmation (or pass --yes); a plugin remove never asks and drops data unless --keep-data", + "usage": "Delete plugin data (schema/tables). v1.5 keeps data unless --delete-data and asks for confirmation (or pass --yes); v1.4 plugin remove never asks and drops data unless --keep-data. Bundle data deletion asks for confirmation (or pass --yes) in both modes", "hidden": false, "deprecated": null, "required": false, diff --git a/.github/facts.json b/.github/facts.json index 17f3d726b..91818ddc4 100644 --- a/.github/facts.json +++ b/.github/facts.json @@ -9,8 +9,8 @@ }, "schema_version": "1", "sources": { - "openapi_sha256": "915c754aea9a69aba703d6c29d35d0e16ef0f81290a85f41581200a57ad7a7b1", - "registry_sha256": "05a06d867e0e8a0c3d5a6ccd201ca60cca3530fde14e67e1d046039153d15a90" + "openapi_sha256": "01ea16e2ac6cebd687983d758b80a9f1ddffe3b6a3a85bda4fc70ce077ca71b6", + "registry_sha256": "37fe98fe4dfa26f0c8af15f5cc50e9a5b81f499976850ae2b6066c536e8fe6c9" }, "verbs": [ "init", diff --git a/.github/openapi.json b/.github/openapi.json index 230b33375..98081f1ba 100644 --- a/.github/openapi.json +++ b/.github/openapi.json @@ -47459,7 +47459,7 @@ "properties": { "delete-data": { "default": false, - "description": "Delete plugin data (schema/tables). A bundle remove asks for confirmation (or pass --yes); a plugin remove never asks and drops data unless --keep-data", + "description": "Delete plugin data (schema/tables). v1.5 keeps data unless --delete-data and asks for confirmation (or pass --yes); v1.4 plugin remove never asks and drops data unless --keep-data. Bundle data deletion asks for confirmation (or pass --yes) in both modes", "type": "boolean" }, "dry-run": { diff --git a/.github/wiki/cmd-deploy.md b/.github/wiki/cmd-deploy.md index 7c1944aad..90abf459b 100644 --- a/.github/wiki/cmd-deploy.md +++ b/.github/wiki/cmd-deploy.md @@ -857,6 +857,9 @@ set with the same pull, per-service `up`, health gate, `nginx -t`, nginx reload apply. The database restore returns the data to the moment of the backup: writes made since then, while the new containers ran, are lost. +Rollback resolves SSH keys and server names only from the target environment's inventory; +the remote path comes from the recorded tar. + The database is restored at most once per record: `db_restored_at` is saved right after `nself backup restore` succeeds, before the re-apply, so a retry of a rollback that failed later (on any host) skips it and redoes only files and services. The `backup_host` is rolled back first. If it diff --git a/.github/wiki/cmd-remove.md b/.github/wiki/cmd-remove.md index 131fe0aa8..5727e2f63 100644 --- a/.github/wiki/cmd-remove.md +++ b/.github/wiki/cmd-remove.md @@ -33,7 +33,7 @@ name removes the whole bundle. | Name | Shorthand | Type | Default | Env | Escalates to | JSON override | Description | |---|---|---|---|---|---|---|---| -| `--delete-data` | | `bool` | `false` | | | | Delete plugin data (schema/tables). A bundle remove asks for confirmation (or pass --yes); a plugin remove never asks and drops data unless --keep-data | +| `--delete-data` | | `bool` | `false` | | | | Delete plugin data (schema/tables). v1.5 keeps data unless --delete-data and asks for confirmation (or pass --yes); v1.4 plugin remove never asks and drops data unless --keep-data. Bundle data deletion asks for confirmation (or pass --yes) in both modes | | `--dry-run` | | `bool` | `false` | | | | Print the planned actions without removing | | `--force` | | `bool` | `false` | | | | Remove even if other plugins depend on this one | | `--keep-data` | | `bool` | `false` | | | | Preserve database data on remove | diff --git a/.github/wiki/llms.txt b/.github/wiki/llms.txt index c566031a3..824bf0a1c 100644 --- a/.github/wiki/llms.txt +++ b/.github/wiki/llms.txt @@ -457,7 +457,7 @@ nself remove [flags] Flags: -- `--delete-data` (default false) — Delete plugin data (schema/tables). A bundle remove asks for confirmation (or pass --yes); a plugin remove never asks and drops data unless --keep-data +- `--delete-data` (default false) — Delete plugin data (schema/tables). v1.5 keeps data unless --delete-data and asks for confirmation (or pass --yes); v1.4 plugin remove never asks and drops data unless --keep-data. Bundle data deletion asks for confirmation (or pass --yes) in both modes - `--dry-run` (default false) — Print the planned actions without removing - `--force` (default false) — Remove even if other plugins depend on this one - `--keep-data` (default false) — Preserve database data on remove diff --git a/cmd/commands/bundle.go b/cmd/commands/bundle.go index 4ae924bdc..601b17534 100644 --- a/cmd/commands/bundle.go +++ b/cmd/commands/bundle.go @@ -75,7 +75,7 @@ func init() { // remove flags bundleRemoveCmd.Flags().Bool("dry-run", false, "Print the planned actions without removing") - bundleRemoveCmd.Flags().Bool("delete-data", false, "Delete plugin data (schema/tables). A bundle remove asks for confirmation (or pass --yes); a plugin remove never asks and drops data unless --keep-data") + bundleRemoveCmd.Flags().Bool("delete-data", false, removeDeleteDataHelp) bundleRemoveCmd.Flags().Bool("keep-data", false, "Deprecated: no-op from v1.5 (data is kept by default); removed in v1.6.0") _ = bundleRemoveCmd.Flags().MarkHidden("keep-data") diff --git a/cmd/commands/deploy_lifecycle_rollback.go b/cmd/commands/deploy_lifecycle_rollback.go index 04144e680..f05ad8cc7 100644 --- a/cmd/commands/deploy_lifecycle_rollback.go +++ b/cmd/commands/deploy_lifecycle_rollback.go @@ -68,13 +68,11 @@ func rollbackRemoteV15(cmd *cobra.Command, workdir, target string) (bool, error) idx := strings.Index(tar, "/"+deploy.BackupDir+"/pre-deploy-") srv, key := controlplane.Server{Name: target + "-app", Host: host, RemotePath: tar[:idx]}, sshKeyPath() if inv != nil { - for _, e := range inv.Environments { - for _, c := range e.Servers { - if spec, err := remote.ParseHostSpec(c.Host); err == nil && spec.String() == host { - srv.Name, srv.Host = c.Name, c.Host - if v := os.Getenv(c.SSHKeyRef); v != "" { - key = v - } + for _, c := range inv.Environments[target].Servers { + if spec, err := remote.ParseHostSpec(c.Host); err == nil && spec.String() == host { + srv.Name, srv.Host = c.Name, c.Host + if v := os.Getenv(c.SSHKeyRef); v != "" { + key = v } } } diff --git a/cmd/commands/deploy_rollback_inventory_test.go b/cmd/commands/deploy_rollback_inventory_test.go new file mode 100644 index 000000000..fb191515d --- /dev/null +++ b/cmd/commands/deploy_rollback_inventory_test.go @@ -0,0 +1,54 @@ +package commands + +import ( + "context" + "errors" + "testing" + + "github.com/nself-org/cli/internal/controlplane" + "github.com/nself-org/cli/internal/deploy" +) + +func TestRollbackInventoryUsesOnlyTargetEnvironment(t *testing.T) { + for _, targetPresent := range []bool{true, false} { + name := "target present" + if !targetPresent { + name = "target absent" + } + t.Run(name, func(t *testing.T) { + proj, _ := v15Project(t) + const host = "deploy@app.example.test" + t.Setenv("NSELF_TEST_PROD_KEY", "/keys/prod") + t.Setenv("NSELF_TEST_OTHER_KEY", "/keys/other") + t.Setenv("NSELF_DEPLOY_SSH_KEY", "/keys/fallback") + envs := map[string]controlplane.Environment{ + "other": {Name: "other", Kind: "remote", Servers: []controlplane.Server{{ + Name: "other-app", Role: controlplane.RoleApp, Host: host, + SSHKeyRef: "NSELF_TEST_OTHER_KEY", RemotePath: "/other", Primary: true, + }}}, + } + wantName, wantKey := "prod-app", "/keys/fallback" + if targetPresent { + envs["prod"] = controlplane.Environment{Name: "prod", Kind: "remote", Servers: []controlplane.Server{{ + Name: "target-app", Role: controlplane.RoleApp, Host: host, + SSHKeyRef: "NSELF_TEST_PROD_KEY", RemotePath: "/inventory-path", Primary: true, + }}} + wantName, wantKey = "target-app", "/keys/prod" + } + writeInventory(t, proj, &controlplane.Inventory{SchemaVersion: 2, Environments: envs}) + rollbackRecord(t, proj, host, host) + stop := errors.New("test stops before remote operations") + opened := false + deployRemoteOpen = func(_ context.Context, srv controlplane.Server, key, env string) (deploy.Remote, error) { + opened = true + if srv.Name != wantName || key != wantKey || env != "prod" || srv.Host != host || srv.RemotePath != "/opt/nself" { + t.Errorf("rollback opened server=%+v key=%q env=%q; want name=%q key=%q and recorded path", srv, key, env, wantName, wantKey) + } + return nil, stop + } + if handled, err := rollbackRemoteV15(yesCmd(), proj, "prod"); !handled || err == nil || !opened { + t.Fatalf("rollback handled=%v err=%v opened=%v", handled, err, opened) + } + }) + } +} diff --git a/cmd/commands/install.go b/cmd/commands/install.go index c80a1f505..ada1352aa 100644 --- a/cmd/commands/install.go +++ b/cmd/commands/install.go @@ -28,6 +28,8 @@ import ( // Constraints: `install` and `remove` must stay thin. Anything that belongs to // installation belongs in internal/plugin or internal/bundle, not here. +const removeDeleteDataHelp = "Delete plugin data (schema/tables). v1.5 keeps data unless --delete-data and asks for confirmation (or pass --yes); v1.4 plugin remove never asks and drops data unless --keep-data. Bundle data deletion asks for confirmation (or pass --yes) in both modes" + var installCmd = &cobra.Command{ Use: "install [name...]", Short: "Install a plugin or bundle", @@ -92,7 +94,7 @@ func init() { removeCmd.Flags().Bool("yes", false, "Skip confirmation prompts") removeCmd.Flags().Bool("keep-data", false, "Preserve database data on remove") - removeCmd.Flags().Bool("delete-data", false, "Delete plugin data (schema/tables). A bundle remove asks for confirmation (or pass --yes); a plugin remove never asks and drops data unless --keep-data") + removeCmd.Flags().Bool("delete-data", false, removeDeleteDataHelp) removeCmd.Flags().Bool("force", false, "Remove even if other plugins depend on this one") removeCmd.Flags().Bool("dry-run", false, "Print the planned actions without removing") diff --git a/cmd/commands/install_test.go b/cmd/commands/install_test.go index e5320bb2b..29aff8504 100644 --- a/cmd/commands/install_test.go +++ b/cmd/commands/install_test.go @@ -8,6 +8,24 @@ import ( "github.com/nself-org/cli/internal/plugin" ) +// TestRemoveDeleteDataHelp describes both compatibility modes so operators do +// not mistake v1.4's implicit data deletion for the v1.5 removal policy. +func TestRemoveDeleteDataHelp(t *testing.T) { + flag := removeCmd.Flags().Lookup("delete-data") + if flag == nil { + t.Fatal("remove is missing --delete-data") + } + for _, policy := range []string{ + "v1.5 keeps data unless --delete-data", + "asks for confirmation (or pass --yes)", + "v1.4 plugin remove never asks and drops data unless --keep-data", + } { + if !strings.Contains(flag.Usage, policy) { + t.Errorf("--delete-data help missing %q: %s", policy, flag.Usage) + } + } +} + // TestInstallResolvesBundlesBeforePlugins pins the resolution order CLI-R19 // depends on. A bundle and a plugin can share a name; `nself install nchat` // must install the bundle, not a single plugin that happens to match. diff --git a/internal/mcpgen/testdata/tools.golden.json b/internal/mcpgen/testdata/tools.golden.json index 05ffa8b8d..80cfc6ecd 100644 --- a/internal/mcpgen/testdata/tools.golden.json +++ b/internal/mcpgen/testdata/tools.golden.json @@ -25802,7 +25802,7 @@ "properties": { "delete-data": { "default": false, - "description": "Delete plugin data (schema/tables). A bundle remove asks for confirmation (or pass --yes); a plugin remove never asks and drops data unless --keep-data", + "description": "Delete plugin data (schema/tables). v1.5 keeps data unless --delete-data and asks for confirmation (or pass --yes); v1.4 plugin remove never asks and drops data unless --keep-data. Bundle data deletion asks for confirmation (or pass --yes) in both modes", "type": "boolean" }, "dry-run": { diff --git a/sdk/ts-cli-api/src/generated/types.ts b/sdk/ts-cli-api/src/generated/types.ts index 2d1552ad2..3459ca094 100644 --- a/sdk/ts-cli-api/src/generated/types.ts +++ b/sdk/ts-cli-api/src/generated/types.ts @@ -6335,7 +6335,7 @@ export interface RemoveData { } export type RemoveFlags = { - /** Delete plugin data (schema/tables). A bundle remove asks for confirmation (or pass --yes); a plugin remove never asks and drops data unless --keep-data */ + /** Delete plugin data (schema/tables). v1.5 keeps data unless --delete-data and asks for confirmation (or pass --yes); v1.4 plugin remove never asks and drops data unless --keep-data. Bundle data deletion asks for confirmation (or pass --yes) in both modes */ "delete-data"?: boolean; /** Print the planned actions without removing */ "dry-run"?: boolean;