diff --git a/.gitea/workflows/android.yml b/.gitea/workflows/android.yml new file mode 100644 index 0000000..3a37fc8 --- /dev/null +++ b/.gitea/workflows/android.yml @@ -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 diff --git a/.gitea/workflows/ios.yml b/.gitea/workflows/ios.yml new file mode 100644 index 0000000..605587a --- /dev/null +++ b/.gitea/workflows/ios.yml @@ -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 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..aeb5055 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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 diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml deleted file mode 100644 index 0a77e40..0000000 --- a/.github/workflows/android.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml deleted file mode 100644 index 968f610..0000000 --- a/.github/workflows/ios.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 105a994..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 93453c0..05b1b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index c7d5aa2..b5c072a 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/docs/ci-runners.md b/docs/ci-runners.md new file mode 100644 index 0000000..ed4118c --- /dev/null +++ b/docs/ci-runners.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. diff --git a/docs/continuation.md b/docs/continuation.md index 9eb0f9f..d6031f6 100644 --- a/docs/continuation.md +++ b/docs/continuation.md @@ -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. diff --git a/docs/release-process.md b/docs/release-process.md index bba4ede..6d532af 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -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. diff --git a/docs/validation.md b/docs/validation.md index a7dc442..4603f6d 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -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. diff --git a/scripts/check-ci-runner.sh b/scripts/check-ci-runner.sh new file mode 100644 index 0000000..e2ef212 --- /dev/null +++ b/scripts/check-ci-runner.sh @@ -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 diff --git a/scripts/check-repository.py b/scripts/check-repository.py index 342190e..ccc86d4 100644 --- a/scripts/check-repository.py +++ b/scripts/check-repository.py @@ -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.") diff --git a/scripts/lint-workflows.py b/scripts/lint-workflows.py new file mode 100644 index 0000000..94796f1 --- /dev/null +++ b/scripts/lint-workflows.py @@ -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).") diff --git a/scripts/test-ci-runner.py b/scripts/test-ci-runner.py new file mode 100644 index 0000000..c4cb49d --- /dev/null +++ b/scripts/test-ci-runner.py @@ -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()