Test builds: tool/dev_release.sh builds Linux here and publishes to a dev prerelease that dev builds update from
X.Y.Z-dev.N builds read the dev prerelease's manifest as well as the stable one and take the newer, so a tester gets each test build through the normal update and lands back on stable at the release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1b0a3db439
commit
ab133dd82f
9 files changed
+228
-23
No files matched your search
@@ -17,6 +17,7 @@ coverage/
|
|||||||
# Build artifacts that should never be committed
|
# Build artifacts that should never be committed
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.zip
|
*.zip
|
||||||
|
/dist/
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
app.*.map.json
|
app.*.map.json
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ class UpdateController {
|
|||||||
final root = _root;
|
final root = _root;
|
||||||
if (root != null) await cleanupUpdateLeftovers(root);
|
if (root != null) await cleanupUpdateLeftovers(root);
|
||||||
try {
|
try {
|
||||||
final m = await fetchManifest(url: manifestUrl);
|
final m = await fetchLatestManifest(
|
||||||
|
currentVersion: currentVersion, url: manifestUrl);
|
||||||
if (!isNewerVersion(currentVersion, m.version)) {
|
if (!isNewerVersion(currentVersion, m.version)) {
|
||||||
_log.info('up to date ($currentVersion, latest ${m.version})');
|
_log.info('up to date ($currentVersion, latest ${m.version})');
|
||||||
lastCheck.value = 'Up to date';
|
lastCheck.value = 'Up to date';
|
||||||
|
|||||||
@@ -1144,7 +1144,11 @@ does the same from the app binary. In the app, `UpdateController`
|
|||||||
indicator, which opens the update card, and restarts the app ("Update and
|
indicator, which opens the update card, and restarts the app ("Update and
|
||||||
restart", closing Hive first).
|
restart", closing Hive first).
|
||||||
Settings has Check now and an auto-check toggle. The manifest is `updates/manifest.json` in
|
Settings has Check now and an auto-check toggle. The manifest is `updates/manifest.json` in
|
||||||
this repo, written by `tool/release.sh`.
|
this repo, written by `tool/release.sh`. Test builds (`X.Y.Z-dev.N`,
|
||||||
|
`isDevBuild`) also read `devManifestUrl`, the `manifest.json` of the `dev`
|
||||||
|
prerelease (`tool/dev_release.sh`), and take the newer of the two
|
||||||
|
(`fetchLatestManifest`); the release X.Y.Z outranks its test builds, so
|
||||||
|
testers land back on stable. `0.0.0-dev` (a dev run) checks nothing.
|
||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ User pushes tags; CI does the rest.
|
|||||||
release if the other job hasn't yet and uploads (or replaces) its asset.
|
release if the other job hasn't yet and uploads (or replaces) its asset.
|
||||||
By hand: package, then the same `dart tool/release_publish.dart` line.
|
By hand: package, then the same `dart tool/release_publish.dart` line.
|
||||||
|
|
||||||
|
Test builds (Linux, no tag, no CI): `tool/dev_release.sh [--no-publish]`
|
||||||
|
(token: `RELEASE_TOKEN` or `~/.config/watch-together-release-token`, a
|
||||||
|
Gitea access token with repository read/write) builds the working tree here as the next patch's
|
||||||
|
`-dev.N` (N one past the published one) and replaces the assets of the
|
||||||
|
`dev` prerelease with the tarball and its `manifest.json`. Dev builds
|
||||||
|
update from it ([Updater](ARCHITECTURE.md#updater)). A stable install
|
||||||
|
moves onto it once with the command the script prints (run with the app
|
||||||
|
open: it finds the install from the running process and copies the new
|
||||||
|
files over it).
|
||||||
|
|
||||||
Tokens come from the environment (`RELEASE_TOKEN`, or Actions-provided
|
Tokens come from the environment (`RELEASE_TOKEN`, or Actions-provided
|
||||||
`GITHUB_TOKEN`/`GITEA_TOKEN`). The manifest has no `sha256` by default; the
|
`GITHUB_TOKEN`/`GITEA_TOKEN`). The manifest has no `sha256` by default; the
|
||||||
updater skips the check when absent. Workflows live in `.gitea/workflows`
|
updater skips the check when absent. Workflows live in `.gitea/workflows`
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ final _log = createLogger('update');
|
|||||||
const updateManifestUrl =
|
const updateManifestUrl =
|
||||||
'https://git.senkochan.moe/Foxyz/sync_replay_project/raw/branch/main/updates/manifest.json';
|
'https://git.senkochan.moe/Foxyz/sync_replay_project/raw/branch/main/updates/manifest.json';
|
||||||
|
|
||||||
|
/// Test builds (`tool/dev_release.sh`): one Gitea prerelease, tag `dev`,
|
||||||
|
/// whose assets each run replaces. Only dev builds read it.
|
||||||
|
const devManifestUrl =
|
||||||
|
'https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/dev/manifest.json';
|
||||||
|
|
||||||
const appVersion = String.fromEnvironment('APP_VERSION',
|
const appVersion = String.fromEnvironment('APP_VERSION',
|
||||||
defaultValue: '0.0.0-dev');
|
defaultValue: '0.0.0-dev');
|
||||||
|
|
||||||
@@ -161,6 +166,34 @@ Future<UpdateManifest> fetchManifest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A test build (`1.1.3-dev.4`); `0.0.0-dev` (a dev run) is not one.
|
||||||
|
bool isDevBuild(String version) => version.contains('-dev.');
|
||||||
|
|
||||||
|
/// The manifest to update from: the stable one, and for a [isDevBuild]
|
||||||
|
/// also the dev one, whichever is newer (either alone if the other fails).
|
||||||
|
/// A release outranks its own test builds, so testers return to stable.
|
||||||
|
Future<UpdateManifest> fetchLatestManifest(
|
||||||
|
{HttpClient? http,
|
||||||
|
String currentVersion = appVersion,
|
||||||
|
String url = updateManifestUrl,
|
||||||
|
String devUrl = devManifestUrl}) async {
|
||||||
|
final stable = fetchManifest(http: http, url: url);
|
||||||
|
if (!isDevBuild(currentVersion)) return stable;
|
||||||
|
stable.ignore(); // awaited below; don't let an early failure go uncaught
|
||||||
|
final dev = await fetchManifest(http: http, url: devUrl)
|
||||||
|
.then<UpdateManifest?>((m) => m, onError: (Object e) {
|
||||||
|
_log.warning('dev manifest: $e');
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (dev == null) return stable;
|
||||||
|
try {
|
||||||
|
final s = await stable;
|
||||||
|
return isNewerVersion(s.version, dev.version) ? dev : s;
|
||||||
|
} catch (_) {
|
||||||
|
return dev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
typedef DownloadProgress = void Function(int received, int? total);
|
typedef DownloadProgress = void Function(int received, int? total);
|
||||||
|
|
||||||
Future<void> downloadFile(String url, File dest,
|
Future<void> downloadFile(String url, File dest,
|
||||||
@@ -538,7 +571,8 @@ Future<int> runUpgrade(
|
|||||||
stdout.writeln('$program $currentVersion (proto ${RelayConstants.protoVersion})');
|
stdout.writeln('$program $currentVersion (proto ${RelayConstants.protoVersion})');
|
||||||
late final UpdateManifest manifest;
|
late final UpdateManifest manifest;
|
||||||
try {
|
try {
|
||||||
manifest = await fetchManifest(http: http, url: manifestUrl);
|
manifest = await fetchLatestManifest(
|
||||||
|
http: http, currentVersion: currentVersion, url: manifestUrl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
stdout.writeln('update check failed: $e');
|
stdout.writeln('update check failed: $e');
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -208,6 +208,35 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('dev builds also read the dev manifest, the newer wins', () async {
|
||||||
|
final versions = {'/stable': '1.1.2', '/dev': '1.1.3-dev.2'};
|
||||||
|
final http = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
|
http.listen((req) async {
|
||||||
|
final v = versions[req.uri.path];
|
||||||
|
if (v == null) {
|
||||||
|
req.response.statusCode = 404;
|
||||||
|
} else {
|
||||||
|
req.response.write(jsonEncode(
|
||||||
|
{'version': v, 'protocol': 1, 'url': 'http://x/$v.tar.gz'}));
|
||||||
|
}
|
||||||
|
await req.response.close();
|
||||||
|
});
|
||||||
|
final base = 'http://127.0.0.1:${http.port}';
|
||||||
|
Future<String> latest(String current) async => (await fetchLatestManifest(
|
||||||
|
currentVersion: current, url: '$base/stable', devUrl: '$base/dev'))
|
||||||
|
.version;
|
||||||
|
try {
|
||||||
|
expect(await latest('1.1.2'), '1.1.2'); // stable builds never see dev
|
||||||
|
expect(await latest('1.1.3-dev.1'), '1.1.3-dev.2');
|
||||||
|
versions['/stable'] = '1.1.3'; // the release outranks its test builds
|
||||||
|
expect(await latest('1.1.3-dev.2'), '1.1.3');
|
||||||
|
versions.remove('/stable');
|
||||||
|
expect(await latest('1.1.3-dev.1'), '1.1.3-dev.2');
|
||||||
|
} finally {
|
||||||
|
await http.close(force: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
group('upgrade flow', () {
|
group('upgrade flow', () {
|
||||||
late Directory work;
|
late Directory work;
|
||||||
late _Server server;
|
late _Server server;
|
||||||
|
|||||||
Executable
+73
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build a Linux test build here and publish it to the `dev` prerelease on
|
||||||
|
# Gitea, where dev builds update from (see Updater in docs/ARCHITECTURE.md).
|
||||||
|
# No tag push, no CI, no commit: the working tree is built as it is.
|
||||||
|
# Usage: tool/dev_release.sh [--no-publish]
|
||||||
|
# Version: the next patch after the pubspec's, `-dev.N`, N one past the
|
||||||
|
# published dev build of that patch (1.1.2 -> 1.1.3-dev.1, -dev.2, ...).
|
||||||
|
# Token: RELEASE_TOKEN, else the file ~/.config/watch-together-release-token
|
||||||
|
# (a Gitea access token with repository read/write; keep the file 0600).
|
||||||
|
# Prints the one-time command that moves a stable install onto test builds.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PUBLISH=1
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--no-publish) PUBLISH=0 ;;
|
||||||
|
*) echo "usage: $0 [--no-publish]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
TOKEN_FILE="$HOME/.config/watch-together-release-token"
|
||||||
|
if [[ -z "${RELEASE_TOKEN:-}" && -f "$TOKEN_FILE" ]]; then
|
||||||
|
RELEASE_TOKEN="$(tr -d '[:space:]' <"$TOKEN_FILE")"; export RELEASE_TOKEN
|
||||||
|
fi
|
||||||
|
if [[ "$PUBLISH" == "1" && -z "${RELEASE_TOKEN:-}" ]]; then
|
||||||
|
echo "error: set RELEASE_TOKEN or write $TOKEN_FILE (or pass --no-publish)" >&2; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
BASE_URL="https://git.senkochan.moe/Foxyz/sync_replay_project/releases/download/dev"
|
||||||
|
|
||||||
|
PUB="$(sed -n 's/^version: \([0-9.]*\).*/\1/p' "$ROOT/apps/desktop/pubspec.yaml")"
|
||||||
|
IFS=. read -r MA MI PA <<<"$PUB"
|
||||||
|
NEXT="$MA.$MI.$((PA + 1))"
|
||||||
|
LAST="$(curl -fsSL "$BASE_URL/manifest.json" 2>/dev/null |
|
||||||
|
python3 -I -c 'import json,sys; print(json.load(sys.stdin)["version"])' 2>/dev/null || true)"
|
||||||
|
N=1
|
||||||
|
if [[ "$LAST" == "$NEXT-dev."* ]]; then N=$(( ${LAST##*-dev.} + 1 )); fi
|
||||||
|
export VERSION="$NEXT-dev.$N"
|
||||||
|
echo "building $VERSION (last published: ${LAST:-none})"
|
||||||
|
|
||||||
|
"$ROOT/tool/package_linux.sh" --no-upload
|
||||||
|
TARBALL="$ROOT/dist/watch-together-$VERSION-linux-x64.tar.gz"
|
||||||
|
[[ -f "$TARBALL" ]] || { echo "error: $TARBALL missing" >&2; exit 1; }
|
||||||
|
|
||||||
|
NOTES="$(git -C "$ROOT" log -1 --format=%s)"
|
||||||
|
[[ -n "$(git -C "$ROOT" status --porcelain --untracked-files=no)" ]] && NOTES="$NOTES (+ uncommitted changes)"
|
||||||
|
MANIFEST="$ROOT/dist/manifest.json"
|
||||||
|
MANIFEST_VER="$VERSION" \
|
||||||
|
MANIFEST_PROTO="$(sed -n 's/.*protoVersion = \([0-9]*\).*/\1/p' "$ROOT/packages/core/lib/src/constants.dart")" \
|
||||||
|
MANIFEST_URL="$BASE_URL/$(basename "$TARBALL")" \
|
||||||
|
MANIFEST_SHA="$(sha256sum "$TARBALL" | cut -d' ' -f1)" \
|
||||||
|
MANIFEST_NOTES="Test build: $NOTES" \
|
||||||
|
python3 -I - "$MANIFEST" <<'EOF'
|
||||||
|
import json, os, sys
|
||||||
|
e = os.environ
|
||||||
|
url = e['MANIFEST_URL']
|
||||||
|
with open(sys.argv[1], 'w') as f:
|
||||||
|
json.dump({'version': e['MANIFEST_VER'], 'protocol': int(e['MANIFEST_PROTO']),
|
||||||
|
'url': url, 'sha256': e['MANIFEST_SHA'], 'notes': e['MANIFEST_NOTES'],
|
||||||
|
'assets': {'linux-x64': url}}, f, indent=2)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ "$PUBLISH" != "1" ]]; then
|
||||||
|
echo "built $TARBALL (not published)"; exit 0
|
||||||
|
fi
|
||||||
|
dart "$ROOT/tool/release_publish.dart" dev "$TARBALL" "$MANIFEST"
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Published $VERSION. Dev builds pick it up with their next update check.
|
||||||
|
A stable install moves onto test builds once: with the app open, run (bash)
|
||||||
|
p=\$(pgrep -xo watch_together) && d=\$(dirname "\$(readlink /proc/\$p/exe)") && t=\$(mktemp -d) && curl -fsSL $BASE_URL/$(basename "$TARBALL") | tar -xz -C "\$t" --strip-components=1 && cp -rf --remove-destination "\$t"/. "\$d"/ && rm -rf "\$t" && echo "Done: restart the app." || echo "Failed: is the app open?"
|
||||||
|
EOF
|
||||||
@@ -31,7 +31,8 @@ APP="$ROOT/apps/desktop"
|
|||||||
NAME="watch-together"
|
NAME="watch-together"
|
||||||
ARCH="x64"
|
ARCH="x64"
|
||||||
|
|
||||||
VERSION="$(sed -n 's/^version: \([0-9.]*\).*/\1/p' "$APP/pubspec.yaml")"
|
# VERSION in the environment overrides it (tool/dev_release.sh).
|
||||||
|
VERSION="${VERSION:-$(sed -n 's/^version: \([0-9.]*\).*/\1/p' "$APP/pubspec.yaml")}"
|
||||||
if [[ -z "$VERSION" ]]; then echo "error: version not found in $APP/pubspec.yaml" >&2; exit 1; fi
|
if [[ -z "$VERSION" ]]; then echo "error: version not found in $APP/pubspec.yaml" >&2; exit 1; fi
|
||||||
|
|
||||||
if [[ "$SKIP_BUILD" != "1" ]]; then
|
if [[ "$SKIP_BUILD" != "1" ]]; then
|
||||||
|
|||||||
+71
-19
@@ -6,24 +6,31 @@
|
|||||||
// asset URLs), so this never touches git history beyond reading notes.
|
// asset URLs), so this never touches git history beyond reading notes.
|
||||||
// Usage: dart tool/release_publish.dart vX.Y.Z <archive>
|
// Usage: dart tool/release_publish.dart vX.Y.Z <archive>
|
||||||
// (run after tool/package_linux.sh or tool/package_windows.ps1)
|
// (run after tool/package_linux.sh or tool/package_windows.ps1)
|
||||||
|
// dart tool/release_publish.dart dev <archive> <manifest.json>
|
||||||
|
// (tool/dev_release.sh): the `dev` prerelease, its assets all replaced.
|
||||||
// Env: RELEASE_TOKEN (preferred) or the Actions-provided GITHUB_TOKEN /
|
// Env: RELEASE_TOKEN (preferred) or the Actions-provided GITHUB_TOKEN /
|
||||||
// GITEA_TOKEN. dart: imports only — runs without `pub get`.
|
// GITEA_TOKEN. dart: imports only — runs without `pub get`.
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
Future<void> main(List<String> args) async {
|
Future<void> main(List<String> args) async {
|
||||||
if (args.length != 2) {
|
final dev = args.isNotEmpty && args[0] == 'dev';
|
||||||
_die('usage: dart tool/release_publish.dart vX.Y.Z <archive>');
|
if (args.length != (dev ? 3 : 2)) {
|
||||||
|
_die('usage: dart tool/release_publish.dart vX.Y.Z <archive>\n'
|
||||||
|
' dart tool/release_publish.dart dev <archive> <manifest.json>');
|
||||||
}
|
}
|
||||||
final tag = args[0];
|
final tag = args[0];
|
||||||
final archive = File(args[1]);
|
final files = [for (final a in args.skip(1)) File(a)];
|
||||||
|
for (final f in files) {
|
||||||
|
if (!f.existsSync()) _die('missing: ${f.path}');
|
||||||
|
}
|
||||||
|
final root = File.fromUri(Platform.script).parent.parent.path;
|
||||||
|
if (dev) return _publishDev(root, files);
|
||||||
|
final archive = files.single;
|
||||||
final ver = tag.startsWith('v') ? tag.substring(1) : tag;
|
final ver = tag.startsWith('v') ? tag.substring(1) : tag;
|
||||||
if (!RegExp(r'^\d+\.\d+\.\d+$').hasMatch(ver)) {
|
if (!RegExp(r'^\d+\.\d+\.\d+$').hasMatch(ver)) {
|
||||||
_die('tag must look like v0.2.0, got $tag');
|
_die('tag must look like v0.2.0, got $tag');
|
||||||
}
|
}
|
||||||
if (!archive.existsSync()) _die('archive missing: ${archive.path}');
|
|
||||||
|
|
||||||
final root = File.fromUri(Platform.script).parent.parent.path;
|
|
||||||
final pubspec = File('$root/apps/desktop/pubspec.yaml').readAsStringSync();
|
final pubspec = File('$root/apps/desktop/pubspec.yaml').readAsStringSync();
|
||||||
final pub = RegExp(r'^version: ([0-9.]+)', multiLine: true)
|
final pub = RegExp(r'^version: ([0-9.]+)', multiLine: true)
|
||||||
.firstMatch(pubspec)
|
.firstMatch(pubspec)
|
||||||
@@ -32,18 +39,8 @@ Future<void> main(List<String> args) async {
|
|||||||
_die('tag $tag != desktop pubspec $pub (run tool/release.sh first)');
|
_die('tag $tag != desktop pubspec $pub (run tool/release.sh first)');
|
||||||
}
|
}
|
||||||
|
|
||||||
final env = Platform.environment;
|
final token = _token();
|
||||||
final token = [env['RELEASE_TOKEN'], env['GITHUB_TOKEN'], env['GITEA_TOKEN']]
|
final api = _apiBase(root);
|
||||||
.firstWhere((t) => t != null && t.isNotEmpty,
|
|
||||||
orElse: () => _die('set RELEASE_TOKEN (or run in Actions)'))!;
|
|
||||||
|
|
||||||
final origin = _git(root, ['remote', 'get-url', 'origin']);
|
|
||||||
final server = RegExp(r'^(https?://[^/]+)/').firstMatch(origin)?.group(1) ??
|
|
||||||
env['GITHUB_SERVER_URL'] ??
|
|
||||||
_die('cannot derive server from origin $origin');
|
|
||||||
final repo =
|
|
||||||
RegExp(r'([^/:]+/[^/]+?)(\.git)?$').firstMatch(origin)!.group(1)!;
|
|
||||||
final api = '$server/api/v1/repos/$repo';
|
|
||||||
|
|
||||||
final prev = _git(root, ['describe', '--tags', '--abbrev=0', '$tag^'],
|
final prev = _git(root, ['describe', '--tags', '--abbrev=0', '$tag^'],
|
||||||
allowFail: true);
|
allowFail: true);
|
||||||
@@ -62,7 +59,7 @@ Future<void> main(List<String> args) async {
|
|||||||
final c = _Api(http, token);
|
final c = _Api(http, token);
|
||||||
var rel = await c.get('$api/releases/tags/$tag');
|
var rel = await c.get('$api/releases/tags/$tag');
|
||||||
if (rel == null) {
|
if (rel == null) {
|
||||||
stdout.writeln('creating release $tag on $repo ...');
|
stdout.writeln('creating release $tag on $api ...');
|
||||||
rel = await c.post('$api/releases', {
|
rel = await c.post('$api/releases', {
|
||||||
'tag_name': tag,
|
'tag_name': tag,
|
||||||
'name': tag,
|
'name': tag,
|
||||||
@@ -95,6 +92,61 @@ Future<void> main(List<String> args) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _token() {
|
||||||
|
final env = Platform.environment;
|
||||||
|
return [env['RELEASE_TOKEN'], env['GITHUB_TOKEN'], env['GITEA_TOKEN']]
|
||||||
|
.firstWhere((t) => t != null && t.isNotEmpty,
|
||||||
|
orElse: () => _die('set RELEASE_TOKEN (or run in Actions)'))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The repo's API URL, from origin.
|
||||||
|
String _apiBase(String root) {
|
||||||
|
final origin = _git(root, ['remote', 'get-url', 'origin']);
|
||||||
|
final server = RegExp(r'^(https?://[^/]+)/').firstMatch(origin)?.group(1) ??
|
||||||
|
Platform.environment['GITHUB_SERVER_URL'] ??
|
||||||
|
_die('cannot derive server from origin $origin');
|
||||||
|
final repo =
|
||||||
|
RegExp(r'([^/:]+/[^/]+?)(\.git)?$').firstMatch(origin)!.group(1)!;
|
||||||
|
return '$server/api/v1/repos/$repo';
|
||||||
|
}
|
||||||
|
|
||||||
|
// The `dev` prerelease (created at main's head on first use): every old
|
||||||
|
// asset deleted, then [files] uploaded. Its manifest's URL is fixed, so
|
||||||
|
// test builds find each new one.
|
||||||
|
Future<void> _publishDev(String root, List<File> files) async {
|
||||||
|
final token = _token();
|
||||||
|
final api = _apiBase(root);
|
||||||
|
final http = HttpClient();
|
||||||
|
try {
|
||||||
|
final c = _Api(http, token);
|
||||||
|
final rel = await c.get('$api/releases/tags/dev') ??
|
||||||
|
await c.post('$api/releases', {
|
||||||
|
'tag_name': 'dev',
|
||||||
|
'target_commitish': 'main',
|
||||||
|
'name': 'Test build',
|
||||||
|
'body': 'Latest test build; dev builds update from here.',
|
||||||
|
'draft': false,
|
||||||
|
'prerelease': true,
|
||||||
|
}) ??
|
||||||
|
_die('could not create or find release dev');
|
||||||
|
final id = rel['id'];
|
||||||
|
for (final a in (rel['assets'] as List? ?? const [])) {
|
||||||
|
if (a is Map) await c.delete('$api/releases/$id/assets/${a['id']}');
|
||||||
|
}
|
||||||
|
for (final f in files) {
|
||||||
|
final name = f.uri.pathSegments.last;
|
||||||
|
stdout.writeln('uploading $name ...');
|
||||||
|
await c.upload(
|
||||||
|
'$api/releases/$id/assets?name=${Uri.encodeQueryComponent(name)}',
|
||||||
|
f,
|
||||||
|
name);
|
||||||
|
}
|
||||||
|
stdout.writeln('published to dev.');
|
||||||
|
} finally {
|
||||||
|
http.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Never _die(String msg) {
|
Never _die(String msg) {
|
||||||
stderr.writeln('error: $msg');
|
stderr.writeln('error: $msg');
|
||||||
exit(1);
|
exit(1);
|
||||||
|
|||||||
Reference in new issue
Block a user