Stop submitting the GitHub changelog link to the Apple stores (#928)

prepareForRelease has appended a "Full changelog:" footer pointing at the
GitHub release page to every store's notes since v3.7.0. App Store review
rejects release notes carrying that link — it offers the app outside the
App Store — and iOS and macOS submissions from v3.7.0 on have come back as
invalid metadata. v3.6.1, the last release to ship, predates the footer.

formatStoreNotes and storeNotesLength now take a nullable footer URL, and
the Apple path passes null. Google Play keeps the link: its 500 character
limit means the notes really are truncated there, so the footer is the only
way to reach the full text.

Also strips the stale v3.9.1 footer out of the two committed
release_notes.txt files, so a resubmission of the current version does not
ship it while waiting on the next prepareForRelease.
This commit is contained in:
Adam Brown 2026-08-20 21:48:25 -07:00 • committed by GitHub
parent 9ead030ab9
commit bec73ea070
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 73 additions and 23 deletions

View file

@ -175,14 +175,15 @@ tasks.register("prepareForRelease") {
val versionsFile = project.rootDir.resolve(versionsPath)
writeSemvar(libs.versions.app.get(), releaseInfo.semVar, versionsFile)
// Store listings carry the app-only notes plus a link to the GitHub release,
// which holds the full text including the web and server changes. A release
// that reaches no store has no store notes; leave the existing metadata alone
// rather than blanking the notes the last client release published.
// Store listings carry the app-only notes. Google Play also gets a link to the
// GitHub release, which holds the full text including the web and server changes;
// the Apple stores must not (see releaseNotesUrl) and get the notes alone. A
// release that reaches no store has no store notes; leave the existing metadata
// alone rather than blanking the notes the last client release published.
val fullNotesUrl = releaseNotesUrl(releaseInfo.tag)
val storeNotes = releaseInfo.storeChangeLog
val writeStoreNotes = storeNotes.isNotBlank()
val notesLength = storeNotesLength(storeNotes, fullNotesUrl)
val playNotesLength = storeNotesLength(storeNotes, fullNotesUrl)
val playChangelog = formatStoreNotes(storeNotes, PLAY_STORE_LIMIT, fullNotesUrl)
// Write the Fastlane changelog file
@ -194,15 +195,15 @@ tasks.register("prepareForRelease") {
if (writeStoreNotes) {
changeLogFile.writeText(playChangelog)
println("Changelog for version ${releaseInfo.semVar} written to $changelogsPath/$versionCode.txt")
if (notesLength > PLAY_STORE_LIMIT) {
if (playNotesLength > PLAY_STORE_LIMIT) {
println(" Google Play notes truncated to $PLAY_STORE_LIMIT characters; full text at $fullNotesUrl")
}
} else {
println("No store notes for this release; store metadata left untouched")
}
val appleChangelog = formatStoreNotes(storeNotes, APPLE_STORE_LIMIT, fullNotesUrl)
if (writeStoreNotes && notesLength > APPLE_STORE_LIMIT) {
val appleChangelog = formatStoreNotes(storeNotes, APPLE_STORE_LIMIT, null)
if (writeStoreNotes && storeNotesLength(storeNotes, null) > APPLE_STORE_LIMIT) {
println("App Store notes truncated to $APPLE_STORE_LIMIT characters; full text at $fullNotesUrl")
}

View file

@ -25,7 +25,14 @@ private val SECTION_HEADER = Regex("""^\[[^]]+]$""")
/** The longest `Label:` prefix treated as an audience label rather than prose. */
private const val MAX_LABEL_LENGTH = 20
/** The GitHub release page for a release tag, where the untruncated notes live. */
/**
* The GitHub release page for a release tag, where the untruncated notes live.
*
* Only Google Play is given this link. App Store review rejects release notes that
* link to the project's GitHub page — it offers the app outside the App Store — so
* the Apple stores pass `null` as the footer URL. The link shipped to iOS and macOS
* from v3.7.0 onward and drew "invalid metadata" rejections until it was removed.
*/
fun releaseNotesUrl(tag: String): String = "$GITHUB_REPO/releases/tag/$tag"
/**
@ -156,8 +163,9 @@ fun deriveStoreNotes(fullChangelog: String): StoreNotesDerivation {
}
/**
* Fits [changelog] into a store's character limit, always leaving room for a footer
* pointing at the full notes. Whole entries are kept so the text does not end
* Fits [changelog] into a store's character limit, leaving room for a footer pointing
* at the full notes when [fullNotesUrl] is given — see [releaseNotesUrl] for why the
* Apple stores pass null. Whole entries are kept so the text does not end
* mid-bullet, and an entry too long for the remaining budget is skipped rather than
* ending the notes. The `…` mark is appended only when an entry was actually left
* out; text that fits once its blank lines are collapsed is not marked truncated.
@ -169,7 +177,7 @@ fun deriveStoreNotes(fullChangelog: String): StoreNotesDerivation {
* A blank changelog yields an empty string: store metadata that is only a link
* describes nothing, and App Store review rejects it.
*/
fun formatStoreNotes(changelog: String, limit: Int, fullNotesUrl: String): String {
fun formatStoreNotes(changelog: String, limit: Int, fullNotesUrl: String?): String {
val body = normalizeNotes(changelog)
if (body.isEmpty()) return ""
@ -202,13 +210,19 @@ fun formatStoreNotes(changelog: String, limit: Int, fullNotesUrl: String): Strin
return fitted + mark + footer
}
/** How many characters [changelog] needs at a store, footer included. */
fun storeNotesLength(changelog: String, fullNotesUrl: String): Int {
/**
* How many characters [changelog] needs at a store, including the footer when
* [fullNotesUrl] is given. Pass the same URL the store's [formatStoreNotes] call
* gets — null for the Apple stores — or the "will it truncate?" comparison is off
* by the footer's length.
*/
fun storeNotesLength(changelog: String, fullNotesUrl: String?): Int {
val body = normalizeNotes(changelog)
return if (body.isEmpty()) 0 else body.length + footerFor(fullNotesUrl).length
}
private fun footerFor(fullNotesUrl: String) = "\n\n$FOOTER_LABEL\n$fullNotesUrl"
private fun footerFor(fullNotesUrl: String?) =
if (fullNotesUrl == null) "" else "\n\n$FOOTER_LABEL\n$fullNotesUrl"
/**
* CHANGELOG.md is checked out with CRLF on Windows and seeds the release dialog, so

View file

@ -213,6 +213,47 @@ class StoreNotesTest {
assertEquals(shortNotes.length, storeNotesLength(short, url))
}
@Test
fun `A null url publishes the notes with no footer and no link`() {
val changelog = "- [Fix] A bug\n- [New] A feature"
val notes = formatStoreNotes(changelog, APPLE_STORE_LIMIT, null)
assertEquals(changelog, notes)
assertEquals(changelog.length, storeNotesLength(changelog, null))
}
@Test
fun `Apple notes never carry a link that App Store review would reject`() {
// The regression that broke every iOS and macOS submission from v3.7.0: the
// footer was appended for Apple too, so the notes ended in a github.com URL.
val changelog = (1..400).joinToString("\n") { "- [New] Feature number $it in this release" }
listOf(
formatStoreNotes("- [Fix] A bug", APPLE_STORE_LIMIT, null),
formatStoreNotes(changelog, APPLE_STORE_LIMIT, null),
formatStoreNotes("word ".repeat(2000).trim(), APPLE_STORE_LIMIT, null),
).forEach { notes ->
assertTrue(!notes.contains("github.com"), "Apple notes carry a GitHub link:\n$notes")
assertTrue(!notes.contains("Full changelog:"), "Apple notes carry the footer:\n$notes")
assertTrue(notes.length <= APPLE_STORE_LIMIT, "Notes were ${notes.length} characters")
}
}
@Test
fun `Dropping the footer gives the body more of the budget`() {
val changelog = (1..40).joinToString("\n") { "- [New] Feature number $it in this release" }
val withFooter = formatStoreNotes(changelog, PLAY_STORE_LIMIT, url)
val without = formatStoreNotes(changelog, PLAY_STORE_LIMIT, null)
assertTrue(
publishedBody(without).length > publishedBody(withFooter).length,
"The footer's characters were not returned to the body",
)
assertTrue(without.length <= PLAY_STORE_LIMIT, "Notes were ${without.length} characters")
}
@Test
fun `Release url points at the tag`() {
assertEquals(

View file

@ -1,5 +1,2 @@
- Fix sharing scene order
- Fix PDF export crash
Full changelog:
https://github.com/Darkrock-Studios/hammer-editor/releases/tag/v3.9.1
- Fix PDF export crash

View file

@ -1,5 +1,2 @@
- Fix sharing scene order
- Fix PDF export crash
Full changelog:
https://github.com/Darkrock-Studios/hammer-editor/releases/tag/v3.9.1
- Fix PDF export crash