Skip to content

feat(cli): add chat share remove command#25842

Draft
DanielleMaywood wants to merge 8 commits into
feat/chat-share-addfrom
feat/chat-share-remove
Draft

feat(cli): add chat share remove command#25842
DanielleMaywood wants to merge 8 commits into
feat/chat-share-addfrom
feat/chat-share-remove

Conversation

@DanielleMaywood
Copy link
Copy Markdown
Contributor

Note

🤖 This PR was written by Coder Agent on behalf of Danielle Maywood

Adds coder exp chat share remove for revoking shared user or group access from chats.

This builds on the chat share helpers from the previous PR, rejects role syntax for removals, maps removed actors to the chat deleted role, updates the ACL, and prints the resulting ACL.

Implementation plan

Chat Share CLI Implementation Plan

For agentic workers: use subagent-driven-development for independent
tasks, or executing-plans for inline execution. Track steps with checkbox
syntax.

Goal: Introduce chat sharing CLI support as a three-PR stack: coder exp chat share add, then remove, then status.

Architecture: Extend the existing experimental chat CLI in cli/exp_chat.go with a share subgroup that uses codersdk.NewExperimentalClient(client) to call the existing chat ACL API. The stack is additive: PR 1 creates shared parsing, lookup, formatting helpers plus add; PR 2 adds remove using the same helpers; PR 3 adds status. Each PR is independently testable and targets the previous branch in the stack.

Tech Stack: Go, github.com/coder/serpent for CLI commands, cli/cliui for table and JSON formatting, codersdk.ExperimentalClient chat ACL methods, coderdtest plus dbgen.Chat for CLI integration tests, make test RUN=..., make fmt, and make lint.


Stack and branch strategy

Use three feature branches and three draft PRs:

  1. PR 1, base main: introduce coder exp chat share add.
  2. PR 2, base PR 1 branch: introduce coder exp chat share remove.
  3. PR 3, base PR 2 branch: introduce coder exp chat share status.

Merge order after review:

  1. Merge PR 3 into PR 2.
  2. Merge PR 2 into PR 1.
  3. Merge PR 1 into main.

Before each commit or push, verify the current branch and remote target. Do not push directly to main. Use explicit branch names, for example:

git switch -c feat/chat-share-add
git switch -c feat/chat-share-remove
git switch -c feat/chat-share-status

When opening draft PRs, set the base branch explicitly:

gh pr create --draft --base main --head feat/chat-share-add
gh pr create --draft --base feat/chat-share-add --head feat/chat-share-remove
gh pr create --draft --base feat/chat-share-remove --head feat/chat-share-status

Include the required Coder Agent disclosure in PR bodies.

Command design

The command namespace is under the existing experimental chat command:

coder exp chat share add <chat-id> --user alice:read --group developers:read
coder exp chat share remove <chat-id> --user alice --group developers
coder exp chat share status <chat-id>

add accepts name:role and defaults an omitted role to read, so --user alice is equivalent to --user alice:read. This preserves future role extensibility while keeping the current single-role UX convenient.

remove accepts names without roles and rejects name:role, because removal maps to codersdk.ChatRoleDeleted and roles on removal are ambiguous.

status prints the current chat ACL without changing it.

All three commands print the current ACL as table output by default. The formatter must also support JSON through cliui.JSONFormat(), matching workspace sharing.

File map

Files to modify:

  • cli/exp_chat.go
    • Register r.chatShareCommand() in chatCommand().Children.
    • Add chatShareCommand, chatShareAddCommand, chatShareRemoveCommand, and chatShareStatusCommand across the stack.
    • Add chat-specific ACL role parsing, user/group lookup, UUID parsing, and table formatting helpers.
  • cli/exp_chat_test.go
    • Add integration tests for add, remove, status, role validation, name lookup, and output.

No SDK, API, database, or generated files should be needed. The API already exists in codersdk/chats.go and coderd/exp_chats_acl.go.

Shared implementation details

Add these helpers in cli/exp_chat.go, near the existing chat helpers or below the command definitions:

const chatShareDefaultGroupDisplay = "-"

