gowalk-cicd 1.0.26 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +13 -0
- package/README.md +52 -8
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/action.yml +14 -4
- package/android-action/scripts/resolve_android.py +9 -7
- package/android-action/scripts/select_jdk.py +213 -0
- package/android-action/scripts/test_select_jdk.py +213 -0
- package/android-action/scripts/test_version_override.py +153 -0
- package/android-action/scripts/version_override.init.gradle +79 -0
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
- package/templates/deploy.yml +45 -1
package/CLAUDE.md
CHANGED
|
@@ -99,3 +99,16 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
99
99
|
- Version stamping is per-build-system: Flutter takes `--build-number`, Gradle
|
|
100
100
|
has no equivalent, so `set_gradle_version()` rewrites the literal in the
|
|
101
101
|
checkout the way the iOS action patches the pbxproj. Never commit that edit.
|
|
102
|
+
The rewrite is best-effort — a module that computes its versionCode has no
|
|
103
|
+
literal to rewrite — so the Gradle build additionally applies the resolved
|
|
104
|
+
values through the Variant API (`scripts/version_override.init.gradle`,
|
|
105
|
+
passed as `--init-script`). That override, not the rewrite, is what makes the
|
|
106
|
+
number authoritative. Do NOT reach for `-Pandroid.injected.version.code`:
|
|
107
|
+
those properties were removed in AGP 7.3 and are silently ignored by every
|
|
108
|
+
AGP 8.x app in the fleet.
|
|
109
|
+
- The JDK is chosen by `scripts/select_jdk.py`, called from the workflow before
|
|
110
|
+
`setup-java`. The `JAVA_VERSION` repo variable always wins; otherwise Flutter
|
|
111
|
+
gets 17 and native Gradle gets 21, except that a positively-detected Kotlin
|
|
112
|
+
Gradle plugin below 1.9.20 drops to 17 (older kapt cannot run under JDK 21).
|
|
113
|
+
Detection logic belongs in that script, where it is unit-tested — not inline
|
|
114
|
+
in the workflow template, where nothing can test it.
|
package/README.md
CHANGED
|
@@ -133,6 +133,33 @@ in Play Console; every later run detects API readiness and uploads to the
|
|
|
133
133
|
`internal` track automatically. Override the track or status with repository
|
|
134
134
|
variables `GOOGLE_PLAY_TRACK` and `GOOGLE_PLAY_STATUS`.
|
|
135
135
|
|
|
136
|
+
### Choosing the JDK
|
|
137
|
+
|
|
138
|
+
Flutter apps build on JDK 17 and native Gradle apps on JDK 21. Set the
|
|
139
|
+
repository variable `JAVA_VERSION` to pin a different one — it always wins, and
|
|
140
|
+
nothing second-guesses it:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
gh variable set JAVA_VERSION --body 17
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Left unset, the Android job keeps those defaults with one exception: a native
|
|
147
|
+
Gradle app whose Kotlin Gradle plugin is older than **1.9.20** gets JDK 17,
|
|
148
|
+
because 1.9.20 is the release that added JDK 21 support. Older `kapt` reaches
|
|
149
|
+
into `javac` internals that JDK 21 moved, and the build dies with
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
Execution failed for task ':app:kaptGenerateStubsReleaseKotlin'
|
|
153
|
+
> Internal compiler error. See log for more details
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
which names neither Kotlin nor the JDK. The job logs the JDK it picked and why.
|
|
157
|
+
|
|
158
|
+
The downgrade only fires when the Kotlin plugin version can actually be read
|
|
159
|
+
out of the repo — `build.gradle(.kts)`, `gradle.properties`,
|
|
160
|
+
`gradle/libs.versions.toml`, or a `buildSrc` version object. When it cannot, or
|
|
161
|
+
when the project explicitly targets Java 21 or newer, the app keeps JDK 21.
|
|
162
|
+
|
|
136
163
|
### Gradle daemon heap
|
|
137
164
|
|
|
138
165
|
A `gradle.properties` sized for a workstation does not fit a CI runner.
|
|
@@ -322,18 +349,35 @@ configuration for either:
|
|
|
322
349
|
| App module | `android/app` | the one module applying `com.android.application` (version-catalog aliases are resolved; a module named `app` wins a tie against a wear/automotive sibling) |
|
|
323
350
|
| Tests (`run-tests`) | `flutter analyze` + `flutter test` | `gradlew test` (all variants, all modules) |
|
|
324
351
|
| Build | `flutter build appbundle --release` | `<module>:bundleRelease` |
|
|
325
|
-
| Toolchain installed | Flutter + JDK 17 | JDK 21
|
|
352
|
+
| Toolchain installed | Flutter + JDK 17 | JDK 21, or 17 on an old Kotlin — see [Choosing the JDK](#choosing-the-jdk) |
|
|
326
353
|
|
|
327
354
|
A Flutter app also carries `android/settings.gradle`, so `pubspec.yaml` wins the
|
|
328
355
|
tie — Flutter apps must be built through the Flutter tool.
|
|
329
356
|
|
|
330
|
-
Gradle has no equivalent of `flutter build --build-number`, so the action
|
|
331
|
-
rewrites the literal `versionCode` (and `versionName`, when
|
|
332
|
-
set) in the app module's build file inside the CI checkout —
|
|
333
|
-
counterpart to the iOS action patching the `.pbxproj
|
|
334
|
-
committed back
|
|
335
|
-
|
|
336
|
-
|
|
357
|
+
Gradle has no equivalent of `flutter build --build-number`, so the action does
|
|
358
|
+
two things. It rewrites the literal `versionCode` (and `versionName`, when
|
|
359
|
+
`build-name` is set) in the app module's build file inside the CI checkout —
|
|
360
|
+
the Gradle counterpart to the iOS action patching the `.pbxproj`, never
|
|
361
|
+
committed back — and it applies the same values to the build through AGP's
|
|
362
|
+
Variant API, using a Gradle init script.
|
|
363
|
+
|
|
364
|
+
The second one is what makes the resolved `versionCode` authoritative. A module
|
|
365
|
+
that computes its version has no literal to rewrite:
|
|
366
|
+
|
|
367
|
+
```groovy
|
|
368
|
+
ext.code = 31
|
|
369
|
+
versionCode code
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Before, that build shipped whatever the checkout said and Play answered
|
|
373
|
+
`Version code 31 has already been used`, failing the upload and the
|
|
374
|
+
no-auto-submit retry alike — the number was baked into the bundle. The init
|
|
375
|
+
script overrides it whatever the build file does, and needs no cooperation from
|
|
376
|
+
the app.
|
|
377
|
+
|
|
378
|
+
> AGP's `-Pandroid.injected.version.code` looks like the obvious way to do this
|
|
379
|
+
> and is a silent no-op: the property was removed in AGP 7.3, so every AGP 8.x
|
|
380
|
+
> app ignores it without reporting anything.
|
|
337
381
|
|
|
338
382
|
### Build toolchain floors (Flutter apps)
|
|
339
383
|
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.28
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.28
|
|
@@ -106,9 +106,11 @@ runs:
|
|
|
106
106
|
flutter "${args[@]}"
|
|
107
107
|
|
|
108
108
|
# --- Native Gradle --------------------------------------------------
|
|
109
|
-
# resolve_android.py
|
|
110
|
-
#
|
|
111
|
-
#
|
|
109
|
+
# resolve_android.py rewrites versionCode/versionName in the module build
|
|
110
|
+
# file when it can find a literal to rewrite, and the build step below
|
|
111
|
+
# additionally overrides them through the Variant API so the resolved
|
|
112
|
+
# versionCode holds even when there is no literal — see
|
|
113
|
+
# scripts/version_override.init.gradle.
|
|
112
114
|
# `test` is the aggregate task ("Run unit tests for all variants") and is the
|
|
113
115
|
# only one guaranteed to exist: AGP only creates testXUnitTest tasks for the
|
|
114
116
|
# variants it enables, and AGP 9 disables the release unit test variant by
|
|
@@ -123,13 +125,21 @@ runs:
|
|
|
123
125
|
chmod +x ./gradlew
|
|
124
126
|
./gradlew test --console=plain --stacktrace
|
|
125
127
|
|
|
128
|
+
# ANDROID_BUILD_NUMBER is already in the environment: resolve_android.py
|
|
129
|
+
# exported it through GITHUB_ENV. ANDROID_BUILD_NAME is only set when the
|
|
130
|
+
# caller actually pinned a build-name — empty means "leave versionName
|
|
131
|
+
# alone", which the init script honours.
|
|
126
132
|
- name: Build release Android App Bundle with Gradle
|
|
127
133
|
if: ${{ steps.config.outputs.project_kind == 'gradle' }}
|
|
128
134
|
shell: bash
|
|
129
135
|
working-directory: ${{ steps.config.outputs.gradle_root || '.' }}
|
|
136
|
+
env:
|
|
137
|
+
ANDROID_BUILD_NAME: ${{ inputs.build-name }}
|
|
130
138
|
run: |
|
|
131
139
|
chmod +x ./gradlew
|
|
132
|
-
./gradlew "${ANDROID_GRADLE_MODULE}:bundleRelease"
|
|
140
|
+
./gradlew "${ANDROID_GRADLE_MODULE}:bundleRelease" \
|
|
141
|
+
--init-script "${{ github.action_path }}/scripts/version_override.init.gradle" \
|
|
142
|
+
--console=plain --stacktrace
|
|
133
143
|
|
|
134
144
|
# --- Shared ---------------------------------------------------------
|
|
135
145
|
- name: Locate Android App Bundle
|
|
@@ -108,9 +108,11 @@ def auto_build_number(package_name: str, service_account: Path) -> int:
|
|
|
108
108
|
def apply_gradle_version(project, build_number: int, build_name: str) -> None:
|
|
109
109
|
"""Write the resolved versionCode/versionName into the Gradle module.
|
|
110
110
|
|
|
111
|
-
A miss is a
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
A miss is not a problem: the build step applies the same values through the
|
|
112
|
+
Variant API (scripts/version_override.init.gradle), which needs no literal
|
|
113
|
+
to rewrite and is what makes the resolved versionCode authoritative. This
|
|
114
|
+
rewrite stays because it also fixes up anything that reads the build file
|
|
115
|
+
directly, and because it is what the fleet has been running.
|
|
114
116
|
"""
|
|
115
117
|
applied = set_gradle_version(project.build_file, build_number, build_name or None)
|
|
116
118
|
relative = project.build_file.name
|
|
@@ -118,13 +120,13 @@ def apply_gradle_version(project, build_number: int, build_name: str) -> None:
|
|
|
118
120
|
print(f"Pinned versionCode {build_number} in {project.module}/{relative}")
|
|
119
121
|
else:
|
|
120
122
|
print(
|
|
121
|
-
f"
|
|
122
|
-
f"
|
|
123
|
+
f"No literal versionCode in {project.module}/{relative}; the Gradle build "
|
|
124
|
+
f"will be given versionCode {build_number} through the Variant API instead"
|
|
123
125
|
)
|
|
124
126
|
if build_name and not applied["version_name"]:
|
|
125
127
|
print(
|
|
126
|
-
f"
|
|
127
|
-
f"
|
|
128
|
+
f"No literal versionName in {project.module}/{relative}; the Gradle build "
|
|
129
|
+
f"will be given versionName {build_name} through the Variant API instead"
|
|
128
130
|
)
|
|
129
131
|
|
|
130
132
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Choose the JDK the Android job installs, without breaking old toolchains.
|
|
3
|
+
|
|
4
|
+
The workflow used to hardcode the choice: 17 for Flutter, 21 for everything
|
|
5
|
+
else. That silently assumes every native Gradle app is new enough for JDK 21,
|
|
6
|
+
and an old one is not — Kotlin only learned to run kapt under JDK 21 in 1.9.20.
|
|
7
|
+
Older kapt reaches into javac internals that JDK 21 moved, so the build dies with
|
|
8
|
+
|
|
9
|
+
Execution failed for task ':app:kaptGenerateStubsReleaseKotlin'
|
|
10
|
+
> Internal compiler error. See log for more details
|
|
11
|
+
|
|
12
|
+
which names neither Kotlin nor the JDK. The same build is fine on 17.
|
|
13
|
+
|
|
14
|
+
Order of precedence:
|
|
15
|
+
|
|
16
|
+
1. the JAVA_VERSION repository variable, which always wins and is never
|
|
17
|
+
second-guessed — an app that needs a specific JDK says so and that is that;
|
|
18
|
+
2. Flutter apps: 17, unchanged;
|
|
19
|
+
3. native Gradle apps whose Kotlin Gradle plugin predates JDK 21 support: 17;
|
|
20
|
+
4. everything else: 21, unchanged.
|
|
21
|
+
|
|
22
|
+
Rule 3 only ever fires on a build that is already broken, and only when the
|
|
23
|
+
Kotlin plugin version is positively identified. A project that explicitly asks
|
|
24
|
+
for Java 21 or newer keeps 21 even on old Kotlin: downgrading it would break a
|
|
25
|
+
build that currently works, and "change nothing that works" outranks the
|
|
26
|
+
kapt repair.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
FLUTTER_JDK = "17"
|
|
36
|
+
GRADLE_JDK = "21"
|
|
37
|
+
LEGACY_KOTLIN_JDK = "17"
|
|
38
|
+
|
|
39
|
+
# Kotlin 1.9.20 is the first release that supports compiling *and* running kapt
|
|
40
|
+
# under JDK 21. Anything below it needs the older JDK.
|
|
41
|
+
KOTLIN_JDK21_FLOOR = (1, 9, 20)
|
|
42
|
+
|
|
43
|
+
VERSION = r"([0-9]+(?:\.[0-9]+){1,2})"
|
|
44
|
+
|
|
45
|
+
# Only the Kotlin *Gradle plugin* version decides this. Patterns deliberately
|
|
46
|
+
# require the plugin coordinate or a version-property assignment, so an
|
|
47
|
+
# unrelated Kotlin-adjacent dependency (kotlinx-coroutines 1.6.4, say) cannot
|
|
48
|
+
# masquerade as an old compiler and drag a modern repo down to JDK 17.
|
|
49
|
+
KOTLIN_PLUGIN_PATTERNS = (
|
|
50
|
+
# ext.kotlin_version = '1.6.21' | kotlinVersion = "1.9.0"
|
|
51
|
+
re.compile(rf"""kotlin[_-]?version\s*[=:]\s*["']{VERSION}["']""", re.IGNORECASE),
|
|
52
|
+
# classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21"
|
|
53
|
+
re.compile(rf"""kotlin-gradle-plugin["']?\s*:\s*["']?{VERSION}"""),
|
|
54
|
+
# id 'org.jetbrains.kotlin.android' version '1.9.0'
|
|
55
|
+
re.compile(rf"""org\.jetbrains\.kotlin[\w.]*["']\s*\)?\s*version\s*["']{VERSION}["']"""),
|
|
56
|
+
# kotlin("android") version "1.9.0"
|
|
57
|
+
re.compile(rf"""kotlin\(\s*["'][\w.-]+["']\s*\)\s*version\s*["']{VERSION}["']"""),
|
|
58
|
+
# gradle/libs.versions.toml: kotlin = "1.9.24"
|
|
59
|
+
# buildSrc Config.kt: const val kotlin = "1.3.61"
|
|
60
|
+
# `kotlinx = "..."` deliberately does not match: the coroutines/serialization
|
|
61
|
+
# libraries version independently of the compiler.
|
|
62
|
+
re.compile(
|
|
63
|
+
rf"""(?m)^\s*(?:(?:const|private|internal|public)\s+)*(?:va[lr]\s+)?"""
|
|
64
|
+
rf"""kotlin(?:[_-]?version)?\s*=\s*["']{VERSION}["']""",
|
|
65
|
+
re.IGNORECASE,
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# An explicit request for Java 21+. If the project asks for it, it gets it —
|
|
70
|
+
# see the module docstring.
|
|
71
|
+
JAVA_TARGET_PATTERNS = (
|
|
72
|
+
re.compile(r"jvmToolchain\s*\(\s*(?:JavaLanguageVersion\.of\s*\(\s*)?([0-9]{2})"),
|
|
73
|
+
re.compile(r"languageVersion\s*=\s*JavaLanguageVersion\.of\s*\(\s*([0-9]{2})"),
|
|
74
|
+
re.compile(r"JavaVersion\.VERSION_([0-9]{2})\b"),
|
|
75
|
+
re.compile(
|
|
76
|
+
r"""(?:sourceCompatibility|targetCompatibility|jvmTarget)\s*=?\s*["']?([0-9]{2})["']?"""
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Where a repo can declare its toolchain. Both roots are checked because the
|
|
81
|
+
# Gradle build lives at the repo root for a standalone Android app and under
|
|
82
|
+
# android/ for a repo that also holds another platform's sources — the same
|
|
83
|
+
# two candidates android_config.GRADLE_ROOT_CANDIDATES uses.
|
|
84
|
+
GRADLE_ROOTS = (".", "android")
|
|
85
|
+
ROOT_FILES = (
|
|
86
|
+
"build.gradle",
|
|
87
|
+
"build.gradle.kts",
|
|
88
|
+
"gradle.properties",
|
|
89
|
+
"gradle/libs.versions.toml",
|
|
90
|
+
)
|
|
91
|
+
MODULE_FILES = ("build.gradle", "build.gradle.kts")
|
|
92
|
+
BUILDSRC_FILE_LIMIT = 50
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_version(text: str) -> tuple[int, ...]:
|
|
96
|
+
"""'1.9' -> (1, 9, 0); '1.6.21' -> (1, 6, 21)."""
|
|
97
|
+
parts = [int(piece) for piece in text.split(".")]
|
|
98
|
+
while len(parts) < 3:
|
|
99
|
+
parts.append(0)
|
|
100
|
+
return tuple(parts[:3])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def candidate_files(workspace: Path) -> list[Path]:
|
|
104
|
+
"""Build files that could carry the Kotlin plugin or Java target."""
|
|
105
|
+
found: list[Path] = []
|
|
106
|
+
for relative in GRADLE_ROOTS:
|
|
107
|
+
root = workspace / relative
|
|
108
|
+
if not root.is_dir():
|
|
109
|
+
continue
|
|
110
|
+
for name in ROOT_FILES:
|
|
111
|
+
path = root / name
|
|
112
|
+
if path.is_file():
|
|
113
|
+
found.append(path)
|
|
114
|
+
# One level down catches :app and any sibling module. Deeper nesting is
|
|
115
|
+
# not worth walking: a module that pins its own Kotlin plugin version
|
|
116
|
+
# below the root's is vanishingly rare, and a miss just keeps today's
|
|
117
|
+
# behaviour.
|
|
118
|
+
for child in sorted(p for p in root.iterdir() if p.is_dir()):
|
|
119
|
+
if child.name.startswith(".") or child.name == "build":
|
|
120
|
+
continue
|
|
121
|
+
for name in MODULE_FILES:
|
|
122
|
+
path = child / name
|
|
123
|
+
if path.is_file():
|
|
124
|
+
found.append(path)
|
|
125
|
+
# A buildSrc repo keeps its versions in Kotlin source rather than in a
|
|
126
|
+
# build file (`object Config { val kotlin = "1.3.61" }`), so the build
|
|
127
|
+
# files above say nothing at all. Capped because this is the only place
|
|
128
|
+
# we walk a tree of unknown depth.
|
|
129
|
+
sources = sorted((root / "buildSrc" / "src").rglob("*.kt"))
|
|
130
|
+
found.extend(sources[:BUILDSRC_FILE_LIMIT])
|
|
131
|
+
return found
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _read(path: Path) -> str:
|
|
135
|
+
try:
|
|
136
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
137
|
+
except OSError:
|
|
138
|
+
return ""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def kotlin_plugin_version(workspace: Path) -> tuple[int, ...] | None:
|
|
142
|
+
"""Lowest Kotlin Gradle plugin version this repo declares, or None.
|
|
143
|
+
|
|
144
|
+
Lowest rather than highest on purpose: if any module still compiles with an
|
|
145
|
+
old Kotlin, that module's kapt is the one that will break under JDK 21.
|
|
146
|
+
"""
|
|
147
|
+
found: list[tuple[int, ...]] = []
|
|
148
|
+
for path in candidate_files(workspace):
|
|
149
|
+
text = _read(path)
|
|
150
|
+
for pattern in KOTLIN_PLUGIN_PATTERNS:
|
|
151
|
+
found.extend(parse_version(match) for match in pattern.findall(text))
|
|
152
|
+
return min(found) if found else None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def requests_java_21_or_newer(workspace: Path) -> bool:
|
|
156
|
+
for path in candidate_files(workspace):
|
|
157
|
+
text = _read(path)
|
|
158
|
+
for pattern in JAVA_TARGET_PATTERNS:
|
|
159
|
+
for match in pattern.findall(text):
|
|
160
|
+
if int(match) >= 21:
|
|
161
|
+
return True
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def select_jdk(workspace: Path, override: str, flutter: bool) -> tuple[str, str]:
|
|
166
|
+
"""Return (java-version, why) for actions/setup-java."""
|
|
167
|
+
override = (override or "").strip()
|
|
168
|
+
if override:
|
|
169
|
+
# setup-java takes a bare major version. Anything else is a typo at best,
|
|
170
|
+
# and a multi-line value would append extra keys to $GITHUB_OUTPUT and
|
|
171
|
+
# invent step outputs, so refuse it rather than pass it through.
|
|
172
|
+
if not override.isdigit():
|
|
173
|
+
print(
|
|
174
|
+
f"::warning::Ignoring JAVA_VERSION={override!r}; expected a major "
|
|
175
|
+
"version number such as 17 or 21"
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
return override, "JAVA_VERSION repository variable"
|
|
179
|
+
if flutter:
|
|
180
|
+
return FLUTTER_JDK, "Flutter app"
|
|
181
|
+
kotlin = kotlin_plugin_version(workspace)
|
|
182
|
+
if kotlin is None:
|
|
183
|
+
return GRADLE_JDK, "native Gradle app (Kotlin plugin version not declared in-repo)"
|
|
184
|
+
pretty = ".".join(str(part) for part in kotlin)
|
|
185
|
+
if kotlin >= KOTLIN_JDK21_FLOOR:
|
|
186
|
+
return GRADLE_JDK, f"native Gradle app on Kotlin {pretty}"
|
|
187
|
+
if requests_java_21_or_newer(workspace):
|
|
188
|
+
return GRADLE_JDK, (
|
|
189
|
+
f"native Gradle app on Kotlin {pretty}, which predates JDK 21 support, "
|
|
190
|
+
f"but the project explicitly targets Java 21+"
|
|
191
|
+
)
|
|
192
|
+
return LEGACY_KOTLIN_JDK, (
|
|
193
|
+
f"native Gradle app on Kotlin {pretty}; kapt only supports JDK 21 from "
|
|
194
|
+
f"1.9.20, so this build needs JDK {LEGACY_KOTLIN_JDK}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def main() -> None:
|
|
199
|
+
workspace = Path(os.environ.get("GITHUB_WORKSPACE", ".")).resolve()
|
|
200
|
+
version, why = select_jdk(
|
|
201
|
+
workspace,
|
|
202
|
+
os.environ.get("JAVA_VERSION", ""),
|
|
203
|
+
os.environ.get("FLUTTER_ENABLED", "") == "true",
|
|
204
|
+
)
|
|
205
|
+
print(f"Using JDK {version} — {why}")
|
|
206
|
+
output = os.environ.get("GITHUB_OUTPUT")
|
|
207
|
+
if output:
|
|
208
|
+
with Path(output).open("a") as stream:
|
|
209
|
+
stream.write(f"version={version}\n")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
main()
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from select_jdk import (
|
|
10
|
+
kotlin_plugin_version,
|
|
11
|
+
parse_version,
|
|
12
|
+
requests_java_21_or_newer,
|
|
13
|
+
select_jdk,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SelectJdkTest(unittest.TestCase):
|
|
18
|
+
def setUp(self) -> None:
|
|
19
|
+
self.temp = tempfile.TemporaryDirectory()
|
|
20
|
+
self.workspace = Path(self.temp.name)
|
|
21
|
+
|
|
22
|
+
def tearDown(self) -> None:
|
|
23
|
+
self.temp.cleanup()
|
|
24
|
+
|
|
25
|
+
def write(self, relative: str, text: str) -> Path:
|
|
26
|
+
path = self.workspace / relative
|
|
27
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
path.write_text(text)
|
|
29
|
+
return path
|
|
30
|
+
|
|
31
|
+
def version_of(self, flutter: bool = False, override: str = "") -> str:
|
|
32
|
+
return select_jdk(self.workspace, override, flutter)[0]
|
|
33
|
+
|
|
34
|
+
# --- the explicit override ------------------------------------------
|
|
35
|
+
def test_repo_variable_beats_everything(self) -> None:
|
|
36
|
+
self.write("build.gradle", "ext.kotlin_version = '1.6.21'")
|
|
37
|
+
|
|
38
|
+
for flutter in (True, False):
|
|
39
|
+
self.assertEqual(self.version_of(flutter=flutter, override="11"), "11")
|
|
40
|
+
|
|
41
|
+
def test_override_is_trimmed_and_blank_is_ignored(self) -> None:
|
|
42
|
+
self.assertEqual(self.version_of(override=" 23 "), "23")
|
|
43
|
+
self.assertEqual(self.version_of(flutter=True, override=" "), "17")
|
|
44
|
+
|
|
45
|
+
def test_malformed_override_is_refused_not_passed_through(self) -> None:
|
|
46
|
+
# setup-java wants a bare major version. A multi-line value would append
|
|
47
|
+
# extra `key=value` lines to $GITHUB_OUTPUT and invent step outputs, so a
|
|
48
|
+
# value that is not all digits must fall through to the normal choice
|
|
49
|
+
# rather than reach the workflow.
|
|
50
|
+
self.write("build.gradle", "ext.kotlin_version = '2.0.21'")
|
|
51
|
+
|
|
52
|
+
for bad in ("17\nEVIL=1", "seventeen", "17;rm -rf /", "17.0.2"):
|
|
53
|
+
with self.subTest(override=bad):
|
|
54
|
+
self.assertEqual(self.version_of(override=bad), "21")
|
|
55
|
+
|
|
56
|
+
def test_override_reason_names_the_variable(self) -> None:
|
|
57
|
+
_, why = select_jdk(self.workspace, "17", flutter=False)
|
|
58
|
+
|
|
59
|
+
self.assertIn("JAVA_VERSION", why)
|
|
60
|
+
|
|
61
|
+
# --- today's defaults, which must not move --------------------------
|
|
62
|
+
def test_flutter_still_gets_17(self) -> None:
|
|
63
|
+
self.assertEqual(self.version_of(flutter=True), "17")
|
|
64
|
+
|
|
65
|
+
def test_native_gradle_still_gets_21_by_default(self) -> None:
|
|
66
|
+
self.assertEqual(self.version_of(), "21")
|
|
67
|
+
|
|
68
|
+
def test_modern_kotlin_still_gets_21(self) -> None:
|
|
69
|
+
self.write("build.gradle", "ext.kotlin_version = '2.0.21'")
|
|
70
|
+
|
|
71
|
+
self.assertEqual(self.version_of(), "21")
|
|
72
|
+
|
|
73
|
+
def test_kotlin_exactly_at_the_floor_gets_21(self) -> None:
|
|
74
|
+
self.write("build.gradle", "ext.kotlin_version = '1.9.20'")
|
|
75
|
+
|
|
76
|
+
self.assertEqual(self.version_of(), "21")
|
|
77
|
+
|
|
78
|
+
def test_unresolvable_kotlin_version_keeps_21(self) -> None:
|
|
79
|
+
# The overwhelmingly common Groovy idiom: the classpath interpolates a
|
|
80
|
+
# property, so the coordinate itself carries no digits.
|
|
81
|
+
self.write(
|
|
82
|
+
"build.gradle",
|
|
83
|
+
'classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"',
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
self.assertEqual(self.version_of(), "21")
|
|
87
|
+
|
|
88
|
+
def test_kotlinx_dependencies_do_not_masquerade_as_the_plugin(self) -> None:
|
|
89
|
+
self.write(
|
|
90
|
+
"app/build.gradle",
|
|
91
|
+
'implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"\n'
|
|
92
|
+
'implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"\n',
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self.assertIsNone(kotlin_plugin_version(self.workspace))
|
|
96
|
+
self.assertEqual(self.version_of(), "21")
|
|
97
|
+
|
|
98
|
+
# --- the repair -----------------------------------------------------
|
|
99
|
+
def test_old_kotlin_drops_to_17(self) -> None:
|
|
100
|
+
# gowalk-public/android-taxi-deals: kapt dies with an "Internal compiler
|
|
101
|
+
# error" on JDK 21 and builds fine on 17.
|
|
102
|
+
self.write("build.gradle", "ext.kotlin_version = '1.6.21'")
|
|
103
|
+
|
|
104
|
+
version, why = select_jdk(self.workspace, "", flutter=False)
|
|
105
|
+
self.assertEqual(version, "17")
|
|
106
|
+
self.assertIn("1.6.21", why)
|
|
107
|
+
|
|
108
|
+
def test_old_kotlin_detected_in_the_android_subdirectory(self) -> None:
|
|
109
|
+
self.write("android/build.gradle", "ext.kotlin_version = '1.7.10'")
|
|
110
|
+
|
|
111
|
+
self.assertEqual(self.version_of(), "17")
|
|
112
|
+
|
|
113
|
+
def test_old_kotlin_in_a_module_wins_over_a_modern_root(self) -> None:
|
|
114
|
+
self.write("build.gradle", "ext.kotlin_version = '2.0.21'")
|
|
115
|
+
self.write("legacy/build.gradle", "ext.kotlin_version = '1.5.31'")
|
|
116
|
+
|
|
117
|
+
self.assertEqual(kotlin_plugin_version(self.workspace), (1, 5, 31))
|
|
118
|
+
self.assertEqual(self.version_of(), "17")
|
|
119
|
+
|
|
120
|
+
def test_a_project_that_asks_for_java_21_keeps_21(self) -> None:
|
|
121
|
+
# Downgrading this one would break a build that works today.
|
|
122
|
+
self.write("build.gradle", "ext.kotlin_version = '1.8.22'")
|
|
123
|
+
self.write("app/build.gradle", "kotlin { jvmToolchain(21) }")
|
|
124
|
+
|
|
125
|
+
version, why = select_jdk(self.workspace, "", flutter=False)
|
|
126
|
+
self.assertEqual(version, "21")
|
|
127
|
+
self.assertIn("explicitly targets Java 21+", why)
|
|
128
|
+
|
|
129
|
+
def test_java_17_target_does_not_block_the_downgrade(self) -> None:
|
|
130
|
+
self.write("build.gradle", "ext.kotlin_version = '1.8.22'")
|
|
131
|
+
self.write(
|
|
132
|
+
"app/build.gradle",
|
|
133
|
+
"compileOptions {\n"
|
|
134
|
+
" sourceCompatibility JavaVersion.VERSION_17\n"
|
|
135
|
+
" targetCompatibility JavaVersion.VERSION_17\n"
|
|
136
|
+
"}\n",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
self.assertFalse(requests_java_21_or_newer(self.workspace))
|
|
140
|
+
self.assertEqual(self.version_of(), "17")
|
|
141
|
+
|
|
142
|
+
def test_legacy_java_8_target_does_not_block_the_downgrade(self) -> None:
|
|
143
|
+
self.write("build.gradle", "ext.kotlin_version = '1.6.21'")
|
|
144
|
+
self.write(
|
|
145
|
+
"app/build.gradle",
|
|
146
|
+
"compileOptions {\n"
|
|
147
|
+
" sourceCompatibility JavaVersion.VERSION_1_8\n"
|
|
148
|
+
"}\n"
|
|
149
|
+
'kotlinOptions { jvmTarget = "1.8" }\n',
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
self.assertFalse(requests_java_21_or_newer(self.workspace))
|
|
153
|
+
self.assertEqual(self.version_of(), "17")
|
|
154
|
+
|
|
155
|
+
# --- where the version can be declared ------------------------------
|
|
156
|
+
def test_reads_the_version_catalog(self) -> None:
|
|
157
|
+
self.write(
|
|
158
|
+
"gradle/libs.versions.toml",
|
|
159
|
+
"[versions]\nkotlin = \"1.7.22\"\nagp = \"8.1.0\"\n",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self.assertEqual(kotlin_plugin_version(self.workspace), (1, 7, 22))
|
|
163
|
+
|
|
164
|
+
def test_reads_the_plugins_dsl(self) -> None:
|
|
165
|
+
self.write(
|
|
166
|
+
"build.gradle",
|
|
167
|
+
"plugins { id 'org.jetbrains.kotlin.android' version '1.8.10' apply false }",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
self.assertEqual(kotlin_plugin_version(self.workspace), (1, 8, 10))
|
|
171
|
+
|
|
172
|
+
def test_reads_the_kotlin_dsl_accessor(self) -> None:
|
|
173
|
+
self.write("build.gradle.kts", 'plugins { kotlin("android") version "1.7.20" }')
|
|
174
|
+
|
|
175
|
+
self.assertEqual(kotlin_plugin_version(self.workspace), (1, 7, 20))
|
|
176
|
+
|
|
177
|
+
def test_reads_a_buildsrc_version_object(self) -> None:
|
|
178
|
+
# gowalk-public/dating-android keeps its versions in Kotlin source, so
|
|
179
|
+
# every build file interpolates `Config.Versions.kotlin` and declares
|
|
180
|
+
# no digits at all.
|
|
181
|
+
self.write(
|
|
182
|
+
"build.gradle.kts",
|
|
183
|
+
'classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Config.Versions.kotlin}")',
|
|
184
|
+
)
|
|
185
|
+
self.write(
|
|
186
|
+
"buildSrc/src/main/java/Config.kt",
|
|
187
|
+
"object Config {\n"
|
|
188
|
+
" object Versions {\n"
|
|
189
|
+
' val kotlin = "1.3.61"\n'
|
|
190
|
+
' val kotlinx = "1.3.3"\n'
|
|
191
|
+
" }\n"
|
|
192
|
+
"}\n",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# 1.3.3 is kotlinx, not the compiler, and must not be mistaken for it.
|
|
196
|
+
self.assertEqual(kotlin_plugin_version(self.workspace), (1, 3, 61))
|
|
197
|
+
self.assertEqual(self.version_of(), "17")
|
|
198
|
+
|
|
199
|
+
def test_reads_a_literal_classpath_coordinate(self) -> None:
|
|
200
|
+
self.write(
|
|
201
|
+
"build.gradle",
|
|
202
|
+
'classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"',
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self.assertEqual(kotlin_plugin_version(self.workspace), (1, 6, 10))
|
|
206
|
+
|
|
207
|
+
def test_two_component_versions_normalise(self) -> None:
|
|
208
|
+
self.assertEqual(parse_version("1.9"), (1, 9, 0))
|
|
209
|
+
self.assertEqual(parse_version("1.9.20"), (1, 9, 20))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
unittest.main()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The resolved versionCode must actually reach a native Gradle build.
|
|
3
|
+
|
|
4
|
+
The plugin resolves a versionCode that cannot collide with what Play already
|
|
5
|
+
holds, but a native app declares its own in build.gradle. resolve_android.py
|
|
6
|
+
rewrites that literal when there is one; gowalk-public/carsharing-android has
|
|
7
|
+
|
|
8
|
+
ext.code = 31
|
|
9
|
+
versionCode code
|
|
10
|
+
|
|
11
|
+
which has no literal to rewrite, so the bundle shipped 31 and Play rejected it
|
|
12
|
+
with "Version code 31 has already been used" — including the no-auto-submit
|
|
13
|
+
retry, because the number was baked into the bundle.
|
|
14
|
+
|
|
15
|
+
The fix is an init script applied to the Gradle invocation. These tests pin the
|
|
16
|
+
wiring, since nothing else in the suite can run Gradle.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
import unittest
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
ACTION_DIR = Path(__file__).resolve().parent.parent
|
|
26
|
+
ACTION_YML = ACTION_DIR / "action.yml"
|
|
27
|
+
INIT_SCRIPT = ACTION_DIR / "scripts" / "version_override.init.gradle"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def gradle_build_step() -> str:
|
|
31
|
+
"""The `Build release Android App Bundle with Gradle` step, as raw text."""
|
|
32
|
+
text = ACTION_YML.read_text()
|
|
33
|
+
start = text.index("- name: Build release Android App Bundle with Gradle")
|
|
34
|
+
rest = text[start + 1 :]
|
|
35
|
+
end = rest.find("\n - name:")
|
|
36
|
+
return rest if end == -1 else rest[:end]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class VersionOverrideWiringTest(unittest.TestCase):
|
|
40
|
+
def test_the_init_script_ships_with_the_action(self) -> None:
|
|
41
|
+
self.assertTrue(INIT_SCRIPT.is_file())
|
|
42
|
+
|
|
43
|
+
def test_the_gradle_build_applies_the_init_script(self) -> None:
|
|
44
|
+
step = gradle_build_step()
|
|
45
|
+
|
|
46
|
+
self.assertIn("--init-script", step)
|
|
47
|
+
self.assertIn("scripts/version_override.init.gradle", step)
|
|
48
|
+
# github.action_path, not a relative path: the step runs with
|
|
49
|
+
# working-directory set to the Gradle root, which may be android/.
|
|
50
|
+
self.assertIn("${{ github.action_path }}", step)
|
|
51
|
+
|
|
52
|
+
def test_the_gradle_build_passes_the_resolved_build_name(self) -> None:
|
|
53
|
+
step = gradle_build_step()
|
|
54
|
+
|
|
55
|
+
self.assertIn("ANDROID_BUILD_NAME: ${{ inputs.build-name }}", step)
|
|
56
|
+
|
|
57
|
+
def test_the_flutter_build_is_left_alone(self) -> None:
|
|
58
|
+
# The Flutter path already honours the resolved number through
|
|
59
|
+
# --build-number and must not be handed a Gradle init script.
|
|
60
|
+
text = ACTION_YML.read_text()
|
|
61
|
+
start = text.index("- name: Build release Android App Bundle\n")
|
|
62
|
+
flutter_step = text[start : text.index("# --- Native Gradle", start)]
|
|
63
|
+
|
|
64
|
+
self.assertIn("--build-number", flutter_step)
|
|
65
|
+
self.assertNotIn("--init-script", flutter_step)
|
|
66
|
+
|
|
67
|
+
def test_only_the_gradle_build_gets_the_init_script(self) -> None:
|
|
68
|
+
# Notably not `./gradlew test`: a unit-test run has no versionCode to
|
|
69
|
+
# get wrong, and the narrower the override's blast radius the better.
|
|
70
|
+
self.assertEqual(ACTION_YML.read_text().count("--init-script"), 1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class InitScriptTest(unittest.TestCase):
|
|
74
|
+
"""Guard rails that keep the override from touching a build it should not."""
|
|
75
|
+
|
|
76
|
+
def setUp(self) -> None:
|
|
77
|
+
self.source = INIT_SCRIPT.read_text()
|
|
78
|
+
|
|
79
|
+
def test_reads_the_values_the_action_exports(self) -> None:
|
|
80
|
+
self.assertIn("ANDROID_BUILD_NUMBER", self.source)
|
|
81
|
+
self.assertIn("ANDROID_BUILD_NAME", self.source)
|
|
82
|
+
|
|
83
|
+
def test_only_touches_application_modules(self) -> None:
|
|
84
|
+
self.assertIn("com.android.application", self.source)
|
|
85
|
+
|
|
86
|
+
def test_does_nothing_when_neither_value_is_set(self) -> None:
|
|
87
|
+
# An unset variable means "no opinion" — the project's own value stands.
|
|
88
|
+
self.assertRegex(
|
|
89
|
+
self.source, r"if \(versionCode != null \|\| versionName != null\)"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def test_a_bad_build_number_warns_instead_of_failing_the_build(self) -> None:
|
|
93
|
+
self.assertIn("NumberFormatException", self.source)
|
|
94
|
+
self.assertIn("::warning::", self.source)
|
|
95
|
+
|
|
96
|
+
def test_supports_agp_before_and_after_the_variant_api(self) -> None:
|
|
97
|
+
self.assertIn("androidComponents", self.source)
|
|
98
|
+
self.assertIn("versionCodeOverride", self.source)
|
|
99
|
+
|
|
100
|
+
def test_does_not_use_the_removed_injected_properties(self) -> None:
|
|
101
|
+
# android.injected.version.code / .name were removed in AGP 7.3 and are
|
|
102
|
+
# silently ignored by every AGP 8.x app in the fleet. Reaching for them
|
|
103
|
+
# would look like a fix and change nothing. The init script's header
|
|
104
|
+
# explains all this, so only the code is checked here.
|
|
105
|
+
code = "\n".join(
|
|
106
|
+
line
|
|
107
|
+
for line in self.source.splitlines()
|
|
108
|
+
if not line.lstrip().startswith("//")
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self.assertNotIn("android.injected.version", code)
|
|
112
|
+
self.assertNotIn("android.injected.version", gradle_build_step())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class NonLiteralVersionCodeTest(unittest.TestCase):
|
|
116
|
+
"""The exact shape that defeated the source rewrite."""
|
|
117
|
+
|
|
118
|
+
def test_the_carsharing_build_file_has_no_literal_to_rewrite(self) -> None:
|
|
119
|
+
import tempfile
|
|
120
|
+
|
|
121
|
+
from android_config import set_gradle_version
|
|
122
|
+
|
|
123
|
+
with tempfile.TemporaryDirectory() as temp:
|
|
124
|
+
build_file = Path(temp) / "build.gradle"
|
|
125
|
+
build_file.write_text(
|
|
126
|
+
"android {\n"
|
|
127
|
+
" defaultConfig {\n"
|
|
128
|
+
" ext.code = 31\n"
|
|
129
|
+
" versionCode code\n"
|
|
130
|
+
' versionName "1.${code}"\n'
|
|
131
|
+
" }\n"
|
|
132
|
+
"}\n"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
applied = set_gradle_version(build_file, 507)
|
|
136
|
+
|
|
137
|
+
# Nothing to rewrite, so the checkout still says 31 — which is
|
|
138
|
+
# precisely why the build needs the Variant API override.
|
|
139
|
+
self.assertFalse(applied["version_code"])
|
|
140
|
+
self.assertIn("ext.code = 31", build_file.read_text())
|
|
141
|
+
|
|
142
|
+
def test_resolve_android_no_longer_calls_that_a_warning(self) -> None:
|
|
143
|
+
# It used to emit ::warning::, which annotated the run as a problem
|
|
144
|
+
# while the real override was about to succeed.
|
|
145
|
+
source = (ACTION_DIR / "scripts" / "resolve_android.py").read_text()
|
|
146
|
+
body = source[source.index("def apply_gradle_version") : source.index("def flutter_root")]
|
|
147
|
+
|
|
148
|
+
self.assertNotIn("::warning::", body)
|
|
149
|
+
self.assertIn("Variant API", body)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == "__main__":
|
|
153
|
+
unittest.main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Makes the versionCode this action resolved authoritative for native Gradle
|
|
2
|
+
// builds. Applied with --init-script by the "Build release Android App Bundle
|
|
3
|
+
// with Gradle" step; never applied to a Flutter build, which takes the number
|
|
4
|
+
// through `flutter build --build-number` and already works.
|
|
5
|
+
//
|
|
6
|
+
// Why an init script rather than AGP's injected properties
|
|
7
|
+
// -------------------------------------------------------
|
|
8
|
+
// The obvious answer looks like
|
|
9
|
+
// ./gradlew ... -Pandroid.injected.version.code=<N>
|
|
10
|
+
// and it is a silent no-op on every app in this fleet. Those properties were
|
|
11
|
+
// removed in AGP 7.3 — verified by disassembling IntegerOption/StringOption
|
|
12
|
+
// across cached AGP releases: android.injected.version.code and
|
|
13
|
+
// android.injected.version.name are present in 4.1 and 7.1 and gone from 7.3
|
|
14
|
+
// onward. Passing them to an AGP 8.x build changes nothing and reports nothing,
|
|
15
|
+
// so the stale versionCode still ships and Play still answers
|
|
16
|
+
// "Version code N has already been used".
|
|
17
|
+
//
|
|
18
|
+
// The supported override is the Variant API, which is what this does. It needs
|
|
19
|
+
// no cooperation from the app and edits none of its sources, so it works
|
|
20
|
+
// whatever the build file does — including the indirection that defeats
|
|
21
|
+
// resolve_android.py's literal rewrite:
|
|
22
|
+
// ext.code = 31
|
|
23
|
+
// versionCode code
|
|
24
|
+
//
|
|
25
|
+
// An unset variable means "no opinion": the project's own value stands. That is
|
|
26
|
+
// what keeps versionName alone unless a build-name was actually resolved.
|
|
27
|
+
|
|
28
|
+
def readSetting = { String name ->
|
|
29
|
+
def value = System.getenv(name)
|
|
30
|
+
return value?.trim() ? value.trim() : null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def versionName = readSetting('ANDROID_BUILD_NAME')
|
|
34
|
+
def versionCodeText = readSetting('ANDROID_BUILD_NUMBER')
|
|
35
|
+
|
|
36
|
+
Integer versionCode = null
|
|
37
|
+
if (versionCodeText != null) {
|
|
38
|
+
try {
|
|
39
|
+
versionCode = Integer.parseInt(versionCodeText)
|
|
40
|
+
} catch (NumberFormatException ignored) {
|
|
41
|
+
println "::warning::ANDROID_BUILD_NUMBER is not an integer (${versionCodeText}); " +
|
|
42
|
+
"leaving the project's own versionCode in place"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (versionCode != null || versionName != null) {
|
|
47
|
+
gradle.beforeProject { project ->
|
|
48
|
+
project.plugins.withId('com.android.application') {
|
|
49
|
+
// AGP 7.0+ — the current Variant API.
|
|
50
|
+
def components = project.extensions.findByName('androidComponents')
|
|
51
|
+
if (components != null) {
|
|
52
|
+
components.onVariants(components.selector().all()) { variant ->
|
|
53
|
+
variant.outputs.each { output ->
|
|
54
|
+
if (versionCode != null) output.versionCode.set(versionCode)
|
|
55
|
+
if (versionName != null) output.versionName.set(versionName)
|
|
56
|
+
}
|
|
57
|
+
println "${project.path} ${variant.name}: versionCode=" +
|
|
58
|
+
"${versionCode ?: 'unchanged'} versionName=${versionName ?: 'unchanged'}"
|
|
59
|
+
}
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
// AGP < 7.0 — no androidComponents extension exists yet.
|
|
63
|
+
def android = project.extensions.findByName('android')
|
|
64
|
+
if (android == null) {
|
|
65
|
+
println "::warning::${project.path} applies com.android.application but exposes " +
|
|
66
|
+
"neither androidComponents nor android; versionCode not overridden"
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
android.applicationVariants.all { variant ->
|
|
70
|
+
variant.outputs.each { output ->
|
|
71
|
+
if (versionCode != null) output.versionCodeOverride = versionCode
|
|
72
|
+
if (versionName != null) output.versionNameOverride = versionName
|
|
73
|
+
}
|
|
74
|
+
println "${project.path} ${variant.name}: versionCode=" +
|
|
75
|
+
"${versionCode ?: 'unchanged'} versionName=${versionName ?: 'unchanged'}"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.28
|
package/package.json
CHANGED
package/templates/deploy.yml
CHANGED
|
@@ -59,6 +59,23 @@ jobs:
|
|
|
59
59
|
~/Library/Caches/CocoaPods
|
|
60
60
|
key: pods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock', 'pubspec.lock') }}
|
|
61
61
|
restore-keys: pods-${{ runner.os }}-
|
|
62
|
+
# Same as the android job: a private git dependency (e.g. the pomogator
|
|
63
|
+
# onboarding SDK) is cloned by `flutter pub get` below. The runner has no
|
|
64
|
+
# SSH key, so an https url only resolves once this token rewrite is in
|
|
65
|
+
# place. No secret => no-op (the dep must then carry its own credentials).
|
|
66
|
+
- name: Authenticate private git dependencies
|
|
67
|
+
if: ${{ steps.flutter.outputs.enabled == 'true' }}
|
|
68
|
+
shell: bash
|
|
69
|
+
env:
|
|
70
|
+
GIT_PRIVATE_TOKEN: ${{ secrets.GIT_PRIVATE_TOKEN }}
|
|
71
|
+
run: |
|
|
72
|
+
if [ -z "${GIT_PRIVATE_TOKEN:-}" ]; then
|
|
73
|
+
echo "No GIT_PRIVATE_TOKEN secret; private git dependencies must carry their own credentials."
|
|
74
|
+
exit 0
|
|
75
|
+
fi
|
|
76
|
+
echo "::add-mask::$GIT_PRIVATE_TOKEN"
|
|
77
|
+
git config --global url."https://x-access-token:${GIT_PRIVATE_TOKEN}@github.com/".insteadOf "https://github.com/"
|
|
78
|
+
echo "Configured authenticated github.com remotes for pub."
|
|
62
79
|
- name: Prepare Flutter iOS project
|
|
63
80
|
if: ${{ steps.flutter.outputs.enabled == 'true' }}
|
|
64
81
|
shell: bash
|
|
@@ -146,12 +163,39 @@ jobs:
|
|
|
146
163
|
/usr/local/lib/node_modules 2>/dev/null || true
|
|
147
164
|
sudo docker image prune --all --force >/dev/null 2>&1 || true
|
|
148
165
|
echo "After:"; df -h / | tail -1
|
|
166
|
+
# The JDK used to be hardcoded here — 17 for Flutter, 21 for everything
|
|
167
|
+
# else — which assumes every native Gradle app is new enough for 21. An
|
|
168
|
+
# app on an old Kotlin is not: kapt reaches into javac internals that JDK
|
|
169
|
+
# 21 moved, and the build dies with "Internal compiler error" out of
|
|
170
|
+
# kaptGenerateStubsReleaseKotlin, naming neither Kotlin nor the JDK.
|
|
171
|
+
#
|
|
172
|
+
# Repository variable JAVA_VERSION pins the JDK for this repo and always
|
|
173
|
+
# wins — set it (e.g. '17') when an app needs a specific JDK and nothing
|
|
174
|
+
# should second-guess it. Left unset, select_jdk.py keeps today's choice
|
|
175
|
+
# and only drops a native Gradle app to 17 when it can positively read a
|
|
176
|
+
# Kotlin Gradle plugin older than 1.9.20, which is the release that added
|
|
177
|
+
# JDK 21 support. See android-action/scripts/select_jdk.py.
|
|
178
|
+
- name: Choose the JDK for this build
|
|
179
|
+
id: jdk
|
|
180
|
+
if: ${{ steps.mode.outputs.mode == 'local' }}
|
|
181
|
+
shell: bash
|
|
182
|
+
env:
|
|
183
|
+
JAVA_VERSION: ${{ vars.JAVA_VERSION }}
|
|
184
|
+
FLUTTER_ENABLED: ${{ steps.flutter.outputs.enabled }}
|
|
185
|
+
run: |
|
|
186
|
+
# Never let a chooser bug take down every Android job: fall back to the
|
|
187
|
+
# JDK this workflow used before the chooser existed.
|
|
188
|
+
if ! python3 .github/actions/android-app/scripts/select_jdk.py; then
|
|
189
|
+
echo "::warning::select_jdk.py failed; falling back"
|
|
190
|
+
if [ "${FLUTTER_ENABLED}" = "true" ]; then fallback=17; else fallback=21; fi
|
|
191
|
+
echo "version=${fallback}" >> "$GITHUB_OUTPUT"
|
|
192
|
+
fi
|
|
149
193
|
- name: Set up Java
|
|
150
194
|
if: ${{ steps.mode.outputs.mode == 'local' }}
|
|
151
195
|
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
|
|
152
196
|
with:
|
|
153
197
|
distribution: temurin
|
|
154
|
-
java-version: ${{ steps.
|
|
198
|
+
java-version: ${{ steps.jdk.outputs.version }}
|
|
155
199
|
cache: gradle
|
|
156
200
|
- name: Set up Flutter
|
|
157
201
|
if: ${{ steps.mode.outputs.mode == 'local' && steps.flutter.outputs.enabled == 'true' }}
|