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>
235 lines
7.3 KiB
Bash
Executable file
235 lines
7.3 KiB
Bash
Executable file
#!/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"
|