11===============================================================================
22PROJECT EXPORT (GIT TRACKED ONLY)
3- Generated: Sun Apr 5 12:05:45 PM EDT 2026
3+ Generated: Sun Apr 5 12:36:10 PM EDT 2026
44Project Path: /home/kushal/src/kotlin/Seal
55===============================================================================
66
@@ -913,7 +913,7 @@ jobs:
913913
914914================================================================================
915915FILE: .github/workflows/android_ci.yml
916- SIZE: .58 KB
916+ SIZE: .63 KB
917917================================================================================
918918
919919name: Android CI
@@ -922,16 +922,18 @@ on:
922922 pull_request:
923923 branches: [ "main" ]
924924
925+ env:
926+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
927+
925928jobs:
926929 build:
927-
928930 runs-on: ubuntu-latest
929931
930932 steps:
931933 - uses: actions/checkout@v4
932934
933- - name: set up JDK 21
934- uses: actions/setup-java@v3
935+ - name: Set up JDK 21
936+ uses: actions/setup-java@v4
935937 with:
936938 java-version: '21'
937939 distribution: 'temurin'
@@ -940,9 +942,11 @@ jobs:
940942 - name: Setup Android SDK
941943 uses: android-actions/setup-android@v3
942944
943- - uses: gradle/actions/setup-gradle@v3
945+ - uses: gradle/actions/setup-gradle@v4
946+
944947 - name: Grant execute permission for gradlew
945948 run: chmod +x gradlew
949+
946950 - name: Build with Gradle
947951 run: ./gradlew buildGenericRelease
948952
@@ -970,14 +974,18 @@ jobs:
970974
971975================================================================================
972976FILE: .github/workflows/release.yml
973- SIZE: 7.42 KB
977+ SIZE: 7.55 KB
974978================================================================================
975979
976980# .github/workflows/release.yml
977981#
978982# Triggers on every push to `main` and produces a signed, full (non-pre-release)
979983# GitHub Release from the `generic` product flavor.
980984#
985+ # Version auto-bumps on every build using a timestamp-based scheme:
986+ # Tag format: v2.0.0-alpha.YYYYMMDD.HHMM
987+ # This ensures every push gets a unique, monotonically increasing version.
988+ #
981989# Required GitHub Secrets (Settings → Secrets and variables → Actions):
982990# ANDROID_KEYSTORE_BASE64 – base64-encoded android.keystore file
983991# ANDROID_SIGNING_PASSWORD – password used for both storePassword and keyPassword
989997 branches:
990998 - main
991999
1000+ # Opt into Node.js 24 to silence the deprecation warnings.
1001+ env:
1002+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
1003+
9921004# Only allow one release workflow to run at a time; cancel any in-progress run
993- # if a new push arrives before it finishes. This prevents duplicate releases.
1005+ # if a new push arrives before it finishes.
9941006concurrency:
9951007 group: release-${{ github.ref }}
9961008 cancel-in-progress: true
@@ -1000,8 +1012,6 @@ jobs:
10001012 name: Build & Publish Release
10011013 runs-on: ubuntu-latest
10021014
1003- # Give the job permission to create releases and upload assets.
1004- # "contents: write" is required by softprops/action-gh-release.
10051015 permissions:
10061016 contents: write
10071017
@@ -1010,8 +1020,6 @@ jobs:
10101020 - name: Checkout source
10111021 uses: actions/checkout@v4
10121022 with:
1013- # Fetch the full history so the tag we create doesn't conflict with
1014- # any shallow-clone limitation.
10151023 fetch-depth: 0
10161024
10171025 # ── 2. Java / Gradle toolchain ─────────────────────────────────────────
@@ -1020,7 +1028,6 @@ jobs:
10201028 with:
10211029 java-version: '21'
10221030 distribution: 'temurin'
1023- # Caches ~/.gradle/caches and ~/.gradle/wrapper between runs.
10241031 cache: 'gradle'
10251032
10261033 # ── 3. Android SDK ─────────────────────────────────────────────────────
@@ -1029,105 +1036,119 @@ jobs:
10291036
10301037 # ── 4. Gradle wrapper ──────────────────────────────────────────────────
10311038 - name: Set up Gradle
1032- uses: gradle/actions/setup-gradle@v3
1039+ uses: gradle/actions/setup-gradle@v4
10331040
10341041 - name: Make gradlew executable
10351042 run: chmod +x gradlew
10361043
1037- # ── 5. Reconstruct the keystore from the base64 secret ─────────────────
1044+ # ── 5. Generate auto-bumping version ───────────────────────────────────
1045+ #
1046+ # We read the base version from buildSrc/Version.kt (e.g. "2.0.0-alpha")
1047+ # and append a timestamp suffix so every push gets a unique version.
1048+ #
1049+ # Format: <base>.<YYYYMMDD>.<HHMM>
1050+ # Example: 2.0.0-alpha.20260405.1614
10381051 #
1039- # We write the keystore to a path that is outside the repo tree so it is
1040- # never accidentally committed. RUNNER_TEMP is a per-job temp directory
1041- # that GitHub Actions provides; it is automatically cleaned up afterward.
1052+ # The versionCode is derived from the Unix timestamp to guarantee it
1053+ # always increases (Android requires versionCode to be monotonically
1054+ # increasing for updates).
1055+ - name: Generate version
1056+ id: version
1057+ run: |
1058+ # Get base version from Gradle (e.g. "2.0.0-alpha.5")
1059+ BASE_VERSION=$(./gradlew -q printVersionName --no-configuration-cache 2>/dev/null || echo "")
1060+
1061+ # Strip trailing build number if present (e.g. "2.0.0-alpha.5" -> "2.0.0-alpha")
1062+ # This gives us the stable prefix to append our timestamp to.
1063+ BASE_PREFIX=$(echo "$BASE_VERSION" | sed -E 's/\.(alpha|beta|rc)\.([0-9]+)$/.\1/')
1064+
1065+ # If we couldn't parse it, fall back to commit-based version
1066+ if [ -z "$BASE_PREFIX" ]; then
1067+ BASE_PREFIX="0.0.0-dev"
1068+ fi
1069+
1070+ # Timestamp components (UTC)
1071+ DATE_PART=$(date -u +%Y%m%d)
1072+ TIME_PART=$(date -u +%H%M)
1073+
1074+ # Final version name: e.g. "2.0.0-alpha.20260405.1614"
1075+ VERSION_NAME="${BASE_PREFIX}.${DATE_PART}.${TIME_PART}"
1076+
1077+ # versionCode: Unix timestamp divided by 60 (fits in int32 until 2038+)
1078+ # This guarantees monotonic increase and stays well within Android's
1079+ # versionCode limit of 2,100,000,000.
1080+ EPOCH=$(date -u +%s)
1081+ VERSION_CODE=$((EPOCH / 60))
1082+
1083+ echo "name=$VERSION_NAME" >> "$GITHUB_OUTPUT"
1084+ echo "code=$VERSION_CODE" >> "$GITHUB_OUTPUT"
1085+ echo "Generated version: $VERSION_NAME (code: $VERSION_CODE)"
1086+
1087+ # ── 6. Reconstruct the keystore ────────────────────────────────────────
10421088 - name: Decode keystore
10431089 env:
10441090 ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
10451091 run: |
10461092 echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/android.keystore"
10471093
1048- # ── 6. Write keystore.properties ───────────────────────────────────────
1049- #
1050- # app/build.gradle.kts already contains logic to read this file:
1051- #
1052- # val keystorePropertiesFile = rootProject.file("keystore.properties")
1053- # if (keystorePropertiesFile.exists()) { ... }
1094+ # ── 7. Write keystore.properties ───────────────────────────────────────
10541095 #
1055- # By writing it here at build time (and NOT committing it to git), we
1056- # keep credentials out of version control entirely.
1057- #
1058- # Key names must match exactly what build.gradle.kts reads:
1059- # keystoreProperties["keyAlias"] → alias used in keytool -alias
1060- # keystoreProperties["keyPassword"] → -keypass value
1061- # keystoreProperties["storeFile"] → absolute path to .keystore
1062- # keystoreProperties["storePassword"] → -storepass value
1096+ # CRITICAL: No leading whitespace! Properties.load() will include spaces
1097+ # in key names if the lines are indented, causing silent signing failure.
10631098 - name: Write keystore.properties
10641099 env:
10651100 ANDROID_SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }}
10661101 run: |
1067- cat > keystore.properties <<EOF
1102+ cat > keystore.properties <<PROPS
10681103 keyAlias=myalias
10691104 keyPassword=${ANDROID_SIGNING_PASSWORD}
10701105 storeFile=$RUNNER_TEMP/android.keystore
10711106 storePassword=${ANDROID_SIGNING_PASSWORD}
1072- EOF
1073-
1074- # ── 7. Read the version name from buildSrc ────────────────────────────
1075- #
1076- # buildSrc/src/main/kotlin/Version.kt defines `currentVersion`.
1077- # Rather than parsing Kotlin, we ask Gradle itself for the value by
1078- # running a one-off task that prints it. This is the single source of
1079- # truth for the version and avoids duplication or drift.
1080- #
1081- # The `generic` flavor + `release` build type is the combination we care
1082- # about; its versionName comes straight from `baseVersionName` in
1083- # build.gradle.kts which equals currentVersion.name.
1084- - name: Read version name from Gradle
1085- id: version
1086- run: |
1087- VERSION=$(./gradlew -q printVersionName --no-configuration-cache 2>/dev/null || \
1088- ./gradlew -q :app:printVersionName 2>/dev/null || \
1089- grep -oP "Version\.\w+\([^)]+\)" buildSrc/src/main/kotlin/Version.kt | tail -1)
1090- # Fallback: derive it from the git SHA if Gradle task is unavailable
1091- if [ -z "$VERSION" ] || echo "$VERSION" | grep -q "Version\."; then
1092- VERSION="$(date -u +%Y%m%d)-$(git rev-parse --short HEAD)"
1093- fi
1094- echo "name=$VERSION" >> "$GITHUB_OUTPUT"
1095- echo "Resolved version: $VERSION"
1107+ PROPS
1108+ # Strip any leading whitespace from each line (heredoc indentation safety)
1109+ sed -i 's/^[[:space:]]*//' keystore.properties
1110+ echo "--- keystore.properties (keys only) ---"
1111+ cut -d'=' -f1 keystore.properties
10961112
1097- # ── 8. Build the signed release APK ───────────────────────────────────
1098- #
1099- # `assembleGenericRelease` builds the `generic` product flavor in release
1100- # mode. Because keystore.properties now exists, build.gradle.kts will
1101- # automatically apply the `githubPublish` signingConfig, so the APK that
1102- # comes out of Gradle is already signed and zipaligned — no separate
1103- # signing step needed.
1113+ # ── 8. Patch version for this build ────────────────────────────────────
11041114 #
1105- # The output file naming in build.gradle.kts is:
1106- # Seal-<versionName>-<variantName>.apk
1107- # With splits enabled (default) you get one APK per ABI plus a universal.
1115+ # We override the version at Gradle level using project properties.
1116+ # This avoids modifying Version.kt (which would dirty the working tree).
11081117 - name: Build signed release APK
1109- run: ./gradlew assembleGenericRelease
1118+ run: |
1119+ ./gradlew assembleGenericRelease \
1120+ -PversionNameOverride="${{ steps.version.outputs.name }}" \
1121+ -PversionCodeOverride="${{ steps.version.outputs.code }}"
11101122
1111- # ── 9. Collect the APKs ───────────── ───────────────────────────────────
1123+ # ── 9. Debug: show what was produced ───────────────────────────────────
11121124 - name: List produced APKs
1113- run: find app/build/outputs/apk -name "*.apk" | sort
1114-
1115- # ── 10. Create GitHub Release ─────────────────────────────────────────
1125+ run: |
1126+ echo "=== All APK files ==="
1127+ find app/build/outputs/apk -name "*.apk" -exec ls -lh {} \;
1128+ echo ""
1129+ echo "=== Verify signing ==="
1130+ for apk in $(find app/build/outputs/apk -name "*.apk"); do
1131+ echo "--- $apk ---"
1132+ $ANDROID_HOME/build-tools/$(ls $ANDROID_HOME/build-tools/ | tail -1)/apksigner verify --print-certs "$apk" 2>&1 || echo "NOT SIGNED"
1133+ done
1134+
1135+ # ── 10. Create GitHub Release ──────────────────────────────────────────
11161136 #
1117- # softprops/action-gh-release creates a tag + release on GitHub.
1118- # `prerelease: false` is explicit — this is a full release.
1119- # The tag is "v<versionName>" so it matches the convention used upstream.
1137+ # The glob pattern must match the actual Gradle output directory structure:
1138+ # app/build/outputs/apk/<flavor>/<buildType>/*.apk
1139+ # Which is: app/build/outputs/apk/generic/release/*.apk
11201140 #
1121- # We upload every APK produced; the glob `**/*.apk` covers all ABI
1122- # splits as well as the universal APK .
1141+ # NOT "genericRelease" — Gradle uses separate directories for flavor and
1142+ # build type .
11231143 - name: Create GitHub Release
11241144 uses: softprops/action-gh-release@v2
11251145 with:
11261146 tag_name: v${{ steps.version.outputs.name }}
11271147 name: Seal ${{ steps.version.outputs.name }}
11281148 prerelease: false
1129- generate_release_notes: true # auto-generates notes from commit messages
1130- files: app/build/outputs/apk/genericRelease/*.apk
1149+ generate_release_notes: true
1150+ files: |
1151+ app/build/outputs/apk/generic/release/*.apk
11311152 env:
11321153 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11331154
@@ -3170,7 +3191,7 @@ SIZE: 0 KB
31703191
31713192================================================================================
31723193FILE: app/build.gradle.kts
3173- SIZE: 6.52 KB
3194+ SIZE: 7.02 KB
31743195================================================================================
31753196
31763197@file:Suppress("UnstableApiUsage")
@@ -3197,8 +3218,22 @@ val abiFilterList = (properties["ABI_FILTERS"] as String).split(';')
31973218
31983219val abiCodes = mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2, "x86" to 3, "x86_64" to 4)
31993220
3200- val baseVersionName = currentVersion.name
3201- val currentVersionCode = currentVersion.code.toInt()
3221+ // ── Version resolution ────────────────────────────────────────────────────────
3222+ //
3223+ // CI passes -PversionNameOverride=... and -PversionCodeOverride=... to inject
3224+ // a timestamp-based auto-bumping version. When building locally (or if the
3225+ // properties aren't set), we fall back to the values from buildSrc/Version.kt.
3226+ val baseVersionName: String = if (project.hasProperty("versionNameOverride")) {
3227+ project.property("versionNameOverride") as String
3228+ } else {
3229+ currentVersion.name
3230+ }
3231+
3232+ val currentVersionCode: Int = if (project.hasProperty("versionCodeOverride")) {
3233+ (project.property("versionCodeOverride") as String).toInt()
3234+ } else {
3235+ currentVersion.code.toInt()
3236+ }
32023237
32033238android {
32043239 compileSdk = 35
@@ -3222,8 +3257,7 @@ android {
32223257 applicationId = "com.junkfood.seal"
32233258 minSdk = 24
32243259 targetSdk = 35
3225- versionCode = 200_000_150
3226- check(versionCode == currentVersionCode)
3260+ versionCode = currentVersionCode
32273261
32283262 versionName = baseVersionName
32293263 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -3375,15 +3409,13 @@ dependencies {
33753409// Usage: ./gradlew printVersionName
33763410// Output: 2.0.0-alpha.5 (whatever currentVersion.name resolves to)
33773411//
3378- // The task is intentionally registered on the :app project (this file) so it
3379- // has direct access to `baseVersionName` which is already resolved above .
3412+ // NOTE: This always prints the Version.kt value, NOT the CI override.
3413+ // The CI uses this as the "base" and then appends a timestamp .
33803414tasks.register("printVersionName") {
33813415 group = "versioning"
33823416 description = "Prints the current versionName to stdout for CI consumption."
3383- // We declare no inputs/outputs so Gradle never considers it up-to-date and
3384- // skips it — we always want a fresh print.
33853417 doLast {
3386- println(baseVersionName )
3418+ println(currentVersion.name )
33873419 }
33883420}
33893421
0 commit comments