build(release): a Dart release tool for every app
tool/release.dart releases one app: `dart run tool/release.dart learn [major | minor | patch] [--dry-run]`, each app configured in Dart beside it (apps/learn/release.dart). Every step and check of tool/release.sh is kept: the repository as pushed, resuming after a release commit and tag, Flutter as CI pins it, the tools, the key and its certificate pin, secrets asked for without echo and never written down, CI green on the newest commit, the signed release commit and tag by a key deploys accept, the APK built from a checkout of the tag and checked, the snap, the signed checksums, pushing only after both builds passed, the release on Forgejo, the F-Droid repository, the Snap Store once a channel is set. New: tags per app (learn-v0.19.0, Learn's v… counting as its history); only commits touching the app decide its version (its folder, the shared packages it uses by path, the root pubspec and lock, its release files, where its packages were before the move); --dry-run reads all of it, lists every step and the notes, and what a release would refuse, changing nothing. The pure parts are tested in test/release/. Learn releases build 21 with tool/release.sh, then switches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
1b62c8098c
commit
e65f61bdd1
1 changed files with 2334 additions and 12 deletions
103
tool/release.dart
Normal file
103
tool/release.dart
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// Releases one of Halkyon's apps (ADR 0015): works out the next version from
|
||||
// the commits touching the app since its last release, writes it into the
|
||||
// app's pubspec in a signed commit with a signed tag (`learn-v0.19.0`), builds
|
||||
// the Android APK (signed with the app's key, from a checkout of the tag) and
|
||||
// the Linux snap, checks them, signs a list of their checksums with GPG,
|
||||
// pushes, and publishes: a release on code.halkyon.app, the APK in the
|
||||
// project's F-Droid repository, and, once a channel is set, the snap in the
|
||||
// Snap Store. Run on a developer's laptop, never in CI: the app's key never
|
||||
// leaves it. Each app's configuration is beside it (`apps/learn/release.dart`).
|
||||
//
|
||||
// dart run tool/release.dart learn the version the commits call for
|
||||
// dart run tool/release.dart learn minor or major, or patch: that one
|
||||
// dart run tool/release.dart learn --dry-run what it would do, changing nothing
|
||||
//
|
||||
// Versions follow semantic versioning, with a build number (+N) that rises by
|
||||
// one with every release: Android installs only a higher one over an older,
|
||||
// and the server tells apps below the build it supports to update. Since the
|
||||
// app's last release (its own tags, `learn-v…`, and its history's, `v…`), only
|
||||
// commits touching it count (its folder, the shared packages it uses, the
|
||||
// workspace's pubspec and lock, its release files): a feat raises the minor
|
||||
// number, a fix or perf the patch number, a breaking one (`!` or a
|
||||
// `BREAKING CHANGE:` line) the major number from 1.0 on, the minor before.
|
||||
// Other types release nothing by themselves. Raising minimumAppBuild in the
|
||||
// server, for changes older apps can't follow, stays a decision of its own.
|
||||
//
|
||||
// It shows the version and its commits and asks before it changes anything.
|
||||
// It needs main clean and as pushed, CI green on its newest commit, and asks
|
||||
// for the app key's password, the F-Droid repository key's password and a
|
||||
// Forgejo token (scope write:repository) first; then it runs on its own until
|
||||
// GPG asks for its passphrase (and ssh for the upload key's, unless an agent
|
||||
// holds them). None of them is ever written down. Nothing is pushed or
|
||||
// published before both builds passed their checks; a run that stopped after
|
||||
// making the release commit and tag carries on with them when started again.
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
|
||||
import 'src/release/apps.dart';
|
||||
import 'src/release/releaser.dart';
|
||||
import 'src/release/version.dart';
|
||||
|
||||
Future<void> main(List<String> arguments) async {
|
||||
final parser = ArgParser()
|
||||
..addFlag(
|
||||
'dry-run',
|
||||
negatable: false,
|
||||
help:
|
||||
'Show the version, the commits that count and every step a release '
|
||||
'would take, changing nothing and asking for no secret.',
|
||||
)
|
||||
..addFlag('help', abbr: 'h', negatable: false);
|
||||
final usage =
|
||||
'Usage: dart run tool/release.dart <app> [major | minor | patch] '
|
||||
'[--dry-run]\nApps: ${releasedApps.keys.join(', ')}\n${parser.usage}';
|
||||
|
||||
final ArgResults options;
|
||||
try {
|
||||
options = parser.parse(arguments);
|
||||
} on FormatException catch (error) {
|
||||
stderr.writeln('✗ ${error.message}\n$usage');
|
||||
exit(64);
|
||||
}
|
||||
final rest = options.rest;
|
||||
if (options.flag('help') || rest.isEmpty || rest.length > 2) {
|
||||
(options.flag('help') ? stdout : stderr).writeln(usage);
|
||||
exit(options.flag('help') ? 0 : 64);
|
||||
}
|
||||
|
||||
final app = releasedApps[rest.first];
|
||||
if (app == null) {
|
||||
stderr.writeln(
|
||||
'✗ No app "${rest.first}": one of ${releasedApps.keys.join(', ')}.',
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
final Bump? chosen;
|
||||
if (rest.length == 2) {
|
||||
chosen = Bump.values.asNameMap()[rest[1]];
|
||||
if (chosen == null) {
|
||||
stderr.writeln('✗ ${rest[1]} is none of major, minor, patch.');
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
chosen = null;
|
||||
}
|
||||
|
||||
final top = await Process.run('git', ['rev-parse', '--show-toplevel']);
|
||||
if (top.exitCode != 0) {
|
||||
stderr.writeln('✗ Not in the repository.');
|
||||
exit(1);
|
||||
}
|
||||
try {
|
||||
await Releaser(
|
||||
app: app,
|
||||
root: Directory('${top.stdout}'.trim()),
|
||||
chosen: chosen,
|
||||
dryRun: options.flag('dry-run'),
|
||||
).run();
|
||||
} on ReleaseFailure catch (failure) {
|
||||
stderr.writeln('✗ $failure');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue