TASK-070: remove the old download page's publishing script

The old GitHub site is no longer used; releases and the version file
live on the apk branch of the team's Gitea.

Agent: main (opus)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
최민영 2026-10-12 14:22:28 +09:00
parent dd90f6932b
commit 85352d3a1c
4 changed files with 18 additions and 190 deletions

View File

@ -1542,3 +1542,13 @@ This is the append-only implementation and handoff record shared by Codex and Cl
`apk` branch has the same SHA-256 as the one built here.
- Caution: published without having been seen on a device, at the product owner's decision. The
APK is the debug build signed with this PC's key.
## 2026-10-12 (continued) — TASK-070 follow-up: the old site is out of the repository
- Request: the product owner will not use the old GitHub site any more and asked for the version
check to live on Gitea too.
- What changed: the version check already reads Gitea since 0.2.2, so nothing in the app changes.
`tools/publish-download.py`, the publisher for the old page, is removed; TASK-066 is marked
superseded. Publishing is the three steps in `tasks/070-update-from-gitea.md`.
- Caution: the page at `choimy0204.github.io/ubgitea/` still exists (password, 0.2.0). Copies of
the app older than 0.2.2 keep reading it and see "up to date".

View File

@ -1,6 +1,6 @@
# TASK-066: download page and in-app update check
Status: done
Status: done; superseded by TASK-070 (the page and its publishing script are no longer used)
Owner: Claude
Reviewer: human product owner

View File

@ -19,14 +19,19 @@ owner no longer uses that page (2026-10-12) and asked for the app to use the Git
## Scope
- Included: the two addresses, the wording, `version.json` on the `apk` branch.
- Excluded: `tools/publish-download.py` and the `choimy0204.github.io/ubgitea/` page. Both are left
as they are and are no longer part of publishing.
- Follow-up (product owner, 2026-10-12: the old site is not used any more):
`tools/publish-download.py`, which built the old page, is removed. Nothing in the repository
points at the old site now.
- Excluded: the `choimy0204.github.io/ubgitea/` page itself. It is in another repository and is
left as it is.
## Writable paths
- `app/src/main/java/com/ubisam/ubgitea/core/update/AppUpdate.kt`
- `app/src/main/java/com/ubisam/ubgitea/MainActivity.kt`
- `app/src/main/res/values/strings_update.xml`
- `tools/publish-download.py` (removed)
- `tasks/066-download-page-and-update-check.md`
- `tasks/070-update-from-gitea.md`
- `docs/development-log.md`

View File

@ -1,187 +0,0 @@
"""Builds the password-protected download page for one APK (TASK-066).
The APK is encrypted with AES-256-GCM under a key derived from the password with PBKDF2-SHA256,
so the files can sit on a public static host: the page asks for the password and decrypts in the
browser with WebCrypto. version.json beside it is plain; the app reads it to know a newer build
is out.
python tools/publish-download.py <apk> <out-dir> --notes "what changed"
The password is read from UBGITEA_DOWNLOAD_PASSWORD, or asked for. It is never written anywhere.
versionCode and versionName are read from app/build.gradle.kts.
"""
import argparse
import base64
import getpass
import hashlib
import html
import json
import os
import re
import sys
from pathlib import Path
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
ITERATIONS = 310_000
ENCRYPTED_NAME = "ubgitea.apk.enc"
CHECK_TEXT = b"UBGitea"
def gradle_version(root: Path):
text = (root / "app" / "build.gradle.kts").read_text(encoding="utf-8")
code = re.search(r"versionCode\s*=\s*(\d+)", text)
name = re.search(r'versionName\s*=\s*"([^"]+)"', text)
if not code or not name:
sys.exit("versionCode / versionName not found in app/build.gradle.kts")
return int(code.group(1)), name.group(1)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("apk", type=Path)
parser.add_argument("out", type=Path)
parser.add_argument("--notes", default="")
args = parser.parse_args()
password = os.environ.get("UBGITEA_DOWNLOAD_PASSWORD") or getpass.getpass("Download password: ")
if not password:
sys.exit("A password is required.")
code, name = gradle_version(Path(__file__).resolve().parent.parent)
data = args.apk.read_bytes()
salt = os.urandom(16)
iv = os.urandom(12)
key = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, ITERATIONS, 32)
sealed = AESGCM(key).encrypt(iv, data, None)
# A few sealed bytes the page opens first: a wrong password is refused before the APK is
# fetched at all, so nothing is downloaded with it.
check_iv = os.urandom(12)
check = AESGCM(key).encrypt(check_iv, CHECK_TEXT, None)
args.out.mkdir(parents=True, exist_ok=True)
(args.out / ENCRYPTED_NAME).write_bytes(sealed)
release = {"versionCode": code, "versionName": name, "notes": args.notes}
(args.out / "version.json").write_text(json.dumps(release, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
meta = {
"salt": base64.b64encode(salt).decode(),
"iv": base64.b64encode(iv).decode(),
"iterations": ITERATIONS,
"check": base64.b64encode(check).decode(),
"checkIv": base64.b64encode(check_iv).decode(),
"file": ENCRYPTED_NAME,
"fileName": f"UBGitea-{name}.apk",
"size": len(data),
}
page = PAGE.replace("{{META}}", json.dumps(meta)) \
.replace("{{VERSION}}", html.escape(name)) \
.replace("{{NOTES}}", html.escape(args.notes)) \
.replace("{{SIZE}}", f"{len(data) / 1024 / 1024:.1f} MB")
(args.out / "index.html").write_text(page, encoding="utf-8")
print(f"UBGitea {name} (versionCode {code}) -> {args.out}")
PAGE = """<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>UBGitea 다운로드</title>
<style>
:root { --bg: #f4f7fc; --card: #ffffff; --ink: #14213d; --muted: #5b6b86; --line: #d8e1f0;
--blue: #1f5fd6; --blue-ink: #ffffff; --bad: #c62828; }
@media (prefers-color-scheme: dark) {
:root { --bg: #0d1524; --card: #16213a; --ink: #e8eefb; --muted: #9fb0cf; --line: #2a3a5c;
--blue: #6d9cff; --blue-ink: #0d1524; --bad: #ff8a80; }
}
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: var(--bg); color: var(--ink);
font-family: system-ui, -apple-system, "Segoe UI", "Malgun Gothic", sans-serif;
display: flex; align-items: center; justify-content: center; padding: 16px; }
main { width: 100%; max-width: 420px; background: var(--card); border: 1px solid var(--line);
border-radius: 20px; padding: 28px 24px; }
h1 { margin: 0 0 4px; font-size: 24px; }
.version { color: var(--blue); font-weight: 600; }
p { color: var(--muted); line-height: 1.5; margin: 8px 0; }
.notes { white-space: pre-line; color: var(--ink); background: var(--bg); border-radius: 12px;
padding: 12px 14px; margin: 16px 0; }
.notes:empty { display: none; }
label { display: block; font-weight: 600; margin: 20px 0 8px; }
input { width: 100%; font-size: 17px; padding: 14px; border-radius: 12px; border: 1px solid var(--line);
background: var(--card); color: var(--ink); }
button { width: 100%; margin-top: 12px; font-size: 17px; font-weight: 600; padding: 14px; border: 0;
border-radius: 12px; background: var(--blue); color: var(--blue-ink); cursor: pointer; }
button:disabled { opacity: .6; cursor: default; }
#status { min-height: 1.5em; margin-top: 12px; }
#status.bad { color: var(--bad); }
ol { color: var(--muted); padding-left: 20px; line-height: 1.6; margin: 20px 0 0; }
</style>
</head>
<body>
<main>
<h1>UBGitea</h1>
<div class="version">버전 {{VERSION}} · {{SIZE}}</div>
<div class="notes">{{NOTES}}</div>
<form id="form">
<label for="password">다운로드 비밀번호</label>
<input id="password" type="password" autocomplete="off" required>
<button id="go" type="submit">APK 다운로드</button>
</form>
<p id="status" role="status" aria-live="polite"></p>
<ol>
<li>받은 APK 파일을 엽니다.</li>
<li>"출처를 알 수 없는 앱" 설치를 이 브라우저에 허용합니다.</li>
<li>설치(업데이트)를 누릅니다. 로그인과 설정은 그대로 남습니다.</li>
</ol>
</main>
<script>
const META = {{META}};
const status = document.getElementById("status");
const go = document.getElementById("go");
const b64 = s => Uint8Array.from(atob(s), c => c.charCodeAt(0));
function say(text, bad) { status.textContent = text; status.className = bad ? "bad" : ""; }
document.getElementById("form").addEventListener("submit", async event => {
event.preventDefault();
go.disabled = true;
try {
say("비밀번호 확인 중…");
const password = new TextEncoder().encode(document.getElementById("password").value);
const base = await crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveKey"]);
const key = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt: b64(META.salt), iterations: META.iterations, hash: "SHA-256" },
base, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
try {
await crypto.subtle.decrypt({ name: "AES-GCM", iv: b64(META.checkIv) }, key, b64(META.check));
} catch (e) {
say("비밀번호가 맞지 않습니다.", true);
return;
}
say("파일을 받는 중…");
const response = await fetch(META.file, { cache: "no-store" });
if (!response.ok) throw new Error("fetch");
const sealed = await response.arrayBuffer();
say("여는 중…");
const apk = await crypto.subtle.decrypt({ name: "AES-GCM", iv: b64(META.iv) }, key, sealed);
const url = URL.createObjectURL(new Blob([apk], { type: "application/vnd.android.package-archive" }));
const link = document.createElement("a");
link.href = url;
link.download = META.fileName;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 60000);
say("다운로드를 시작했습니다. 받은 파일을 열어 설치하세요.");
} catch (e) {
say("파일을 받지 못했습니다. 잠시 뒤 다시 시도하세요.", true);
} finally {
go.disabled = false;
}
});
</script>
</body>
</html>
"""
if __name__ == "__main__":
main()