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 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
- ### Gradle wrapper floor (Flutter apps)
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::`. Same policy as the versionCode
224
- rewrite: the edit is never committed back, so the project keeps whatever
225
- version its authors chose. Commit the bump yourself to silence the warning.
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
@@ -1 +1 @@
1
- 1.0.15
1
+ 1.0.17
@@ -1 +1 @@
1
- 1.0.15
1
+ 1.0.17
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Raise a Flutter app's Gradle wrapper to the version its Flutter SDK requires.
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
- _ERROR_VERSION = re.compile(r"errorGradleVersion\s*:\s*Version\s*=\s*Version\((\d+),\s*(\d+),\s*(\d+)\)")
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 flutter_minimum_gradle(flutter_root: Path) -> Version | None:
97
- """The oldest Gradle this Flutter SDK will apply its plugin against."""
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
- match = _ERROR_VERSION.search(checker.read_text(encoding="utf-8"))
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 to a wrapper older than
174
- # the SDK's floor, and CI tracks `stable`, so that floor rises
175
- # without the app changing. Raise it in the checkout rather than
176
- # letting every app in the fleet break on Flutter's release day.
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 Gradle wrapper check")
180
+ print("::warning::Flutter SDK not found; skipping the build toolchain check")
180
181
  else:
181
- message = gradle_wrapper.ensure_minimum(workspace, root)
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())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {