From c32c59463d5863836ea98708382e76cecebc9c50 Mon Sep 17 00:00:00 2001 From: guibeira Date: Thu, 27 Aug 2026 20:29:06 -0300 Subject: [PATCH 1/4] ci: cache harness integration binaries --- .github/scripts/discover_changed_workers.py | 1 + .../tests/test_discover_changed_workers.py | 4 +- .../scripts/tests/test_rust_ci_workflows.py | 146 +++++++- .github/workflows/_harness-integration.yml | 323 ++++++++++++++++-- harness/Makefile | 22 +- harness/tests/integration/compose-smoke.sh | 210 ++++++++++++ harness/tests/integration/compose-smoke.yaml | 96 ++++++ harness/tests/integration/compose.lock | 8 + 8 files changed, 779 insertions(+), 31 deletions(-) create mode 100644 harness/tests/integration/compose-smoke.sh create mode 100644 harness/tests/integration/compose-smoke.yaml create mode 100644 harness/tests/integration/compose.lock diff --git a/.github/scripts/discover_changed_workers.py b/.github/scripts/discover_changed_workers.py index 3ab288628..4fe0f364a 100644 --- a/.github/scripts/discover_changed_workers.py +++ b/.github/scripts/discover_changed_workers.py @@ -45,6 +45,7 @@ "context-manager", "iii-directory", "state", + "database", "console", } INTEGRATION_DOC_GLOBS = ( diff --git a/.github/scripts/tests/test_discover_changed_workers.py b/.github/scripts/tests/test_discover_changed_workers.py index 1b8d3eae6..552f51433 100644 --- a/.github/scripts/tests/test_discover_changed_workers.py +++ b/.github/scripts/tests/test_discover_changed_workers.py @@ -338,7 +338,7 @@ def test_llm_router_docs_do_not_run_live_router_integration(self, tmp_path): assert r.returncode == 0, r.stderr assert json.loads(r.stdout)["llm_router_integration"] is False - def test_database_change_stays_out_of_integration(self, tmp_path): + def test_database_change_runs_integration(self, tmp_path): repo = make_repo_with_harness(tmp_path) (repo / "database" / "lib.rs").write_text("// change\n") subprocess.run( @@ -354,7 +354,7 @@ def test_database_change_stays_out_of_integration(self, tmp_path): assert r.returncode == 0, r.stderr data = json.loads(r.stdout) assert data["changed_workers"] == ["database"] - assert data["integration_changed"] is False + assert data["integration_changed"] is True def test_subscription_provider_change_stays_out_of_integration(self, tmp_path): repo = make_repo_with_harness(tmp_path) diff --git a/.github/scripts/tests/test_rust_ci_workflows.py b/.github/scripts/tests/test_rust_ci_workflows.py index 83534be65..7faed8b87 100644 --- a/.github/scripts/tests/test_rust_ci_workflows.py +++ b/.github/scripts/tests/test_rust_ci_workflows.py @@ -78,24 +78,166 @@ def test_interface_smoke_bounds_each_engine_readiness_probe() -> None: ) in run -def test_harness_integration_caches_the_final_engine_and_skips_rebuilds() -> None: +def test_harness_integration_keeps_scenarios_on_the_source_pinned_engine() -> None: integration = workflow("_harness-integration.yml") steps = integration["jobs"]["integration"]["steps"] restore = named_step(steps, "Restore pinned engine binary") build = named_step(steps, "Build pinned engine") save = named_step(steps, "Save pinned engine binary") - stack_cache = named_step(steps, "Restore integration Rust cache") + lock = tomllib.loads( + (REPOSITORY / "harness" / "tests" / "integration" / "engine.lock").read_text() + ) + assert lock["revision"] == "15dc993ebfdbfcabe5d299cf4cae4dd676db4c06" expected_path = "target/integration-engine-src/${{ steps.lock.outputs.binary }}" assert restore["with"]["path"] == expected_path assert "integration-engine-bin-rust-1.97.1" in restore["with"]["key"] assert "hashFiles('harness/tests/integration/engine.lock')" in restore["with"]["key"] + assert named_step(steps, "Checkout pinned engine source")["with"]["ref"] == ( + "${{ steps.lock.outputs.revision }}" + ) assert build["if"] == "steps.engine-cache.outputs.cache-hit != 'true'" assert "--locked --release --timings" in build["run"] assert save["with"]["path"] == expected_path + assert named_step(steps, "Run integration scenarios")["run"].endswith( + 'III_BIN="${{ steps.engine.outputs.bin }}"' + ) + + +def test_harness_integration_installs_checksum_pinned_compose_release() -> None: + integration = workflow("_harness-integration.yml") + steps = integration["jobs"]["integration"]["steps"] + install = named_step(steps, "Install pinned Compose release") + stack_cache = named_step(steps, "Restore fallback integration Rust cache") + lock = tomllib.loads( + (REPOSITORY / "harness" / "tests" / "integration" / "compose.lock").read_text() + ) + + assert lock["tag"] == "iii/v0.23.0-rc.5" + assert lock["revision"] == "42b8627dd697076a339f8a1318843fae1f38a283" + assert lock["x86_64_asset"].endswith("unknown-linux-musl.tar.gz") + assert len(lock["x86_64_sha256"]) == 64 + assert len(lock["aarch64_sha256"]) == 64 + assert "--proto '=https' --tlsv1.2" in install["run"] + assert "sha256sum --check" in install["run"] + assert '[[ "$(tar -tzf "$archive")" == "$BINARY" ]]' in install["run"] + assert "expected_version=${TAG#iii/v}" in install["run"] assert "database -> target" in stack_cache["with"]["workspaces"] +def test_harness_integration_restores_and_saves_final_component_binaries() -> None: + steps = workflow("_harness-integration.yml")["jobs"]["integration"]["steps"] + step_names = [step.get("name") for step in steps] + components = { + "harness": ( + "Restore harness binaries", + "Save harness binaries", + "harness/target/release/harness-integration", + ), + "queue": ( + "Restore queue binary", + "Save queue binary", + "queue/target/release/queue", + ), + "iii-directory": ( + "Restore iii-directory binary", + "Save iii-directory binary", + "iii-directory/target/release/iii-directory", + ), + "session-manager": ( + "Restore session-manager binary", + "Save session-manager binary", + "session-manager/target/release/session-manager", + ), + "context-manager": ( + "Restore context-manager binary", + "Save context-manager binary", + "context-manager/target/release/context-manager", + ), + "state": ( + "Restore state binary", + "Save state binary", + "state/target/release/state", + ), + "database": ( + "Restore database binary", + "Save database binary", + "database/target/release/database", + ), + "console": ( + "Restore console binary", + "Save console binary", + "console/target/release/console", + ), + } + + for component, (restore_name, save_name, binary) in components.items(): + restore = named_step(steps, restore_name) + save = named_step(steps, save_name) + assert restore["uses"] == "actions/cache/restore@v4" + assert binary in restore["with"]["path"] + assert "integration-component-bin-v1-rust-1.97.1" in restore["with"]["key"] + assert f"'{component}/**'" in restore["with"]["key"] + assert save["uses"] == "actions/cache/save@v4" + assert save["with"]["key"].endswith(".outputs.cache-primary-key }}") + assert save["if"].startswith("inputs.save-cache &&") + assert step_names.index(save_name) > step_names.index( + "Verify integration report links" + ) + + build = named_step(steps, "Build missing integration binaries")["run"] + run = named_step(steps, "Run integration scenarios")["run"] + assert "cargo build --locked --release --timings" in build + assert "steps.harness-bin-cache.outputs.cache-hit" in build + assert "steps.database-bin-cache.outputs.cache-hit" in build + assert "integration-run" in run + assert "integration-test" not in run + assert all(step.get("name") != "Build Console worker" for step in steps) + + +def test_makefile_can_build_and_run_the_integration_stack_separately() -> None: + makefile = (REPOSITORY / "harness" / "Makefile").read_text() + assert "integration-test: integration-build integration-run" in makefile + assert "integration-build:" in makefile + assert "integration-run:" in makefile + + +def test_harness_integration_smokes_the_compose_lifecycle() -> None: + steps = workflow("_harness-integration.yml")["jobs"]["integration"]["steps"] + smoke_step = named_step(steps, "Smoke test iii compose") + fixture_template = ( + REPOSITORY / "harness" / "tests" / "integration" / "compose-smoke.yaml" + ).read_text() + fixture = yaml.load( + fixture_template.replace("@COMPOSE_ENGINE_PORT@", "3210"), + Loader=yaml.BaseLoader, + ) + script = ( + REPOSITORY / "harness" / "tests" / "integration" / "compose-smoke.sh" + ).read_text() + + expected = { + "queue", + "iii-directory", + "session-manager", + "context-manager", + "state", + "database", + } + assert smoke_step["env"]["III_BIN"] == "${{ steps.compose-engine.outputs.bin }}" + assert smoke_step["run"] == "bash harness/tests/integration/compose-smoke.sh" + assert set(fixture["containers"]) == expected + assert all( + container["scripts"]["run"].startswith("@") + for container in fixture["containers"].values() + ) + assert 'compose --up --file "$COMPOSE_FILE"' in script + assert "trigger compose::status" in script + assert "trigger state::set" in script + assert "trigger compose::down" in script + assert 'kill -TERM "$compose_pid"' in script + + def test_slow_rust_builds_upload_cargo_timing_reports() -> None: integration = workflow("_harness-integration.yml") integration_steps = integration["jobs"]["integration"]["steps"] diff --git a/.github/workflows/_harness-integration.yml b/.github/workflows/_harness-integration.yml index 5046ff3f3..41f946b86 100644 --- a/.github/workflows/_harness-integration.yml +++ b/.github/workflows/_harness-integration.yml @@ -100,10 +100,8 @@ jobs: package-manager-cache: false node-version: 22 - # The engine source is immutable at the locked revision. Cache the final - # executable, not its multi-gigabyte target directory, so a hit can skip - # Cargo entirely instead of merely making the unconditional rebuild less - # cold. + # The scenario engine source is immutable at the locked revision. Cache + # its final executable so a hit skips the engine build. - name: Restore pinned engine binary id: engine-cache uses: actions/cache/restore@v4 @@ -132,7 +130,172 @@ jobs: sha256sum "$bin" echo "bin=$bin" >> "$GITHUB_OUTPUT" - - name: Restore integration Rust cache + - name: Read and validate compose.lock + id: compose-lock + run: | + set -euo pipefail + lock=harness/tests/integration/compose.lock + read_field() { + local field=$1 values + values=$(sed -n "s/^${field} = \"\\([^\"]*\\)\"$/\\1/p" "$lock") + [[ $(printf '%s\n' "$values" | sed '/^$/d' | wc -l) -eq 1 ]] || { + echo "compose.lock: $field must appear exactly once" + exit 3 + } + printf '%s' "$values" + } + invalid=$(sed \ + -e '/^[[:space:]]*$/d' \ + -e '/^[[:space:]]*#/d' \ + -e '/^\(repository\|tag\|revision\|binary\|x86_64_asset\|x86_64_sha256\|aarch64_asset\|aarch64_sha256\) = "[^"]*"$/d' \ + "$lock") + [[ -z "$invalid" ]] || { + echo "compose.lock: unsupported or malformed entry" + printf '%s\n' "$invalid" + exit 3 + } + repository=$(read_field repository) + tag=$(read_field tag) + revision=$(read_field revision) + binary=$(read_field binary) + x86_64_asset=$(read_field x86_64_asset) + x86_64_sha256=$(read_field x86_64_sha256) + aarch64_asset=$(read_field aarch64_asset) + aarch64_sha256=$(read_field aarch64_sha256) + [[ "$repository" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]] || { + echo "compose.lock: repository must be an owner/name slug" + exit 3 + } + [[ "$tag" =~ ^iii/v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]] || { + echo "compose.lock: tag must be an iii release candidate" + exit 3 + } + [[ "$revision" =~ ^[0-9a-f]{40}$ ]] || { echo "compose.lock: revision must be 40-hex"; exit 3; } + [[ "$binary" == "iii" ]] || { echo "compose.lock: binary must be iii"; exit 3; } + [[ "$x86_64_asset" == "iii-x86_64-unknown-linux-musl.tar.gz" ]] || { + echo "compose.lock: unsupported x86_64 asset" + exit 3 + } + [[ "$aarch64_asset" == "iii-aarch64-unknown-linux-gnu.tar.gz" ]] || { + echo "compose.lock: unsupported aarch64 asset" + exit 3 + } + [[ "$x86_64_sha256" =~ ^[0-9a-f]{64}$ ]] || { echo "compose.lock: invalid x86_64 digest"; exit 3; } + [[ "$aarch64_sha256" =~ ^[0-9a-f]{64}$ ]] || { echo "compose.lock: invalid aarch64 digest"; exit 3; } + { + echo "repository=$repository" + echo "tag=$tag" + echo "revision=$revision" + echo "binary=$binary" + echo "x86_64_asset=$x86_64_asset" + echo "x86_64_sha256=$x86_64_sha256" + echo "aarch64_asset=$aarch64_asset" + echo "aarch64_sha256=$aarch64_sha256" + } >> "$GITHUB_OUTPUT" + + - name: Install pinned Compose release + id: compose-engine + env: + ARCH: ${{ runner.arch }} + REPOSITORY: ${{ steps.compose-lock.outputs.repository }} + TAG: ${{ steps.compose-lock.outputs.tag }} + BINARY: ${{ steps.compose-lock.outputs.binary }} + X86_64_ASSET: ${{ steps.compose-lock.outputs.x86_64_asset }} + X86_64_SHA256: ${{ steps.compose-lock.outputs.x86_64_sha256 }} + AARCH64_ASSET: ${{ steps.compose-lock.outputs.aarch64_asset }} + AARCH64_SHA256: ${{ steps.compose-lock.outputs.aarch64_sha256 }} + run: | + set -euo pipefail + case "$ARCH" in + X64) asset=$X86_64_ASSET; expected=$X86_64_SHA256 ;; + ARM64) asset=$AARCH64_ASSET; expected=$AARCH64_SHA256 ;; + *) echo "unsupported runner architecture: $ARCH"; exit 3 ;; + esac + archive="$RUNNER_TEMP/$asset" + destination="$GITHUB_WORKSPACE/target/integration-compose-release" + url="https://github.com/$REPOSITORY/releases/download/$TAG/$asset" + curl --proto '=https' --tlsv1.2 --retry 3 --retry-all-errors -fsSLo "$archive" "$url" + printf '%s %s\n' "$expected" "$archive" | sha256sum --check + [[ "$(tar -tzf "$archive")" == "$BINARY" ]] || { + echo "release archive must contain only $BINARY" + exit 3 + } + mkdir -p "$destination" + tar -xzf "$archive" -C "$destination" + bin="$destination/$BINARY" + [[ -x "$bin" ]] || { echo "release binary missing at $bin"; exit 3; } + expected_version=${TAG#iii/v} + actual_version=$("$bin" --version) + [[ "$actual_version" == "$expected_version" ]] || { + echo "release binary version does not match $TAG" + exit 3 + } + sha256sum "$bin" + echo "bin=$bin" >> "$GITHUB_OUTPUT" + echo "release=$TAG" >> "$GITHUB_OUTPUT" + + # Restore only final executables first. A PR normally gets these exact + # binaries from the latest trusted main build, so Cargo runs only for a + # component whose own inputs (or shared Rust inputs) changed. + - name: Restore harness binaries + id: harness-bin-cache + uses: actions/cache/restore@v4 + with: + path: | + harness/target/release/harness + harness/target/release/harness-integration + key: integration-component-bin-v1-rust-1.97.1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.coverage && 'coverage' || 'release' }}-harness-${{ hashFiles('harness/**', 'crates/**', 'packages/console-ui/**', 'rust-toolchain.toml', 'pnpm-lock.yaml', 'package.json') }} + + - name: Restore queue binary + id: queue-bin-cache + uses: actions/cache/restore@v4 + with: + path: queue/target/release/queue + key: integration-component-bin-v1-rust-1.97.1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.coverage && 'coverage' || 'release' }}-queue-${{ hashFiles('queue/**', 'crates/**', 'rust-toolchain.toml') }} + + - name: Restore iii-directory binary + id: directory-bin-cache + uses: actions/cache/restore@v4 + with: + path: iii-directory/target/release/iii-directory + key: integration-component-bin-v1-rust-1.97.1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.coverage && 'coverage' || 'release' }}-iii-directory-${{ hashFiles('iii-directory/**', 'crates/**', 'packages/console-ui/**', 'rust-toolchain.toml', 'pnpm-lock.yaml', 'package.json') }} + + - name: Restore session-manager binary + id: session-bin-cache + uses: actions/cache/restore@v4 + with: + path: session-manager/target/release/session-manager + key: integration-component-bin-v1-rust-1.97.1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.coverage && 'coverage' || 'release' }}-session-manager-${{ hashFiles('session-manager/**', 'crates/**', 'rust-toolchain.toml') }} + + - name: Restore context-manager binary + id: context-bin-cache + uses: actions/cache/restore@v4 + with: + path: context-manager/target/release/context-manager + key: integration-component-bin-v1-rust-1.97.1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.coverage && 'coverage' || 'release' }}-context-manager-${{ hashFiles('context-manager/**', 'crates/**', 'packages/console-ui/**', 'rust-toolchain.toml', 'pnpm-lock.yaml', 'package.json') }} + + - name: Restore state binary + id: state-bin-cache + uses: actions/cache/restore@v4 + with: + path: state/target/release/state + key: integration-component-bin-v1-rust-1.97.1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.coverage && 'coverage' || 'release' }}-state-${{ hashFiles('state/**', 'crates/**', 'packages/console-ui/**', 'rust-toolchain.toml', 'pnpm-lock.yaml', 'package.json') }} + + - name: Restore database binary + id: database-bin-cache + uses: actions/cache/restore@v4 + with: + path: database/target/release/database + key: integration-component-bin-v1-rust-1.97.1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.coverage && 'coverage' || 'release' }}-database-${{ hashFiles('database/**', 'crates/**', 'packages/console-ui/**', 'rust-toolchain.toml', 'pnpm-lock.yaml', 'package.json') }} + + - name: Restore console binary + id: console-bin-cache + uses: actions/cache/restore@v4 + with: + path: console/target/release/console + key: integration-component-bin-v1-rust-1.97.1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.coverage && 'coverage' || 'release' }}-console-${{ hashFiles('console/**', 'crates/**', 'packages/console-ui/**', 'rust-toolchain.toml', 'pnpm-lock.yaml', 'package.json') }} + + - name: Restore fallback integration Rust cache id: stack-cache uses: Swatinem/rust-cache@v2 with: @@ -160,14 +323,61 @@ jobs: if: inputs.coverage run: mkdir -p target/coverage/profraw - - name: Run integration scenarios + - name: Build missing integration binaries env: RUSTFLAGS: ${{ inputs.coverage && '-Cinstrument-coverage -Cllvm-args=-runtime-counter-relocation' || '' }} LLVM_PROFILE_FILE: ${{ inputs.coverage && format('{0}/target/coverage/profraw/%m_%p%c.profraw', github.workspace) || '' }} - run: >- - make -C harness integration-test - III_BIN="${{ steps.engine.outputs.bin }}" - CARGO_BUILD_FLAGS="--locked --timings" + run: | + set -euo pipefail + build_component() { + local hit=$1 manifest=$2 + shift 2 + if [[ "$hit" == "true" ]]; then + echo "using cached binaries for $manifest" + return + fi + cargo build --locked --release --timings --manifest-path "$manifest" "$@" + } + + build_component '${{ steps.queue-bin-cache.outputs.cache-hit }}' queue/Cargo.toml + build_component '${{ steps.directory-bin-cache.outputs.cache-hit }}' iii-directory/Cargo.toml + build_component '${{ steps.session-bin-cache.outputs.cache-hit }}' session-manager/Cargo.toml + build_component '${{ steps.context-bin-cache.outputs.cache-hit }}' context-manager/Cargo.toml + build_component '${{ steps.state-bin-cache.outputs.cache-hit }}' state/Cargo.toml + build_component '${{ steps.database-bin-cache.outputs.cache-hit }}' database/Cargo.toml + build_component '${{ steps.harness-bin-cache.outputs.cache-hit }}' harness/Cargo.toml -p harness -p harness-integration + build_component '${{ steps.console-bin-cache.outputs.cache-hit }}' console/Cargo.toml + + - name: Verify integration binaries + run: | + set -euo pipefail + binaries=( + harness/target/release/harness + harness/target/release/harness-integration + queue/target/release/queue + iii-directory/target/release/iii-directory + session-manager/target/release/session-manager + context-manager/target/release/context-manager + state/target/release/state + database/target/release/database + console/target/release/console + ) + for binary in "${binaries[@]}"; do + [[ -x "$binary" ]] || { echo "missing integration binary: $binary"; exit 3; } + sha256sum "$binary" + done + + - name: Smoke test iii compose + env: + III_BIN: ${{ steps.compose-engine.outputs.bin }} + COMPOSE_SMOKE_ROOT: ${{ github.workspace }}/target/integration-compose-smoke + COMPOSE_LLVM_PROFILE_FILE: ${{ inputs.coverage && format('{0}/target/coverage/profraw/%m_%p%c.profraw', github.workspace) || '' }} + run: bash harness/tests/integration/compose-smoke.sh + + - name: Run integration scenarios + env: + LLVM_PROFILE_FILE: ${{ inputs.coverage && format('{0}/target/coverage/profraw/%m_%p%c.profraw', github.workspace) || '' }} + run: make -C harness integration-run III_BIN="${{ steps.engine.outputs.bin }}" - name: Install Console web dependencies working-directory: console/web @@ -177,12 +387,6 @@ jobs: working-directory: console/web run: pnpm build - - name: Build Console worker - env: - RUSTFLAGS: ${{ inputs.coverage && '-Cinstrument-coverage -Cllvm-args=-runtime-counter-relocation' || '' }} - LLVM_PROFILE_FILE: ${{ inputs.coverage && format('{0}/target/coverage/profraw/%m_%p%c.profraw', github.workspace) || '' }} - run: cargo build --locked --release --timings --manifest-path console/Cargo.toml - - name: Install Playwright Chromium working-directory: console/web run: pnpm exec playwright install --with-deps chromium @@ -229,6 +433,66 @@ jobs: } done + # Publish component binaries only after every integration and browser + # check passed on trusted main. + - name: Save harness binaries + if: inputs.save-cache && steps.harness-bin-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: | + harness/target/release/harness + harness/target/release/harness-integration + key: ${{ steps.harness-bin-cache.outputs.cache-primary-key }} + + - name: Save queue binary + if: inputs.save-cache && steps.queue-bin-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: queue/target/release/queue + key: ${{ steps.queue-bin-cache.outputs.cache-primary-key }} + + - name: Save iii-directory binary + if: inputs.save-cache && steps.directory-bin-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: iii-directory/target/release/iii-directory + key: ${{ steps.directory-bin-cache.outputs.cache-primary-key }} + + - name: Save session-manager binary + if: inputs.save-cache && steps.session-bin-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: session-manager/target/release/session-manager + key: ${{ steps.session-bin-cache.outputs.cache-primary-key }} + + - name: Save context-manager binary + if: inputs.save-cache && steps.context-bin-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: context-manager/target/release/context-manager + key: ${{ steps.context-bin-cache.outputs.cache-primary-key }} + + - name: Save state binary + if: inputs.save-cache && steps.state-bin-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: state/target/release/state + key: ${{ steps.state-bin-cache.outputs.cache-primary-key }} + + - name: Save database binary + if: inputs.save-cache && steps.database-bin-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: database/target/release/database + key: ${{ steps.database-bin-cache.outputs.cache-primary-key }} + + - name: Save console binary + if: inputs.save-cache && steps.console-bin-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: console/target/release/console + key: ${{ steps.console-bin-cache.outputs.cache-primary-key }} + - name: Generate coverage report if: always() && inputs.coverage run: | @@ -261,6 +525,15 @@ jobs: if: always() env: ENGINE_HIT: ${{ steps.engine-cache.outputs.cache-hit }} + COMPOSE_RELEASE: ${{ steps.compose-engine.outputs.release }} + HARNESS_HIT: ${{ steps.harness-bin-cache.outputs.cache-hit }} + QUEUE_HIT: ${{ steps.queue-bin-cache.outputs.cache-hit }} + DIRECTORY_HIT: ${{ steps.directory-bin-cache.outputs.cache-hit }} + SESSION_HIT: ${{ steps.session-bin-cache.outputs.cache-hit }} + CONTEXT_HIT: ${{ steps.context-bin-cache.outputs.cache-hit }} + STATE_HIT: ${{ steps.state-bin-cache.outputs.cache-hit }} + DATABASE_HIT: ${{ steps.database-bin-cache.outputs.cache-hit }} + CONSOLE_HIT: ${{ steps.console-bin-cache.outputs.cache-hit }} SAVE_CACHE: ${{ inputs.save-cache }} run: | { @@ -268,8 +541,17 @@ jobs: echo echo "| Cache | Hit | Save enabled |" echo "| --- | --- | --- |" - echo "| pinned engine | ${ENGINE_HIT:-false} | $SAVE_CACHE |" - echo "| integration Rust | see rust-cache log | $SAVE_CACHE |" + echo "| scenario engine | ${ENGINE_HIT:-false} | $SAVE_CACHE |" + echo "| Compose release ${COMPOSE_RELEASE:-unavailable} | verified download | false |" + echo "| harness | ${HARNESS_HIT:-false} | $SAVE_CACHE |" + echo "| queue | ${QUEUE_HIT:-false} | $SAVE_CACHE |" + echo "| iii-directory | ${DIRECTORY_HIT:-false} | $SAVE_CACHE |" + echo "| session-manager | ${SESSION_HIT:-false} | $SAVE_CACHE |" + echo "| context-manager | ${CONTEXT_HIT:-false} | $SAVE_CACHE |" + echo "| state | ${STATE_HIT:-false} | $SAVE_CACHE |" + echo "| database | ${DATABASE_HIT:-false} | $SAVE_CACHE |" + echo "| console | ${CONSOLE_HIT:-false} | $SAVE_CACHE |" + echo "| fallback Rust objects | see rust-cache log | $SAVE_CACHE |" } >> "$GITHUB_STEP_SUMMARY" - name: Upload Rust build timings @@ -292,6 +574,7 @@ jobs: target/integration/*/result.json target/integration/*/execution.json target/integration/*/teardown.json + target/integration-compose-smoke/*.json if-no-files-found: ignore - name: Upload full non-pass artifacts @@ -299,7 +582,9 @@ jobs: uses: actions/upload-artifact@v6 with: name: integration-artifacts - path: target/integration/ + path: | + target/integration/ + target/integration-compose-smoke/ retention-days: 14 if-no-files-found: ignore diff --git a/harness/Makefile b/harness/Makefile index 224eee6c9..5675e4b94 100644 --- a/harness/Makefile +++ b/harness/Makefile @@ -30,7 +30,7 @@ RUST_WORKERS := $(filter-out $(PYTHON_WORKERS),$(STACK)) GOAL_ARGS := $(filter-out help install-iii-rc build install-local clean cargo-clean dev-run dev-up dev-down dev-restart \ engine wait-engine wait-base wait-stack restart restart-worker stop stop-worker stop-work stop-engine status smoke logs attach \ - ensure-session prepare-scrapling require-engine where integration-test integration-coverage integration-playground integration-validate quickstart-validate,$(MAKECMDGOALS)) + ensure-session prepare-scrapling require-engine where integration-test integration-build integration-run integration-coverage integration-playground integration-validate quickstart-validate,$(MAKECMDGOALS)) SELECTED_WORKERS := $(strip $(GOAL_ARGS)) ACTIVE_WORKERS := $(if $(SELECTED_WORKERS),$(SELECTED_WORKERS),$(STACK)) UNKNOWN_WORKERS := $(filter-out $(STACK),$(SELECTED_WORKERS)) @@ -42,7 +42,7 @@ RUN_FLAG := $(if $(filter release,$(RUN_PROFILE)),--release,) .PHONY: help install-iii-rc build install-local clean cargo-clean dev-run dev-up dev-down dev-restart engine wait-engine wait-base wait-stack \ restart restart-worker stop stop-worker stop-work stop-engine status smoke logs attach ensure-session prepare-scrapling \ - require-engine where integration-test integration-coverage integration-playground integration-validate quickstart-validate $(STACK) + require-engine where integration-test integration-build integration-run integration-coverage integration-playground integration-validate quickstart-validate $(STACK) help: @printf '%s\n' \ @@ -65,6 +65,8 @@ help: ' make cargo-clean [workers...] cargo clean stack or selected workers' \ ' make install-local [workers...] build and symlink into ~/.iii/workers' \ ' make integration-test III_BIN= run the harness integration scenarios (tests/integration)' \ + ' make integration-build build integration binaries without running scenarios' \ + ' make integration-run III_BIN= run scenarios with binaries that are already built' \ ' make integration-coverage III_BIN= run the scenarios instrumented and emit an LLVM coverage report' \ ' make integration-playground III_BIN= open the Console against an isolated integration stack' \ ' make integration-validate validate every integration scenario' \ @@ -383,18 +385,22 @@ INTEGRATION_ARTIFACTS ?= $(REPO_ROOT)/target/integration INTEGRATION_PLAYGROUND_SCENARIO ?= console-streamed-text INTEGRATION_PLAYGROUND_ARTIFACTS ?= $(REPO_ROOT)/target/console-e2e -integration-test: - @if [ -z "$(III_BIN)" ]; then \ - echo "III_BIN is required: path to the pinned iii engine binary."; \ - echo "Pinned source: harness/tests/integration/engine.lock"; \ - exit 3; \ - fi +integration-test: integration-build integration-run + +integration-build: @for w in $(INTEGRATION_WORKERS) harness; do \ echo "building $$w ($(INTEGRATION_PROFILE))"; \ cargo build $(CARGO_BUILD_FLAGS) $(INTEGRATION_FLAG) --manifest-path "$(REPO_ROOT)/$$w/Cargo.toml" || exit 1; \ done @echo "building harness-integration ($(INTEGRATION_PROFILE))" @cargo build $(CARGO_BUILD_FLAGS) $(INTEGRATION_FLAG) --manifest-path "$(MAKEFILE_DIR)Cargo.toml" -p harness-integration + +integration-run: + @if [ -z "$(III_BIN)" ]; then \ + echo "III_BIN is required: path to the pinned iii engine binary."; \ + echo "Pinned source: harness/tests/integration/engine.lock"; \ + exit 3; \ + fi @"$(MAKEFILE_DIR)target/$(INTEGRATION_PROFILE)/harness-integration" \ run \ --engine-bin "$(III_BIN)" \ diff --git a/harness/tests/integration/compose-smoke.sh b/harness/tests/integration/compose-smoke.sh new file mode 100644 index 000000000..0e6043a10 --- /dev/null +++ b/harness/tests/integration/compose-smoke.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd -- "$SCRIPT_DIR/../../.." && pwd) +PROFILE=${INTEGRATION_PROFILE:-release} +III_BIN=${III_BIN:-${1:-}} +COMPOSE_TEMPLATE="$SCRIPT_DIR/compose-smoke.yaml" +SMOKE_ROOT=${COMPOSE_SMOKE_ROOT:-$REPO_ROOT/target/integration-compose-smoke} +COMPOSE_FILE="$SMOKE_ROOT/worker-compose.yaml" +PYTHON=${PYTHON:-python3} +NAMESPACE=integration-compose-smoke + +[[ -n "$III_BIN" && -x "$III_BIN" ]] || { + echo "III_BIN must point to an executable iii release" + exit 3 +} + +export QUEUE_BIN=${QUEUE_BIN:-$REPO_ROOT/queue/target/$PROFILE/queue} +export III_DIRECTORY_BIN=${III_DIRECTORY_BIN:-$REPO_ROOT/iii-directory/target/$PROFILE/iii-directory} +export SESSION_MANAGER_BIN=${SESSION_MANAGER_BIN:-$REPO_ROOT/session-manager/target/$PROFILE/session-manager} +export CONTEXT_MANAGER_BIN=${CONTEXT_MANAGER_BIN:-$REPO_ROOT/context-manager/target/$PROFILE/context-manager} +export STATE_BIN=${STATE_BIN:-$REPO_ROOT/state/target/$PROFILE/state} +export DATABASE_BIN=${DATABASE_BIN:-$REPO_ROOT/database/target/$PROFILE/database} +export QUEUE_WORKER_DIR="$REPO_ROOT/queue" +export III_DIRECTORY_WORKER_DIR="$REPO_ROOT/iii-directory" +export SESSION_MANAGER_WORKER_DIR="$REPO_ROOT/session-manager" +export CONTEXT_MANAGER_WORKER_DIR="$REPO_ROOT/context-manager" +export STATE_WORKER_DIR="$REPO_ROOT/state" +export DATABASE_WORKER_DIR="$REPO_ROOT/database" + +for binary in \ + "$QUEUE_BIN" \ + "$III_DIRECTORY_BIN" \ + "$SESSION_MANAGER_BIN" \ + "$CONTEXT_MANAGER_BIN" \ + "$STATE_BIN" \ + "$DATABASE_BIN"; do + [[ -x "$binary" ]] || { echo "missing Compose smoke binary: $binary"; exit 3; } +done + +mkdir -p \ + "$SMOKE_ROOT/config" \ + "$SMOKE_ROOT/skills" \ + "$SMOKE_ROOT/local-skills" \ + "$SMOKE_ROOT/agents" \ + "$SMOKE_ROOT/global-agents" \ + "$SMOKE_ROOT/agents-skills" \ + "$SMOKE_ROOT/global-agents-skills" \ + "$SMOKE_ROOT/sessions" \ + "$SMOKE_ROOT/leases" \ + "$SMOKE_ROOT/state" + +port=$( + "$PYTHON" -c \ + 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()' +) +export COMPOSE_ENGINE_PORT=$port +export COMPOSE_ENGINE_URL="ws://127.0.0.1:$port" +export COMPOSE_CONFIG_DIR="$SMOKE_ROOT/config" +export COMPOSE_QUEUE_PATH="$SMOKE_ROOT/queue.json" +export COMPOSE_SKILLS_DIR="$SMOKE_ROOT/skills" +export COMPOSE_LOCAL_SKILLS_DIR="$SMOKE_ROOT/local-skills" +export COMPOSE_AGENTS_DIR="$SMOKE_ROOT/agents" +export COMPOSE_GLOBAL_AGENTS_DIR="$SMOKE_ROOT/global-agents" +export COMPOSE_AGENTS_SKILLS_DIR="$SMOKE_ROOT/agents-skills" +export COMPOSE_GLOBAL_AGENTS_SKILLS_DIR="$SMOKE_ROOT/global-agents-skills" +export COMPOSE_SESSION_DIR="$SMOKE_ROOT/sessions" +export COMPOSE_LEASES_DIR="$SMOKE_ROOT/leases" +export COMPOSE_DATABASE_PATH="$SMOKE_ROOT/database.sqlite" +export III_COMPOSE_STATE_DIR="$SMOKE_ROOT/state" +export COMPOSE_LLVM_PROFILE_FILE=${COMPOSE_LLVM_PROFILE_FILE:-} +export COMPOSE_TEMPLATE COMPOSE_FILE + +"$PYTHON" <<'PY' +import os +import re +from pathlib import Path + +template = Path(os.environ["COMPOSE_TEMPLATE"]).read_text() +names = ( + "COMPOSE_ENGINE_PORT", + "COMPOSE_ENGINE_URL", + "COMPOSE_CONFIG_DIR", + "COMPOSE_QUEUE_PATH", + "COMPOSE_SKILLS_DIR", + "COMPOSE_LOCAL_SKILLS_DIR", + "COMPOSE_AGENTS_DIR", + "COMPOSE_GLOBAL_AGENTS_DIR", + "COMPOSE_AGENTS_SKILLS_DIR", + "COMPOSE_GLOBAL_AGENTS_SKILLS_DIR", + "COMPOSE_SESSION_DIR", + "COMPOSE_LEASES_DIR", + "COMPOSE_DATABASE_PATH", + "COMPOSE_LLVM_PROFILE_FILE", + "QUEUE_BIN", + "III_DIRECTORY_BIN", + "SESSION_MANAGER_BIN", + "CONTEXT_MANAGER_BIN", + "STATE_BIN", + "DATABASE_BIN", + "QUEUE_WORKER_DIR", + "III_DIRECTORY_WORKER_DIR", + "SESSION_MANAGER_WORKER_DIR", + "CONTEXT_MANAGER_WORKER_DIR", + "STATE_WORKER_DIR", + "DATABASE_WORKER_DIR", +) +for name in names: + template = template.replace(f"@{name}@", os.environ[name]) +if re.search(r"@[A-Z0-9_]+@", template): + raise SystemExit("compose smoke template contains an unresolved placeholder") +Path(os.environ["COMPOSE_FILE"]).write_text(template) +PY + +compose_log="$SMOKE_ROOT/compose.log" +compose_pid= + +cleanup() { + exit_code=$? + trap - EXIT + set +e + if [[ -n "$compose_pid" ]] && kill -0 "$compose_pid" 2>/dev/null; then + kill -TERM "$compose_pid" 2>/dev/null + for _ in {1..60}; do + kill -0 "$compose_pid" 2>/dev/null || break + sleep 0.25 + done + if kill -0 "$compose_pid" 2>/dev/null; then + kill -KILL "$compose_pid" 2>/dev/null + fi + wait "$compose_pid" 2>/dev/null + fi + if ((exit_code != 0)); then + echo "iii compose smoke failed; captured output follows" + sed -n '1,240p' "$compose_log" 2>/dev/null + fi + exit "$exit_code" +} +trap cleanup EXIT + +III_TELEMETRY_ENABLED=false "$III_BIN" compose --up --file "$COMPOSE_FILE" \ + >"$compose_log" 2>&1 & +compose_pid=$! + +status_file="$SMOKE_ROOT/compose-status.json" +deadline=$((SECONDS + 45)) +while true; do + if timeout --signal=KILL 5s "$III_BIN" trigger compose::status \ + --port "$port" \ + --namespace "$NAMESPACE" \ + --json "{\"file\":\"$COMPOSE_FILE\"}" >"$status_file" 2>/dev/null \ + && jq -e ' + ([.containers[].container] | sort) == + ["context-manager", "database", "iii-directory", "queue", "session-manager", "state"] + and all(.containers[]; .state == "ready" and .owned == true) + ' "$status_file" >/dev/null; then + break + fi + if ! kill -0 "$compose_pid" 2>/dev/null; then + echo "iii compose exited before the stack became ready" + exit 1 + fi + if ((SECONDS >= deadline)); then + echo "iii compose did not make every worker ready before the deadline" + exit 1 + fi + sleep 1 +done + +timeout --signal=KILL 5s "$III_BIN" trigger state::set \ + --port "$port" \ + --namespace "$NAMESPACE" \ + --json '{"scope":"compose-smoke","key":"ready","value":true}' \ + >"$SMOKE_ROOT/state-set.json" +timeout --signal=KILL 5s "$III_BIN" trigger state::get \ + --port "$port" \ + --namespace "$NAMESPACE" \ + --json '{"scope":"compose-smoke","key":"ready"}' \ + >"$SMOKE_ROOT/state-get.json" +jq -e '. == true' "$SMOKE_ROOT/state-get.json" >/dev/null + +timeout --signal=KILL 10s "$III_BIN" trigger compose::down \ + --port "$port" \ + --namespace "$NAMESPACE" \ + --json "{\"file\":\"$COMPOSE_FILE\"}" \ + >"$SMOKE_ROOT/compose-down.json" +jq -e ' + .status == "ok" + and ([.containers[].container] | sort) == + ["context-manager", "database", "iii-directory", "queue", "session-manager", "state"] + and all(.containers[]; .state == "stopped") +' "$SMOKE_ROOT/compose-down.json" >/dev/null + +if kill -0 "$compose_pid" 2>/dev/null; then + kill -TERM "$compose_pid" +fi +for _ in {1..60}; do + kill -0 "$compose_pid" 2>/dev/null || break + sleep 0.25 +done +if kill -0 "$compose_pid" 2>/dev/null; then + echo "iii compose did not stop after SIGTERM" + exit 1 +fi +wait "$compose_pid" || true +compose_pid= +trap - EXIT + +echo "iii compose started, verified, and stopped all integration workers" diff --git a/harness/tests/integration/compose-smoke.yaml b/harness/tests/integration/compose-smoke.yaml new file mode 100644 index 000000000..bad387aed --- /dev/null +++ b/harness/tests/integration/compose-smoke.yaml @@ -0,0 +1,96 @@ +namespace: integration-compose-smoke +startup_timeout: 30s +stop_timeout: 5s + +engine: + url: "@COMPOSE_ENGINE_URL@" + workers: + configuration: + adapter: + name: fs + config: + directory: "@COMPOSE_CONFIG_DIR@" + iii-worker-manager: + host: 127.0.0.1 + port: @COMPOSE_ENGINE_PORT@ + iii-stream: {} + +containers: + queue: + worker: "path://@QUEUE_WORKER_DIR@" + config_name: queue + environment: + LLVM_PROFILE_FILE: "@COMPOSE_LLVM_PROFILE_FILE@" + config_override: + adapter: + name: builtin + config: + store_method: file_based + file_path: "@COMPOSE_QUEUE_PATH@" + save_interval_ms: 1000 + scripts: + run: "@QUEUE_BIN@" + + iii-directory: + worker: "path://@III_DIRECTORY_WORKER_DIR@" + config_name: iii-directory + environment: + LLVM_PROFILE_FILE: "@COMPOSE_LLVM_PROFILE_FILE@" + config_override: + skills_folder: "@COMPOSE_SKILLS_DIR@" + local_skills_folder: "@COMPOSE_LOCAL_SKILLS_DIR@" + agents_folder: "@COMPOSE_AGENTS_DIR@" + global_agents_folder: "@COMPOSE_GLOBAL_AGENTS_DIR@" + agents_skills_folder: "@COMPOSE_AGENTS_SKILLS_DIR@" + global_agents_skills_folder: "@COMPOSE_GLOBAL_AGENTS_SKILLS_DIR@" + auto_download: false + scripts: + run: "@III_DIRECTORY_BIN@" + + session-manager: + worker: "path://@SESSION_MANAGER_WORKER_DIR@" + config_name: session-manager + environment: + LLVM_PROFILE_FILE: "@COMPOSE_LLVM_PROFILE_FILE@" + config_override: + adapter: + name: fs + config: + data_dir: "@COMPOSE_SESSION_DIR@" + scripts: + run: "@SESSION_MANAGER_BIN@" + + context-manager: + worker: "path://@CONTEXT_MANAGER_WORKER_DIR@" + config_name: context-manager + environment: + LLVM_PROFILE_FILE: "@COMPOSE_LLVM_PROFILE_FILE@" + config_override: + lease_dir: "@COMPOSE_LEASES_DIR@" + scripts: + run: "@CONTEXT_MANAGER_BIN@" + + state: + worker: "path://@STATE_WORKER_DIR@" + config_name: state + environment: + LLVM_PROFILE_FILE: "@COMPOSE_LLVM_PROFILE_FILE@" + config_override: + adapter: + name: kv + config: + store_method: in_memory + scripts: + run: "@STATE_BIN@" + + database: + worker: "path://@DATABASE_WORKER_DIR@" + config_name: database + environment: + LLVM_PROFILE_FILE: "@COMPOSE_LLVM_PROFILE_FILE@" + config_override: + databases: + primary: + url: "sqlite:@COMPOSE_DATABASE_PATH@" + scripts: + run: "@DATABASE_BIN@" diff --git a/harness/tests/integration/compose.lock b/harness/tests/integration/compose.lock new file mode 100644 index 000000000..67cbd0916 --- /dev/null +++ b/harness/tests/integration/compose.lock @@ -0,0 +1,8 @@ +repository = "iii-hq/iii" +tag = "iii/v0.23.0-rc.5" +revision = "42b8627dd697076a339f8a1318843fae1f38a283" +binary = "iii" +x86_64_asset = "iii-x86_64-unknown-linux-musl.tar.gz" +x86_64_sha256 = "84a55c93b724655cef1f9bcaa4a693bc5762c592388085cf751af4e1dbe402de" +aarch64_asset = "iii-aarch64-unknown-linux-gnu.tar.gz" +aarch64_sha256 = "ca42ccaa75bd506d392294e8f8aef9a7108c8ad8f175f8a536e3b30c91063c93" From 9f17f965707f15779aed20e9184a1aa5ab5e3b43 Mon Sep 17 00:00:00 2001 From: guibeira Date: Thu, 27 Aug 2026 20:51:39 -0300 Subject: [PATCH 2/4] fix: harden integration workflow sequencing --- .github/scripts/tests/test_rust_ci_workflows.py | 4 +++- .github/workflows/_harness-integration.yml | 3 ++- harness/Makefile | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/scripts/tests/test_rust_ci_workflows.py b/.github/scripts/tests/test_rust_ci_workflows.py index 7faed8b87..2bd8d3f0a 100644 --- a/.github/scripts/tests/test_rust_ci_workflows.py +++ b/.github/scripts/tests/test_rust_ci_workflows.py @@ -119,6 +119,7 @@ def test_harness_integration_installs_checksum_pinned_compose_release() -> None: assert len(lock["x86_64_sha256"]) == 64 assert len(lock["aarch64_sha256"]) == 64 assert "--proto '=https' --tlsv1.2" in install["run"] + assert "--location --proto-redir '=https'" in install["run"] assert "sha256sum --check" in install["run"] assert '[[ "$(tar -tzf "$archive")" == "$BINARY" ]]' in install["run"] assert "expected_version=${TAG#iii/v}" in install["run"] @@ -197,7 +198,8 @@ def test_harness_integration_restores_and_saves_final_component_binaries() -> No def test_makefile_can_build_and_run_the_integration_stack_separately() -> None: makefile = (REPOSITORY / "harness" / "Makefile").read_text() - assert "integration-test: integration-build integration-run" in makefile + assert "integration-test: integration-build\n" in makefile + assert "\t@$(MAKE) --no-print-directory integration-run" in makefile assert "integration-build:" in makefile assert "integration-run:" in makefile diff --git a/.github/workflows/_harness-integration.yml b/.github/workflows/_harness-integration.yml index 41f946b86..e4fa60277 100644 --- a/.github/workflows/_harness-integration.yml +++ b/.github/workflows/_harness-integration.yml @@ -214,7 +214,8 @@ jobs: archive="$RUNNER_TEMP/$asset" destination="$GITHUB_WORKSPACE/target/integration-compose-release" url="https://github.com/$REPOSITORY/releases/download/$TAG/$asset" - curl --proto '=https' --tlsv1.2 --retry 3 --retry-all-errors -fsSLo "$archive" "$url" + curl --location --proto-redir '=https' --proto '=https' --tlsv1.2 \ + --retry 3 --retry-all-errors -fsSLo "$archive" "$url" printf '%s %s\n' "$expected" "$archive" | sha256sum --check [[ "$(tar -tzf "$archive")" == "$BINARY" ]] || { echo "release archive must contain only $BINARY" diff --git a/harness/Makefile b/harness/Makefile index 5675e4b94..9f167c415 100644 --- a/harness/Makefile +++ b/harness/Makefile @@ -385,7 +385,8 @@ INTEGRATION_ARTIFACTS ?= $(REPO_ROOT)/target/integration INTEGRATION_PLAYGROUND_SCENARIO ?= console-streamed-text INTEGRATION_PLAYGROUND_ARTIFACTS ?= $(REPO_ROOT)/target/console-e2e -integration-test: integration-build integration-run +integration-test: integration-build + @$(MAKE) --no-print-directory integration-run integration-build: @for w in $(INTEGRATION_WORKERS) harness; do \ From 5774eadbfe3e0306132e04ff39af70e9d11d9a9c Mon Sep 17 00:00:00 2001 From: guibeira Date: Fri, 28 Aug 2026 10:08:53 -0300 Subject: [PATCH 3/4] ci: retrigger checks From 5e0e76c9380874b0263156dce2735d9938ad60a0 Mon Sep 17 00:00:00 2001 From: guibeira Date: Fri, 28 Aug 2026 10:22:11 -0300 Subject: [PATCH 4/4] ci: compare cache behavior