type chatRoleLookupParams struct {
	Client      *codersdk.Client
	OrgID       uuid.UUID
	OrgName     string
	Users       [][2]string
	Groups      [][2]string
	DefaultRole codersdk.ChatRole
}

func parseChatShareID(raw string) (uuid.UUID, error)
func parseChatShareActorRole(raw string) ([2]string, error)
func parseChatShareActor(raw string) ([2]string, error)
func stringToChatRole(role string) (codersdk.ChatRole, error)
func fetchChatUsersAndGroups(ctx context.Context, params chatRoleLookupParams) (map[string]codersdk.ChatRole, map[string]codersdk.ChatRole, error)
func chatACLToTable(ctx context.Context, acl *codersdk.ChatACL) (string, error)

Expected helper behavior:

  • parseChatShareID parses UUIDs and returns invalid chat ID %q: %w on failure.
  • parseChatShareActorRole parses name and name:role. It returns [2]string{name, role} with an empty role when omitted. It rejects empty names, empty roles after a colon, and strings with more than one colon.
  • parseChatShareActor parses removal actors. It rejects any colon with an error like invalid user format %q: roles are only accepted by chat share add from the calling command.
  • stringToChatRole accepts only read and deleted role "". For user-entered add roles, only call it after defaulting empty role to read, so "" is not accepted as explicit user input for add.
  • fetchChatUsersAndGroups mirrors fetchUsersAndGroups in cli/sharing.go, but returns codersdk.ChatRole maps. Resolve users through client.OrganizationMembers(ctx, orgID) and groups through client.Groups(ctx, codersdk.GroupArguments{Organization: orgID.String()}).
  • chatACLToTable uses cliui.NewOutputFormatter(cliui.TableFormat([]chatShareRow{}, []string{"User", "Group", "Role"}), cliui.JSONFormat()). Output one row per non-deleted user and one row per non-deleted group. Do not expand group members.

Use the chat's organization for lookup. GetChat(ctx, chatID) returns a codersdk.Chat; use chat.OrganizationID when calling fetchChatUsersAndGroups.

PR 1: Introduce coder exp chat share add

Task 1: Add failing tests for chat share add

Files:

  • Test: cli/exp_chat_test.go

  • Step 1: Add test imports.

Extend imports in cli/exp_chat_test.go to include the packages required by chat ACL integration tests:

import (
	"bytes"
	"fmt"
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/cli/clitest"
	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbgen"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/testutil"
)

Keep existing imports that are still used and let gofmt sort them.

  • Step 2: Add TestExpChatShareAdd.

Create a test with subtests:

func TestExpChatShareAdd(t *testing.T) {
	t.Parallel()

	t.Run("ShareWithUserExplicitReadRole", func(t *testing.T) {
		t.Parallel()

		client, db := coderdtest.NewWithDatabase(t, nil)
		firstUser := coderdtest.CreateFirstUser(t, client)
		chatOwnerClient, chatOwner := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID, rbac.ScopedRoleOrgAuditor(firstUser.OrganizationID))
		_, toShareWithUser := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
		chat := dbgen.Chat(t, db, database.Chat{
			OrganizationID: firstUser.OrganizationID,
			OwnerID:        chatOwner.ID,
			Title:          "share add user",
		})

		ctx := testutil.Context(t, testutil.WaitMedium)
		inv, root := clitest.New(t, "exp", "chat", "share", "add", chat.ID.String(), "--user", toShareWithUser.Username+":read")
		clitest.SetupConfig(t, chatOwnerClient, root)

		out := new(bytes.Buffer)
		inv.Stdout = out
		err := inv.WithContext(ctx).Run()
		require.NoError(t, err)

		acl, err := codersdk.NewExperimentalClient(chatOwnerClient).GetChatACL(ctx, chat.ID)
		require.NoError(t, err)
		assert.Contains(t, acl.Users, codersdk.ChatUser{
			MinimalUser: codersdk.MinimalUser{
				ID:        toShareWithUser.ID,
				Username:  toShareWithUser.Username,
				Name:      toShareWithUser.Name,
				AvatarURL: toShareWithUser.AvatarURL,
			},
			Role: codersdk.ChatRoleRead,
		})
		assert.Contains(t, out.String(), toShareWithUser.Username)
		assert.Contains(t, out.String(), string(codersdk.ChatRoleRead))
	})
}

