feat: check requests to install an app - #646
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #646 +/- ##
==========================================
+ Coverage 72.38% 72.53% +0.15%
==========================================
Files 238 239 +1
Lines 20072 20228 +156
==========================================
+ Hits 14529 14673 +144
- Misses 4275 4279 +4
- Partials 1268 1276 +8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Add a hidden `slack app requests` command behind the app-approval-status experiment that reports the most recent install approval request for the selected app on each team in the token's scope. Co-authored-by: Cursor <cursoragent@cursor.com>
Exercise the interrupted app selection and missing app ID branches of the command, plus the unknown timestamp, status, and cancellation actor fallbacks of the output. Co-authored-by: Cursor <cursoragent@cursor.com>
fc808a6 to
1e4be76
Compare
The app select prompt only offers apps saved to a project, so apps created elsewhere could not be checked. An app ID provided with the --app flag now skips both the project requirement and the project app list, gathering a token from the authenticated accounts instead. Co-authored-by: Cursor <cursoragent@cursor.com>
zimeg
left a comment
There was a problem hiding this comment.
🏁 @AmyScript Thanks for sending this in! It's LGTM per usual and I leave a few comments that might be nice before merge but some are rambles... 🗣️
Overall I lean toward returning API responses or errors instead of expecting certain values and some flag options and error outputs. Nothing blocking! These are exciting experiment to bring to release 🤖⚡
| ErrFeatureNotEnabled: { | ||
| Code: ErrFeatureNotEnabled, | ||
| Message: "This feature is not enabled for the team", | ||
| Remediation: "Reach out to an admin for additional information", | ||
| }, | ||
|
|
There was a problem hiding this comment.
| ErrFeatureNotEnabled: { | |
| Code: ErrFeatureNotEnabled, | |
| Message: "This feature is not enabled for the team", | |
| Remediation: "Reach out to an admin for additional information", | |
| }, |
🪓 note: I don't think this is used in calling code and might be nice to keep out? Would API responses be alright to match this?
There was a problem hiding this comment.
It is used, just not through explicit calling code. slackerror.NewAPIError calls getErrorIfKnown, which looks up ErrorCodeMap by whatever code the API returned, so this entry is what turns feature_not_enabled into a message and remediation. Without it the user sees the bare code. I added an API test case asserting the rendered message so the wiring is visible in the suite.
There was a problem hiding this comment.
It would be good to update the name to ErrAPIFeatureNotEnabled so that it's self-documenting that this is an API error and not an internal Slack CLI error. Otherwise, future maintainers may remove it under the same assumption as @zimeg.
| ErrRestrictedAction: { | ||
| Code: ErrRestrictedAction, | ||
| Message: "The requested action is not allowed for a specified team", | ||
| Remediation: "Check that each team belongs to the organization of the authenticated account", | ||
| }, | ||
|
|
There was a problem hiding this comment.
| ErrRestrictedAction: { | |
| Code: ErrRestrictedAction, | |
| Message: "The requested action is not allowed for a specified team", | |
| Remediation: "Check that each team belongs to the organization of the authenticated account", | |
| }, |
🌲 ramble: I fear checking for errors perhaps unreferenced similar but I'm wondering if this is used in unit tests or when handling the API response? If it's alright to remove I might suggest but all so is rambles!
There was a problem hiding this comment.
Same mechanism as above. Test_Client_ListAppApprovalRequests has an "errors when a team is outside of the organization" case returning restricted_action. I strengthened it to assert the mapped message and remediation rather than just the code, so removing the entry would now fail the test rather than silently degrade the output.
There was a problem hiding this comment.
Same suggestion as above to rename it ErrAPIRestrictedAction so that it's self-documenting that it's an API error.
mwbrooks
left a comment
There was a problem hiding this comment.
@AmyScript Please don't merge this yet - I'd like a moment to review it. Just dropping the change request, since @zimeg already approved it which enables merging.
| func NewRequestsCommand(clients *shared.ClientFactory) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "requests [flags]", | ||
| Aliases: []string{"approval-requests", "approvals"}, |
There was a problem hiding this comment.
suggestion: Assuming we go with app approval, alias approvals and remove the others. It's hard to remove an alias once we add it, but it's easy to add one in the future.
| Aliases: []string{"approval-requests", "approvals"}, | |
| Aliases: []string{"approvals"}, |
zimeg
left a comment
There was a problem hiding this comment.
@AmyScript Super appreciate the fast changes! I'll toss another approval on but defer to @mwbrooks for ongoing review. Nothing is blocking experimental releases for me!
🧪 I do notice the E2E tests aren't running and I think that's an intentional setting to avoid passing secrets to forked branches... It catches us often and I'm unsure a workaround remains but I share this for possible updates:
🔗 https://github.com/slackapi/slack-cli/blob/main/.github/MAINTAINERS_GUIDE.md#fork
| }, | ||
| Expected: []string{"Cancelled by: workflow"}, | ||
| }, | ||
| "requests are sorted by the team ID": { |
There was a problem hiding this comment.
🧮 praise: Thanks for bringing consistent outputs to this command!
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestPromptTeamSlackAuth(t *testing.T) { |
There was a problem hiding this comment.
🦠 praise: Thanks for adding coverage to this prompt!
🔬 ramble: I'm hoping we can improve the mock longterm to avoid stubbing these selections within each command and this is so helpful in building confidence toward this I think.
Should I have just branched off main? I read about the fork so I forked 😆. Is there a way for me to run the e2e tests locally with a command? |
There was a problem hiding this comment.
🙌🏻 Thanks for the PR @AmyScript! Admin approval management has been a painfully missed feature in the CLI, so I'm happy to see you giving it some TLC.
🙇🏻 Some praise goes to you on code quality. You rocked our latest conventions, patterns, and test coverage goals. Thanks for that!
💡 Small tip for next time - please create a branch on this repo instead of forking. External contributors fork, but as a maintainer you can work on a branch allowing our CI/CD to safely run E2E tests. Before merging, we should push this to a branch on the origin to ensure the E2E tests pass alright.
🧠 My main ask before approving this PR is to consider app approval (alias: approvals) instead of app requests. I think it reads better from the user/developer perspective, aligns with our documentation, and aligns with our API. It also opens space for 3rd-tier of commands such as app approval cancel or app approval list (alias app approval - what you're landing here).
🧪 I hear you're aiming to land this into production end-of-week or early next week. I'd suggestion you delete the experiment entirely, because we won't be doing a release between now-and-then. All production features should not be behind an experiment.
🚦 When you remove the experiment, please switch this to a semver:minor.
📝 The rest of my suggestions are minor. Nice to haves but not blockers if you are short on time.
Once we settle on the name, happy to hit ✅ approve!
| // NewRequestsCommand returns a new Cobra command | ||
| func NewRequestsCommand(clients *shared.ClientFactory) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "requests [flags]", |
There was a problem hiding this comment.
suggestion: After some thought, I'd like to suggest renaming this to approval (singular)
The reason is that app approval matches the API (apps.approvals.requests.list) and leaves room for slack app approval [create|cancel|list] later.
We've had requests for a cancel feature, so this opens the namespace for it.
The bare approval also matches the bare app → app list, trigger → trigger list, auth → auth list, and manifest → manifest list convention. So, if we expand the command with create | cancel | list subcommands, we've made decisions that allow for a smooth forward rollout.
The approvals command also matches the terminology used in our documentation, where as requests is more from the admin-perspective:
- https://docs.slack.dev/admins/managing-app-approvals/
- https://slack.com/help/articles/222386767-Manage-app-approval-for-your-workspace
We should alias the plural approvals. The CLI usually has the canonical command as singular while aliasing the plural. Long ago, we mixed-and-matched singular/plural depending on the command and it was confusing for developers. So, we try to use singular when possible.
| }, | ||
| } | ||
|
|
||
| cmd.Flags().StringSliceVar(&requestsFlags.workspaceIDs, "workspace-ids", nil, "also check these workspaces of an organization,\nwith a maximum of 50 workspaces") |
There was a problem hiding this comment.
thought: Is the 50 workspace limit enforced by the API? A brief look and it appears the API returns invalid_arguments with no remediation. If so, we may want to validate the length here and include it in the error message since it's easy to overlook the help documentation.
| } | ||
|
|
||
| if len(allAuths) == 1 { | ||
| clients.Auth().SetSelectedAuth(ctx, allAuths[0], clients.Config, clients.Os) |
There was a problem hiding this comment.
note: Right place for this, but it also changes app link and sandbox, which now get the selected auth set on the single-account path. Worth confirming neither relies on it being unset.
|
|
||
| // FormatRequestsSuccess formats the install request of each team for an app | ||
| func FormatRequestsSuccess(appID string, requests []api.AppsApprovalsRequest) (secondaryText []string) { | ||
| sort.Slice(requests, func(i, j int) bool { |
There was a problem hiding this comment.
suggestion: sort.Slice reorders the caller's original slice. Harmless today, but this function mutating its input will be surprise later if used elsewhere and can introduce unexpected bugs. I'd recommend sorting a copy of requests (requestsSorted).
| cmd.AddCommand(NewDeleteCommand(clients)) | ||
| cmd.AddCommand(NewLinkCommand(clients)) | ||
| cmd.AddCommand(NewListCommand(clients)) | ||
| cmd.AddCommand(NewRequestsCommand(clients)) |
There was a problem hiding this comment.
praise: Appreciate the attention to detail on naming conventions and alphabetical order. 🙇🏻
| func NewRequestsCommand(clients *shared.ClientFactory) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "requests [flags]", | ||
| Aliases: []string{"approval-requests", "approvals"}, |
There was a problem hiding this comment.
suggestion: Assuming we go with app approval, alias approvals and remove the others. It's hard to remove an alias once we add it, but it's easy to add one in the future.
| Aliases: []string{"approval-requests", "approvals"}, | |
| Aliases: []string{"approvals"}, |
| cmd := &cobra.Command{ | ||
| Use: "requests [flags]", | ||
| Aliases: []string{"approval-requests", "approvals"}, | ||
| Short: "Check requests to install the app", |
There was a problem hiding this comment.
suggestion: Most developers aren't familiar with AAA, so I think the extra word can go along way and aligns with our documentation.
| Short: "Check requests to install the app", | |
| Short: "Check approval requests to install the app", |
| {Command: "app requests", Meaning: "Check requests to install an app"}, | ||
| {Command: "app requests --app A0123456789", Meaning: "Check requests for an app outside a project"}, | ||
| {Command: "app requests --workspace-ids T0123456789,T9876543210", Meaning: "Check requests on certain workspaces of an organization"}, |
There was a problem hiding this comment.
suggestion: Assuming we go with app approval:
| {Command: "app requests", Meaning: "Check requests to install an app"}, | |
| {Command: "app requests --app A0123456789", Meaning: "Check requests for an app outside a project"}, | |
| {Command: "app requests --workspace-ids T0123456789,T9876543210", Meaning: "Check requests on certain workspaces of an organization"}, | |
| {Command: "app approval", Meaning: "Check requests to install an app"}, | |
| {Command: "app approval --app A0123456789", Meaning: "Check requests for an app outside a project"}, | |
| {Command: "app approval --workspace-ids T0123456789,T9876543210", Meaning: "Check requests on certain workspaces of an organization"}, |
| // runRequestsCommand will execute the requests command | ||
| func runRequestsCommand(cmd *cobra.Command, clients *shared.ClientFactory) error { | ||
| ctx := cmd.Context() | ||
| span, ctx := opentracing.StartSpanFromContext(ctx, "cmd.app.requests") |
There was a problem hiding this comment.
suggestion: If we accept app approval then we should adjust all of our tracing.
| span, ctx := opentracing.StartSpanFromContext(ctx, "cmd.app.requests") | |
| span, ctx := opentracing.StartSpanFromContext(ctx, "cmd.app.approval") |
|
|
||
| clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ | ||
| Emoji: "lock", | ||
| Text: "App Requests", |
There was a problem hiding this comment.
suggestion: Adding more clarity for developers who aren't familiar with AAA.
| Text: "App Requests", | |
| Text: "App Approval Requests", |
Changelog
Added the
app-approval-statusexperiment with aslack app requestscommand that checks the status of your most recent request to have an app approved for install.Summary
Installing an app on a team with admin approval required creates an approval request, but there was no way to check what happened to it from the CLI. This adds a hidden
slack app requestscommand, gated behind the newapp-approval-statusexperiment, that reports the most recent request for the selected app on each team in the token's scope.Requests are searched on the team of the authenticated account. An account of a workspace belonging to an organization also searches that organization, while an account of an organization searches the organization alone. Other workspaces of an organization can be searched with
--team-ids, up to the 50 the API accepts.Apps saved to a project are chosen with the usual prompt. Since that prompt only offers apps recorded in the project, an app can also be named by ID with
--app, which skips both the project requirement and the project app list and gathers a token from the authenticated accounts instead.Output includes the request ID, status, and timestamps, plus the actor that cancelled a request and a hint when the account can install the app without approval:
Preview
No recording yet — the command is unreleased and hidden, and the output above is taken from the unit tests rather than a live run.
Testing
--appapp ID path, including a failed team selection and a team without a token.--experiment app-approval-status, that--team-idsrejects more than 50 teams, and that a project is still required unless an app ID is given.Notes
Hidden, matching howmanifest syncgates itself; the experiment check lives inPreRunE.feature_not_enabledandrestricted_actionerror codes returned by the endpoint.app_idis required by the endpoint, so requests are reported one app at a time rather than for every app the account owns.Requirements