gowalk-cicd 1.0.0

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.
Files changed (84) hide show
  1. package/CLAUDE.md +94 -0
  2. package/README.md +417 -0
  3. package/action/.daemux-version +1 -0
  4. package/action/MIGRATION.md +232 -0
  5. package/action/action.yml +973 -0
  6. package/action/prompts/generate_dependent_fields.prompt.yml +78 -0
  7. package/action/prompts/generate_descriptions.prompt.yml +59 -0
  8. package/action/scripts/app_context_scanner.py +239 -0
  9. package/action/scripts/asc_build_history.py +284 -0
  10. package/action/scripts/asc_common.py +246 -0
  11. package/action/scripts/asc_metadata_applier.py +368 -0
  12. package/action/scripts/asc_metadata_detector.py +250 -0
  13. package/action/scripts/asc_version_create.py +365 -0
  14. package/action/scripts/asc_version_fetch.py +274 -0
  15. package/action/scripts/asc_version_reuse.py +177 -0
  16. package/action/scripts/auto_detect.py +391 -0
  17. package/action/scripts/autoupdate_check.sh +118 -0
  18. package/action/scripts/cert_factory.py +190 -0
  19. package/action/scripts/cfg_io.py +27 -0
  20. package/action/scripts/cfg_resolve.py +147 -0
  21. package/action/scripts/commit_bot_changes.sh +82 -0
  22. package/action/scripts/creds_store.py +328 -0
  23. package/action/scripts/keychain.py +103 -0
  24. package/action/scripts/lookup_app_id.py +51 -0
  25. package/action/scripts/manage_marketing_version.py +337 -0
  26. package/action/scripts/metadata_constants.py +102 -0
  27. package/action/scripts/mmv_decide_create.py +181 -0
  28. package/action/scripts/mmv_floor_check.py +265 -0
  29. package/action/scripts/next_build_number.py +195 -0
  30. package/action/scripts/pbxproj_editor.py +220 -0
  31. package/action/scripts/prepare_signing.py +223 -0
  32. package/action/scripts/profile_io.py +64 -0
  33. package/action/scripts/profile_manager.py +307 -0
  34. package/action/scripts/read_config.py +315 -0
  35. package/action/scripts/resolve_marketing_version.py +173 -0
  36. package/action/scripts/set_app_store_whats_new.py +317 -0
  37. package/action/scripts/team_resolver.py +130 -0
  38. package/action/scripts/test_app_context_scanner.py +167 -0
  39. package/action/scripts/test_asc_build_history.py +221 -0
  40. package/action/scripts/test_asc_builds_prerelease.py +219 -0
  41. package/action/scripts/test_asc_common_timeouts.py +121 -0
  42. package/action/scripts/test_asc_metadata_applier.py +850 -0
  43. package/action/scripts/test_asc_metadata_detector.py +445 -0
  44. package/action/scripts/test_auto_detect.py +432 -0
  45. package/action/scripts/test_cfg_resolve_credentials.py +48 -0
  46. package/action/scripts/test_compute_next_version.py +95 -0
  47. package/action/scripts/test_compute_next_version_patch_backcompat.py +41 -0
  48. package/action/scripts/test_compute_next_version_strictness.py +67 -0
  49. package/action/scripts/test_fetch_versions.py +163 -0
  50. package/action/scripts/test_ground_truth_floor.py +104 -0
  51. package/action/scripts/test_manage_marketing_version.py +134 -0
  52. package/action/scripts/test_manage_marketing_version_autoroll.py +149 -0
  53. package/action/scripts/test_manage_marketing_version_floor.py +170 -0
  54. package/action/scripts/test_manage_marketing_version_match.py +187 -0
  55. package/action/scripts/test_mmv_409_classifier.py +231 -0
  56. package/action/scripts/test_mmv_autobump_persist_gate.py +131 -0
  57. package/action/scripts/test_mmv_decide_create.py +119 -0
  58. package/action/scripts/test_mmv_floor_check.py +158 -0
  59. package/action/scripts/test_mmv_floor_crosscheck.py +132 -0
  60. package/action/scripts/test_mmv_helpers.py +57 -0
  61. package/action/scripts/test_next_build_number.py +174 -0
  62. package/action/scripts/test_read_config_auto_detect.py +257 -0
  63. package/action/scripts/test_resolve_marketing_version.py +298 -0
  64. package/action/scripts/test_reuse_stale_editable.py +182 -0
  65. package/action/scripts/test_set_app_store_whats_new.py +71 -0
  66. package/action/scripts/test_team_fallback_wiring.py +157 -0
  67. package/action/scripts/test_team_resolver.py +249 -0
  68. package/action/scripts/tests_common.py +167 -0
  69. package/action/scripts/version_utils.py +373 -0
  70. package/android-action/.daemux-version +1 -0
  71. package/android-action/action.yml +92 -0
  72. package/android-action/scripts/android_config.py +130 -0
  73. package/android-action/scripts/bitrise_deploy.py +160 -0
  74. package/android-action/scripts/find_bundle.py +25 -0
  75. package/android-action/scripts/play_preflight.py +69 -0
  76. package/android-action/scripts/resolve_android.py +70 -0
  77. package/android-action/scripts/sign_bundle.py +70 -0
  78. package/android-action/scripts/test_android_config.py +97 -0
  79. package/android-action/scripts/test_bitrise_deploy.py +124 -0
  80. package/android-action/scripts/test_sign_bundle.py +82 -0
  81. package/bin/cli.mjs +63 -0
  82. package/package.json +57 -0
  83. package/src/install.mjs +208 -0
  84. package/templates/deploy.yml +150 -0
@@ -0,0 +1,432 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tests for auto_detect.py — Xcode project / scheme / bundle_id auto-detection.
4
+
5
+ All tests run offline. xcodebuild invocations are stubbed through
6
+ ``unittest.mock`` so they never shell out.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import sys
13
+ import tempfile
14
+ import unittest
15
+ from pathlib import Path
16
+ from unittest import mock
17
+
18
+ # Make sibling scripts importable regardless of the pytest invocation cwd.
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
20
+
21
+ import auto_detect # noqa: E402
22
+
23
+
24
+ APP_TYPE = "com.apple.product-type.application"
25
+ EXT_TYPE = "com.apple.product-type.app-extension"
26
+ TUN_TYPE = "com.apple.product-type.tunnel-provider"
27
+
28
+
29
+ class AutoDetectProjectTests(unittest.TestCase):
30
+ """auto_detect_project: find *.xcworkspace or *.xcodeproj at repo root."""
31
+
32
+ def test_prefers_xcworkspace_over_xcodeproj(self):
33
+ with tempfile.TemporaryDirectory() as tmp:
34
+ root = Path(tmp)
35
+ (root / "MyApp.xcworkspace").mkdir()
36
+ (root / "MyApp.xcodeproj").mkdir()
37
+ project, workspace = auto_detect.auto_detect_project(root)
38
+ self.assertEqual(project, "")
39
+ self.assertEqual(workspace, "MyApp.xcworkspace")
40
+
41
+ def test_returns_single_xcodeproj_when_no_workspace(self):
42
+ with tempfile.TemporaryDirectory() as tmp:
43
+ root = Path(tmp)
44
+ (root / "MyApp.xcodeproj").mkdir()
45
+ project, workspace = auto_detect.auto_detect_project(root)
46
+ self.assertEqual(project, "MyApp.xcodeproj")
47
+ self.assertEqual(workspace, "")
48
+
49
+ def test_prefers_xcodeproj_matching_repo_basename_when_multiple(self):
50
+ with tempfile.TemporaryDirectory() as tmp:
51
+ # Create a named subdirectory so basename is predictable.
52
+ root = Path(tmp) / "myapp"
53
+ root.mkdir()
54
+ (root / "Other.xcodeproj").mkdir()
55
+ (root / "myapp.xcodeproj").mkdir()
56
+ project, workspace = auto_detect.auto_detect_project(root)
57
+ self.assertEqual(project, "myapp.xcodeproj")
58
+ self.assertEqual(workspace, "")
59
+
60
+ def test_falls_back_alphabetical_when_no_basename_match(self):
61
+ with tempfile.TemporaryDirectory() as tmp:
62
+ root = Path(tmp) / "ambiguous-repo"
63
+ root.mkdir()
64
+ (root / "Zebra.xcodeproj").mkdir()
65
+ (root / "Alpha.xcodeproj").mkdir()
66
+ project, workspace = auto_detect.auto_detect_project(root)
67
+ self.assertEqual(project, "Alpha.xcodeproj")
68
+ self.assertEqual(workspace, "")
69
+
70
+ def test_returns_empty_when_no_project_found(self):
71
+ with tempfile.TemporaryDirectory() as tmp:
72
+ root = Path(tmp)
73
+ project, workspace = auto_detect.auto_detect_project(root)
74
+ self.assertEqual(project, "")
75
+ self.assertEqual(workspace, "")
76
+
77
+ def test_runs_xcodegen_when_only_project_yml_present(self):
78
+ """project.yml without .xcodeproj should trigger xcodegen + retry."""
79
+ with tempfile.TemporaryDirectory() as tmp:
80
+ root = Path(tmp)
81
+ (root / "project.yml").write_text("name: MyApp\n")
82
+
83
+ def fake_xcodegen(cmd, **kw):
84
+ # Simulate xcodegen materializing the .xcodeproj.
85
+ self.assertEqual(cmd[:2], ["xcodegen", "generate"])
86
+ (root / "MyApp.xcodeproj").mkdir()
87
+ result = mock.MagicMock()
88
+ result.returncode = 0
89
+ result.stdout = ""
90
+ result.stderr = ""
91
+ return result
92
+
93
+ with mock.patch("auto_detect.subprocess.run", side_effect=fake_xcodegen) as mrun:
94
+ project, workspace = auto_detect.auto_detect_project(root)
95
+ mrun.assert_called_once()
96
+ self.assertEqual(project, "MyApp.xcodeproj")
97
+ self.assertEqual(workspace, "")
98
+
99
+ def test_skips_xcodegen_when_xcodeproj_already_present(self):
100
+ """xcodegen should NOT run if an .xcodeproj is already there."""
101
+ with tempfile.TemporaryDirectory() as tmp:
102
+ root = Path(tmp)
103
+ (root / "project.yml").write_text("name: MyApp\n")
104
+ (root / "MyApp.xcodeproj").mkdir()
105
+
106
+ with mock.patch("auto_detect.subprocess.run") as mrun:
107
+ project, workspace = auto_detect.auto_detect_project(root)
108
+ mrun.assert_not_called()
109
+ self.assertEqual(project, "MyApp.xcodeproj")
110
+
111
+ def test_xcodegen_failure_returns_empty(self):
112
+ """xcodegen returning non-zero leaves project/workspace empty."""
113
+ with tempfile.TemporaryDirectory() as tmp:
114
+ root = Path(tmp)
115
+ (root / "project.yml").write_text("name: Broken\n")
116
+
117
+ def fake_xcodegen(cmd, **kw):
118
+ result = mock.MagicMock()
119
+ result.returncode = 1
120
+ result.stdout = ""
121
+ result.stderr = "boom"
122
+ return result
123
+
124
+ with mock.patch("auto_detect.subprocess.run", side_effect=fake_xcodegen):
125
+ project, workspace = auto_detect.auto_detect_project(root)
126
+ self.assertEqual(project, "")
127
+ self.assertEqual(workspace, "")
128
+
129
+ def test_xcodegen_missing_binary_returns_empty(self):
130
+ """FileNotFoundError (xcodegen not installed) is handled gracefully.
131
+
132
+ When the Homebrew install fallback also fails (brew missing or
133
+ install rc!=0), the function returns empty without raising.
134
+ """
135
+ with tempfile.TemporaryDirectory() as tmp:
136
+ root = Path(tmp)
137
+ (root / "project.yml").write_text("name: NoTool\n")
138
+
139
+ # Raise FileNotFoundError on xcodegen, then brew install also
140
+ # FileNotFoundErrors — simulates neither tool available.
141
+ with mock.patch(
142
+ "auto_detect.subprocess.run",
143
+ side_effect=FileNotFoundError("binary"),
144
+ ):
145
+ project, workspace = auto_detect.auto_detect_project(root)
146
+ self.assertEqual(project, "")
147
+ self.assertEqual(workspace, "")
148
+
149
+ def test_xcodegen_installed_via_brew_on_missing_binary(self):
150
+ """When xcodegen is missing, brew install runs and we retry."""
151
+ with tempfile.TemporaryDirectory() as tmp:
152
+ root = Path(tmp)
153
+ (root / "project.yml").write_text("name: Installable\n")
154
+
155
+ call_log: list[str] = []
156
+
157
+ def fake_run(cmd, **kw):
158
+ call_log.append(cmd[0])
159
+ result = mock.MagicMock()
160
+ result.returncode = 0
161
+ result.stdout = ""
162
+ result.stderr = ""
163
+ if cmd[0] == "xcodegen" and call_log.count("xcodegen") == 1:
164
+ # First xcodegen call fails as FileNotFoundError
165
+ raise FileNotFoundError("xcodegen")
166
+ if cmd[0] == "xcodegen":
167
+ # Second call succeeds after brew install
168
+ (root / "Installable.xcodeproj").mkdir()
169
+ return result
170
+ if cmd[0] == "brew":
171
+ # brew install succeeds
172
+ return result
173
+ return result
174
+
175
+ with mock.patch("auto_detect.subprocess.run", side_effect=fake_run):
176
+ project, workspace = auto_detect.auto_detect_project(root)
177
+ self.assertEqual(project, "Installable.xcodeproj")
178
+ self.assertEqual(call_log, ["xcodegen", "brew", "xcodegen"])
179
+
180
+ def test_detects_alt_xcodegen_spec_names(self):
181
+ """xcodegen also accepts Project.yml / project.yaml / Project.yaml."""
182
+ for spec_name in ("Project.yml", "project.yaml", "Project.yaml"):
183
+ with self.subTest(spec=spec_name):
184
+ with tempfile.TemporaryDirectory() as tmp:
185
+ root = Path(tmp)
186
+ (root / spec_name).write_text("name: Alt\n")
187
+
188
+ def fake_xcodegen(cmd, **kw):
189
+ (root / "Alt.xcodeproj").mkdir()
190
+ result = mock.MagicMock()
191
+ result.returncode = 0
192
+ result.stdout = ""
193
+ result.stderr = ""
194
+ return result
195
+
196
+ with mock.patch(
197
+ "auto_detect.subprocess.run",
198
+ side_effect=fake_xcodegen,
199
+ ):
200
+ project, workspace = auto_detect.auto_detect_project(root)
201
+ self.assertEqual(project, "Alt.xcodeproj")
202
+ self.assertEqual(workspace, "")
203
+
204
+
205
+ class AutoDetectSchemeTests(unittest.TestCase):
206
+ """auto_detect_scheme: pick the single application-type scheme."""
207
+
208
+ def setUp(self):
209
+ auto_detect._clear_caches()
210
+
211
+ def _list_output(self, schemes: list[str], project_name: str = "MyApp") -> str:
212
+ return json.dumps({"project": {"name": project_name, "schemes": schemes}})
213
+
214
+ def _show_settings(self, product_type: str, bundle_id: str = "com.x.y") -> str:
215
+ return json.dumps(
216
+ [
217
+ {
218
+ "action": "build",
219
+ "buildSettings": {
220
+ "PRODUCT_TYPE": product_type,
221
+ "PRODUCT_BUNDLE_IDENTIFIER": bundle_id,
222
+ },
223
+ }
224
+ ]
225
+ )
226
+
227
+ def test_returns_single_application_scheme(self):
228
+ def fake_run(cmd, **kw):
229
+ result = mock.MagicMock()
230
+ result.returncode = 0
231
+ if "-list" in cmd:
232
+ result.stdout = self._list_output(["MyApp"])
233
+ else:
234
+ result.stdout = self._show_settings(APP_TYPE)
235
+ return result
236
+
237
+ root = Path("/tmp/x")
238
+ with mock.patch.object(auto_detect.subprocess, "run", side_effect=fake_run):
239
+ scheme = auto_detect.auto_detect_scheme(root, "MyApp.xcodeproj", "")
240
+ self.assertEqual(scheme, "MyApp")
241
+
242
+ def test_filters_out_extension_and_tunnel_schemes(self):
243
+ def fake_run(cmd, **kw):
244
+ result = mock.MagicMock()
245
+ result.returncode = 0
246
+ if "-list" in cmd:
247
+ result.stdout = self._list_output(["Tunnel", "VPNProj2"])
248
+ else:
249
+ # Find the scheme name in the command
250
+ idx = cmd.index("-scheme") + 1
251
+ scheme_name = cmd[idx]
252
+ if scheme_name == "Tunnel":
253
+ result.stdout = self._show_settings(TUN_TYPE)
254
+ else:
255
+ result.stdout = self._show_settings(APP_TYPE)
256
+ return result
257
+
258
+ root = Path("/tmp/mirage")
259
+ with mock.patch.object(auto_detect.subprocess, "run", side_effect=fake_run):
260
+ scheme = auto_detect.auto_detect_scheme(root, "VPNProj2.xcodeproj", "")
261
+ self.assertEqual(scheme, "VPNProj2")
262
+
263
+ def test_prefers_basename_match_when_multiple_app_schemes(self):
264
+ def fake_run(cmd, **kw):
265
+ result = mock.MagicMock()
266
+ result.returncode = 0
267
+ if "-list" in cmd:
268
+ result.stdout = self._list_output(["AppA", "myapp"])
269
+ else:
270
+ result.stdout = self._show_settings(APP_TYPE)
271
+ return result
272
+
273
+ root = Path("/tmp/myapp")
274
+ with mock.patch.object(auto_detect.subprocess, "run", side_effect=fake_run):
275
+ scheme = auto_detect.auto_detect_scheme(root, "Proj.xcodeproj", "")
276
+ self.assertEqual(scheme, "myapp")
277
+
278
+ def test_falls_back_alphabetical_when_ambiguous(self):
279
+ def fake_run(cmd, **kw):
280
+ result = mock.MagicMock()
281
+ result.returncode = 0
282
+ if "-list" in cmd:
283
+ result.stdout = self._list_output(["Zebra", "Alpha"])
284
+ else:
285
+ result.stdout = self._show_settings(APP_TYPE)
286
+ return result
287
+
288
+ root = Path("/tmp/repo")
289
+ with mock.patch.object(auto_detect.subprocess, "run", side_effect=fake_run):
290
+ scheme = auto_detect.auto_detect_scheme(root, "Proj.xcodeproj", "")
291
+ self.assertEqual(scheme, "Alpha")
292
+
293
+ def test_returns_none_when_no_application_scheme(self):
294
+ def fake_run(cmd, **kw):
295
+ result = mock.MagicMock()
296
+ result.returncode = 0
297
+ if "-list" in cmd:
298
+ result.stdout = self._list_output(["Tunnel"])
299
+ else:
300
+ result.stdout = self._show_settings(TUN_TYPE)
301
+ return result
302
+
303
+ root = Path("/tmp/repo")
304
+ with mock.patch.object(auto_detect.subprocess, "run", side_effect=fake_run):
305
+ scheme = auto_detect.auto_detect_scheme(root, "Proj.xcodeproj", "")
306
+ self.assertIsNone(scheme)
307
+
308
+ def test_returns_none_when_xcodebuild_list_fails(self):
309
+ def fake_run(cmd, **kw):
310
+ result = mock.MagicMock()
311
+ result.returncode = 1
312
+ result.stdout = ""
313
+ result.stderr = "xcodebuild error"
314
+ return result
315
+
316
+ root = Path("/tmp/repo")
317
+ with mock.patch.object(auto_detect.subprocess, "run", side_effect=fake_run):
318
+ scheme = auto_detect.auto_detect_scheme(root, "Proj.xcodeproj", "")
319
+ self.assertIsNone(scheme)
320
+
321
+
322
+ class AutoDetectBundleIdTests(unittest.TestCase):
323
+ """auto_detect_bundle_id: extract PRODUCT_BUNDLE_IDENTIFIER from showBuildSettings."""
324
+
325
+ def setUp(self):
326
+ auto_detect._clear_caches()
327
+
328
+ def _show_settings(self, bundle_id: str) -> str:
329
+ return json.dumps(
330
+ [
331
+ {
332
+ "action": "build",
333
+ "buildSettings": {
334
+ "PRODUCT_TYPE": APP_TYPE,
335
+ "PRODUCT_BUNDLE_IDENTIFIER": bundle_id,
336
+ },
337
+ }
338
+ ]
339
+ )
340
+
341
+ def test_returns_bundle_id_from_showBuildSettings(self):
342
+ def fake_run(cmd, **kw):
343
+ result = mock.MagicMock()
344
+ result.returncode = 0
345
+ result.stdout = self._show_settings("com.example.myapp")
346
+ return result
347
+
348
+ root = Path("/tmp/repo")
349
+ with mock.patch.object(auto_detect.subprocess, "run", side_effect=fake_run):
350
+ bundle = auto_detect.auto_detect_bundle_id(
351
+ root, "Proj.xcodeproj", "", "MyApp", "Release"
352
+ )
353
+ self.assertEqual(bundle, "com.example.myapp")
354
+
355
+ def test_returns_none_when_bundle_id_empty(self):
356
+ def fake_run(cmd, **kw):
357
+ result = mock.MagicMock()
358
+ result.returncode = 0
359
+ result.stdout = self._show_settings("")
360
+ return result
361
+
362
+ root = Path("/tmp/repo")
363
+ with mock.patch.object(auto_detect.subprocess, "run", side_effect=fake_run):
364
+ bundle = auto_detect.auto_detect_bundle_id(
365
+ root, "Proj.xcodeproj", "", "MyApp", "Release"
366
+ )
367
+ self.assertIsNone(bundle)
368
+
369
+ def test_rejects_unresolved_variable_reference(self):
370
+ # $(…) or $… patterns mean xcodebuild couldn't evaluate — reject.
371
+ def fake_run(cmd, **kw):
372
+ result = mock.MagicMock()
373
+ result.returncode = 0
374
+ result.stdout = self._show_settings("$(PRODUCT_NAME)")
375
+ return result
376
+
377
+ root = Path("/tmp/repo")
378
+ with mock.patch.object(auto_detect.subprocess, "run", side_effect=fake_run):
379
+ bundle = auto_detect.auto_detect_bundle_id(
380
+ root, "Proj.xcodeproj", "", "MyApp", "Release"
381
+ )
382
+ self.assertIsNone(bundle)
383
+
384
+ def test_returns_none_when_xcodebuild_fails(self):
385
+ def fake_run(cmd, **kw):
386
+ result = mock.MagicMock()
387
+ result.returncode = 65
388
+ result.stdout = ""
389
+ result.stderr = "xcodebuild error: scheme not found"
390
+ return result
391
+
392
+ root = Path("/tmp/repo")
393
+ with mock.patch.object(auto_detect.subprocess, "run", side_effect=fake_run):
394
+ bundle = auto_detect.auto_detect_bundle_id(
395
+ root, "Proj.xcodeproj", "", "MyApp", "Release"
396
+ )
397
+ self.assertIsNone(bundle)
398
+
399
+
400
+ class CachingTests(unittest.TestCase):
401
+ """xcodebuild -list output and showBuildSettings output must be cached."""
402
+
403
+ def setUp(self):
404
+ auto_detect._clear_caches()
405
+
406
+ def test_list_schemes_cached_across_calls(self):
407
+ calls = {"count": 0}
408
+
409
+ def fake_run(cmd, **kw):
410
+ result = mock.MagicMock()
411
+ result.returncode = 0
412
+ if "-list" in cmd:
413
+ calls["count"] += 1
414
+ result.stdout = json.dumps(
415
+ {"project": {"name": "MyApp", "schemes": ["MyApp"]}}
416
+ )
417
+ else:
418
+ result.stdout = json.dumps(
419
+ [{"buildSettings": {"PRODUCT_TYPE": APP_TYPE,
420
+ "PRODUCT_BUNDLE_IDENTIFIER": "com.x"}}]
421
+ )
422
+ return result
423
+
424
+ root = Path("/tmp/repo")
425
+ with mock.patch.object(auto_detect.subprocess, "run", side_effect=fake_run):
426
+ auto_detect.auto_detect_scheme(root, "Proj.xcodeproj", "")
427
+ auto_detect.auto_detect_scheme(root, "Proj.xcodeproj", "")
428
+ self.assertEqual(calls["count"], 1, "xcodebuild -list should be cached")
429
+
430
+
431
+ if __name__ == "__main__":
432
+ unittest.main()
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import tempfile
4
+ import unittest
5
+ from pathlib import Path
6
+
7
+ from cfg_resolve import find_p8, parse_p8_filename
8
+
9
+
10
+ class CredentialFilenameTest(unittest.TestCase):
11
+ def test_parses_canonical_name(self):
12
+ parsed = parse_p8_filename(
13
+ "AuthKey_SG24AB2JHM_Issuer_f24f1629-3461-4a57-8807-8f7c9964d652.p8"
14
+ )
15
+ self.assertEqual(
16
+ parsed,
17
+ ("SG24AB2JHM", "f24f1629-3461-4a57-8807-8f7c9964d652"),
18
+ )
19
+
20
+ def test_parses_metadata_rich_existing_name(self):
21
+ parsed = parse_p8_filename(
22
+ "AE2(key_id_SG24AB2JHM_issuer_"
23
+ "f24f1629-3461-4a57-8807-8f7c9964d652_vendor_id_92561414).p8"
24
+ )
25
+ self.assertEqual(
26
+ parsed,
27
+ ("SG24AB2JHM", "f24f1629-3461-4a57-8807-8f7c9964d652"),
28
+ )
29
+
30
+ def test_find_p8_accepts_metadata_rich_name(self):
31
+ with tempfile.TemporaryDirectory() as directory:
32
+ workspace = Path(directory)
33
+ creds = workspace / "creds"
34
+ creds.mkdir()
35
+ key = creds / (
36
+ "AE2(key_id_SG24AB2JHM_issuer_"
37
+ "f24f1629-3461-4a57-8807-8f7c9964d652_vendor_id_92561414).p8"
38
+ )
39
+ key.write_text("private-key-placeholder")
40
+
41
+ self.assertEqual(
42
+ find_p8(workspace),
43
+ (key, "SG24AB2JHM", "f24f1629-3461-4a57-8807-8f7c9964d652"),
44
+ )
45
+
46
+
47
+ if __name__ == "__main__":
48
+ unittest.main()
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tests for ``version_utils.compute_next_version`` -- 'rollover' policy.
4
+
5
+ This file holds the rollover-policy scenarios. Strictness invariants and
6
+ legacy-'patch' backcompat regressions live in sibling files (round 2
7
+ codex review split):
8
+
9
+ - ``test_compute_next_version_strictness`` -- ``result > floor`` AND
10
+ ``result > current`` invariant guard across all policies.
11
+ - ``test_compute_next_version_patch_backcompat`` -- guards that the
12
+ legacy unbounded 'patch' policy is unchanged for consumers pinning
13
+ the historical default.
14
+
15
+ Splitting keeps each file under the project's 10-functions-per-file cap.
16
+
17
+ Rollover semantics: patch+1 with carry. At the resulting patch>9 it
18
+ rolls into the next minor (1.0.9 -> 1.1.0); at the resulting minor>9 it
19
+ cascades into the next major (1.9.9 -> 2.0.0). Major has no upper limit
20
+ (9.9.9 -> 10.0.0). The carry uses ``>9`` (not ``==10``) so projects with
21
+ inputs already past the boundary -- e.g. a 1.0.10 left from the legacy
22
+ 'patch' policy -- still cascade cleanly. Cross-train semantics inherited
23
+ from 'patch' are preserved: floor on a higher major.minor train wins
24
+ outright, and rollover bumps from there.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import sys
30
+ import unittest
31
+ from pathlib import Path
32
+
33
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
34
+
35
+ import version_utils # noqa: E402
36
+
37
+
38
+ class ComputeNextVersionRolloverTests(unittest.TestCase):
39
+ """Rollover policy: patch bumps with carry into minor / major."""
40
+
41
+ def test_compute_next_version_rollover_simple_patch_bump(self):
42
+ # Below the .9 boundary -> simple patch increment.
43
+ result = version_utils.compute_next_version("1.0.5", "1.0.5", "rollover")
44
+ self.assertEqual(result, "1.0.6")
45
+
46
+ def test_compute_next_version_rollover_patch_overflow(self):
47
+ # Patch at .9 must roll: 1.0.9 -> 1.1.0.
48
+ result = version_utils.compute_next_version("1.0.9", "1.0.9", "rollover")
49
+ self.assertEqual(result, "1.1.0")
50
+
51
+ def test_compute_next_version_rollover_minor_overflow(self):
52
+ # Both patch and minor at .9 must cascade: 1.9.9 -> 2.0.0.
53
+ result = version_utils.compute_next_version("1.9.9", "1.9.9", "rollover")
54
+ self.assertEqual(result, "2.0.0")
55
+
56
+ def test_compute_next_version_rollover_floor_higher_patch(self):
57
+ # Floor sits above current on the same train; rollover from floor.
58
+ # 1.0.5 vs floor 1.0.9 -> bump from .9 -> 1.1.0.
59
+ result = version_utils.compute_next_version("1.0.5", "1.0.9", "rollover")
60
+ self.assertEqual(result, "1.1.0")
61
+
62
+ def test_compute_next_version_rollover_cross_train(self):
63
+ # Floor on a higher major.minor train wins outright; rollover then
64
+ # bumps from floor's patch. 1.0.5 vs floor 2.0.5 -> 2.0.6.
65
+ result = version_utils.compute_next_version("1.0.5", "2.0.5", "rollover")
66
+ self.assertEqual(result, "2.0.6")
67
+
68
+ def test_compute_next_version_rollover_major_overflow_no_cap(self):
69
+ # Major has no rollover ceiling; 9.9.9 -> 10.0.0 (just keeps growing).
70
+ result = version_utils.compute_next_version("9.9.9", "9.9.9", "rollover")
71
+ self.assertEqual(result, "10.0.0")
72
+
73
+ def test_compute_next_version_rollover_two_component_equal(self):
74
+ # 2-component MARKETING_VERSION ("1.0") parses .patch=0 -> .1.
75
+ result = version_utils.compute_next_version("1.0", "1.0", "rollover")
76
+ self.assertEqual(result, "1.0.1")
77
+
78
+ def test_compute_next_version_rollover_over_nine_patch_input(self):
79
+ # Round-2 codex: a project previously on 'patch' may sit at
80
+ # 1.0.10 before switching to 'rollover'. The carry must use
81
+ # ``new_patch > 9`` (not ``== 10``) so 11 still cascades.
82
+ # 1.0.10 -> +1 -> patch=11 -> >9 -> patch=0, minor+=1 -> 1.1.0.
83
+ result = version_utils.compute_next_version("1.0.10", "1.0.10", "rollover")
84
+ self.assertEqual(result, "1.1.0")
85
+
86
+ def test_compute_next_version_rollover_over_nine_with_floor_higher(self):
87
+ # Round-2 codex: floor pulls patch_base above 9 too. trains
88
+ # match -> patch_base=max(15,20)=20 -> +1=21 -> >9 cascade.
89
+ # 1.0.15 vs floor 1.0.20 -> 1.1.0.
90
+ result = version_utils.compute_next_version("1.0.15", "1.0.20", "rollover")
91
+ self.assertEqual(result, "1.1.0")
92
+
93
+
94
+ if __name__ == "__main__":
95
+ unittest.main()
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Backcompat regression tests for the legacy 'patch' policy.
4
+
5
+ Split from ``test_compute_next_version`` (round 2 codex review) so the
6
+ rollover-policy file stays under the project's 10-functions-per-file
7
+ cap. Guards that the historical unbounded 'patch' bump (1.0.9 -> 1.0.10)
8
+ is preserved verbatim -- existing 0.0.27 consumers that pin
9
+ ``marketing-version-auto-bump: 'patch'`` MUST keep their current
10
+ behavior even after rollover ships as the new default. Any future
11
+ refactor that silently grafts rollover semantics onto the 'patch'
12
+ branch should fail HERE first.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import sys
18
+ import unittest
19
+ from pathlib import Path
20
+
21
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
22
+
23
+ import version_utils # noqa: E402
24
+
25
+
26
+ class ComputeNextVersionPatchBackcompatTests(unittest.TestCase):
27
+ """Confirm the historical 'patch' policy is unchanged."""
28
+
29
+ def test_compute_next_version_patch_no_rollover_at_nine(self):
30
+ # 'patch' must NOT roll over: 1.0.9 -> 1.0.10 (legacy behavior).
31
+ result = version_utils.compute_next_version("1.0.9", "1.0.9", "patch")
32
+ self.assertEqual(result, "1.0.10")
33
+
34
+ def test_compute_next_version_patch_no_rollover_at_minor_nine(self):
35
+ # 'patch' stays on its train even when minor=9: 1.9.9 -> 1.9.10.
36
+ result = version_utils.compute_next_version("1.9.9", "1.9.9", "patch")
37
+ self.assertEqual(result, "1.9.10")
38
+
39
+
40
+ if __name__ == "__main__":
41
+ unittest.main()
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Strictness invariant tests for ``version_utils.compute_next_version``.
4
+
5
+ Split from ``test_compute_next_version`` (round 2 codex review) so the
6
+ rollover-policy file stays under the project's 10-functions-per-file
7
+ cap. ``compute_next_version`` MUST return a value STRICTLY greater
8
+ than BOTH ``current`` and ``floor`` -- never equal. Equality would
9
+ re-collide with the row that triggered auto-bump (e.g. the
10
+ READY_FOR_SALE row that started the auto-roll), defeating the whole
11
+ purpose of the bump. This file iterates the invariant across a small
12
+ grid for every supported policy ('patch', 'minor', 'rollover').
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import sys
18
+ import unittest
19
+ from pathlib import Path
20
+
21
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
22
+
23
+ import version_utils # noqa: E402
24
+
25
+
26
+ class ComputeNextVersionStrictnessTests(unittest.TestCase):
27
+ """Round-6 regression guard: result is STRICTLY > both inputs."""
28
+
29
+ def test_compute_next_version_equal_to_floor_patch(self):
30
+ result = version_utils.compute_next_version("1.0.5", "1.0.5", "patch")
31
+ self.assertEqual(result, "1.0.6")
32
+
33
+ def test_compute_next_version_equal_to_floor_minor(self):
34
+ result = version_utils.compute_next_version("1.0.5", "1.0.5", "minor")
35
+ self.assertEqual(result, "1.1.0")
36
+
37
+ def test_compute_next_version_two_component_equal(self):
38
+ # 2-component MARKETING_VERSION ("1.0") must auto-roll cleanly.
39
+ result = version_utils.compute_next_version("1.0", "1.0", "patch")
40
+ self.assertEqual(result, "1.0.1")
41
+
42
+ def test_compute_next_version_strictly_greater_invariant(self):
43
+ """Property check across a small grid: result > both inputs."""
44
+ cases = [
45
+ ("1.0.5", "1.0.5"), ("1.0.0", "1.0.5"),
46
+ ("1.0.5", "1.0.0"), ("0.0", "0.0"),
47
+ ("99.99.99", "99.99.99"), ("2.0.0", "1.0.5"),
48
+ ("1.0.5", "2.0.0"),
49
+ ]
50
+ for current, floor in cases:
51
+ for policy in ("patch", "minor", "rollover"):
52
+ result = version_utils.compute_next_version(
53
+ current, floor, policy,
54
+ )
55
+ rt = version_utils._parse(result)
56
+ self.assertGreater(
57
+ rt, version_utils._parse(current),
58
+ msg=f"{policy}: {result} not > current={current}",
59
+ )
60
+ self.assertGreater(
61
+ rt, version_utils._parse(floor),
62
+ msg=f"{policy}: {result} not > floor={floor}",
63
+ )
64
+
65
+
66
+ if __name__ == "__main__":
67
+ unittest.main()