Add these additional subtests in the same function:

  • ShareWithUserDefaultReadRole: call --user <username> and assert the persisted role is read.
  • ShareWithMultipleUsers: call fmt.Sprintf("--user=%s:read,%s:read", user1.Username, user2.Username) and assert both users are in the ACL.
  • RejectsUnknownRole: use --user alice:write with a valid chat and assert the error contains invalid role "write".
  • RequiresActor: invoke exp chat share add <chat-id> without --user or --group and assert the error contains at least one user or group must be provided.
  • RejectsInvalidChatID: invoke exp chat share add not-a-uuid --user alice:read and assert the error contains invalid chat ID.

Do not add group tests in PR 1 unless the initial user tests are passing quickly. If group tests are added, create a group through the same helper pattern used in cli/sharing_test.go and assert one group row is printed.

  • Step 3: Run the new test and verify it fails because the command does not exist.
make test RUN=TestExpChatShareAdd

Expected: failure mentions the missing share or add command.

Task 2: Implement chat share add

Files:

  • Modify: cli/exp_chat.go

  • Step 1: Add required imports.

Add imports used by the new helpers:

"context"
"strings"

"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"

Keep existing imports and run gofmt later.

  • Step 2: Register the share subgroup.

Update chatCommand().Children from:

Children: []*serpent.Command{
	r.chatContextCommand(),
},

to:

Children: []*serpent.Command{
	r.chatContextCommand(),
	r.chatShareCommand(),
},
  • Step 3: Add chatShareCommand with only add in PR 1.
func (r *RootCmd) chatShareCommand() *serpent.Command {
	return &serpent.Command{
		Use:   "share",
		Short: "Manage chat sharing",
		Long:  "Share chats with users and groups.",
		Handler: func(i *serpent.Invocation) error {
			return i.Command.HelpHandler(i)
		},
		Children: []*serpent.Command{
			r.chatShareAddCommand(),
		},
	}
}
  • Step 4: Add chatShareAddCommand.

Implement this shape:

func (r *RootCmd) chatShareAddCommand() *serpent.Command {
	var users []string
	var groups []string

	return &serpent.Command{
		Use:   "add <chat-id> --user <user>:<role> --group <group>:<role>",
		Short: "Share a chat with a user or group.",
		Options: serpent.OptionSet{
			{Name: "user", Description: "A comma separated list of users to share the chat with.", Flag: "user", Value: serpent.StringArrayOf(&users)},
			{Name: "group", Description: "A comma separated list of groups to share the chat with.", Flag: "group", Value: serpent.StringArrayOf(&groups)},
		},
		Middleware: serpent.Chain(serpent.RequireNArgs(1)),
		Handler: func(inv *serpent.Invocation) error {
			if len(users) == 0 && len(groups) == 0 {
				return xerrors.New("at least one user or group must be provided")
			}

			client, err := r.InitClient(inv)
			if err != nil {
				return err
			}
			experimentalClient := codersdk.NewExperimentalClient(client)

			chatID, err := parseChatShareID(inv.Args[0])
			if err != nil {
				return err
			}

			chat, err := experimentalClient.GetChat(inv.Context(), chatID)
			if err != nil {
				return xerrors.Errorf("unable to fetch chat %s: %w", inv.Args[0], err)
			}

			userRoleStrings := make([][2]string, len(users))
			for i, user := range users {
				parsed, err := parseChatShareActorRole(user)
				if err != nil {
					return xerrors.Errorf("invalid user format %q: %w", user, err)
				}
				userRoleStrings[i] = parsed
			}

			groupRoleStrings := make([][2]string, len(groups))
			for i, group := range groups {
				parsed, err := parseChatShareActorRole(group)
				if err != nil {
					return xerrors.Errorf("invalid group format %q: %w", group, err)
				}
				groupRoleStrings[i] = parsed
			}

			userRoles, groupRoles, err := fetchChatUsersAndGroups(inv.Context(), chatRoleLookupParams{
				Client:      client,
				OrgID:       chat.OrganizationID,
				Users:       userRoleStrings,
				Groups:      groupRoleStrings,
				DefaultRole: codersdk.ChatRoleRead,
			})
			if err != nil {
				return err
			}

			if err := experimentalClient.UpdateChatACL(inv.Context(), chat.ID, codersdk.UpdateChatACL{UserRoles: userRoles, GroupRoles: groupRoles}); err != nil {
				return err
			}

			acl, err := experimentalClient.GetChatACL(inv.Context(), chat.ID)
			if err != nil {
				return xerrors.Errorf("could not fetch current chat ACL after sharing: %w", err)
			}
			out, err := chatACLToTable(inv.Context(), &acl)
			if err != nil {
				return err
			}
			_, err = fmt.Fprintln(inv.Stdout, out)
			return err
		},
	}
}

If codersdk.Chat does not expose OrganizationID under that exact name, inspect codersdk/chats.go and use the actual field.

  • Step 5: Add shared helpers.

Implement the helper signatures from the shared implementation section. Use strings.Count(raw, ":") for actor parsing. Use codersdk.UsernameValidRegex.MatchString(name) for basic actor-name validation, matching workspace sharing's validation style. For groups, use the same validation as users unless an existing group-name validation helper is available nearby.

stringToChatRole must be:

func stringToChatRole(role string) (codersdk.ChatRole, error) {
	switch role {
	case string(codersdk.ChatRoleRead):
		return codersdk.ChatRoleRead, nil
	case string(codersdk.ChatRoleDeleted):
		return codersdk.ChatRoleDeleted, nil
	default:
		return "", xerrors.Errorf("invalid role %q: expected %q", role, codersdk.ChatRoleRead)
	}
}

In fetchChatUsersAndGroups, when a parsed role is empty, replace it with string(params.DefaultRole) before calling stringToChatRole.

  • Step 6: Run gofmt.
make fmt
  • Step 7: Run the new test and verify it passes.
make test RUN=TestExpChatShareAdd

Task 3: Verify PR 1 broadly and commit

Files:

  • Modify: cli/exp_chat.go

  • Test: cli/exp_chat_test.go

  • Step 1: Run adjacent chat CLI tests.

make test RUN='TestExpChat'
  • Step 2: Run workspace sharing tests to check helper-adjacent behavior did not regress.
make test RUN='TestSharing'
  • Step 3: Run lint if time permits before PR, otherwise note it for pre-commit.
make lint
  • Step 4: Commit on the PR 1 branch.

Check branch first:

git status --short --branch
git branch --show-current

Commit message:

git add cli/exp_chat.go cli/exp_chat_test.go
git commit -m 'feat(cli): add chat share add command'

PR 2: Introduce coder exp chat share remove

Start from the PR 1 branch and create a stacked branch:

git switch feat/chat-share-add
git switch -c feat/chat-share-remove

Task 4: Add failing tests for chat share remove

Files:

  • Test: cli/exp_chat_test.go

  • Step 1: Add TestExpChatShareRemove.

Create a test with subtests:

func TestExpChatShareRemove(t *testing.T) {
	t.Parallel()

	t.Run("RemoveSharedUser", func(t *testing.T) {
		t.Parallel()

		client, db := coderdtest.NewWithDatabase(t, nil)
		firstUser := coderdtest.CreateFirstUser(t, client)
		chatOwnerClient, chatOwner := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID, rbac.ScopedRoleOrgAuditor(firstUser.OrganizationID))
		_, sharedUser := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
		chat := dbgen.Chat(t, db, database.Chat{
			OrganizationID: firstUser.OrganizationID,
			OwnerID:        chatOwner.ID,
			Title:          "share remove user",
		})
		experimentalClient := codersdk.NewExperimentalClient(chatOwnerClient)
		err := experimentalClient.UpdateChatACL(testutil.Context(t, testutil.WaitMedium), chat.ID, codersdk.UpdateChatACL{
			UserRoles: map[string]codersdk.ChatRole{sharedUser.ID.String(): codersdk.ChatRoleRead},
		})
		require.NoError(t, err)

		ctx := testutil.Context(t, testutil.WaitMedium)
		inv, root := clitest.New(t, "exp", "chat", "share", "remove", chat.ID.String(), "--user", sharedUser.Username)
		clitest.SetupConfig(t, chatOwnerClient, root)

		out := new(bytes.Buffer)
		inv.Stdout = out
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)

		acl, err := experimentalClient.GetChatACL(ctx, chat.ID)
		require.NoError(t, err)
		for _, user := range acl.Users {
			assert.NotEqual(t, sharedUser.ID, user.ID)
		}
		assert.NotContains(t, out.String(), sharedUser.Username)
	})
}

Add these additional subtests:

  • RequiresActor: invoke exp chat share remove <chat-id> without --user or --group and assert the error contains at least one user or group must be provided.

  • RejectsRoleSyntax: invoke exp chat share remove <chat-id> --user alice:read and assert the error contains roles are only accepted by chat share add.

  • RejectsInvalidChatID: invoke exp chat share remove not-a-uuid --user alice and assert the error contains invalid chat ID.

  • Step 2: Run the new test and verify it fails because remove does not exist.

make test RUN=TestExpChatShareRemove

Task 5: Implement chat share remove

Files:

  • Modify: cli/exp_chat.go

  • Step 1: Register remove in chatShareCommand.

Update children to:

Children: []*serpent.Command{
	r.chatShareAddCommand(),
	r.chatShareRemoveCommand(),
},
  • Step 2: Add chatShareRemoveCommand.

Implement the same structure as add with these differences:

  • Use: remove <chat-id> --user <user> --group <group>
  • Short: Remove shared access for users or groups from a chat.
  • Parse actors with parseChatShareActor, not parseChatShareActorRole.
  • On parse error, wrap with invalid user format %q: %w or invalid group format %q: %w.
  • Call fetchChatUsersAndGroups with DefaultRole: codersdk.ChatRoleDeleted.
  • Call UpdateChatACL with deleted-role maps.
  • Fetch and print the ACL after removal with chatACLToTable.

parseChatShareActor should return an error when strings.Contains(raw, ":") is true. The error text should contain roles are only accepted by chat share add.

  • Step 3: Run gofmt.
make fmt
  • Step 4: Run the remove test and verify it passes.
make test RUN=TestExpChatShareRemove

Task 6: Verify PR 2 broadly and commit

Files:

  • Modify: cli/exp_chat.go

  • Test: cli/exp_chat_test.go

  • Step 1: Run all chat share tests from PR 1 and PR 2.

make test RUN='TestExpChatShare(Add|Remove)'
  • Step 2: Run adjacent chat CLI tests.
make test RUN='TestExpChat'
  • Step 3: Commit on the PR 2 branch.
git status --short --branch
git branch --show-current
git add cli/exp_chat.go cli/exp_chat_test.go
git commit -m 'feat(cli): add chat share remove command'

PR 3: Introduce coder exp chat share status

Start from the PR 2 branch and create a stacked branch:

git switch feat/chat-share-remove
git switch -c feat/chat-share-status

Task 7: Add failing tests for chat share status

Files:

  • Test: cli/exp_chat_test.go

  • Step 1: Add TestExpChatShareStatus.

Create a test with subtests:

func TestExpChatShareStatus(t *testing.T) {
	t.Parallel()

	t.Run("ListsSharedUsers", func(t *testing.T) {
		t.Parallel()

		client, db := coderdtest.NewWithDatabase(t, nil)
		firstUser := coderdtest.CreateFirstUser(t, client)
		chatOwnerClient, chatOwner := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID, rbac.ScopedRoleOrgAuditor(firstUser.OrganizationID))
		_, sharedUser := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
		chat := dbgen.Chat(t, db, database.Chat{
			OrganizationID: firstUser.OrganizationID,
			OwnerID:        chatOwner.ID,
			Title:          "share status user",
		})
		experimentalClient := codersdk.NewExperimentalClient(chatOwnerClient)
		ctx := testutil.Context(t, testutil.WaitMedium)
		err := experimentalClient.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{
			UserRoles: map[string]codersdk.ChatRole{sharedUser.ID.String(): codersdk.ChatRoleRead},
		})
		require.NoError(t, err)

		inv, root := clitest.New(t, "exp", "chat", "share", "status", chat.ID.String())
		clitest.SetupConfig(t, chatOwnerClient, root)

		out := new(bytes.Buffer)
		inv.Stdout = out
		err = inv.WithContext(ctx).Run()
		require.NoError(t, err)

		assert.Contains(t, out.String(), sharedUser.Username)
		assert.Contains(t, out.String(), string(codersdk.ChatRoleRead))
	})
}

