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.
@@ -1 +1 @@
1
- 1.0.1
1
+ 1.0.3
@@ -1 +1 @@
1
- 1.0.1
1
+ 1.0.3
@@ -10,7 +10,10 @@ inputs:
10
10
  required: false
11
11
  default: ""
12
12
  build-number:
13
- description: Android versionCode override; defaults to GITHUB_RUN_NUMBER
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: flutter test
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
- raw_number = os.environ.get("INPUT_BUILD_NUMBER") or os.environ.get("GITHUB_RUN_NUMBER", "")
44
- build_number = resolve_build_number(raw_number)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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: