Skip to content

Conversation

altitude
Copy link
Member

Add --metadata flag to fctl stack update to enable updating stack metadata as described in the help text.

The fctl stack update --help output indicated that stack metadata could be updated, but the --metadata flag was not implemented, leading to a discrepancy between documentation and functionality. This PR resolves that by adding the flag and its processing logic.


Slack Thread

Open in Cursor Open in Web

Copy link

cursor bot commented Sep 17, 2025

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

Copy link
Contributor

coderabbitai bot commented Sep 17, 2025

Walkthrough

Introduces a new --metadata flag to the stack update command, parses user-supplied metadata, merges it with the protected metadata (which is enforced/overridden), and sends the merged map in UpdateStackRequest.

Changes

Cohort / File(s) Summary
CLI update command: metadata flag and merge logic
cmd/stack/update.go
- Add metadataFlag and register WithStringSliceFlag
- Parse metadata via fctl.ParseMetadata(GetStringSlice)
- Build metadata map by merging user-provided entries, then setting ProtectedStackMetadata value
- Pass merged metadata to UpdateStackRequest

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant CLI as stack update (cmd)
  participant F as fctl (flag & metadata utils)
  participant API as UpdateStack API

  U->>CLI: run "stack update --metadata k=v ..."
  CLI->>F: GetStringSlice(metadataFlag)
  F-->>CLI: []string{"k=v", ...}
  CLI->>F: ParseMetadata(strings)
  F-->>CLI: map{ k: v, ... } or error
  alt parse error
    CLI-->>U: fail with "parsing metadata"
  else success
    note over CLI: Create metadata map<br/>Merge user entries<br/>Override ProtectedStackMetadata
    CLI->>API: UpdateStackRequest(metadata=merged)
    API-->>CLI: response
    CLI-->>U: success
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

I twitch my whiskers, flags in tow,
A map of keys begins to grow.
User notes I gently blend,
Protect the sacred to the end.
With stacks updated, off I hop—
Metadata merged, flip-flop! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "fix(stack): add missing stack metadata update flag" succinctly and accurately describes the primary change—adding the missing --metadata flag to the stack update command—and follows conventional commit style; it is specific, concise, and directly related to the changes in the diff. This makes it clear to reviewers what the main intent of the PR is.
Description Check ✅ Passed The description explains that the PR adds the --metadata flag to restore parity between help text and functionality, references a supporting Slack thread, and matches the changes shown in the summary, so it is directly related to the changeset. Given the lenient criteria for this check, the description is sufficient.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor/investigate-missing-stack-metadata-update-flag-f3d9

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@altitude altitude changed the title Investigate missing stack metadata update flag fix(stack): add missing stack metadata update flag Sep 17, 2025
@altitude altitude marked this pull request as ready for review September 17, 2025 09:21
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
cmd/stack/update.go (1)

62-67: Parsing path is fine once the default is fixed; consider trimming/ignoring empties defensively.

Optional: filter out blank entries before parsing to be resilient to odd env/quoting cases.

Example:

-    userMetadata, err := fctl.ParseMetadata(fctl.GetStringSlice(cmd, metadataFlag))
+    rawMD := fctl.GetStringSlice(cmd, metadataFlag)
+    filtered := make([]string, 0, len(rawMD))
+    for _, s := range rawMD {
+        if strings.TrimSpace(s) != "" {
+            filtered = append(filtered, s)
+        }
+    }
+    userMetadata, err := fctl.ParseMetadata(filtered)

Note: add strings import if you adopt this.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 47aee05 and a574ef1.

