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")