Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/mobile-app-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ jobs:
path: ${{ steps.stage_apk.outputs.apk_path }}

- name: Ensure GitHub Release exists
if: ${{ github.event_name == 'push' || github.event.inputs.upload_to_release != 'false' }}
# Tagged APKs have one publisher: android-apk.yml. This combined
# workflow may still publish Android when explicitly dispatched.
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.upload_to_release != 'false' }}
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.event.inputs.release_tag || github.ref_name || 'v0.1.78' }}
Expand All @@ -172,7 +174,7 @@ jobs:
fi

- name: Upload APK to GitHub Release
if: ${{ github.event_name == 'push' || github.event.inputs.upload_to_release != 'false' }}
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.upload_to_release != 'false' }}
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.event.inputs.release_tag || github.ref_name || 'v0.1.78' }}
Expand Down
16 changes: 10 additions & 6 deletions mobile_agent/tooling/test_verify_ios_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@
import unittest
from pathlib import Path

from verify_ios_app import verify
from verify_ios_app import _read_version, verify


class VerifyIosAppTest(unittest.TestCase):
def setUp(self) -> None:
self.semantic, self.build = _read_version()
self.expected_tag = f'v{self.semantic}'

def _app(self, directory: Path) -> Path:
app = directory / 'Runner.app'
app.mkdir()
(app / 'Info.plist').write_bytes(
plistlib.dumps(
{
'CFBundleShortVersionString': '0.1.73',
'CFBundleVersion': '63',
'CFBundleShortVersionString': self.semantic,
'CFBundleVersion': self.build,
'NSMicrophoneUsageDescription': 'Microphone prompt',
'NSSpeechRecognitionUsageDescription': 'Speech prompt',
}
Expand All @@ -31,7 +35,7 @@ def test_valid_bundle_and_clean_log_pass(self) -> None:
log = directory / 'runner.log'
log.write_text('Runner launched normally.', encoding='utf-8')

verify(app, 'v0.1.73', log)
verify(app, self.expected_tag, log)

def test_nested_usage_description_does_not_pass(self) -> None:
with tempfile.TemporaryDirectory() as directory_name:
Expand All @@ -45,7 +49,7 @@ def test_nested_usage_description_does_not_pass(self) -> None:
(app / 'Info.plist').write_bytes(plistlib.dumps(document))

with self.assertRaisesRegex(ValueError, 'top-level'):
verify(app, 'v0.1.73', None)
verify(app, self.expected_tag, None)

def test_privacy_crash_signature_fails(self) -> None:
with tempfile.TemporaryDirectory() as directory_name:
Expand All @@ -59,7 +63,7 @@ def test_privacy_crash_signature_fails(self) -> None:
)

with self.assertRaisesRegex(ValueError, 'crash signature'):
verify(app, 'v0.1.73', log)
verify(app, self.expected_tag, log)


if __name__ == '__main__':
Expand Down
39 changes: 38 additions & 1 deletion scripts/test_verify_public_release_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

sys.path.insert(0, str(Path(__file__).resolve().parent))

from verify_public_release_workflows import find_violations, verify
from verify_public_release_workflows import (
ANDROID_WORKFLOW,
COMBINED_WORKFLOW,
find_apk_publisher_violations,
find_violations,
verify,
)


class PublicReleaseWorkflowPolicyTest(unittest.TestCase):
Expand Down Expand Up @@ -50,6 +56,37 @@ def test_missing_workflow_fails_closed(self) -> None:

self.assertEqual(["missing.yml: required public release workflow is missing"], violations)

def test_tagged_apk_has_one_release_publisher(self) -> None:
contents = {
ANDROID_WORKFLOW: "- name: Upload APK to GitHub Release\n run: gh release upload",
COMBINED_WORKFLOW: "\n".join(
(
" - name: Upload APK to GitHub Release",
" if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.upload_to_release != 'false' }}",
" run: gh release upload",
)
),
}

self.assertEqual([], find_apk_publisher_violations(contents))

def test_combined_tagged_apk_publisher_is_rejected(self) -> None:
contents = {
ANDROID_WORKFLOW: "- name: Upload APK to GitHub Release\n run: gh release upload",
COMBINED_WORKFLOW: "\n".join(
(
" - name: Upload APK to GitHub Release",
" if: ${{ github.event_name == 'push' || github.event.inputs.upload_to_release != 'false' }}",
" run: gh release upload",
)
),
}

violations = find_apk_publisher_violations(contents)

self.assertEqual(1, len(violations))
self.assertIn("manual-only", violations[0])


if __name__ == "__main__":
unittest.main()
37 changes: 36 additions & 1 deletion scripts/verify_public_release_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
"--dart-define-from-file",
)

ANDROID_WORKFLOW = Path(".github/workflows/android-apk.yml")
COMBINED_WORKFLOW = Path(".github/workflows/mobile-app-release.yml")
APK_UPLOAD_STEP = "- name: Upload APK to GitHub Release"
MANUAL_ONLY_UPLOAD_CONDITION = (
"if: ${{ github.event_name == 'workflow_dispatch' && "
"github.event.inputs.upload_to_release != 'false' }}"
)


def find_violations(path: Path, content: str) -> list[str]:
violations: list[str] = []
Expand All @@ -34,14 +42,41 @@ def find_violations(path: Path, content: str) -> list[str]:
return violations


def find_apk_publisher_violations(contents: dict[Path, str]) -> list[str]:
android = contents.get(ANDROID_WORKFLOW)
combined = contents.get(COMBINED_WORKFLOW)
if android is None or combined is None:
return []

violations: list[str] = []
if APK_UPLOAD_STEP not in android:
violations.append(
f"{ANDROID_WORKFLOW}: dedicated tagged APK publisher is missing"
)

upload_start = combined.find(APK_UPLOAD_STEP)
upload_end = combined.find("\n - name:", upload_start + 1)
upload_block = combined[upload_start:upload_end if upload_end >= 0 else None]
if upload_start < 0 or MANUAL_ONLY_UPLOAD_CONDITION not in upload_block:
violations.append(
f"{COMBINED_WORKFLOW}: APK Release upload must be manual-only; "
f"tagged APKs are published by {ANDROID_WORKFLOW}"
)
return violations


def verify(root: Path, workflow_paths: tuple[Path, ...]) -> list[str]:
violations: list[str] = []
contents: dict[Path, str] = {}
for relative_path in workflow_paths:
path = root / relative_path
if not path.is_file():
violations.append(f"{relative_path}: required public release workflow is missing")
continue
violations.extend(find_violations(relative_path, path.read_text(encoding="utf-8")))
content = path.read_text(encoding="utf-8")
contents[relative_path] = content
violations.extend(find_violations(relative_path, content))
violations.extend(find_apk_publisher_violations(contents))
return violations


Expand Down