Skip to content

Commit

Permalink
add a tweak to contribute package publishing automation (#34)
Browse files Browse the repository at this point in the history
* general refactor and contribute an auto-publish fix

* finish auto_publish tweak; add tests

* revert change to the gitignore file
  • Loading branch information
devoncarew authored Jan 18, 2023
1 parent 40afc5b commit 94155f5
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 45 deletions.
17 changes: 17 additions & 0 deletions pkgs/blast_repo/bin/blast_repo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ Future<void> main(List<String> args) async {
'keep-temp',
negatable: false,
)
..addMultiOption('tweaks',
help: 'Optionally list the specific tweaks to run (defaults to all '
'stable tweaks)',
allowed: allTweaks.map((t) => t.id),
valueHelp: 'tweak1,tweak2')
..addFlag(
'include-unstable',
help: 'To run tweaks that are not stable.',
Expand All @@ -36,6 +41,11 @@ Future<void> main(List<String> args) async {
void printUsage() {
print('Usage: $packageName <options> [org/repo]\n');
print(parser.usage);
print('\navailable tweaks:');
for (var tweak in allTweaks) {
var unstable = tweak.stable ? '' : ' (unstable)';
print(' ${tweak.id}: ${tweak.description}$unstable');
}
}

final ArgResults argResults;
Expand All @@ -59,11 +69,18 @@ Future<void> main(List<String> args) async {

final includeUnstable = argResults['include-unstable'] as bool;
final prReviewer = argResults['pr-reviewer'] as String?;
final explicitTweakIds = argResults['tweaks'] as List<String>;
final explicitTweaks = explicitTweakIds.isEmpty
? null
: explicitTweakIds
.map((id) => allTweaks.firstWhere((t) => t.id == id))
.toList();

try {
await runFix(
slug: slug,
deleteTemp: !keepTemp,
tweaks: explicitTweaks,
onlyStable: !includeUnstable,
prReviewer: prReviewer,
);
Expand Down
33 changes: 21 additions & 12 deletions pkgs/blast_repo/lib/src/exact_file_tweak.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import 'repo_tweak.dart';
abstract class ExactFileTweak extends RepoTweak {
ExactFileTweak({
required this.filePath,
required this.expectedContent,
required super.name,
required super.id,
required super.description,
this.alternateFilePaths = const {},
}) : assert(p.isRelative(filePath)) {
Expand Down Expand Up @@ -45,24 +44,34 @@ abstract class ExactFileTweak extends RepoTweak {

final String filePath;
final Set<String> alternateFilePaths;
final String expectedContent;

String expectedContent(String repoSlug);

@override
FutureOr<FixResult> fix(Directory checkout) {
FutureOr<FixResult> fix(Directory checkout, String repoSlug) {
final file = _targetFile(checkout);

final exists = file.existsSync();
if (exists) {
final existingContent = file.readAsStringSync();
assert(existingContent != expectedContent);
var fixResults = <String>[];

final newContent = expectedContent(repoSlug);
if (!file.existsSync()) {
file.writeAsStringSync(newContent);
fixResults.add('$filePath has been created.');
} else if (file.readAsStringSync() != newContent) {
file.writeAsStringSync(newContent);
fixResults.add('$filePath has been updated.');
}
file.writeAsStringSync(expectedContent);

return FixResult(
fixes: ['$filePath has been ${exists ? 'updated' : 'created'}.'],
);
fixResults.addAll(performAdditionalFixes(checkout, repoSlug));

return fixResults.isEmpty
? FixResult.noFixesMade
: FixResult(fixes: fixResults);
}

List<String> performAdditionalFixes(Directory checkout, String repoSlug) =>
[];

File _targetFile(Directory checkout) {
assert(checkout.existsSync());

Expand Down
8 changes: 4 additions & 4 deletions pkgs/blast_repo/lib/src/repo_tweak.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import 'dart:io';

abstract class RepoTweak {
const RepoTweak({
required this.name,
required this.id,
required this.description,
});

final String name;
final String id;
final String description;

bool get stable => true;
Expand All @@ -24,10 +24,10 @@ abstract class RepoTweak {
///
/// If the repo cannot be checked or if a required fix cannot be applied,
/// an error is thrown.
FutureOr<FixResult> fix(Directory checkout);
FutureOr<FixResult> fix(Directory checkout, String repoSlug);

@override
String toString() => name;
String toString() => id;
}

class CheckResult {
Expand Down
39 changes: 26 additions & 13 deletions pkgs/blast_repo/lib/src/top_level.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import 'dart:io';
import 'package:git/git.dart';

import 'repo_tweak.dart';
import 'tweaks/auto_publish_tweak.dart';
import 'tweaks/dependabot_tweak.dart';
import 'tweaks/github_action_tweak.dart';
import 'tweaks/no_reponse_tweak.dart';
import 'utils.dart';

final allTweaks = Set<RepoTweak>.unmodifiable([
AutoPublishTweak(),
DependabotTweak(),
GitHubActionTweak(),
NoResponseTweak(),
Expand All @@ -25,17 +27,23 @@ Future<void> runFix({
required bool deleteTemp,
required bool onlyStable,
required String? prReviewer,
Iterable<RepoTweak>? tweaks,
}) async {
await withSystemTemp(
deleteTemp: deleteTemp,
(tempDir, runKey) async {
await cloneGitHubRepoToPath(slug, tempDir.path);

final result = await fixAll(tempDir, onlyStable: onlyStable);
final result = await fixAll(
slug,
tempDir,
tweaks: tweaks,
onlyStable: onlyStable,
);

final fixes = result.entries
.where((element) => element.value.fixes.isNotEmpty)
.map((e) => e.key.name)
.map((e) => e.key.id)
.toList()
..sort();

Expand All @@ -46,7 +54,7 @@ Future<void> runFix({

printHeader('Fixes:');
for (var entry in result.entries) {
print(entry.key.name);
print(entry.key.id);
print(const JsonEncoder.withIndent(' ').convert(entry.value.fixes));
}

Expand Down Expand Up @@ -93,25 +101,30 @@ ${fixes.join('\n')}
}

Future<Map<RepoTweak, FixResult>> fixAll(
String repoSlug,
Directory checkout, {
Iterable<RepoTweak>? tweaks,
required bool onlyStable,
}) async =>
{
for (var tweak in tweaks.orAll(onlyStable: onlyStable))
tweak: await _safeRun(checkout, tweak.name, tweak.fix),
};
}) async {
tweaks ??= allTweaks.orAll(onlyStable: onlyStable);

return {
for (var tweak in tweaks)
tweak: await _safeRun(repoSlug, checkout, tweak.id, tweak.fix),
};
}

Future<T> _safeRun<T>(
String repoSlug,
Directory checkout,
String description,
FutureOr<T> Function(Directory) action,
String id,
FutureOr<T> Function(Directory, String repoSlug) action,
) async {
printHeader('Running "$description"');
printHeader('Running "$id"');
try {
return await action(checkout);
return await action(checkout, repoSlug);
} catch (_) {
printError(' Error running $description');
printError(' Error running $id');
rethrow;
}
}
Expand Down
94 changes: 94 additions & 0 deletions pkgs/blast_repo/lib/src/tweaks/auto_publish_tweak.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. 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:io';

import 'package:path/path.dart' as path;

import '../exact_file_tweak.dart';

final _instance = AutoPublishTweak._();

class AutoPublishTweak extends ExactFileTweak {
factory AutoPublishTweak() => _instance;

AutoPublishTweak._()
: super(
id: 'auto-publish',
description:
'configure a github action to enable package auto-publishing',
filePath: '.github/workflows/publish.yaml',
);

@override
bool get stable => false;

@override
String expectedContent(String repoSlug) {
final org = repoSlug.split('/').first;

// Substitute the org value for the pattern '{org}'.
return publishContents.replaceAll('{org}', org);
}

@override
List<String> performAdditionalFixes(Directory checkout, String repoSlug) {
var results = <String>[];

// Update the readme to include contribution + publishing info.
const tag = 'Contributions, PRs, and publishing';

var readmeFile = File(path.join(checkout.path, 'README.md'));

if (readmeFile.existsSync()) {
var contents = readmeFile.readAsStringSync();

if (!contents.contains(tag)) {
var newContents = '${contents.trimRight()}\n\n$readmeSection';
readmeFile.writeAsStringSync(newContents);
results.add('README.md updated with contribution and publishing info.');
}
}

return results;
}
}

const publishContents = r'''
# A CI configuration to auto-publish pub packages.
name: Publish
on:
pull_request:
branches: [ main ]
push:
tags: [ 'v[0-9]+.[0-9]+.[0-9]+*' ]
jobs:
publish:
if: github.repository_owner == '{org}'
uses: devoncarew/firehose/.github/workflows/publish.yaml@main
''';

const String readmeSection = '''
## Contributions, PRs, and publishing
When contributing to this repo:
- if the package version is a stable semver version (`x.y.z`), the latest
changes have been published to pub. Please add a new changelog section for
your change, rev the service portion of the version, append `-dev`, and update
the pubspec version to agree with the new version
- if the package version ends in `-dev`, the latest changes are unpublished;
please add a new changelog entry for your change in the most recent section.
When we decide to publish the latest changes we'll drop the `-dev` suffix
from the package version
- for PRs, the `Publish` bot will perform basic validation of the info in the
pubspec.yaml and CHANGELOG.md files
- when the PR is merged into the main branch, if the change includes reving to
a new stable version, a repo maintainer will tag that commit with the pubspec
version (e.g., `v1.2.3`); that tag event will trigger the `Publish` bot to
publish a new version of the package to pub.dev
''';
7 changes: 3 additions & 4 deletions pkgs/blast_repo/lib/src/tweaks/dependabot_tweak.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ class DependabotTweak extends RepoTweak {

DependabotTweak._()
: super(
name: 'Dependabot',
description:
'Ensure "$_filePath" exists and has the correct content.',
id: 'dependabot',
description: 'ensure "$_filePath" exists and has the correct content',
);

@override
FutureOr<FixResult> fix(Directory checkout) {
FutureOr<FixResult> fix(Directory checkout, String repoSlug) {
final file = _dependabotFile(checkout);

if (file == null) {
Expand Down
8 changes: 4 additions & 4 deletions pkgs/blast_repo/lib/src/tweaks/github_action_tweak.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ class GitHubActionTweak extends RepoTweak {

GitHubActionTweak._()
: super(
name: 'GitHub Action',
id: 'github-actions',
description:
'Ensure GitHub actions use the latest versions and are keyed by '
'SHA.',
'ensure GitHub actions use the latest versions and are keyed '
'by SHA',
);

@override
FutureOr<FixResult> fix(Directory checkout) async {
FutureOr<FixResult> fix(Directory checkout, String repoSlug) async {
final files = _workflowFiles(checkout);

if (files.isEmpty) {
Expand Down
17 changes: 9 additions & 8 deletions pkgs/blast_repo/lib/src/tweaks/no_reponse_tweak.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,31 @@ class NoResponseTweak extends ExactFileTweak {

NoResponseTweak._()
: super(
name: 'No Response',
id: 'no-response',
description:
"Configure a 'no response' bot to handle needs-info labels.",
"configure a 'no response' bot to handle needs-info labels",
filePath: _filePath,
expectedContent: _noResponseContent,
);

// TODO(devoncarew): Remove this after some iteration.
@override
bool get stable => false;

@override
FutureOr<FixResult> fix(Directory checkout) {
String expectedContent(String repoSlug) => _noResponseContent;

@override
FutureOr<FixResult> fix(Directory checkout, String repoSlug) {
// Check for and fail if this fix is not being run for a dart-lang repo.
if (!_isDartLangOrgRepo(checkout)) {
if (!isDartLangOrgRepo(checkout)) {
print(' repo is not in the dart-lang/ org');
return FixResult.noFixesMade;
}

return super.fix(checkout);
return super.fix(checkout, repoSlug);
}
}

bool _isDartLangOrgRepo(Directory checkout) {
bool isDartLangOrgRepo(Directory checkout) {
final gitConfigFile = File(path.join(checkout.path, '.git', 'config'));
final contents = gitConfigFile.readAsStringSync();

Expand Down
1 change: 1 addition & 0 deletions pkgs/blast_repo/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies:
dev_dependencies:
dart_flutter_team_lints: ^0.1.0
test: ^1.22.0
test_descriptor: ^2.0.0

executables:
blast_repo:
Expand Down
Loading

0 comments on commit 94155f5

Please sign in to comment.