Skip to content

feat(experiment): create and delete developer sandboxes#389

Open
vegeris wants to merge 5 commits intomainfrom
evegeris-sandbox-integration-create-delete
Open

feat(experiment): create and delete developer sandboxes#389
vegeris wants to merge 5 commits intomainfrom
evegeris-sandbox-integration-create-delete

Conversation

@vegeris
Copy link
Contributor

@vegeris vegeris commented Mar 11, 2026

Changelog

Experimental Feature: --experiment sandboxes

We're adding support for managing Slack developer sandboxes from within the CLI. We now allow one to create or delete a sandbox from the CLI.

  • sandbox create allows you to create a new developer sandbox.
  • sandbox delete allows you to archive an existing developer sandbox.

Summary

This PR adds sandbox create and sandbox delete commands, which allow one to create or delete a sandbox from the CLI.

Follow up to #379

Testing

Try out the commands:

% hermes sandbox create --experiment=sandboxes --name "my-test-box" --password "secretPassword"

% hermes sandbox delete --experiment=sandboxes --sandbox-id E012345

Requirements

@vegeris vegeris added the semver:patch Use on pull requests to describe the release version increment label Mar 11, 2026
@vegeris vegeris force-pushed the evegeris-sandbox-integration-create-delete branch 5 times, most recently from 6a724f1 to 37c62fb Compare March 11, 2026 18:38
@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 67.54386% with 74 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.03%. Comparing base (4d5f768) to head (a35fbc3).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
internal/api/sandbox.go 0.00% 46 Missing ⚠️
cmd/sandbox/create.go 85.83% 10 Missing and 7 partials ⚠️
cmd/sandbox/delete.go 83.05% 6 Missing and 4 partials ⚠️
cmd/sandbox/sandbox.go 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #389      +/-   ##
==========================================
+ Coverage   67.01%   67.03%   +0.01%     
==========================================
  Files         218      220       +2     
  Lines       18090    18318     +228     
==========================================
+ Hits        12123    12279     +156     
- Misses       4833     4892      +59     
- Partials     1134     1147      +13     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vegeris vegeris force-pushed the evegeris-sandbox-integration-create-delete branch 2 times, most recently from 98ecc15 to f200970 Compare March 11, 2026 23:43
@vegeris vegeris force-pushed the evegeris-sandbox-integration-create-delete branch from f200970 to cea2bcd Compare March 12, 2026 18:56
@vegeris vegeris marked this pull request as ready for review March 12, 2026 19:00
@vegeris vegeris requested a review from a team as a code owner March 12, 2026 19:00
},
}))
clients.IO.PrintInfo(ctx, false, "Manage this sandbox from the CLI or visit\n%s", style.Secondary("https://api.slack.com/developer-program/sandboxes"))
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Preview:

% hermes sandbox create --experiment=sandboxes --name "EV test box 7245"  --password "jhsdjdkhfkdfgkgd"

🏖️  Sandbox Created
   Team ID: E0197Q6CTV1
   URL: https://ev-test-box-7245.enterprise.dev.slack.com/

Manage this sandbox from the CLI or visit
https://api.slack.com/developer-program/sandboxes

Copy link
Member

Choose a reason for hiding this comment

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

praise: Excellent formatting choice!

}

return nil
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Preview:

% hermes sandbox delete --experiment=sandboxes --sandbox-id E0196ATAVGT
Choose a Slack team where your email address matches your Slack developer account
? Select a team for authentication slackforceorg E014LMDF01H

⚠️  Danger zone
   Sandbox (E0196ATAVGT) and all of its data will be permanently deleted
   This cannot be undone

? Are you sure you want to delete the sandbox? Yes

✅ Sandbox deleted
   Sandbox E0196ATAVGT has been permanently deleted

🏖️  Developer Sandboxes
   Owned by Slack developer account ev@mail.com

  EV test box 8274368 (E0123456)
    URL: https://ev-test-box-29378.slack.com
    Status: ACTIVE
    Created: 2026-03-12
    Expires: 2026-09-12

Learn more at https://docs.slack.dev/tools/developer-sandboxes

Copy link
Member

Choose a reason for hiding this comment

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

🫧 thought: This is solid! I'm optimistic that a conclusion of the charm experiment might let us hide selections and warnings earlier for outputs that are focused on the result, but nothing to think about in this PR!

Copy link
Member

