Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!(cli-test): Use child_process spawn arguments properly, fixing JSON encoding on the command line on Windows #2090

Merged
merged 6 commits into from
Nov 4, 2024

Conversation

filmaj
Copy link
Contributor

@filmaj filmaj commented Nov 1, 2024

Summary

(FYI a majority of changes in this PR are relatively cosmetic - will post comments for areas of particular interest for the kind reviewer)

There is a (technically) breaking change in this PR: the string arguments passed to SlackCLIProcess should be arranged in an array now, and these are passed as the second argument into child_process.spawn. This, in combination with the following changes, also fixes the datastore commands on Windows:

  • On Windows, shelling out to the CLI process is now wrapped in a cmd.exe /c /s invocation, which helps to manage how node will strip quotes out of arguments on Windows when passed to child_process APIs.
  • Additional post-processing of JSON string arguments is done (currently only in the datastore commands) to ensure double quotes within JSON arguments are escaped.

TODO:

  • check that Windows CI tests pass

@filmaj filmaj added semver:major enhancement M-T: A feature request for new functionality pkg:cli-test applies to `@slack/cli-test` labels Nov 1, 2024
@filmaj filmaj requested review from cchensh, vegeris and a team November 1, 2024 19:47
@filmaj filmaj self-assigned this Nov 1, 2024
Copy link

codecov bot commented Nov 1, 2024

Codecov Report

Attention: Patch coverage is 82.96296% with 23 lines in your changes missing coverage. Please review.

Project coverage is 91.65%. Comparing base (1be7a9e) to head (4ec7f60).
Report is 11 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2090      +/-   ##
==========================================
- Coverage   91.83%   91.65%   -0.18%     
==========================================
  Files          38       38              
  Lines       10264    10311      +47     
  Branches      646      647       +1     
==========================================
+ Hits         9426     9451      +25     
- Misses        826      848      +22     
  Partials       12       12              
Flag Coverage Δ
cli-hooks 95.23% <ø> (ø)
cli-test 94.47% <82.96%> (-1.23%) ⬇️
oauth 77.39% <ø> (ø)
socket-mode 58.22% <ø> (ø)
web-api 96.88% <ø> (-0.01%) ⬇️
webhook 96.65% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

