From 79df1542923f083ffbda3f592d6c031606c775e5 Mon Sep 17 00:00:00 2001 From: Jatin K Malik Date: Sat, 12 Sep 2026 13:42:42 -0700 Subject: [PATCH] ci(flatpak): restore previous builder cache and skip src-only PRs (#825) The builder action restore-keys is `flatpak-builder-${arch}`. Our key was `flatpak-builder-${hash}`, which the action stores as `flatpak-builder-${hash}-x86_64`. That does not start with `flatpak-builder-x86_64`, so a packaging hash change (a lock refresh, or merging one) discarded the whole .flatpak-builder cache and recompiled numpy and pywhispercpp from source, ~18 min instead of ~4. Put ${{ matrix.arch }} in the key first so the fallback matches. Then flatpak-builder skips unchanged modules and only rebuilds what the hash change actually touched. PRs no longer watch src/**. python-test already covers those, and the expensive compile does not. push to main still does, so a sandbox install break fails the default branch. --- .github/workflows/flatpak.yml | 13 ++++++++---- .github/workflows/release.yml | 6 ++++-- tests/test_flatpak_packaging.py | 35 +++++++++++++++++++++++++++++++++ tests/test_release_integrity.py | 7 +++++++ 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 568e382..084e166 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -16,7 +16,10 @@ on: branches: [ main, master ] paths: - 'packaging/flatpak/**' - - 'src/**' + # src/** is push-only. A text-injection PR already runs python-test; + # the Flatpak job's cost is compiling numpy and pywhispercpp from + # source, which those PRs do not change. main still builds src-only + # pushes so a sandbox install break fails the default branch. - 'pyproject.toml' # The dependency manifest is generated from the hash-pinned export, so a # lock refresh moves what this build ships. Without this, `just lock` @@ -67,10 +70,12 @@ jobs: with: bundle: vocalinux-${{ matrix.arch }}.flatpak manifest-path: packaging/flatpak/com.vocalinux.Vocalinux.yml - # Stable key so .flatpak-builder cache restores across commits. + # The action appends -${arch} and restore-keys is + # `flatpak-builder-${arch}`, so the key has to start with that + # prefix. Putting the packaging hash first made every lock + # refresh a full rebuild (numpy + pywhispercpp from source). # github.sha in the key forced a full rebuild every run. - # Arch is appended automatically by the action. - cache-key: flatpak-builder-${{ hashFiles('packaging/flatpak/**') }} + cache-key: flatpak-builder-${{ matrix.arch }}-${{ hashFiles('packaging/flatpak/**') }} arch: ${{ matrix.arch }} artifact-name: vocalinux-flatpak-${{ matrix.arch }} # Flathub mirrors screenshots at submission time; mirror locally so the diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df4796a..cf19983 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -275,7 +275,9 @@ jobs: with: bundle: Vocalinux-${{ steps.get_version.outputs.VERSION }}-x86_64.flatpak manifest-path: packaging/flatpak/com.vocalinux.Vocalinux.yml - cache-key: flatpak-builder-${{ hashFiles('packaging/flatpak/**') }} + # Same prefix rule as flatpak.yml: restore-keys is + # `flatpak-builder-${arch}`. The action appends -${arch} again. + cache-key: flatpak-builder-x86_64-${{ hashFiles('packaging/flatpak/**') }} arch: x86_64 # Pinned actions/upload-artifact below, same digest as AppImage. upload-artifact: false @@ -310,7 +312,7 @@ jobs: with: bundle: Vocalinux-${{ steps.get_version.outputs.VERSION }}-aarch64.flatpak manifest-path: packaging/flatpak/com.vocalinux.Vocalinux.yml - cache-key: flatpak-builder-${{ hashFiles('packaging/flatpak/**') }} + cache-key: flatpak-builder-aarch64-${{ hashFiles('packaging/flatpak/**') }} arch: aarch64 upload-artifact: false mirror-screenshots-url: https://dl.flathub.org/media diff --git a/tests/test_flatpak_packaging.py b/tests/test_flatpak_packaging.py index cf30c27..649d614 100644 --- a/tests/test_flatpak_packaging.py +++ b/tests/test_flatpak_packaging.py @@ -242,6 +242,17 @@ def test_no_source_is_pinned_to_one_python_abi(source: dict[str, str]) -> None: ) +def _on_event_block(event: str) -> str: + """The body of one `on:` event, up to the next same-indent key.""" + match = re.search( + rf"^ {re.escape(event)}:\n(.*?)(?=^ [a-z_]+:|\Z)", + WORKFLOW.read_text(encoding="utf-8"), + re.M | re.S, + ) + assert match, f"flatpak.yml has no on.{event} block" + return match.group(1) + + def test_a_lock_refresh_reaches_the_flatpak_build() -> None: """The manifest is generated from requirements/, so a change there changes what the Flatpak ships. Without the path filter, `just lock` could move @@ -251,3 +262,27 @@ def test_a_lock_refresh_reaches_the_flatpak_build() -> None: "flatpak.yml does not watch requirements/**, so a lock refresh changes" " what the Flatpak builds and runs no build" ) + + +def test_prs_skip_the_flatpak_build_when_only_src_changed() -> None: + """numpy and pywhispercpp compile from source. A text-injection PR does + not need that. push to main still watches src/** so a sandbox install + break still fails the default branch.""" + assert "'src/**'" not in _on_event_block("pull_request") + assert "'src/**'" in _on_event_block("push") + + +def test_builder_cache_key_is_a_prefix_the_action_can_restore() -> None: + """The action restore-keys is `flatpak-builder-${arch}`. + + cache-key is passed through as `${cache-key}-${arch}`. A key of + `flatpak-builder-${hash}` becomes `flatpak-builder-${hash}-x86_64`, + which does not start with `flatpak-builder-x86_64`, so a packaging + hash change discarded the previous .flatpak-builder cache and + recompiled numpy and pywhispercpp from scratch (~18 min). Putting + `${{ matrix.arch }}` in the key first makes the fallback match. + """ + assert ( + "cache-key: flatpak-builder-${{ matrix.arch }}-" + "${{ hashFiles('packaging/flatpak/**') }}" in WORKFLOW.read_text(encoding="utf-8") + ) diff --git a/tests/test_release_integrity.py b/tests/test_release_integrity.py index fa5c192..b3ac796 100644 --- a/tests/test_release_integrity.py +++ b/tests/test_release_integrity.py @@ -202,6 +202,13 @@ def test_flatpak_release_jobs_reuse_ci_builder_pins(): assert "options: --privileged" in block, f"{name} is not a privileged container" assert _FLATPAK_BUILDER in block, f"{name} does not use the pinned builder action" assert f"arch: {arch}" in block + assert ( + f"cache-key: flatpak-builder-{arch}-${{{{ hashFiles('packaging/flatpak/**') }}}}" + in block + ), ( + f"{name} cache-key must start with flatpak-builder-{arch} so the " + "action's restore-keys prefix matches after it appends -${arch}" + ) assert f"runs-on: {runner}" in block assert f"Vocalinux-${{{{ steps.get_version.outputs.VERSION }}}}-{arch}.flatpak" in block assert "actions/upload-artifact" in block, f"{name} does not upload a workflow artifact"