ci: target Gitea Ubuntu and native Apple Silicon runners
iOS validation / simulator (push) Failing after 3s
Android validation / emulator (35) (push) Failing after 9s
Android validation / emulator (37.0) (push) Failing after 9s
Android validation / build (push) Successful in 3m40s

Move workflows into Gitea's workflow directory and use the supplied ubuntu-latest and macos-arm64 labels. Provision the Android SDK, require KVM without changing host permissions, support SHA-256 checkout and Gitea artifact uploads, and isolate scratch output on persistent runners. Skip untrusted fork PRs and keep release builds unsigned because Gitea ignores GitHub environment approval gates. Local workflow lint, repository invariants, ten preflight regressions, eight evidence-verifier tests, inline shell checks, scratch cleanup and SHA-256 fetch checks pass; remote runner execution remains unverified.
This commit is contained in:
Yuvomi Mobile Contributors committed 2026-10-21 07:40:19 +02:00
1 parent 2d12491516
commit 11bf4ca620
16 files changed
+442 -192

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
name: Android validation
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: android-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
env:
GIT_DEFAULT_HASH: sha256
with:
persist-credentials: false
- run: bash scripts/check-ci-runner.sh android
- uses: https://github.com/actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
with:
distribution: temurin
java-version: '17'
- uses: https://github.com/android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
with:
packages: 'platform-tools platforms;android-37.0 build-tools;36.0.0'
log-accepted-android-sdk-licenses: false
- name: Build, test and lint (release remains unsigned)
run: bash android/gradlew -p android testDebugUnitTest lintDebug lintRelease assembleDebug assembleRelease bundleRelease
- name: Source formatting and repository invariants
run: |
bash scripts/check-format.sh
python3 scripts/check-repository.py
python3 -m py_compile scripts/*.py
python3 scripts/test-verifier.py
python3 scripts/test-ci-runner.py
- name: Check dependencies against OSV
run: python3 scripts/check-dependencies.py
- uses: https://gitea.com/actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de # v3.2.2-node20 (Gitea artifact protocol)
if: always()
with:
name: android-builds-and-reports
path: |
android/app/build/outputs/apk/debug/*.apk
android/app/build/outputs/apk/release/*unsigned.apk
android/app/build/outputs/bundle/release/*.aab
android/app/build/reports/
retention-days: 14
emulator:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
api: ['35', '37.0']
timeout-minutes: 60
steps:
- uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262
env:
GIT_DEFAULT_HASH: sha256
with:
persist-credentials: false
- run: bash scripts/check-ci-runner.sh emulator
- uses: https://github.com/actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3
with:
distribution: temurin
java-version: '17'
- uses: https://github.com/android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407
with:
packages: 'platform-tools platforms;android-37.0 build-tools;36.0.0'
log-accepted-android-sdk-licenses: false
- uses: https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22.22.1'
- name: Prepare pinned, unmodified upstream
run: |
upstream=$(mktemp -d "$RUNNER_TEMP/yuvomi-upstream.XXXXXX")
echo "YUVOMI_UPSTREAM=$upstream" >> "$GITHUB_ENV"
git clone --filter=blob:none https://github.com/ulsklyc/yuvomi.git "$upstream"
git -C "$upstream" checkout 10bfef7686ed7303b4c74c7fcc55a5eb16356c9d
npm ci --omit=dev --prefix "$upstream"
- uses: https://github.com/reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2
with:
api-level: ${{ matrix.api }}
arch: x86_64
target: google_apis
disable-animations: true
script: |
bash android/gradlew -p android connectedDebugAndroidTest
python3 scripts/verify-test-results.py android/app/build/outputs/androidTest-results/connected/debug --minimum 6
python3 scripts/test-integration.py "$YUVOMI_UPSTREAM" --android
- uses: https://gitea.com/actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de
if: always()
with:
name: android-device-test-reports-${{ matrix.api }}
path: android/app/build/reports/androidTests/
retention-days: 14
- name: Remove only this job's allocated upstream fixture
if: always()
run: |
case "${YUVOMI_UPSTREAM:-}" in
"$RUNNER_TEMP"/yuvomi-upstream.*) rm -rf -- "$YUVOMI_UPSTREAM" ;;
esac
+55
View File
@@ -0,0 +1,55 @@
name: iOS validation
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ios-${{ github.ref }}
cancel-in-progress: true
jobs:
simulator:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: macos-arm64
timeout-minutes: 60
steps:
- uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262
env:
GIT_DEFAULT_HASH: sha256
with:
persist-credentials: false
- run: bash scripts/check-ci-runner.sh ios
- name: Allocate isolated scratch space on the persistent Mac runner
run: |
scratch=$(mktemp -d "$RUNNER_TEMP/yuvomi-ios.XXXXXX")
echo "YUVOMI_CI_TEMP=$scratch" >> "$GITHUB_ENV"
- name: Install verified XcodeGen and generate project
run: |
bash scripts/install-xcodegen.sh "$YUVOMI_CI_TEMP/xcodegen"
"$YUVOMI_CI_TEMP/xcodegen/xcodegen/bin/xcodegen" generate --spec ios/project.yml
xcodebuild -version
- name: Portable URL policy tests and Swift syntax
run: |
swift test --package-path ios --scratch-path "$YUVOMI_CI_TEMP/SwiftPolicy"
swiftc -frontend -parse ios/Sources/*.swift ios/Tests/*.swift ios/UITests/*.swift
- name: Simulator build and XCTest (no production signing)
run: |
device=$(python3 scripts/select-simulator.py)
xcodebuild -project ios/Yuvomi.xcodeproj -scheme Yuvomi -destination "platform=iOS Simulator,id=$device" -derivedDataPath "$YUVOMI_CI_TEMP/DerivedData" -resultBundlePath "$YUVOMI_CI_TEMP/Simulator.xcresult" CODE_SIGNING_ALLOWED=NO test
- name: Unsigned device archive validation
run: xcodebuild -project ios/Yuvomi.xcodeproj -scheme Yuvomi -destination 'generic/platform=iOS' -derivedDataPath "$YUVOMI_CI_TEMP/DerivedData" -archivePath "$YUVOMI_CI_TEMP/Yuvomi-unsigned.xcarchive" CODE_SIGNING_ALLOWED=NO archive
- uses: https://gitea.com/actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de
if: always() && env.YUVOMI_CI_TEMP != ''
with:
name: ios-unsigned-archive-and-test-results
path: |
${{ env.YUVOMI_CI_TEMP }}/Simulator.xcresult
${{ env.YUVOMI_CI_TEMP }}/Yuvomi-unsigned.xcarchive
retention-days: 14
- name: Remove only this job's allocated scratch directory
if: always()
run: |
case "${YUVOMI_CI_TEMP:-}" in
"$RUNNER_TEMP"/yuvomi-ios.*) rm -rf -- "$YUVOMI_CI_TEMP" ;;
esac
+33
View File
@@ -0,0 +1,33 @@
name: Maintainer unsigned release build (never publishes)
on:
workflow_dispatch:
permissions:
contents: read
jobs:
android:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262
env:
GIT_DEFAULT_HASH: sha256
with:
persist-credentials: false
- run: bash scripts/check-ci-runner.sh android
- uses: https://github.com/actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3
with:
distribution: temurin
java-version: '17'
- uses: https://github.com/android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407
with:
packages: 'platform-tools platforms;android-37.0 build-tools;36.0.0'
log-accepted-android-sdk-licenses: false
- run: bash android/gradlew -p android testDebugUnitTest lintRelease assembleRelease bundleRelease
# Gitea ignores GitHub environment approval gates: never load signing keys here.
- uses: https://gitea.com/actions/upload-artifact@c6a3b2bd78b3985e4b2f15397fec357f0fd808de
with:
name: maintainer-android-release-builds
path: |
android/app/build/outputs/apk/release/*.apk
android/app/build/outputs/bundle/release/*.aab
retention-days: 7
-82
View File
@@ -1,82 +0,0 @@
name: Android validation
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: android-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
with:
distribution: temurin
java-version: '17'
- name: Build, test and lint (release remains unsigned)
run: bash android/gradlew -p android testDebugUnitTest lintDebug lintRelease assembleDebug assembleRelease bundleRelease
- name: Source formatting and repository invariants
run: |
bash scripts/check-format.sh
python3 scripts/check-repository.py
python3 -m py_compile scripts/*.py
python3 scripts/test-verifier.py
- name: Check dependencies against OSV
run: python3 scripts/check-dependencies.py
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: android-builds-and-reports
path: |
android/app/build/outputs/apk/debug/*.apk
android/app/build/outputs/apk/release/*unsigned.apk
android/app/build/outputs/bundle/release/*.aab
android/app/build/reports/
retention-days: 14
emulator:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
api: ['35', '37.0']
timeout-minutes: 35
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3
with:
distribution: temurin
java-version: '17'
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22.22.1'
- name: Prepare pinned, unmodified upstream
run: |
git clone --filter=blob:none https://github.com/ulsklyc/yuvomi.git "$RUNNER_TEMP/upstream"
git -C "$RUNNER_TEMP/upstream" checkout 10bfef7686ed7303b4c74c7fcc55a5eb16356c9d
npm ci --omit=dev --prefix "$RUNNER_TEMP/upstream"
- name: Enable KVM for the emulator
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2
with:
api-level: ${{ matrix.api }}
arch: x86_64
target: google_apis
disable-animations: true
script: |
bash android/gradlew -p android connectedDebugAndroidTest
python3 scripts/verify-test-results.py android/app/build/outputs/androidTest-results/connected/debug --minimum 6
python3 scripts/test-integration.py "$RUNNER_TEMP/upstream" --android
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
if: always()
with:
name: android-device-test-reports-${{ matrix.api }}
path: android/app/build/reports/androidTests/
retention-days: 14
-39
View File
@@ -1,39 +0,0 @@
name: iOS validation
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ios-${{ github.ref }}
cancel-in-progress: true
jobs:
simulator:
runs-on: macos-15
timeout-minutes: 35
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- name: Install verified XcodeGen and generate project
run: |
bash scripts/install-xcodegen.sh "$RUNNER_TEMP/xcodegen"
"$RUNNER_TEMP/xcodegen/xcodegen/bin/xcodegen" generate --spec ios/project.yml
xcodebuild -version
- name: Portable URL policy tests and Swift syntax
run: |
swift test --package-path ios
swiftc -frontend -parse ios/Sources/*.swift ios/Tests/*.swift ios/UITests/*.swift
- name: Simulator build and XCTest (no production signing)
run: |
device=$(python3 scripts/select-simulator.py)
xcodebuild -project ios/Yuvomi.xcodeproj -scheme Yuvomi -destination "platform=iOS Simulator,id=$device" -derivedDataPath "$RUNNER_TEMP/DerivedData" -resultBundlePath "$RUNNER_TEMP/Simulator.xcresult" CODE_SIGNING_ALLOWED=NO test
- name: Unsigned device archive validation
run: xcodebuild -project ios/Yuvomi.xcodeproj -scheme Yuvomi -destination 'generic/platform=iOS' -archivePath "$RUNNER_TEMP/Yuvomi-unsigned.xcarchive" CODE_SIGNING_ALLOWED=NO archive
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
if: always()
with:
name: ios-unsigned-archive-and-test-results
path: |
${{ runner.temp }}/Simulator.xcresult
${{ runner.temp }}/Yuvomi-unsigned.xcarchive
retention-days: 14
-48
View File
@@ -1,48 +0,0 @@
name: Maintainer release build (never publishes)
on:
workflow_dispatch:
inputs:
sign_android:
description: Sign APK and AAB using the protected release environment
type: boolean
default: false
permissions:
contents: read
jobs:
android:
runs-on: ubuntu-24.04
environment: release
timeout-minutes: 30
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
- uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3
with:
distribution: temurin
java-version: '17'
- run: bash android/gradlew -p android testDebugUnitTest lintRelease assembleRelease bundleRelease
- name: Sign with maintainer-supplied credentials
if: inputs.sign_android
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
run: |
set -euo pipefail
umask 077
test -n "$KEYSTORE_BASE64" && test -n "$STORE_PASSWORD" && test -n "$KEY_PASSWORD" && test -n "$KEY_ALIAS"
trap 'rm -f "$RUNNER_TEMP/release.jks"' EXIT
printf '%s' "$KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/release.jks"
tools="$ANDROID_HOME/build-tools/36.0.0"
"$tools/zipalign" -p -f 4 android/app/build/outputs/apk/release/app-release-unsigned.apk "$RUNNER_TEMP/aligned.apk"
"$tools/apksigner" sign --ks "$RUNNER_TEMP/release.jks" --ks-key-alias "$KEY_ALIAS" --ks-pass env:STORE_PASSWORD --key-pass env:KEY_PASSWORD --out android/app/build/outputs/apk/release/yuvomi-release-signed.apk "$RUNNER_TEMP/aligned.apk"
"$tools/apksigner" verify --verbose android/app/build/outputs/apk/release/yuvomi-release-signed.apk
jarsigner -keystore "$RUNNER_TEMP/release.jks" -storepass:env STORE_PASSWORD -keypass:env KEY_PASSWORD android/app/build/outputs/bundle/release/app-release.aab "$KEY_ALIAS"
jarsigner -verify android/app/build/outputs/bundle/release/app-release.aab
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: maintainer-android-release-builds
path: |
android/app/build/outputs/apk/release/*.apk
android/app/build/outputs/bundle/release/*.aab
retention-days: 7
+4 -1
View File
@@ -2,12 +2,15 @@
## [Unreleased]
- Gitea workflows for the maintainer's Ubuntu pool and native M4 Pro runner,
explicit SDK/KVM prerequisites, SHA-256 checkout and unsigned-only CI release builds.
- Native Android HTTPS onboarding, origin-bound browsing, session persistence,
recovery, server erasure, camera/document selection, downloads and blob exports.
- SwiftUI/WKWebView iOS implementation, isolated web sign-in, native settings,
recovery, file export, and strict URL/metadata policies; Xcode validation pending.
- JVM, Android emulator, portable Swift, and real pinned-upstream HTTPS tests.
- Reproducible build configuration, CI and maintainer-controlled signing workflow.
- Reproducible build configuration, CI and maintainer-controlled release procedures.
- MIT licensing, upstream audit, security model and release documentation.
No stable release has been published. No native push or browser-to-app session
+1
View File
@@ -18,6 +18,7 @@ reconnect or erase local browsing data and switch installations.
- [Android build](docs/build-android.md) / [iOS build](docs/build-ios.md)
- [Compatibility and authentication](docs/server-compatibility.md)
- [Release process](docs/release-process.md)
- [Gitea CI runners](docs/ci-runners.md)
- [Maintainer continuation](docs/continuation.md)
MIT licensed. Upstream notices are preserved in `THIRD_PARTY_NOTICES.md`.
+80
View File
@@ -0,0 +1,80 @@
# Gitea CI runners
Workflows live in `.gitea/workflows/` for `git.nesterovic.cc`. Enable repository
Actions in Gitea settings. Scheduling uses the labels supplied by the maintainer:
| Job | `runs-on` | Eligible runners |
| --- | --- | --- |
| Android builds, JVM/lint checks | `ubuntu-latest` | `ubuntu-runner-1` through `ubuntu-runner-6` |
| Android emulator matrix (API 35 / 37.0) | `ubuntu-latest` | Same Ubuntu pool, with KVM access |
| Manual unsigned Android release build | `ubuntu-latest` | Same Ubuntu pool |
| iOS simulator, XCTest and unsigned archive | `macos-arm64` | `macos-m4pro` |
Runner names are informational; Gitea matches labels. The Mac's additional
`macos` label is not needed. No `self-hosted` label is requested because it is not
present in the supplied configuration. No runner registration or host settings
are changed by these workflows.
## Ubuntu prerequisites
The job environment must be Linux x86_64 with Bash, Git (SHA-256 capable), curl,
unzip, Python 3 and the shared libraries needed by the Android SDK/emulator and
Node's native build tools. Use a maintained Ubuntu runner image with compiler,
make, SQLite build prerequisites and emulator runtime libraries available. The
pinned setup actions provision Temurin 17, Android SDK packages and Node 22 for
the upstream fixture. SDK installation accepts the Android SDK licenses.
Emulator jobs require virtualization and readable/writable `/dev/kvm` **inside
the job environment**. For container-backed runners, the administrator must pass
the device through (for example `--device=/dev/kvm`) and configure group access.
VM hosts need nested virtualization. The preflight fails with a clear message
instead of attempting privileged host changes or silently using slow emulation.
All runners sharing `ubuntu-latest` must meet these prerequisites. Run each
emulator job in its own container; if using host execution, do not share an ADB
server/AVD directory with simultaneous jobs on that host.
## Apple Silicon prerequisites
Map `macos-arm64` to native host execution on the M4 Pro (not a Linux container or
Rosetta shell). Use a dedicated unprivileged CI account with Bash, SHA-256-capable Git, curl,
unzip and Python 3 available to the runner service's PATH. Install/select full
Xcode 16.4+ (or a current release supporting this project), accept its license and
complete first-launch setup. Install an iOS 18.4+ simulator runtime and create an
available iPhone simulator; `select-simulator.py` chooses the newest eligible one.
XcodeGen's pinned, checksum-verified binary includes native arm64 and x86_64
slices; Rosetta/Homebrew installation is not required by the workflow. Build,
archive and Swift package scratch outputs use `RUNNER_TEMP`. Scratch directories
and the upstream clone are uniquely allocated per job and removed after artifact
upload to avoid collisions and buildup on persistent hosts. No Apple signing
credentials are required or provisioned. Avoid concurrent native jobs on the same
Mac user/simulator; configure runner capacity accordingly.
## Forge compatibility and security
- External actions use explicit HTTPS hosts and full commit pins. The Node 20
Gitea-compatible artifact action uses the v3 protocol, not GitHub's v4 backend.
- Checkout sets `GIT_DEFAULT_HASH=sha256` for this repository and does not persist
its token. Upstream remains a separate, unmodified SHA-1 clone.
- Fork pull requests are skipped on these private runners. Same-repository PRs,
pushes and manual runs are trusted code: restrict repository write access,
require review, isolate runner accounts and keep unrelated credentials away.
Do not enable `pull_request_target` to bypass this restriction.
- Gitea ignores GitHub `environment` approval gates. Consequently the manual
release workflow only emits **unsigned** APK/AAB files and never loads signing
secrets or publishes. Signing remains an owner-controlled external operation.
Local checks for workflow changes:
```sh
python3 scripts/check-repository.py
python3 scripts/test-ci-runner.py
python3 scripts/test-verifier.py
python3 scripts/lint-workflows.py /path/to/actionlint
bash -n scripts/check-ci-runner.sh
```
The actionlint adapter normalizes Gitea's absolute action URLs only for parsing;
repository checks validate the actual hosts, pins, checkout settings and labels.
These checks do not prove a remote job ran. Inspect the first Gitea Actions runs
and their artifacts before claiming hosted Android or Apple-platform validation.
+4 -2
View File
@@ -73,5 +73,7 @@ validation must happen on macOS with Xcode and real supported devices.
5. Establish private vulnerability reporting, owned identifiers, authorized CI,
human security review and owner-controlled signing before any distribution.
The provided GitHub Actions workflows do not automatically execute on the current
non-GitHub forge. Do not claim hosted CI or store readiness without actual runs.
Gitea workflows in `.gitea/workflows/` target the maintainer's `ubuntu-latest`
pool and `macos-arm64` M4 Pro runner. Read [runner setup](ci-runners.md) before
changing their labels or signing policy. Hosted CI and store readiness still
require actual successful runs; runner registration alone is not test evidence.
+14 -17
View File
@@ -23,19 +23,16 @@ to either store. A debug APK or unsigned archive is not a production release.
## Android
The manual `release.yml` workflow uses a protected GitHub environment named
`release`. Configure branch restrictions and required reviewers before adding
secrets. With `sign_android=false` it emits unsigned artifacts. Signing requires:
The manual `.gitea/workflows/release.yml` workflow runs on `ubuntu-latest` and
emits **unsigned** artifacts only. Gitea ignores GitHub environment approval gates,
so this workflow does not load signing secrets or offer a signing toggle.
- `ANDROID_KEYSTORE_BASE64`
- `ANDROID_STORE_PASSWORD`
- `ANDROID_KEY_PASSWORD`
- `ANDROID_KEY_ALIAS`
The workflow uses `zipalign`, `apksigner`, and `jarsigner`; private material exists
only in a restrictive runner-temporary file removed on exit. Securely back up the
real upload key and use Play App Signing according to the owner's policy. No
production key is generated here. Manually verify APK/AAB signatures and inspect
An authorized maintainer must separately sign the verified APK/AAB using their
own keystore, alias and passwords with `zipalign`, `apksigner`, and `jarsigner`.
Keep private material in a protected signing environment, never on the general
validation runners. Securely back up the real upload key and use Play App Signing
according to the owner's policy. No production key is generated here.
Manually verify APK/AAB signatures and inspect
the final manifest: no debuggable flag, HTTP/test CA or unintended exported component.
Never distribute an `.integration` package or include its generated test resources.
Run `python3 scripts/collect-android-artifacts.py` immediately after the clean-source
@@ -65,8 +62,8 @@ privacy need physical-device validation beyond simulator tests.
## CI hosting
GitHub Actions files are provided, but the configured community remote is hosted
on a different forge. GitHub workflows will not automatically run there. Enable
an authorized GitHub mirror/repository (or port the jobs to the forge's runner)
and inspect real runs before claiming CI is operational. No destination is
created automatically. CI service availability is not an application dependency.
Gitea Actions workflows are provided in `.gitea/workflows/`, using the supplied
`ubuntu-latest` and `macos-arm64` runner labels. See [runner setup](ci-runners.md)
for SDK, KVM, Xcode, simulator and security requirements. Repository Actions must
be enabled. Inspect actual runs before claiming CI is operational; no GitHub
mirror or store destination is created. CI is not an application dependency.
+14 -1
View File
@@ -121,10 +121,23 @@ is available; no iOS build/archive/UI result is claimed.
Also run minimum-supported API 26 and a current OS/WebView device matrix.
- VoiceOver/TalkBack, large text, contrast, keyboard/safe areas, reduced motion,
light/dark appearance and responsive web modules on actual supported devices.
- Operational CI runs on an authorized GitHub repository/runner, private security
- Successful CI runs on the configured Gitea runners, private security
reporting contact, human security review, store ownership and production signing.
No Firebase/APNs registration, closed-app push, offline editing, App Store approval,
signing, publication or physical-device validation is claimed. See `SECURITY.md`
and `docs/security-review.md` for residual limitations.
Resume work using [maintainer continuation](continuation.md).
## Gitea runner adaptation
Workflows now live in `.gitea/workflows/` and target the supplied `ubuntu-latest`
pool and native `macos-arm64` M4 Pro runner. Local workflow grammar lint (Gitea URL
syntax normalized), repository invariants, shell/Python syntax, ten mocked runner
preflight regressions and eight evidence-verifier regressions pass. SHA-256 Git
initialization/fetch was exercised locally; the pinned XcodeGen archive's checksum
and native arm64 slice were verified. These are not remote runner execution or
Apple SDK results. See [runner requirements](ci-runners.md); the local emulator
preflight currently rejects unavailable/inaccessible KVM as designed. Original
application build evidence above remains historical and is not a rebuild of the
workflow-only changes.
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Validate prerequisites without installing packages or modifying the runner host.
set -euo pipefail
fail() { printf 'Runner prerequisite: %s\n' "$*" >&2; exit 1; }
for tool in git curl unzip python3; do
command -v "$tool" >/dev/null || fail "install $tool in the job environment"
done
case "${1:-}" in
android|emulator)
[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || fail 'ubuntu-latest must provide Linux x86_64'
if [[ "$1" == emulator ]]; then
[[ -c /dev/kvm && -r /dev/kvm && -w /dev/kvm ]] || fail 'expose /dev/kvm to the job and grant the runner user read/write access (nested virtualization required on VMs)'
fi
;;
ios)
[[ "$(uname -s)" == Darwin && "$(uname -m)" == arm64 ]] || fail 'macos-arm64 must execute natively on the Apple Silicon host'
command -v xcodebuild >/dev/null || fail 'install full Xcode and select it with xcode-select'
xcodebuild -version
xcrun --find swift >/dev/null
xcrun simctl list devices available >/dev/null
;;
*) fail 'usage: bash scripts/check-ci-runner.sh android|emulator|ios' ;;
esac
+13 -2
View File
@@ -47,8 +47,19 @@ network = ET.parse(ROOT / "android/app/src/main/res/xml/network_security_config.
assert network.find("base-config").get("cleartextTrafficPermitted") == "false"
assert [node.get("src") for node in network.findall(".//certificates")] == ["system"]
assert "distributionSha256Sum=" in (ROOT / "android/gradle/wrapper/gradle-wrapper.properties").read_text()
for workflow in (ROOT / ".github/workflows").glob("*.yml"):
workflows = sorted((ROOT / ".gitea/workflows").glob("*.yml"))
assert {path.name for path in workflows} == {"android.yml", "ios.yml", "release.yml"}, "Missing Gitea workflows"
for workflow in workflows:
for action in re.findall(r"uses:\s*([^\s#]+)", workflow.read_text()):
assert re.fullmatch(r"[\w.-]+/[\w./-]+@[0-9a-f]{40}", action), f"Unpinned action: {action}"
assert re.fullmatch(r"https://(?:github.com|gitea.com)/[\w.-]+/[\w./-]+@[0-9a-f]{40}", action), f"Unpinned action: {action}"
expected_runner = "macos-arm64" if workflow.name == "ios.yml" else "ubuntu-latest"
labels = re.findall(r"runs-on:\s*([^\s#]+)", workflow.read_text())
assert labels and all(label == expected_runner for label in labels), f"Wrong runner in {workflow.name}"
checkouts = workflow.read_text().count("https://github.com/actions/checkout@")
assert checkouts == (2 if workflow.name == "android.yml" else 1), "Each job needs checkout"
assert workflow.read_text().count("GIT_DEFAULT_HASH: sha256") == checkouts, "SHA-256 checkout must be explicit"
assert workflow.read_text().count("persist-credentials: false") == checkouts, "Do not retain CI checkout tokens"
release = (ROOT / ".gitea/workflows/release.yml").read_text()
assert "secrets." not in release and "environment:" not in release, "Gitea signing needs a separately reviewed approval mechanism"
assert "NSAllowsArbitraryLoads" not in (ROOT / "ios/project.yml").read_text()
print(f"Repository invariants: {len(paths)} source/documentation paths checked.")
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Lint Gitea workflows using actionlint's GitHub-compatible grammar.
Only the forge-specific absolute action URL syntax is normalized for actionlint;
the real URLs/pins and runner assignments are checked by check-repository.py.
"""
from pathlib import Path
import re
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
actionlint = sys.argv[1] if len(sys.argv) > 1 else "actionlint"
base = Path("/tmp/opencode") if Path("/tmp/opencode").exists() else None
with tempfile.TemporaryDirectory(prefix="yuvomi-workflow-lint-", dir=base) as temporary:
config = Path(temporary) / "actionlint.yaml"
config.write_text("self-hosted-runner:\n labels: [macos-arm64]\n")
workflows = sorted((ROOT / ".gitea/workflows").glob("*.yml"))
if not workflows:
raise SystemExit("No Gitea workflows found")
for path in workflows:
normalized = re.sub(r"(uses:\s*)https://(?:github.com|gitea.com)/", r"\1", path.read_text())
subprocess.run([actionlint, "-config-file", str(config), "-stdin-filename", str(path), "-"],
input=normalized, text=True, check=True, cwd=ROOT)
print(f"actionlint: {len(workflows)} Gitea workflows passed (action URL syntax normalized).")
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Test runner prerequisite decisions with fake tools, not actual Apple SDKs."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
SCRIPT = Path(__file__).with_name("check-ci-runner.sh")
class RunnerPrerequisiteTests(unittest.TestCase):
def run_check(self, mode, system="Linux", architecture="x86_64", missing=None, broken_xcode=False):
with tempfile.TemporaryDirectory(prefix="yuvomi-runner-test-") as temporary:
directory = Path(temporary)
for name in ["git", "curl", "unzip", "python3", "xcodebuild", "xcrun"]:
if name == missing:
continue
path = directory / name
status = 1 if name == "xcodebuild" and broken_xcode else 0
path.write_text(f"#!/bin/sh\nexit {status}\n")
path.chmod(0o700)
uname = directory / "uname"
uname.write_text('#!/bin/sh\ncase "$1" in -s) echo "$TEST_SYSTEM";; -m) echo "$TEST_ARCH";; esac\n')
uname.chmod(0o700)
return subprocess.run(["/bin/bash", str(SCRIPT), mode], capture_output=True, text=True,
env={**os.environ, "PATH": temporary, "TEST_SYSTEM": system,
"TEST_ARCH": architecture})
def test_android_accepts_linux_x64(self):
self.assertEqual(self.run_check("android").returncode, 0)
def test_android_rejects_arm(self):
self.assertNotEqual(self.run_check("android", architecture="aarch64").returncode, 0)
def test_android_rejects_macos(self):
self.assertNotEqual(self.run_check("android", system="Darwin").returncode, 0)
def test_ios_accepts_native_arm_with_tools(self):
self.assertEqual(self.run_check("ios", system="Darwin", architecture="arm64").returncode, 0)
def test_ios_rejects_rosetta_or_intel(self):
self.assertNotEqual(self.run_check("ios", system="Darwin").returncode, 0)
def test_ios_rejects_linux(self):
self.assertNotEqual(self.run_check("ios", architecture="arm64").returncode, 0)
def test_missing_python_is_actionable(self):
result = self.run_check("android", missing="python3")
self.assertNotEqual(result.returncode, 0)
self.assertIn("install python3", result.stderr)
def test_missing_full_xcode_fails(self):
result = self.run_check("ios", system="Darwin", architecture="arm64", missing="xcodebuild")
self.assertNotEqual(result.returncode, 0)
self.assertIn("full Xcode", result.stderr)
def test_broken_xcode_fails(self):
self.assertNotEqual(self.run_check("ios", system="Darwin", architecture="arm64",
broken_xcode=True).returncode, 0)
def test_unknown_mode_fails(self):
self.assertNotEqual(self.run_check("other").returncode, 0)
if __name__ == "__main__":
unittest.main()