feat(release): the keystores' passwords from Proton Pass too, and F-Droid's tools checked first
All checks were successful
CI / check (push) Successful in 38m54s
CI / deploy (push) Successful in 56s

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:
Tizian 「ティツィアーン」 2026-10-31 20:51:21 +01:00
commit 7aa8ec7304
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 79 additions and 9 deletions

View file

@ -299,7 +299,10 @@ final class Releaser({
}
_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(
tools.keytool,
keystore,
@ -319,13 +322,17 @@ final class Releaser({
if (app.fdroid case final fdroid?) {
fdroidEnvironment['HALKYON_FDROID_KEYSTORE_PASSWORD'] = _secret(
"Password of the F-Droid repository's key: ",
variable: 'HALKYON_FDROID_KEYSTORE_PASSWORD',
);
await _run(fdroid.script, ['--check'], environment: fdroidEnvironment);
}
final forgejo = _Forgejo(
app.forgejo,
app.repository,
_secret('Forgejo token for ${app.forgejo} (write:repository): '),
_secret(
'Forgejo token for ${app.forgejo} (write:repository): ',
variable: 'HALKYON_FORGEJO_TOKEN',
),
);
try {
await forgejo.checkAccess();
@ -835,6 +842,18 @@ final class Releaser({
_refuse('No JDK found (java, keytool).');
}
_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
// refused above, and the dry run goes on to say what else it would.
if (app.snap?.storeChannel case final channel?
@ -1023,8 +1042,15 @@ final class Releaser({
void _step(String name) => stdout.writeln('▸ $name');
/// Asks for a secret on the terminal, without echoing it.
String _secret(String prompt) {
/// A secret: from the environment's [variable] where it holds one (as
/// `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) {
throw const ReleaseFailure('Secrets are asked for on a terminal.');
}

View file

@ -3,14 +3,16 @@
# 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
# 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
# Pass: the release tool asks for them, as ever.
# their paths from, and removed however the run ends. Their passwords, each item's hidden
# 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 train minor --dry-run
#
# Needs pass-cli, logged in (pass-cli login), and jq. Reads of the items take only their
# IDs and their attachments' names, never a field.
# Needs pass-cli, logged in (pass-cli login), and jq. Reads of the items take their IDs,
# 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
cd "$(git rev-parse --show-toplevel)"
@ -61,12 +63,33 @@ fetch() {
[ -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"
fetch "$app_item" android-release.p12 "$keys/android-release.p12"
fetch "$fdroid_item" fdroid-index.p12 "$keys/fdroid-index.p12"
export "$app_variable=$keys/android-release.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
dart run tool/release.dart "$app" "$@" || status=$?
exit "$status"