Skip to content

Publish packages automatically after merge #394

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

Merged
merged 6 commits into from
May 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 0 additions & 4 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,3 @@
# Tools
'tools':
- tools/**/*

# Publishing
'needs-publishing':
- packages/**/pubspec.yaml
45 changes: 45 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Release

on:
pull_request:
push:
branches:
- master

jobs:
release:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'flutter-tizen' }}
steps:
- uses: subosito/flutter-action@v1
with:
channel: stable
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Dry run on pull request
if: ${{ github.event_name == 'pull_request' }}
run: |
./tools/tools_runner.sh publish-plugin \
--all-changed \
--pub-publish-flags=--dry-run \
--base-sha=HEAD~
- name: Wait on all tests
if: ${{ github.event_name == 'push' }}
uses: lewagon/[email protected]
with:
ref: ${{ github.sha }}
running-workflow-name: 'release'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 180 # seconds
allowed-conclusions: success
verbose: false
- name: Publish plugins
if: ${{ github.event_name == 'push' }}
run: |
./tools/tools_runner.sh publish-plugin \
--all-changed \
--base-sha=HEAD~ \
--skip-confirmation
env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"}

4 changes: 3 additions & 1 deletion tools/lib/src/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:flutter_plugin_tools/src/list_command.dart';

import 'build_examples_command.dart';
import 'integration_test_command.dart';
import 'publish_plugin_command.dart';

void main(List<String> args) {
const FileSystem fileSystem = LocalFileSystem();
Expand All @@ -36,7 +37,8 @@ void main(List<String> args) {
..addCommand(BuildExamplesCommand(packagesDir))
..addCommand(FormatCommand(packagesDir))
..addCommand(IntegrationTestCommand(packagesDir))
..addCommand(ListCommand(packagesDir));
..addCommand(ListCommand(packagesDir))
..addCommand(PublishPluginCommand(packagesDir));

commandRunner.run(args).catchError((Object e) {
final ToolExit toolExit = e as ToolExit;
Expand Down
4 changes: 4 additions & 0 deletions tools/lib/src/process_runner_apis.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:convert';
import 'dart:io' as io;

Expand Down
302 changes: 302 additions & 0 deletions tools/lib/src/publish_plugin_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
// Copyright 2022 Samsung Electronics Co., Ltd. All rights reserved.
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;

import 'package:file/file.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/common/file_utils.dart';
import 'package:flutter_plugin_tools/src/common/git_version_finder.dart';
import 'package:flutter_plugin_tools/src/common/package_looping_command.dart';
import 'package:flutter_plugin_tools/src/common/plugin_command.dart';
import 'package:flutter_plugin_tools/src/common/process_runner.dart';
import 'package:flutter_plugin_tools/src/common/pub_version_finder.dart';
import 'package:flutter_plugin_tools/src/common/repository_package.dart';
import 'package:git/git.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as p;
import 'package:platform/platform.dart';
import 'package:pub_semver/pub_semver.dart';

/// Wraps pub publish with a few niceties used by the flutter-tizen team.
// The code is mostly copied from `PublishPluginCommand` in
// `flutter_plugin_tools`, parts regarding git tagging are removed as we don't
// tag our releases.
class PublishPluginCommand extends PackageLoopingCommand {
/// Creates an instance of the publish command.
PublishPluginCommand(
Directory packagesDir, {
ProcessRunner processRunner = const ProcessRunner(),
Platform platform = const LocalPlatform(),
io.Stdin? stdinput,
GitDir? gitDir,
http.Client? httpClient,
}) : _pubVersionFinder = PubVersionFinder(
httpClient: httpClient ?? http.Client(),
),
_stdin = stdinput ?? io.stdin,
super(
packagesDir,
platform: platform,
processRunner: processRunner,
gitDir: gitDir,
) {
argParser.addMultiOption(
_pubFlagsOption,
help:
'A list of options that will be forwarded on to pub. Separate multiple flags with commas.',
);
argParser.addFlag(
_allChangedFlag,
help:
'Release all packages that contains pubspec changes at the current commit compares to the base-sha.\n'
'The --packages option is ignored if this is on.',
defaultsTo: false,
);
argParser.addFlag(
_dryRunFlag,
help:
'Skips the real `pub publish` command and assumes the command is successful.\n'
'This does not run `pub publish --dry-run`.\n'
'If you want to run the command with `pub publish --dry-run`, use `pub-publish-flags=--dry-run`',
defaultsTo: false,
negatable: true,
);
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',
defaultsTo: false,
negatable: true,
);
}

static const String _pubFlagsOption = 'pub-publish-flags';
static const String _allChangedFlag = 'all-changed';
static const String _dryRunFlag = 'dry-run';
static const String _skipConfirmationFlag = 'skip-confirmation';

static const String _pubCredentialName = 'PUB_CREDENTIALS';

@override
String get name => 'publish-plugin';

@override
String get description => 'Attempts to publish the given packages to pub.\n'
'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n'
'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.';

final io.Stdin _stdin;
StreamSubscription<String>? _stdinSubscription;
final PubVersionFinder _pubVersionFinder;

late List<String> _publishFlags;

@override
String get successSummaryMessage => 'published';

@override
String get failureListHeader =>
'The following packages had failures during publishing:';

@override
Future<void> initializeRun() async {
_publishFlags = <String>[
...getStringListArg(_pubFlagsOption),
if (getBoolArg(_skipConfirmationFlag)) '--force',
];

if (getBoolArg(_dryRunFlag)) {
print('=============== DRY RUN ===============');
}
}

@override
Stream<PackageEnumerationEntry> getPackagesToProcess() async* {
if (getBoolArg(_allChangedFlag)) {
final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
final String baseSha = await gitVersionFinder.getBaseSha();
print(
'Publishing all packages that have changed relative to "$baseSha"\n');
final List<String> changedPubspecs =
await gitVersionFinder.getChangedPubSpecs();

for (final String pubspecPath in changedPubspecs) {
// git outputs a relative, Posix-style path.
final File pubspecFile = childFileWithSubcomponents(
packagesDir.fileSystem.directory((await gitDir).path),
p.posix.split(pubspecPath));
yield PackageEnumerationEntry(RepositoryPackage(pubspecFile.parent),
excluded: false);
}
} else {
yield* getTargetPackages(filterExcluded: false);
}
}

@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
final PackageResult? checkResult = await _checkNeedsRelease(package);
if (checkResult != null) {
return checkResult;
}

if (!await _checkGitStatus(package)) {
return PackageResult.fail(<String>['uncommitted changes']);
}

if (!await _publish(package)) {
return PackageResult.fail(<String>['publish failed']);
}

print('\nPublished ${package.directory.basename} successfully!');
return PackageResult.success();
}

@override
Future<void> completeRun() async {
_pubVersionFinder.httpClient.close();
await _stdinSubscription?.cancel();
_stdinSubscription = null;
}

Future<PackageResult?> _checkNeedsRelease(RepositoryPackage package) async {
if (!package.pubspecFile.existsSync()) {
logWarning('''
The pubspec file for ${package.displayName} does not exist, so no publishing will happen.
Safe to ignore if the package is deleted in this commit.
''');
return PackageResult.skip('package deleted');
}

final Pubspec pubspec = package.parsePubspec();

if (pubspec.publishTo == 'none') {
return PackageResult.skip('publish_to: none');
}

if (pubspec.version == null) {
printError(
'No version found. A package that intentionally has no version should be marked "publish_to: none"');
return PackageResult.fail(<String>['no version']);
}

// Check if the package named `packageName` with `version` has already
// been published.
final Version version = pubspec.version!;
final PubVersionFinderResponse pubVersionFinderResponse =
await _pubVersionFinder.getPackageVersion(packageName: pubspec.name);
if (pubVersionFinderResponse.versions.contains(version)) {
print('${pubspec.name} $version has already been published.');
return PackageResult.skip('already published');
}
return null;
}

Future<bool> _checkGitStatus(RepositoryPackage package) async {
final io.ProcessResult statusResult = await (await gitDir).runCommand(
<String>[
'status',
'--porcelain',
'--ignored',
package.directory.absolute.path
],
throwOnError: false,
);
if (statusResult.exitCode != 0) {
return false;
}

final String statusOutput = statusResult.stdout as String;
if (statusOutput.isNotEmpty) {
printError(
"There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n"
'$statusOutput\n'
'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.');
}
return statusOutput.isEmpty;
}

Future<bool> _publish(RepositoryPackage package) async {
print('Publishing...');
print('Running `pub publish ${_publishFlags.join(' ')}` in '
'${package.directory.absolute.path}...\n');
if (getBoolArg(_dryRunFlag)) {
return true;
}

if (_publishFlags.contains('--force')) {
_ensureValidPubCredential();
}

final io.Process publish = await processRunner.start(
flutterCommand, <String>['pub', 'publish', ..._publishFlags],
workingDirectory: package.directory);
publish.stdout.transform(utf8.decoder).listen((String data) => print(data));
publish.stderr.transform(utf8.decoder).listen((String data) => print(data));
_stdinSubscription ??= _stdin
.transform(utf8.decoder)
.listen((String data) => publish.stdin.writeln(data));
final int result = await publish.exitCode;
if (result != 0) {
printError('Publishing ${package.directory.basename} failed.');
return false;
}

print('Package published!');
return true;
}

void _ensureValidPubCredential() {
final String credentialsPath = _credentialsPath;
final File credentialFile = packagesDir.fileSystem.file(credentialsPath);
if (credentialFile.existsSync() &&
credentialFile.readAsStringSync().isNotEmpty) {
return;
}
final String? credential = io.Platform.environment[_pubCredentialName];
if (credential == null) {
printError('''
No pub credential available. Please check if `$credentialsPath` is valid.
If running this command on CI, you can set the pub credential content in the $_pubCredentialName environment variable.
''');
throw ToolExit(1);
}
credentialFile.openSync(mode: FileMode.writeOnlyAppend)
..writeStringSync(credential)
..closeSync();
}
}

final String _credentialsPath = () {
String? cacheDir;
final String? pubCache = io.Platform.environment['PUB_CACHE'];
if (pubCache != null) {
cacheDir = pubCache;
} else if (io.Platform.isWindows) {
final String? appData = io.Platform.environment['APPDATA'];
if (appData == null) {
printError('"APPDATA" environment variable is not set.');
} else {
cacheDir = p.join(appData, 'Pub', 'Cache');
}
} else {
final String? home = io.Platform.environment['HOME'];
if (home == null) {
printError('"HOME" environment variable is not set.');
} else {
cacheDir = p.join(home, '.pub-cache');
}
}

if (cacheDir == null) {
printError('Unable to determine pub cache location');
throw ToolExit(1);
}

return p.join(cacheDir, 'credentials.json');
}();
Loading