release: only publish to Play for a stable tag

Gate the Play upload on a full vX.Y.Z tag and mark other tags as
prereleases. Fold the changelog length limit into MetadataConstraintsTest
and make its image checks reject anything but PNG or JPEG. Keep the
decoded keystore at 0600 and note in .forgejo/README.md that the GitHub
CI now factors the build into composite actions.
This commit is contained in:
austinried
2026-10-08 18:52:44 +00:00
parent b4b5f66d81
commit ff8011eaf4
4 changed files with 57 additions and 50 deletions
+1
View File
@@ -47,6 +47,7 @@ runs:
KEYSTORE_BASE64: ${{ inputs.keystore-base64 }}
KEYPROPERTIES_BASE64: ${{ inputs.keyproperties-base64 }}
run: |
umask 077
echo "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.keystore"
alias=$(printf '%s' "$KEYPROPERTIES_BASE64" | base64 -d | sed -n 's/^keyAlias=//p' | tr -d '\r')
test -n "$alias" || { echo "no keyAlias in keyproperties" >&2; exit 1; }
+14 -4
View File
@@ -49,7 +49,16 @@ jobs:
name: release
path: dist
- uses: ./.github/actions/nix
- name: Detect stable tag
id: stable
run: |
if [[ "$GITHUB_REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "value=true" >> "$GITHUB_OUTPUT"
else
echo "value=false" >> "$GITHUB_OUTPUT"
fi
- name: Upload to Google Play
if: steps.stable.outputs.value == 'true'
env:
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
run: |
@@ -72,7 +81,8 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "$GITHUB_REF_NAME" subtracks.apk \
--verify-tag \
--title "$GITHUB_REF_NAME" \
--notes-file "metadata/en-US/changelogs/${{ needs.build.outputs.version-code }}.txt"
args=(--verify-tag --title "$GITHUB_REF_NAME" --notes-file "metadata/en-US/changelogs/${{ needs.build.outputs.version-code }}.txt")
if [ "${{ steps.stable.outputs.value }}" != "true" ]; then
args+=(--prerelease)
fi
gh release create "$GITHUB_REF_NAME" subtracks.apk "${args[@]}"
@@ -1,33 +0,0 @@
package com.subtracks.metadata
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
class ChangelogTest {
@Test
fun changelogsFitStoreLimit() {
val root =
generateSequence(File(".").canonicalFile) { it.parentFile }
.first { File(it, "settings.gradle.kts").isFile }
val files =
File(root, "metadata")
.listFiles()
.orEmpty()
.flatMap { locale -> File(locale, "changelogs").listFiles().orEmpty().toList() }
.filter { it.isFile && it.extension == "txt" }
assertTrue("no changelog files found under metadata/", files.isNotEmpty())
files.forEach { file ->
val text = file.readText().replace("\r\n", "\n")
val length = text.codePointCount(0, text.length)
assertTrue(
"metadata changelogs must be at most $LIMIT characters; ${file.name} is $length",
length <= LIMIT,
)
}
}
private companion object {
const val LIMIT = 500
}
}
@@ -23,9 +23,21 @@ class MetadataConstraintsTest {
}
@Test
fun iconsAre512OpaqueAlphaPngsWithinOneMegabyte() {
fun changelogsFitStoreLimit() {
localeFiles("changelogs").forEach { dir ->
dir
.listFiles()
.orEmpty()
.filter { it.isFile && it.extension == "txt" }
.forEach { assertAtMost(it, CHANGELOG_LIMIT) }
}
}
@Test
fun iconsAre512AlphaPngsWithinOneMegabyte() {
localeFiles("images/icon.png").forEach { file ->
val image = readImage(file)
assertEquals("${file.path} must be a PNG", ImageFormat.PNG, image.format)
assertEquals("${file.path} must be 512x512", 512, image.width)
assertEquals("${file.path} must be 512x512", 512, image.height)
assertEquals(
@@ -43,7 +55,7 @@ class MetadataConstraintsTest {
val image = readImage(file)
assertEquals("${file.path} must be 1024x500", 1024, image.width)
assertEquals("${file.path} must be 1024x500", 500, image.height)
assertNoAlpha(file, image)
assertOpaquePngOrJpeg(file, image)
}
}
@@ -67,21 +79,31 @@ class MetadataConstraintsTest {
assertTrue("${file.path} shortest side $short must be at least 320", short >= 320)
assertTrue("${file.path} longest side $long must be at most 3840", long <= 3840)
assertTrue("${file.path} aspect ratio exceeds 2:1", long <= 2 * short)
assertNoAlpha(file, image)
assertOpaquePngOrJpeg(file, image)
}
}
}
private fun assertNoAlpha(
private fun assertOpaquePngOrJpeg(
file: File,
image: ImageInfo,
) {
if (image.isPng) {
assertEquals(
"${file.path} must be a 24-bit PNG without alpha (colorType $RGB_COLOR_TYPE); colorType ${image.colorType}",
RGB_COLOR_TYPE,
image.colorType,
)
when (image.format) {
ImageFormat.PNG -> {
assertEquals(
"${file.path} must be a 24-bit PNG without alpha (colorType $RGB_COLOR_TYPE); colorType ${image.colorType}",
RGB_COLOR_TYPE,
image.colorType,
)
}
ImageFormat.JPEG -> {
Unit
}
ImageFormat.OTHER -> {
assertTrue("${file.path} must be a PNG or JPEG", false)
}
}
}
@@ -104,11 +126,13 @@ class MetadataConstraintsTest {
generateSequence(File(".").canonicalFile) { it.parentFile }
.first { File(it, "settings.gradle.kts").isFile }
private enum class ImageFormat { PNG, JPEG, OTHER }
private data class ImageInfo(
val width: Int,
val height: Int,
val colorType: Int?,
val isPng: Boolean,
val format: ImageFormat,
)
private fun readImage(file: File): ImageInfo {
@@ -118,12 +142,12 @@ class MetadataConstraintsTest {
width = bytes.beInt(PNG_WIDTH_OFFSET),
height = bytes.beInt(PNG_HEIGHT_OFFSET),
colorType = bytes[PNG_COLOR_TYPE_OFFSET].toInt() and 0xFF,
isPng = true,
format = ImageFormat.PNG,
)
}
val image = ImageIO.read(file)
assertTrue("unreadable image ${file.path}", image != null)
return ImageInfo(image.width, image.height, colorType = null, isPng = false)
return ImageInfo(image.width, image.height, colorType = null, format = if (bytes.isJpeg()) ImageFormat.JPEG else ImageFormat.OTHER)
}
private fun ByteArray.isPng(): Boolean =
@@ -133,6 +157,8 @@ class MetadataConstraintsTest {
this[2] == 'N'.code.toByte() &&
this[3] == 'G'.code.toByte()
private fun ByteArray.isJpeg(): Boolean = size > 2 && this[0] == 0xFF.toByte() && this[1] == 0xD8.toByte()
private fun ByteArray.beInt(offset: Int): Int =
((this[offset].toInt() and 0xFF) shl 24) or
((this[offset + 1].toInt() and 0xFF) shl 16) or
@@ -149,6 +175,9 @@ class MetadataConstraintsTest {
// Play and F-Droid both allow 4000.
const val FULL_DESCRIPTION_LIMIT = 4000
// Play's "What's new" and F-Droid's changelog both allow 500.
const val CHANGELOG_LIMIT = 500
const val RGB_COLOR_TYPE = 2
const val RGBA_COLOR_TYPE = 6
val IMAGE_EXTENSIONS = setOf("png", "jpg", "jpeg")