diff --git a/.github/scripts/tests/test_release_workflows.py b/.github/scripts/tests/test_release_workflows.py index b80a2b248..96be8e255 100644 --- a/.github/scripts/tests/test_release_workflows.py +++ b/.github/scripts/tests/test_release_workflows.py @@ -154,3 +154,25 @@ def test_reusable_release_executors_have_no_implicit_inputs() -> None: def test_registry_publish_authenticates_iii_installer() -> None: body = (WORKFLOWS / "_publish-registry.yml").read_text() assert "GITHUB_TOKEN: ${{ github.token }}" in body + + +def test_rust_binary_cache_is_keyed_by_frontend_bundle_digest() -> None: + jobs = workflow(WORKFLOWS / "_rust-binary.yml")["jobs"] + web_build = jobs["web-build"] + assert web_build["outputs"]["frontend_digest"] == "${{ steps.stage.outputs.frontend_digest }}" + assert web_build["outputs"]["frontends"] == "${{ steps.dirs.outputs.frontends }}" + + stage = next(step for step in web_build["steps"] if step.get("name") == "Stage frontend bundles") + assert stage["id"] == "stage" + assert "frontend_digest=$digest" in stage["run"] + assert "git hash-object" in stage["run"] + + build_steps = jobs["build"]["steps"] + cache = next(step for step in build_steps if step.get("uses") == "Swatinem/rust-cache@v2") + assert "needs.web-build.outputs.frontend_digest" in cache["with"]["key"] + + verify = next(step for step in build_steps if step.get("name") == "Verify frontend bundle digest") + assert verify["if"] == "inputs.web_bundle" + assert verify["env"]["EXPECTED_DIGEST"] == "${{ needs.web-build.outputs.frontend_digest }}" + assert "git hash-object" in verify["run"] + assert '[[ "$actual" == "$EXPECTED_DIGEST" ]]' in verify["run"] diff --git a/.github/workflows/_rust-binary.yml b/.github/workflows/_rust-binary.yml index 511a66a53..4d1d4f127 100644 --- a/.github/workflows/_rust-binary.yml +++ b/.github/workflows/_rust-binary.yml @@ -88,6 +88,9 @@ jobs: needs: [matrix] if: inputs.web_bundle runs-on: ubuntu-latest + outputs: + frontend_digest: ${{ steps.stage.outputs.frontend_digest }} + frontends: ${{ steps.dirs.outputs.frontends }} steps: - uses: actions/checkout@v5 with: @@ -163,6 +166,7 @@ jobs: done - name: Stage frontend bundles + id: stage env: FRONTENDS: ${{ steps.dirs.outputs.frontends }} run: | @@ -177,6 +181,19 @@ jobs: mkdir -p "$stage/$bundle" cp -a "$bundle/dist" "$stage/$bundle/" done + digest=$( + for bundle in "${bundles[@]}"; do + find "$bundle/dist" -type f -print + done \ + | LC_ALL=C sort \ + | while IFS= read -r file; do + printf '%s %s\n' "$(git hash-object "$file")" "$file" + done \ + | git hash-object --stdin + ) + [[ "$digest" =~ ^[0-9a-f]{40}$ ]] + echo "frontend_digest=$digest" >> "$GITHUB_OUTPUT" + echo "::notice::frontend bundle digest: $digest" - name: Upload frontend bundles uses: actions/upload-artifact@v6 @@ -245,7 +262,10 @@ jobs: - name: Cache cargo registry & build uses: Swatinem/rust-cache@v2 with: - key: ${{ inputs.bin_name }}-${{ matrix.target }} + # Cargo caches compiled include_str!/rust-embed output. Key UI + # releases by the generated bytes so a new dist/ can never restore + # a worker artifact that still embeds the previous bundle. + key: ${{ inputs.bin_name }}-${{ matrix.target }}${{ inputs.web_bundle && format('-frontend-{0}', needs.web-build.outputs.frontend_digest) || '' }} # Without this, rust-cache defaults to the repo root, where there is no # Cargo.toml — its `cargo metadata` exits 101 and the cache is never # found or saved, so every build of every target ships cold. @@ -274,6 +294,28 @@ jobs: # any path-dependency crates with their own ui/dist). path: ./ + - name: Verify frontend bundle digest + if: inputs.web_bundle + shell: bash + env: + EXPECTED_DIGEST: ${{ needs.web-build.outputs.frontend_digest }} + FRONTENDS: ${{ needs.web-build.outputs.frontends }} + run: | + set -euo pipefail + IFS=, read -ra bundles <<< "$FRONTENDS" + actual=$( + for bundle in "${bundles[@]}"; do + test -d "$bundle/dist" + find "$bundle/dist" -type f -print + done \ + | LC_ALL=C sort \ + | while IFS= read -r file; do + printf '%s %s\n' "$(git hash-object "$file")" "$file" + done \ + | git hash-object --stdin + ) + [[ "$actual" == "$EXPECTED_DIGEST" ]] + - name: Build and upload binary uses: taiki-e/upload-rust-binary-action@v1 env: