Add script to mint and upload Forgejo Obtainium release secrets.

Generates a local upload keystore and pushes Actions secrets without printing values, so Obtainium APK builds can be signed on desktop-release.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
copilot 2026-09-30 11:34:44 +02:00
parent cdbccc23a8
commit a1b9a5e46d

View file

@ -0,0 +1,235 @@
#!/usr/bin/env bash
# Generate Android release signing material + upload Forgejo Actions secrets for
# Obtainium APK builds — without printing secret values.
#
# Usage (from anywhere):
# bash /home/yuna/git/frosty/scripts/setup-forgejo-release-secrets.sh
#
# What it does:
# 1. Writes secrets to ~/.config/frosty/forgejo-release-secrets.env (mode 600)
# — generates a new upload keystore + passwords if the file is missing
# — merges CLIENT_ID / SECRET from frosty/.env when present (never echoes them)
# 2. PUTs each secret to Forgejo Actions on copilot/frosty
#
# Re-run safely: existing secrets file is reused (keystore not regenerated) unless
# FORCE_REGENERATE=1 bash …/setup-forgejo-release-secrets.sh
#
# Twitch Helix: if CLIENT_ID/SECRET are still empty after merge, the script stops
# before upload and tells you to edit the secrets file (add the two lines) then re-run.
set -euo pipefail
REPO="${REPO:-copilot/frosty}"
FORGEJO_URL="${FORGEJO_URL:-https://forgejo.fsociety00.cc}"
TOKEN_FILE="${TOKEN_FILE:-${HOME}/.config/forgejo/token}"
SECRETS_DIR="${SECRETS_DIR:-${HOME}/.config/frosty}"
SECRETS_FILE="${SECRETS_FILE:-${SECRETS_DIR}/forgejo-release-secrets.env}"
KEYSTORE_FILE="${KEYSTORE_FILE:-${SECRETS_DIR}/upload-keystore.jks}"
FROSTY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="${ENV_FILE:-${FROSTY_ROOT}/.env}"
KEY_ALIAS_DEFAULT="${KEY_ALIAS_DEFAULT:-upload}"
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "ERROR: missing command: $1" >&2
exit 1
}
}
need_cmd curl
need_cmd python3
need_cmd openssl
need_cmd keytool
need_cmd base64
if [[ ! -f "$TOKEN_FILE" ]]; then
echo "ERROR: Forgejo token not found: $TOKEN_FILE" >&2
exit 1
fi
TOKEN="$(tr -d '[:space:]' <"$TOKEN_FILE")"
[[ -n "$TOKEN" ]] || {
echo "ERROR: empty token in $TOKEN_FILE" >&2
exit 1
}
mkdir -p "$SECRETS_DIR"
chmod 700 "$SECRETS_DIR"
rand_pw() {
# URL/shell-safe password, no quotes
openssl rand -base64 24 | tr -d '\n/+=\r' | head -c 32
}
write_secrets_file() {
local store_pw key_pw alias b64
store_pw="$1"
key_pw="$2"
alias="$3"
b64="$4"
# Atomic write; never echo values
local tmp
tmp="$(mktemp "${SECRETS_DIR}/.secrets.XXXXXX")"
umask 077
cat >"$tmp" <<EOF
# Frosty Forgejo Actions release secrets — DO NOT COMMIT
# Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)
# Upload target: ${FORGEJO_URL}/${REPO}
RELEASE_KEYSTORE_BASE64=${b64}
STORE_PASSWORD=${store_pw}
KEY_ALIAS=${alias}
KEY_PASSWORD=${key_pw}
CLIENT_ID=
SECRET=
EOF
mv -f "$tmp" "$SECRETS_FILE"
chmod 600 "$SECRETS_FILE"
}
merge_twitch_from_dotenv() {
# Pull CLIENT_ID / SECRET from frosty/.env into SECRETS_FILE if empty there.
# Uses python so values never hit the shell prompt or this script's stdout.
if [[ ! -f "$ENV_FILE" ]]; then
return 0
fi
python3 - "$ENV_FILE" "$SECRETS_FILE" <<'PY'
import pathlib, re, sys
env_path, secrets_path = map(pathlib.Path, sys.argv[1:3])
env = {}
for line in env_path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, _, v = line.partition("=")
env[k.strip()] = v.strip().strip('"').strip("'")
text = secrets_path.read_text()
changed = False
for key in ("CLIENT_ID", "SECRET"):
m = re.search(rf"^{key}=(.*)$", text, re.M)
current = m.group(1) if m else ""
incoming = env.get(key, "")
if not current and incoming:
if m:
text = re.sub(rf"^{key}=.*$", f"{key}={incoming}", text, count=1, flags=re.M)
else:
text += f"\n{key}={incoming}\n"
changed = True
if changed:
secrets_path.write_text(text)
print(f"merged Twitch keys from {env_path.name} into secrets file (values not shown)", file=sys.stderr)
PY
}
secrets_complete() {
python3 - "$SECRETS_FILE" <<'PY'
import pathlib, re, sys
text = pathlib.Path(sys.argv[1]).read_text()
needed = [
"RELEASE_KEYSTORE_BASE64",
"STORE_PASSWORD",
"KEY_ALIAS",
"KEY_PASSWORD",
"CLIENT_ID",
"SECRET",
]
missing = []
for key in needed:
m = re.search(rf"^{key}=(.*)$", text, re.M)
if not m or not m.group(1).strip():
missing.append(key)
if missing:
print("MISSING:" + ",".join(missing))
sys.exit(2)
print("OK")
PY
}
put_secret() {
local name="$1"
# Read value from secrets file without printing it
local payload code
payload="$(
python3 - "$SECRETS_FILE" "$name" <<'PY'
import json, pathlib, re, sys
text = pathlib.Path(sys.argv[1]).read_text()
name = sys.argv[2]
m = re.search(rf"^{re.escape(name)}=(.*)$", text, re.M)
if not m:
raise SystemExit(f"missing {name}")
print(json.dumps({"data": m.group(1)}))
PY
)"
code="$(
curl -sS -o /tmp/frosty-forgejo-secret-resp.json -w '%{http_code}' \
-X PUT "${FORGEJO_URL}/api/v1/repos/${REPO}/actions/secrets/${name}" \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$payload"
)"
if [[ "$code" != "201" && "$code" != "204" && "$code" != "200" ]]; then
echo "ERROR: PUT secret ${name} → HTTP ${code}" >&2
# Response may contain error text only — still avoid dumping request body
python3 -c 'import pathlib; p=pathlib.Path("/tmp/frosty-forgejo-secret-resp.json"); print(p.read_text()[:500] if p.exists() else "")' >&2 || true
exit 1
fi
echo " set ${name} (HTTP ${code})"
}
# --- generate if needed ---
if [[ ! -f "$SECRETS_FILE" || "${FORCE_REGENERATE:-0}" == "1" ]]; then
echo "→ Generating new upload keystore + passwords → ${SECRETS_FILE}"
STORE_PW="$(rand_pw)"
KEY_PW="$(rand_pw)"
ALIAS="$KEY_ALIAS_DEFAULT"
rm -f "$KEYSTORE_FILE"
keytool -genkeypair \
-keystore "$KEYSTORE_FILE" \
-storetype JKS \
-storepass "$STORE_PW" \
-keypass "$KEY_PW" \
-alias "$ALIAS" \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-dname "CN=Frosty Forgejo Release, OU=Newas, O=fsociety00, L=Home, ST=NA, C=DE" \
>/dev/null 2>&1
chmod 600 "$KEYSTORE_FILE"
B64="$(base64 -w0 "$KEYSTORE_FILE" 2>/dev/null || base64 "$KEYSTORE_FILE" | tr -d '\n')"
write_secrets_file "$STORE_PW" "$KEY_PW" "$ALIAS" "$B64"
# Drop locals
unset STORE_PW KEY_PW B64
echo " wrote keystore ${KEYSTORE_FILE}"
echo " wrote secrets file (mode 600)"
else
echo "→ Reusing existing secrets file: ${SECRETS_FILE}"
echo " (set FORCE_REGENERATE=1 to mint a new keystore)"
fi
echo "→ Merging Twitch CLIENT_ID/SECRET from ${ENV_FILE} if present…"
merge_twitch_from_dotenv || true
if ! status="$(secrets_complete)"; then
echo >&2
echo "Twitch Helix secrets still missing in ${SECRETS_FILE}." >&2
echo "Edit that file and set CLIENT_ID=… and SECRET=… (from https://dev.twitch.tv/console)," >&2
echo "or put them in ${ENV_FILE}, then re-run this script." >&2
echo "Signing secrets are already generated; they will be uploaded together." >&2
exit 3
fi
echo "→ Secrets file complete (${status})"
echo "→ Uploading Actions secrets to ${FORGEJO_URL}/${REPO} (values not printed)"
for name in RELEASE_KEYSTORE_BASE64 STORE_PASSWORD KEY_ALIAS KEY_PASSWORD CLIENT_ID SECRET; do
put_secret "$name"
done
echo
echo "Done. Secrets live only in:"
echo " ${SECRETS_FILE}"
echo " ${KEYSTORE_FILE}"
echo " Forgejo Actions → ${REPO}"
echo
echo "Next: Forgejo → Actions → Release APK → Run workflow"
echo "Obtainium: https://forgejo.fsociety00.cc/api/v1/repos/copilot/frosty/releases"
echo "APK filter: frosty-arm64-v8a-v.*\\.apk"