📒 Files selected for processing (1)
  • cmd/stack/update.go (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
cmd/stack/update.go (5)
pkg/command.go (1)
  • WithStringSliceFlag (160-164)
pkg/metadata.go (1)
  • ParseMetadata (13-23)
pkg/flags.go (2)
  • GetStringSlice (43-53)
  • GetBool (26-33)
pkg/approval.go (1)
  • ProtectedStackMetadata (25-25)
pkg/ptr.go (1)
  • BoolPointerToString (12-17)
🪛 GitHub Actions: Default
cmd/stack/update.go

[error] 68-79: CI cleanliness check failed due to uncommitted changes. The script ran 'git status --porcelain' and detected changes in cmd/stack/update.go, causing the step to exit with code 1.

🔇 Additional comments (2)
cmd/stack/update.go (2)

13-15: LGTM on adding metadataFlag.

Restores parity between help and functionality.


1-1: CI cleanliness: run formatting/codegen and commit

CI found a dirty working tree (affected: cmd/stack/update.go). Sandbox verification failed with "telemetry sidecar: os.Executable: readlink /proc/self/exe: no such file or directory" and "go: go.mod file not found"; cannot auto-verify here.

Run locally and push fixes:

  • git status --porcelain
  • git diff -- cmd/stack/update.go
  • gofmt -s -w .
  • go mod tidy
  • if [ -f Makefile ]; then make fmt && make generate; fi
  • git add cmd/stack/update.go
  • git commit -m "chore: format/codegen" && git push

}),
fctl.WithBoolFlag(unprotectFlag, false, "Unprotect stacks (no confirmation on write commands)"),
fctl.WithStringFlag(nameFlag, "", "Name of the stack"),
fctl.WithStringSliceFlag(metadataFlag, []string{""}, "Metadata to use"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: default []string{""} makes ParseMetadata fail when flag is omitted.

With the current default, GetStringSlice returns a single empty string, and ParseMetadata errors with “malformed metadata: ”. Use an empty slice as default.

Apply this diff:

-        fctl.WithStringSliceFlag(metadataFlag, []string{""}, "Metadata to use"),
+        fctl.WithStringSliceFlag(metadataFlag, []string{}, "Metadata key=value. Repeat the flag to add multiple entries"),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fctl.WithStringSliceFlag(metadataFlag, []string{""}, "Metadata to use"),
fctl.WithStringSliceFlag(metadataFlag, []string{}, "Metadata key=value. Repeat the flag to add multiple entries"),
🤖 Prompt for AI Agents
In cmd/stack/update.go around line 50, the flag default is set to []string{""}
which causes GetStringSlice to return a single empty string and ParseMetadata to
fail; change the flag default to an empty slice (i.e., []string{}) so omitted
flag yields no entries, then run tests/CLI to verify ParseMetadata no longer
errors when the flag is omitted.

Comment on lines +68 to 79
// Merge user metadata with protected metadata
protected := !fctl.GetBool(cmd, unprotectFlag)
metadata := map[string]string{
fctl.ProtectedStackMetadata: fctl.BoolPointerToString(&protected),
metadata := make(map[string]string)

// Add user metadata first
for k, v := range userMetadata {
metadata[k] = v
}

// Add/override with protected metadata
metadata[fctl.ProtectedStackMetadata] = fctl.BoolPointerToString(&protected)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

❓ Verification inconclusive

Risk: updating name with no --metadata will wipe existing metadata keys.

You always send a fresh map containing only user pairs plus the protected key. If the stack already has other metadata, those keys are lost on update. Preserve existing metadata and still enforce the protected key.

Apply this minimal merge and gate the request payload:

-    // Merge user metadata with protected metadata
-    protected := !fctl.GetBool(cmd, unprotectFlag)
-    metadata := make(map[string]string)
-    
-    // Add user metadata first
-    for k, v := range userMetadata {
-        metadata[k] = v
-    }
-    
-    // Add/override with protected metadata
-    metadata[fctl.ProtectedStackMetadata] = fctl.BoolPointerToString(&protected)
+    // Prepare metadata (user-supplied now, merge existing later)
+    protected := !fctl.GetBool(cmd, unprotectFlag)
+    metadata := make(map[string]string, len(userMetadata)+1)
+    for k, v := range userMetadata {
+        metadata[k] = v
+    }
+    // Enforce protected flag; prefer direct bool->string if available.
+    metadata[fctl.ProtectedStackMetadata] = fctl.BoolToString(protected)
@@
-    name := fctl.GetString(cmd, nameFlag)
+    // Preserve existing metadata keys not provided by the user
+    if stack.Data != nil && stack.Data.Metadata != nil {
+        for k, v := range stack.Data.Metadata {
+            if _, exists := metadata[k]; !exists {
+                metadata[k] = v
+            }
+        }
+    }
+    name := fctl.GetString(cmd, nameFlag)
@@
-    req := membershipclient.UpdateStackRequest{
-        Name:     name,
-        Metadata: pointer.For(metadata),
-    }
+    // If neither --metadata nor --unprotect was provided, avoid sending Metadata to prevent unintended changes.
+    var metaPtr *map[string]string
+    if len(userMetadata) > 0 || cmd.Flags().Changed(unprotectFlag) {
+        metaPtr = pointer.For(metadata)
+    }
+    req := membershipclient.UpdateStackRequest{
+        Name:     name,
+        Metadata: metaPtr,
+    }

If BoolToString isn’t available in your pkg, keep BoolPointerToString(&protected).


Don't overwrite existing stack metadata — merge current metadata and only send Metadata when the user changed it.

File: cmd/stack/update.go Lines: 68-79

The code builds a fresh metadata map (userMetadata + protected key) and will erase any existing metadata on update; merge stack.Data.Metadata (preserving keys not provided by the user) and only include Metadata in UpdateStackRequest when userMetadata is non-empty or the unprotect flag was changed.

-    // Merge user metadata with protected metadata
-    protected := !fctl.GetBool(cmd, unprotectFlag)
-    metadata := make(map[string]string)
-    
-    // Add user metadata first
-    for k, v := range userMetadata {
-        metadata[k] = v
-    }
-    
-    // Add/override with protected metadata
-    metadata[fctl.ProtectedStackMetadata] = fctl.BoolPointerToString(&protected)
+    // Prepare metadata (user-supplied now, merge existing later)
+    protected := !fctl.GetBool(cmd, unprotectFlag)
+    metadata := make(map[string]string, len(userMetadata)+1)
+    for k, v := range userMetadata {
+        metadata[k] = v
+    }
+    // Enforce protected flag; prefer direct bool->string if available.
+    metadata[fctl.ProtectedStackMetadata] = fctl.BoolToString(protected)
@@
-    name := fctl.GetString(cmd, nameFlag)
+    // Preserve existing metadata keys not provided by the user
+    if stack.Data != nil && stack.Data.Metadata != nil {
+        for k, v := range stack.Data.Metadata {
+            if _, exists := metadata[k]; !exists {
+                metadata[k] = v
+            }
+        }
+    }
+    name := fctl.GetString(cmd, nameFlag)
@@
-    req := membershipclient.UpdateStackRequest{
-        Name:     name,
-        Metadata: pointer.For(metadata),
-    }
+    // If neither --metadata nor --unprotect was provided, avoid sending Metadata to prevent unintended changes.
+    var metaPtr *map[string]string
+    if len(userMetadata) > 0 || cmd.Flags().Changed(unprotectFlag) {
+        metaPtr = pointer.For(metadata)
+    }
+    req := membershipclient.UpdateStackRequest{
+        Name:     name,
+        Metadata: metaPtr,
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Merge user metadata with protected metadata
protected := !fctl.GetBool(cmd, unprotectFlag)
metadata := map[string]string{
fctl.ProtectedStackMetadata: fctl.BoolPointerToString(&protected),
metadata := make(map[string]string)
// Add user metadata first
for k, v := range userMetadata {
metadata[k] = v
}
// Add/override with protected metadata
metadata[fctl.ProtectedStackMetadata] = fctl.BoolPointerToString(&protected)
// Prepare metadata (user-supplied now, merge existing later)
protected := !fctl.GetBool(cmd, unprotectFlag)
metadata := make(map[string]string, len(userMetadata)+1)
for k, v := range userMetadata {
metadata[k] = v
}
// Enforce protected flag; prefer direct bool->string if available.
metadata[fctl.ProtectedStackMetadata] = fctl.BoolToString(protected)
// Preserve existing metadata keys not provided by the user
if stack.Data != nil && stack.Data.Metadata != nil {
for k, v := range stack.Data.Metadata {
if _, exists := metadata[k]; !exists {
metadata[k] = v
}
}
}
name := fctl.GetString(cmd, nameFlag)
// If neither --metadata nor --unprotect was provided, avoid sending Metadata to prevent unintended changes.
var metaPtr *map[string]string
if len(userMetadata) > 0 || cmd.Flags().Changed(unprotectFlag) {
metaPtr = pointer.For(metadata)
}
req := membershipclient.UpdateStackRequest{
Name: name,
Metadata: metaPtr,
}
🧰 Tools
🪛 GitHub Actions: Default

[error] 68-79: CI cleanliness check failed due to uncommitted changes. The script ran 'git status --porcelain' and detected changes in cmd/stack/update.go, causing the step to exit with code 1.

🤖 Prompt for AI Agents
In cmd/stack/update.go around lines 68-79, the current code creates a fresh
metadata map which wipes existing stack metadata; instead, fetch and clone the
current stack.Data.Metadata into your working metadata map first, then overlay
userMetadata and the protected key so keys not provided by the user are
preserved. Only set the UpdateStackRequest.Metadata field if the user provided
any metadata changes (userMetadata is non-empty) or if the protected/unprotect
flag changed compared to the original stack.Data.Metadata value (compare
original protected value vs intended protected); otherwise omit Metadata from
the request to avoid erasing existing values.

@Dav-14
Copy link
Contributor

Dav-14 commented Sep 17, 2025

Resolve coderabbit comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants