Merge tools/release-docs-in-step
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
5a338d0e0a
@ -13,6 +13,15 @@ An Android reader for [Ganjoor](https://ganjoor.net), the open archive of Persia
|
|||||||
240 poets and ~135,000 poems, laid out for comfortable long-form reading in Persian, Urdu
|
240 poets and ~135,000 poems, laid out for comfortable long-form reading in Persian, Urdu
|
||||||
and Arabic script, online or fully offline.
|
and Arabic script, online or fully offline.
|
||||||
|
|
||||||
|
### ⬇ [Download ganjoor-0.4.0.apk](releases/ganjoor-0.4.0.apk)
|
||||||
|
|
||||||
|
Android 7.0 and up · 31.7 MB · [older releases](releases/) · `sha256 1c64cefa…fb863ab9fc7`
|
||||||
|
|
||||||
|
Android will call the developer unknown and offer to install anyway — it says that about every
|
||||||
|
app installed outside a store. Allow installs from your browser once and it will go through.
|
||||||
|
Every release is signed with the same key, so a new one upgrades the last without losing your
|
||||||
|
bookmarks. [Build it yourself](#build) instead if you would rather.
|
||||||
|
|
||||||
Jetpack Compose, Material 3, `minSdk 24`. No account, no tracking, no server of its own.
|
Jetpack Compose, Material 3, `minSdk 24`. No account, no tracking, no server of its own.
|
||||||
|
|
||||||
## Reading
|
## Reading
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import org.gradle.api.tasks.PathSensitivity
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@ -75,6 +76,14 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReleaseDocsTest reads these, so a change to either has to re-run the tests. Without this
|
||||||
|
// Gradle sees only Kotlin sources, calls the task up to date, and a README that has fallen
|
||||||
|
// behind the build sails through green.
|
||||||
|
tasks.withType<Test>().configureEach {
|
||||||
|
inputs.file(rootProject.file("README.md")).withPathSensitivity(PathSensitivity.RELATIVE)
|
||||||
|
inputs.file(rootProject.file("releases/README.md")).withPathSensitivity(PathSensitivity.RELATIVE)
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|||||||
78
app/src/test/java/com/ganjoor/android/ReleaseDocsTest.kt
Normal file
78
app/src/test/java/com/ganjoor/android/ReleaseDocsTest.kt
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package com.ganjoor.android
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The download the README offers must be the release the build actually produces.
|
||||||
|
*
|
||||||
|
* A README that advertises last version is worse than one that says nothing: someone follows the
|
||||||
|
* link, installs an old build, and has no way to know. tools/update_release_docs.py keeps these in
|
||||||
|
* step; this is what makes forgetting to run it a failed build rather than a quiet wrong answer.
|
||||||
|
*/
|
||||||
|
class ReleaseDocsTest {
|
||||||
|
|
||||||
|
private val root: File =
|
||||||
|
generateSequence(File(System.getProperty("user.dir")!!)) { it.parentFile }
|
||||||
|
.first { File(it, "settings.gradle.kts").exists() }
|
||||||
|
|
||||||
|
private fun read(path: String) = File(root, path).readText()
|
||||||
|
|
||||||
|
private fun sha256(file: File): String =
|
||||||
|
MessageDigest.getInstance("SHA-256").digest(file.readBytes())
|
||||||
|
.joinToString("") { "%02x".format(it) }
|
||||||
|
|
||||||
|
private val declaredVersion: String by lazy {
|
||||||
|
Regex("versionName\\s*=\\s*\"([^\"]+)\"")
|
||||||
|
.find(read("app/build.gradle.kts"))!!
|
||||||
|
.groupValues[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the README offers the version the build declares`() {
|
||||||
|
val offered = Regex("### . \\[Download ganjoor-([^\\]]+)\\.apk]")
|
||||||
|
.find(read("README.md"))
|
||||||
|
?.groupValues?.get(1)
|
||||||
|
assertEquals(
|
||||||
|
"README.md offers a different version than app/build.gradle.kts declares — " +
|
||||||
|
"run tools/update_release_docs.py",
|
||||||
|
declaredVersion,
|
||||||
|
offered,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the APK the README links to is archived`() {
|
||||||
|
val apk = File(root, "releases/ganjoor-" + declaredVersion + ".apk")
|
||||||
|
assertTrue(apk.name + " is linked from README.md but not in releases/", apk.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the README short checksum matches the archived APK`() {
|
||||||
|
val short = Regex("`sha256 ([0-9a-f]+)…([0-9a-f]+)`")
|
||||||
|
.find(read("README.md"))!!
|
||||||
|
.groupValues
|
||||||
|
val full = sha256(File(root, "releases/ganjoor-" + declaredVersion + ".apk"))
|
||||||
|
assertTrue(
|
||||||
|
"README.md's checksum does not match releases/ganjoor-" + declaredVersion + ".apk",
|
||||||
|
full.startsWith(short[1]) && full.endsWith(short[2]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `every checksum in releases matches the APK beside it`() {
|
||||||
|
val listed = Regex("^([0-9a-f]{64}) (ganjoor-.+\\.apk)$", RegexOption.MULTILINE)
|
||||||
|
.findAll(read("releases/README.md"))
|
||||||
|
.map { it.groupValues[1] to it.groupValues[2] }
|
||||||
|
.toList()
|
||||||
|
assertTrue("no checksums found in releases/README.md", listed.isNotEmpty())
|
||||||
|
listed.forEach { (digest, name) ->
|
||||||
|
val apk = File(root, "releases/" + name)
|
||||||
|
assertTrue(name + " is listed in releases/README.md but missing", apk.exists())
|
||||||
|
assertEquals(name + " does not match its listed checksum", digest, sha256(apk))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
tools/update_release_docs.py
Executable file
87
tools/update_release_docs.py
Executable file
@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Bring the download line in README.md and the checksum list in releases/README.md
|
||||||
|
into step with whatever version app/build.gradle.kts declares.
|
||||||
|
|
||||||
|
Run it after building and copying the signed APK into releases/:
|
||||||
|
|
||||||
|
./gradlew :app:assembleRelease
|
||||||
|
cp app/build/outputs/apk/release/ganjoor-<v>-release.apk releases/ganjoor-<v>.apk
|
||||||
|
python3 tools/update_release_docs.py
|
||||||
|
|
||||||
|
It reads the version from the build file rather than taking an argument, so the
|
||||||
|
documentation can only ever describe the release the build actually produces.
|
||||||
|
ReleaseDocsTest fails if either file falls behind, so forgetting this is caught
|
||||||
|
rather than shipped.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def version() -> str:
|
||||||
|
build = (ROOT / "app/build.gradle.kts").read_text(encoding="utf-8")
|
||||||
|
m = re.search(r'versionName\s*=\s*"([^"]+)"', build)
|
||||||
|
if not m:
|
||||||
|
sys.exit("could not find versionName in app/build.gradle.kts")
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
v = version()
|
||||||
|
apk = ROOT / "releases" / f"ganjoor-{v}.apk"
|
||||||
|
if not apk.exists():
|
||||||
|
sys.exit(f"{apk.relative_to(ROOT)} is missing — build and copy it in first")
|
||||||
|
|
||||||
|
digest = hashlib.sha256(apk.read_bytes()).hexdigest()
|
||||||
|
megabytes = apk.stat().st_size / 1_000_000
|
||||||
|
|
||||||
|
# README.md — the download heading and the line of facts under it.
|
||||||
|
readme = ROOT / "README.md"
|
||||||
|
text = readme.read_text(encoding="utf-8")
|
||||||
|
text, n = re.subn(
|
||||||
|
r"### ⬇ \[Download ganjoor-[^\]]+\]\(releases/ganjoor-[^)]+\)",
|
||||||
|
f"### ⬇ [Download ganjoor-{v}.apk](releases/ganjoor-{v}.apk)",
|
||||||
|
text,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
if n != 1:
|
||||||
|
sys.exit("could not find the download heading in README.md")
|
||||||
|
short = f"{digest[:8]}…{digest[-11:]}"
|
||||||
|
text, n = re.subn(
|
||||||
|
r"Android 7\.0 and up · [^·]+· \[older releases\]\(releases/\) · `sha256 [^`]+`",
|
||||||
|
f"Android 7.0 and up · {megabytes:.1f} MB · [older releases](releases/) · `sha256 {short}`",
|
||||||
|
text,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
if n != 1:
|
||||||
|
sys.exit("could not find the download facts line in README.md")
|
||||||
|
readme.write_text(text, encoding="utf-8")
|
||||||
|
|
||||||
|
# releases/README.md — one checksum line per archived APK, in version order.
|
||||||
|
index = ROOT / "releases" / "README.md"
|
||||||
|
listing = index.read_text(encoding="utf-8")
|
||||||
|
line = f"{digest} ganjoor-{v}.apk"
|
||||||
|
if line not in listing:
|
||||||
|
# replace an existing line for this version, or append to the block
|
||||||
|
existing = re.search(rf"^[0-9a-f]{{64}} ganjoor-{re.escape(v)}\.apk$", listing, re.M)
|
||||||
|
if existing:
|
||||||
|
listing = listing[: existing.start()] + line + listing[existing.end():]
|
||||||
|
else:
|
||||||
|
last = None
|
||||||
|
for last in re.finditer(r"^[0-9a-f]{64} ganjoor-[^\n]+$", listing, re.M):
|
||||||
|
pass
|
||||||
|
if not last:
|
||||||
|
sys.exit("could not find the checksum block in releases/README.md")
|
||||||
|
listing = listing[: last.end()] + "\n" + line + listing[last.end():]
|
||||||
|
index.write_text(listing, encoding="utf-8")
|
||||||
|
|
||||||
|
print(f"{v} {megabytes:.1f} MB {digest}")
|
||||||
|
print("README.md and releases/README.md are in step")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user