From ff8011eaf48c5e310ed707f3dbb43a192402e771 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:16:37 +0000 Subject: [PATCH] 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. --- .github/actions/build/action.yml | 1 + .github/workflows/release.yml | 18 ++++-- .../com/subtracks/metadata/ChangelogTest.kt | 33 ----------- .../metadata/MetadataConstraintsTest.kt | 55 ++++++++++++++----- 4 files changed, 57 insertions(+), 50 deletions(-) delete mode 100644 app/src/test/kotlin/com/subtracks/metadata/ChangelogTest.kt diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index c354c7f4..aaa5290e 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -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; } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fd9adc4..ae3a139e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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[@]}" diff --git a/app/src/test/kotlin/com/subtracks/metadata/ChangelogTest.kt b/app/src/test/kotlin/com/subtracks/metadata/ChangelogTest.kt deleted file mode 100644 index 8d904197..00000000 --- a/app/src/test/kotlin/com/subtracks/metadata/ChangelogTest.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/test/kotlin/com/subtracks/metadata/MetadataConstraintsTest.kt b/app/src/test/kotlin/com/subtracks/metadata/MetadataConstraintsTest.kt index 07dd326a..11fc6c87 100644 --- a/app/src/test/kotlin/com/subtracks/metadata/MetadataConstraintsTest.kt +++ b/app/src/test/kotlin/com/subtracks/metadata/MetadataConstraintsTest.kt @@ -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")