halkyon/tool/with_signing_keys.sh
Tizian 「ティツィアーン」 f06fc23023
All checks were successful
CI / check (push) Successful in 41m7s
CI / deploy (push) Successful in 1m2s
build(release): several apps released in one run (train:minor learn), their version commits pushed without the checks
The release tool and tool/with_signing_keys.sh take several apps, each with its version
after a colon or none, and release them one after another, each as alone, stopping at
the first that fails; the key script fetches every named app's key.

The pre-push hook skips tool/check.dart when a push brings release commits alone: the
release tool's chore(release) subject, an app's pubspec changed in its version line and
its texts of what's new, nothing else (tool/release_push.dart). Signatures are checked as
before, and CI still runs the checks. A release no longer costs a run of the checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-03 12:16:10 +01:00

155 lines
6.3 KiB
Shell
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Runs a release with its signing keys fetched from Proton Pass, where alone they are
# 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. Their passwords, each item's hidden
# field "Password", go to it in its environment (<keystore variable>_PASSWORD), so it
# asks for none, nor for the Forgejo token, a login's password in the vault Infra
# (HALKYON_FORGEJO_TOKEN); 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
# tool/with_signing_keys.sh train:minor learn several, one after another
# tool/with_signing_keys.sh fdroid [tool/fdroid_repo.sh's arguments]
#
# The last runs tool/fdroid_repo.sh alone, with the F-Droid index's key and its password
# only: to publish the repository again, or a release's APK whose upload failed after its
# release was made (build/release/<tag>/<apk>).
#
# 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)"
readonly vault='Infra · Signing'
fail() {
echo "✗ $*" >&2
exit 1
}
[ $# -ge 1 ] || fail 'Usage: tool/with_signing_keys.sh <app[:version] … | fdroid> [arguments]'
# Each app released (tool/src/release/apps.dart): the variable its keystore's path goes
# in (its AndroidSigning's keystoreVariable) and the item keeping it, set for the app $1.
key_of() {
case "$1" in
learn) app_variable=HALKYON_ANDROID_KEYSTORE app_item='Halkyon Learn – Android release keystore' ;;
train) app_variable=HALKYON_TRAIN_ANDROID_KEYSTORE app_item='Halkyon Train – Android release keystore' ;;
todo) app_variable=HALKYON_TODO_ANDROID_KEYSTORE app_item='Halkyon Todo – Android release keystore' ;;
*) fail "No signing key known for $1." ;;
esac
}
# The apps the arguments name (`train:minor learn`, `train minor --dry-run`): every
# word but the options and a version on its own, without a version after a colon.
apps=()
if [ "$1" != fdroid ]; then
for word in "$@"; do
case "$word" in
-* | major | minor | patch) ;;
*) apps+=("${word%%:*}") ;;
esac
done
[ ${#apps[@]} -ge 1 ] || fail 'No app named.'
for app in "${apps[@]}"; do
key_of "$app"
done
fi
# The F-Droid repository's index key, the same for every app listed there.
readonly fdroid_item='Halkyon Learn – F-Droid index keystore'
# The Forgejo token releases publish with (write:repository), a login's password in
# the vault Infra (its folder API Tokens), made for this laptop.
readonly token_vault='Infra'
readonly token_item='Forgejo Token halkyon_learn release signing script (melchior)'
for tool in pass-cli jq; do
command -v "$tool" >/dev/null || fail "$tool is missing."
done
[ -n "${XDG_RUNTIME_DIR:-}" ] && [ -d "$XDG_RUNTIME_DIR" ] ||
fail 'No $XDG_RUNTIME_DIR to keep the keys in memory.'
keys=$(mktemp -d -p "$XDG_RUNTIME_DIR")
trap 'rm -rf "$keys"' EXIT
trap 'exit 130' INT TERM HUP
# Downloads the attachment named $2 of the item titled $1 to $3.
fetch() {
local item="$1" name="$2" to="$3" ids
ids=$(pass-cli item view --vault-name "$vault" --item-title "$item" --output json |
jq -r --arg name "$name" \
'[.item.share_id, .item.id, (.attachments[] | select(.content.name == $name) | .id)]
| select(length == 3) | @tsv') ||
fail "Can't read the item \"$item\" in $vault (pass-cli login?)."
[ -n "$ids" ] || fail "The item \"$item\" in $vault has no attachment $name."
local share item_id attachment
IFS=$'\t' read -r share item_id attachment <<<"$ids"
pass-cli item attachment download --share-id "$share" --item-id "$item_id" \
--attachment-id "$attachment" --output "$to" >/dev/null
[ -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"
}
# The Forgejo token: the password of the login $token_item in $token_vault.
forgejo_token() {
local value
value=$(pass-cli item view --vault-name "$token_vault" --item-title "$token_item" --output json |
jq -r '.item.content.content.Login.password // empty') ||
fail "Can't read the item \"$token_item\" in $token_vault (pass-cli login?)."
[ -n "$value" ] || fail "The item \"$token_item\" in $token_vault has no password."
printf '%s' "$value"
}
if [ "$1" = fdroid ]; then
shift
echo "▸ Fetching the F-Droid index's key from $vault"
fetch "$fdroid_item" fdroid-index.p12 "$keys/fdroid-index.p12"
export HALKYON_FDROID_KEYSTORE="$keys/fdroid-index.p12"
fdroid_password=$(password "$fdroid_item")
export HALKYON_FDROID_KEYSTORE_PASSWORD="$fdroid_password"
unset fdroid_password
status=0
tool/fdroid_repo.sh "$@" || status=$?
exit "$status"
fi
dry_run=false
for argument in "$@"; do
[ "$argument" = --dry-run ] && dry_run=true
done
echo "▸ Fetching the signing keys from $vault"
# Each app's keystore under a name of its own, as each has a variable of its own.
for app in "${apps[@]}"; do
key_of "$app"
fetch "$app_item" android-release.p12 "$keys/$app-android-release.p12"
export "$app_variable=$keys/$app-android-release.p12"
if ! $dry_run; then
app_password=$(password "$app_item")
export "${app_variable}_PASSWORD=$app_password"
unset app_password
fi
done
fetch "$fdroid_item" fdroid-index.p12 "$keys/fdroid-index.p12"
export HALKYON_FDROID_KEYSTORE="$keys/fdroid-index.p12"
if ! $dry_run; then
fdroid_password=$(password "$fdroid_item")
export HALKYON_FDROID_KEYSTORE_PASSWORD="$fdroid_password"
token=$(forgejo_token)
export HALKYON_FORGEJO_TOKEN="$token"
unset fdroid_password token
fi
status=0
dart run tool/release.dart "$@" || status=$?
exit "$status"