Add these additional subtests:

  • RejectsInvalidChatID: invoke exp chat share status not-a-uuid and assert invalid chat ID.

  • RequiresChatID: invoke exp chat share status and assert the command fails due to required argument.

  • Step 2: Run the status test and verify it fails because status does not exist.

make test RUN=TestExpChatShareStatus

Task 8: Implement chat share status

Files:

  • Modify: cli/exp_chat.go

  • Step 1: Register status in chatShareCommand.

Update children to:

Children: []*serpent.Command{
	r.chatShareAddCommand(),
	r.chatShareRemoveCommand(),
	r.chatShareStatusCommand(),
},
  • Step 2: Add chatShareStatusCommand.

Implement this shape:

func (r *RootCmd) chatShareStatusCommand() *serpent.Command {
	return &serpent.Command{
		Use:   "status <chat-id>",
		Short: "List all users and groups the given chat is shared with.",
		Middleware: serpent.Chain(serpent.RequireNArgs(1)),
		Handler: func(inv *serpent.Invocation) error {
			client, err := r.InitClient(inv)
			if err != nil {
				return err
			}
			chatID, err := parseChatShareID(inv.Args[0])
			if err != nil {
				return err
			}
			acl, err := codersdk.NewExperimentalClient(client).GetChatACL(inv.Context(), chatID)
			if err != nil {
				return xerrors.Errorf("unable to fetch ACL for chat: %w", err)
			}
			out, err := chatACLToTable(inv.Context(), &acl)
			if err != nil {
				return err
			}
			_, err = fmt.Fprintln(inv.Stdout, out)
			return err
		},
	}
}
  • Step 3: Run gofmt.
make fmt
  • Step 4: Run the status test and verify it passes.
make test RUN=TestExpChatShareStatus

Task 9: Verify PR 3 and full stack

Files:

  • Modify: cli/exp_chat.go

  • Test: cli/exp_chat_test.go

  • Step 1: Run all chat share tests.

make test RUN='TestExpChatShare'
  • Step 2: Run all experimental chat CLI tests.
make test RUN='TestExpChat'
  • Step 3: Run workspace sharing tests as regression coverage.
make test RUN='TestSharing'
  • Step 4: Run lint.
make lint
  • Step 5: Commit on the PR 3 branch.
git status --short --branch
git branch --show-current
git add cli/exp_chat.go cli/exp_chat_test.go
git commit -m 'feat(cli): add chat share status command'

PR descriptions

Each PR should be a draft PR and include the Coder Agent disclosure:

> [!NOTE]
> 🤖 This PR was written by Coder Agent on behalf of Danielle Maywood

PR 1 summary:

Adds `coder exp chat share add` for granting read access to a chat by user or group. The command resolves names in the chat organization, updates the existing chat ACL API, and prints the resulting ACL.

PR 2 summary:

Adds `coder exp chat share remove` on top of chat share add. The command resolves users and groups in the chat organization, removes their chat ACL entries, and prints the resulting ACL.

PR 3 summary:

Adds `coder exp chat share status` on top of chat share add/remove. The command lists users and groups with access to a chat using the existing chat ACL API.

Final verification checklist

Run from the top stacked branch after PR 3 implementation:

make fmt
make test RUN='TestExpChatShare'
make test RUN='TestExpChat'
make test RUN='TestSharing'
make lint

Expected result: all commands pass. If make lint reports generated or unrelated repository issues, capture the exact output in the PR notes and do not hide it.

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.

1 participant