Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add 'publish' flags
  • Loading branch information
stuartmorgan-g committed Feb 28, 2024
commit d2200b1b02692eca59cfe13b24c8c6ffe2fced67
53 changes: 46 additions & 7 deletions script/tool/lib/src/publish_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ class PublishCommand extends PackageLoopingCommand {
}) : _pubVersionFinder =
PubVersionFinder(httpClient: httpClient ?? http.Client()),
_stdin = stdinput ?? io.stdin {
argParser.addFlag(_alreadyTaggedFlag,
help:
'Instead of tagging, validates that the current checkout is aleardy tagged with the expected version.\n'
Copy link
Contributor

Choose a reason for hiding this comment

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

typo: already

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Little known fact, "aleardy" is actually the adverbal form of "all ears"; this is validating that it was tagged in a way that makes it all ears.

(Fixed.)

'This is primarily intended for use in CI publish steps triggered by tagging.',
negatable: false);
argParser.addMultiOption(_pubFlagsOption,
help:
'A list of options that will be forwarded on to pub. Separate multiple flags with commas.');
Expand All @@ -83,13 +88,20 @@ class PublishCommand extends PackageLoopingCommand {
argParser.addFlag(_skipConfirmationFlag,
help: 'Run the command without asking for Y/N inputs.\n'
'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n');
argParser.addFlag(_tagForAutopublishFlag,
help:
'Runs the dry-run publish, and tags if it succeeds, but does not actually publish.\n'
'This is intended for use with a separate publish step that is based on tag push events.',
negatable: false);
}

static const String _alreadyTaggedFlag = 'already-tagged';
static const String _pubFlagsOption = 'pub-publish-flags';
static const String _remoteOption = 'remote';
static const String _allChangedFlag = 'all-changed';
static const String _dryRunFlag = 'dry-run';
static const String _skipConfirmationFlag = 'skip-confirmation';
static const String _tagForAutopublishFlag = 'tag-for-autopublish';
Copy link
Contributor

Choose a reason for hiding this comment

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

is auto publish one word?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nope! Fixed.


static const String _pubCredentialName = 'PUB_CREDENTIALS';

Expand Down Expand Up @@ -193,15 +205,27 @@ class PublishCommand extends PackageLoopingCommand {
return PackageResult.fail(<String>['uncommitted changes']);
}

if (!await _publish(package)) {
return PackageResult.fail(<String>['publish failed']);
final bool tagOnly = getBoolArg(_tagForAutopublishFlag);
if (!tagOnly) {
if (!await _publish(package)) {
return PackageResult.fail(<String>['publish failed']);
}
}

if (!await _tagRelease(package)) {
return PackageResult.fail(<String>['tagging failed']);
final String tag = _getTag(package);
if (getBoolArg(_alreadyTaggedFlag)) {
if (!(await _getCurrentTags()).contains(tag)) {
printError('The current checkout is not already tagged "$tag"');
return PackageResult.fail(<String>['missing tag']);
}
} else {
if (!await _tagRelease(package, tag)) {
return PackageResult.fail(<String>['tagging failed']);
}
}

print('\nPublished ${package.directory.basename} successfully!');
final String action = tagOnly ? 'Tagged' : 'Published';
print('\n$action ${package.directory.basename} successfully!');
return PackageResult.success();
}

Expand Down Expand Up @@ -277,8 +301,7 @@ Safe to ignore if the package is deleted in this commit.
// Tag the release with <package-name>-v<version>, and push it to the remote.
//
// Return `true` if successful, `false` otherwise.
Future<bool> _tagRelease(RepositoryPackage package) async {
final String tag = _getTag(package);
Future<bool> _tagRelease(RepositoryPackage package, String tag) async {
print('Tagging release $tag...');
if (!getBoolArg(_dryRunFlag)) {
final io.ProcessResult result = await (await gitDir).runCommand(
Expand All @@ -301,6 +324,22 @@ Safe to ignore if the package is deleted in this commit.
return success;
}

Future<Iterable<String>> _getCurrentTags() async {
// git tag --points-at HEAD
final io.ProcessResult tagsResult = await (await gitDir).runCommand(
<String>['tag', '--points-at', 'HEAD'],
throwOnError: false,
);
if (tagsResult.exitCode != 0) {
return <String>[];
}

return (tagsResult.stdout as String)
.split('\n')
.map((String line) => line.trim())
.where((String line) => line.isNotEmpty);
}

Future<bool> _checkGitStatus(RepositoryPackage package) async {
final io.ProcessResult statusResult = await (await gitDir).runCommand(
<String>[
Expand Down
110 changes: 110 additions & 0 deletions script/tool/test/publish_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,33 @@ void main() {
),
);
});

test('skips publish with --tag-for-autopublish', () async {
const String packageName = 'a_package';
createFakePackage(packageName, packagesDir);

final List<String> output =
await runCapturingPrint(commandRunner, <String>[
'publish',
'--packages=$packageName',
'--tag-for-autopublish',
]);

// There should be no variant of any command containing "publish".
expect(
processRunner.recordedCalls
.map((ProcessCall call) => call.toString()),
isNot(contains(contains('publish'))));
// The output should indicate that it was tagged, not published.
expect(
output,
containsAllInOrder(
<Matcher>[
contains('Tagged a_package successfully!'),
],
),
);
});
});

group('Tags release', () {
Expand Down Expand Up @@ -390,6 +417,18 @@ void main() {
isNot(contains(
const ProcessCall('git-tag', <String>['foo-v0.0.1'], null))));
});

test('when passed --tag-for-autopublish', () async {
createFakePlugin('foo', packagesDir, examples: <String>[]);
await runCapturingPrint(commandRunner, <String>[
'publish',
'--packages=foo',
'--tag-for-autopublish',
]);

expect(processRunner.recordedCalls,
contains(const ProcessCall('git-tag', <String>['foo-v0.0.1'], null)));
});
});

group('Pushes tags', () {
Expand Down Expand Up @@ -439,6 +478,21 @@ void main() {
]));
});

test('when passed --tag-for-autopublish', () async {
createFakePlugin('foo', packagesDir, examples: <String>[]);
await runCapturingPrint(commandRunner, <String>[
'publish',
'--packages=foo',
'--skip-confirmation',
'--tag-for-autopublish',
]);

expect(
processRunner.recordedCalls,
contains(const ProcessCall(
'git-push', <String>['upstream', 'foo-v0.0.1'], null)));
});

test('to upstream by default, dry run', () async {
final RepositoryPackage plugin =
createFakePlugin('foo', packagesDir, examples: <String>[]);
Expand Down Expand Up @@ -488,6 +542,62 @@ void main() {
});
});

group('--already-tagged', () {
test('passes when HEAD has the expected tag', () async {
createFakePlugin('foo', packagesDir, examples: <String>[]);

processRunner.mockProcessesForExecutable['git-tag'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess()), // Skip the initializeRun call.
FakeProcessInfo(MockProcess(stdout: 'foo-v0.0.1\n'),
<String>['--points-at', 'HEAD'])
];

await runCapturingPrint(commandRunner,
<String>['publish', '--packages=foo', '--already-tagged']);
});

test('fails if HEAD does not have the expected tag', () async {
createFakePlugin('foo', packagesDir, examples: <String>[]);

Error? commandError;
final List<String> output = await runCapturingPrint(commandRunner,
<String>['publish', '--packages=foo', '--already-tagged'],
errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The current checkout is not already tagged "foo-v0.0.1"'),
contains('missing tag'),
]));
});

test('does not create or push tags', () async {
createFakePlugin('foo', packagesDir, examples: <String>[]);

processRunner.mockProcessesForExecutable['git-tag'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess()), // Skip the initializeRun call.
FakeProcessInfo(MockProcess(stdout: 'foo-v0.0.1\n'),
<String>['--points-at', 'HEAD'])
];

await runCapturingPrint(commandRunner,
<String>['publish', '--packages=foo', '--already-tagged']);

expect(
processRunner.recordedCalls,
isNot(contains(
const ProcessCall('git-tag', <String>['foo-v0.0.1'], null))));
expect(
processRunner.recordedCalls
.map((ProcessCall call) => call.executable),
isNot(contains('git-push')));
});
});

group('Auto release (all-changed flag)', () {
test('can release newly created plugins', () async {
mockHttpResponses['plugin1'] = <String, dynamic>{
Expand Down