gowalk-cicd 1.0.15 → 1.0.17
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/README.md +8 -4
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/scripts/gradle_wrapper.py +119 -5
- package/android-action/scripts/resolve_android.py +7 -7
- package/android-action/scripts/test_gradle_wrapper.py +137 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -205,7 +205,7 @@ committed back. If the module computes its `versionCode` instead of declaring a
|
|
|
205
205
|
literal, the action emits a `::warning::` and leaves the project's own value
|
|
206
206
|
alone.
|
|
207
207
|
|
|
208
|
-
###
|
|
208
|
+
### Build toolchain floors (Flutter apps)
|
|
209
209
|
|
|
210
210
|
The Flutter Gradle plugin refuses to apply to a project whose wrapper is older
|
|
211
211
|
than the SDK's floor:
|
|
@@ -220,9 +220,13 @@ release cadence rather than the app's — every app in a fleet breaks on the sam
|
|
|
220
220
|
day, long after the last commit that could have anticipated it. Before building,
|
|
221
221
|
the action reads the floor out of the runner's Flutter SDK and, when the
|
|
222
222
|
project's `gradle-wrapper.properties` is below it, rewrites `distributionUrl`
|
|
223
|
-
**in the checkout** with a `::warning::`.
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
**in the checkout** with a `::warning::`. The Android Gradle Plugin and Kotlin
|
|
224
|
+
Gradle Plugin versions declared in `android/settings.gradle` (or the older
|
|
225
|
+
`buildscript { classpath ... }` / `ext.kotlin_version` form) are raised the same
|
|
226
|
+
way against Flutter's `errorAGPVersion` / `errorKGPVersion`. Same policy as the
|
|
227
|
+
versionCode rewrite: the edits are never committed back, so the project keeps
|
|
228
|
+
whatever versions its authors chose. Commit the bumps yourself to silence the
|
|
229
|
+
warnings.
|
|
226
230
|
|
|
227
231
|
(The rewritten name is verified against `services.gradle.org` because Gradle's
|
|
228
232
|
own naming is inconsistent across majors — `gradle-8.14-all.zip` but
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.17
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.17
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Raise a Flutter app's
|
|
2
|
+
"""Raise a Flutter app's build toolchain to the versions its Flutter SDK requires.
|
|
3
3
|
|
|
4
4
|
The Flutter Gradle plugin refuses to apply when the project's wrapper is older
|
|
5
5
|
than the SDK's floor:
|
|
@@ -25,7 +25,15 @@ from pathlib import Path
|
|
|
25
25
|
# `stable` moves the floor over time, so read it from the SDK rather than
|
|
26
26
|
# hardcoding a number this file would have to chase.
|
|
27
27
|
_CHECKER = Path("packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt")
|
|
28
|
-
|
|
28
|
+
# The same file declares a floor for each of the three: Gradle, the Android
|
|
29
|
+
# Gradle Plugin and the Kotlin Gradle Plugin. All three are hard errors, and
|
|
30
|
+
# all three move with `stable`.
|
|
31
|
+
_FLOORS = {
|
|
32
|
+
"gradle": r"errorGradleVersion\s*:\s*Version\s*=\s*Version\((\d+),\s*(\d+),\s*(\d+)\)",
|
|
33
|
+
"agp": r"errorAGPVersion\s*:\s*AndroidPluginVersion\s*=\s*AndroidPluginVersion\((\d+),\s*(\d+),\s*(\d+)\)",
|
|
34
|
+
"kgp": r"errorKGPVersion\s*:\s*Version\s*=\s*Version\((\d+),\s*(\d+),\s*(\d+)\)",
|
|
35
|
+
}
|
|
36
|
+
_ERROR_VERSION = re.compile(_FLOORS["gradle"])
|
|
29
37
|
_DISTRIBUTION = re.compile(r"^(distributionUrl=.*gradle-)([0-9]+(?:\.[0-9]+)*)(-(?:all|bin)\.zip)$", re.M)
|
|
30
38
|
|
|
31
39
|
Version = tuple[int, ...]
|
|
@@ -93,18 +101,24 @@ def normalize(version: Version, probe=_exists) -> str:
|
|
|
93
101
|
return options[0]
|
|
94
102
|
|
|
95
103
|
|
|
96
|
-
def
|
|
97
|
-
"""The oldest
|
|
104
|
+
def flutter_minimum(flutter_root: Path, tool: str) -> Version | None:
|
|
105
|
+
"""The oldest `tool` (gradle | agp | kgp) this Flutter SDK will build against."""
|
|
98
106
|
checker = flutter_root / _CHECKER
|
|
99
107
|
try:
|
|
100
|
-
|
|
108
|
+
text = checker.read_text(encoding="utf-8")
|
|
101
109
|
except OSError:
|
|
102
110
|
return None
|
|
111
|
+
match = re.search(_FLOORS[tool], text)
|
|
103
112
|
if not match:
|
|
104
113
|
return None
|
|
105
114
|
return tuple(int(group) for group in match.groups())
|
|
106
115
|
|
|
107
116
|
|
|
117
|
+
def flutter_minimum_gradle(flutter_root: Path) -> Version | None:
|
|
118
|
+
"""The oldest Gradle this Flutter SDK will apply its plugin against."""
|
|
119
|
+
return flutter_minimum(flutter_root, "gradle")
|
|
120
|
+
|
|
121
|
+
|
|
108
122
|
def wrapper_properties(workspace: Path, gradle_root: Path | None = None) -> Path:
|
|
109
123
|
root = gradle_root or (workspace / "android")
|
|
110
124
|
return root / "gradle" / "wrapper" / "gradle-wrapper.properties"
|
|
@@ -155,3 +169,103 @@ def ensure_minimum(workspace: Path, flutter_root: Path, gradle_root: Path | None
|
|
|
155
169
|
f"{normalize(minimum)}; raised it to {normalize(minimum)} for this build only. "
|
|
156
170
|
f"Commit the bump in {properties.name} to make it permanent."
|
|
157
171
|
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# --- Android Gradle Plugin and Kotlin Gradle Plugin -------------------------
|
|
175
|
+
#
|
|
176
|
+
# Both are declared in `android/settings.gradle(.kts)` in the modern Flutter
|
|
177
|
+
# template and in the top-level `android/build.gradle` in the older one. Flutter
|
|
178
|
+
# floors both, and both floors move with `stable`, so an app that built fine
|
|
179
|
+
# last month stops building without a single line of it having changed.
|
|
180
|
+
|
|
181
|
+
_PLUGIN_BLOCK = {
|
|
182
|
+
"agp": re.compile(
|
|
183
|
+
r"""(id\s*\(?\s*["']com\.android\.application["']\s*\)?\s*version\s*["'])([0-9][0-9.]*)(["'])"""
|
|
184
|
+
),
|
|
185
|
+
"kgp": re.compile(
|
|
186
|
+
r"""(id\s*\(?\s*["']org\.jetbrains\.kotlin\.android["']\s*\)?\s*version\s*["'])([0-9][0-9.]*)(["'])"""
|
|
187
|
+
),
|
|
188
|
+
}
|
|
189
|
+
_LEGACY = {
|
|
190
|
+
"agp": re.compile(r"""(["']com\.android\.tools\.build:gradle:)([0-9][0-9.]*)(["'])"""),
|
|
191
|
+
"kgp": re.compile(r"""(ext\.kotlin_version\s*=\s*["'])([0-9][0-9.]*)(["'])"""),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
_LABEL = {"agp": "Android Gradle Plugin", "kgp": "Kotlin Gradle Plugin"}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def toolchain_files(workspace: Path, gradle_root: Path | None = None) -> list[Path]:
|
|
198
|
+
root = gradle_root or (workspace / "android")
|
|
199
|
+
names = ("settings.gradle", "settings.gradle.kts", "build.gradle", "build.gradle.kts")
|
|
200
|
+
return [root / name for name in names if (root / name).is_file()]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def read_declared(files: list[Path], tool: str) -> tuple[Path, Version] | None:
|
|
204
|
+
"""First declaration of `tool` across the candidate build files."""
|
|
205
|
+
for path in files:
|
|
206
|
+
try:
|
|
207
|
+
text = path.read_text(encoding="utf-8")
|
|
208
|
+
except OSError:
|
|
209
|
+
continue
|
|
210
|
+
for pattern in (_PLUGIN_BLOCK[tool], _LEGACY[tool]):
|
|
211
|
+
match = pattern.search(text)
|
|
212
|
+
if match:
|
|
213
|
+
version = parse_version(match.group(2))
|
|
214
|
+
if version is not None:
|
|
215
|
+
return path, version
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def set_declared(path: Path, tool: str, version: Version) -> bool:
|
|
220
|
+
try:
|
|
221
|
+
text = path.read_text(encoding="utf-8")
|
|
222
|
+
except OSError:
|
|
223
|
+
return False
|
|
224
|
+
total = 0
|
|
225
|
+
for pattern in (_PLUGIN_BLOCK[tool], _LEGACY[tool]):
|
|
226
|
+
text, count = pattern.subn(lambda m: f"{m.group(1)}{_render(version)}{m.group(3)}", text)
|
|
227
|
+
total += count
|
|
228
|
+
if not total:
|
|
229
|
+
return False
|
|
230
|
+
path.write_text(text, encoding="utf-8")
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def ensure_toolchain(
|
|
235
|
+
workspace: Path, flutter_root: Path, gradle_root: Path | None = None
|
|
236
|
+
) -> list[str]:
|
|
237
|
+
"""Raise Gradle, AGP and KGP to this Flutter SDK's floors. Returns log lines."""
|
|
238
|
+
messages = []
|
|
239
|
+
gradle = ensure_minimum(workspace, flutter_root, gradle_root)
|
|
240
|
+
if gradle:
|
|
241
|
+
messages.append(gradle)
|
|
242
|
+
files = toolchain_files(workspace, gradle_root)
|
|
243
|
+
if not files:
|
|
244
|
+
return messages
|
|
245
|
+
for tool in ("agp", "kgp"):
|
|
246
|
+
minimum = flutter_minimum(flutter_root, tool)
|
|
247
|
+
if minimum is None:
|
|
248
|
+
messages.append(
|
|
249
|
+
f"::warning::Could not read Flutter's minimum {_LABEL[tool]} version; leaving it alone"
|
|
250
|
+
)
|
|
251
|
+
continue
|
|
252
|
+
found = read_declared(files, tool)
|
|
253
|
+
if found is None:
|
|
254
|
+
# Plenty of valid projects inherit these from elsewhere; that is not
|
|
255
|
+
# something to fail a deploy over.
|
|
256
|
+
continue
|
|
257
|
+
path, current = found
|
|
258
|
+
if current >= minimum:
|
|
259
|
+
messages.append(
|
|
260
|
+
f"{_LABEL[tool]} {_render(current)} meets Flutter's minimum {_render(minimum)}"
|
|
261
|
+
)
|
|
262
|
+
continue
|
|
263
|
+
if not set_declared(path, tool, minimum):
|
|
264
|
+
messages.append(f"::warning::Failed to raise the {_LABEL[tool]} version in {path}")
|
|
265
|
+
continue
|
|
266
|
+
messages.append(
|
|
267
|
+
f"::warning::{_LABEL[tool]} {_render(current)} is below Flutter's minimum "
|
|
268
|
+
f"{_render(minimum)}; raised it to {_render(minimum)} in {path.name} for this build "
|
|
269
|
+
f"only. Commit the bump to make it permanent."
|
|
270
|
+
)
|
|
271
|
+
return messages
|
|
@@ -170,16 +170,16 @@ def main() -> None:
|
|
|
170
170
|
project, build_number, os.environ.get("INPUT_BUILD_NAME") or ""
|
|
171
171
|
)
|
|
172
172
|
else:
|
|
173
|
-
# Flutter refuses to apply its Gradle plugin
|
|
174
|
-
# the SDK's floor, and CI
|
|
175
|
-
#
|
|
176
|
-
#
|
|
173
|
+
# Flutter refuses to apply its Gradle plugin when the project's
|
|
174
|
+
# Gradle, AGP or Kotlin plugin is older than the SDK's floor, and CI
|
|
175
|
+
# tracks `stable`, so those floors rise without the app changing.
|
|
176
|
+
# Raise them in the checkout rather than letting every app in the
|
|
177
|
+
# fleet break on Flutter's release day.
|
|
177
178
|
root = flutter_root()
|
|
178
179
|
if root is None:
|
|
179
|
-
print("::warning::Flutter SDK not found; skipping the
|
|
180
|
+
print("::warning::Flutter SDK not found; skipping the build toolchain check")
|
|
180
181
|
else:
|
|
181
|
-
message
|
|
182
|
-
if message:
|
|
182
|
+
for message in gradle_wrapper.ensure_toolchain(workspace, root):
|
|
183
183
|
print(message)
|
|
184
184
|
except (ConfigError, KeyError, OSError) as exc:
|
|
185
185
|
print(f"::error::{exc}")
|
|
@@ -122,3 +122,140 @@ class WrapperFileTest(unittest.TestCase):
|
|
|
122
122
|
|
|
123
123
|
if __name__ == "__main__":
|
|
124
124
|
unittest.main()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
SETTINGS = """
|
|
128
|
+
pluginManagement { includeBuild("../flutter/packages/flutter_tools/gradle") }
|
|
129
|
+
plugins {
|
|
130
|
+
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
|
131
|
+
id "com.android.application" version "8.9.1" apply false
|
|
132
|
+
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
|
133
|
+
}
|
|
134
|
+
include ":app"
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
LEGACY_BUILD = """
|
|
138
|
+
buildscript {
|
|
139
|
+
ext.kotlin_version = '1.7.10'
|
|
140
|
+
dependencies { classpath 'com.android.tools.build:gradle:7.4.2' }
|
|
141
|
+
}
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
CHECKER_WITH_ALL_FLOORS = """
|
|
145
|
+
internal val errorGradleVersion: Version = Version(8, 14, 0)
|
|
146
|
+
internal val errorAGPVersion: AndroidPluginVersion = AndroidPluginVersion(8, 11, 1)
|
|
147
|
+
internal val errorKGPVersion: Version = Version(2, 0, 0)
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ToolchainTest(unittest.TestCase):
|
|
152
|
+
def setUp(self):
|
|
153
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
154
|
+
self.workspace = Path(self.tmp.name)
|
|
155
|
+
android = self.workspace / "android"
|
|
156
|
+
android.mkdir()
|
|
157
|
+
(android / "settings.gradle").write_text(SETTINGS, encoding="utf-8")
|
|
158
|
+
properties = gw.wrapper_properties(self.workspace)
|
|
159
|
+
properties.parent.mkdir(parents=True)
|
|
160
|
+
properties.write_text(WRAPPER, encoding="utf-8")
|
|
161
|
+
self.settings = android / "settings.gradle"
|
|
162
|
+
self.flutter = self.workspace / "flutter"
|
|
163
|
+
checker = self.flutter / gw._CHECKER
|
|
164
|
+
checker.parent.mkdir(parents=True)
|
|
165
|
+
checker.write_text(CHECKER_WITH_ALL_FLOORS, encoding="utf-8")
|
|
166
|
+
self.addCleanup(self.tmp.cleanup)
|
|
167
|
+
|
|
168
|
+
def test_reads_each_floor_from_the_sdk(self):
|
|
169
|
+
self.assertEqual(gw.flutter_minimum(self.flutter, "gradle"), (8, 14, 0))
|
|
170
|
+
self.assertEqual(gw.flutter_minimum(self.flutter, "agp"), (8, 11, 1))
|
|
171
|
+
self.assertEqual(gw.flutter_minimum(self.flutter, "kgp"), (2, 0, 0))
|
|
172
|
+
|
|
173
|
+
def test_reads_the_plugins_block(self):
|
|
174
|
+
files = gw.toolchain_files(self.workspace)
|
|
175
|
+
self.assertEqual(gw.read_declared(files, "agp")[1], (8, 9, 1))
|
|
176
|
+
self.assertEqual(gw.read_declared(files, "kgp")[1], (2, 1, 0))
|
|
177
|
+
|
|
178
|
+
def test_raises_agp_but_leaves_a_compliant_kgp(self):
|
|
179
|
+
messages = gw.ensure_toolchain(self.workspace, self.flutter)
|
|
180
|
+
text = self.settings.read_text()
|
|
181
|
+
self.assertIn('"com.android.application" version "8.11.1"', text)
|
|
182
|
+
self.assertIn('"org.jetbrains.kotlin.android" version "2.1.0"', text)
|
|
183
|
+
self.assertTrue(any("Android Gradle Plugin 8.9.1 is below" in m for m in messages))
|
|
184
|
+
self.assertTrue(any("Kotlin Gradle Plugin 2.1.0 meets" in m for m in messages))
|
|
185
|
+
|
|
186
|
+
def test_raises_the_wrapper_in_the_same_pass(self):
|
|
187
|
+
gw.ensure_toolchain(self.workspace, self.flutter)
|
|
188
|
+
self.assertIn("gradle-8.14-all.zip", gw.wrapper_properties(self.workspace).read_text())
|
|
189
|
+
|
|
190
|
+
def test_handles_the_legacy_buildscript_form(self):
|
|
191
|
+
self.settings.unlink()
|
|
192
|
+
(self.workspace / "android" / "build.gradle").write_text(LEGACY_BUILD, encoding="utf-8")
|
|
193
|
+
gw.ensure_toolchain(self.workspace, self.flutter)
|
|
194
|
+
text = (self.workspace / "android" / "build.gradle").read_text()
|
|
195
|
+
self.assertIn("com.android.tools.build:gradle:8.11.1", text)
|
|
196
|
+
self.assertIn("ext.kotlin_version = '2.0.0'", text)
|
|
197
|
+
|
|
198
|
+
def test_a_project_declaring_neither_is_left_alone(self):
|
|
199
|
+
self.settings.write_text("include ':app'\n", encoding="utf-8")
|
|
200
|
+
messages = gw.ensure_toolchain(self.workspace, self.flutter)
|
|
201
|
+
self.assertFalse(any("Android Gradle Plugin" in m for m in messages))
|
|
202
|
+
|
|
203
|
+
def test_an_unreadable_sdk_warns_per_tool(self):
|
|
204
|
+
messages = gw.ensure_toolchain(self.workspace, self.workspace / "nope")
|
|
205
|
+
self.assertEqual(sum("::warning::" in m for m in messages), 3)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
SETTINGS_KTS = """
|
|
209
|
+
plugins {
|
|
210
|
+
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
|
211
|
+
id("com.android.application") version "8.9.1" apply false
|
|
212
|
+
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
|
213
|
+
}
|
|
214
|
+
include(":app")
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class KotlinDslTest(unittest.TestCase):
|
|
219
|
+
"""The Kotlin DSL spells a plugin id `id("x") version "y"`, Groovy spells it
|
|
220
|
+
`id "x" version "y"`. A matcher written against only one silently no-ops on
|
|
221
|
+
the other — and a no-op here means shipping an app whose AGP is still below
|
|
222
|
+
Flutter's floor while the log claims it was raised."""
|
|
223
|
+
|
|
224
|
+
def setUp(self):
|
|
225
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
226
|
+
self.workspace = Path(self.tmp.name)
|
|
227
|
+
android = self.workspace / "android"
|
|
228
|
+
android.mkdir()
|
|
229
|
+
self.settings = android / "settings.gradle.kts"
|
|
230
|
+
self.settings.write_text(SETTINGS_KTS, encoding="utf-8")
|
|
231
|
+
properties = gw.wrapper_properties(self.workspace)
|
|
232
|
+
properties.parent.mkdir(parents=True)
|
|
233
|
+
properties.write_text(WRAPPER, encoding="utf-8")
|
|
234
|
+
self.flutter = self.workspace / "flutter"
|
|
235
|
+
checker = self.flutter / gw._CHECKER
|
|
236
|
+
checker.parent.mkdir(parents=True)
|
|
237
|
+
checker.write_text(CHECKER_WITH_ALL_FLOORS, encoding="utf-8")
|
|
238
|
+
self.addCleanup(self.tmp.cleanup)
|
|
239
|
+
|
|
240
|
+
def test_finds_settings_gradle_kts(self):
|
|
241
|
+
self.assertIn("settings.gradle.kts", [f.name for f in gw.toolchain_files(self.workspace)])
|
|
242
|
+
|
|
243
|
+
def test_reads_both_plugin_ids_from_the_kotlin_dsl(self):
|
|
244
|
+
files = gw.toolchain_files(self.workspace)
|
|
245
|
+
self.assertEqual(gw.read_declared(files, "agp")[1], (8, 9, 1))
|
|
246
|
+
self.assertEqual(gw.read_declared(files, "kgp")[1], (2, 1, 0))
|
|
247
|
+
|
|
248
|
+
def test_actually_rewrites_the_kotlin_dsl(self):
|
|
249
|
+
gw.ensure_toolchain(self.workspace, self.flutter)
|
|
250
|
+
text = self.settings.read_text()
|
|
251
|
+
self.assertIn('id("com.android.application") version "8.11.1"', text)
|
|
252
|
+
# Untouched: 2.1.0 already meets the 2.0.0 floor.
|
|
253
|
+
self.assertIn('id("org.jetbrains.kotlin.android") version "2.1.0"', text)
|
|
254
|
+
# The loader's own version must not be caught by the AGP matcher.
|
|
255
|
+
self.assertIn('id("dev.flutter.flutter-plugin-loader") version "1.0.0"', text)
|
|
256
|
+
|
|
257
|
+
def test_reports_the_raise_only_when_the_file_really_changed(self):
|
|
258
|
+
messages = gw.ensure_toolchain(self.workspace, self.flutter)
|
|
259
|
+
raised = [m for m in messages if "Android Gradle Plugin 8.9.1 is below" in m]
|
|
260
|
+
self.assertEqual(len(raised), 1)
|
|
261
|
+
self.assertIn("8.11.1", self.settings.read_text())
|