Choose a reason for hiding this comment

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

praise: Excellent formatting choice!

@vegeris vegeris force-pushed the evegeris-sandbox-integration-create-delete branch from b23fbe4 to a35fbc3 Compare March 12, 2026 19:09
Example: style.ExampleCommandsf([]style.ExampleCommand{
{Command: "sandbox create --name test-box --password mypass", Meaning: "Create a sandbox named test-box"},
{Command: "sandbox create --name test-box --password mypass --domain test-box --archive-ttl 1d", Meaning: "Create a temporary sandbox that will be archived in 1 day"},
{Command: "sandbox create --name test-box --password mypass --domain test-box --archive-date 2025-12-31", Meaning: "Create a sandbox that will be archived on a specific date"},
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm wondering if it makes sense to support both of these ways of setting archive date; alternatively we could just accept the date. We currently archive sandboxes via a nightly cron job so even if you create a sandbox with a time to live of a few hours, it won't get archived until that job runs

Copy link
Member

Choose a reason for hiding this comment

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

two-cents: I actually like both flags --archive-date <YYYY-MM-DD> and --archive-ttl <str>. In CI/CD environments, I think the --archive-ttl will be easier to use while --archive-date is more preferred for a human.

If the Slack API supports both, I think we should support both. We'll just need to make sure that developers can't provide both together and document the various expressions supported by TTL.

Copy link
Member

@zimeg zimeg left a comment

Choose a reason for hiding this comment

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

@vegeris LGTM! Thanks for bringing a complete sandbox experiment to scripting. I'm excited for these options 🌈 ✨

A few comments that follow are quibbles to implementation but nothing that ought block this from merging. It might be nice to add more tests to API methods, but we can explore some of this in future changes of course 🚢 💨

Copy link
Member

Choose a reason for hiding this comment

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

🧪 question: Do we have adjacent unit test or two for these API methods?

}

// Prompt the user to select a team to use for authentication
clients.IO.PrintInfo(ctx, false, "%s", style.Secondary("Choose a Slack team where your email address matches your Slack developer account"))
Copy link
Member

Choose a reason for hiding this comment

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

💌 praise: Super helpful comment for the unfamiliar explorers I think! Thanks for adding this and team prompts also!

🔭 thought: We might have adjacent experimental changes that we can use to inline this as "help" text within the prompt - #400. It might be nice to revisit this output once that becomes stable?

Suggested change
clients.IO.PrintInfo(ctx, false, "%s", style.Secondary("Choose a Slack team where your email address matches your Slack developer account"))
// TODO(experiment:charm): Change this to prompt "help" message once charm is stable
clients.IO.PrintInfo(ctx, false, "%s", style.Secondary("Choose a Slack team where your email address matches your Slack developer account"))

// Sandbox represents a Slack Developer Sandbox
type Sandbox struct {
DateArchived int64 `json:"date_archived"` // When the developer sandbox is or will be archived, as epoch seconds
DateCreated int64 `json:"date_created"` // When the developer sandbox was created, as epoch seconds
Copy link
Member

Choose a reason for hiding this comment

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

🌟 praise: To kind epoch!

Comment on lines +187 to +193
// Trim leading/trailing hyphens
for len(b) > 0 && b[0] == '-' {
b = b[1:]
}
for len(b) > 0 && b[len(b)-1] == '-' {
b = b[:len(b)-1]
}
Copy link
Member

Choose a reason for hiding this comment

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

🐭 suggestion: We can perhaps use strings.Trim here instead?

if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b = append(b, byte(r))
} else if r >= 'A' && r <= 'Z' {
b = append(b, byte(r+32))
Copy link
Member

Choose a reason for hiding this comment

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

🔍 suggestion: I forget what "string" characters are available in a loop, but would the strings.ToLower or the unicode.ToLower function be more clear here?


clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "white_check_mark",
Text: "Sandbox deleted",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Text: "Sandbox deleted",
Text: "Sandbox Deleted",

🧮 suggestion: Title casings for section headers!

}

return nil
}
Copy link
Member

Choose a reason for hiding this comment

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

🫧 thought: This is solid! I'm optimistic that a conclusion of the charm experiment might let us hide selections and warnings earlier for outputs that are focused on the result, but nothing to think about in this PR!

Comment on lines +74 to +79
if err := cmd.MarkFlagRequired("name"); err != nil {
panic(err)
}
if err := cmd.MarkFlagRequired("password"); err != nil {
panic(err)
}
Copy link
Member

Choose a reason for hiding this comment

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

🏁 issue(non-blocking): We might want prompt alternatives for required flags but no blocker for this PR!

}

cmd.Flags().StringVar(&createCmdFlags.name, "name", "", "Organization name for the new sandbox")
cmd.Flags().StringVar(&createCmdFlags.domain, "domain", "", "Team domain (e.g., pizzaknifefight). If not provided, derived from org name")
Copy link
Member

Choose a reason for hiding this comment

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

🍕 praise: Toward common workplace shenanigan IIRC!

Comment on lines +250 to +251
tests := []struct {
name string
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
tests := []struct {
name string
tests := map[string]struct {

🧪 quibble: Let's use table tests here! I was confused in thinking "24h" was both the expected case and actual result at a glance...

Copy link
Member

Choose a reason for hiding this comment

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

👾 ramble: And perhaps a bounds of expected archive date might be most confident in testing? Like, some "24h" is greater than now but less than "48h" from now?

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, we have a standard table test format now and all of our other tests have been updated to use it. Let's use it here too.

@zimeg zimeg added experiment Experimental feature accessed behind the --experiment flag or toggle enhancement M-T: A feature request for new functionality labels Mar 16, 2026
@zimeg zimeg added this to the Next Release milestone Mar 16, 2026
@zimeg zimeg changed the title feat: create and delete developer sandboxes feat(experiment): create and delete developer sandboxes Mar 16, 2026
Copy link
Member

@mwbrooks mwbrooks left a comment

Choose a reason for hiding this comment

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

✅ Looking good @vegeris! Thank you for adding the sandbox create and sandbox delete commands.

🧪 Local testing works well!

✏️ I've left some suggestions that I'd appreciate you handle before merging this PR. The 🔡 alphabetical ones feel like nits but go a long way for future maintainers who need to read and find content.

Example: style.ExampleCommandsf([]style.ExampleCommand{
{Command: "sandbox create --name test-box --password mypass", Meaning: "Create a sandbox named test-box"},
{Command: "sandbox create --name test-box --password mypass --domain test-box --archive-ttl 1d", Meaning: "Create a temporary sandbox that will be archived in 1 day"},
{Command: "sandbox create --name test-box --password mypass --domain test-box --archive-date 2025-12-31", Meaning: "Create a sandbox that will be archived on a specific date"},
Copy link
Member

Choose a reason for hiding this comment

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

two-cents: I actually like both flags --archive-date <YYYY-MM-DD> and --archive-ttl <str>. In CI/CD environments, I think the --archive-ttl will be easier to use while --archive-date is more preferred for a human.

If the Slack API supports both, I think we should support both. We'll just need to make sure that developers can't provide both together and document the various expressions supported by TTL.

cmd.Flags().StringVar(&createCmdFlags.locale, "locale", "", "Locale (eg. en-us, languageCode-countryCode)")
cmd.Flags().StringVar(&createCmdFlags.template, "template", "", "Template ID for pre-defined data to preload")
cmd.Flags().StringVar(&createCmdFlags.eventCode, "event-code", "", "Event code for the sandbox")
cmd.Flags().StringVar(&createCmdFlags.archiveTTL, "archive-ttl", "", "Time-to-live duration; sandbox will be archived at end of day after this period (e.g., 2h, 1d, 7d)")
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: In the Long Description can be list all of the available formats for TTL? 1h, 1d, 1w?, 1m? 1y?, etc

cmd.Flags().StringVar(&createCmdFlags.template, "template", "", "Template ID for pre-defined data to preload")
cmd.Flags().StringVar(&createCmdFlags.eventCode, "event-code", "", "Event code for the sandbox")
cmd.Flags().StringVar(&createCmdFlags.archiveTTL, "archive-ttl", "", "Time-to-live duration; sandbox will be archived at end of day after this period (e.g., 2h, 1d, 7d)")
cmd.Flags().StringVar(&createCmdFlags.archiveDate, "archive-date", "", "Explicit archive date in yyyy-mm-dd format. Cannot be used with --archive")
Copy link
Member

Choose a reason for hiding this comment

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

question: Should this be Cannot be used with --archive-ttl?

Suggested change
cmd.Flags().StringVar(&createCmdFlags.archiveDate, "archive-date", "", "Explicit archive date in yyyy-mm-dd format. Cannot be used with --archive")
cmd.Flags().StringVar(&createCmdFlags.archiveDate, "archive-date", "", "Explicit archive date in yyyy-mm-dd format. Cannot be used with --archive-ttl")

cmd.Flags().StringVar(&createCmdFlags.owningOrgID, "owning-org-id", "", "Enterprise team ID that manages your developer account, if applicable")

if err := cmd.MarkFlagRequired("name"); err != nil {
panic(err)
Copy link
Member

Choose a reason for hiding this comment

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

question: Have we checked what happens when we throw a panic(err) here? I don't want developers to see some Golang stack trace.

I don't think we've used the .MarkFlagRequired anywhere before.

},
}))
clients.IO.PrintInfo(ctx, false, "Manage this sandbox from the CLI or visit\n%s", style.Secondary("https://api.slack.com/developer-program/sandboxes"))
}
Copy link
Member

Choose a reason for hiding this comment

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

praise: Excellent formatting choice!

Comment on lines +223 to +232
func (m *APIMock) CreateSandbox(ctx context.Context, token, name, domain, password, locale, owningOrgID, template, eventCode string, archiveDate int64) (string, string, error) {
args := m.Called(ctx, token, name, domain, password, locale, owningOrgID, template, eventCode, archiveDate)
return args.String(0), args.String(1), args.Error(2)
}

func (m *APIMock) DeleteSandbox(ctx context.Context, token, sandboxID string) error {
args := m.Called(ctx, token, sandboxID)
return args.Error(0)
}

Copy link
Member

Choose a reason for hiding this comment

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

suggestion: We'd really appreciate it if you can alphabetize these to be declared above the ListSandboxes function. 🙏🏻

Comment on lines +29 to +31
sandboxListMethod = "developer.sandbox.list"
sandboxCreateMethod = "enterprise.signup.createDevOrg"
sandboxDeleteMethod = "developer.sandbox.delete"
Copy link
Member

Choose a reason for hiding this comment

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

nit: Alphabetical please

Suggested change
sandboxListMethod = "developer.sandbox.list"
sandboxCreateMethod = "enterprise.signup.createDevOrg"
sandboxDeleteMethod = "developer.sandbox.delete"
sandboxCreateMethod = "enterprise.signup.createDevOrg"
sandboxDeleteMethod = "developer.sandbox.delete"
sandboxListMethod = "developer.sandbox.list"

Comment on lines 36 to +38
ListSandboxes(ctx context.Context, token string, filter string) ([]types.Sandbox, error)
CreateSandbox(ctx context.Context, token, name, domain, password, locale, owningOrgID, template, eventCode string, archiveDate int64) (teamID, sandboxURL string, err error)
DeleteSandbox(ctx context.Context, token, sandboxID string) error
Copy link
Member

Choose a reason for hiding this comment

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

nit: Alphabetical please

Suggested change
ListSandboxes(ctx context.Context, token string, filter string) ([]types.Sandbox, error)
CreateSandbox(ctx context.Context, token, name, domain, password, locale, owningOrgID, template, eventCode string, archiveDate int64) (teamID, sandboxURL string, err error)
DeleteSandbox(ctx context.Context, token, sandboxID string) error
CreateSandbox(ctx context.Context, token, name, domain, password, locale, owningOrgID, template, eventCode string, archiveDate int64) (teamID, sandboxURL string, err error)
DeleteSandbox(ctx context.Context, token, sandboxID string) error
ListSandboxes(ctx context.Context, token string, filter string) ([]types.Sandbox, error)

Comment on lines +80 to +85
type createSandboxResponse struct {
extendedBaseResponse
TeamID string `json:"team_id"`
UserID string `json:"user_id"`
URL string `json:"url"`
}
Copy link
Member

Choose a reason for hiding this comment

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

nit: Please declare this at the top of the file.

Comment on lines +87 to +88
// CreateSandbox creates a new developer sandbox
func (c *Client) CreateSandbox(ctx context.Context, token, name, domain, password, locale, owningOrgID, template, eventCode string, archiveDate int64) (teamID, sandboxURL string, err error) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: Please put the CreateSandbox and DeleteSandbox above the ListSandbox.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement M-T: A feature request for new functionality experiment Experimental feature accessed behind the --experiment flag or toggle semver:patch Use on pull requests to describe the release version increment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants