Skip to content

Commit

Permalink
fix: maintain working directory across script steps (#711)
Browse files Browse the repository at this point in the history
This PR addresses the issue raised in #470, where the working directory
is not maintained across multiple steps in a script.

For example, consider the following script:

```yaml
fail:
  steps:
    - cd packages/
    - pwd
    
```
When running this script using melos run, the output will show the
workspace root directory, not the `packages/` directory as expected.

This PR introduces a change to ensure that the working directory is
properly maintained across multiple steps in a script.

The key changes are:

- Modify the script execution logic to preserve the working directory
from one step to the next.
- Add a safeguard to prevent users from mixing && operators with
multi-step scripts, as this can lead to unexpected behavior.

By running the script defined in the issue with the fix implemented in
this PR, the output is now:

```shell
➜  melos git:(bugfix/melos_run_script_steps_working_directory) ✗ melos success
Building package executable...
Built melos:melos.
melos run success
  └> cd packages/
     └> RUNNING


melos run success
  └> cd packages/
     └> SUCCESS

melos run success
  └> pwd
     └> RUNNING

/Users/jessicatarra/Development/melos/packages

melos run success
  └> pwd
     └> SUCCESS
```

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ `feat` -- New feature (non-breaking change which adds
functionality)
- [x] 🛠️ `fix` -- Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ `!` -- Breaking change (fix or feature that would cause existing
functionality to change)
- [x] 🧹 `refactor` -- Code refactor
- [ ] ✅ `ci` -- Build configuration change
- [ ] 📝 `docs` -- Documentation
- [ ] 🗑️ `chore` -- Chore
  • Loading branch information
jessicatarra authored Sep 13, 2024
1 parent dc05a52 commit a3784c1
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 125 deletions.
47 changes: 12 additions & 35 deletions packages/melos/lib/src/commands/run.dart
Original file line number Diff line number Diff line change
Expand Up @@ -275,48 +275,25 @@ mixin _RunMixin on _Melos {
Script script,
Map<String, String> environment,
) async {
for (final step in steps) {
final scriptCommand = _buildScriptCommand(step, scripts);

final scriptSourceCode = targetStyle(
step.withoutTrailing('\n'),
);

await _executeAndLogCommand(
script,
scriptSourceCode,
scriptCommand,
environment,
);
}
}

Future<void> _executeAndLogCommand(
Script script,
String scriptSourceCode,
String scriptCommand,
Map<String, String> environment,
) async {
logger.command('melos run ${script.name}');
logger.child(scriptSourceCode).child(runningLabel).newLine();

final exitCode = await startCommand(
[scriptCommand],
final shell = PersistentShell(
logger: logger,
environment: environment,
workingDirectory: config.path,
environment: environment,
);

logger.newLine();
await shell.startShell();
logger.command('melos run ${script.name}');
final resultLogger = logger.child(scriptSourceCode);

if (exitCode != 0) {
resultLogger.child(failedLabel);
} else {
resultLogger.child(successLabel);
for (final step in steps) {
final scriptCommand = _buildScriptCommand(step, scripts);

final shouldContinue = await shell.sendCommand(scriptCommand);
if (!shouldContinue) {
break;
}
}
logger.newLine();

await shell.stopShell();
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/melos/lib/src/commands/runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import '../common/glob.dart';
import '../common/intellij_project.dart';
import '../common/io.dart';
import '../common/pending_package_update.dart';
import '../common/persistent_shell.dart';
import '../common/platform.dart';
import '../common/utils.dart' as utils;
import '../common/utils.dart';
Expand Down
108 changes: 108 additions & 0 deletions packages/melos/lib/src/common/persistent_shell.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import '../logging.dart';
import 'environment_variable_key.dart';
import 'platform.dart';
import 'utils.dart';

class PersistentShell {
PersistentShell({
required this.logger,
required this.environment,
this.workingDirectory,
});

final _isWindows = currentPlatform.isWindows;
final MelosLogger logger;
final Map<String, String> environment;
final String? workingDirectory;
late final Process _process;
Completer<void>? _commandCompleter;
final String _successEndMarker = '__SUCCESS_COMMAND_END__';
final String _failureEndMarker = '__FAILURE_COMMAND_END__';

Future<void> startShell() async {
final executable = _isWindows ? 'cmd.exe' : '/bin/sh';

_process = await Process.start(
executable,
[],
workingDirectory: workingDirectory,
environment: {
...environment,
EnvironmentVariableKey.melosTerminalWidth: terminalWidth.toString(),
},
);

_listenToProcessStream(_process.stdout);
_listenToProcessStream(_process.stderr, isError: true);
}

Future<bool> sendCommand(String command) async {
assert(_commandCompleter == null, 'A command is already in progress.');
_commandCompleter = Completer<void>();

final fullCommand = _buildFullCommand(command);
_process.stdin.writeln(fullCommand);

return _awaitCommandCompletion();
}

Future<void> stopShell() async {
await _process.stdin.close();
final exitCode = await _process.exitCode;
if (exitCode == 0) {
logger.log(successLabel);
return;
}
logger.log(failedLabel);
}

Future<bool> _awaitCommandCompletion() async {
try {
await _commandCompleter!.future;
return true;
} catch (e) {
return false;
} finally {
_commandCompleter = null;
}
}

void _listenToProcessStream(
Stream<List<int>> stream, {
bool isError = false,
}) {
stream.listen((event) {
final output = utf8.decode(event, allowMalformed: true);
logger.logAndCompleteBasedOnMarkers(
output,
_successEndMarker,
_failureEndMarker,
_commandCompleter,
isError: isError,
);
});
}

String _buildFullCommand(String command) {
final formattedScriptStep =
targetStyle(command.addStepPrefixEmoji().withoutTrailing('\n'));
final echoCommand = 'echo "$formattedScriptStep"';
final echoSuccess = 'echo $_successEndMarker';
final echoFailure = 'echo $_failureEndMarker';

if (_isWindows) {
return '''
$echoCommand && $command || VER>NUL && if %ERRORLEVEL% NEQ 0 ($echoFailure) else ($echoSuccess)
''';
}

return '''
$echoCommand && $command || true && if [ \$? -ne 0 ];
then $echoFailure; else $echoSuccess; fi
''';
}
}
4 changes: 4 additions & 0 deletions packages/melos/lib/src/common/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ final melosPackageUri = Uri.parse('package:melos/melos.dart');
final _camelCasedDelimiterRegExp = RegExp(r'[_\s-]+');

extension StringUtils on String {
String addStepPrefixEmoji() {
return '➡️ step: $this';
}

String indent(String indent) {
final split = this.split('\n');

Expand Down
44 changes: 44 additions & 0 deletions packages/melos/lib/src/logging.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:ansi_styles/ansi_styles.dart';
import 'package:cli_util/cli_logging.dart';

Expand Down Expand Up @@ -67,6 +69,48 @@ class MelosLogger with _DelegateLogger {
write(message);
}

void logAndCompleteBasedOnMarkers(
String message,
String successMarker,
String failureMarker,
Completer<void>? completer, {
bool isError = false,
}) {
final modifiedMessage = _processMessageBasedOnMarkers(
message,
successMarker,
failureMarker,
completer,
);
_logMessage(modifiedMessage, isError);
}

String _processMessageBasedOnMarkers(
String message,
String successMarker,
String failureMarker,
Completer<void>? completer,
) {
if (message.contains(successMarker)) {
completer?.complete();
return message.replaceAll(successMarker, '');
}

if (message.contains(failureMarker)) {
completer?.complete();
return message.replaceAll(failureMarker, '');
}

return message;
}

void _logMessage(String message, bool isError) {
if (isError) {
error(message);
}
write(message);
}

void command(String command, {bool withDollarSign = false}) {
if (withDollarSign) {
stdout('${commandColor(r'$')} ${commandStyle(command)}');
Expand Down
Loading

0 comments on commit a3784c1

Please sign in to comment.