Skip to content

Commit 7d84be0

Browse files
committed
merge with fork
2 parents 2278c48 + 140159f commit 7d84be0

23 files changed

Lines changed: 1208 additions & 163 deletions

File tree

.github/actions/setup-android-gradle/action.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,23 @@ runs:
3232
cache: gradle
3333

3434
- name: Set up Android SDK
35-
uses: android-actions/setup-android@v3.2.2
35+
uses: android-actions/setup-android@v4.0.1
3636
with:
3737
packages: platform-tools
3838

39+
- name: Restore Android SDK package cache
40+
uses: actions/cache@v4.2.4
41+
with:
42+
path: |
43+
${{ env.ANDROID_HOME }}/platforms/android-${{ inputs.android-api-level }}
44+
${{ env.ANDROID_HOME }}/build-tools/${{ inputs.android-build-tools }}
45+
key: android-sdk-packages-${{ runner.os }}-${{ inputs.android-api-level }}-${{ inputs.android-build-tools }}
46+
restore-keys: |
47+
android-sdk-packages-${{ runner.os }}-${{ inputs.android-api-level }}-
48+
android-sdk-packages-${{ runner.os }}-
49+
3950
- name: Set up Gradle
40-
uses: gradle/actions/setup-gradle@v5.0.2
51+
uses: gradle/actions/setup-gradle@v6.1.0
4152

