mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
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:
@@ -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; }
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user