@@ -49,7 +49,7 @@ export class SlackCLIProcess {
/**
* @description The CLI command to invoke
*/
public command: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

One part of the breaking change: commands are now composed of an array of strings, representing the space-delimited shell invocation.

Copy link
Member

Choose a reason for hiding this comment

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

I think this is a learning for all of our back-pockets. When dealing with CLI arguments in code, it's best to represent them as an array rather than a single string. Command-line frameworks like Cobra also always parse the raw command string into an array as well and it's likely because it's a more reliable way to interact with CLI arguments in code.

@@ -14,6 +14,10 @@ export interface DatastoreCommandArguments {
queryExpressionValues: object;
}

function escapeJSON(obj: Record<string, unknown>): string {
return `"${JSON.stringify(obj).replace(/"/g, '\\"')}"`;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This in combination with using using child_process spawn et al's args array of arguments helped with dealing with JSON encoding of command line arguments in a cross-platform way.

Copy link
Member

Choose a reason for hiding this comment

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

It may be worthwhile leaving a code comment to explain that this is to help support cross-platform JSON encoding

// In windows, we actually spawn a command prompt and tell it to invoke the CLI command.
// The combination of windows and node's child_process spawning is complicated: on windows, child_process strips quotes from arguments. This makes passing JSON difficult.
// As a workaround, by telling Windows Command Prompt (cmd.exe) to execute a command to completion (/c) and leave spaces intact (/c), combined with feeding arguments as an argument array into child_process, we can get around this mess.
const windowsArgs = ['/s', '/c'].concat([command]).concat(args);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the root of the Windows compatibility hack

Copy link
Contributor

@vegeris vegeris Nov 4, 2024

Choose a reason for hiding this comment

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

What are /s and /c? (What do they mean / do?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's described in the comment - let me know if you have a suggestion on wording or formatting this differently to make it possibly clearer

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see 🤡 execute a command to completion (/c) and leave spaces intact (/s)

Copy link
Contributor

Choose a reason for hiding this comment

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

I found it really helpful to see it spelled out that this means Windows will execute cmd.exe /c /s "slack cli command here" and would suggest that get added to the comment as well; for a while I thought it was slack \c \s datastore etc., which didn't work when I tried it on my Windows laptop 😅

}, /this is bat country/);
});
if (process.platform === 'win32') {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This fork in the test code is to test stuff on Windows vs. non-Windows properly, since we now include Windows-specific code.

This also explains why the code coverage dropped: since the code coverage is generated on a GitHub ubuntu runner, it won't exercise the Windows-specific code path, so the coverage reporter will report a lower coverage score. But, since our CI now runs tests on Windows, this code path is exercised (you can see that in the GitHub Action output for the windows check here)

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.

Pfff... amazing work @filmaj! That's some incredible detective work 🕵🏻 to uncover the right combination of flags to keep JSON intact on Windows Command Prompt.

I've left a few minor comments and suggestions from the code review. I'll let the others throw a proper ✅ on it but it looks good on my side.

Comment on lines +52 to +57
await datastore.datastoreQuery({
appPath: '/some/path',
datastoreName: 'datastore',
queryExpression: 'id = :id',
queryExpressionValues: expressObj,
});
Copy link
Member

Choose a reason for hiding this comment

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

:bliss: Much more readable

@@ -14,6 +14,10 @@ export interface DatastoreCommandArguments {
queryExpressionValues: object;
}

function escapeJSON(obj: Record<string, unknown>): string {
return `"${JSON.stringify(obj).replace(/"/g, '\\"')}"`;
Copy link
Member

Choose a reason for hiding this comment

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

It may be worthwhile leaving a code comment to explain that this is to help support cross-platform JSON encoding

@@ -49,7 +49,7 @@ export class SlackCLIProcess {
/**
* @description The CLI command to invoke
*/
public command: string;
Copy link
Member

Choose a reason for hiding this comment

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

I think this is a learning for all of our back-pockets. When dealing with CLI arguments in code, it's best to represent them as an array rather than a single string. Command-line frameworks like Cobra also always parse the raw command string into an array as well and it's likely because it's a more reliable way to interact with CLI arguments in code.

packages/cli-test/src/cli/shell.ts Outdated Show resolved Hide resolved
@vegeris
Copy link
Contributor

vegeris commented Nov 4, 2024

Just wondering, how were the Windows e2e tests verified? (Are we able to run them in CircleCI with a given version of the cli-test package?)

Edit: Ah, I see: https://github.com/slackapi/platform-devxp-test/pull/151

@filmaj
Copy link
Contributor Author

filmaj commented Nov 4, 2024

@vegeris I have a PR using this branch of cli-test in the relevant repo up here: https://github.com/slackapi/platform-devxp-test/pull/151

The two most recent CircleCI runs for this branch show both the non-windows (e2e-test) and windows (windows-e2e-test) results: https://app.circleci.com/pipelines/github/slackapi/platform-devxp-test?branch=cli-test-pshell

The windows-e2e-test job I triggered manually via the CircleCI UI from the same link: https://app.circleci.com/pipelines/github/slackapi/platform-devxp-test?branch=cli-test-pshell

Copy link
Contributor

@vegeris vegeris left a comment

Choose a reason for hiding this comment

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

This is great!

I take it we're fine to merge this despite the general restrictions this week as an update to the testing package that won't impact customer-facing behaviour?

@filmaj filmaj merged commit dcd0183 into main Nov 4, 2024
55 of 57 checks passed
@filmaj filmaj deleted the windows-spawn-args branch November 4, 2024 17:08
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 pkg:cli-test applies to `@slack/cli-test` semver:major
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants