gowalk-cicd 1.0.1 → 1.0.3
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/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/action.yml +19 -2
- package/android-action/scripts/resolve_android.py +75 -2
- package/android-action/scripts/test_android_config.py +37 -0
- package/package.json +1 -1
- package/templates/deploy.yml +7 -0
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.3
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.3
|
|
@@ -10,7 +10,10 @@ inputs:
|
|
|
10
10
|
required: false
|
|
11
11
|
default: ""
|
|
12
12
|
build-number:
|
|
13
|
-
description:
|
|
13
|
+
description: >-
|
|
14
|
+
Android versionCode override. When unset, the action uses
|
|
15
|
+
max(GITHUB_RUN_NUMBER, highest versionCode already on Play + 1), so a repo whose
|
|
16
|
+
CI run numbers start at 1 cannot collide with codes Play already holds.
|
|
14
17
|
required: false
|
|
15
18
|
default: ""
|
|
16
19
|
build-name:
|
|
@@ -39,6 +42,13 @@ outputs:
|
|
|
39
42
|
runs:
|
|
40
43
|
using: composite
|
|
41
44
|
steps:
|
|
45
|
+
# resolve_android.py asks Google Play for the highest versionCode already
|
|
46
|
+
# uploaded, so it needs these. Installing them here (rather than relying on the
|
|
47
|
+
# caller) keeps the action self-contained.
|
|
48
|
+
- name: Install Play API dependencies
|
|
49
|
+
shell: bash
|
|
50
|
+
run: python3 -m pip install --disable-pip-version-check -q 'google-auth>=2.40,<3' 'requests>=2.32,<3' || true
|
|
51
|
+
|
|
42
52
|
- name: Resolve Android app and credentials
|
|
43
53
|
id: config
|
|
44
54
|
shell: bash
|
|
@@ -56,10 +66,17 @@ runs:
|
|
|
56
66
|
shell: bash
|
|
57
67
|
run: flutter analyze
|
|
58
68
|
|
|
69
|
+
# `flutter test` hard-fails with "Test directory \"test\" not found" when a repo
|
|
70
|
+
# has no tests, which is not a real failure. Skip instead of blowing up the deploy.
|
|
59
71
|
- name: Test Flutter app
|
|
60
72
|
if: ${{ inputs.run-tests == 'true' }}
|
|
61
73
|
shell: bash
|
|
62
|
-
run:
|
|
74
|
+
run: |
|
|
75
|
+
if [ -d test ]; then
|
|
76
|
+
flutter test
|
|
77
|
+
else
|
|
78
|
+
echo "No test/ directory; skipping flutter test."
|
|
79
|
+
fi
|
|
63
80
|
|
|
64
81
|
- name: Build release Android App Bundle
|
|
65
82
|
shell: bash
|
|
@@ -30,6 +30,76 @@ def resolve_build_number(raw: str) -> int:
|
|
|
30
30
|
return number
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def highest_play_version_code(package_name: str, service_account: Path) -> int | None:
|
|
34
|
+
"""Highest versionCode Google Play already holds for this package, or None.
|
|
35
|
+
|
|
36
|
+
Best-effort: any failure (offline, no permission, app not yet created) returns
|
|
37
|
+
None so the caller falls back to GITHUB_RUN_NUMBER.
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
import google.auth.transport.requests # noqa: PLC0415
|
|
41
|
+
import requests # noqa: PLC0415
|
|
42
|
+
from google.oauth2 import service_account as gsa # noqa: PLC0415
|
|
43
|
+
|
|
44
|
+
creds = gsa.Credentials.from_service_account_file(
|
|
45
|
+
str(service_account),
|
|
46
|
+
scopes=["https://www.googleapis.com/auth/androidpublisher"],
|
|
47
|
+
)
|
|
48
|
+
creds.refresh(google.auth.transport.requests.Request())
|
|
49
|
+
base = "https://androidpublisher.googleapis.com/androidpublisher/v3"
|
|
50
|
+
headers = {"Authorization": f"Bearer {creds.token}"}
|
|
51
|
+
|
|
52
|
+
edit = requests.post(
|
|
53
|
+
f"{base}/applications/{package_name}/edits", headers=headers, timeout=60
|
|
54
|
+
)
|
|
55
|
+
edit.raise_for_status()
|
|
56
|
+
edit_id = edit.json()["id"]
|
|
57
|
+
try:
|
|
58
|
+
listed = requests.get(
|
|
59
|
+
f"{base}/applications/{package_name}/edits/{edit_id}/bundles",
|
|
60
|
+
headers=headers,
|
|
61
|
+
timeout=60,
|
|
62
|
+
)
|
|
63
|
+
listed.raise_for_status()
|
|
64
|
+
codes = [
|
|
65
|
+
int(b["versionCode"])
|
|
66
|
+
for b in listed.json().get("bundles", [])
|
|
67
|
+
if b.get("versionCode") is not None
|
|
68
|
+
]
|
|
69
|
+
finally:
|
|
70
|
+
requests.delete(
|
|
71
|
+
f"{base}/applications/{package_name}/edits/{edit_id}",
|
|
72
|
+
headers=headers,
|
|
73
|
+
timeout=60,
|
|
74
|
+
)
|
|
75
|
+
return max(codes) if codes else None
|
|
76
|
+
except Exception as exc: # noqa: BLE001 - never fail the build over this
|
|
77
|
+
print(f"::warning::Could not read existing Play versionCodes ({exc}); "
|
|
78
|
+
f"falling back to GITHUB_RUN_NUMBER")
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def auto_build_number(package_name: str, service_account: Path) -> int:
|
|
83
|
+
"""versionCode to use when the caller did not pin one.
|
|
84
|
+
|
|
85
|
+
GITHUB_RUN_NUMBER alone is wrong: it restarts at 1 on a freshly-onboarded repo,
|
|
86
|
+
so it collides with versionCodes Play already holds and every upload is rejected
|
|
87
|
+
("Version code N has already been used"). Take whichever is higher: the run
|
|
88
|
+
number, or one past the highest code Play knows about.
|
|
89
|
+
"""
|
|
90
|
+
run_number = resolve_build_number(os.environ.get("GITHUB_RUN_NUMBER", "1"))
|
|
91
|
+
highest = highest_play_version_code(package_name, service_account)
|
|
92
|
+
if highest is None:
|
|
93
|
+
return run_number
|
|
94
|
+
candidate = max(run_number, highest + 1)
|
|
95
|
+
if candidate != run_number:
|
|
96
|
+
print(
|
|
97
|
+
f"Play already holds versionCode {highest}; using {candidate} "
|
|
98
|
+
f"instead of GITHUB_RUN_NUMBER {run_number}"
|
|
99
|
+
)
|
|
100
|
+
return candidate
|
|
101
|
+
|
|
102
|
+
|
|
33
103
|
def main() -> None:
|
|
34
104
|
workspace = Path(os.environ.get("GITHUB_WORKSPACE", ".")).resolve()
|
|
35
105
|
env_path = Path(os.environ["GITHUB_ENV"])
|
|
@@ -40,8 +110,11 @@ def main() -> None:
|
|
|
40
110
|
signing = find_signing_config(workspace)
|
|
41
111
|
service_account = find_play_service_account(workspace)
|
|
42
112
|
package_name = os.environ.get("INPUT_PACKAGE_NAME") or detect_package_name(workspace)
|
|
43
|
-
|
|
44
|
-
|
|
113
|
+
pinned = os.environ.get("INPUT_BUILD_NUMBER") or ""
|
|
114
|
+
if pinned:
|
|
115
|
+
build_number = resolve_build_number(pinned)
|
|
116
|
+
else:
|
|
117
|
+
build_number = auto_build_number(package_name, service_account)
|
|
45
118
|
except (ConfigError, KeyError, OSError) as exc:
|
|
46
119
|
print(f"::error::{exc}")
|
|
47
120
|
raise SystemExit(1) from exc
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
import tempfile
|
|
7
8
|
import unittest
|
|
8
9
|
from pathlib import Path
|
|
10
|
+
from unittest import mock
|
|
9
11
|
|
|
10
12
|
from android_config import (
|
|
11
13
|
ConfigError,
|
|
@@ -95,3 +97,38 @@ class AndroidConfigTest(unittest.TestCase):
|
|
|
95
97
|
|
|
96
98
|
if __name__ == "__main__":
|
|
97
99
|
unittest.main()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AutoBuildNumberTest(unittest.TestCase):
|
|
103
|
+
"""versionCode must never collide with what Play already holds."""
|
|
104
|
+
|
|
105
|
+
def test_uses_play_high_water_mark_when_run_number_is_lower(self) -> None:
|
|
106
|
+
import resolve_android
|
|
107
|
+
|
|
108
|
+
with mock.patch.object(resolve_android, "highest_play_version_code", return_value=173):
|
|
109
|
+
with mock.patch.dict(os.environ, {"GITHUB_RUN_NUMBER": "1"}):
|
|
110
|
+
# A freshly-onboarded repo starts at run 1; Play already has 173.
|
|
111
|
+
self.assertEqual(
|
|
112
|
+
resolve_android.auto_build_number("com.example.app", Path("sa.json")),
|
|
113
|
+
174,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def test_uses_run_number_when_it_is_already_higher(self) -> None:
|
|
117
|
+
import resolve_android
|
|
118
|
+
|
|
119
|
+
with mock.patch.object(resolve_android, "highest_play_version_code", return_value=5):
|
|
120
|
+
with mock.patch.dict(os.environ, {"GITHUB_RUN_NUMBER": "42"}):
|
|
121
|
+
self.assertEqual(
|
|
122
|
+
resolve_android.auto_build_number("com.example.app", Path("sa.json")),
|
|
123
|
+
42,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def test_falls_back_to_run_number_when_play_is_unreachable(self) -> None:
|
|
127
|
+
import resolve_android
|
|
128
|
+
|
|
129
|
+
with mock.patch.object(resolve_android, "highest_play_version_code", return_value=None):
|
|
130
|
+
with mock.patch.dict(os.environ, {"GITHUB_RUN_NUMBER": "7"}):
|
|
131
|
+
self.assertEqual(
|
|
132
|
+
resolve_android.auto_build_number("com.example.app", Path("sa.json")),
|
|
133
|
+
7,
|
|
134
|
+
)
|
package/package.json
CHANGED
package/templates/deploy.yml
CHANGED
|
@@ -61,6 +61,13 @@ jobs:
|
|
|
61
61
|
# AppLocalizations now generate real sources. Nothing else generates them,
|
|
62
62
|
# and analyze/compile fail on the missing import if we skip this.
|
|
63
63
|
if [ -f l10n.yaml ]; then flutter gen-l10n; fi
|
|
64
|
+
# Apps wired up by `flutterfire configure` carry an Xcode run-script phase that
|
|
65
|
+
# shells out to the flutterfire CLI to upload Crashlytics dSYMs. That CLI is not
|
|
66
|
+
# on a clean runner, so the phase exits non-zero and fails the whole archive.
|
|
67
|
+
if grep -q "firebase_crashlytics:" pubspec.yaml 2>/dev/null; then
|
|
68
|
+
dart pub global activate flutterfire_cli >/dev/null 2>&1 || true
|
|
69
|
+
echo "$HOME/.pub-cache/bin" >> "$GITHUB_PATH"
|
|
70
|
+
fi
|
|
64
71
|
flutter build ios --release --no-codesign --config-only
|
|
65
72
|
- uses: ./.github/actions/swift-app
|
|
66
73
|
with:
|