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:
parent
cdbccc23a8
commit
a1b9a5e46d
1 changed files with 235 additions and 0 deletions
235
scripts/setup-forgejo-release-secrets.sh
Executable file
235
scripts/setup-forgejo-release-secrets.sh
Executable 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue