SHA256
ci: target Gitea Ubuntu and native Apple Silicon runners
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:
1 parent
2d12491516
commit
11bf4ca620
16 files changed
+442
-192
No files matched your search
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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,
|
- Native Android HTTPS onboarding, origin-bound browsing, session persistence,
|
||||||
recovery, server erasure, camera/document selection, downloads and blob exports.
|
recovery, server erasure, camera/document selection, downloads and blob exports.
|
||||||
- SwiftUI/WKWebView iOS implementation, isolated web sign-in, native settings,
|
- SwiftUI/WKWebView iOS implementation, isolated web sign-in, native settings,
|
||||||
recovery, file export, and strict URL/metadata policies; Xcode validation pending.
|
recovery, file export, and strict URL/metadata policies; Xcode validation pending.
|
||||||
- JVM, Android emulator, portable Swift, and real pinned-upstream HTTPS tests.
|
- 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.
|
- MIT licensing, upstream audit, security model and release documentation.
|
||||||
|
|
||||||
No stable release has been published. No native push or browser-to-app session
|
No stable release has been published. No native push or browser-to-app session
|
||||||
|
|||||||
@@ -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)
|
- [Android build](docs/build-android.md) / [iOS build](docs/build-ios.md)
|
||||||
- [Compatibility and authentication](docs/server-compatibility.md)
|
- [Compatibility and authentication](docs/server-compatibility.md)
|
||||||
- [Release process](docs/release-process.md)
|
- [Release process](docs/release-process.md)
|
||||||
|
- [Gitea CI runners](docs/ci-runners.md)
|
||||||
- [Maintainer continuation](docs/continuation.md)
|
- [Maintainer continuation](docs/continuation.md)
|
||||||
|
|
||||||
MIT licensed. Upstream notices are preserved in `THIRD_PARTY_NOTICES.md`.
|
MIT licensed. Upstream notices are preserved in `THIRD_PARTY_NOTICES.md`.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -73,5 +73,7 @@ validation must happen on macOS with Xcode and real supported devices.
|
|||||||
5. Establish private vulnerability reporting, owned identifiers, authorized CI,
|
5. Establish private vulnerability reporting, owned identifiers, authorized CI,
|
||||||
human security review and owner-controlled signing before any distribution.
|
human security review and owner-controlled signing before any distribution.
|
||||||
|
|
||||||
The provided GitHub Actions workflows do not automatically execute on the current
|
Gitea workflows in `.gitea/workflows/` target the maintainer's `ubuntu-latest`
|
||||||
non-GitHub forge. Do not claim hosted CI or store readiness without actual runs.
|
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
@@ -23,19 +23,16 @@ to either store. A debug APK or unsigned archive is not a production release.
|
|||||||
|
|
||||||
## Android
|
## Android
|
||||||
|
|
||||||
The manual `release.yml` workflow uses a protected GitHub environment named
|
The manual `.gitea/workflows/release.yml` workflow runs on `ubuntu-latest` and
|
||||||
`release`. Configure branch restrictions and required reviewers before adding
|
emits **unsigned** artifacts only. Gitea ignores GitHub environment approval gates,
|
||||||
secrets. With `sign_android=false` it emits unsigned artifacts. Signing requires:
|
so this workflow does not load signing secrets or offer a signing toggle.
|
||||||
|
|
||||||
- `ANDROID_KEYSTORE_BASE64`
|
An authorized maintainer must separately sign the verified APK/AAB using their
|
||||||
- `ANDROID_STORE_PASSWORD`
|
own keystore, alias and passwords with `zipalign`, `apksigner`, and `jarsigner`.
|
||||||
- `ANDROID_KEY_PASSWORD`
|
Keep private material in a protected signing environment, never on the general
|
||||||
- `ANDROID_KEY_ALIAS`
|
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.
|
||||||
The workflow uses `zipalign`, `apksigner`, and `jarsigner`; private material exists
|
Manually verify APK/AAB signatures and inspect
|
||||||
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
|
|
||||||
the final manifest: no debuggable flag, HTTP/test CA or unintended exported component.
|
the final manifest: no debuggable flag, HTTP/test CA or unintended exported component.
|
||||||
Never distribute an `.integration` package or include its generated test resources.
|
Never distribute an `.integration` package or include its generated test resources.
|
||||||
Run `python3 scripts/collect-android-artifacts.py` immediately after the clean-source
|
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
|
## CI hosting
|
||||||
|
|
||||||
GitHub Actions files are provided, but the configured community remote is hosted
|
Gitea Actions workflows are provided in `.gitea/workflows/`, using the supplied
|
||||||
on a different forge. GitHub workflows will not automatically run there. Enable
|
`ubuntu-latest` and `macos-arm64` runner labels. See [runner setup](ci-runners.md)
|
||||||
an authorized GitHub mirror/repository (or port the jobs to the forge's runner)
|
for SDK, KVM, Xcode, simulator and security requirements. Repository Actions must
|
||||||
and inspect real runs before claiming CI is operational. No destination is
|
be enabled. Inspect actual runs before claiming CI is operational; no GitHub
|
||||||
created automatically. CI service availability is not an application dependency.
|
mirror or store destination is created. CI is not an application dependency.
|
||||||
+14
-1
@@ -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.
|
Also run minimum-supported API 26 and a current OS/WebView device matrix.
|
||||||
- VoiceOver/TalkBack, large text, contrast, keyboard/safe areas, reduced motion,
|
- VoiceOver/TalkBack, large text, contrast, keyboard/safe areas, reduced motion,
|
||||||
light/dark appearance and responsive web modules on actual supported devices.
|
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.
|
reporting contact, human security review, store ownership and production signing.
|
||||||
|
|
||||||
No Firebase/APNs registration, closed-app push, offline editing, App Store approval,
|
No Firebase/APNs registration, closed-app push, offline editing, App Store approval,
|
||||||
signing, publication or physical-device validation is claimed. See `SECURITY.md`
|
signing, publication or physical-device validation is claimed. See `SECURITY.md`
|
||||||
and `docs/security-review.md` for residual limitations.
|
and `docs/security-review.md` for residual limitations.
|
||||||
Resume work using [maintainer continuation](continuation.md).
|
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.
|
||||||
@@ -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
|
||||||
@@ -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 network.find("base-config").get("cleartextTrafficPermitted") == "false"
|
||||||
assert [node.get("src") for node in network.findall(".//certificates")] == ["system"]
|
assert [node.get("src") for node in network.findall(".//certificates")] == ["system"]
|
||||||
assert "distributionSha256Sum=" in (ROOT / "android/gradle/wrapper/gradle-wrapper.properties").read_text()
|
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()):
|
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()
|
assert "NSAllowsArbitraryLoads" not in (ROOT / "ios/project.yml").read_text()
|
||||||
print(f"Repository invariants: {len(paths)} source/documentation paths checked.")
|
print(f"Repository invariants: {len(paths)} source/documentation paths checked.")
|
||||||
@@ -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).")
|
||||||
@@ -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()
|
||||||
Reference in new issue
Block a user