From 16b8d6e676f246596c1f454e217109aec00ac2a8 Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 09:14:08 -0400 Subject: [PATCH 01/17] Release v2.4.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Fixed - Broader head-unit coverage for live metadata. Continuous notifications across track, play-state, position, and repeat/shuffle edges; head units detect each new track even when titles repeat; clean subscription state on every fresh connection. - Metadata + play / pause indicators now work on the broad class of head units and speakers that strictly require their AVRCP transaction IDs be echoed back. Previously, those head units silently rejected every response Y1 sent and fell back to key-press-only mode — metadata panes stayed blank and play-state indicators drifted out of sync. - Head units that gate metadata on AVRCP browse capability now enter full metadata mode. Restored the public-browse-group SDP attribute that some head units use as a "this peer supports full AVRCP" discriminator. - Metadata no longer freezes on head units that close the audio stream between tracks. The AVRCP control channel now survives audio open/close cycles, so the metadata view stays in sync without re-handshaking after every skip. - Head-unit play / pause glyphs flip reliably after a head-unit-initiated PAUSE. The music-player Activity was seeding a PLAYING announcement immediately after its own startup-reset PAUSE, racing out to the AVRCP wire as PAUSED → PLAYING; head units saw the trailing PLAYING and refused to flip. - Discrete PAUSE on head units with separate Play and Pause buttons pauses idempotently instead of toggling. - Spurious paused-state blips during track changes no longer interrupt head-unit playback indicators. ### Changed - Lower-latency metadata responses under sustained head-unit polling. Track-info exchange between the music app and the Bluetooth stack now uses shared memory (single-digit-ms reads vs ~25 ms before) with no torn reads at track edges. ### Added - `apply.bash --debug` build emits per-emit wire-side markers (`Y1T :` logcat tag) for diagnosing head-unit-specific AVRCP issues. Pair `tools/avrcp-wire-trace.py` with `tools/btlog-parse.py --avrcp` on a simultaneously-captured `btlog.bin` for the matching mtkbt-internal view. --- CHANGELOG.md | 2 +- apply.bash | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 368d50a..16bc526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/spec/v2.0.0.html). For full prose detail on any entry, see `git log`. -## [Unreleased] +## [2.4.0] - 2026-05-22 ### Fixed - Broader head-unit coverage for live metadata. Continuous notifications across track, play-state, position, and repeat/shuffle edges; head units detect each new track even when titles repeat; clean subscription state on every fresh connection. - Metadata + play / pause indicators now work on the broad class of head units and speakers that strictly require their AVRCP transaction IDs be echoed back. Previously, those head units silently rejected every response Y1 sent and fell back to key-press-only mode — metadata panes stayed blank and play-state indicators drifted out of sync. diff --git a/apply.bash b/apply.bash index 33ad7cd..73c154a 100755 --- a/apply.bash +++ b/apply.bash @@ -6,7 +6,7 @@ # Add a row to support a new build. # # Author: Sean Halpin (github.com/SeanathanVT) -# Version: 2.3.0 +# Version: 2.4.0 # Changelog: see CHANGELOG.md # Patches: see docs/PATCHES.md # From 41ad020d912d9434e40c39387836f729903c1088 Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 20:17:41 +0100 Subject: [PATCH 02/17] Add GitHub Actions pipeline to build patched rom.zip releases from upstream stock and Rockbox ROMs. Co-authored-by: Cursor --- .github/workflows/build-firmware-releases.yml | 125 +++++++++++++ README.md | 31 ++++ apply.bash | 163 ++++++++++++---- docs/SUPPORTED-FIRMWARE-CI.md | 52 ++++++ staging/README.md | 2 + tools/ci/build-one.sh | 175 ++++++++++++++++++ tools/ci/discover-inputs.sh | 106 +++++++++++ tools/ci/extract-rom.sh | 34 ++++ tools/ci/repack-rom.sh | 77 ++++++++ 9 files changed, 731 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/build-firmware-releases.yml create mode 100644 docs/SUPPORTED-FIRMWARE-CI.md create mode 100644 tools/ci/build-one.sh create mode 100644 tools/ci/discover-inputs.sh create mode 100644 tools/ci/extract-rom.sh create mode 100644 tools/ci/repack-rom.sh diff --git a/.github/workflows/build-firmware-releases.yml b/.github/workflows/build-firmware-releases.yml new file mode 100644 index 0000000..969b764 --- /dev/null +++ b/.github/workflows/build-firmware-releases.yml @@ -0,0 +1,125 @@ +name: Build firmware releases + +on: + workflow_dispatch: + inputs: + force: + description: Rebuild even when upstream digest unchanged + type: boolean + default: false + source_repo: + description: Limit to one upstream repo (OWNER/NAME) + type: string + default: "" + schedule: + - cron: "0 6 * * 1" + push: + branches: [main] + paths: + - "apply.bash" + - "src/patches/**" + - "src/Y1Bridge/**" + - "src/su/**" + - ".github/workflows/build-firmware-releases.yml" + - "tools/ci/**" + +permissions: + contents: write + +jobs: + discover: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.wrap.outputs.matrix }} + has_matrix: ${{ steps.wrap.outputs.has_matrix }} + steps: + - uses: actions/checkout@v4 + + - name: Discover upstream rom.zip releases + id: discover + env: + GH_TOKEN: ${{ github.token }} + run: | + chmod +x tools/ci/*.sh + args=() + if [[ -n "${{ github.event.inputs.source_repo }}" ]]; then + args+=(--source-repo "${{ github.event.inputs.source_repo }}") + fi + if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.force }}" == "true" ]]; then + args+=(--force) + fi + ./tools/ci/discover-inputs.sh "${args[@]}" > matrix.json + echo "Discovered inputs:" + cat matrix.json + + - name: Wrap matrix for strategy + id: wrap + run: | + matrix="$(cat matrix.json)" + if [[ "$matrix" == "[]" ]]; then + echo "has_matrix=false" >> "$GITHUB_OUTPUT" + echo 'matrix={"include":[]}' >> "$GITHUB_OUTPUT" + else + echo "has_matrix=true" >> "$GITHUB_OUTPUT" + echo "matrix={\"include\":${matrix}}" >> "$GITHUB_OUTPUT" + fi + + build: + needs: discover + if: needs.discover.outputs.has_matrix == 'true' + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 2 + matrix: ${{ fromJson(needs.discover.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + + - name: Install host packages + run: | + sudo apt-get update + sudo apt-get install -y \ + unzip zip curl \ + android-sdk-libsparse-utils \ + gcc-arm-linux-gnueabi binutils-arm-linux-gnueabi make \ + openjdk-17-jdk + + - name: Cache tooling + uses: actions/cache@v4 + with: + path: | + tools/mtkclient + tools/python-venv + tools/android-sdk + ~/.gradle/caches + ~/.gradle/wrapper + key: koensayr-tooling-${{ runner.os }}-${{ hashFiles('tools/setup.sh', 'tools/python-requirements.txt', 'tools/install-android-sdk.sh', 'src/Y1Bridge/**') }} + restore-keys: | + koensayr-tooling-${{ runner.os }}- + + - name: Build dependencies + run: | + chmod +x tools/ci/*.sh apply.bash tools/setup.sh tools/install-android-sdk.sh + ./tools/setup.sh + ( cd src/su && make ) + ./tools/install-android-sdk.sh + # shellcheck disable=SC1091 + source tools/android-sdk-env.sh + ( cd src/Y1Bridge && ./gradlew --stop && ./gradlew assembleDebug ) + + - name: Patch and publish release + env: + GH_TOKEN: ${{ github.token }} + run: | + args=( + --source-repo "${{ matrix.source_repo }}" + --source-tag "${{ matrix.source_tag }}" + --release-tag "${{ matrix.release_tag }}" + --download-url "${{ matrix.download_url }}" + --digest "${{ matrix.digest }}" + --slug "${{ matrix.slug }}" + ) + if [[ "${{ matrix.force }}" == "true" ]]; then + args+=(--force) + fi + ./tools/ci/build-one.sh "${args[@]}" diff --git a/README.md b/README.md index 1e3c4c3..580acd1 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,10 @@ Override bundled tooling with `--mtkclient-dir ` / `--python-venv ` | `--remove-apps` | Remove bloatware (`ApplicationGuide`, `BasicDreams`, …). | | `--root` | Install `src/su/build/su` at `/system/xbin/su` (mode 06755). Pre-requires `make` in `src/su/`. | | `--all` | All of the above. Pre-requires the `src/su/` + `src/Y1Bridge/` builds. | +| `--no-flash` | Patch only; write `system-*-devel.img` without MTKClient flash (CI / repack). | +| `--accept-any-firmware` | Skip `KNOWN_FIRMWARES` MD5 checks; use `--firmware-slug` when unknown. Implies `--skip-md5` on patchers. | +| `--firmware-slug ` | Output label when upstream `rom.zip` is not in the manifest (e.g. `y1-stock-rom-2.8.2`). | +| `--skip-md5` | Bypass stock-binary MD5 gates in `patch_*.py` (diagnostic / CI). | Run `./apply.bash --help` for full flag detail. Patchers can also be run standalone — see [`src/patches/README.md`](src/patches/README.md). @@ -104,9 +108,36 @@ Stock sizes: 3.0.2 `rom.zip` 259,502,414 bytes (raw `system.img` inside); 3.0.7 - For `--root` only: prebuilt `src/su/build/su` (`cd src/su && make`). Toolchain: `dnf install -y epel-release && dnf install -y gcc-arm-linux-gnu binutils-arm-linux-gnu make` (Rocky/Alma/RHEL/Fedora) or `gcc-arm-linux-gnueabi` on Debian/Ubuntu. - For `--avrcp` only: Android SDK + JDK 17+. `tools/install-android-sdk.sh` auto-installs into `tools/android-sdk/` (~1.5 GB, idempotent). Manual instructions: [`docs/ANDROID-SDK.md`](docs/ANDROID-SDK.md). +## Automated releases (GitHub Actions) + +Workflow [`.github/workflows/build-firmware-releases.yml`](.github/workflows/build-firmware-releases.yml) discovers every upstream release asset named **`rom.zip`** from: + +- [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) +- [rockbox-y1/rockbox](https://github.com/rockbox-y1/rockbox) + +For each input it runs `./apply.bash --all --no-flash --accept-any-firmware`, repacks `rom.zip`, and publishes a release on this repo. + +**Release tag pattern:** `{repo-slug}@{upstream-tag}` (e.g. `y1-stock-rom@3.0.2`, `rockbox@stable-v0.5`). Each release attaches one patched **`rom.zip`**. + +**Triggers:** weekly schedule, pushes that touch patcher code, and manual `workflow_dispatch` (optional `force` / `source_repo` filter). + +**Confidence:** Stock **3.0.2** / **3.0.7** OTAs are hardware-verified. Older stock builds and Rockbox `rom.zip` files are built best-effort (`--skip-md5`); byte patchers may fail until offsets are validated for that image. See [`docs/SUPPORTED-FIRMWARE-CI.md`](docs/SUPPORTED-FIRMWARE-CI.md). + +Local dry-run (no GitHub publish): + +```bash +KOENSAYR_SKIP_PUBLISH=1 ./tools/ci/build-one.sh \ + --source-repo y1-community/y1-stock-rom --source-tag 3.0.2 \ + --release-tag y1-stock-rom@3.0.2 \ + --download-url "$(gh release view 3.0.2 --repo y1-community/y1-stock-rom --json assets -q '.assets[] | select(.name==\"rom.zip\") | .url')" \ + --digest "$(gh release view 3.0.2 --repo y1-community/y1-stock-rom --json assets -q '.assets[] | select(.name==\"rom.zip\") | .digest' | sed 's/sha256://')" \ + --slug y1-stock-rom-3.0.2 +``` + ## Documentation - [CHANGELOG.md](CHANGELOG.md) — version history (Keep a Changelog format) +- [docs/SUPPORTED-FIRMWARE-CI.md](docs/SUPPORTED-FIRMWARE-CI.md) — CI upstream mapping and build expectations - [docs/ANDROID-SDK.md](docs/ANDROID-SDK.md) — Android SDK install instructions (only needed for `--avrcp` / Y1Bridge build) - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — AVRCP metadata proxy architecture: data-path diagram, trampoline chain, response-builder calling conventions, ELF segment-extension technique, code-cave inventory. Read this first if working on the metadata pipeline. - [docs/BT-COMPLIANCE.md](docs/BT-COMPLIANCE.md) — current ICS Table 7 coverage scorecard (every Mandatory + every Optional row) diff --git a/apply.bash b/apply.bash index 73c154a..8517ce0 100755 --- a/apply.bash +++ b/apply.bash @@ -38,6 +38,16 @@ FLAGS: --all --adb + --avrcp + --bluetooth + --music-apk + --remove-apps + --root. Pre-requires the src/su/ and src/Y1Bridge/ builds (see those flags above). + --no-flash Patch and write system-*-devel.img only; skip MTKClient flash. + Used by CI / headless repack flows. + --accept-any-firmware + Do not require rom.zip / system.img MD5s in KNOWN_FIRMWARES. + Use --firmware-slug when rom.zip is not in the manifest. + --firmware-slug + Label for output naming (e.g. y1-stock-rom-3.0.2). Required + with --accept-any-firmware when rom.zip MD5 is unknown. + --skip-md5 Pass --skip-md5 to every patch_*.py (diagnostic / CI). + Implied by --accept-any-firmware. --debug Build patches with KOENSAYR_DEBUG=1. Build-time switch (reflash to toggle); zero runtime overhead when omitted. Surfaces three independent log streams: @@ -71,6 +81,10 @@ FLAG_DEBUG=false FLAG_MUSIC_APK=false FLAG_REMOVE_APPS=false FLAG_ROOT=false +FLAG_NO_FLASH=false +FLAG_ACCEPT_ANY_FIRMWARE=false +FLAG_SKIP_MD5=false +FIRMWARE_SLUG="" PATH_ARTIFACTS="" # Tooling overrides — explicit flag wins over the tools/ default. @@ -145,6 +159,24 @@ while [[ $# -gt 0 ]]; do FLAG_ANY_SPECIFIED=true shift ;; + --no-flash) + FLAG_NO_FLASH=true + shift + ;; + --accept-any-firmware) + FLAG_ACCEPT_ANY_FIRMWARE=true + FLAG_SKIP_MD5=true + shift + ;; + --firmware-slug) + require_value --firmware-slug "${2:-}" + FIRMWARE_SLUG="$2" + shift 2 + ;; + --skip-md5) + FLAG_SKIP_MD5=true + shift + ;; --mtkclient-dir) require_value --mtkclient-dir "${2:-}" OVERRIDE_MTKCLIENT_DIR="$2" @@ -352,6 +384,32 @@ print_known_firmwares() { done } +# discover_music_apk — sets FILENAME_MUSIC_APK from /system/app/com.innioasis.y1*.apk +discover_music_apk() { + local matches=() + while IFS= read -r line; do + [[ -n "$line" ]] && matches+=("$line") + done < <(sudo find "${PATH_MOUNT}/app" -maxdepth 1 -name 'com.innioasis.y1*.apk' -printf '%f\n' 2>/dev/null | sort -u) + if [[ ${#matches[@]} -eq 0 ]]; then + echo "ERROR: no com.innioasis.y1*.apk under ${PATH_MOUNT}/app" >&2 + exit 1 + fi + if [[ ${#matches[@]} -gt 1 ]]; then + echo "ERROR: multiple music APKs under ${PATH_MOUNT}/app:" >&2 + printf ' %s\n' "${matches[@]}" >&2 + exit 1 + fi + FILENAME_MUSIC_APK="${matches[0]}" + echo " → music APK: app/${FILENAME_MUSIC_APK}" +} + +# patcher_extra_args — extra CLI flags for patch_*.py invocations. +patcher_extra_args() { + if [[ "$FLAG_SKIP_MD5" == true ]]; then + echo --skip-md5 + fi +} + # Stages stock binaries extracted from the mount + their patched output before write-back. PATH_TMP_STAGE="$(mktemp -d -t koensayr.XXXXXX)" MOUNTED=false @@ -381,7 +439,8 @@ patch_in_place_bytes() { sudo cp "${PATH_MOUNT}/${mount_rel}" "${stock}" sudo chown "$(id -u):$(id -g)" "${stock}" - if ! python3 "${PATH_SCRIPT_DIR}/src/patches/${script}" "${stock}" --output "${patched}"; then + # shellcheck disable=SC2046 + if ! python3 "${PATH_SCRIPT_DIR}/src/patches/${script}" "${stock}" --output "${patched}" $(patcher_extra_args); then echo "ERROR: ${script} failed for ${mount_rel}" >&2 exit 1 fi @@ -412,10 +471,11 @@ patch_in_place_y1_apk() { local pyvenv pyvenv="$(resolve_python_venv)" + # shellcheck disable=SC2046 if ! ( cd "${PATH_SCRIPT_DIR}/src/patches" [[ -n "$pyvenv" ]] && source "${pyvenv}/bin/activate" - python3 patch_y1_apk.py "${stock}" + python3 patch_y1_apk.py "${stock}" $(patcher_extra_args) ); then echo "ERROR: patch_y1_apk.py failed for ${mount_rel}" >&2 exit 1 @@ -452,11 +512,23 @@ if ! command -v python3 >/dev/null 2>&1; then exit 1 fi -echo "Validating rom.zip against stock-firmware manifest.." rom_md5=$(md5_of "$rom") +MANIFEST_MATCH=false if VERSION_FIRMWARE=$(resolve_version rom "$rom_md5"); then + MANIFEST_MATCH=true + echo "Validating rom.zip against stock-firmware manifest.." echo " → matched v${VERSION_FIRMWARE} (rom.zip md5 ${rom_md5})" +elif [[ "$FLAG_ACCEPT_ANY_FIRMWARE" == true ]]; then + echo "Accepting rom.zip without manifest match (md5 ${rom_md5}).." + if [[ -z "$FIRMWARE_SLUG" ]]; then + echo "ERROR: rom.zip is not in KNOWN_FIRMWARES; pass --firmware-slug with --accept-any-firmware." >&2 + print_known_firmwares + exit 1 + fi + VERSION_FIRMWARE="$FIRMWARE_SLUG" + echo " → firmware slug ${VERSION_FIRMWARE}" else + echo "Validating rom.zip against stock-firmware manifest.." echo "ERROR: ${FILENAME_ROM_ZIP} md5 ${rom_md5} does not match any known stock firmware." >&2 print_known_firmwares exit 1 @@ -501,17 +573,27 @@ EOF PATH_SYSTEM_IMG="$raw" fi - sys_md5=$(md5_of "$PATH_SYSTEM_IMG") - expected=$(firmware_field "$VERSION_FIRMWARE" system_md5) - if [[ "$sys_md5" != "$expected" ]]; then - echo "ERROR: extracted system.img md5 ${sys_md5} differs from manifest v${VERSION_FIRMWARE} (expected ${expected})" >&2 - exit 1 + if [[ "$MANIFEST_MATCH" == true && "$FLAG_ACCEPT_ANY_FIRMWARE" == false ]]; then + sys_md5=$(md5_of "$PATH_SYSTEM_IMG") + expected=$(firmware_field "$VERSION_FIRMWARE" system_md5) + if [[ "$sys_md5" != "$expected" ]]; then + echo "ERROR: extracted system.img md5 ${sys_md5} differs from manifest v${VERSION_FIRMWARE} (expected ${expected})" >&2 + exit 1 + fi + elif [[ "$MANIFEST_MATCH" == true && "$FLAG_ACCEPT_ANY_FIRMWARE" == true ]]; then + sys_md5=$(md5_of "$PATH_SYSTEM_IMG") + expected=$(firmware_field "$VERSION_FIRMWARE" system_md5) + if [[ "$sys_md5" != "$expected" ]]; then + echo " WARNING: extracted system.img md5 ${sys_md5} differs from manifest v${VERSION_FIRMWARE} (expected ${expected}); continuing (--accept-any-firmware)" + fi fi fi # Version-dependent filenames now resolvable. FILENAME_SYSTEM_IMAGE_TARGET="system-${VERSION_FIRMWARE}-devel.img" -FILENAME_MUSIC_APK="$(firmware_field "$VERSION_FIRMWARE" music_apk)" +if [[ "$MANIFEST_MATCH" == true ]]; then + FILENAME_MUSIC_APK="$(firmware_field "$VERSION_FIRMWARE" music_apk)" +fi # Copy validated raw system.img into the artifacts dir (mtkclient flashes from there) and mount. if [[ "$FLAG_ANY_SYSTEM_PATCH" == true ]]; then @@ -535,6 +617,10 @@ if [[ "$FLAG_ANY_SYSTEM_PATCH" == true ]]; then exit 1 fi MOUNTED=true + + if [[ -z "${FILENAME_MUSIC_APK:-}" ]]; then + discover_music_apk + fi fi # Enable ADB debugging @@ -656,34 +742,43 @@ if [[ "$FLAG_ANY_SYSTEM_PATCH" == true ]]; then fi MOUNTED=false - # Flash via MTKClient. Resolve location + venv only now (no point checking - # earlier — the patch steps don't need MTKClient). - PATH_MTKCLIENT="$(resolve_mtkclient_dir)" - PATH_VENV_MTKCLIENT="${PATH_MTKCLIENT}/venv" - if [[ ! -f "${PATH_VENV_MTKCLIENT}/bin/activate" ]]; then - echo "ERROR: MTKClient venv missing at ${PATH_VENV_MTKCLIENT}." >&2 - echo " Run: ${PATH_SCRIPT_DIR}/tools/setup.sh" >&2 - exit 1 - fi + devel_img="${PATH_ARTIFACTS}/${FILENAME_SYSTEM_IMAGE_TARGET}" + if [[ "$FLAG_NO_FLASH" == true ]]; then + echo "Skipping MTKClient flash (--no-flash). Patched image: ${devel_img}" + if [[ "$FLAG_AVRCP" == true ]]; then + echo "After flashing this build on-device, clear MultiDex cache once:" + echo " adb shell rm -rf /data/data/com.innioasis.y1/code_cache/secondary-dexes/" + fi + else + # Flash via MTKClient. Resolve location + venv only now (no point checking + # earlier — the patch steps don't need MTKClient). + PATH_MTKCLIENT="$(resolve_mtkclient_dir)" + PATH_VENV_MTKCLIENT="${PATH_MTKCLIENT}/venv" + if [[ ! -f "${PATH_VENV_MTKCLIENT}/bin/activate" ]]; then + echo "ERROR: MTKClient venv missing at ${PATH_VENV_MTKCLIENT}." >&2 + echo " Run: ${PATH_SCRIPT_DIR}/tools/setup.sh" >&2 + exit 1 + fi - echo "Activating MTKClient venv (${PATH_MTKCLIENT}).." - if ! cd "${PATH_MTKCLIENT}"; then - echo "ERROR: failed to cd into ${PATH_MTKCLIENT} (permissions?)" >&2 - exit 1 - fi - # shellcheck disable=SC1091 - source "${PATH_VENV_MTKCLIENT}/bin/activate" + echo "Activating MTKClient venv (${PATH_MTKCLIENT}).." + if ! cd "${PATH_MTKCLIENT}"; then + echo "ERROR: failed to cd into ${PATH_MTKCLIENT} (permissions?)" >&2 + exit 1 + fi + # shellcheck disable=SC1091 + source "${PATH_VENV_MTKCLIENT}/bin/activate" + + echo "Writing new system.img (plug in and reset Y1 device using button near USB-C port).." + if ! python3 "${PATH_MTKCLIENT}/mtk.py" w android "${devel_img}"; then + echo "ERROR: mtk.py write failed — device left in an unknown state." >&2 + echo " Common causes: device not in BROM mode, USB cable not data-capable," >&2 + echo " mtkclient version mismatch, missing libusb." >&2 + deactivate + exit 1 + fi - echo "Writing new system.img (plug in and reset Y1 device using button near USB-C port).." - if ! python3 "${PATH_MTKCLIENT}/mtk.py" w android "${PATH_ARTIFACTS}/${FILENAME_SYSTEM_IMAGE_TARGET}"; then - echo "ERROR: mtk.py write failed — device left in an unknown state." >&2 - echo " Common causes: device not in BROM mode, USB cable not data-capable," >&2 - echo " mtkclient version mismatch, missing libusb." >&2 + echo "Deactivating MTKClient venv.." deactivate - exit 1 fi - - echo "Deactivating MTKClient venv.." - deactivate fi echo "Done!" diff --git a/docs/SUPPORTED-FIRMWARE-CI.md b/docs/SUPPORTED-FIRMWARE-CI.md new file mode 100644 index 0000000..af9b297 --- /dev/null +++ b/docs/SUPPORTED-FIRMWARE-CI.md @@ -0,0 +1,52 @@ +# CI firmware builds + +GitHub Actions workflow [`build-firmware-releases.yml`](../.github/workflows/build-firmware-releases.yml) produces patched **`rom.zip`** releases on [ryan-specter/koensayr-auto](https://github.com/ryan-specter/koensayr-auto/releases). + +## Upstream sources + +| Repository | Asset filter | Example release tags | +|------------|--------------|----------------------| +| [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) | `rom.zip` only | `Latest-3.0.7`, `3.0.2`, `2.8.2`, `ADB-2.1.9` | +| [rockbox-y1/rockbox](https://github.com/rockbox-y1/rockbox) | `rom.zip` only | `stable-v0.5`, recent `nightly-*` tags | + +**Not built:** `rom_type_b.zip`, `rom_240p.zip`, `update.zip`, voice packs, and other non-`rom.zip` assets. + +## Output naming + +- **GitHub release tag:** `{slug}@{upstream-tag}` (e.g. `y1-stock-rom@3.0.2`, `rockbox@stable-v0.5`) +- **Internal firmware slug** (`--firmware-slug`): `@` replaced with `-` (e.g. `y1-stock-rom-3.0.2`) for `system-*-devel.img` naming + +## Patch set + +Every green CI build runs `./apply.bash --all --no-flash --accept-any-firmware`: + +- Music-player UX (`--music-apk`) +- Bluetooth pairing (`--bluetooth`) +- ADB + bloat removal (`--adb`, `--remove-apps`) +- Root (`--root`) +- AVRCP 1.3 + Y1Bridge (`--avrcp`) + +Diagnostic tooling under `tools/` is **not** embedded in the ROM. + +## Expectations by upstream + +| Input | CI expectation | +|-------|----------------| +| y1-stock-rom **3.0.2** / **3.0.7** | Highest confidence; matches [`KNOWN_FIRMWARES`](../apply.bash) | +| y1-stock-rom **2.8.2** / **ADB-2.1.9** | Attempted with `--skip-md5`; may fail until patch offsets are verified | +| rockbox **rom.zip** | Best-effort; custom `system.img` / BT stack may differ from stock | + +Failed matrix jobs do not block other releases (`fail-fast: false`). + +## Idempotency + +A build is skipped when a release already exists and its notes contain the upstream asset SHA256, unless `workflow_dispatch` sets **force**. + +## Scripts + +| Script | Role | +|--------|------| +| [`tools/ci/discover-inputs.sh`](../tools/ci/discover-inputs.sh) | Emit JSON matrix of upstream `rom.zip` assets | +| [`tools/ci/build-one.sh`](../tools/ci/build-one.sh) | Download → patch → repack → `gh release` | +| [`tools/ci/extract-rom.sh`](../tools/ci/extract-rom.sh) | Unzip upstream ROM; record sparse `system.img` | +| [`tools/ci/repack-rom.sh`](../tools/ci/repack-rom.sh) | Replace `system.img` and zip patched `rom.zip` | diff --git a/staging/README.md b/staging/README.md index 3053376..a5de35f 100644 --- a/staging/README.md +++ b/staging/README.md @@ -11,4 +11,6 @@ Override with `./apply.bash --artifacts-dir ` to point at a different dire The contents of this directory (other than this README) are `.gitignore`d so firmware never lands in commits. **`git clean -dfx` will nuke whatever you stage here** along with other build artifacts — keep a backup of `rom.zip` if you'd rather not re-download. +CI builds use ephemeral directories under `/tmp` via [`tools/ci/build-one.sh`](../tools/ci/build-one.sh), not this folder. + See the top-level [README](../README.md) Quick start for the full flow. diff --git a/tools/ci/build-one.sh b/tools/ci/build-one.sh new file mode 100644 index 0000000..c658181 --- /dev/null +++ b/tools/ci/build-one.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# build-one.sh — download one upstream rom.zip, patch with koensayr --all, repack, +# and publish (or skip) a GitHub release on the current repo. +# +# Usage: +# ./tools/ci/build-one.sh \ +# --source-repo y1-community/y1-stock-rom \ +# --source-tag 3.0.2 \ +# --release-tag y1-stock-rom@3.0.2 \ +# --download-url \ +# --digest \ +# --slug y1-stock-rom-3.0.2 \ +# [--force] + +set -euo pipefail + +SOURCE_REPO="" +SOURCE_TAG="" +RELEASE_TAG="" +DOWNLOAD_URL="" +DIGEST="" +SLUG="" +FORCE=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --source-repo) SOURCE_REPO="$2"; shift 2 ;; + --source-tag) SOURCE_TAG="$2"; shift 2 ;; + --release-tag) RELEASE_TAG="$2"; shift 2 ;; + --download-url) DOWNLOAD_URL="$2"; shift 2 ;; + --digest) DIGEST="$2"; shift 2 ;; + --slug) SLUG="$2"; shift 2 ;; + --force) FORCE=true; shift ;; + -h|--help) + cat <<'EOF' +Usage: ./tools/ci/build-one.sh --source-repo REPO --source-tag TAG \ + --release-tag TAG --download-url URL --digest SHA256 --slug SLUG [--force] + +Environment: + KOENSAYR_SKIP_PUBLISH=1 Build only; do not create/upload GitHub release. + GITHUB_REPOSITORY Target repo for releases (defaults from gh). +EOF + exit 0 + ;; + *) + echo "ERROR: unknown arg $1" >&2 + exit 1 + ;; + esac +done + +if [[ -z "$SOURCE_REPO" || -z "$SOURCE_TAG" || -z "$RELEASE_TAG" || -z "$DOWNLOAD_URL" || -z "$SLUG" ]]; then + echo "ERROR: --source-repo, --source-tag, --release-tag, --download-url, and --slug are required" >&2 + exit 1 +fi + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +CI_DIR="${REPO_ROOT}/tools/ci" +WORKDIR="$(mktemp -d -t koensayr-ci.XXXXXX)" +trap 'rm -rf "$WORKDIR"' EXIT + +STAGING="${WORKDIR}/staging" +EXTRACT="${WORKDIR}/rom-extract" +mkdir -p "$STAGING" "$EXTRACT" + +KOENSAYR_VERSION="$(grep -E '^# Version:' apply.bash | awk '{print $3}')" + +# Idempotency: skip when release exists with matching upstream digest. +if [[ "$FORCE" != true && "${KOENSAYR_SKIP_PUBLISH:-}" != "1" ]]; then + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + if [[ -n "$DIGEST" ]] && gh release view "$RELEASE_TAG" --json body -q .body | grep -qF "$DIGEST"; then + echo "[build-one] Release ${RELEASE_TAG} already published for digest ${DIGEST}; skipping." + exit 0 + fi + fi +fi + +echo "[build-one] Downloading upstream rom.zip.." +curl -fsSL -o "${STAGING}/rom.zip" "$DOWNLOAD_URL" + +echo "[build-one] Extracting upstream layout.." +"${CI_DIR}/extract-rom.sh" "${STAGING}/rom.zip" "$EXTRACT" + +echo "[build-one] Patching (koensayr ${KOENSAYR_VERSION}).." +./apply.bash --all --no-flash --accept-any-firmware \ + --firmware-slug "$SLUG" \ + --artifacts-dir "$STAGING" + +DEVEL_IMG="${STAGING}/system-${SLUG}-devel.img" +if [[ ! -f "$DEVEL_IMG" ]]; then + echo "ERROR: expected patched image ${DEVEL_IMG}" >&2 + exit 1 +fi + +OUTPUT_ROM="${WORKDIR}/rom-koensayr.zip" +"${CI_DIR}/repack-rom.sh" "$EXTRACT" "$DEVEL_IMG" "$OUTPUT_ROM" + +# Copy staging rom for convenience +cp -f "$OUTPUT_ROM" "${STAGING}/rom-koensayr.zip" + +MANIFEST="${WORKDIR}/build-manifest.json" +python3 - "$MANIFEST" </dev/null || true + exit 0 +fi + +NOTES="${WORKDIR}/release-notes.md" +cat > "$NOTES" </dev/null 2>&1; then + gh release upload "$RELEASE_TAG" "$OUTPUT_ROM" --clobber + gh release edit "$RELEASE_TAG" --notes-file "$NOTES" +else + gh release create "$RELEASE_TAG" "$OUTPUT_ROM" \ + --title "Koensayr ${RELEASE_TAG}" \ + --notes-file "$NOTES" +fi + +echo "[build-one] Uploaded ${RELEASE_TAG}" diff --git a/tools/ci/discover-inputs.sh b/tools/ci/discover-inputs.sh new file mode 100644 index 0000000..22aac3f --- /dev/null +++ b/tools/ci/discover-inputs.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# discover-inputs.sh — list upstream GitHub releases that ship rom.zip. +# +# Usage: +# ./tools/ci/discover-inputs.sh [--source-repo OWNER/NAME] [--force] +# +# Writes JSON array to stdout (GHA matrix "include" entries). + +set -euo pipefail + +SOURCE_FILTER="" +FORCE=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --source-repo) + SOURCE_FILTER="$2" + shift 2 + ;; + --force) + FORCE=true + shift + ;; + -h|--help) + cat <<'EOF' +Usage: ./tools/ci/discover-inputs.sh [--source-repo OWNER/NAME] [--force] + +Emits a JSON array of matrix objects: + source_repo, source_tag, release_tag, download_url, digest, slug + +Only assets named exactly "rom.zip" are included. +Repos scanned: y1-community/y1-stock-rom, rockbox-y1/rockbox +EOF + exit 0 + ;; + *) + echo "ERROR: unknown arg $1" >&2 + exit 1 + ;; + esac +done + +if ! command -v gh >/dev/null 2>&1; then + echo "ERROR: gh CLI required" >&2 + exit 1 +fi +if ! command -v python3 >/dev/null 2>&1; then + echo "ERROR: python3 required" >&2 + exit 1 +fi + +REPOS=( + "y1-community/y1-stock-rom" + "rockbox-y1/rockbox" +) + +python3 - "$SOURCE_FILTER" "$FORCE" "${REPOS[@]}" <<'PY' +import json, subprocess, sys + +source_filter = sys.argv[1] +force = sys.argv[2] == "true" +repos = sys.argv[3:] + +def slug_for_repo(full_name: str) -> str: + if full_name == "y1-community/y1-stock-rom": + return "y1-stock-rom" + if full_name == "rockbox-y1/rockbox": + return "rockbox" + return full_name.split("/")[-1] + +entries = [] + +for repo in repos: + if source_filter and repo != source_filter: + continue + slug_base = slug_for_repo(repo) + out = subprocess.run( + ["gh", "api", f"repos/{repo}/releases", "--paginate"], + capture_output=True, + text=True, + check=True, + ) + releases = json.loads(out.stdout) + for rel in releases: + if rel.get("draft"): + continue + tag = rel["tag_name"] + for asset in rel.get("assets") or []: + if asset.get("name") != "rom.zip": + continue + digest = asset.get("digest") or "" + if digest.startswith("sha256:"): + digest = digest[7:] + release_tag = f"{slug_base}@{tag}" + entries.append({ + "source_repo": repo, + "source_tag": tag, + "release_tag": release_tag, + "download_url": asset["browser_download_url"], + "digest": digest, + "slug": release_tag.replace("@", "-"), + "force": force, + }) + +print(json.dumps(entries)) +PY diff --git a/tools/ci/extract-rom.sh b/tools/ci/extract-rom.sh new file mode 100644 index 0000000..812280e --- /dev/null +++ b/tools/ci/extract-rom.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# extract-rom.sh — unzip rom.zip and record whether system.img was sparse. +# +# Usage: ./tools/ci/extract-rom.sh + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: $0 " >&2 + exit 1 +fi + +ROM_ZIP="$1" +EXTRACT_DIR="$2" + +mkdir -p "$EXTRACT_DIR" +unzip -q -o "$ROM_ZIP" -d "$EXTRACT_DIR" + +SYS="${EXTRACT_DIR}/system.img" +if [[ ! -f "$SYS" ]]; then + echo "ERROR: ${ROM_ZIP} has no system.img at zip root" >&2 + exit 1 +fi + +is_sparse=0 +if command -v file >/dev/null 2>&1 && file "$SYS" | grep -q "Android sparse image"; then + is_sparse=1 +else + magic=$(head -c 4 "$SYS" 2>/dev/null | od -An -v -t x1 | tr -d ' \n') + [[ "$magic" == "3aff26ed" ]] && is_sparse=1 +fi + +echo "$is_sparse" > "${EXTRACT_DIR}/.koensayr-system-sparse" +echo "[extract] ${ROM_ZIP} → ${EXTRACT_DIR} (system.img sparse=${is_sparse})" diff --git a/tools/ci/repack-rom.sh b/tools/ci/repack-rom.sh new file mode 100644 index 0000000..7976c69 --- /dev/null +++ b/tools/ci/repack-rom.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# repack-rom.sh — replace system.img inside an extracted rom.zip tree with a +# patched raw (or re-sparsified) system image, then produce rom.zip. +# +# Usage: +# ./tools/ci/repack-rom.sh +# +# Writes /.koensayr-system-sparse (0 or 1) on first extract if +# missing; pass the same extract dir used when the flag was recorded. + +set -euo pipefail + +case "${1:-}" in + -h|--help) + cat <<'EOF' +Usage: ./tools/ci/repack-rom.sh + +Expects to contain the contents of upstream rom.zip (including +system.img). Replaces system.img with (raw ext4 from +apply.bash). If .koensayr-system-sparse in the extract dir is 1, runs img2simg +before zipping. + +Create .koensayr-system-sparse when extracting upstream rom.zip: + echo 1 > dir/.koensayr-system-sparse # if system.img inside zip was sparse + echo 0 > dir/.koensayr-system-sparse # raw +EOF + exit 0 + ;; +esac + +if [[ $# -ne 3 ]]; then + echo "usage: $0 " >&2 + echo " see: $0 --help" >&2 + exit 1 +fi + +EXTRACT_DIR="$1" +PATCHED_RAW="$2" +OUTPUT_ZIP="$3" +TARGET="${EXTRACT_DIR}/system.img" +SPARSE_FLAG="${EXTRACT_DIR}/.koensayr-system-sparse" + +if [[ ! -d "$EXTRACT_DIR" ]]; then + echo "ERROR: extract dir not found: ${EXTRACT_DIR}" >&2 + exit 1 +fi +if [[ ! -f "$PATCHED_RAW" ]]; then + echo "ERROR: patched system image not found: ${PATCHED_RAW}" >&2 + exit 1 +fi + +was_sparse=0 +if [[ -f "$SPARSE_FLAG" ]]; then + read -r was_sparse < "$SPARSE_FLAG" || was_sparse=0 +fi + +if [[ "$was_sparse" == "1" ]]; then + if ! command -v img2simg >/dev/null 2>&1; then + echo "ERROR: img2simg required to re-sparsify system.img (install android-tools)" >&2 + exit 1 + fi + echo "[repack] Converting patched raw system.img → sparse.." + img2simg "$PATCHED_RAW" "$TARGET" +else + echo "[repack] Installing patched raw system.img.." + cp -f "$PATCHED_RAW" "$TARGET" +fi + +echo "[repack] Building ${OUTPUT_ZIP}.." +rm -f "$OUTPUT_ZIP" +( + cd "$EXTRACT_DIR" + # Zip root entries only (exclude marker file from archive). + zip -r -9 "$OUTPUT_ZIP" . -x '.koensayr-system-sparse' +) + +echo "[repack] Done: ${OUTPUT_ZIP} ($(wc -c < "$OUTPUT_ZIP") bytes)" From 6939d3386d4bf5d1aea51eb99902438ebc0a7d0e Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 20:21:19 +0100 Subject: [PATCH 03/17] Fix su cross-compile on Debian CI by auto-detecting arm-linux-gnueabi-gcc. Co-authored-by: Cursor --- src/su/Makefile | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/su/Makefile b/src/su/Makefile index 0901213..e15cb29 100644 --- a/src/su/Makefile +++ b/src/su/Makefile @@ -4,15 +4,26 @@ # libc dependency. The only library it links is libgcc.a (for compiler # builtins like __aeabi_*), which the EPEL gcc-arm-linux-gnu package ships. # -# Install (Rocky/Alma/RHEL/Fedora with EPEL): -# sudo dnf install -y epel-release -# sudo dnf install -y gcc-arm-linux-gnu binutils-arm-linux-gnu +# Install: +# Debian / Ubuntu: sudo apt install gcc-arm-linux-gnueabi binutils-arm-linux-gnueabi +# Rocky/Fedora: sudo dnf install epel-release && \ +# sudo dnf install gcc-arm-linux-gnu binutils-arm-linux-gnu # -# The Fedora/RHEL package layout uses the arm-linux-gnueabi triplet -# internally (in /usr/lib/gcc/arm-linux-gnueabi/) but installs binaries with -# the arm-linux-gnu- prefix. Both names refer to the same toolchain. +# Fedora/RHEL ships arm-linux-gnu-gcc; Debian/Ubuntu ships arm-linux-gnueabi-gcc. +# Both target the same ARM EABI triplet for this no-libc build. -CC := arm-linux-gnu-gcc +CROSS_PREFIX ?= +ifeq ($(CROSS_PREFIX),) + ifneq ($(shell command -v arm-linux-gnu-gcc 2>/dev/null),) + CROSS_PREFIX := arm-linux-gnu- + else ifneq ($(shell command -v arm-linux-gnueabi-gcc 2>/dev/null),) + CROSS_PREFIX := arm-linux-gnueabi- + endif +endif + +CC := $(CROSS_PREFIX)gcc +STRIP := $(CROSS_PREFIX)strip +READELF := $(CROSS_PREFIX)readelf CFLAGS := -nostdlib -ffreestanding -fno-builtin -fno-stack-protector \ -Os -Wall -Wextra -std=gnu99 \ @@ -34,9 +45,9 @@ all: $(TARGET) verify-toolchain: @command -v $(CC) >/dev/null 2>&1 || { \ - echo "ERROR: $(CC) not in PATH"; \ - echo "Install: sudo dnf install -y epel-release && \\"; \ - echo " sudo dnf install -y gcc-arm-linux-gnu binutils-arm-linux-gnu"; \ + echo "ERROR: ARM cross-compiler not in PATH (tried arm-linux-gnu-gcc, arm-linux-gnueabi-gcc)"; \ + echo " Debian / Ubuntu: sudo apt install gcc-arm-linux-gnueabi binutils-arm-linux-gnueabi"; \ + echo " Rocky / Fedora: sudo dnf install gcc-arm-linux-gnu binutils-arm-linux-gnu"; \ exit 1; } @$(CC) --version | head -n1 @@ -51,7 +62,7 @@ $(BUILD_DIR)/su.o: su.c | $(BUILD_DIR) verify-toolchain $(TARGET): $(OBJS) $(CC) $(LDFLAGS) -o $@ $^ $(LIBS) - arm-linux-gnu-strip --strip-all $@ + $(STRIP) --strip-all $@ @echo "" @echo "Built: $@" @file $@ | sed 's/^/ /' @@ -62,7 +73,7 @@ $(TARGET): $(OBJS) check: $(TARGET) @file $(TARGET) | grep -q "ELF 32-bit LSB.*ARM" || { echo "FAIL: not ARM ELF"; exit 1; } @file $(TARGET) | grep -q "statically linked" || { echo "FAIL: not statically linked"; exit 1; } - @! arm-linux-gnu-readelf -d $(TARGET) | grep -q NEEDED || { echo "FAIL: has NEEDED entries"; exit 1; } + @! $(READELF) -d $(TARGET) | grep -q NEEDED || { echo "FAIL: has NEEDED entries"; exit 1; } @echo "OK: $(TARGET) is a self-contained statically-linked ARM ELF" clean: From 96a5d46a9c995d3db3089793b2cbccdd79b4aa71 Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 20:33:43 +0100 Subject: [PATCH 04/17] Restrict CI firmware discovery to allowlisted upstream releases. Only build y1-stock-rom tags 3.0.2 and Latest-3.0.7 (published as @3.0.7) and rockbox stable-v0.5+. Fix patched APK and devel image path resolution for CI matrix builds. Co-authored-by: Cursor --- .github/workflows/build-firmware-releases.yml | 2 +- README.md | 8 +- apply.bash | 17 +- docs/SUPPORTED-FIRMWARE-CI.md | 25 +- src/patches/patch_y1_apk.py | 345 +++++++++++------- tools/ci/build-one.sh | 30 +- tools/ci/discover-inputs.sh | 145 ++++++-- 7 files changed, 390 insertions(+), 182 deletions(-) diff --git a/.github/workflows/build-firmware-releases.yml b/.github/workflows/build-firmware-releases.yml index 969b764..e78a0b1 100644 --- a/.github/workflows/build-firmware-releases.yml +++ b/.github/workflows/build-firmware-releases.yml @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Discover upstream rom.zip releases + - name: Discover allowlisted upstream rom.zip releases id: discover env: GH_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index 580acd1..d0fc76c 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,10 @@ Stock sizes: 3.0.2 `rom.zip` 259,502,414 bytes (raw `system.img` inside); 3.0.7 ## Automated releases (GitHub Actions) -Workflow [`.github/workflows/build-firmware-releases.yml`](.github/workflows/build-firmware-releases.yml) discovers every upstream release asset named **`rom.zip`** from: +Workflow [`.github/workflows/build-firmware-releases.yml`](.github/workflows/build-firmware-releases.yml) builds an allowlisted set of upstream **`rom.zip`** releases: -- [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) -- [rockbox-y1/rockbox](https://github.com/rockbox-y1/rockbox) +- [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom): upstream tags **3.0.2** and **Latest-3.0.7** (published as `y1-stock-rom@3.0.2` / `@3.0.7`) +- [rockbox-y1/rockbox](https://github.com/rockbox-y1/rockbox): **stable-v0.5** and newer **stable-v\*** releases For each input it runs `./apply.bash --all --no-flash --accept-any-firmware`, repacks `rom.zip`, and publishes a release on this repo. @@ -121,7 +121,7 @@ For each input it runs `./apply.bash --all --no-flash --accept-any-firmware`, re **Triggers:** weekly schedule, pushes that touch patcher code, and manual `workflow_dispatch` (optional `force` / `source_repo` filter). -**Confidence:** Stock **3.0.2** / **3.0.7** OTAs are hardware-verified. Older stock builds and Rockbox `rom.zip` files are built best-effort (`--skip-md5`); byte patchers may fail until offsets are validated for that image. See [`docs/SUPPORTED-FIRMWARE-CI.md`](docs/SUPPORTED-FIRMWARE-CI.md). +**Confidence:** Stock **3.0.2** / **3.0.7** OTAs are hardware-verified. Rockbox stable releases are built best-effort. See [`docs/SUPPORTED-FIRMWARE-CI.md`](docs/SUPPORTED-FIRMWARE-CI.md). Local dry-run (no GitHub publish): diff --git a/apply.bash b/apply.bash index 8517ce0..0fe6c96 100755 --- a/apply.bash +++ b/apply.bash @@ -462,9 +462,10 @@ patch_in_place_y1_apk() { local mount_rel="$1" local stage_dir="${PATH_TMP_STAGE}/$(basename "${mount_rel}")" local stock="${stage_dir}/stock.apk" - local patched="${PATH_SCRIPT_DIR}/src/patches/output/com.innioasis.y1_${VERSION_FIRMWARE}-patched.apk" + local out_dir="${PATH_SCRIPT_DIR}/src/patches/output" + local patched="" - mkdir -p "${stage_dir}" + mkdir -p "${stage_dir}" "${out_dir}" echo " ${mount_rel}: extract → patch_y1_apk.py → write-back" sudo cp "${PATH_MOUNT}/${mount_rel}" "${stock}" sudo chown "$(id -u):$(id -g)" "${stock}" @@ -481,10 +482,18 @@ patch_in_place_y1_apk() { exit 1 fi - if [[ ! -f "${patched}" ]]; then - echo "ERROR: patch_y1_apk.py did not produce ${patched}" >&2 + shopt -s nullglob + local produced=( "${out_dir}"/com.innioasis.y1_*-patched.apk ) + shopt -u nullglob + if (( ${#produced[@]} == 0 )); then + echo "ERROR: patch_y1_apk.py did not produce com.innioasis.y1_*-patched.apk in ${out_dir}" >&2 + exit 1 + fi + if (( ${#produced[@]} > 1 )); then + echo "ERROR: multiple patched APKs in ${out_dir}: ${produced[*]}" >&2 exit 1 fi + patched="${produced[0]}" if ! sudo cp "${patched}" "${PATH_MOUNT}/${mount_rel}"; then echo "ERROR: failed to write patched ${mount_rel} back to mount" >&2 diff --git a/docs/SUPPORTED-FIRMWARE-CI.md b/docs/SUPPORTED-FIRMWARE-CI.md index af9b297..bc5b5f5 100644 --- a/docs/SUPPORTED-FIRMWARE-CI.md +++ b/docs/SUPPORTED-FIRMWARE-CI.md @@ -2,14 +2,20 @@ GitHub Actions workflow [`build-firmware-releases.yml`](../.github/workflows/build-firmware-releases.yml) produces patched **`rom.zip`** releases on [ryan-specter/koensayr-auto](https://github.com/ryan-specter/koensayr-auto/releases). -## Upstream sources +## Upstream sources (allowlist) -| Repository | Asset filter | Example release tags | -|------------|--------------|----------------------| -| [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) | `rom.zip` only | `Latest-3.0.7`, `3.0.2`, `2.8.2`, `ADB-2.1.9` | -| [rockbox-y1/rockbox](https://github.com/rockbox-y1/rockbox) | `rom.zip` only | `stable-v0.5`, recent `nightly-*` tags | +CI only builds these upstream tags (see [`tools/ci/discover-inputs.sh`](../tools/ci/discover-inputs.sh)): -**Not built:** `rom_type_b.zip`, `rom_240p.zip`, `update.zip`, voice packs, and other non-`rom.zip` assets. +| Repository | Upstream tags | Asset | +|------------|---------------|-------| +| [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) | upstream tags **3.0.2**, **Latest-3.0.7** | `rom.zip` | +| [rockbox-y1/rockbox](https://github.com/rockbox-y1/rockbox) | **stable-v0.5** and newer **stable-v\*** releases | `rom.zip` | + +**Koensayr release names:** stock **3.0.7** firmware is published as `y1-stock-rom@3.0.7` even though the upstream tag is `Latest-3.0.7`. Release notes still record the upstream tag. + +**Not built by CI:** other stock tags (`2.8.2`, `ADB-2.1.9`, `type-b-1.7.6`, …), Rockbox `nightly-*`, `rom_type_b.zip`, `rom_240p.zip`, `update.zip`, `rockbox.apk`, voice packs, and other assets. + +To add a stock tag or change Rockbox rules, edit the allowlist in `discover-inputs.sh`. ## Output naming @@ -32,9 +38,8 @@ Diagnostic tooling under `tools/` is **not** embedded in the ROM. | Input | CI expectation | |-------|----------------| -| y1-stock-rom **3.0.2** / **3.0.7** | Highest confidence; matches [`KNOWN_FIRMWARES`](../apply.bash) | -| y1-stock-rom **2.8.2** / **ADB-2.1.9** | Attempted with `--skip-md5`; may fail until patch offsets are verified | -| rockbox **rom.zip** | Best-effort; custom `system.img` / BT stack may differ from stock | +| y1-stock-rom **3.0.2** / **3.0.7** | Supported; matches [`KNOWN_FIRMWARES`](../apply.bash) | +| rockbox **stable-v0.5+** `rom.zip` | Best-effort; custom `system.img` / BT stack may differ from stock | Failed matrix jobs do not block other releases (`fail-fast: false`). @@ -46,7 +51,7 @@ A build is skipped when a release already exists and its notes contain the upstr | Script | Role | |--------|------| -| [`tools/ci/discover-inputs.sh`](../tools/ci/discover-inputs.sh) | Emit JSON matrix of upstream `rom.zip` assets | +| [`tools/ci/discover-inputs.sh`](../tools/ci/discover-inputs.sh) | Emit JSON matrix for allowlisted upstream `rom.zip` assets | | [`tools/ci/build-one.sh`](../tools/ci/build-one.sh) | Download → patch → repack → `gh release` | | [`tools/ci/extract-rom.sh`](../tools/ci/extract-rom.sh) | Unzip upstream ROM; record sparse `system.img` | | [`tools/ci/repack-rom.sh`](../tools/ci/repack-rom.sh) | Replace `system.img` and zip patched `rom.zip` | diff --git a/src/patches/patch_y1_apk.py b/src/patches/patch_y1_apk.py index d219533..30c914b 100755 --- a/src/patches/patch_y1_apk.py +++ b/src/patches/patch_y1_apk.py @@ -1057,36 +1057,126 @@ def _repl(m): # ============================================================ -# Patch H: BaseActivity.smali — propagate unhandled discrete media keys +# Patch H / H': dispatchKeyEvent — AVRCP discrete media key propagation # ============================================================ -# Stock dispatchKeyEvent always returns TRUE, swallowing AVRCP-derived -# KEYCODE_MEDIA_PLAY/_PAUSE/_STOP/_NEXT/_PREVIOUS that don't match the -# device's KeyMap. We early-return FALSE on those keycodes for repeatCount==0 -# (so they propagate to AudioService → PlayControllerReceiver Patch E discrete -# arms) and TRUE on repeatCount>0 (silent consume — defangs framework -# InputDispatcher::synthesizeKeyRepeatLocked synthesised repeats that drove -# the "stuck fast-forwarding" symptom). Full rationale + side-effects -# (hardware NEXT/PREV touch buttons lose long-press FF/RW) in -# docs/PATCHES.md Patch H section. -BASE_ACTIVITY_SMALI = "smali/com/innioasis/y1/base/BaseActivity.smali" -base_activity_path = os.path.join(UNPACKED_DIR, BASE_ACTIVITY_SMALI) -if not os.path.exists(base_activity_path): - sys.exit(f"ERROR: Expected smali not found: {base_activity_path}") +def _patch_h_avrcp_block(key_reg: str, label_prefix: str) -> str: + """Smali inserted immediately after getKeyCode move-result (uses v3 scratch).""" + L = label_prefix + return ( + f" const/16 v3, 0x7e\n\n" + f" if-eq {key_reg}, v3, :{L}_avrcp_key\n\n" + f" const/16 v3, 0x7f\n\n" + f" if-eq {key_reg}, v3, :{L}_avrcp_key\n\n" + f" const/16 v3, 0x56\n\n" + f" if-eq {key_reg}, v3, :{L}_avrcp_key\n\n" + f" const/16 v3, 0x57\n\n" + f" if-eq {key_reg}, v3, :{L}_avrcp_key\n\n" + f" const/16 v3, 0x58\n\n" + f" if-eq {key_reg}, v3, :{L}_avrcp_key\n\n" + f" goto :{L}_continue\n\n" + f" :{L}_avrcp_key\n" + f" invoke-virtual {{p1}}, Landroid/view/KeyEvent;->getRepeatCount()I\n\n" + f" move-result v3\n\n" + f" if-eqz v3, :{L}_propagate\n\n" + f" const/4 v0, 0x1\n\n" + f" return v0\n\n" + f" :{L}_propagate\n" + f" const/4 v0, 0x0\n\n" + f" return v0\n\n" + f" :{L}_continue" + ) -with open(base_activity_path, 'r') as f: - base_activity_src = f.read() -OLD_DISPATCH_HEAD = """\ -.method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z - .locals 7 +def _dispatch_keyevent_anchor(smali_src: str): + """Return (insert_pos, key_reg, locals_n) for dispatchKeyEvent getKeyCode, or None.""" + meth = re.search( + r"\.method public dispatchKeyEvent\(Landroid/view/KeyEvent;\)Z.*?(?=\.end method)", + smali_src, + re.DOTALL, + ) + if not meth: + return None + body = meth.group(0) + anch = re.search( + r"invoke-virtual \{p1\}, Landroid/view/KeyEvent;->getKeyCode\(\)I\n+" + r" move-result (v\d+)\n+", + body, + ) + if not anch: + return None + locals_m = re.search(r" \.locals (\d+)", body) + locals_n = int(locals_m.group(1)) if locals_m else 2 + insert_pos = meth.start() + anch.end() + return insert_pos, anch.group(1), locals_n + + +def _apply_dispatch_at_method_entry(smali_src: str, label_prefix: str) -> tuple[str, bool]: + """Insert AVRCP key handling at the start of dispatchKeyEvent (Kotlin / unknown layouts).""" + hdr = re.search( + r"(\.method public dispatchKeyEvent\(Landroid/view/KeyEvent;\)Z\n \.locals (\d+)\n\n)", + smali_src, + ) + if not hdr: + return smali_src, False + + insert_at = hdr.end() + locals_n = int(hdr.group(2)) + if locals_n < 4: + new_hdr = re.sub(r"\.locals \d+", ".locals 4", hdr.group(1), count=1) + smali_src = smali_src[: hdr.start()] + new_hdr + smali_src[hdr.end() :] + insert_at = hdr.start() + len(new_hdr) + + meth = re.search( + r"\.method public dispatchKeyEvent\(Landroid/view/KeyEvent;\)Z.*?\.end method", + smali_src[hdr.start() :], + re.DOTALL, + ) + if not meth: + return smali_src, False - const/4 v0, 0x1 + kc = re.search( + r"invoke-virtual \{p1\}, Landroid/view/KeyEvent;->getKeyCode\(\)I\n\s+move-result (v\d+)", + meth.group(0), + ) + key_reg = kc.group(1) if kc else "v2" + block = _patch_h_avrcp_block(key_reg, label_prefix) + "\n\n" + return smali_src[:insert_at] + block + smali_src[insert_at:], True + + +def _apply_dispatch_keyevent_patch(smali_src: str, label_prefix: str, exact_pairs): + """Try known prologue replacements, then anchor insert after getKeyCode.""" + for old, new in exact_pairs: + if old in smali_src: + return smali_src.replace(old, new, 1), True + + anchor = _dispatch_keyevent_anchor(smali_src) + if anchor: + insert_pos, key_reg, locals_n = anchor + if locals_n < 4: + smali_src = re.sub( + r"(\.method public dispatchKeyEvent\(Landroid/view/KeyEvent;\)Z\n" + r" \.locals )\d+", + r"\g<1>4", + smali_src, + count=1, + ) + anchor = _dispatch_keyevent_anchor(smali_src) + if anchor: + insert_pos, key_reg, locals_n = anchor - if-nez p1, :cond_0 + block = _patch_h_avrcp_block(key_reg, label_prefix) + "\n\n" + return smali_src[:insert_pos] + block + smali_src[insert_pos:], True - return v0 + return _apply_dispatch_at_method_entry(smali_src, label_prefix) + +def _dispatch_head_with_avrcp_block(key_reg: str, label_prefix: str, suffix: str) -> str: + return _patch_h_avrcp_block(key_reg, label_prefix) + "\n\n " + suffix + + +# Stock Java (3.0.2 / 3.0.7) — includes .line debug comments. +_OLD_DISPATCH_JAVA_LINES = """\ .line 673 :cond_0 invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I @@ -1100,75 +1190,139 @@ def _repl(m): const/4 v3, 0x3""" -NEW_DISPATCH_HEAD = """\ -.method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z - .locals 7 - - const/4 v0, 0x1 - - if-nez p1, :cond_0 - - return v0 - - .line 673 +# Same logic without .line comments (EN_2.8.x and other Kotlin builds). +_OLD_DISPATCH_JAVA = """\ :cond_0 invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I move-result v1 - .line 674 invoke-virtual {p1}, Landroid/view/KeyEvent;->getKeyCode()I move-result v2 - const/16 v3, 0x7e + const/4 v3, 0x3""" - if-eq v2, v3, :patch_h_avrcp_key +_OLD_DISPATCH_KOTLIN = """\ + :cond_0 + invoke-static {p1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNull(Ljava/lang/Object;)V - const/16 v3, 0x7f + invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I - if-eq v2, v3, :patch_h_avrcp_key + move-result v1 - const/16 v3, 0x56 + invoke-virtual {p1}, Landroid/view/KeyEvent;->getKeyCode()I - if-eq v2, v3, :patch_h_avrcp_key + move-result v2 - const/16 v3, 0x57 + const/4 v3, 0x3""" - if-eq v2, v3, :patch_h_avrcp_key - const/16 v3, 0x58 +def _base_activity_dispatch_pairs(): + prefix = """\ +.method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z + .locals 7 - if-eq v2, v3, :patch_h_avrcp_key + const/4 v0, 0x1 - goto :patch_h_continue + if-nez p1, :cond_0 - :patch_h_avrcp_key - invoke-virtual {p1}, Landroid/view/KeyEvent;->getRepeatCount()I + return v0 - move-result v3 +""" + suffix = _dispatch_head_with_avrcp_block("v2", "patch_h", "const/4 v3, 0x3") + pairs = [] + for old_mid in (_OLD_DISPATCH_JAVA_LINES, _OLD_DISPATCH_JAVA, _OLD_DISPATCH_KOTLIN): + pairs.append((prefix + old_mid, prefix + suffix)) + # Kotlin 2.8.x often declares .locals 2 while still using v2/v3. + prefix_l2 = prefix.replace(".locals 7", ".locals 2", 1) + pairs.append((prefix_l2 + _OLD_DISPATCH_KOTLIN, prefix_l2 + suffix)) + return pairs + + +def _base_player_dispatch_pairs(): + pairs = [] + + def _player_new_head(line304: str) -> str: + return ( + ".method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z\n" + " .locals 2\n\n" + " invoke-virtual {p1}, Landroid/view/KeyEvent;->getKeyCode()I\n\n" + " move-result v0\n\n" + + _patch_h_avrcp_block("v0", "patch_h2") + + "\n\n" + + line304 + + " invoke-static {p1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNull(Ljava/lang/Object;)V\n\n" + " invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I\n\n" + " move-result v0" + ) - if-eqz v3, :patch_h_propagate + old_with_line = ( + ".method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z\n" + " .locals 2\n\n" + " .line 304\n" + " invoke-static {p1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNull(Ljava/lang/Object;)V\n\n" + " invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I\n\n" + " move-result v0" + ) + pairs.append((old_with_line, _player_new_head(" .line 304\n"))) + pairs.append(( + old_with_line.replace(" .line 304\n", ""), + _player_new_head(""), + )) + + # EN_2.8.x: getKeyCode already appears before getAction (anchor / in-place block). + old_kc_first = ( + ".method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z\n" + " .locals 2\n\n" + " invoke-static {p1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNull(Ljava/lang/Object;)V\n\n" + " invoke-virtual {p1}, Landroid/view/KeyEvent;->getKeyCode()I\n\n" + " move-result v0\n\n" + " invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I\n\n" + " move-result v1" + ) + new_kc_first = ( + old_kc_first.replace( + " move-result v0\n\n invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I", + " move-result v0\n\n" + + _patch_h_avrcp_block("v0", "patch_h2") + + "\n\n invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I", + 1, + ) + ) + pairs.append((old_kc_first, new_kc_first)) + return pairs - return v0 - :patch_h_propagate - const/4 v0, 0x0 +# Patch H: BaseActivity.smali — propagate unhandled discrete media keys +# Stock dispatchKeyEvent always returns TRUE, swallowing AVRCP-derived +# KEYCODE_MEDIA_PLAY/_PAUSE/_STOP/_NEXT/_PREVIOUS that don't match the +# device's KeyMap. We early-return FALSE on those keycodes for repeatCount==0 +# (so they propagate to AudioService → PlayControllerReceiver Patch E discrete +# arms) and TRUE on repeatCount>0 (silent consume — defangs framework +# InputDispatcher::synthesizeKeyRepeatLocked synthesised repeats that drove +# the "stuck fast-forwarding" symptom). Full rationale + side-effects +# (hardware NEXT/PREV touch buttons lose long-press FF/RW) in +# docs/PATCHES.md Patch H section. - return v0 +BASE_ACTIVITY_SMALI = "smali/com/innioasis/y1/base/BaseActivity.smali" +base_activity_path = os.path.join(UNPACKED_DIR, BASE_ACTIVITY_SMALI) +if not os.path.exists(base_activity_path): + sys.exit(f"ERROR: Expected smali not found: {base_activity_path}") - :patch_h_continue - const/4 v3, 0x3""" +with open(base_activity_path, 'r') as f: + base_activity_src = f.read() -if OLD_DISPATCH_HEAD not in base_activity_src: +base_activity_src, patch_h_ok = _apply_dispatch_keyevent_patch( + base_activity_src, "patch_h", _base_activity_dispatch_pairs() +) +if not patch_h_ok: sys.exit( "ERROR: BaseActivity dispatchKeyEvent prologue not found.\n" f" File: {base_activity_path}\n" " The smali shape may differ from supported stock builds." ) -base_activity_src = base_activity_src.replace(OLD_DISPATCH_HEAD, NEW_DISPATCH_HEAD, 1) - if DEBUG_LOGGING: base_activity_src = _inject_log_d( base_activity_src, @@ -1205,83 +1359,16 @@ def _repl(m): with open(base_player_activity_path, 'r') as f: base_player_activity_src = f.read() -OLD_PLAYER_DISPATCH_HEAD = """\ -.method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z - .locals 2 - - .line 304 - invoke-static {p1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNull(Ljava/lang/Object;)V - - invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I - - move-result v0""" - -NEW_PLAYER_DISPATCH_HEAD = """\ -.method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z - .locals 2 - - invoke-virtual {p1}, Landroid/view/KeyEvent;->getKeyCode()I - - move-result v0 - - const/16 v1, 0x7e - - if-eq v0, v1, :patch_h2_avrcp_key - - const/16 v1, 0x7f - - if-eq v0, v1, :patch_h2_avrcp_key - - const/16 v1, 0x56 - - if-eq v0, v1, :patch_h2_avrcp_key - - const/16 v1, 0x57 - - if-eq v0, v1, :patch_h2_avrcp_key - - const/16 v1, 0x58 - - if-eq v0, v1, :patch_h2_avrcp_key - - goto :patch_h2_continue - - :patch_h2_avrcp_key - invoke-virtual {p1}, Landroid/view/KeyEvent;->getRepeatCount()I - - move-result v0 - - if-eqz v0, :patch_h2_propagate - - const/4 v0, 0x1 - - return v0 - - :patch_h2_propagate - const/4 v0, 0x0 - - return v0 - - :patch_h2_continue - - .line 304 - invoke-static {p1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNull(Ljava/lang/Object;)V - - invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I - - move-result v0""" - -if OLD_PLAYER_DISPATCH_HEAD not in base_player_activity_src: +base_player_activity_src, patch_h2_ok = _apply_dispatch_keyevent_patch( + base_player_activity_src, "patch_h2", _base_player_dispatch_pairs() +) +if not patch_h2_ok: sys.exit( "ERROR: BasePlayerActivity dispatchKeyEvent prologue not found.\n" f" File: {base_player_activity_path}\n" " The smali shape may differ from supported stock builds." ) -base_player_activity_src = base_player_activity_src.replace( - OLD_PLAYER_DISPATCH_HEAD, NEW_PLAYER_DISPATCH_HEAD, 1 -) - if DEBUG_LOGGING: base_player_activity_src = _inject_log_d( base_player_activity_src, diff --git a/tools/ci/build-one.sh b/tools/ci/build-one.sh index c658181..51ffc6f 100644 --- a/tools/ci/build-one.sh +++ b/tools/ci/build-one.sh @@ -88,11 +88,35 @@ echo "[build-one] Patching (koensayr ${KOENSAYR_VERSION}).." --firmware-slug "$SLUG" \ --artifacts-dir "$STAGING" -DEVEL_IMG="${STAGING}/system-${SLUG}-devel.img" -if [[ ! -f "$DEVEL_IMG" ]]; then - echo "ERROR: expected patched image ${DEVEL_IMG}" >&2 +# apply.bash names the loop-mounted image system-${VERSION_FIRMWARE}-devel.img +# (manifest version e.g. 3.0.2, or --firmware-slug when using --accept-any-firmware). +resolve_devel_img() { + local staging="$1" slug="$2" source_tag="$3" + local p + for p in \ + "${staging}/system-${slug}-devel.img" \ + "${staging}/system-${source_tag}-devel.img"; do + if [[ -f "$p" ]]; then + echo "$p" + return 0 + fi + done + for p in "${staging}"/system-*-devel.img; do + if [[ -f "$p" ]]; then + echo "$p" + return 0 + fi + done + return 1 +} + +DEVEL_IMG="$(resolve_devel_img "$STAGING" "$SLUG" "$SOURCE_TAG" || true)" +if [[ -z "$DEVEL_IMG" || ! -f "$DEVEL_IMG" ]]; then + echo "ERROR: expected patched system image under ${STAGING}/" >&2 + echo " (tried system-${SLUG}-devel.img and system-${SOURCE_TAG}-devel.img)" >&2 exit 1 fi +echo "[build-one] Using patched system image: ${DEVEL_IMG}" OUTPUT_ROM="${WORKDIR}/rom-koensayr.zip" "${CI_DIR}/repack-rom.sh" "$EXTRACT" "$DEVEL_IMG" "$OUTPUT_ROM" diff --git a/tools/ci/discover-inputs.sh b/tools/ci/discover-inputs.sh index 22aac3f..5f21327 100644 --- a/tools/ci/discover-inputs.sh +++ b/tools/ci/discover-inputs.sh @@ -1,10 +1,12 @@ #!/usr/bin/env bash -# discover-inputs.sh — list upstream GitHub releases that ship rom.zip. +# discover-inputs.sh — build matrix for allowed upstream rom.zip releases. +# +# Only these upstream tags are considered (no open-ended gh release scan): +# y1-community/y1-stock-rom → tags 3.0.2, Latest-3.0.7 (firmware 3.0.7) +# rockbox-y1/rockbox → stable-v0.5 and later stable-v* tags # # Usage: # ./tools/ci/discover-inputs.sh [--source-repo OWNER/NAME] [--force] -# -# Writes JSON array to stdout (GHA matrix "include" entries). set -euo pipefail @@ -28,8 +30,9 @@ Usage: ./tools/ci/discover-inputs.sh [--source-repo OWNER/NAME] [--force] Emits a JSON array of matrix objects: source_repo, source_tag, release_tag, download_url, digest, slug -Only assets named exactly "rom.zip" are included. -Repos scanned: y1-community/y1-stock-rom, rockbox-y1/rockbox +Upstream allowlist (rom.zip only): + y1-community/y1-stock-rom: 3.0.2, Latest-3.0.7 (→ koensayr release y1-stock-rom@3.0.7) + rockbox-y1/rockbox: stable-v0.5 and newer stable-v* releases EOF exit 0 ;; @@ -55,52 +58,132 @@ REPOS=( ) python3 - "$SOURCE_FILTER" "$FORCE" "${REPOS[@]}" <<'PY' -import json, subprocess, sys +import json +import subprocess +import sys source_filter = sys.argv[1] force = sys.argv[2] == "true" repos = sys.argv[3:] +Y1_REPO = "y1-community/y1-stock-rom" +ROCKBOX_REPO = "rockbox-y1/rockbox" +# Upstream GitHub release tag → firmware version for koensayr release naming. +Y1_UPSTREAM_TAGS = { + "3.0.2": "3.0.2", + "Latest-3.0.7": "3.0.7", +} +ROCKBOX_MIN_STABLE = (0, 5, 0) + + def slug_for_repo(full_name: str) -> str: - if full_name == "y1-community/y1-stock-rom": + if full_name == Y1_REPO: return "y1-stock-rom" - if full_name == "rockbox-y1/rockbox": + if full_name == ROCKBOX_REPO: return "rockbox" return full_name.split("/")[-1] -entries = [] -for repo in repos: - if source_filter and repo != source_filter: - continue - slug_base = slug_for_repo(repo) +def parse_stable_v(tag: str) -> tuple[int, ...] | None: + if not tag.startswith("stable-v"): + return None + rest = tag[len("stable-v") :] + parts: list[int] = [] + for part in rest.split("."): + if not part.isdigit(): + return None + parts.append(int(part)) + while len(parts) < 3: + parts.append(0) + return tuple(parts[:3]) + + +def tag_allowed(repo: str, tag: str) -> bool: + if repo == Y1_REPO: + return tag in Y1_UPSTREAM_TAGS + if repo == ROCKBOX_REPO: + ver = parse_stable_v(tag) + return ver is not None and ver >= ROCKBOX_MIN_STABLE + return False + + +def tags_to_probe(repo: str) -> list[str]: + if repo == Y1_REPO: + return sorted( + Y1_UPSTREAM_TAGS.keys(), + key=lambda t: Y1_UPSTREAM_TAGS[t], + ) + if repo == ROCKBOX_REPO: + out = subprocess.run( + ["gh", "api", f"repos/{repo}/releases", "--paginate", "--jq", ".[].tag_name"], + capture_output=True, + text=True, + check=True, + ) + tags = json.loads(out.stdout) + allowed = [t for t in tags if tag_allowed(repo, t)] + return sorted(allowed, key=lambda t: parse_stable_v(t) or ()) + return [] + + +def release_has_rom_zip(repo: str, tag: str) -> dict | None: out = subprocess.run( - ["gh", "api", f"repos/{repo}/releases", "--paginate"], + [ + "gh", + "api", + f"repos/{repo}/releases/tags/{tag}", + "--jq", + '[.assets[] | select(.name == "rom.zip")][0]', + ], capture_output=True, text=True, - check=True, + check=False, ) - releases = json.loads(out.stdout) - for rel in releases: - if rel.get("draft"): + if out.returncode != 0 or not out.stdout.strip() or out.stdout.strip() == "null": + return None + try: + asset = json.loads(out.stdout) + except json.JSONDecodeError: + return None + digest = asset.get("digest") or "" + if digest.startswith("sha256:"): + digest = digest[7:] + return { + "browser_download_url": asset["browser_download_url"], + "digest": digest, + } + + +entries: list[dict] = [] + +for repo in repos: + if source_filter and repo != source_filter: + continue + slug_base = slug_for_repo(repo) + for upstream_tag in tags_to_probe(repo): + asset = release_has_rom_zip(repo, upstream_tag) + if asset is None: continue - tag = rel["tag_name"] - for asset in rel.get("assets") or []: - if asset.get("name") != "rom.zip": - continue - digest = asset.get("digest") or "" - if digest.startswith("sha256:"): - digest = digest[7:] - release_tag = f"{slug_base}@{tag}" - entries.append({ + if repo == Y1_REPO: + fw_version = Y1_UPSTREAM_TAGS[upstream_tag] + release_tag = f"{slug_base}@{fw_version}" + slug = f"{slug_base}-{fw_version}" + source_tag = upstream_tag + else: + release_tag = f"{slug_base}@{upstream_tag}" + slug = release_tag.replace("@", "-") + source_tag = upstream_tag + entries.append( + { "source_repo": repo, - "source_tag": tag, + "source_tag": source_tag, "release_tag": release_tag, "download_url": asset["browser_download_url"], - "digest": digest, - "slug": release_tag.replace("@", "-"), + "digest": asset["digest"], + "slug": slug, "force": force, - }) + } + ) print(json.dumps(entries)) PY From 191dfc53b625bafe7b231134596d96d7b0a4ae14 Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 20:34:42 +0100 Subject: [PATCH 05/17] Fix discover job JSON parse for Rockbox release tags. Co-authored-by: Cursor --- tools/ci/discover-inputs.sh | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tools/ci/discover-inputs.sh b/tools/ci/discover-inputs.sh index 5f21327..9508951 100644 --- a/tools/ci/discover-inputs.sh +++ b/tools/ci/discover-inputs.sh @@ -107,6 +107,20 @@ def tag_allowed(repo: str, tag: str) -> bool: return False +def upstream_release_tags(repo: str) -> list[str]: + """Tag names from gh release list (JSON array, not paginated NDJSON).""" + out = subprocess.run( + ["gh", "release", "list", "--repo", repo, "--limit", "200", "--json", "tagName"], + capture_output=True, + text=True, + check=True, + ) + if not out.stdout.strip(): + return [] + releases = json.loads(out.stdout) + return [r["tagName"] for r in releases if r.get("tagName")] + + def tags_to_probe(repo: str) -> list[str]: if repo == Y1_REPO: return sorted( @@ -114,13 +128,7 @@ def tags_to_probe(repo: str) -> list[str]: key=lambda t: Y1_UPSTREAM_TAGS[t], ) if repo == ROCKBOX_REPO: - out = subprocess.run( - ["gh", "api", f"repos/{repo}/releases", "--paginate", "--jq", ".[].tag_name"], - capture_output=True, - text=True, - check=True, - ) - tags = json.loads(out.stdout) + tags = upstream_release_tags(repo) allowed = [t for t in tags if tag_allowed(repo, t)] return sorted(allowed, key=lambda t: parse_stable_v(t) or ()) return [] From d56db684e896198a9cc8fbd3adbd035b6bf12278 Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 20:47:22 +0100 Subject: [PATCH 06/17] Limit CI firmware builds to y1-stock-rom 3.0.2 and 3.0.7 only. Rockbox releases are removed from discover-inputs.sh because the stock patcher requires com.innioasis.y1 in system/app. Docs updated to match. Co-authored-by: Cursor --- README.md | 9 ++-- docs/SUPPORTED-FIRMWARE-CI.md | 12 ++--- tools/ci/discover-inputs.sh | 91 +++++------------------------------ 3 files changed, 22 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index d0fc76c..c8297f4 100644 --- a/README.md +++ b/README.md @@ -110,18 +110,17 @@ Stock sizes: 3.0.2 `rom.zip` 259,502,414 bytes (raw `system.img` inside); 3.0.7 ## Automated releases (GitHub Actions) -Workflow [`.github/workflows/build-firmware-releases.yml`](.github/workflows/build-firmware-releases.yml) builds an allowlisted set of upstream **`rom.zip`** releases: +Workflow [`.github/workflows/build-firmware-releases.yml`](.github/workflows/build-firmware-releases.yml) builds an allowlisted set of [y1-stock-rom](https://github.com/y1-community/y1-stock-rom) **`rom.zip`** releases only: -- [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom): upstream tags **3.0.2** and **Latest-3.0.7** (published as `y1-stock-rom@3.0.2` / `@3.0.7`) -- [rockbox-y1/rockbox](https://github.com/rockbox-y1/rockbox): **stable-v0.5** and newer **stable-v\*** releases +- Upstream tags **3.0.2** and **Latest-3.0.7** (published as `y1-stock-rom@3.0.2` / `@3.0.7`) For each input it runs `./apply.bash --all --no-flash --accept-any-firmware`, repacks `rom.zip`, and publishes a release on this repo. -**Release tag pattern:** `{repo-slug}@{upstream-tag}` (e.g. `y1-stock-rom@3.0.2`, `rockbox@stable-v0.5`). Each release attaches one patched **`rom.zip`**. +**Release tag pattern:** `y1-stock-rom@{firmware-version}` (e.g. `y1-stock-rom@3.0.2`). Each release attaches one patched **`rom.zip`**. **Triggers:** weekly schedule, pushes that touch patcher code, and manual `workflow_dispatch` (optional `force` / `source_repo` filter). -**Confidence:** Stock **3.0.2** / **3.0.7** OTAs are hardware-verified. Rockbox stable releases are built best-effort. See [`docs/SUPPORTED-FIRMWARE-CI.md`](docs/SUPPORTED-FIRMWARE-CI.md). +**Confidence:** Stock **3.0.2** / **3.0.7** OTAs are hardware-verified. See [`docs/SUPPORTED-FIRMWARE-CI.md`](docs/SUPPORTED-FIRMWARE-CI.md). Local dry-run (no GitHub publish): diff --git a/docs/SUPPORTED-FIRMWARE-CI.md b/docs/SUPPORTED-FIRMWARE-CI.md index bc5b5f5..0215b98 100644 --- a/docs/SUPPORTED-FIRMWARE-CI.md +++ b/docs/SUPPORTED-FIRMWARE-CI.md @@ -8,18 +8,19 @@ CI only builds these upstream tags (see [`tools/ci/discover-inputs.sh`](../tools | Repository | Upstream tags | Asset | |------------|---------------|-------| -| [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) | upstream tags **3.0.2**, **Latest-3.0.7** | `rom.zip` | -| [rockbox-y1/rockbox](https://github.com/rockbox-y1/rockbox) | **stable-v0.5** and newer **stable-v\*** releases | `rom.zip` | +| [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) | **3.0.2**, **Latest-3.0.7** | `rom.zip` | **Koensayr release names:** stock **3.0.7** firmware is published as `y1-stock-rom@3.0.7` even though the upstream tag is `Latest-3.0.7`. Release notes still record the upstream tag. -**Not built by CI:** other stock tags (`2.8.2`, `ADB-2.1.9`, `type-b-1.7.6`, …), Rockbox `nightly-*`, `rom_type_b.zip`, `rom_240p.zip`, `update.zip`, `rockbox.apk`, voice packs, and other assets. +**Not built by CI:** other stock tags (`2.8.2`, `ADB-2.1.9`, `type-b-1.7.6`, …), `rom_type_b.zip`, `rom_240p.zip`, `update.zip`, voice packs, and other assets. -To add a stock tag or change Rockbox rules, edit the allowlist in `discover-inputs.sh`. +Rockbox-Y1 `rom.zip` is out of scope: the image has no Innioasis `com.innioasis.y1` music APK, so the stock patch pipeline does not apply. Build Rockbox releases manually if needed. + +To add another stock tag, extend `Y1_UPSTREAM_TAGS` in `discover-inputs.sh`. ## Output naming -- **GitHub release tag:** `{slug}@{upstream-tag}` (e.g. `y1-stock-rom@3.0.2`, `rockbox@stable-v0.5`) +- **GitHub release tag:** `{slug}@{firmware-version}` (e.g. `y1-stock-rom@3.0.2`, `y1-stock-rom@3.0.7`) - **Internal firmware slug** (`--firmware-slug`): `@` replaced with `-` (e.g. `y1-stock-rom-3.0.2`) for `system-*-devel.img` naming ## Patch set @@ -39,7 +40,6 @@ Diagnostic tooling under `tools/` is **not** embedded in the ROM. | Input | CI expectation | |-------|----------------| | y1-stock-rom **3.0.2** / **3.0.7** | Supported; matches [`KNOWN_FIRMWARES`](../apply.bash) | -| rockbox **stable-v0.5+** `rom.zip` | Best-effort; custom `system.img` / BT stack may differ from stock | Failed matrix jobs do not block other releases (`fail-fast: false`). diff --git a/tools/ci/discover-inputs.sh b/tools/ci/discover-inputs.sh index 9508951..693e1c8 100644 --- a/tools/ci/discover-inputs.sh +++ b/tools/ci/discover-inputs.sh @@ -1,9 +1,8 @@ #!/usr/bin/env bash -# discover-inputs.sh — build matrix for allowed upstream rom.zip releases. +# discover-inputs.sh — build matrix for allowed y1-stock-rom rom.zip releases. # -# Only these upstream tags are considered (no open-ended gh release scan): -# y1-community/y1-stock-rom → tags 3.0.2, Latest-3.0.7 (firmware 3.0.7) -# rockbox-y1/rockbox → stable-v0.5 and later stable-v* tags +# Only these upstream tags are considered: +# y1-community/y1-stock-rom → 3.0.2, Latest-3.0.7 (published as y1-stock-rom@3.0.2 / @3.0.7) # # Usage: # ./tools/ci/discover-inputs.sh [--source-repo OWNER/NAME] [--force] @@ -31,8 +30,7 @@ Emits a JSON array of matrix objects: source_repo, source_tag, release_tag, download_url, digest, slug Upstream allowlist (rom.zip only): - y1-community/y1-stock-rom: 3.0.2, Latest-3.0.7 (→ koensayr release y1-stock-rom@3.0.7) - rockbox-y1/rockbox: stable-v0.5 and newer stable-v* releases + y1-community/y1-stock-rom: 3.0.2, Latest-3.0.7 (→ koensayr release y1-stock-rom@3.0.2 / @3.0.7) EOF exit 0 ;; @@ -54,7 +52,6 @@ fi REPOS=( "y1-community/y1-stock-rom" - "rockbox-y1/rockbox" ) python3 - "$SOURCE_FILTER" "$FORCE" "${REPOS[@]}" <<'PY' @@ -67,71 +64,11 @@ force = sys.argv[2] == "true" repos = sys.argv[3:] Y1_REPO = "y1-community/y1-stock-rom" -ROCKBOX_REPO = "rockbox-y1/rockbox" # Upstream GitHub release tag → firmware version for koensayr release naming. Y1_UPSTREAM_TAGS = { "3.0.2": "3.0.2", "Latest-3.0.7": "3.0.7", } -ROCKBOX_MIN_STABLE = (0, 5, 0) - - -def slug_for_repo(full_name: str) -> str: - if full_name == Y1_REPO: - return "y1-stock-rom" - if full_name == ROCKBOX_REPO: - return "rockbox" - return full_name.split("/")[-1] - - -def parse_stable_v(tag: str) -> tuple[int, ...] | None: - if not tag.startswith("stable-v"): - return None - rest = tag[len("stable-v") :] - parts: list[int] = [] - for part in rest.split("."): - if not part.isdigit(): - return None - parts.append(int(part)) - while len(parts) < 3: - parts.append(0) - return tuple(parts[:3]) - - -def tag_allowed(repo: str, tag: str) -> bool: - if repo == Y1_REPO: - return tag in Y1_UPSTREAM_TAGS - if repo == ROCKBOX_REPO: - ver = parse_stable_v(tag) - return ver is not None and ver >= ROCKBOX_MIN_STABLE - return False - - -def upstream_release_tags(repo: str) -> list[str]: - """Tag names from gh release list (JSON array, not paginated NDJSON).""" - out = subprocess.run( - ["gh", "release", "list", "--repo", repo, "--limit", "200", "--json", "tagName"], - capture_output=True, - text=True, - check=True, - ) - if not out.stdout.strip(): - return [] - releases = json.loads(out.stdout) - return [r["tagName"] for r in releases if r.get("tagName")] - - -def tags_to_probe(repo: str) -> list[str]: - if repo == Y1_REPO: - return sorted( - Y1_UPSTREAM_TAGS.keys(), - key=lambda t: Y1_UPSTREAM_TAGS[t], - ) - if repo == ROCKBOX_REPO: - tags = upstream_release_tags(repo) - allowed = [t for t in tags if tag_allowed(repo, t)] - return sorted(allowed, key=lambda t: parse_stable_v(t) or ()) - return [] def release_has_rom_zip(repo: str, tag: str) -> dict | None: @@ -167,24 +104,20 @@ entries: list[dict] = [] for repo in repos: if source_filter and repo != source_filter: continue - slug_base = slug_for_repo(repo) - for upstream_tag in tags_to_probe(repo): + for upstream_tag in sorted( + Y1_UPSTREAM_TAGS.keys(), + key=lambda t: Y1_UPSTREAM_TAGS[t], + ): asset = release_has_rom_zip(repo, upstream_tag) if asset is None: continue - if repo == Y1_REPO: - fw_version = Y1_UPSTREAM_TAGS[upstream_tag] - release_tag = f"{slug_base}@{fw_version}" - slug = f"{slug_base}-{fw_version}" - source_tag = upstream_tag - else: - release_tag = f"{slug_base}@{upstream_tag}" - slug = release_tag.replace("@", "-") - source_tag = upstream_tag + fw_version = Y1_UPSTREAM_TAGS[upstream_tag] + release_tag = f"y1-stock-rom@{fw_version}" + slug = f"y1-stock-rom-{fw_version}" entries.append( { "source_repo": repo, - "source_tag": source_tag, + "source_tag": upstream_tag, "release_tag": release_tag, "download_url": asset["browser_download_url"], "digest": asset["digest"], From 77d55b04171db5d5421b684229933c290aa841b1 Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 20:51:01 +0100 Subject: [PATCH 07/17] Fix APK DEX assembly in CI by correcting Patch H' register locals. BasePlayerActivity dispatchKeyEvent patches used v3 while declaring .locals 2, which makes apktool smali assembly fail. Bump to .locals 4, prefer JDK 17 in find_java, pin JAVA_HOME in the workflow, and surface apktool build logs when DEX output is missing. Co-authored-by: Cursor --- .github/workflows/build-firmware-releases.yml | 1 + src/patches/patch_y1_apk.py | 71 ++++++++++++------- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build-firmware-releases.yml b/.github/workflows/build-firmware-releases.yml index e78a0b1..4a873f1 100644 --- a/.github/workflows/build-firmware-releases.yml +++ b/.github/workflows/build-firmware-releases.yml @@ -110,6 +110,7 @@ jobs: - name: Patch and publish release env: GH_TOKEN: ${{ github.token }} + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 run: | args=( --source-repo "${{ matrix.source_repo }}" diff --git a/src/patches/patch_y1_apk.py b/src/patches/patch_y1_apk.py index 30c914b..91a7047 100755 --- a/src/patches/patch_y1_apk.py +++ b/src/patches/patch_y1_apk.py @@ -87,11 +87,9 @@ # `apt install openjdk-21-jdk` and either `update-alternatives --config java` # or invoke /usr/lib/jvm/java-21-openjdk-*/bin/java directly). -# apktool 2.9.3's smali assembler is memory-frugal and runs fine at the -# default JVM heap on this APK. Newer apktool releases (2.10+) use a parallel -# ThreadPoolExecutor that may need `-Xmx2g` for large APKs; keeping this -# slot here so a future bump can wire it up by changing one constant. -APKTOOL_JVM_FLAGS: list = [] +# apktool 2.9.3's smali assembler is memory-frugal on this APK; CI runners +# still need headroom when assembling two large DEX trees in one `b` pass. +APKTOOL_JVM_FLAGS: list = ["-Xmx4g"] # Stock APK md5s — pulled from /system/app/com.innioasis.y1/ on clean stock # devices. The smali pattern matches in this script assume unpatched bytecode, @@ -149,13 +147,23 @@ def run(cmd, **kw): return result def find_java(): - for candidate in ["java", - "/usr/lib/jvm/java-21-openjdk-amd64/bin/java", - "/usr/lib/jvm/java-17-openjdk-amd64/bin/java", - "/usr/lib/jvm/default-java/bin/java"]: - if shutil.which(candidate): + # Prefer JDK 17/21 — apktool 2.9.3's smali assembler is unreliable on Java 22+. + env_home = os.environ.get("JAVA_HOME", "").strip() + if env_home: + env_java = os.path.join(env_home, "bin", "java") + if os.path.isfile(env_java): + return env_java + for candidate in [ + "/usr/lib/jvm/java-17-openjdk-amd64/bin/java", + "/usr/lib/jvm/java-21-openjdk-amd64/bin/java", + "/usr/lib/jvm/java-17-openjdk/bin/java", + "/usr/lib/jvm/java-21-openjdk/bin/java", + "java", + "/usr/lib/jvm/default-java/bin/java", + ]: + if shutil.which(candidate) or os.path.isfile(candidate): return candidate - sys.exit("ERROR: Java not found. Install Java 11+ and ensure 'java' is on PATH.") + sys.exit("ERROR: Java not found. Install Java 11–21 and ensure 'java' is on PATH.") def md5_file(path: str) -> str: @@ -1234,9 +1242,9 @@ def _base_activity_dispatch_pairs(): pairs = [] for old_mid in (_OLD_DISPATCH_JAVA_LINES, _OLD_DISPATCH_JAVA, _OLD_DISPATCH_KOTLIN): pairs.append((prefix + old_mid, prefix + suffix)) - # Kotlin 2.8.x often declares .locals 2 while still using v2/v3. - prefix_l2 = prefix.replace(".locals 7", ".locals 2", 1) - pairs.append((prefix_l2 + _OLD_DISPATCH_KOTLIN, prefix_l2 + suffix)) + # Kotlin 2.8.x prologue with a low .locals count — AVRCP block needs v3. + prefix_l4 = prefix.replace(".locals 7", ".locals 4", 1) + pairs.append((prefix_l4 + _OLD_DISPATCH_KOTLIN, prefix_l4 + suffix)) return pairs @@ -1246,7 +1254,7 @@ def _base_player_dispatch_pairs(): def _player_new_head(line304: str) -> str: return ( ".method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z\n" - " .locals 2\n\n" + " .locals 4\n\n" " invoke-virtual {p1}, Landroid/view/KeyEvent;->getKeyCode()I\n\n" " move-result v0\n\n" + _patch_h_avrcp_block("v0", "patch_h2") @@ -1282,13 +1290,15 @@ def _player_new_head(line304: str) -> str: " move-result v1" ) new_kc_first = ( - old_kc_first.replace( - " move-result v0\n\n invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I", - " move-result v0\n\n" - + _patch_h_avrcp_block("v0", "patch_h2") - + "\n\n invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I", - 1, - ) + ".method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z\n" + " .locals 4\n\n" + " invoke-static {p1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNull(Ljava/lang/Object;)V\n\n" + " invoke-virtual {p1}, Landroid/view/KeyEvent;->getKeyCode()I\n\n" + " move-result v0\n\n" + + _patch_h_avrcp_block("v0", "patch_h2") + + "\n\n" + " invoke-virtual {p1}, Landroid/view/KeyEvent;->getAction()I\n\n" + " move-result v1" ) pairs.append((old_kc_first, new_kc_first)) return pairs @@ -2808,9 +2818,9 @@ def _apply_b5_dbg_instrumentation(src_rel, smali): # -- Step 4: Reassemble DEX with apktool ------------------------------------- print(f"\n[4/4] Reassembling smali -> DEX (this takes ~30 seconds)...") # apktool builds smali->DEX first, then tries aapt for resources. -# Since we decoded with --no-res, the aapt step fails -- but the DEX -# is already built by that point. We ignore the exit code intentionally. -subprocess.run( +# Since we decoded with --no-res, the aapt step often fails after DEX +# assembly; we still require classes.dex + classes2.dex under build/apk/. +build_result = subprocess.run( [java, *APKTOOL_JVM_FLAGS, "-jar", APKTOOL_JAR, "b", UNPACKED_DIR], capture_output=True, text=True ) @@ -2818,7 +2828,16 @@ def _apply_b5_dbg_instrumentation(src_rel, smali): dex1 = os.path.join(UNPACKED_DIR, "build", "apk", "classes.dex") dex2 = os.path.join(UNPACKED_DIR, "build", "apk", "classes2.dex") if not os.path.exists(dex1) or not os.path.exists(dex2): - sys.exit("ERROR: DEX assembly failed -- classes.dex or classes2.dex not produced.") + tail = (build_result.stdout or "") + (build_result.stderr or "") + if tail.strip(): + print(" apktool build output (last 4000 chars):") + print(tail[-4000:]) + sys.exit( + "ERROR: DEX assembly failed -- classes.dex or classes2.dex not produced.\n" + f" apktool exit code: {build_result.returncode}\n" + " Typical causes: smali register overflow (.locals too small), method-id\n" + " cap, or Java 22+ smali-assembler quirks — use JDK 17/21." + ) print(f" classes.dex {os.path.getsize(dex1):,} bytes") print(f" classes2.dex {os.path.getsize(dex2):,} bytes") From a85b60ce73ab84aee4ae4c41a9feba460e337eeb Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 20:55:39 +0100 Subject: [PATCH 08/17] Fix BaseActivity Patch H smali pairs to preserve :cond_0 prologue. Exact-match replacements were dropping getAction/getKeyCode and the :cond_0 label, breaking apktool DEX assembly. Only splice the AVRCP block after move-result from getKeyCode. Co-authored-by: Cursor --- src/patches/patch_y1_apk.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/patches/patch_y1_apk.py b/src/patches/patch_y1_apk.py index 91a7047..4737e07 100755 --- a/src/patches/patch_y1_apk.py +++ b/src/patches/patch_y1_apk.py @@ -1183,6 +1183,17 @@ def _dispatch_head_with_avrcp_block(key_reg: str, label_prefix: str, suffix: str return _patch_h_avrcp_block(key_reg, label_prefix) + "\n\n " + suffix +def _dispatch_after_get_keycode(key_reg: str, label_prefix: str) -> str: + """getKeyCode → move-result → AVRCP filter → const/4 v3, 0x3 (Patch H tail).""" + return ( + " invoke-virtual {p1}, Landroid/view/KeyEvent;->getKeyCode()I\n\n" + f" move-result {key_reg}\n\n" + + _patch_h_avrcp_block(key_reg, label_prefix) + + "\n\n" + " const/4 v3, 0x3" + ) + + # Stock Java (3.0.2 / 3.0.7) — includes .line debug comments. _OLD_DISPATCH_JAVA_LINES = """\ .line 673 @@ -1238,13 +1249,24 @@ def _base_activity_dispatch_pairs(): return v0 """ - suffix = _dispatch_head_with_avrcp_block("v2", "patch_h", "const/4 v3, 0x3") pairs = [] for old_mid in (_OLD_DISPATCH_JAVA_LINES, _OLD_DISPATCH_JAVA, _OLD_DISPATCH_KOTLIN): - pairs.append((prefix + old_mid, prefix + suffix)) + kc = " invoke-virtual {p1}, Landroid/view/KeyEvent;->getKeyCode()I" + if kc not in old_mid: + continue + head, _, _ = old_mid.partition(kc) + pairs.append(( + prefix + old_mid, + prefix + head + _dispatch_after_get_keycode("v2", "patch_h"), + )) # Kotlin 2.8.x prologue with a low .locals count — AVRCP block needs v3. prefix_l4 = prefix.replace(".locals 7", ".locals 4", 1) - pairs.append((prefix_l4 + _OLD_DISPATCH_KOTLIN, prefix_l4 + suffix)) + kc = " invoke-virtual {p1}, Landroid/view/KeyEvent;->getKeyCode()I" + head, _, _ = _OLD_DISPATCH_KOTLIN.partition(kc) + pairs.append(( + prefix_l4 + _OLD_DISPATCH_KOTLIN, + prefix_l4 + head + _dispatch_after_get_keycode("v2", "patch_h"), + )) return pairs From 0752dffd159c784f7b1428acc528aaa63548523c Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 22:54:05 +0100 Subject: [PATCH 09/17] Harden CI firmware publishes and document release verification. Refuse byte-identical or smaller repacks, publish rom.zip plus build-manifest.json, update 3.0.7 rom.zip MD5 in KNOWN_FIRMWARES, and add release size/hash table in README. --- README.md | 9 ++++++++- apply.bash | 2 +- tools/ci/build-one.sh | 29 ++++++++++++++++++++++++++--- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c8297f4..db56903 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,14 @@ Workflow [`.github/workflows/build-firmware-releases.yml`](.github/workflows/bui For each input it runs `./apply.bash --all --no-flash --accept-any-firmware`, repacks `rom.zip`, and publishes a release on this repo. -**Release tag pattern:** `y1-stock-rom@{firmware-version}` (e.g. `y1-stock-rom@3.0.2`). Each release attaches one patched **`rom.zip`**. +**Release tag pattern:** `y1-stock-rom@{firmware-version}` (e.g. `y1-stock-rom@3.0.2`). Each release attaches **`rom.zip`** (patched) plus **`build-manifest.json`**. + +Download from **this repo’s release tag**, not from [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) — upstream `rom.zip` is stock (~238–259 MB). Patched builds are larger (~295–329 MB) and have a different SHA256 (listed in the release notes / manifest). + +| Release | Expected `rom.zip` size (approx.) | Patched SHA256 (May 2026 CI) | +|---------|-----------------------------------|------------------------------| +| [y1-stock-rom@3.0.2](https://github.com/ryan-specter/koensayr-auto/releases/tag/y1-stock-rom%403.0.2) | 329,015,308 bytes | `2371ac0970c0dbac318077373467859439aa0414caa15e29b90d8e879b8bbd80` | +| [y1-stock-rom@3.0.7](https://github.com/ryan-specter/koensayr-auto/releases/tag/y1-stock-rom%403.0.7) | 309,073,126 bytes | `2fa3fb7bf9ced11a21d0ce3bd0aec8a521dd3c6f22d9e0be8301b9ca3951dddb` | **Triggers:** weekly schedule, pushes that touch patcher code, and manual `workflow_dispatch` (optional `force` / `source_repo` filter). diff --git a/apply.bash b/apply.bash index 0fe6c96..6a6e39d 100755 --- a/apply.bash +++ b/apply.bash @@ -268,7 +268,7 @@ FILENAME_MUSIC_APK="" # system.img md5 is for the RAW image (post-simg2img if input was sparse). KNOWN_FIRMWARES=( "3.0.2|473991dadeb1a8c4d25902dee9ee362b|1f7920228a20c01ad274c61c94a8cf36|82657db82578a38c6f1877e02407127a|com.innioasis.y1_3.0.2.apk" - "3.0.7|663baf9f7f2a08caa82e3fba7a9baa28|83b946d1799b4f0281ba8e808ed7911b|02ae3ae89e20bde0a20e940f73e1ed1b|com.innioasis.y1_3.0.7.apk" + "3.0.7|663baf9f7f2a08caa82e3fba7a9baa28|83b946d1799b4f0281ba8e808ed7911b|aa9847088859176c76d8e203970e7032|com.innioasis.y1_3.0.7.apk" ) # (PATH_SCRIPT_DIR set earlier — used by the --artifacts-dir staging fallback.) diff --git a/tools/ci/build-one.sh b/tools/ci/build-one.sh index 51ffc6f..8173de2 100644 --- a/tools/ci/build-one.sh +++ b/tools/ci/build-one.sh @@ -121,6 +121,18 @@ echo "[build-one] Using patched system image: ${DEVEL_IMG}" OUTPUT_ROM="${WORKDIR}/rom-koensayr.zip" "${CI_DIR}/repack-rom.sh" "$EXTRACT" "$DEVEL_IMG" "$OUTPUT_ROM" +# Fail closed if we accidentally repacked an unmodified upstream image. +upstream_sha="$(sha256sum "${STAGING}/rom.zip" | awk '{print $1}')" +output_sha="$(sha256sum "$OUTPUT_ROM" | awk '{print $1}')" +if [[ "$upstream_sha" == "$output_sha" ]]; then + echo "ERROR: patched rom.zip is byte-identical to upstream — refusing to publish." >&2 + exit 1 +fi +if [[ "$(wc -c < "${STAGING}/rom.zip")" -ge "$(wc -c < "$OUTPUT_ROM")" ]]; then + echo "ERROR: patched rom.zip is not larger than upstream (expected system.img changes)." >&2 + exit 1 +fi + # Copy staging rom for convenience cp -f "$OUTPUT_ROM" "${STAGING}/rom-koensayr.zip" @@ -166,6 +178,11 @@ Patched **rom.zip** built by [koensayr-auto](https://github.com/${GITHUB_REPOSIT - Asset: \`rom.zip\` - Upstream SHA256: \`${DIGEST}\` +## Build output + +- Patched \`rom.zip\` SHA256: \`${output_sha}\` (see \`build-manifest.json\` on this release) +- Upstream \`rom.zip\` was re-hashed at build time; output must differ (CI enforces this). + ## Patches (\`--all\`) - Music-player UX (Artist→Album navigation) @@ -186,14 +203,20 @@ adb shell rm -rf /data/data/com.innioasis.y1/code_cache/secondary-dexes/ EOF +# Flash tools expect the outer archive to be named rom.zip. +RELEASE_ASSET="${WORKDIR}/rom.zip" +cp -f "$OUTPUT_ROM" "$RELEASE_ASSET" + echo "[build-one] Publishing GitHub release ${RELEASE_TAG}.." +echo "[build-one] upstream sha256: ${upstream_sha}" +echo "[build-one] output sha256: ${output_sha}" if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then - gh release upload "$RELEASE_TAG" "$OUTPUT_ROM" --clobber + gh release upload "$RELEASE_TAG" "$RELEASE_ASSET" "$MANIFEST" --clobber gh release edit "$RELEASE_TAG" --notes-file "$NOTES" else - gh release create "$RELEASE_TAG" "$OUTPUT_ROM" \ + gh release create "$RELEASE_TAG" "$RELEASE_ASSET" "$MANIFEST" \ --title "Koensayr ${RELEASE_TAG}" \ --notes-file "$NOTES" fi -echo "[build-one] Uploaded ${RELEASE_TAG}" +echo "[build-one] Uploaded ${RELEASE_TAG} (rom.zip + build-manifest.json)" From 4ca3e14fc53b403169f77fe0f1d35c462eccf2dc Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 23:20:27 +0100 Subject: [PATCH 10/17] Enforce KNOWN_FIRMWARES MD5 whitelist in CI builds. --- README.md | 4 +-- docs/SUPPORTED-FIRMWARE-CI.md | 5 ++-- tools/ci/build-one.sh | 38 +++++++++++++++++++----- tools/ci/firmware-manifest.sh | 55 +++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 tools/ci/firmware-manifest.sh diff --git a/README.md b/README.md index db56903..965b8a4 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Known stock firmwares recognised by `KNOWN_FIRMWARES` in the bash. Add a row (sa | Version | system.img (raw, extracted) | boot.img (in zip; not consumed since v1.7.0) | rom.zip (input) | Music APK basename in `app/` | |---|---|---|---|---| | **3.0.2** | `473991dadeb1a8c4d25902dee9ee362b` | `1f7920228a20c01ad274c61c94a8cf36` | `82657db82578a38c6f1877e02407127a` | `com.innioasis.y1_3.0.2.apk` | -| **3.0.7** | `663baf9f7f2a08caa82e3fba7a9baa28` | `83b946d1799b4f0281ba8e808ed7911b` | `02ae3ae89e20bde0a20e940f73e1ed1b` | `com.innioasis.y1_3.0.7.apk` | +| **3.0.7** | `663baf9f7f2a08caa82e3fba7a9baa28` | `83b946d1799b4f0281ba8e808ed7911b` | `aa9847088859176c76d8e203970e7032` | `com.innioasis.y1_3.0.7.apk` | The MediaTek BT stack (`bin/mtkbt`, `lib/libextavrcp*.so`, `lib/libaudio.a2dp.default.so`, `app/MtkBt.odex`) is byte-identical between 3.0.2 and 3.0.7 — every native patch in `--avrcp` / `--bluetooth` applies unchanged. Only the music APK differs (resource-ID shifts + a few additions in `Y1Repository`), and `patch_y1_apk.py`'s smali anchors handle both builds. @@ -114,7 +114,7 @@ Workflow [`.github/workflows/build-firmware-releases.yml`](.github/workflows/bui - Upstream tags **3.0.2** and **Latest-3.0.7** (published as `y1-stock-rom@3.0.2` / `@3.0.7`) -For each input it runs `./apply.bash --all --no-flash --accept-any-firmware`, repacks `rom.zip`, and publishes a release on this repo. +For each input it downloads upstream `rom.zip`, verifies SHA256 (from the release asset) and MD5 against [`KNOWN_FIRMWARES`](apply.bash), runs `./apply.bash --all --no-flash`, repacks `rom.zip`, and publishes a release on this repo. **Release tag pattern:** `y1-stock-rom@{firmware-version}` (e.g. `y1-stock-rom@3.0.2`). Each release attaches **`rom.zip`** (patched) plus **`build-manifest.json`**. diff --git a/docs/SUPPORTED-FIRMWARE-CI.md b/docs/SUPPORTED-FIRMWARE-CI.md index 0215b98..5e43c8f 100644 --- a/docs/SUPPORTED-FIRMWARE-CI.md +++ b/docs/SUPPORTED-FIRMWARE-CI.md @@ -25,7 +25,7 @@ To add another stock tag, extend `Y1_UPSTREAM_TAGS` in `discover-inputs.sh`. ## Patch set -Every green CI build runs `./apply.bash --all --no-flash --accept-any-firmware`: +Every green CI build verifies upstream `rom.zip` SHA256 and MD5 against [`KNOWN_FIRMWARES`](../apply.bash), then runs `./apply.bash --all --no-flash` (strict manifest match — no `--accept-any-firmware`): - Music-player UX (`--music-apk`) - Bluetooth pairing (`--bluetooth`) @@ -52,6 +52,7 @@ A build is skipped when a release already exists and its notes contain the upstr | Script | Role | |--------|------| | [`tools/ci/discover-inputs.sh`](../tools/ci/discover-inputs.sh) | Emit JSON matrix for allowlisted upstream `rom.zip` assets | -| [`tools/ci/build-one.sh`](../tools/ci/build-one.sh) | Download → patch → repack → `gh release` | +| [`tools/ci/build-one.sh`](../tools/ci/build-one.sh) | Download → MD5/SHA256 gate → patch → repack → `gh release` | +| [`tools/ci/firmware-manifest.sh`](../tools/ci/firmware-manifest.sh) | Read `KNOWN_FIRMWARES` rows from `apply.bash` | | [`tools/ci/extract-rom.sh`](../tools/ci/extract-rom.sh) | Unzip upstream ROM; record sparse `system.img` | | [`tools/ci/repack-rom.sh`](../tools/ci/repack-rom.sh) | Replace `system.img` and zip patched `rom.zip` | diff --git a/tools/ci/build-one.sh b/tools/ci/build-one.sh index 8173de2..0e49e3e 100644 --- a/tools/ci/build-one.sh +++ b/tools/ci/build-one.sh @@ -58,6 +58,8 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$REPO_ROOT" CI_DIR="${REPO_ROOT}/tools/ci" +# shellcheck source=firmware-manifest.sh +source "${CI_DIR}/firmware-manifest.sh" WORKDIR="$(mktemp -d -t koensayr-ci.XXXXXX)" trap 'rm -rf "$WORKDIR"' EXIT @@ -80,20 +82,42 @@ fi echo "[build-one] Downloading upstream rom.zip.." curl -fsSL -o "${STAGING}/rom.zip" "$DOWNLOAD_URL" +if ! FW_VERSION="$(firmware_version_from_slug "$SLUG")"; then + echo "ERROR: slug ${SLUG} is not a known y1-stock-rom firmware id" >&2 + exit 1 +fi + +if [[ -n "$DIGEST" ]]; then + actual_digest="$(sha256sum "${STAGING}/rom.zip" | awk '{print $1}')" + if [[ "$actual_digest" != "$DIGEST" ]]; then + echo "ERROR: downloaded rom.zip sha256 ${actual_digest} != expected ${DIGEST}" >&2 + exit 1 + fi + echo "[build-one] Upstream rom.zip sha256 verified (${DIGEST:0:16}…)" +fi + +rom_md5="$(md5sum "${STAGING}/rom.zip" | awk '{print $1}')" +expected_rom_md5="$(firmware_manifest_field "$FW_VERSION" rom_md5)" +if [[ "$rom_md5" != "$expected_rom_md5" ]]; then + echo "ERROR: rom.zip md5 ${rom_md5} does not match KNOWN_FIRMWARES v${FW_VERSION} (expected ${expected_rom_md5})" >&2 + echo " Refusing to patch — update apply.bash manifest or fix the upstream download URL." >&2 + exit 1 +fi +echo "[build-one] rom.zip md5 matches KNOWN_FIRMWARES v${FW_VERSION}" + echo "[build-one] Extracting upstream layout.." "${CI_DIR}/extract-rom.sh" "${STAGING}/rom.zip" "$EXTRACT" echo "[build-one] Patching (koensayr ${KOENSAYR_VERSION}).." -./apply.bash --all --no-flash --accept-any-firmware \ - --firmware-slug "$SLUG" \ - --artifacts-dir "$STAGING" +./apply.bash --all --no-flash --artifacts-dir "$STAGING" # apply.bash names the loop-mounted image system-${VERSION_FIRMWARE}-devel.img -# (manifest version e.g. 3.0.2, or --firmware-slug when using --accept-any-firmware). +# (manifest version e.g. 3.0.2 / 3.0.7 when rom.zip matches KNOWN_FIRMWARES). resolve_devel_img() { - local staging="$1" slug="$2" source_tag="$3" + local staging="$1" fw_version="$2" slug="$3" source_tag="$4" local p for p in \ + "${staging}/system-${fw_version}-devel.img" \ "${staging}/system-${slug}-devel.img" \ "${staging}/system-${source_tag}-devel.img"; do if [[ -f "$p" ]]; then @@ -110,10 +134,10 @@ resolve_devel_img() { return 1 } -DEVEL_IMG="$(resolve_devel_img "$STAGING" "$SLUG" "$SOURCE_TAG" || true)" +DEVEL_IMG="$(resolve_devel_img "$STAGING" "$FW_VERSION" "$SLUG" "$SOURCE_TAG" || true)" if [[ -z "$DEVEL_IMG" || ! -f "$DEVEL_IMG" ]]; then echo "ERROR: expected patched system image under ${STAGING}/" >&2 - echo " (tried system-${SLUG}-devel.img and system-${SOURCE_TAG}-devel.img)" >&2 + echo " (tried system-${FW_VERSION}-devel.img, system-${SLUG}-devel.img, system-${SOURCE_TAG}-devel.img)" >&2 exit 1 fi echo "[build-one] Using patched system image: ${DEVEL_IMG}" diff --git a/tools/ci/firmware-manifest.sh b/tools/ci/firmware-manifest.sh new file mode 100644 index 0000000..657e8ce --- /dev/null +++ b/tools/ci/firmware-manifest.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# firmware-manifest.sh — read KNOWN_FIRMWARES rows from apply.bash (single source of truth). +# +# Usage (sourced): +# firmware_manifest_row 3.0.2 +# firmware_manifest_field 3.0.2 rom_md5 + +set -euo pipefail + +FIRMWARE_MANIFEST_APPLY_BASH="${FIRMWARE_MANIFEST_APPLY_BASH:-}" + +firmware_manifest_apply_bash() { + if [[ -n "$FIRMWARE_MANIFEST_APPLY_BASH" && -f "$FIRMWARE_MANIFEST_APPLY_BASH" ]]; then + echo "$FIRMWARE_MANIFEST_APPLY_BASH" + return 0 + fi + local root="${REPO_ROOT:-}" + if [[ -z "$root" ]]; then + root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + fi + echo "${root}/apply.bash" +} + +firmware_manifest_row() { + local version="$1" + local apply + apply="$(firmware_manifest_apply_bash)" + grep -E "^[[:space:]]+\"${version}\|" "$apply" | head -1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/^"//;s/"$//' +} + +firmware_manifest_field() { + local version="$1" field="$2" + local row idx + row="$(firmware_manifest_row "$version")" + if [[ -z "$row" ]]; then + return 1 + fi + case "$field" in + system_md5) idx=2 ;; + boot_md5) idx=3 ;; + rom_md5) idx=4 ;; + music_apk) idx=5 ;; + *) echo "ERROR: unknown firmware_manifest_field ${field}" >&2; return 2 ;; + esac + echo "$row" | cut -d'|' -f"$idx" +} + +firmware_version_from_slug() { + local slug="$1" + if [[ "$slug" =~ ^y1-stock-rom-([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + return 1 +} From b09f04e8e46be6f08ca91e000d1bb88017efbb8e Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 23:33:46 +0100 Subject: [PATCH 11/17] Rebuild firmware releases on every push to main. --- .github/workflows/build-firmware-releases.yml | 5 +- docs/SUPPORTED-FIRMWARE-CI.md | 5 +- tools/ci/build-one.sh | 50 ++++++++++++++++--- tools/ci/patch-revision.sh | 23 +++++++++ 4 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 tools/ci/patch-revision.sh diff --git a/.github/workflows/build-firmware-releases.yml b/.github/workflows/build-firmware-releases.yml index 4a873f1..efd46f9 100644 --- a/.github/workflows/build-firmware-releases.yml +++ b/.github/workflows/build-firmware-releases.yml @@ -45,7 +45,10 @@ jobs: if [[ -n "${{ github.event.inputs.source_repo }}" ]]; then args+=(--source-repo "${{ github.event.inputs.source_repo }}") fi - if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.force }}" == "true" ]]; then + # Every push to main republishes firmware so patched com.innioasis.y1 APKs stay current. + if [[ "${{ github.event_name }}" == "push" ]]; then + args+=(--force) + elif [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.force }}" == "true" ]]; then args+=(--force) fi ./tools/ci/discover-inputs.sh "${args[@]}" > matrix.json diff --git a/docs/SUPPORTED-FIRMWARE-CI.md b/docs/SUPPORTED-FIRMWARE-CI.md index 5e43c8f..6bcdd02 100644 --- a/docs/SUPPORTED-FIRMWARE-CI.md +++ b/docs/SUPPORTED-FIRMWARE-CI.md @@ -45,7 +45,9 @@ Failed matrix jobs do not block other releases (`fail-fast: false`). ## Idempotency -A build is skipped when a release already exists and its notes contain the upstream asset SHA256, unless `workflow_dispatch` sets **force**. +- **Push to `main`:** always rebuilds and republishes (`--force`), so patched `com.innioasis.y1` APKs and other `--all` changes land in the latest `rom.zip` releases. +- **Weekly schedule:** skips a matrix entry only when `build-manifest.json` on the existing release matches both the upstream `rom.zip` SHA256 and the current koensayr git revision (`koensayr_git_sha`). +- **`workflow_dispatch`:** set **force** to rebuild regardless. ## Scripts @@ -54,5 +56,6 @@ A build is skipped when a release already exists and its notes contain the upstr | [`tools/ci/discover-inputs.sh`](../tools/ci/discover-inputs.sh) | Emit JSON matrix for allowlisted upstream `rom.zip` assets | | [`tools/ci/build-one.sh`](../tools/ci/build-one.sh) | Download → MD5/SHA256 gate → patch → repack → `gh release` | | [`tools/ci/firmware-manifest.sh`](../tools/ci/firmware-manifest.sh) | Read `KNOWN_FIRMWARES` rows from `apply.bash` | +| [`tools/ci/patch-revision.sh`](../tools/ci/patch-revision.sh) | Current git SHA recorded in `build-manifest.json` | | [`tools/ci/extract-rom.sh`](../tools/ci/extract-rom.sh) | Unzip upstream ROM; record sparse `system.img` | | [`tools/ci/repack-rom.sh`](../tools/ci/repack-rom.sh) | Replace `system.img` and zip patched `rom.zip` | diff --git a/tools/ci/build-one.sh b/tools/ci/build-one.sh index 0e49e3e..77799a1 100644 --- a/tools/ci/build-one.sh +++ b/tools/ci/build-one.sh @@ -68,14 +68,46 @@ EXTRACT="${WORKDIR}/rom-extract" mkdir -p "$STAGING" "$EXTRACT" KOENSAYR_VERSION="$(grep -E '^# Version:' apply.bash | awk '{print $3}')" +PATCH_REVISION="$("${CI_DIR}/patch-revision.sh")" + +# Idempotency: skip only when an existing release was built from the same upstream +# rom.zip *and* the same koensayr git revision (patches / apply.bash / Y1Bridge / su). +# Push workflows pass --force to always republish after commits to main. +release_already_current() { + local tag="$1" manifest_dir published_rev published_digest + if ! gh release view "$tag" >/dev/null 2>&1; then + return 1 + fi + manifest_dir="$(mktemp -d)" + if ! gh release download "$tag" -p "build-manifest.json" -D "$manifest_dir" >/dev/null 2>&1; then + rm -rf "$manifest_dir" + return 1 + fi + if [[ ! -f "${manifest_dir}/build-manifest.json" ]]; then + rm -rf "$manifest_dir" + return 1 + fi + read -r published_rev published_digest < <( + python3 - "${manifest_dir}/build-manifest.json" <<'PY' +import json, sys +data = json.load(open(sys.argv[1])) +print(data.get("koensayr_git_sha", ""), data.get("upstream_digest_sha256", "")) +PY + ) + rm -rf "$manifest_dir" + if [[ -z "$published_rev" || "$published_rev" != "$PATCH_REVISION" ]]; then + return 1 + fi + if [[ -n "$DIGEST" && "$published_digest" != "$DIGEST" ]]; then + return 1 + fi + return 0 +} -# Idempotency: skip when release exists with matching upstream digest. if [[ "$FORCE" != true && "${KOENSAYR_SKIP_PUBLISH:-}" != "1" ]]; then - if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then - if [[ -n "$DIGEST" ]] && gh release view "$RELEASE_TAG" --json body -q .body | grep -qF "$DIGEST"; then - echo "[build-one] Release ${RELEASE_TAG} already published for digest ${DIGEST}; skipping." - exit 0 - fi + if release_already_current "$RELEASE_TAG"; then + echo "[build-one] Release ${RELEASE_TAG} already matches upstream digest and patch revision ${PATCH_REVISION:0:12}; skipping." + exit 0 fi fi @@ -171,6 +203,7 @@ with rom.open("rb") as f: h.update(chunk) data = { "koensayr_version": "${KOENSAYR_VERSION}", + "koensayr_git_sha": "${PATCH_REVISION}", "source_repo": "${SOURCE_REPO}", "source_tag": "${SOURCE_TAG}", "release_tag": "${RELEASE_TAG}", @@ -202,6 +235,11 @@ Patched **rom.zip** built by [koensayr-auto](https://github.com/${GITHUB_REPOSIT - Asset: \`rom.zip\` - Upstream SHA256: \`${DIGEST}\` +## Koensayr build + +- Version: \`${KOENSAYR_VERSION}\` +- Git revision: \`${PATCH_REVISION}\` (includes \`com.innioasis.y1\` APK patches under \`src/patches/\`) + ## Build output - Patched \`rom.zip\` SHA256: \`${output_sha}\` (see \`build-manifest.json\` on this release) diff --git a/tools/ci/patch-revision.sh b/tools/ci/patch-revision.sh new file mode 100644 index 0000000..c68ce9a --- /dev/null +++ b/tools/ci/patch-revision.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# patch-revision.sh — identifier for koensayr patch sources baked into CI rom.zip output. +# +# Used to skip scheduled rebuilds only when upstream firmware and this revision +# already produced the current GitHub release. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + git rev-parse HEAD + exit 0 +fi + +# Fallback when .git is unavailable (local tarball builds). +{ + grep -E '^# Version:' apply.bash || true + find apply.bash src/patches src/Y1Bridge src/su -type f 2>/dev/null | LC_ALL=C sort | while read -r f; do + sha256sum "$f" + done +} | sha256sum | awk '{print $1}' From dc5780995a287a2a55f8de3a6125cd15b88d2a6c Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 23:33:56 +0100 Subject: [PATCH 12/17] Document CI push-triggered firmware republish. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 965b8a4..b58bf2d 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Download from **this repo’s release tag**, not from [y1-community/y1-stock-rom | [y1-stock-rom@3.0.2](https://github.com/ryan-specter/koensayr-auto/releases/tag/y1-stock-rom%403.0.2) | 329,015,308 bytes | `2371ac0970c0dbac318077373467859439aa0414caa15e29b90d8e879b8bbd80` | | [y1-stock-rom@3.0.7](https://github.com/ryan-specter/koensayr-auto/releases/tag/y1-stock-rom%403.0.7) | 309,073,126 bytes | `2fa3fb7bf9ced11a21d0ce3bd0aec8a521dd3c6f22d9e0be8301b9ca3951dddb` | -**Triggers:** weekly schedule, pushes that touch patcher code, and manual `workflow_dispatch` (optional `force` / `source_repo` filter). +**Triggers:** weekly schedule, pushes to `main` that touch patcher/CI paths (always republish both firmware tags), and manual `workflow_dispatch` (optional `force` / `source_repo` filter). **Confidence:** Stock **3.0.2** / **3.0.7** OTAs are hardware-verified. See [`docs/SUPPORTED-FIRMWARE-CI.md`](docs/SUPPORTED-FIRMWARE-CI.md). From 8d5b18ca27258219c3019ecedfa48e14cb215152 Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Fri, 22 May 2026 23:41:44 +0100 Subject: [PATCH 13/17] Accept y1-community pre-expanded system.img MD5 in firmware manifest. --- README.md | 4 ++-- apply.bash | 31 +++++++++++++++++++++++-------- docs/SUPPORTED-FIRMWARE-CI.md | 2 +- tools/ci/build-one.sh | 4 ++-- tools/ci/firmware-manifest.sh | 13 +++++++++++++ 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b58bf2d..5d958b6 100644 --- a/README.md +++ b/README.md @@ -92,11 +92,11 @@ Known stock firmwares recognised by `KNOWN_FIRMWARES` in the bash. Add a row (sa | Version | system.img (raw, extracted) | boot.img (in zip; not consumed since v1.7.0) | rom.zip (input) | Music APK basename in `app/` | |---|---|---|---|---| | **3.0.2** | `473991dadeb1a8c4d25902dee9ee362b` | `1f7920228a20c01ad274c61c94a8cf36` | `82657db82578a38c6f1877e02407127a` | `com.innioasis.y1_3.0.2.apk` | -| **3.0.7** | `663baf9f7f2a08caa82e3fba7a9baa28` | `83b946d1799b4f0281ba8e808ed7911b` | `aa9847088859176c76d8e203970e7032` | `com.innioasis.y1_3.0.7.apk` | +| **3.0.7** | `663baf9f…` (Innioasis sparse→raw) or `017d62eb…` ([y1-community](https://github.com/y1-community/y1-stock-rom) pre-expanded) | `83b946d1799b4f0281ba8e808ed7911b` | `aa9847088859176c76d8e203970e7032` | `com.innioasis.y1_3.0.7.apk` | The MediaTek BT stack (`bin/mtkbt`, `lib/libextavrcp*.so`, `lib/libaudio.a2dp.default.so`, `app/MtkBt.odex`) is byte-identical between 3.0.2 and 3.0.7 — every native patch in `--avrcp` / `--bluetooth` applies unchanged. Only the music APK differs (resource-ID shifts + a few additions in `Y1Repository`), and `patch_y1_apk.py`'s smali anchors handle both builds. -Stock sizes: 3.0.2 `rom.zip` 259,502,414 bytes (raw `system.img` inside); 3.0.7 `rom.zip` 189,791,144 bytes (sparse `system.img` inside, auto-de-sparsed via `simg2img`). Both `system.img`s expand to 681,574,400 bytes raw ext4. `boot.img` 4,706,304 bytes on both. +Stock sizes: 3.0.2 `rom.zip` 259,502,414 bytes (raw `system.img` inside); 3.0.7 `rom.zip` from Innioasis may ship a sparse `system.img` (auto-de-sparsed via `simg2img`), while [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) releases use the same `rom.zip` MD5 but a pre-expanded raw `system.img`. Both raw `system.img`s are 681,574,400-byte ext4. `boot.img` 4,706,304 bytes on both. ## Requirements diff --git a/apply.bash b/apply.bash index 6a6e39d..47764ff 100755 --- a/apply.bash +++ b/apply.bash @@ -264,11 +264,12 @@ VERSION_FIRMWARE="" FILENAME_SYSTEM_IMAGE_TARGET="" FILENAME_MUSIC_APK="" -# Schema: ||||. -# system.img md5 is for the RAW image (post-simg2img if input was sparse). +# Schema: ||||. +# system.img md5 is the RAW ext4 image used for patching (post-simg2img if the zip held a sparse image). +# Multiple system.img md5s are comma-separated (Innioasis sparse→raw vs y1-community pre-expanded zips). KNOWN_FIRMWARES=( "3.0.2|473991dadeb1a8c4d25902dee9ee362b|1f7920228a20c01ad274c61c94a8cf36|82657db82578a38c6f1877e02407127a|com.innioasis.y1_3.0.2.apk" - "3.0.7|663baf9f7f2a08caa82e3fba7a9baa28|83b946d1799b4f0281ba8e808ed7911b|aa9847088859176c76d8e203970e7032|com.innioasis.y1_3.0.7.apk" + "3.0.7|663baf9f7f2a08caa82e3fba7a9baa28,017d62eb54fe18a4dc0b70d380763c22|83b946d1799b4f0281ba8e808ed7911b|aa9847088859176c76d8e203970e7032|com.innioasis.y1_3.0.7.apk" ) # (PATH_SCRIPT_DIR set earlier — used by the --artifacts-dir staging fallback.) @@ -327,6 +328,20 @@ md5_of() { fi } +# md5_matches_field +# Manifest fields may list comma-separated alternates (e.g. Innioasis vs y1-community system.img). +md5_matches_field() { + local field_value="$1" actual="$2" alt + IFS=',' read -ra alts <<< "$field_value" + for alt in "${alts[@]}"; do + alt="${alt//[[:space:]]/}" + if [[ "$actual" == "$alt" ]]; then + return 0 + fi + done + return 1 +} + # resolve_version — echos matching firmware # version on stdout, returns 1 on no match. resolve_version() { @@ -340,7 +355,7 @@ resolve_version() { local row parts for row in "${KNOWN_FIRMWARES[@]}"; do IFS='|' read -ra parts <<< "$row" - if [[ "${parts[$idx]}" == "$md5" ]]; then + if md5_matches_field "${parts[$idx]}" "$md5"; then echo "${parts[0]}" return 0 fi @@ -585,15 +600,15 @@ EOF if [[ "$MANIFEST_MATCH" == true && "$FLAG_ACCEPT_ANY_FIRMWARE" == false ]]; then sys_md5=$(md5_of "$PATH_SYSTEM_IMG") expected=$(firmware_field "$VERSION_FIRMWARE" system_md5) - if [[ "$sys_md5" != "$expected" ]]; then - echo "ERROR: extracted system.img md5 ${sys_md5} differs from manifest v${VERSION_FIRMWARE} (expected ${expected})" >&2 + if ! md5_matches_field "$expected" "$sys_md5"; then + echo "ERROR: extracted system.img md5 ${sys_md5} differs from manifest v${VERSION_FIRMWARE} (expected one of: ${expected})" >&2 exit 1 fi elif [[ "$MANIFEST_MATCH" == true && "$FLAG_ACCEPT_ANY_FIRMWARE" == true ]]; then sys_md5=$(md5_of "$PATH_SYSTEM_IMG") expected=$(firmware_field "$VERSION_FIRMWARE" system_md5) - if [[ "$sys_md5" != "$expected" ]]; then - echo " WARNING: extracted system.img md5 ${sys_md5} differs from manifest v${VERSION_FIRMWARE} (expected ${expected}); continuing (--accept-any-firmware)" + if ! md5_matches_field "$expected" "$sys_md5"; then + echo " WARNING: extracted system.img md5 ${sys_md5} differs from manifest v${VERSION_FIRMWARE} (expected one of: ${expected}); continuing (--accept-any-firmware)" fi fi fi diff --git a/docs/SUPPORTED-FIRMWARE-CI.md b/docs/SUPPORTED-FIRMWARE-CI.md index 6bcdd02..560e581 100644 --- a/docs/SUPPORTED-FIRMWARE-CI.md +++ b/docs/SUPPORTED-FIRMWARE-CI.md @@ -39,7 +39,7 @@ Diagnostic tooling under `tools/` is **not** embedded in the ROM. | Input | CI expectation | |-------|----------------| -| y1-stock-rom **3.0.2** / **3.0.7** | Supported; matches [`KNOWN_FIRMWARES`](../apply.bash) | +| y1-stock-rom **3.0.2** / **3.0.7** | Supported; matches [`KNOWN_FIRMWARES`](../apply.bash) (3.0.7 accepts both Innioasis and y1-community `system.img` layouts) | Failed matrix jobs do not block other releases (`fail-fast: false`). diff --git a/tools/ci/build-one.sh b/tools/ci/build-one.sh index 77799a1..cf16208 100644 --- a/tools/ci/build-one.sh +++ b/tools/ci/build-one.sh @@ -130,8 +130,8 @@ fi rom_md5="$(md5sum "${STAGING}/rom.zip" | awk '{print $1}')" expected_rom_md5="$(firmware_manifest_field "$FW_VERSION" rom_md5)" -if [[ "$rom_md5" != "$expected_rom_md5" ]]; then - echo "ERROR: rom.zip md5 ${rom_md5} does not match KNOWN_FIRMWARES v${FW_VERSION} (expected ${expected_rom_md5})" >&2 +if ! firmware_md5_matches_field "$expected_rom_md5" "$rom_md5"; then + echo "ERROR: rom.zip md5 ${rom_md5} does not match KNOWN_FIRMWARES v${FW_VERSION} (expected one of: ${expected_rom_md5})" >&2 echo " Refusing to patch — update apply.bash manifest or fix the upstream download URL." >&2 exit 1 fi diff --git a/tools/ci/firmware-manifest.sh b/tools/ci/firmware-manifest.sh index 657e8ce..3406efc 100644 --- a/tools/ci/firmware-manifest.sh +++ b/tools/ci/firmware-manifest.sh @@ -45,6 +45,19 @@ firmware_manifest_field() { echo "$row" | cut -d'|' -f"$idx" } +# firmware_md5_matches_field +firmware_md5_matches_field() { + local field_value="$1" actual="$2" alt + IFS=',' read -ra alts <<< "$field_value" + for alt in "${alts[@]}"; do + alt="${alt//[[:space:]]/}" + if [[ "$actual" == "$alt" ]]; then + return 0 + fi + done + return 1 +} + firmware_version_from_slug() { local slug="$1" if [[ "$slug" =~ ^y1-stock-rom-([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then From a233c247857915a7ff4ef938f8a85ddab63d17a7 Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Sat, 23 May 2026 00:11:25 +0100 Subject: [PATCH 14/17] Replace GitHub firmware releases on each CI publish. --- README.md | 2 +- docs/SUPPORTED-FIRMWARE-CI.md | 2 +- tools/ci/build-one.sh | 13 ++++++------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5d958b6..8c4f3bd 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ Download from **this repo’s release tag**, not from [y1-community/y1-stock-rom | [y1-stock-rom@3.0.2](https://github.com/ryan-specter/koensayr-auto/releases/tag/y1-stock-rom%403.0.2) | 329,015,308 bytes | `2371ac0970c0dbac318077373467859439aa0414caa15e29b90d8e879b8bbd80` | | [y1-stock-rom@3.0.7](https://github.com/ryan-specter/koensayr-auto/releases/tag/y1-stock-rom%403.0.7) | 309,073,126 bytes | `2fa3fb7bf9ced11a21d0ce3bd0aec8a521dd3c6f22d9e0be8301b9ca3951dddb` | -**Triggers:** weekly schedule, pushes to `main` that touch patcher/CI paths (always republish both firmware tags), and manual `workflow_dispatch` (optional `force` / `source_repo` filter). +**Triggers:** weekly schedule, pushes to `main` that touch patcher/CI paths (always republish both firmware tags; existing releases are deleted and recreated), and manual `workflow_dispatch` (optional `force` / `source_repo` filter). **Confidence:** Stock **3.0.2** / **3.0.7** OTAs are hardware-verified. See [`docs/SUPPORTED-FIRMWARE-CI.md`](docs/SUPPORTED-FIRMWARE-CI.md). diff --git a/docs/SUPPORTED-FIRMWARE-CI.md b/docs/SUPPORTED-FIRMWARE-CI.md index 560e581..90fa81f 100644 --- a/docs/SUPPORTED-FIRMWARE-CI.md +++ b/docs/SUPPORTED-FIRMWARE-CI.md @@ -45,7 +45,7 @@ Failed matrix jobs do not block other releases (`fail-fast: false`). ## Idempotency -- **Push to `main`:** always rebuilds and republishes (`--force`), so patched `com.innioasis.y1` APKs and other `--all` changes land in the latest `rom.zip` releases. +- **Push to `main`:** always rebuilds and republishes (`--force`), so patched `com.innioasis.y1` APKs and other `--all` changes land in the latest `rom.zip` releases. Each publish **deletes** the prior GitHub release tag and recreates it (no leftover assets such as older `rom-koensayr.zip` names). - **Weekly schedule:** skips a matrix entry only when `build-manifest.json` on the existing release matches both the upstream `rom.zip` SHA256 and the current koensayr git revision (`koensayr_git_sha`). - **`workflow_dispatch`:** set **force** to rebuild regardless. diff --git a/tools/ci/build-one.sh b/tools/ci/build-one.sh index cf16208..0881ab4 100644 --- a/tools/ci/build-one.sh +++ b/tools/ci/build-one.sh @@ -273,12 +273,11 @@ echo "[build-one] Publishing GitHub release ${RELEASE_TAG}.." echo "[build-one] upstream sha256: ${upstream_sha}" echo "[build-one] output sha256: ${output_sha}" if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then - gh release upload "$RELEASE_TAG" "$RELEASE_ASSET" "$MANIFEST" --clobber - gh release edit "$RELEASE_TAG" --notes-file "$NOTES" -else - gh release create "$RELEASE_TAG" "$RELEASE_ASSET" "$MANIFEST" \ - --title "Koensayr ${RELEASE_TAG}" \ - --notes-file "$NOTES" + echo "[build-one] Removing prior release ${RELEASE_TAG} (replace with fresh build).." + gh release delete "$RELEASE_TAG" --yes fi +gh release create "$RELEASE_TAG" "$RELEASE_ASSET" "$MANIFEST" \ + --title "Koensayr ${RELEASE_TAG}" \ + --notes-file "$NOTES" -echo "[build-one] Uploaded ${RELEASE_TAG} (rom.zip + build-manifest.json)" +echo "[build-one] Published ${RELEASE_TAG} (rom.zip + build-manifest.json)" From 2e526ef65f13df1c79a85c4b3d7b04d27b47773e Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Sat, 23 May 2026 00:18:18 +0100 Subject: [PATCH 15/17] Add workflow.md intro and SeanathanVT notes to firmware releases. --- .github/workflows/workflow.md | 6 ++++++ docs/SUPPORTED-FIRMWARE-CI.md | 1 + tools/ci/build-one.sh | 37 ++++++++++++++++++++++++++++++++--- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/workflow.md diff --git a/.github/workflows/workflow.md b/.github/workflows/workflow.md new file mode 100644 index 0000000..d05a350 --- /dev/null +++ b/.github/workflows/workflow.md @@ -0,0 +1,6 @@ +Koensayr is a mod for the Y1's OS that makes the following improvements to the original OS: + +- Browsing the albums of an artist from Main Menu > Artists > Albums +- Adds Bluetooth remote control support and track info displayed on your bluetooth receiver, perfect for using your Y1 in a car with the infotainment system + +Devs: edit this file to describe new features; CI prepends it to every firmware release on GitHub. diff --git a/docs/SUPPORTED-FIRMWARE-CI.md b/docs/SUPPORTED-FIRMWARE-CI.md index 90fa81f..9ba5ffe 100644 --- a/docs/SUPPORTED-FIRMWARE-CI.md +++ b/docs/SUPPORTED-FIRMWARE-CI.md @@ -57,5 +57,6 @@ Failed matrix jobs do not block other releases (`fail-fast: false`). | [`tools/ci/build-one.sh`](../tools/ci/build-one.sh) | Download → MD5/SHA256 gate → patch → repack → `gh release` | | [`tools/ci/firmware-manifest.sh`](../tools/ci/firmware-manifest.sh) | Read `KNOWN_FIRMWARES` rows from `apply.bash` | | [`tools/ci/patch-revision.sh`](../tools/ci/patch-revision.sh) | Current git SHA recorded in `build-manifest.json` | +| [`.github/workflows/workflow.md`](../.github/workflows/workflow.md) | User-facing intro prepended to each GitHub release (edit for new features) | | [`tools/ci/extract-rom.sh`](../tools/ci/extract-rom.sh) | Unzip upstream ROM; record sparse `system.img` | | [`tools/ci/repack-rom.sh`](../tools/ci/repack-rom.sh) | Replace `system.img` and zip patched `rom.zip` | diff --git a/tools/ci/build-one.sh b/tools/ci/build-one.sh index 0881ab4..a099ed5 100644 --- a/tools/ci/build-one.sh +++ b/tools/ci/build-one.sh @@ -69,6 +69,19 @@ mkdir -p "$STAGING" "$EXTRACT" KOENSAYR_VERSION="$(grep -E '^# Version:' apply.bash | awk '{print $3}')" PATCH_REVISION="$("${CI_DIR}/patch-revision.sh")" +RELEASE_INTRO_FILE="${REPO_ROOT}/.github/workflows/workflow.md" + +# True when the commit being built was authored by SeanathanVT (GitHub / git display name). +commit_author_is_seanathanvt() { + local author email + author="$(git log -1 --format='%an')" + email="$(git log -1 --format='%ae')" + shopt -s nocasematch + [[ "$author" == *seanathanvt* || "$email" == *seanathanvt* ]] + local rc=$? + shopt -u nocasematch + return $rc +} # Idempotency: skip only when an existing release was built from the same upstream # rom.zip *and* the same koensayr git revision (patches / apply.bash / Y1Bridge / su). @@ -223,12 +236,30 @@ if [[ "${KOENSAYR_SKIP_PUBLISH:-}" == "1" ]]; then fi NOTES="${WORKDIR}/release-notes.md" -cat > "$NOTES" < "$NOTES" +cat >> "$NOTES" < Date: Sat, 23 May 2026 00:22:22 +0100 Subject: [PATCH 16/17] Use firmware-koensayr-version release tags and workflow.md release intro. --- .github/workflows/build-firmware-releases.yml | 1 + .github/workflows/workflow.md | 5 ++- README.md | 10 ++--- docs/SUPPORTED-FIRMWARE-CI.md | 4 +- tools/ci/build-one.sh | 44 ++++++++++++------- tools/ci/discover-inputs.sh | 14 +++--- 6 files changed, 49 insertions(+), 29 deletions(-) diff --git a/.github/workflows/build-firmware-releases.yml b/.github/workflows/build-firmware-releases.yml index efd46f9..2df62df 100644 --- a/.github/workflows/build-firmware-releases.yml +++ b/.github/workflows/build-firmware-releases.yml @@ -21,6 +21,7 @@ on: - "src/Y1Bridge/**" - "src/su/**" - ".github/workflows/build-firmware-releases.yml" + - ".github/workflows/workflow.md" - "tools/ci/**" permissions: diff --git a/.github/workflows/workflow.md b/.github/workflows/workflow.md index d05a350..75b7818 100644 --- a/.github/workflows/workflow.md +++ b/.github/workflows/workflow.md @@ -1,6 +1,7 @@ Koensayr is a mod for the Y1's OS that makes the following improvements to the original OS: -- Browsing the albums of an artist from Main Menu > Artists > Albums -- Adds Bluetooth remote control support and track info displayed on your bluetooth receiver, perfect for using your Y1 in a car with the infotainment system +Browsing the albums of an artist from Main Menu > Artists > Albums + +Adds Bluetooth remote control support and track info displayed on your bluetooth receiver, perfect for using your Y1 in a car with the infotainment system. Devs: edit this file to describe new features; CI prepends it to every firmware release on GitHub. diff --git a/README.md b/README.md index 8c4f3bd..c51468c 100644 --- a/README.md +++ b/README.md @@ -112,18 +112,18 @@ Stock sizes: 3.0.2 `rom.zip` 259,502,414 bytes (raw `system.img` inside); 3.0.7 Workflow [`.github/workflows/build-firmware-releases.yml`](.github/workflows/build-firmware-releases.yml) builds an allowlisted set of [y1-stock-rom](https://github.com/y1-community/y1-stock-rom) **`rom.zip`** releases only: -- Upstream tags **3.0.2** and **Latest-3.0.7** (published as `y1-stock-rom@3.0.2` / `@3.0.7`) +- Upstream tags **3.0.2** and **Latest-3.0.7** (published as `3.0.2-koensayr-*` / `3.0.7-koensayr-*`, where `*` is the koensayr version from `apply.bash`) For each input it downloads upstream `rom.zip`, verifies SHA256 (from the release asset) and MD5 against [`KNOWN_FIRMWARES`](apply.bash), runs `./apply.bash --all --no-flash`, repacks `rom.zip`, and publishes a release on this repo. -**Release tag pattern:** `y1-stock-rom@{firmware-version}` (e.g. `y1-stock-rom@3.0.2`). Each release attaches **`rom.zip`** (patched) plus **`build-manifest.json`**. +**Release tag pattern:** `{firmware-version}-koensayr-{koensayr-version}` (e.g. `3.0.7-koensayr-2.4.0`). Release notes start with [`.github/workflows/workflow.md`](.github/workflows/workflow.md). Each release attaches **`rom.zip`** (patched) plus **`build-manifest.json`**. Download from **this repo’s release tag**, not from [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) — upstream `rom.zip` is stock (~238–259 MB). Patched builds are larger (~295–329 MB) and have a different SHA256 (listed in the release notes / manifest). | Release | Expected `rom.zip` size (approx.) | Patched SHA256 (May 2026 CI) | |---------|-----------------------------------|------------------------------| -| [y1-stock-rom@3.0.2](https://github.com/ryan-specter/koensayr-auto/releases/tag/y1-stock-rom%403.0.2) | 329,015,308 bytes | `2371ac0970c0dbac318077373467859439aa0414caa15e29b90d8e879b8bbd80` | -| [y1-stock-rom@3.0.7](https://github.com/ryan-specter/koensayr-auto/releases/tag/y1-stock-rom%403.0.7) | 309,073,126 bytes | `2fa3fb7bf9ced11a21d0ce3bd0aec8a521dd3c6f22d9e0be8301b9ca3951dddb` | +| 3.0.2-koensayr-* | 329,015,308 bytes (May 2026 CI) | `2371ac0970c0dbac318077373467859439aa0414caa15e29b90d8e879b8bbd80` | +| 3.0.7-koensayr-* | 309,073,126 bytes (May 2026 CI) | `2fa3fb7bf9ced11a21d0ce3bd0aec8a521dd3c6f22d9e0be8301b9ca3951dddb` | **Triggers:** weekly schedule, pushes to `main` that touch patcher/CI paths (always republish both firmware tags; existing releases are deleted and recreated), and manual `workflow_dispatch` (optional `force` / `source_repo` filter). @@ -134,7 +134,7 @@ Local dry-run (no GitHub publish): ```bash KOENSAYR_SKIP_PUBLISH=1 ./tools/ci/build-one.sh \ --source-repo y1-community/y1-stock-rom --source-tag 3.0.2 \ - --release-tag y1-stock-rom@3.0.2 \ + --release-tag 3.0.2-koensayr-2.4.0 \ --download-url "$(gh release view 3.0.2 --repo y1-community/y1-stock-rom --json assets -q '.assets[] | select(.name==\"rom.zip\") | .url')" \ --digest "$(gh release view 3.0.2 --repo y1-community/y1-stock-rom --json assets -q '.assets[] | select(.name==\"rom.zip\") | .digest' | sed 's/sha256://')" \ --slug y1-stock-rom-3.0.2 diff --git a/docs/SUPPORTED-FIRMWARE-CI.md b/docs/SUPPORTED-FIRMWARE-CI.md index 9ba5ffe..20531c9 100644 --- a/docs/SUPPORTED-FIRMWARE-CI.md +++ b/docs/SUPPORTED-FIRMWARE-CI.md @@ -10,7 +10,7 @@ CI only builds these upstream tags (see [`tools/ci/discover-inputs.sh`](../tools |------------|---------------|-------| | [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) | **3.0.2**, **Latest-3.0.7** | `rom.zip` | -**Koensayr release names:** stock **3.0.7** firmware is published as `y1-stock-rom@3.0.7` even though the upstream tag is `Latest-3.0.7`. Release notes still record the upstream tag. +**Koensayr release tags:** `{firmware}-koensayr-{version}` (e.g. `3.0.7-koensayr-2.4.0`). Stock **3.0.7** is built from upstream tag `Latest-3.0.7`. User-facing copy comes from [`.github/workflows/workflow.md`](../.github/workflows/workflow.md); commits by **SeanathanVT** append a **Detailed notes:** section with the latest commit message. **Not built by CI:** other stock tags (`2.8.2`, `ADB-2.1.9`, `type-b-1.7.6`, …), `rom_type_b.zip`, `rom_240p.zip`, `update.zip`, voice packs, and other assets. @@ -20,7 +20,7 @@ To add another stock tag, extend `Y1_UPSTREAM_TAGS` in `discover-inputs.sh`. ## Output naming -- **GitHub release tag:** `{slug}@{firmware-version}` (e.g. `y1-stock-rom@3.0.2`, `y1-stock-rom@3.0.7`) +- **GitHub release tag / title:** `{firmware-version}-koensayr-{koensayr-version}` (e.g. `3.0.2-koensayr-2.4.0`, `3.0.7-koensayr-2.4.0`) - **Internal firmware slug** (`--firmware-slug`): `@` replaced with `-` (e.g. `y1-stock-rom-3.0.2`) for `system-*-devel.img` naming ## Patch set diff --git a/tools/ci/build-one.sh b/tools/ci/build-one.sh index a099ed5..e1ed2ea 100644 --- a/tools/ci/build-one.sh +++ b/tools/ci/build-one.sh @@ -6,7 +6,7 @@ # ./tools/ci/build-one.sh \ # --source-repo y1-community/y1-stock-rom \ # --source-tag 3.0.2 \ -# --release-tag y1-stock-rom@3.0.2 \ +# [--release-tag 3.0.2-koensayr-2.4.0] # optional; derived from slug + apply.bash version if omitted # --download-url \ # --digest \ # --slug y1-stock-rom-3.0.2 \ @@ -17,6 +17,7 @@ set -euo pipefail SOURCE_REPO="" SOURCE_TAG="" RELEASE_TAG="" +RELEASE_TAG_ARG="" DOWNLOAD_URL="" DIGEST="" SLUG="" @@ -26,7 +27,7 @@ while [[ $# -gt 0 ]]; do case "$1" in --source-repo) SOURCE_REPO="$2"; shift 2 ;; --source-tag) SOURCE_TAG="$2"; shift 2 ;; - --release-tag) RELEASE_TAG="$2"; shift 2 ;; + --release-tag) RELEASE_TAG_ARG="$2"; shift 2 ;; --download-url) DOWNLOAD_URL="$2"; shift 2 ;; --digest) DIGEST="$2"; shift 2 ;; --slug) SLUG="$2"; shift 2 ;; @@ -49,8 +50,8 @@ EOF esac done -if [[ -z "$SOURCE_REPO" || -z "$SOURCE_TAG" || -z "$RELEASE_TAG" || -z "$DOWNLOAD_URL" || -z "$SLUG" ]]; then - echo "ERROR: --source-repo, --source-tag, --release-tag, --download-url, and --slug are required" >&2 +if [[ -z "$SOURCE_REPO" || -z "$SOURCE_TAG" || -z "$DOWNLOAD_URL" || -z "$SLUG" ]]; then + echo "ERROR: --source-repo, --source-tag, --download-url, and --slug are required" >&2 exit 1 fi @@ -71,6 +72,16 @@ KOENSAYR_VERSION="$(grep -E '^# Version:' apply.bash | awk '{print $3}')" PATCH_REVISION="$("${CI_DIR}/patch-revision.sh")" RELEASE_INTRO_FILE="${REPO_ROOT}/.github/workflows/workflow.md" +if ! FW_VERSION="$(firmware_version_from_slug "$SLUG")"; then + echo "ERROR: slug ${SLUG} is not a known y1-stock-rom firmware id" >&2 + exit 1 +fi +RELEASE_TAG="${FW_VERSION}-koensayr-${KOENSAYR_VERSION}" +RELEASE_TITLE="${FW_VERSION}-koensayr-${KOENSAYR_VERSION}" +if [[ -n "$RELEASE_TAG_ARG" && "$RELEASE_TAG_ARG" != "$RELEASE_TAG" ]]; then + echo "[build-one] NOTE: matrix release-tag ${RELEASE_TAG_ARG} ignored; using ${RELEASE_TAG}" +fi + # True when the commit being built was authored by SeanathanVT (GitHub / git display name). commit_author_is_seanathanvt() { local author email @@ -127,11 +138,6 @@ fi echo "[build-one] Downloading upstream rom.zip.." curl -fsSL -o "${STAGING}/rom.zip" "$DOWNLOAD_URL" -if ! FW_VERSION="$(firmware_version_from_slug "$SLUG")"; then - echo "ERROR: slug ${SLUG} is not a known y1-stock-rom firmware id" >&2 - exit 1 -fi - if [[ -n "$DIGEST" ]]; then actual_digest="$(sha256sum "${STAGING}/rom.zip" | awk '{print $1}')" if [[ "$actual_digest" != "$DIGEST" ]]; then @@ -220,6 +226,8 @@ data = { "source_repo": "${SOURCE_REPO}", "source_tag": "${SOURCE_TAG}", "release_tag": "${RELEASE_TAG}", + "release_title": "${RELEASE_TITLE}", + "firmware_version": "${FW_VERSION}", "slug": "${SLUG}", "upstream_digest_sha256": "${DIGEST}", "output_rom_sha256": h.hexdigest(), @@ -237,8 +245,6 @@ fi NOTES="${WORKDIR}/release-notes.md" { - echo "# Koensayr ${RELEASE_TAG}" - echo "" if [[ -f "$RELEASE_INTRO_FILE" ]]; then grep -v '^Devs:' "$RELEASE_INTRO_FILE" || true else @@ -246,7 +252,7 @@ NOTES="${WORKDIR}/release-notes.md" fi echo "" if commit_author_is_seanathanvt; then - echo "## Detailed notes" + echo "Detailed notes:" echo "" git log -1 --format='%B' echo "" @@ -254,7 +260,9 @@ NOTES="${WORKDIR}/release-notes.md" echo "---" echo "" cat </dev/null 2>&1; then + echo "[build-one] Removing legacy release tag ${LEGACY_RELEASE_TAG}.." + gh release delete "$LEGACY_RELEASE_TAG" --yes +fi + +echo "[build-one] Publishing GitHub release ${RELEASE_TAG} (${RELEASE_TITLE}).." echo "[build-one] upstream sha256: ${upstream_sha}" echo "[build-one] output sha256: ${output_sha}" if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then @@ -308,7 +322,7 @@ if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then gh release delete "$RELEASE_TAG" --yes fi gh release create "$RELEASE_TAG" "$RELEASE_ASSET" "$MANIFEST" \ - --title "Koensayr ${RELEASE_TAG}" \ + --title "${RELEASE_TITLE}" \ --notes-file "$NOTES" echo "[build-one] Published ${RELEASE_TAG} (rom.zip + build-manifest.json)" diff --git a/tools/ci/discover-inputs.sh b/tools/ci/discover-inputs.sh index 693e1c8..a2268ce 100644 --- a/tools/ci/discover-inputs.sh +++ b/tools/ci/discover-inputs.sh @@ -2,7 +2,7 @@ # discover-inputs.sh — build matrix for allowed y1-stock-rom rom.zip releases. # # Only these upstream tags are considered: -# y1-community/y1-stock-rom → 3.0.2, Latest-3.0.7 (published as y1-stock-rom@3.0.2 / @3.0.7) +# y1-community/y1-stock-rom → 3.0.2, Latest-3.0.7 (published as 3.0.2-koensayr-* / 3.0.7-koensayr-*) # # Usage: # ./tools/ci/discover-inputs.sh [--source-repo OWNER/NAME] [--force] @@ -30,7 +30,7 @@ Emits a JSON array of matrix objects: source_repo, source_tag, release_tag, download_url, digest, slug Upstream allowlist (rom.zip only): - y1-community/y1-stock-rom: 3.0.2, Latest-3.0.7 (→ koensayr release y1-stock-rom@3.0.2 / @3.0.7) + y1-community/y1-stock-rom: 3.0.2, Latest-3.0.7 (→ koensayr release 3.0.2-koensayr-VERSION / 3.0.7-koensayr-VERSION) EOF exit 0 ;; @@ -54,14 +54,18 @@ REPOS=( "y1-community/y1-stock-rom" ) -python3 - "$SOURCE_FILTER" "$FORCE" "${REPOS[@]}" <<'PY' +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +KOENSAYR_VERSION="$(grep -E '^# Version:' "${REPO_ROOT}/apply.bash" | awk '{print $3}')" + +python3 - "$SOURCE_FILTER" "$FORCE" "$KOENSAYR_VERSION" "${REPOS[@]}" <<'PY' import json import subprocess import sys source_filter = sys.argv[1] force = sys.argv[2] == "true" -repos = sys.argv[3:] +koensayr_version = sys.argv[3] +repos = sys.argv[4:] Y1_REPO = "y1-community/y1-stock-rom" # Upstream GitHub release tag → firmware version for koensayr release naming. @@ -112,7 +116,7 @@ for repo in repos: if asset is None: continue fw_version = Y1_UPSTREAM_TAGS[upstream_tag] - release_tag = f"y1-stock-rom@{fw_version}" + release_tag = f"{fw_version}-koensayr-{koensayr_version}" slug = f"y1-stock-rom-{fw_version}" entries.append( { From 16f65333696597b084d34931d4c94f7c5352e9e5 Mon Sep 17 00:00:00 2001 From: Ryan Specter Date: Sat, 23 May 2026 00:47:57 +0100 Subject: [PATCH 17/17] Tag firmware releases as koensayrversion-koensayr-osversion for Updater. --- README.md | 10 +++++----- docs/SUPPORTED-FIRMWARE-CI.md | 4 ++-- tools/ci/build-one.sh | 21 +++++++++++++-------- tools/ci/discover-inputs.sh | 6 +++--- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c51468c..59580f0 100644 --- a/README.md +++ b/README.md @@ -112,18 +112,18 @@ Stock sizes: 3.0.2 `rom.zip` 259,502,414 bytes (raw `system.img` inside); 3.0.7 Workflow [`.github/workflows/build-firmware-releases.yml`](.github/workflows/build-firmware-releases.yml) builds an allowlisted set of [y1-stock-rom](https://github.com/y1-community/y1-stock-rom) **`rom.zip`** releases only: -- Upstream tags **3.0.2** and **Latest-3.0.7** (published as `3.0.2-koensayr-*` / `3.0.7-koensayr-*`, where `*` is the koensayr version from `apply.bash`) +- Upstream tags **3.0.2** and **Latest-3.0.7** (published as `{koensayr-version}-koensayr-3.0.2` / `{koensayr-version}-koensayr-3.0.7`) For each input it downloads upstream `rom.zip`, verifies SHA256 (from the release asset) and MD5 against [`KNOWN_FIRMWARES`](apply.bash), runs `./apply.bash --all --no-flash`, repacks `rom.zip`, and publishes a release on this repo. -**Release tag pattern:** `{firmware-version}-koensayr-{koensayr-version}` (e.g. `3.0.7-koensayr-2.4.0`). Release notes start with [`.github/workflows/workflow.md`](.github/workflows/workflow.md). Each release attaches **`rom.zip`** (patched) plus **`build-manifest.json`**. +**Release tag pattern:** `{koensayr-version}-koensayr-{firmware-version}` (e.g. `2.4.0-koensayr-3.0.7`) for Innioasis Updater compatibility. Release notes start with [`.github/workflows/workflow.md`](.github/workflows/workflow.md). Each release attaches **`rom.zip`** (patched) plus **`build-manifest.json`**. Download from **this repo’s release tag**, not from [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) — upstream `rom.zip` is stock (~238–259 MB). Patched builds are larger (~295–329 MB) and have a different SHA256 (listed in the release notes / manifest). | Release | Expected `rom.zip` size (approx.) | Patched SHA256 (May 2026 CI) | |---------|-----------------------------------|------------------------------| -| 3.0.2-koensayr-* | 329,015,308 bytes (May 2026 CI) | `2371ac0970c0dbac318077373467859439aa0414caa15e29b90d8e879b8bbd80` | -| 3.0.7-koensayr-* | 309,073,126 bytes (May 2026 CI) | `2fa3fb7bf9ced11a21d0ce3bd0aec8a521dd3c6f22d9e0be8301b9ca3951dddb` | +| *-koensayr-3.0.2 | 329,015,308 bytes (May 2026 CI) | `2371ac0970c0dbac318077373467859439aa0414caa15e29b90d8e879b8bbd80` | +| *-koensayr-3.0.7 | 309,073,126 bytes (May 2026 CI) | `2fa3fb7bf9ced11a21d0ce3bd0aec8a521dd3c6f22d9e0be8301b9ca3951dddb` | **Triggers:** weekly schedule, pushes to `main` that touch patcher/CI paths (always republish both firmware tags; existing releases are deleted and recreated), and manual `workflow_dispatch` (optional `force` / `source_repo` filter). @@ -134,7 +134,7 @@ Local dry-run (no GitHub publish): ```bash KOENSAYR_SKIP_PUBLISH=1 ./tools/ci/build-one.sh \ --source-repo y1-community/y1-stock-rom --source-tag 3.0.2 \ - --release-tag 3.0.2-koensayr-2.4.0 \ + --release-tag 2.4.0-koensayr-3.0.2 \ --download-url "$(gh release view 3.0.2 --repo y1-community/y1-stock-rom --json assets -q '.assets[] | select(.name==\"rom.zip\") | .url')" \ --digest "$(gh release view 3.0.2 --repo y1-community/y1-stock-rom --json assets -q '.assets[] | select(.name==\"rom.zip\") | .digest' | sed 's/sha256://')" \ --slug y1-stock-rom-3.0.2 diff --git a/docs/SUPPORTED-FIRMWARE-CI.md b/docs/SUPPORTED-FIRMWARE-CI.md index 20531c9..afc839f 100644 --- a/docs/SUPPORTED-FIRMWARE-CI.md +++ b/docs/SUPPORTED-FIRMWARE-CI.md @@ -10,7 +10,7 @@ CI only builds these upstream tags (see [`tools/ci/discover-inputs.sh`](../tools |------------|---------------|-------| | [y1-community/y1-stock-rom](https://github.com/y1-community/y1-stock-rom) | **3.0.2**, **Latest-3.0.7** | `rom.zip` | -**Koensayr release tags:** `{firmware}-koensayr-{version}` (e.g. `3.0.7-koensayr-2.4.0`). Stock **3.0.7** is built from upstream tag `Latest-3.0.7`. User-facing copy comes from [`.github/workflows/workflow.md`](../.github/workflows/workflow.md); commits by **SeanathanVT** append a **Detailed notes:** section with the latest commit message. +**Koensayr release tags:** `{koensayr-version}-koensayr-{stock-firmware}` (e.g. `2.4.0-koensayr-3.0.7`) so Innioasis Updater can read the stock OS version from the suffix. Stock **3.0.7** is built from upstream tag `Latest-3.0.7`. User-facing copy comes from [`.github/workflows/workflow.md`](../.github/workflows/workflow.md); commits by **SeanathanVT** append a **Detailed notes:** section with the latest commit message. **Not built by CI:** other stock tags (`2.8.2`, `ADB-2.1.9`, `type-b-1.7.6`, …), `rom_type_b.zip`, `rom_240p.zip`, `update.zip`, voice packs, and other assets. @@ -20,7 +20,7 @@ To add another stock tag, extend `Y1_UPSTREAM_TAGS` in `discover-inputs.sh`. ## Output naming -- **GitHub release tag / title:** `{firmware-version}-koensayr-{koensayr-version}` (e.g. `3.0.2-koensayr-2.4.0`, `3.0.7-koensayr-2.4.0`) +- **GitHub release tag / title:** `{koensayr-version}-koensayr-{firmware-version}` (e.g. `2.4.0-koensayr-3.0.2`, `2.4.0-koensayr-3.0.7`) - **Internal firmware slug** (`--firmware-slug`): `@` replaced with `-` (e.g. `y1-stock-rom-3.0.2`) for `system-*-devel.img` naming ## Patch set diff --git a/tools/ci/build-one.sh b/tools/ci/build-one.sh index e1ed2ea..33d1f64 100644 --- a/tools/ci/build-one.sh +++ b/tools/ci/build-one.sh @@ -6,7 +6,7 @@ # ./tools/ci/build-one.sh \ # --source-repo y1-community/y1-stock-rom \ # --source-tag 3.0.2 \ -# [--release-tag 3.0.2-koensayr-2.4.0] # optional; derived from slug + apply.bash version if omitted +# [--release-tag 2.4.0-koensayr-3.0.2] # optional; derived as VERSION-koensayr-FW if omitted # --download-url \ # --digest \ # --slug y1-stock-rom-3.0.2 \ @@ -76,8 +76,8 @@ if ! FW_VERSION="$(firmware_version_from_slug "$SLUG")"; then echo "ERROR: slug ${SLUG} is not a known y1-stock-rom firmware id" >&2 exit 1 fi -RELEASE_TAG="${FW_VERSION}-koensayr-${KOENSAYR_VERSION}" -RELEASE_TITLE="${FW_VERSION}-koensayr-${KOENSAYR_VERSION}" +RELEASE_TAG="${KOENSAYR_VERSION}-koensayr-${FW_VERSION}" +RELEASE_TITLE="${KOENSAYR_VERSION}-koensayr-${FW_VERSION}" if [[ -n "$RELEASE_TAG_ARG" && "$RELEASE_TAG_ARG" != "$RELEASE_TAG" ]]; then echo "[build-one] NOTE: matrix release-tag ${RELEASE_TAG_ARG} ignored; using ${RELEASE_TAG}" fi @@ -245,6 +245,8 @@ fi NOTES="${WORKDIR}/release-notes.md" { + echo "# ${RELEASE_TITLE}" + echo "" if [[ -f "$RELEASE_INTRO_FILE" ]]; then grep -v '^Devs:' "$RELEASE_INTRO_FILE" || true else @@ -308,11 +310,14 @@ EOF RELEASE_ASSET="${WORKDIR}/rom.zip" cp -f "$OUTPUT_ROM" "$RELEASE_ASSET" -LEGACY_RELEASE_TAG="y1-stock-rom@${FW_VERSION}" -if [[ "$LEGACY_RELEASE_TAG" != "$RELEASE_TAG" ]] && gh release view "$LEGACY_RELEASE_TAG" >/dev/null 2>&1; then - echo "[build-one] Removing legacy release tag ${LEGACY_RELEASE_TAG}.." - gh release delete "$LEGACY_RELEASE_TAG" --yes -fi +for LEGACY_RELEASE_TAG in \ + "y1-stock-rom@${FW_VERSION}" \ + "${FW_VERSION}-koensayr-${KOENSAYR_VERSION}"; do + if [[ "$LEGACY_RELEASE_TAG" != "$RELEASE_TAG" ]] && gh release view "$LEGACY_RELEASE_TAG" >/dev/null 2>&1; then + echo "[build-one] Removing legacy release tag ${LEGACY_RELEASE_TAG}.." + gh release delete "$LEGACY_RELEASE_TAG" --yes + fi +done echo "[build-one] Publishing GitHub release ${RELEASE_TAG} (${RELEASE_TITLE}).." echo "[build-one] upstream sha256: ${upstream_sha}" diff --git a/tools/ci/discover-inputs.sh b/tools/ci/discover-inputs.sh index a2268ce..113b387 100644 --- a/tools/ci/discover-inputs.sh +++ b/tools/ci/discover-inputs.sh @@ -2,7 +2,7 @@ # discover-inputs.sh — build matrix for allowed y1-stock-rom rom.zip releases. # # Only these upstream tags are considered: -# y1-community/y1-stock-rom → 3.0.2, Latest-3.0.7 (published as 3.0.2-koensayr-* / 3.0.7-koensayr-*) +# y1-community/y1-stock-rom → 3.0.2, Latest-3.0.7 (published as VERSION-koensayr-3.0.2 / VERSION-koensayr-3.0.7) # # Usage: # ./tools/ci/discover-inputs.sh [--source-repo OWNER/NAME] [--force] @@ -30,7 +30,7 @@ Emits a JSON array of matrix objects: source_repo, source_tag, release_tag, download_url, digest, slug Upstream allowlist (rom.zip only): - y1-community/y1-stock-rom: 3.0.2, Latest-3.0.7 (→ koensayr release 3.0.2-koensayr-VERSION / 3.0.7-koensayr-VERSION) + y1-community/y1-stock-rom: 3.0.2, Latest-3.0.7 (→ koensayr release VERSION-koensayr-3.0.2 / VERSION-koensayr-3.0.7) EOF exit 0 ;; @@ -116,7 +116,7 @@ for repo in repos: if asset is None: continue fw_version = Y1_UPSTREAM_TAGS[upstream_tag] - release_tag = f"{fw_version}-koensayr-{koensayr_version}" + release_tag = f"{koensayr_version}-koensayr-{fw_version}" slug = f"y1-stock-rom-{fw_version}" entries.append( {