feat(release): the keystores' passwords from Proton Pass too, and F-Droid's tools checked first
tool/with_signing_keys.sh reads each keystore's password from its item's hidden field "Password" and hands it to the release tool in its environment (<keystore variable>_PASSWORD), but for a dry run, which asks for no secret; the release tool takes a secret from there where it is set, else asks as before (the Forgejo token from HALKYON_FORGEJO_TOKEN). Typing them guarded nothing the Pass session didn't already open. The release tool checks fdroid, rsync, curl and jq with the other tools, so a dry run says when one is missing, rather than the F-Droid script after the passwords. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
50c0d28194
commit
7aa8ec7304
4 changed files with 79 additions and 9 deletions
|
|
@ -82,7 +82,7 @@ Checklist after doing changes, in this order:
|
||||||
- Check `serverpod` MCP `tail_server_logs` and `tail_flutter_logs` for any issues.
|
- Check `serverpod` MCP `tail_server_logs` and `tail_flutter_logs` for any issues.
|
||||||
|
|
||||||
- **Melos** (decided 2026-10-19, ADR 0015) runs scripts across the workspace's packages: a dev dependency of the root (`dart run melos …`, its version in the lock), its scripts under `melos:` in the root `pubspec.yaml`: `analyze`; `test:dart` (the packages without Flutter, one at a time, as each server's tests start a PostgreSQL of their own on the same port); `test:flutter` (`flutter test -x requires-server`, one package at a time); `generate` (Serverpod's code generation in every package with a `config/generator.yaml`, the CLI on the PATH); `check` (`tool/check.dart`). Every script takes Melos' filters: `dart run melos run test:flutter --scope=halkyon_app`, or `--diff=origin/main --include-dependents` for what changed and what depends on it; `dart run melos list` lists the packages. The root overrides `cli_util` to 0.5.2 for it: Learn's `flutter_launcher_icons` (0.14.4, its latest) asks for ^0.4.1 and uses only the logger, which 0.5 keeps; drop the override once that package allows 0.5.
|
- **Melos** (decided 2026-10-19, ADR 0015) runs scripts across the workspace's packages: a dev dependency of the root (`dart run melos …`, its version in the lock), its scripts under `melos:` in the root `pubspec.yaml`: `analyze`; `test:dart` (the packages without Flutter, one at a time, as each server's tests start a PostgreSQL of their own on the same port); `test:flutter` (`flutter test -x requires-server`, one package at a time); `generate` (Serverpod's code generation in every package with a `config/generator.yaml`, the CLI on the PATH); `check` (`tool/check.dart`). Every script takes Melos' filters: `dart run melos run test:flutter --scope=halkyon_app`, or `--diff=origin/main --include-dependents` for what changed and what depends on it; `dart run melos list` lists the packages. The root overrides `cli_util` to 0.5.2 for it: Learn's `flutter_launcher_icons` (0.14.4, its latest) asks for ^0.4.1 and uses only the logger, which 0.5 keeps; drop the override once that package allows 0.5.
|
||||||
- **Tools are Dart where they hold logic** (decided 2026-10-19): conditions, parsing, parallel work, anything worth a test, as `tool/check.dart` (its file selection in `tool/src/check_files.dart`, tested in the root's `test/`). **Bash for glue**, short and linear (`tool/pre-push`, `tool/check_signatures.sh`, `tool/workspace_only.sh`, which narrows the root pubspec's workspace to the packages an image builds), **and for everything run on the production host**, which has no Dart SDK and gets none (`deploy/redeploy.sh`, `deploy/backup.sh`). **Python only where a library exists only there** (the apps' icons with librsvg and Pillow, `tool/app_icons.py`, its Anki fixtures with Anki itself); Learn's data scripts and `packages/halkyon_app/tool/subdivision_names.py` need nothing but Python's standard library and may move to Dart when they next change. **Releases come from `tool/release.dart`** (decided 2026-10-19, ADR 0015), shared by the apps, each configured in Dart beside it (`apps/<id>/release.dart`, typed and commented, registered in `tool/src/release/apps.dart`): `dart run tool/release.dart learn [major | minor | patch] [--dry-run]`, on a developer's laptop, never in CI, through `tool/with_signing_keys.sh learn …` (Bash glue): the keystores are kept in Proton Pass alone (vault *Infra · Signing*, each a note's attachment, the password its hidden field), and it downloads the app's and the F-Droid index's into a folder in memory (`$XDG_RUNTIME_DIR`), hands their paths to the release tool and removes them however the run ends (`test/release/signing_keys_test.dart` holds it to every app released). Tags are per app (`learn-v0.19.0`; an app's earlier tags, Learn's `v…`, count as its history), and only commits touching the app decide its version: its folder, the shared packages its packages use by path (transitively), the root pubspec and lock, its release files (key pins, F-Droid listing, snap recipe) and where its packages were before a move. Each release's *What's new* (Train's ADR 0030, D61), one file a language in the app's `assets/whats_new/` (`<version>.<language>.txt`, English and German), is drafted by it from the commits after *Go on?* and edited in `$VISUAL`/`$EDITOR` until it fits (no `TRANSLATE:` left, F-Droid's 500 characters), committed with the version, and is the app's sheet after the update, the Forgejo notes and F-Droid's changelog. `--dry-run` reads everything a release would (the version, the commits that count and what each touches, the steps, the notes it would publish), changes nothing, asks for no secret, and lists what a release would refuse. Its pure logic is in `tool/src/release/`, tested in the root's `test/release/`. `tool/fdroid_repo.sh` stays Bash (glue for fdroidserver). Build 21 (`v0.18.0`) was the last release of `tool/release.sh`, the Bash script it replaced, removed since.
|
- **Tools are Dart where they hold logic** (decided 2026-10-19): conditions, parsing, parallel work, anything worth a test, as `tool/check.dart` (its file selection in `tool/src/check_files.dart`, tested in the root's `test/`). **Bash for glue**, short and linear (`tool/pre-push`, `tool/check_signatures.sh`, `tool/workspace_only.sh`, which narrows the root pubspec's workspace to the packages an image builds), **and for everything run on the production host**, which has no Dart SDK and gets none (`deploy/redeploy.sh`, `deploy/backup.sh`). **Python only where a library exists only there** (the apps' icons with librsvg and Pillow, `tool/app_icons.py`, its Anki fixtures with Anki itself); Learn's data scripts and `packages/halkyon_app/tool/subdivision_names.py` need nothing but Python's standard library and may move to Dart when they next change. **Releases come from `tool/release.dart`** (decided 2026-10-19, ADR 0015), shared by the apps, each configured in Dart beside it (`apps/<id>/release.dart`, typed and commented, registered in `tool/src/release/apps.dart`): `dart run tool/release.dart learn [major | minor | patch] [--dry-run]`, on a developer's laptop, never in CI, through `tool/with_signing_keys.sh learn …` (Bash glue): the keystores are kept in Proton Pass alone (vault *Infra · Signing*, each a note's attachment, the password its hidden field), and it downloads the app's and the F-Droid index's into a folder in memory (`$XDG_RUNTIME_DIR`), hands their paths and, but for a dry run, their passwords (each item's hidden field *Password*, in the environment as `<keystore variable>_PASSWORD`, so the tool asks for neither) to the release tool and removes the files however the run ends (`test/release/signing_keys_test.dart` holds it to every app released). Tags are per app (`learn-v0.19.0`; an app's earlier tags, Learn's `v…`, count as its history), and only commits touching the app decide its version: its folder, the shared packages its packages use by path (transitively), the root pubspec and lock, its release files (key pins, F-Droid listing, snap recipe) and where its packages were before a move. Each release's *What's new* (Train's ADR 0030, D61), one file a language in the app's `assets/whats_new/` (`<version>.<language>.txt`, English and German), is drafted by it from the commits after *Go on?* and edited in `$VISUAL`/`$EDITOR` until it fits (no `TRANSLATE:` left, F-Droid's 500 characters), committed with the version, and is the app's sheet after the update, the Forgejo notes and F-Droid's changelog. `--dry-run` reads everything a release would (the version, the commits that count and what each touches, the steps, the notes it would publish), changes nothing, asks for no secret, and lists what a release would refuse. Its pure logic is in `tool/src/release/`, tested in the root's `test/release/`. `tool/fdroid_repo.sh` stays Bash (glue for fdroidserver). Build 21 (`v0.18.0`) was the last release of `tool/release.sh`, the Bash script it replaced, removed since.
|
||||||
|
|
||||||
## CI and deploys
|
## CI and deploys
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,25 @@ void main() {
|
||||||
expect(script, contains(r'mktemp -d -p "$XDG_RUNTIME_DIR"'));
|
expect(script, contains(r'mktemp -d -p "$XDG_RUNTIME_DIR"'));
|
||||||
expect(script, contains("trap 'rm -rf \"\$keys\"' EXIT"));
|
expect(script, contains("trap 'rm -rf \"\$keys\"' EXIT"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("the keystores' passwords come from the items' field Password, "
|
||||||
|
'under the variables the release tool reads them from, but for a dry '
|
||||||
|
'run', () {
|
||||||
|
final releaser = File('tool/src/release/releaser.dart').readAsStringSync();
|
||||||
|
expect(script, contains('select(.name == "Password")'));
|
||||||
|
expect(
|
||||||
|
script,
|
||||||
|
contains(r'export "${app_variable}_PASSWORD=$app_password"'),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
releaser,
|
||||||
|
contains(r"variable: '${app.android.keystoreVariable}_PASSWORD'"),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
script,
|
||||||
|
contains(r'export HALKYON_FDROID_KEYSTORE_PASSWORD="$fdroid_password"'),
|
||||||
|
);
|
||||||
|
expect(releaser, contains("variable: 'HALKYON_FDROID_KEYSTORE_PASSWORD'"));
|
||||||
|
expect(script, contains(r'if ! $dry_run; then'));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,10 @@ final class Releaser({
|
||||||
}
|
}
|
||||||
|
|
||||||
_step('Asking for the secrets');
|
_step('Asking for the secrets');
|
||||||
var password = _secret('Password of $keystore: ');
|
var password = _secret(
|
||||||
|
'Password of $keystore: ',
|
||||||
|
variable: '${app.android.keystoreVariable}_PASSWORD',
|
||||||
|
);
|
||||||
final keyCertificate = await _keyCertificate(
|
final keyCertificate = await _keyCertificate(
|
||||||
tools.keytool,
|
tools.keytool,
|
||||||
keystore,
|
keystore,
|
||||||
|
|
@ -319,13 +322,17 @@ final class Releaser({
|
||||||
if (app.fdroid case final fdroid?) {
|
if (app.fdroid case final fdroid?) {
|
||||||
fdroidEnvironment['HALKYON_FDROID_KEYSTORE_PASSWORD'] = _secret(
|
fdroidEnvironment['HALKYON_FDROID_KEYSTORE_PASSWORD'] = _secret(
|
||||||
"Password of the F-Droid repository's key: ",
|
"Password of the F-Droid repository's key: ",
|
||||||
|
variable: 'HALKYON_FDROID_KEYSTORE_PASSWORD',
|
||||||
);
|
);
|
||||||
await _run(fdroid.script, ['--check'], environment: fdroidEnvironment);
|
await _run(fdroid.script, ['--check'], environment: fdroidEnvironment);
|
||||||
}
|
}
|
||||||
final forgejo = _Forgejo(
|
final forgejo = _Forgejo(
|
||||||
app.forgejo,
|
app.forgejo,
|
||||||
app.repository,
|
app.repository,
|
||||||
_secret('Forgejo token for ${app.forgejo} (write:repository): '),
|
_secret(
|
||||||
|
'Forgejo token for ${app.forgejo} (write:repository): ',
|
||||||
|
variable: 'HALKYON_FORGEJO_TOKEN',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await forgejo.checkAccess();
|
await forgejo.checkAccess();
|
||||||
|
|
@ -835,6 +842,18 @@ final class Releaser({
|
||||||
_refuse('No JDK found (java, keytool).');
|
_refuse('No JDK found (java, keytool).');
|
||||||
}
|
}
|
||||||
_environment['PATH'] = '$jdk:${_environment['PATH'] ?? ''}';
|
_environment['PATH'] = '$jdk:${_environment['PATH'] ?? ''}';
|
||||||
|
// What the F-Droid repository's script needs, said before any secret
|
||||||
|
// is asked for, and by the dry run.
|
||||||
|
if (app.fdroid != null) {
|
||||||
|
for (final tool in ['fdroid', 'rsync', 'curl', 'jq']) {
|
||||||
|
if (_which(tool) == null) {
|
||||||
|
_refuse(
|
||||||
|
'$tool is missing, which ${app.fdroid!.script} needs'
|
||||||
|
'${tool == 'fdroid' ? ' (pipx install fdroidserver==2.4.5)' : ''}.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Asked only where snapcraft is there: without, the release was
|
// Asked only where snapcraft is there: without, the release was
|
||||||
// refused above, and the dry run goes on to say what else it would.
|
// refused above, and the dry run goes on to say what else it would.
|
||||||
if (app.snap?.storeChannel case final channel?
|
if (app.snap?.storeChannel case final channel?
|
||||||
|
|
@ -1023,8 +1042,15 @@ final class Releaser({
|
||||||
|
|
||||||
void _step(String name) => stdout.writeln('▸ $name');
|
void _step(String name) => stdout.writeln('▸ $name');
|
||||||
|
|
||||||
/// Asks for a secret on the terminal, without echoing it.
|
/// A secret: from the environment's [variable] where it holds one (as
|
||||||
String _secret(String prompt) {
|
/// `tool/with_signing_keys.sh` hands the keystores' passwords over from
|
||||||
|
/// Proton Pass), else asked for on the terminal, without echoing it.
|
||||||
|
String _secret(String prompt, {required String variable}) {
|
||||||
|
if (Platform.environment[variable] case final secret?
|
||||||
|
when secret.isNotEmpty) {
|
||||||
|
stdout.writeln('$prompt(from $variable)');
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
if (!stdin.hasTerminal) {
|
if (!stdin.hasTerminal) {
|
||||||
throw const ReleaseFailure('Secrets are asked for on a terminal.');
|
throw const ReleaseFailure('Secrets are asked for on a terminal.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,16 @@
|
||||||
# kept (vault "Infra · Signing"): the app's Android keystore and the F-Droid index's,
|
# kept (vault "Infra · Signing"): the app's Android keystore and the F-Droid index's,
|
||||||
# each a note's attachment, downloaded into a folder in memory ($XDG_RUNTIME_DIR, a
|
# each a note's attachment, downloaded into a folder in memory ($XDG_RUNTIME_DIR, a
|
||||||
# tmpfs only the own user reads), handed to the release tool by the variables it reads
|
# tmpfs only the own user reads), handed to the release tool by the variables it reads
|
||||||
# their paths from, and removed however the run ends. The keystores' passwords stay in
|
# their paths from, and removed however the run ends. Their passwords, each item's hidden
|
||||||
# Pass: the release tool asks for them, as ever.
|
# field "Password", go to it in its environment (<keystore variable>_PASSWORD), so it
|
||||||
|
# asks for none; a dry run asks for no secret, and gets none.
|
||||||
#
|
#
|
||||||
# tool/with_signing_keys.sh <app> [release.dart's arguments]
|
# tool/with_signing_keys.sh <app> [release.dart's arguments]
|
||||||
# tool/with_signing_keys.sh train minor --dry-run
|
# tool/with_signing_keys.sh train minor --dry-run
|
||||||
#
|
#
|
||||||
# Needs pass-cli, logged in (pass-cli login), and jq. Reads of the items take only their
|
# Needs pass-cli, logged in (pass-cli login), and jq. Reads of the items take their IDs,
|
||||||
# IDs and their attachments' names, never a field.
|
# their attachments' names and, but for a dry run, the field "Password", which goes
|
||||||
|
# nowhere but into the release tool's environment.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
|
@ -61,12 +63,33 @@ fetch() {
|
||||||
[ -s "$to" ] || fail "The attachment $name of \"$item\" came empty."
|
[ -s "$to" ] || fail "The attachment $name of \"$item\" came empty."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The hidden field "Password" of the item titled $1.
|
||||||
|
password() {
|
||||||
|
local item="$1" value
|
||||||
|
value=$(pass-cli item view --vault-name "$vault" --item-title "$item" --output json |
|
||||||
|
jq -r '.item.content.extra_fields[]? | select(.name == "Password") | .content.Hidden // empty') ||
|
||||||
|
fail "Can't read the item \"$item\" in $vault (pass-cli login?)."
|
||||||
|
[ -n "$value" ] || fail "The item \"$item\" in $vault has no field Password."
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
echo "▸ Fetching the signing keys from $vault"
|
echo "▸ Fetching the signing keys from $vault"
|
||||||
fetch "$app_item" android-release.p12 "$keys/android-release.p12"
|
fetch "$app_item" android-release.p12 "$keys/android-release.p12"
|
||||||
fetch "$fdroid_item" fdroid-index.p12 "$keys/fdroid-index.p12"
|
fetch "$fdroid_item" fdroid-index.p12 "$keys/fdroid-index.p12"
|
||||||
|
|
||||||
export "$app_variable=$keys/android-release.p12"
|
export "$app_variable=$keys/android-release.p12"
|
||||||
export HALKYON_FDROID_KEYSTORE="$keys/fdroid-index.p12"
|
export HALKYON_FDROID_KEYSTORE="$keys/fdroid-index.p12"
|
||||||
|
dry_run=false
|
||||||
|
for argument in "$@"; do
|
||||||
|
[ "$argument" = --dry-run ] && dry_run=true
|
||||||
|
done
|
||||||
|
if ! $dry_run; then
|
||||||
|
app_password=$(password "$app_item")
|
||||||
|
fdroid_password=$(password "$fdroid_item")
|
||||||
|
export "${app_variable}_PASSWORD=$app_password"
|
||||||
|
export HALKYON_FDROID_KEYSTORE_PASSWORD="$fdroid_password"
|
||||||
|
unset app_password fdroid_password
|
||||||
|
fi
|
||||||
status=0
|
status=0
|
||||||
dart run tool/release.dart "$app" "$@" || status=$?
|
dart run tool/release.dart "$app" "$@" || status=$?
|
||||||
exit "$status"
|
exit "$status"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue