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.
- package/CLAUDE.md +94 -0
- package/README.md +417 -0
- package/action/.daemux-version +1 -0
- package/action/MIGRATION.md +232 -0
- package/action/action.yml +973 -0
- package/action/prompts/generate_dependent_fields.prompt.yml +78 -0
- package/action/prompts/generate_descriptions.prompt.yml +59 -0
- package/action/scripts/app_context_scanner.py +239 -0
- package/action/scripts/asc_build_history.py +284 -0
- package/action/scripts/asc_common.py +246 -0
- package/action/scripts/asc_metadata_applier.py +368 -0
- package/action/scripts/asc_metadata_detector.py +250 -0
- package/action/scripts/asc_version_create.py +365 -0
- package/action/scripts/asc_version_fetch.py +274 -0
- package/action/scripts/asc_version_reuse.py +177 -0
- package/action/scripts/auto_detect.py +391 -0
- package/action/scripts/autoupdate_check.sh +118 -0
- package/action/scripts/cert_factory.py +190 -0
- package/action/scripts/cfg_io.py +27 -0
- package/action/scripts/cfg_resolve.py +147 -0
- package/action/scripts/commit_bot_changes.sh +82 -0
- package/action/scripts/creds_store.py +328 -0
- package/action/scripts/keychain.py +103 -0
- package/action/scripts/lookup_app_id.py +51 -0
- package/action/scripts/manage_marketing_version.py +337 -0
- package/action/scripts/metadata_constants.py +102 -0
- package/action/scripts/mmv_decide_create.py +181 -0
- package/action/scripts/mmv_floor_check.py +265 -0
- package/action/scripts/next_build_number.py +195 -0
- package/action/scripts/pbxproj_editor.py +220 -0
- package/action/scripts/prepare_signing.py +223 -0
- package/action/scripts/profile_io.py +64 -0
- package/action/scripts/profile_manager.py +307 -0
- package/action/scripts/read_config.py +315 -0
- package/action/scripts/resolve_marketing_version.py +173 -0
- package/action/scripts/set_app_store_whats_new.py +317 -0
- package/action/scripts/team_resolver.py +130 -0
- package/action/scripts/test_app_context_scanner.py +167 -0
- package/action/scripts/test_asc_build_history.py +221 -0
- package/action/scripts/test_asc_builds_prerelease.py +219 -0
- package/action/scripts/test_asc_common_timeouts.py +121 -0
- package/action/scripts/test_asc_metadata_applier.py +850 -0
- package/action/scripts/test_asc_metadata_detector.py +445 -0
- package/action/scripts/test_auto_detect.py +432 -0
- package/action/scripts/test_cfg_resolve_credentials.py +48 -0
- package/action/scripts/test_compute_next_version.py +95 -0
- package/action/scripts/test_compute_next_version_patch_backcompat.py +41 -0
- package/action/scripts/test_compute_next_version_strictness.py +67 -0
- package/action/scripts/test_fetch_versions.py +163 -0
- package/action/scripts/test_ground_truth_floor.py +104 -0
- package/action/scripts/test_manage_marketing_version.py +134 -0
- package/action/scripts/test_manage_marketing_version_autoroll.py +149 -0
- package/action/scripts/test_manage_marketing_version_floor.py +170 -0
- package/action/scripts/test_manage_marketing_version_match.py +187 -0
- package/action/scripts/test_mmv_409_classifier.py +231 -0
- package/action/scripts/test_mmv_autobump_persist_gate.py +131 -0
- package/action/scripts/test_mmv_decide_create.py +119 -0
- package/action/scripts/test_mmv_floor_check.py +158 -0
- package/action/scripts/test_mmv_floor_crosscheck.py +132 -0
- package/action/scripts/test_mmv_helpers.py +57 -0
- package/action/scripts/test_next_build_number.py +174 -0
- package/action/scripts/test_read_config_auto_detect.py +257 -0
- package/action/scripts/test_resolve_marketing_version.py +298 -0
- package/action/scripts/test_reuse_stale_editable.py +182 -0
- package/action/scripts/test_set_app_store_whats_new.py +71 -0
- package/action/scripts/test_team_fallback_wiring.py +157 -0
- package/action/scripts/test_team_resolver.py +249 -0
- package/action/scripts/tests_common.py +167 -0
- package/action/scripts/version_utils.py +373 -0
- package/android-action/.daemux-version +1 -0
- package/android-action/action.yml +92 -0
- package/android-action/scripts/android_config.py +130 -0
- package/android-action/scripts/bitrise_deploy.py +160 -0
- package/android-action/scripts/find_bundle.py +25 -0
- package/android-action/scripts/play_preflight.py +69 -0
- package/android-action/scripts/resolve_android.py +70 -0
- package/android-action/scripts/sign_bundle.py +70 -0
- package/android-action/scripts/test_android_config.py +97 -0
- package/android-action/scripts/test_bitrise_deploy.py +124 -0
- package/android-action/scripts/test_sign_bundle.py +82 -0
- package/bin/cli.mjs +63 -0
- package/package.json +57 -0
- package/src/install.mjs +208 -0
- package/templates/deploy.yml +150 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for the persistence-context gate on auto-bump (round-9 lock-in).
|
|
4
|
+
|
|
5
|
+
``mmv_floor_check.maybe_auto_bump`` only fires when the run can also
|
|
6
|
+
commit the bumped value back to the default branch. The action's
|
|
7
|
+
commit-back step is gated on ``event=push`` AND
|
|
8
|
+
``ref=refs/heads/<default>``; auto-bumping in any other context
|
|
9
|
+
(workflow_dispatch / pull_request / push to a feature branch) would
|
|
10
|
+
upload an IPA at the bumped version while git stays at the old
|
|
11
|
+
value, causing the next run to recompute against stale state.
|
|
12
|
+
|
|
13
|
+
These tests pin the persistence-context envs explicitly to prove the
|
|
14
|
+
gate's exact triggers / refusals. They are intentionally separate
|
|
15
|
+
from ``test_manage_marketing_version_autoroll.py`` (which exercises
|
|
16
|
+
the auto-roll DECISION matrix at terminal vs in-review) so each test
|
|
17
|
+
file stays under the 10-functions-per-file cap.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import io
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import unittest
|
|
26
|
+
from contextlib import redirect_stderr
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from unittest import mock
|
|
29
|
+
|
|
30
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
31
|
+
|
|
32
|
+
import mmv_floor_check # noqa: E402
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PersistenceContextGateTests(unittest.TestCase):
|
|
36
|
+
"""``maybe_auto_bump`` must refuse when the run cannot commit back."""
|
|
37
|
+
|
|
38
|
+
def _run(self, env_overrides: dict) -> tuple[object, str]:
|
|
39
|
+
"""Drive ``maybe_auto_bump`` with the given env overrides; mocks
|
|
40
|
+
``write_marketing_version`` so the only thing under test is the
|
|
41
|
+
policy + persistence-context gating. Returns (result, stderr)."""
|
|
42
|
+
env = {"MARKETING_VERSION_AUTO_BUMP": "patch"}
|
|
43
|
+
env.update(env_overrides)
|
|
44
|
+
buf = io.StringIO()
|
|
45
|
+
with mock.patch.dict(os.environ, env, clear=False), \
|
|
46
|
+
mock.patch(
|
|
47
|
+
"version_utils.write_marketing_version",
|
|
48
|
+
return_value=True), \
|
|
49
|
+
redirect_stderr(buf):
|
|
50
|
+
result = mmv_floor_check.maybe_auto_bump("1.0.5", "1.0.5")
|
|
51
|
+
return result, buf.getvalue()
|
|
52
|
+
|
|
53
|
+
def test_push_to_default_branch_fires_autobump(self):
|
|
54
|
+
result, _ = self._run({
|
|
55
|
+
"GITHUB_EVENT_NAME": "push",
|
|
56
|
+
"GITHUB_REF": "refs/heads/main",
|
|
57
|
+
"GITHUB_DEFAULT_BRANCH": "main",
|
|
58
|
+
})
|
|
59
|
+
self.assertEqual(result, "1.0.6")
|
|
60
|
+
|
|
61
|
+
def test_workflow_dispatch_refuses_autobump(self):
|
|
62
|
+
result, stderr = self._run({
|
|
63
|
+
"GITHUB_EVENT_NAME": "workflow_dispatch",
|
|
64
|
+
"GITHUB_REF": "refs/heads/main",
|
|
65
|
+
"GITHUB_DEFAULT_BRANCH": "main",
|
|
66
|
+
})
|
|
67
|
+
self.assertIsNone(result)
|
|
68
|
+
self.assertIn("cannot persist the bump", stderr)
|
|
69
|
+
self.assertIn("workflow_dispatch", stderr)
|
|
70
|
+
|
|
71
|
+
def test_push_to_feature_branch_refuses_autobump(self):
|
|
72
|
+
result, stderr = self._run({
|
|
73
|
+
"GITHUB_EVENT_NAME": "push",
|
|
74
|
+
"GITHUB_REF": "refs/heads/feature-x",
|
|
75
|
+
"GITHUB_DEFAULT_BRANCH": "main",
|
|
76
|
+
})
|
|
77
|
+
self.assertIsNone(result)
|
|
78
|
+
self.assertIn("cannot persist the bump", stderr)
|
|
79
|
+
self.assertIn("feature-x", stderr)
|
|
80
|
+
|
|
81
|
+
def test_pull_request_refuses_autobump(self):
|
|
82
|
+
result, stderr = self._run({
|
|
83
|
+
"GITHUB_EVENT_NAME": "pull_request",
|
|
84
|
+
"GITHUB_REF": "refs/pull/42/merge",
|
|
85
|
+
"GITHUB_DEFAULT_BRANCH": "main",
|
|
86
|
+
})
|
|
87
|
+
self.assertIsNone(result)
|
|
88
|
+
self.assertIn("cannot persist the bump", stderr)
|
|
89
|
+
|
|
90
|
+
def test_missing_default_branch_env_refuses_autobump(self):
|
|
91
|
+
"""Defensive: empty default-branch env -> refuse."""
|
|
92
|
+
result, stderr = self._run({
|
|
93
|
+
"GITHUB_EVENT_NAME": "push",
|
|
94
|
+
"GITHUB_REF": "refs/heads/main",
|
|
95
|
+
"GITHUB_DEFAULT_BRANCH": "",
|
|
96
|
+
})
|
|
97
|
+
self.assertIsNone(result)
|
|
98
|
+
self.assertIn("cannot persist the bump", stderr)
|
|
99
|
+
|
|
100
|
+
def test_alternative_default_branch_master_works(self):
|
|
101
|
+
"""Sanity: a repo whose default branch is `master` still bumps."""
|
|
102
|
+
result, _ = self._run({
|
|
103
|
+
"GITHUB_EVENT_NAME": "push",
|
|
104
|
+
"GITHUB_REF": "refs/heads/master",
|
|
105
|
+
"GITHUB_DEFAULT_BRANCH": "master",
|
|
106
|
+
})
|
|
107
|
+
self.assertEqual(result, "1.0.6")
|
|
108
|
+
|
|
109
|
+
def test_policy_none_short_circuits_before_persist_check(self):
|
|
110
|
+
"""Order-of-checks lock-in: policy=none returns None silently
|
|
111
|
+
without surfacing the persistence-context warning."""
|
|
112
|
+
env = {
|
|
113
|
+
"MARKETING_VERSION_AUTO_BUMP": "none",
|
|
114
|
+
"GITHUB_EVENT_NAME": "workflow_dispatch",
|
|
115
|
+
"GITHUB_REF": "refs/heads/main",
|
|
116
|
+
"GITHUB_DEFAULT_BRANCH": "main",
|
|
117
|
+
}
|
|
118
|
+
buf = io.StringIO()
|
|
119
|
+
with mock.patch.dict(os.environ, env, clear=False), \
|
|
120
|
+
mock.patch(
|
|
121
|
+
"version_utils.write_marketing_version",
|
|
122
|
+
return_value=True), \
|
|
123
|
+
redirect_stderr(buf):
|
|
124
|
+
result = mmv_floor_check.maybe_auto_bump("1.0.5", "1.0.5")
|
|
125
|
+
self.assertIsNone(result)
|
|
126
|
+
# Persistence-context warning must NOT fire when policy=none.
|
|
127
|
+
self.assertNotIn("cannot persist the bump", buf.getvalue())
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
unittest.main()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for mmv_decide_create.py — the orchestrator decisions made via
|
|
4
|
+
``_stale_editable_id`` and ``decide_create``.
|
|
5
|
+
|
|
6
|
+
Covers:
|
|
7
|
+
- StaleEditableSelectionTests: ``_stale_editable_id`` decides the
|
|
8
|
+
kwarg passed into ``create_or_reuse_with_stale``; lower-version
|
|
9
|
+
editables get PATCH-renamed; equal/higher-version editables fail
|
|
10
|
+
fast (Issue 10) so a future draft cannot be silently downgraded.
|
|
11
|
+
- In-review rows (Issue 13) are skipped as PATCH-rename candidates;
|
|
12
|
+
the CREATE path falls through to a plain POST.
|
|
13
|
+
|
|
14
|
+
The asc_version_create.reuse_stale_editable seam (PATCH /
|
|
15
|
+
DELETE+POST fallback / exit-5 path / logging) lives in
|
|
16
|
+
``test_reuse_stale_editable.py``. The 409 classifier path (POST
|
|
17
|
+
returns 409, never bumps, fails fast) lives in
|
|
18
|
+
``test_mmv_409_classifier.py``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
import unittest
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
28
|
+
|
|
29
|
+
from tests_common import ( # noqa: E402
|
|
30
|
+
_assert_decide_exits,
|
|
31
|
+
_run_decide_for_version,
|
|
32
|
+
_v,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StaleEditableSelectionTests(unittest.TestCase):
|
|
37
|
+
"""``_stale_editable_id`` decides the kwarg that ``decide_create``
|
|
38
|
+
passes into ``create_or_reuse_with_stale``. Lower-version editables
|
|
39
|
+
get PATCH-renamed to target; equal/higher-version editables fail
|
|
40
|
+
fast (Issue 10) so a future draft cannot be silently downgraded."""
|
|
41
|
+
|
|
42
|
+
def test_create_patch_renames_stale_editable_to_target(self):
|
|
43
|
+
"""target=1.1.0 with editable PFS at 1.0.5 -> CREATE via PATCH-rename;
|
|
44
|
+
stale_editable_id kwarg must be the lower editable's id."""
|
|
45
|
+
editable = _v("1.0.5", "PREPARE_FOR_SUBMISSION", vid="ed-105")
|
|
46
|
+
result, stderr, calls = _run_decide_for_version(
|
|
47
|
+
"1.1.0", versions=[editable], ground_truth="1.0.5",
|
|
48
|
+
)
|
|
49
|
+
self.assertEqual(result["decision"], "CREATE")
|
|
50
|
+
self.assertEqual(result["versionString"], "1.1.0")
|
|
51
|
+
self.assertEqual(len(calls), 1)
|
|
52
|
+
self.assertEqual(calls[0].kwargs.get("stale_editable_id"), "ed-105")
|
|
53
|
+
self.assertIn("PATCH-rename of stale editable id=ed-105", stderr)
|
|
54
|
+
|
|
55
|
+
def test_create_no_stale_editable_no_patch(self):
|
|
56
|
+
"""target=1.1.0 with no editable at all -> CREATE via plain POST;
|
|
57
|
+
stale_editable_id kwarg must be None."""
|
|
58
|
+
result, stderr, calls = _run_decide_for_version(
|
|
59
|
+
"1.1.0", versions=[], ground_truth=None,
|
|
60
|
+
)
|
|
61
|
+
self.assertEqual(result["decision"], "CREATE")
|
|
62
|
+
self.assertEqual(len(calls), 1)
|
|
63
|
+
self.assertIsNone(calls[0].kwargs.get("stale_editable_id"))
|
|
64
|
+
self.assertNotIn("PATCH-rename", stderr)
|
|
65
|
+
|
|
66
|
+
def test_create_blocked_by_higher_editable_pfs(self):
|
|
67
|
+
"""Issue 10: a future draft at 1.2.0 (PFS) must NOT be PATCH-renamed
|
|
68
|
+
downward to 1.1.0; SystemExit(2) with downgrade wording."""
|
|
69
|
+
higher = _v("1.2.0", "PREPARE_FOR_SUBMISSION", vid="ed-120")
|
|
70
|
+
stderr = _assert_decide_exits(
|
|
71
|
+
self, "1.1.0", [higher], ground_truth=None,
|
|
72
|
+
)
|
|
73
|
+
self.assertIn("1.2.0", stderr)
|
|
74
|
+
self.assertIn("ed-120", stderr)
|
|
75
|
+
self.assertIn("downgrade", stderr.lower())
|
|
76
|
+
self.assertIn("PREPARE_FOR_SUBMISSION", stderr)
|
|
77
|
+
|
|
78
|
+
def test_create_blocked_by_higher_editable_other_state(self):
|
|
79
|
+
"""Issue 10 covers every editable state, not just PFS. A draft
|
|
80
|
+
in WAITING_FOR_REVIEW above target must also block CREATE."""
|
|
81
|
+
higher = _v("1.2.0", "WAITING_FOR_REVIEW", vid="ed-wfr-120")
|
|
82
|
+
stderr = _assert_decide_exits(
|
|
83
|
+
self, "1.1.0", [higher], ground_truth=None,
|
|
84
|
+
)
|
|
85
|
+
self.assertIn("1.2.0", stderr)
|
|
86
|
+
self.assertIn("ed-wfr-120", stderr)
|
|
87
|
+
self.assertIn("downgrade", stderr.lower())
|
|
88
|
+
self.assertIn("WAITING_FOR_REVIEW", stderr)
|
|
89
|
+
|
|
90
|
+
def test_create_blocked_by_equal_version_editable(self):
|
|
91
|
+
"""When an editable already sits at exactly target, REUSE wins
|
|
92
|
+
via ``_find_match_at_target`` BEFORE decide_create is reached;
|
|
93
|
+
no CREATE-path downgrade-rename is attempted."""
|
|
94
|
+
match = _v("1.1.0", "PREPARE_FOR_SUBMISSION", vid="ed-110")
|
|
95
|
+
result, stderr, calls = _run_decide_for_version(
|
|
96
|
+
"1.1.0", versions=[match], ground_truth=None,
|
|
97
|
+
)
|
|
98
|
+
self.assertEqual(result["decision"], "REUSE")
|
|
99
|
+
self.assertEqual(result["appStoreVersionId"], "ed-110")
|
|
100
|
+
self.assertEqual(calls, [])
|
|
101
|
+
self.assertIn("decision=REUSE", stderr)
|
|
102
|
+
|
|
103
|
+
def test_lower_in_review_skipped_create_via_post(self):
|
|
104
|
+
"""Issue 13: a WAITING_FOR_REVIEW row at version < target must
|
|
105
|
+
NOT be PATCH-renamed (Apple is reviewing it). CREATE falls
|
|
106
|
+
through to a plain POST with stale_editable_id=None."""
|
|
107
|
+
in_review = _v("1.0.5", "WAITING_FOR_REVIEW", vid="ed-wfr-105")
|
|
108
|
+
result, stderr, calls = _run_decide_for_version(
|
|
109
|
+
"1.1.0", versions=[in_review], ground_truth="1.0.5",
|
|
110
|
+
)
|
|
111
|
+
self.assertEqual(result["decision"], "CREATE")
|
|
112
|
+
self.assertEqual(result["versionString"], "1.1.0")
|
|
113
|
+
self.assertEqual(len(calls), 1)
|
|
114
|
+
self.assertIsNone(calls[0].kwargs.get("stale_editable_id"))
|
|
115
|
+
self.assertNotIn("PATCH-rename", stderr)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
unittest.main()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for mmv_floor_check.py.
|
|
4
|
+
|
|
5
|
+
Covers:
|
|
6
|
+
- ``get_combined_floor`` per-source breakdown across appStoreVersions,
|
|
7
|
+
preReleaseVersions, and builds->preReleaseVersion.
|
|
8
|
+
|
|
9
|
+
``get_ground_truth_floor`` tests live in ``test_ground_truth_floor.py``
|
|
10
|
+
(round 11 split). The cross-check error-message tests live in
|
|
11
|
+
``test_mmv_floor_crosscheck.py`` (round 8 split).
|
|
12
|
+
``fetch_versions`` pagination / filter / limit / logging tests live in
|
|
13
|
+
``test_fetch_versions.py``. The /builds and /preReleaseVersions
|
|
14
|
+
fetcher tests live in ``test_asc_build_history.py`` and
|
|
15
|
+
``test_asc_builds_prerelease.py`` respectively.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
import unittest
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from unittest import mock
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
26
|
+
|
|
27
|
+
import manage_marketing_version as mmv # noqa: E402
|
|
28
|
+
import asc_build_history # noqa: E402
|
|
29
|
+
|
|
30
|
+
from tests_common import ( # noqa: E402
|
|
31
|
+
_FakeResponse,
|
|
32
|
+
_mk_prerelease_entry,
|
|
33
|
+
_v,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _builds_payload_with_prerelease(
|
|
38
|
+
build_id: str, build_version: str, pr_id: str, pr_version: str,
|
|
39
|
+
) -> dict:
|
|
40
|
+
"""Combined-floor fixture: one /builds record paired with a
|
|
41
|
+
preReleaseVersion include. Used to verify the combined-floor wiring
|
|
42
|
+
consults the relationship rather than build.attributes.version."""
|
|
43
|
+
return {
|
|
44
|
+
"data": [{
|
|
45
|
+
"id": build_id,
|
|
46
|
+
"type": "builds",
|
|
47
|
+
"attributes": {"version": build_version},
|
|
48
|
+
"relationships": {
|
|
49
|
+
"preReleaseVersion": {
|
|
50
|
+
"data": {"type": "preReleaseVersions", "id": pr_id},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}],
|
|
54
|
+
"included": [{
|
|
55
|
+
"id": pr_id,
|
|
56
|
+
"type": "preReleaseVersions",
|
|
57
|
+
"attributes": {"version": pr_version, "platform": "IOS"},
|
|
58
|
+
}],
|
|
59
|
+
"links": {},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CombinedFloorTests(unittest.TestCase):
|
|
64
|
+
"""Combined floor spans appStoreVersions + preReleaseVersions +
|
|
65
|
+
builds->preReleaseVersion. /v1/apps/{id}/builds.attributes.version
|
|
66
|
+
(integer build number) is NOT consulted as a marketing version."""
|
|
67
|
+
|
|
68
|
+
def test_combined_floor_excludes_builds_attributes_version(self):
|
|
69
|
+
seen_paths: list[str] = []
|
|
70
|
+
seen_params: list[dict | None] = []
|
|
71
|
+
builds_payload = _builds_payload_with_prerelease(
|
|
72
|
+
"b-42", "42", "pr-10", "1.0",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def fake_request(method, path, token, *, params=None, **_kw):
|
|
76
|
+
seen_paths.append(path)
|
|
77
|
+
seen_params.append(params)
|
|
78
|
+
if "/builds" in path:
|
|
79
|
+
return _FakeResponse(builds_payload)
|
|
80
|
+
return _FakeResponse({"data": [], "links": {}})
|
|
81
|
+
|
|
82
|
+
with mock.patch.object(
|
|
83
|
+
asc_build_history, "request", side_effect=fake_request):
|
|
84
|
+
floor, _ = mmv.get_combined_floor(
|
|
85
|
+
"111", "tok",
|
|
86
|
+
appstore_versions=[_v("1.0", "READY_FOR_SALE")],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self.assertTrue(seen_paths)
|
|
90
|
+
self.assertTrue(any("/builds" in p for p in seen_paths))
|
|
91
|
+
self.assertTrue(any("/preReleaseVersions" in p for p in seen_paths))
|
|
92
|
+
builds_params = [
|
|
93
|
+
p for p, path in zip(seen_params, seen_paths)
|
|
94
|
+
if "/builds" in path and p is not None
|
|
95
|
+
]
|
|
96
|
+
self.assertTrue(builds_params)
|
|
97
|
+
self.assertEqual(builds_params[0].get("include"), "preReleaseVersion")
|
|
98
|
+
self.assertEqual(floor, "1.0")
|
|
99
|
+
self.assertFalse(
|
|
100
|
+
hasattr(asc_build_history, "fetch_build_versions"),
|
|
101
|
+
"asc_build_history.fetch_build_versions must stay removed",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def test_combined_floor_includes_prerelease(self):
|
|
105
|
+
with mock.patch.object(
|
|
106
|
+
asc_build_history, "fetch_prerelease_versions",
|
|
107
|
+
return_value=["1.0.4"]), \
|
|
108
|
+
mock.patch.object(
|
|
109
|
+
asc_build_history, "fetch_builds_prerelease_versions",
|
|
110
|
+
return_value=[]):
|
|
111
|
+
floor, _ = mmv.get_combined_floor(
|
|
112
|
+
"111", "tok",
|
|
113
|
+
appstore_versions=[_v("1.0", "READY_FOR_SALE")],
|
|
114
|
+
)
|
|
115
|
+
self.assertEqual(floor, "1.0.4")
|
|
116
|
+
|
|
117
|
+
def test_combined_floor_none_when_empty_everywhere(self):
|
|
118
|
+
with mock.patch.object(
|
|
119
|
+
asc_build_history, "fetch_prerelease_versions",
|
|
120
|
+
return_value=[]), \
|
|
121
|
+
mock.patch.object(
|
|
122
|
+
asc_build_history, "fetch_builds_prerelease_versions",
|
|
123
|
+
return_value=[]):
|
|
124
|
+
floor, _ = mmv.get_combined_floor(
|
|
125
|
+
"111", "tok", appstore_versions=[],
|
|
126
|
+
)
|
|
127
|
+
self.assertIsNone(floor)
|
|
128
|
+
|
|
129
|
+
def test_combined_floor_unknown_endpoint_shape_doesnt_crash(self):
|
|
130
|
+
def fake_request(method, path, token, *, params=None, **_kw):
|
|
131
|
+
return _FakeResponse({
|
|
132
|
+
"data": [
|
|
133
|
+
{"id": "null-version", "attributes": {}},
|
|
134
|
+
_mk_prerelease_entry("1.0.3", pid="good"),
|
|
135
|
+
],
|
|
136
|
+
"links": {},
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
with mock.patch.object(
|
|
140
|
+
asc_build_history, "request", side_effect=fake_request):
|
|
141
|
+
prereleases = asc_build_history.fetch_prerelease_versions(
|
|
142
|
+
"111", "tok"
|
|
143
|
+
)
|
|
144
|
+
self.assertEqual(prereleases, ["1.0.3"])
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# GroundTruthCrossCheckMessageTests +
|
|
148
|
+
# GroundTruthCrossCheckProductionPerSourceTests were extracted to
|
|
149
|
+
# test_mmv_floor_crosscheck.py (round 8).
|
|
150
|
+
# ComputeNextVersionStrictnessTests was extracted to
|
|
151
|
+
# test_compute_next_version.py (round 8).
|
|
152
|
+
# GroundTruthFloorTests was extracted to test_ground_truth_floor.py
|
|
153
|
+
# (round 11).
|
|
154
|
+
# All three splits keep this file under the 10-functions-per-file cap.
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
unittest.main()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for the ground-truth cross-check error message produced by
|
|
4
|
+
``assert_target_meets_floor``.
|
|
5
|
+
|
|
6
|
+
Covers the per-source breakdown surfaced in production (not
|
|
7
|
+
synthesized): when the binding floor comes from preReleaseVersions or
|
|
8
|
+
buildsViaPreRelease, the error message MUST attribute the floor to the
|
|
9
|
+
real source rather than emitting ``<none>``.
|
|
10
|
+
|
|
11
|
+
Extracted from ``test_mmv_floor_check.py`` in round 8 to keep each
|
|
12
|
+
test file under the 10-functions-per-file cap. The
|
|
13
|
+
``GroundTruthFloorTests`` and ``CombinedFloorTests`` classes still
|
|
14
|
+
live in ``test_mmv_floor_check.py``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import unittest
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from unittest import mock
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
26
|
+
|
|
27
|
+
import manage_marketing_version as mmv # noqa: E402
|
|
28
|
+
import asc_build_history # noqa: E402
|
|
29
|
+
|
|
30
|
+
from tests_common import ( # noqa: E402
|
|
31
|
+
_assert_decide_exits,
|
|
32
|
+
_assert_mentions_build_setting_source,
|
|
33
|
+
_v,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def setUpModule() -> None:
|
|
38
|
+
"""Pin auto-bump to 'none' for the whole module.
|
|
39
|
+
|
|
40
|
+
The cross-check tests assert the historical fail-on-floor error
|
|
41
|
+
message text. With auto-bump enabled (the default 'patch'), the
|
|
42
|
+
floor-violation path would auto-bump instead of failing, which
|
|
43
|
+
is a different code path covered elsewhere."""
|
|
44
|
+
global _ENV_PATCH
|
|
45
|
+
_ENV_PATCH = mock.patch.dict(
|
|
46
|
+
os.environ, {"MARKETING_VERSION_AUTO_BUMP": "none"}, clear=False,
|
|
47
|
+
)
|
|
48
|
+
_ENV_PATCH.start()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def tearDownModule() -> None:
|
|
52
|
+
_ENV_PATCH.stop()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class GroundTruthCrossCheckMessageTests(unittest.TestCase):
|
|
56
|
+
"""The cross-check error message MUST name each source separately
|
|
57
|
+
AND reference at least one build-setting source name."""
|
|
58
|
+
|
|
59
|
+
def test_error_message_lists_each_source_max(self):
|
|
60
|
+
per_source = {
|
|
61
|
+
"appStoreVersions": "1.0",
|
|
62
|
+
"preReleaseVersions": "1.0.4",
|
|
63
|
+
"buildsViaPreRelease": None,
|
|
64
|
+
}
|
|
65
|
+
stderr = _assert_decide_exits(
|
|
66
|
+
self, "1.0.0", [_v("1.0", "READY_FOR_SALE")],
|
|
67
|
+
ground_truth="1.0.4", combined_floor="1.0.4",
|
|
68
|
+
per_source=per_source,
|
|
69
|
+
)
|
|
70
|
+
self.assertIn("appStoreVersions", stderr)
|
|
71
|
+
self.assertIn("preReleaseVersions", stderr)
|
|
72
|
+
self.assertIn("buildsViaPreRelease", stderr)
|
|
73
|
+
self.assertIn("1.0.4", stderr)
|
|
74
|
+
_assert_mentions_build_setting_source(self, stderr)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class GroundTruthCrossCheckProductionPerSourceTests(unittest.TestCase):
|
|
78
|
+
"""Production seam: when the binding floor comes from preReleaseVersions
|
|
79
|
+
or buildsViaPreRelease, the error message MUST surface the real
|
|
80
|
+
per-source value (not synthesized `<none>`)."""
|
|
81
|
+
|
|
82
|
+
def test_error_attributes_floor_to_prerelease_when_prerelease_pinned(self):
|
|
83
|
+
per_source = {
|
|
84
|
+
"appStoreVersions": "1.0",
|
|
85
|
+
"preReleaseVersions": "1.1.1",
|
|
86
|
+
"buildsViaPreRelease": None,
|
|
87
|
+
}
|
|
88
|
+
stderr = _assert_decide_exits(
|
|
89
|
+
self, "1.0.0", [_v("1.0", "READY_FOR_SALE")],
|
|
90
|
+
ground_truth=None, combined_floor="1.1.1",
|
|
91
|
+
per_source=per_source,
|
|
92
|
+
)
|
|
93
|
+
self.assertIn("preReleaseVersions=1.1.1", stderr)
|
|
94
|
+
self.assertNotIn("preReleaseVersions=<none>", stderr)
|
|
95
|
+
self.assertIn("appStoreVersions=1.0", stderr)
|
|
96
|
+
self.assertIn("1.1.1", stderr)
|
|
97
|
+
_assert_mentions_build_setting_source(self, stderr)
|
|
98
|
+
|
|
99
|
+
def test_error_attributes_floor_to_buildsViaPreRelease_when_builds_pinned(self):
|
|
100
|
+
per_source = {
|
|
101
|
+
"appStoreVersions": "1.0",
|
|
102
|
+
"preReleaseVersions": None,
|
|
103
|
+
"buildsViaPreRelease": "1.2.0",
|
|
104
|
+
}
|
|
105
|
+
stderr = _assert_decide_exits(
|
|
106
|
+
self, "1.0.0", [_v("1.0", "READY_FOR_SALE")],
|
|
107
|
+
ground_truth=None, combined_floor="1.2.0",
|
|
108
|
+
per_source=per_source,
|
|
109
|
+
)
|
|
110
|
+
self.assertIn("buildsViaPreRelease=1.2.0", stderr)
|
|
111
|
+
self.assertNotIn("buildsViaPreRelease=<none>", stderr)
|
|
112
|
+
|
|
113
|
+
def test_get_combined_floor_returns_tuple_in_production(self):
|
|
114
|
+
"""The mmv.get_combined_floor wrapper now forwards (floor, per_source)
|
|
115
|
+
from asc_version_fetch verbatim -- no synthesis."""
|
|
116
|
+
with mock.patch.object(
|
|
117
|
+
asc_build_history, "fetch_prerelease_versions",
|
|
118
|
+
return_value=["1.1.1"]), \
|
|
119
|
+
mock.patch.object(
|
|
120
|
+
asc_build_history, "fetch_builds_prerelease_versions",
|
|
121
|
+
return_value=[]):
|
|
122
|
+
floor, per_source = mmv.get_combined_floor(
|
|
123
|
+
"111", "tok",
|
|
124
|
+
appstore_versions=[_v("1.0", "READY_FOR_SALE")],
|
|
125
|
+
)
|
|
126
|
+
self.assertEqual(floor, "1.1.1")
|
|
127
|
+
self.assertEqual(per_source.get("preReleaseVersions"), "1.1.1")
|
|
128
|
+
self.assertEqual(per_source.get("appStoreVersions"), "1.0")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
unittest.main()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for pure-helper functions in the manage_marketing_version family.
|
|
4
|
+
|
|
5
|
+
Covers:
|
|
6
|
+
- ``semver_tuple`` parsing edge cases.
|
|
7
|
+
- ``bump_patch_for_message`` text used inside the build-setting error
|
|
8
|
+
message.
|
|
9
|
+
|
|
10
|
+
Split out of ``test_manage_marketing_version.py`` (Issue 15: file
|
|
11
|
+
function-count cap) so each file groups tests by behaviour boundary.
|
|
12
|
+
``DecideForVersionTests`` remains in the original module since those
|
|
13
|
+
tests are the orchestrator's primary surface.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
import unittest
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
23
|
+
|
|
24
|
+
import manage_marketing_version as mmv # noqa: E402
|
|
25
|
+
|
|
26
|
+
from mmv_floor_check import bump_patch_for_message # noqa: E402
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SemverParsingTests(unittest.TestCase):
|
|
30
|
+
"""Pure-function coverage: semver_tuple."""
|
|
31
|
+
|
|
32
|
+
def test_semver_tuple_parses_three_part(self):
|
|
33
|
+
self.assertEqual(mmv.semver_tuple("1.2.3"), (1, 2, 3))
|
|
34
|
+
|
|
35
|
+
def test_semver_tuple_parses_two_part_as_zero_patch(self):
|
|
36
|
+
self.assertEqual(mmv.semver_tuple("1.2"), (1, 2, 0))
|
|
37
|
+
|
|
38
|
+
def test_semver_tuple_nonmatch_returns_sentinel(self):
|
|
39
|
+
self.assertEqual(mmv.semver_tuple("beta"), (-1, -1, -1))
|
|
40
|
+
self.assertEqual(mmv.semver_tuple(""), (-1, -1, -1))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BumpPatchForMessageTests(unittest.TestCase):
|
|
44
|
+
"""Helper used in the build-setting error message text."""
|
|
45
|
+
|
|
46
|
+
def test_bumps_three_part_patch(self):
|
|
47
|
+
self.assertEqual(bump_patch_for_message("1.0.6"), "1.0.7")
|
|
48
|
+
|
|
49
|
+
def test_treats_missing_patch_as_zero(self):
|
|
50
|
+
self.assertEqual(bump_patch_for_message("1.2"), "1.2.1")
|
|
51
|
+
|
|
52
|
+
def test_non_semver_returns_passthrough_hint(self):
|
|
53
|
+
self.assertIn("foo", bump_patch_for_message("foo"))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
unittest.main()
|