feat(experiment): create and delete developer sandboxes#389
feat(experiment): create and delete developer sandboxes#389
Conversation
…geris-sandbox-integration
6a724f1 to
37c62fb
Compare
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
98ecc15 to
f200970
Compare
f200970 to
cea2bcd
Compare
…geris-sandbox-integration-create-delete
| }, | ||
| })) | ||
| clients.IO.PrintInfo(ctx, false, "Manage this sandbox from the CLI or visit\n%s", style.Secondary("https://api.slack.com/developer-program/sandboxes")) | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
praise: Excellent formatting choice!
| } | ||
|
|
||
| return nil | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
🫧 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!
There was a problem hiding this comment.
praise: Excellent formatting choice!
b23fbe4 to
a35fbc3
Compare
| 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"}, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
zimeg
left a comment
There was a problem hiding this comment.
@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 🚢 💨
There was a problem hiding this comment.
🧪 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")) |
There was a problem hiding this comment.
💌 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?
| 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 |
| // 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] | ||
| } |
There was a problem hiding this comment.
🐭 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)) |
There was a problem hiding this comment.
🔍 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", |
There was a problem hiding this comment.
| Text: "Sandbox deleted", | |
| Text: "Sandbox Deleted", |
🧮 suggestion: Title casings for section headers!
| } | ||
|
|
||
| return nil | ||
| } |
There was a problem hiding this comment.
🫧 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!
| if err := cmd.MarkFlagRequired("name"); err != nil { | ||
| panic(err) | ||
| } | ||
| if err := cmd.MarkFlagRequired("password"); err != nil { | ||
| panic(err) | ||
| } |
There was a problem hiding this comment.
🏁 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") |
There was a problem hiding this comment.
🍕 praise: Toward common workplace shenanigan IIRC!
| tests := []struct { | ||
| name string |
There was a problem hiding this comment.
| 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...
There was a problem hiding this comment.
👾 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?
There was a problem hiding this comment.
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.
mwbrooks
left a comment
There was a problem hiding this comment.
✅ 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"}, |
There was a problem hiding this comment.
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)") |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
question: Should this be Cannot be used with --archive-ttl?
| 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) |
There was a problem hiding this comment.
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")) | ||
| } |
There was a problem hiding this comment.
praise: Excellent formatting choice!
| 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) | ||
| } | ||
|
|
There was a problem hiding this comment.
suggestion: We'd really appreciate it if you can alphabetize these to be declared above the ListSandboxes function. 🙏🏻
| sandboxListMethod = "developer.sandbox.list" | ||
| sandboxCreateMethod = "enterprise.signup.createDevOrg" | ||
| sandboxDeleteMethod = "developer.sandbox.delete" |
There was a problem hiding this comment.
nit: Alphabetical please
| sandboxListMethod = "developer.sandbox.list" | |
| sandboxCreateMethod = "enterprise.signup.createDevOrg" | |
| sandboxDeleteMethod = "developer.sandbox.delete" | |
| sandboxCreateMethod = "enterprise.signup.createDevOrg" | |
| sandboxDeleteMethod = "developer.sandbox.delete" | |
| sandboxListMethod = "developer.sandbox.list" |
| 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 |
There was a problem hiding this comment.
nit: Alphabetical please
| 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) |
| type createSandboxResponse struct { | ||
| extendedBaseResponse | ||
| TeamID string `json:"team_id"` | ||
| UserID string `json:"user_id"` | ||
| URL string `json:"url"` | ||
| } |
There was a problem hiding this comment.
nit: Please declare this at the top of the file.
| // 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) { |
There was a problem hiding this comment.
nit: Please put the CreateSandbox and DeleteSandbox above the ListSandbox.
Changelog
Summary
This PR adds
sandbox createandsandbox deletecommands, which allow one to create or delete a sandbox from the CLI.Follow up to #379
Testing
Try out the commands:
Requirements