4253
- name: Install Android SDK packages
4354
shell: bash
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
from __future__ import annotations
2+
3+
import hashlib
4+
import json
5+
import os
6+
import re
7+
import shutil
8+
from datetime import datetime, timezone
9+
from pathlib import Path
10+
11+
from github import GithubException
12+
13+
from .release import _get_release, _github_repository, _sync_release_assets
14+
15+
16+
DEFAULT_PREBUILD_RELEASE_TAG = "openvino-android-prebuilds-nightly"
17+
DEFAULT_PREBUILD_CHANNEL = "nightly"
18+
DEFAULT_APK_RELEASE_TAG = "openvino-notes-android-apk-nightly"
19+
DEFAULT_APK_RELEASE_TITLE = "OpenVINO Notes Android APK Nightly"
20+
DEFAULT_APK_RELEASE_NOTES_PREFIX = "Rolling nightly signed release APKs built from published OpenVINO Android prebuilds."
21+
APK_RELEASE_MANIFEST_NAME = "openvino-notes-android-apk-manifest.json"
22+
APK_PACKAGE_MANIFEST_PREFIX = "openvino-notes-android-apk-package-manifest"
23+
24+
25+
def _env(name: str, default: str = "") -> str:
26+
return os.environ.get(name, default).strip()
27+
28+
29+
def _write_output(name: str, value: str) -> None:
30+
output_path = _env("GITHUB_OUTPUT")
31+
if not output_path:
32+
print(f"{name}={value}", flush=True)
33+
return
34+
35+
with Path(output_path).open("a", encoding="utf-8") as output:
36+
output.write(f"{name}={value}\n")
37+
38+
39+
def _sha256(path: Path) -> str:
40+
hasher = hashlib.sha256()
41+
with path.open("rb") as source:
42+
for chunk in iter(lambda: source.read(8 * 1024 * 1024), b""):
43+
hasher.update(chunk)
44+
return hasher.hexdigest()
45+
46+
47+
def _asset_names(tag: str) -> list[str]:
48+
repository = _github_repository()
49+
release = _get_release(repository, tag)
50+
if release is None:
51+
raise SystemExit(f"GitHub release was not found: {tag}")
52+
53+
try:
54+
return sorted(
55+
asset.name for asset in release.get_assets() if getattr(asset, "state", "uploaded") == "uploaded"
56+
)
57+
except GithubException as error:
58+
raise SystemExit(f"Failed to list GitHub release assets for {tag}: {error}") from error
59+
60+
61+
def plan_apk_release_matrix_from_env() -> None:
62+
prebuild_tag = _env("PREBUILD_RELEASE_TAG", DEFAULT_PREBUILD_RELEASE_TAG)
63+
channel = _env("PREBUILD_CHANNEL", DEFAULT_PREBUILD_CHANNEL)
64+
common_asset = f"openvino-android-common-{channel}.zip"
65+
runtime_pattern = re.compile(rf"^openvino-android-runtime-(?P<abi>.+)-{re.escape(channel)}\.zip$")
66+
assets = _asset_names(prebuild_tag)
67+
68+
if common_asset not in assets:
69+
raise SystemExit(f"Required common prebuild asset is missing from {prebuild_tag}: {common_asset}")
70+
71+
matrix_entries: list[dict[str, str]] = []
72+
for asset_name in assets:
73+
match = runtime_pattern.match(asset_name)
74+
if match is None:
75+
continue
76+
abi = match.group("abi")
77+
matrix_entries.append(
78+
{
79+
"android_abi": abi,
80+
"common_asset": common_asset,
81+
"runtime_asset": asset_name,
82+
"prebuild_release_tag": prebuild_tag,
83+
}
84+
)
85+
86+
if not matrix_entries:
87+
raise SystemExit(f"No Android runtime prebuild assets found in {prebuild_tag} for channel {channel}.")
88+
89+
matrix = {"include": sorted(matrix_entries, key=lambda item: item["android_abi"])}
90+
matrix_json = json.dumps(matrix, sort_keys=True, separators=(",", ":"))
91+
_write_output("matrix", matrix_json)
92+
print(json.dumps(matrix, indent=2, sort_keys=True), flush=True)
93+
94+
95+
def package_release_apk_from_env() -> None:
96+
abi = _env("ANDROID_ABI")
97+
if not abi:
98+
raise SystemExit("ANDROID_ABI must be set to package a release APK.")
99+
100+
source_apk = Path(_env("RELEASE_APK_PATH", "app/build/outputs/apk/release/app-release.apk"))
101+
if not source_apk.is_file():
102+
raise SystemExit(f"Release APK was not found: {source_apk}")
103+
104+
artifacts_dir_value = _env("ARTIFACTS_DIR")
105+
if not artifacts_dir_value:
106+
raise SystemExit("ARTIFACTS_DIR must be set to package a release APK.")
107+
artifacts_dir = Path(artifacts_dir_value)
108+
artifacts_dir.mkdir(parents=True, exist_ok=True)
109+
110+
channel = _env("APK_CHANNEL", DEFAULT_PREBUILD_CHANNEL)
111+
apk_name = f"openvino-notes-{abi}-{channel}.apk"
112+
apk_path = artifacts_dir / apk_name
113+
shutil.copy2(source_apk, apk_path)
114+
115+
manifest = {
116+
"schema_version": 1,
117+
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
118+
"android": {
119+
"abi": abi,
120+
"runtime_asset": _env("RUNTIME_PREBUILD_ASSET"),
121+
},
122+
"prebuilds": {
123+
"release_tag": _env("PREBUILD_RELEASE_TAG", DEFAULT_PREBUILD_RELEASE_TAG),
124+
"common_asset": _env("COMMON_PREBUILD_ASSET"),
125+
"runtime_asset": _env("RUNTIME_PREBUILD_ASSET"),
126+
},
127+
"source": {
128+
"repository": _env("GITHUB_REPOSITORY"),
129+
"workflow_run_id": _env("GITHUB_RUN_ID"),
130+
"sha": _env("GITHUB_SHA"),
131+
"ref": _env("GITHUB_REF_NAME"),
132+
},
133+
"asset": {
134+
"name": apk_name,
135+
"type": "apk",
136+
"abi": abi,
137+
"size_bytes": apk_path.stat().st_size,
138+
"sha256": _sha256(apk_path),
139+
},
140+
}
141+
manifest_path = artifacts_dir / f"{APK_PACKAGE_MANIFEST_PREFIX}-{abi}.json"
142+
manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
143+
144+
print(f"{apk_path} {apk_path.stat().st_size} bytes sha256={manifest['asset']['sha256']}", flush=True)
145+
print(f"{manifest_path} {manifest_path.stat().st_size} bytes", flush=True)
146+
147+
148+
def _load_apk_package_manifests(artifacts_dir: Path) -> list[dict[str, object]]:
149+
manifests = sorted(artifacts_dir.glob(f"{APK_PACKAGE_MANIFEST_PREFIX}-*.json"))
150+
if not manifests:
151+
raise SystemExit(f"No APK package manifests found in {artifacts_dir}")
152+
return [json.loads(path.read_text(encoding="utf-8")) for path in manifests]
153+
154+
155+
def _build_apk_release_manifest(artifacts_dir: Path) -> dict[str, object]:
156+
package_manifests = _load_apk_package_manifests(artifacts_dir)
157+
apk_assets: list[dict[str, object]] = []
158+
targets: list[dict[str, object]] = []
159+
first_prebuilds = package_manifests[0].get("prebuilds", {})
160+
prebuilds = first_prebuilds if isinstance(first_prebuilds, dict) else {}
161+
162+
for package_manifest in package_manifests:
163+
asset = package_manifest.get("asset")
164+
android = package_manifest.get("android")
165+
if not isinstance(asset, dict) or not isinstance(asset.get("name"), str):
166+
raise SystemExit("Invalid APK asset entry in package manifest.")
167+
if not isinstance(android, dict):
168+
raise SystemExit("Invalid Android target entry in package manifest.")
169+
170+
apk_path = artifacts_dir / asset["name"]
171+
if not apk_path.is_file():
172+
raise SystemExit(f"APK package manifest references missing asset: {apk_path}")
173+
apk_assets.append(asset)
174+
targets.append(android)
175+
176+
return {
177+
"schema_version": 1,
178+
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
179+
"prebuilds": {
180+
"release_tag": prebuilds.get("release_tag", ""),
181+
"common_asset": prebuilds.get("common_asset", ""),
182+
},
183+
"source": package_manifests[0].get("source", {}),
184+
"android_targets": sorted(targets, key=lambda item: str(item.get("abi", ""))),
185+
"assets": sorted(apk_assets, key=lambda item: str(item.get("name", ""))),
186+
}
187+
188+
189+
def _format_size(size_bytes: object) -> str:
190+
if not isinstance(size_bytes, int):
191+
return ""
192+
return f"{size_bytes / (1024 * 1024):.1f} MiB"
193+
194+
195+
def _apk_release_notes(prefix: str, release_manifest: dict[str, object]) -> str:
196+
source = release_manifest.get("source", {})
197+
prebuilds = release_manifest.get("prebuilds", {})
198+
targets = release_manifest.get("android_targets", [])
199+
assets = release_manifest.get("assets", [])
200+
201+
lines = [prefix, ""]
202+
if isinstance(prebuilds, dict):
203+
lines.extend(
204+
[
205+
"Prebuild source:",
206+
f"- Release tag: {prebuilds.get('release_tag', '')}",
207+
f"- Common asset: {prebuilds.get('common_asset', '')}",
208+
"",
209+
]
210+
)
211+
212+
if isinstance(source, dict):
213+
lines.extend(
214+
[
215+
"App source:",
216+
f"- Repository: {source.get('repository', '')}",
217+
f"- Ref: {source.get('ref', '')}",
218+
f"- SHA: {str(source.get('sha', ''))[:12]}",
219+
"",
220+
]
221+
)
222+
223+
lines.append("Android targets:")
224+
if isinstance(targets, list):
225+
for target in targets:
226+
if isinstance(target, dict):
227+
lines.append(f"- {target.get('abi', '')}: {target.get('runtime_asset', '')}")
228+
229+
lines.extend(["", "Assets:"])
230+
if isinstance(assets, list):
231+
for asset in assets:
232+
if not isinstance(asset, dict):
233+
continue
234+
digest = str(asset.get("sha256", ""))[:12]
235+
lines.append(f"- {asset.get('name', '')}: {_format_size(asset.get('size_bytes'))}, sha256 {digest}")
236+
237+
workflow_url = ""
238+
if os.environ.get("GITHUB_SERVER_URL") and os.environ.get("GITHUB_REPOSITORY") and os.environ.get("GITHUB_RUN_ID"):
239+
workflow_url = f"{os.environ['GITHUB_SERVER_URL']}/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}"
240+
if workflow_url:
241+
lines.extend(["", f"Workflow run: {workflow_url}"])
242+
243+
return "\n".join(lines) + "\n"
244+
245+
246+
def publish_apk_release_from_env() -> None:
247+
artifacts_dir_value = _env("ARTIFACTS_DIR")
248+
if not artifacts_dir_value:
249+
raise SystemExit("ARTIFACTS_DIR must be set to publish APK release assets.")
250+
artifacts_dir = Path(artifacts_dir_value)
251+
252+
release_manifest = _build_apk_release_manifest(artifacts_dir)
253+
release_manifest_path = artifacts_dir / APK_RELEASE_MANIFEST_NAME
254+
release_manifest_path.write_text(json.dumps(release_manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
255+
256+
notes_prefix = _env("RELEASE_NOTES_PREFIX", DEFAULT_APK_RELEASE_NOTES_PREFIX)
257+
notes = _apk_release_notes(notes_prefix, release_manifest)
258+
(artifacts_dir / "apk-release-notes.md").write_text(notes, encoding="utf-8")
259+
260+
tag = _env("RELEASE_TAG", DEFAULT_APK_RELEASE_TAG)
261+
title = _env("RELEASE_TITLE", DEFAULT_APK_RELEASE_TITLE)
262+
repository = _github_repository()
263+
release = _get_release(repository, tag)
264+
try:
265+
if release is None:
266+
print(f"Creating GitHub prerelease: {tag}", flush=True)
267+
release = repository.create_git_release(
268+
tag=tag,
269+
name=title,
270+
message=notes,
271+
prerelease=True,
272+
make_latest="false",
273+
)
274+
else:
275+
print(f"Updating GitHub prerelease: {tag}", flush=True)
276+
release = release.update_release(
277+
name=title,
278+
message=notes,
279+
draft=False,
280+
prerelease=True,
281+
tag_name=tag,
282+
make_latest="false",
283+
)
284+
except GithubException as error:
285+
raise SystemExit(f"Failed to create or update GitHub release {tag}: {error}") from error
286+
287+
apks = sorted(artifacts_dir.glob("*.apk"))
288+
if not apks:
289+
raise SystemExit(f"No APK assets found in {artifacts_dir}")
290+
_sync_release_assets(release, [*apks, release_manifest_path])

0 commit comments

Comments
 (0)