mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
# Kubernetes backend plugin (crates/buzz-backend-kubernetes) + desktop deploy path Implements docs/remote-agents.md (merged @28ae6cd21) as ONE PR: the provider binary, the desktop changes that make it work, the harness inactivity reaper, the Sprig image, and the conformance/live-test suites. Channel: buzz-remote-agents (29414326-dba7-402d-b384-b1b34d63a2e6), thread c42b70ef. ## What's here (by lane) - **crates/buzz-backend-kubernetes** (Dawn): stdin/stdout JSON provider, info + deploy; pure classify.rs (one match arm per spec state-machine row); reconcile/GC with ownership-marker gate + same-clock orphan check; per-attempt immutable Secrets; three-tier env with clear-then-write authoritative tier. - **Desktop** (Mari): KD3 launch block from resolved descriptor, KD5 pre-secret negotiation gate (resolve-once → stage-and-digest → info → protocol gate → deploy), KD1 Windows extension strip, bundling (externalBin + Justfile + release/canary workflows + stub loops), tauri.windows.conf.json platform override (Decision B: no Windows artifact). - **buzz-acp** (Max): KD4 BUZZ_ACP_EXIT_AFTER_INACTIVITY reaper (pool-independent; reset only at accepted dispatch; in-flight turn/heartbeat defers, never resets); BUZZ_ACP_EXIT_AFTER_INACTIVITY + BUZZ_ACP_NO_PRESENCE reserved. KD8 fix. - **Image + tests** (Perci): Dockerfile.sprig (digest-pinned bases, exec buzz-acp PID 1, relay-scoped credential config), image contract script, provider conformance suites (golden wire fixtures shared with desktop tests), live-local runbook (namespace-scoped, shared-cluster safe). - **Docs** (Sami, first commit): citation re-pin c1bca1b56 →28ae6cd21(44/49 were already byte-exact; 3 offsets fixed) + I3 presence-bound correction (below). ## Named spec deviations (deliberate, each with rationale) 1. **No baked default image yet.** ghcr.io/block/buzz-sprig is unpublished (verified: anonymous pull 403 vs control 200). Omitted `image` returns an in-band field-required error instead of a default. 2. **Image override STRICTER than spec §Image:** digest-only (`name@sha256:<64hex>`); ALL tags rejected; `name:tag@digest` normalized. With no baked default the override is the only path, so tag-acceptance would make mutability the v1 norm. Strictness is reversible; a moved tag under an nsec is not. Baked digest default + tag re-acceptance = follow-up with image publish. 3. **imagePullSecrets not in schema (v1).** Explicit user images may rely on namespace-preprovisioned pull credentials — the substrate boundary. Field added only if the publish decision proves it necessary. 9-field budget intact. 4. **Decision A closed: writable empty workspace.** Nest projection = named follow-up; no image-side scaffolding. 5. **Decision D overridden by Tyler (event b55398d8):** provider ships bundled with the desktop like buzz-acp/buzz-agent; spec §Distribution's separate release workflow deleted for v1. 6. **I3/vision presence bound corrected 90s → 180s.** PRESENCE_TTL_SECS moved in #3783 during this spec's base→merge window; the number was inherited, not chosen. Spec :206/:216/:928 + inline quote + VISION_REMOTE_AGENTS.md:59 corrected. ← Tyler: the vision is your document; this edit is flagged for your explicit eyes. 7. **Spec citations are pinned to 28ae6cd21** (main at spec merge) and resolve there, not at this PR's head — this PR's own lanes move crates/buzz-acp/src/lib.rs by ~100 lines (19 citations across KD4/KD6/KD7/ §Stop/§Launch data). Known Defects rows fixed BY this PR retire on merge; the section documents main as of the pin. 8. **KD7 grace tension declared:** pod terminationGracePeriodSeconds=60 vs KD7's measured ~87s shutdown tail at parallelism 10 (~197s at cap 32). KD7 is ruled out of scope, so L1-3's "enough grace for full graceful shutdown" is NOT met at default config — deliberate, resolved by the KD7 follow-up, not silently. ## Question for Tyler Will ghcr.io/block/buzz-sprig publish PUBLIC? If private-by-policy, §Image needs an imagePullSecrets story before the baked-default follow-up can land. ## Out of scope (named follow-ups) KD6 exit-code contract + KD7 shutdown budget (gate OnFailure), OnFailure restart policy, Windows provider binary, PVCs/nest projection, mesh deployability, sprig image publish workflow + baked multi-arch digest default. ## Reproduce locally (four traps that cost us real time) **1. Git hooks inherit the invoking shell's PATH — pin the shell, not just your verification commands.** `rust-toolchain.toml` pins `1.95.0`, but the rustup shim that honors that pin lives in `~/.cargo/bin`. If Homebrew's cargo is earlier on PATH, `cargo` in this repo is 1.89.0, which cannot build the workspace at all: ``` $ /opt/homebrew/bin/cargo check -p buzz-db error: rustc 1.89.0 is not supported by the following packages: sqlx@0.9.0 requires rustc 1.94.0 ... # exit 101 ``` Verifying with `PATH="$HOME/.cargo/bin:$PATH" cargo test` does *not* protect the push: lefthook's `pre-push` → `just test-unit` re-resolves `cargo` from the shell's own PATH, so a green local run is followed by a hook failure on a crate you never touched. Export the PATH for the whole shell, not per-command. This bit twice. **2. Line-scope your mutations, or the mutation edits its own detector.** When mutation-testing the respond-to guard, a whole-file `sed` on the mode literal touches 5 sites — the guard *and* the fixtures/assertions that test it. The mutation and its detector move together and the suite stays green, which reads as "this code is dead" when it actually means "you deleted the experiment": ``` # WRONG — 5 sites, guard and tests mutate together $ sed -i '' 's/"allowlist"/"allowlist-DISABLED"/g' src/env.rs test result: ok. 145 passed; 0 failed # false survivor # RIGHT — 1 site, anchored to the guard's own definition line $ sed -i '' '/^const RESPOND_TO_ALLOWLIST/s/"allowlist"/"allowlist-DISABLED"/' src/env.rs failures: env::tests::allowlist_mode_with_an_empty_list_is_refused env::tests::an_allowlist_entry_that_is_not_64_hex_is_refused test result: FAILED. 143 passed; 2 failed # real kill ``` Restore by copying a pristine file back and confirming `git diff --stat` is empty, not by re-running an inverse `sed`. **3. A completeness guard is not a correctness guard.** The shared wire fixture `tests/fixtures/provider-wire/deploy-full-launch.request.json` passed every test we had while containing four classes of invented data (wrong `respond_to` encoding, an env key no emitter writes, allowlist entries that fail the harness's own 64-hex rule, a `launch.env` key from no descriptor layer). The provider's tests could not have caught this: its types are deliberately indifferent to these values (`Option<String>`, `Vec<String>`, arbitrary map), so "the provider parses it" was never evidence that the desktop emits it. The fix was not a stronger provider assertion but a rule about provenance — "recorded" means executed-and-transcribed, and the desktop's whole-object equality test is the only enforcement that can exist. See the fixture README. **4. Every drift this arc was a value that agreed with itself.** Five invented values were found, and not one was caught by an assertion failing — each was caught by someone asking where a value came from. A named constant referenced symbolically on both the fixture and assertion side. A `sed` that mutated its own detector. Six probe rows that all died at the same unrelated error. A descriptor struct literal compared against a fixture built from that literal (`launch.args: ["run","--session"]`, which the resolver actually returns as `["acp"]`). The general defense is not more assertions but provenance: a stub is a control that varies nothing, and the more faithful it looks the better it hides. Ask what executed, not what passed. *Fixture-test determinism caveat (post-verification, Quinn + Dawn).* The desktop's whole-object fixture test calls the real resolver, which consults a process-global harness registry whose own docs require `registry_test_lock` for any test touching it. The fixture test holds no lock and is nonetheless deterministic — but by containment, not by ordering. Measured, not derived: planting a definition with `id: "goose"` directly into the registry (bypassing the loader) changes the resolved descriptor from `args: ["acp"]` to `args: ["--poisoned"]`, so `resolve_effective_harness_descriptor` **does** reach the registry for this id — it does not short-circuit on the builtin table first. Two controls discriminate: an empty registry and a registry poisoned under a *different* id both return `["acp"]`. What actually protects the test is that the registry has exactly one writer (`update_loaded_harness_registry`, reached only via `warm_harness_registry_from_dir`) — but that writer concatenates **two** sources of unequal strength (`custom_harnesses.rs:319-326`). Custom files pass through `load_custom_harnesses`, whose `check_id_collision` rejects the reserved builtin id `goose` case-insensitively at the loader — and that leg is tested (`load_applies_id_collision_check` writes a real `goose.json` and asserts the loader drops it). Preset definitions (`preset_harness_definitions`, `presets.rs:177-193`) are a bare `.map` over `PRESET_HARNESSES` with **no collision check** — exhaustive call-site enumeration at `60007fda4` finds four production `check_id_collision` sites, none on the preset path. That leg holds only because `goose` is not in the preset table today (intersection of TIER1 and preset ids is empty) — executed, not just read: adding a preset with `id: "goose"`, `args: ["--poisoned"]` and warming via the normal preset-only path (`warm_harness_registry_from_dir(None)`, no custom dir, no direct writer) flips the fixture's emitted `launch.args` from `["acp"]` to `["--poisoned"]` at `60007fda4`, command/env/policy_env unchanged. So: no test in the suite can put a `goose` entry in the registry via the custom path, and no preset currently carries one, so no interleaving can perturb this fixture — containment with one checked leg and one coincidental one. A future fixture built on a **non-builtin** runtime id has no containment at all — it would be order-dependent against whatever registry-writing test ran last and must take the lock. *Late instance, found while reviewing the mode guard.* The guard exact-matches `respond_to` untrimmed and case-sensitively, which is only correct if clap's `ValueEnum` derive is case-sensitive. `config.rs` gives two answers: the derive at `:448-453` carries no `ignore_case`, while the crate's own tests call `RespondTo::from_str(s, true)` — `ignore_case = true`. Reading the source supports either. Measured on the built binary instead: `owner-only` starts, `OWNER-ONLY` / `Owner-Only` / `ALLOWLIST` / `NOBODY` all exit rc=2 `invalid value`. Case-sensitive at the CLI, so the guard is right — and right for a reason the source does not state. The `from_str(_, true)` tests exercise a different surface and are not evidence about the CLI. *Corollary, and the sharper half.* When a test helper **reimplements** production instead of calling it, the helper is a fork — and a fork can be right while production is wrong, or wrong in the same way, and the suite reports green either way. Both `BUZZ_ACP_ALLOWED_*` gates are forked like this: production compares **strings** while the helpers compare **post-parse enums** (`config.rs:2623`) or re-derive the split (`buzz-cli/.../channels.rs:1296`). Production and the helper each carry their *own* copy of the empty-entry filter (`:1025` and `:1300`), so fixing one says nothing about the other. Measured on `buzz-cli`, restoring byte-exact between runs: | tree | result | |---|---| | baseline | 274 passed | | drop the empty-filter in **production** only (the real fix) | **274 passed** — no signal | | drop it in the **test helper** only | **273 passed, 1 failed** (`channels.rs:1338`) | Two independent defects, stacked, and worse together than either alone: production can be fixed with no test ever noticing, *and* the helper cannot be corrected without a false alarm demanding the bug back. The root cause is one bit of type information — `check_allowed_channel_add_policy(allowed_raw: &str, ..)` cannot represent "unset", while production reads `env::var(..) -> Result`, where unset and `""` are different states. A helper whose parameter type can't represent all of production's input states isn't testing production's states — it's testing a subset it silently chose. Same family as the struct-literal descriptor and the fixture drift: the test and the thing it tests agreeing with each other, rather than the test measuring the thing. Neither defect is in this PR's diff (`git diff --name-only28ae6cd21<head> -- crates/buzz-cli` is empty); both are now filed as NIP-34 issues on this repo: the fail-open + fork-helper defect at issue event `0524a4113f2d97fd…` and the respond-to self-lock at `e32837498969b5e7…` (filed 2026-08-02 after Quinn measured that no prior filing existed — zero hits on GitHub `block/buzz` open *or* closed and zero on the relay's kind:1621 issues, against working positive controls). The prescription was itself mutation-tested before being written down: repairing the fork's signature (`Option<&str>` + assertion → `None`) still let the reintroduced production bug ship 274-green — an expressive fork is still a fork; it never executes production. So the `buzz-cli` fix has **three parts and one explicit keep**: drop the production filter; **delete** the helper and point its tests at the real `cmd_set_add_policy` (which self-discriminates by error variant — `Usage` = refused, `Network(BadScheme)` = passed the gate — no relay needed); serialize the env-var tests behind one **`tokio::sync::Mutex::const_new`** lock taken with `.lock().await`, including the pre-existing `:1362` integration test (the fork was silently buying test isolation — without the lock, parallel runs flake nondeterministically; a `std::sync::Mutex` held across `.await` trips `clippy::await_holding_lock` under `-D warnings`); and **keep** the then-dead `!allowed.is_empty()` clause with a comment saying why. It is unreachable-false (`split(',')` never yields an empty vec), but it is the only thing that keeps the reintroduced production bug detectable — mutation-tested: on a tree that deletes the clause, reintroducing the empty-filter bug survives 275/0, because `""`/`","`/`" "` refuse either way and the filter goes semantically inert. Dead code can be load-bearing for tests: "provably unreachable" is an argument about behavior, never about coverage. When a helper forks production, the fix has to delete the fork: any change that leaves two implementations standing can only ever be verified against the one the tests call. *Final shape:* the keep and the broad lock are both artifacts of the fork surviving in some form. The extraction variant (Dawn, mutation-tested at `60007fda4`) removes the tension: extract one `check_channel_add_policy_allowed(Option<&str>, &str)` that **production calls**, with the `Option` placed at the env boundary where the `Result<String, VarError>` bit actually lives. 5/6 mutants killed; the empty-filter survivor is proven **equivalent** (exhaustive 6174-pair check, 0 divergences, with a diverging negative control; independently re-derived by a second generator — different tokens and shape — 0 divergences on admitted policies, 500 on a non-admitted control), not a coverage hole — on a one-implementation tree there is no fork left to witness, so no dead clause needs keeping. One scope line on that equivalence: it is **caller-conditional**, a property of the only current caller, not of the gate function — `cmd_set_add_policy`'s own match at `:1027-1034` admits only three policies before the gate runs; a second caller reaching the gate with arbitrary strings resurrects m1 as a real hole. The lock does not disappear, it narrows (Dawn's own correction, caught by Mari): lock exactly the tests that mutate the process env — three-plus-one on a fork tree, two on the extraction tree — behind one `tokio::sync::Mutex`, and the lock is part of the assertion, not hygiene: with it deleted, the gate test fails 8/8 runs deterministically by receiving `Network(BadScheme)` where it expects `Usage` — the unset test's `remove_var` clobbers the other's `set_var`, and **the gate test passes straight through the gate**, a false negative on the exact authz assertion the test exists to make. State it as an outcome: these two tests must not observe each other's env writes. 276/0 stable across 5 parallel runs, clippy `-D warnings` clean; independently verified (patch applied to a second worktree: result blob `d67e584be` matches the patch index, full mutant matrix reproduces row for row). One new row no earlier prescription covered: collapsing unset into `Some("")` fails **closed** — an unconfigured deployment refuses every policy — killed by the unset test. Patch: `OUTBOX/BUZZ_CLI_ADD_POLICY_GATE_EXTRACT_FIX.patch`. The filed issue (`0524a411…`) carries the fork-shape prescription; whoever picks it up should prefer the extraction shape, drop the dead-clause keep with it, and keep part 3 outcome-shaped: serialize whichever tests mutate the env. ## Verification (final HEAD `60007fda4`) - Full touched-package suites at each integration merge (log in plan file). At candidate parent `00e5b5fe9`: buzz-backend-kubernetes 154, buzz-acp 673, desktop tauri 2100+3, pnpm 3908, workspace clippy/fmt/tsc all clean. The only delta to `60007fda4` is one character in `scripts/test-k8s-sprig-image-live.sh` (heredoc escape so the readlink probe evaluates pod-side, not host-side at render); `crates/` tree hash is byte-identical at both SHAs, so the Rust receipts attach by tree identity. buzz-backend-kubernetes suite re-run in-shell at `HEAD == 60007fda4`: 154 passed. - Adversarial one-HEAD gate (Sami): guard matrix 12/12, predicate mutants 7/7, doomed-invocation finding closed end-to-end; tree-hash carry to `60007fda4` confirmed (crates/buzz-backend-kubernetes blob unchanged). - Live-local pass per TESTING.md + skill-buzz-testing (Perci, at `60007fda4`): explicit `docker-desktop` context, digest-qualified image imported into node containerd `k8s.io` namespace, pull policy `Never`; pod printed `DIGEST_ABI_OK`, `resolved_spec` and `image_id` both the exact requested digest, script exit 0. Dedicated per-run namespace, ownership labels on every object, scoped cleanup verified empty after. - Implementation review (Wren) at `60007fda4`: 9.6 minimalness / 9.4 elegance / 9.3 correctness, no blocker. - `origin/eva/k8s-backend` == `60007fda4` (ls-remote verified; SHA identity is byte identity). --------- Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz>
1110 lines
48 KiB
YAML
1110 lines
48 KiB
YAML
name: CI
|
|
on:
|
|
push:
|
|
branches: [main, release]
|
|
pull_request:
|
|
|
|
concurrency:
|
|
group: ci-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev
|
|
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright
|
|
|
|
jobs:
|
|
changes:
|
|
name: Detect Changed Paths
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 2
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
outputs:
|
|
rust: ${{ steps.filter.outputs.rust }}
|
|
desktop: ${{ steps.filter.outputs.desktop }}
|
|
desktop-rust: ${{ steps.filter.outputs.desktop-rust }}
|
|
web: ${{ steps.filter.outputs.web }}
|
|
mobile: ${{ steps.filter.outputs.mobile }}
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
fetch-depth: 2
|
|
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
|
|
id: filter
|
|
with:
|
|
token: ''
|
|
filters: |
|
|
rust:
|
|
- 'crates/**'
|
|
- 'migrations/**'
|
|
- 'schema/**'
|
|
- 'Cargo.toml'
|
|
- 'Cargo.lock'
|
|
- 'rust-toolchain.toml'
|
|
- 'deny.toml'
|
|
- '.github/workflows/ci.yml'
|
|
- 'scripts/run-tests.sh'
|
|
- 'justfile'
|
|
desktop:
|
|
- 'scripts/check-file-sizes-core.mjs'
|
|
- 'scripts/check-file-sizes-core.test.mjs'
|
|
- 'desktop/**'
|
|
- '!desktop/src-tauri/**'
|
|
- 'pnpm-lock.yaml'
|
|
desktop-rust:
|
|
- 'desktop/src-tauri/**'
|
|
web:
|
|
- 'scripts/check-file-sizes-core.mjs'
|
|
- 'scripts/check-file-sizes-core.test.mjs'
|
|
- 'web/**'
|
|
- 'pnpm-lock.yaml'
|
|
mobile:
|
|
- 'scripts/check-file-sizes-core.mjs'
|
|
- 'scripts/check-file-sizes-core.test.mjs'
|
|
- 'mobile/**'
|
|
- 'scripts/mobile-release.sh'
|
|
- 'scripts/mobile-worktree-overrides.sh'
|
|
- 'scripts/mobile-worktree-clean.sh'
|
|
- 'scripts/publish-mobile-release-candidate.sh'
|
|
- 'scripts/release-rulesets.sh'
|
|
- 'scripts/test-mobile-release-contract.sh'
|
|
- 'scripts/test-mobile-release-candidate-publisher.sh'
|
|
- 'scripts/test-mobile-worktree-overrides.sh'
|
|
- '.github/workflows/mobile-release-candidate.yml'
|
|
- '.github/workflows/ci.yml'
|
|
- name: Release workflow source contract
|
|
run: scripts/test-release-ref-contract.sh
|
|
- name: Desktop release candidate contract
|
|
run: scripts/test-desktop-release-candidate.sh
|
|
- name: Mobile release contract
|
|
run: |
|
|
scripts/test-mobile-release-contract.sh
|
|
scripts/test-mobile-release-candidate-publisher.sh
|
|
- name: Mobile worktree identity contract
|
|
run: scripts/test-mobile-worktree-overrides.sh
|
|
- name: File size ratchet unit tests
|
|
run: node --test scripts/check-file-sizes-core.test.mjs
|
|
|
|
rust-lint:
|
|
name: Rust Lint
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true'
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
with:
|
|
save-if: ${{ github.event_name != 'pull_request' }}
|
|
- name: Format check
|
|
run: just fmt-check
|
|
- name: Desktop Tauri format check
|
|
run: just desktop-tauri-fmt-check
|
|
- name: Clippy
|
|
run: just clippy
|
|
|
|
unit-tests:
|
|
name: Unit Tests
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.rust == 'true'
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
with:
|
|
save-if: ${{ github.event_name != 'pull_request' }}
|
|
- name: Install cargo-nextest
|
|
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
|
with:
|
|
tool: cargo-nextest@0.9.136
|
|
- name: Unit tests
|
|
run: just test-unit
|
|
|
|
desktop-core:
|
|
name: Desktop Core
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
fetch-depth: 2
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
with:
|
|
workspaces: desktop/src-tauri
|
|
save-if: ${{ github.event_name != 'pull_request' }}
|
|
- name: Install Tauri dependencies (Linux)
|
|
env:
|
|
DEBIAN_FRONTEND: noninteractive
|
|
run: |
|
|
sudo apt-get update \
|
|
-o Acquire::Retries=3 \
|
|
-o Acquire::http::Timeout=30 \
|
|
-o Acquire::https::Timeout=30
|
|
sudo apt-get install -y --no-install-recommends \
|
|
-o Acquire::Retries=3 \
|
|
-o Acquire::http::Timeout=30 \
|
|
-o Acquire::https::Timeout=30 \
|
|
-o DPkg::Lock::Timeout=120 \
|
|
build-essential \
|
|
curl \
|
|
file \
|
|
libasound2-dev \
|
|
libayatana-appindicator3-dev \
|
|
libgtk-3-dev \
|
|
librsvg2-dev \
|
|
libssl-dev \
|
|
libwebkit2gtk-4.1-dev \
|
|
libxdo-dev \
|
|
patchelf \
|
|
wget
|
|
- name: Get pnpm store directory
|
|
id: pnpm-cache
|
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
|
- name: Restore pnpm store cache
|
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
|
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
restore-keys: pnpm-${{ runner.os }}-
|
|
- name: Install desktop dependencies
|
|
run: just desktop-install-ci
|
|
- name: Desktop lint and format
|
|
run: just desktop-check
|
|
- name: Desktop unit tests
|
|
run: just desktop-test
|
|
- name: Desktop build
|
|
run: just desktop-build
|
|
- name: Desktop Tauri clippy
|
|
run: just desktop-tauri-clippy
|
|
env:
|
|
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
|
|
- name: Desktop Tauri check
|
|
run: just desktop-tauri-check
|
|
env:
|
|
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
|
|
- name: Desktop Tauri tests
|
|
run: just desktop-tauri-test
|
|
env:
|
|
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
|
|
- name: Desktop Tauri compiled-flag verification
|
|
run: just desktop-tauri-test-compiled-flags
|
|
env:
|
|
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
|
|
- name: Upload desktop e2e artifacts
|
|
if: failure()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: desktop-e2e-artifacts
|
|
path: |
|
|
desktop/playwright-report
|
|
desktop/test-results
|
|
if-no-files-found: ignore
|
|
- name: Save pnpm store cache
|
|
if: github.event_name == 'push'
|
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
|
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
|
|
desktop-smoke-e2e:
|
|
name: Desktop Smoke E2E (${{ matrix.shard }})
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
shard: [1, 2, 3, 4]
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- name: Get pnpm store directory
|
|
id: pnpm-cache
|
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
|
- name: Restore pnpm store cache
|
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
|
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
restore-keys: pnpm-${{ runner.os }}-
|
|
- name: Install desktop dependencies
|
|
run: just desktop-install-ci
|
|
- name: Get Playwright version
|
|
id: pw-version
|
|
run: echo "version=$(cd desktop && node -e "console.log(require('@playwright/test/package.json').version)")" >> "$GITHUB_OUTPUT"
|
|
- name: Restore Playwright browser cache
|
|
id: playwright-cache
|
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
|
|
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
|
- name: Install Playwright Chromium
|
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
run: cd desktop && pnpm exec playwright install chromium
|
|
- name: Install Playwright system dependencies
|
|
run: cd desktop && pnpm exec playwright install-deps chromium
|
|
- name: Save Playwright browser cache
|
|
if: steps.playwright-cache.outputs.cache-hit != 'true' && github.event_name == 'push' && matrix.shard == 1
|
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
|
|
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
|
- name: Desktop E2E build
|
|
run: pnpm -C desktop build:e2e
|
|
- name: Desktop smoke e2e
|
|
run: cd desktop && pnpm exec playwright test --project=smoke --shard=${{ matrix.shard }}/4
|
|
- name: Summarize flaky tests
|
|
if: ${{ !cancelled() }}
|
|
run: node scripts/summarize-flaky-tests.mjs playwright-report.json "Desktop Smoke E2E (${{ matrix.shard }})"
|
|
working-directory: desktop
|
|
- name: Upload desktop smoke e2e artifacts
|
|
if: ${{ !cancelled() }}
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: desktop-smoke-e2e-artifacts-${{ matrix.shard }}
|
|
path: |
|
|
desktop/playwright-report
|
|
desktop/playwright-report.json
|
|
desktop/test-results
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|
|
|
|
desktop:
|
|
name: Desktop
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
needs: [changes, desktop-core, desktop-smoke-e2e]
|
|
if: always() && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- name: Check desktop jobs
|
|
run: |
|
|
if [ "${{ needs.desktop-core.result }}" != "success" ]; then
|
|
echo "Desktop Core finished with: ${{ needs.desktop-core.result }}"
|
|
exit 1
|
|
fi
|
|
if [ "${{ needs.desktop-smoke-e2e.result }}" != "success" ]; then
|
|
echo "Desktop Smoke E2E shards finished with: ${{ needs.desktop-smoke-e2e.result }}"
|
|
exit 1
|
|
fi
|
|
echo "Desktop jobs passed"
|
|
|
|
desktop-e2e-relay:
|
|
name: Desktop E2E Relay
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
# Reuse the relay binaries and backend test archive when none of their
|
|
# inputs changed (desktop-only PRs hit this every time). The key covers
|
|
# everything they embed, including migrations via sqlx migrate!.
|
|
- name: Restore relay artifacts cache
|
|
id: relay-artifacts-cache
|
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: |
|
|
target/ci/buzz-relay
|
|
target/ci/git-credential-nostr
|
|
target/ci/backend-integration-tests.tar.zst
|
|
key: relay-artifacts-${{ runner.os }}-${{ hashFiles('crates/**', 'migrations/**', 'Dockerfile', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo/config.toml', '.github/workflows/ci.yml') }}
|
|
- uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
|
|
if: steps.relay-artifacts-cache.outputs.cache-hit != 'true'
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
if: steps.relay-artifacts-cache.outputs.cache-hit != 'true'
|
|
with:
|
|
workspaces: |
|
|
.
|
|
desktop/src-tauri
|
|
save-if: ${{ github.event_name != 'pull_request' }}
|
|
- name: Install cargo-nextest
|
|
if: steps.relay-artifacts-cache.outputs.cache-hit != 'true'
|
|
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
|
with:
|
|
tool: cargo-nextest@0.9.136
|
|
- name: Build relay artifacts
|
|
if: steps.relay-artifacts-cache.outputs.cache-hit != 'true'
|
|
run: |
|
|
cargo build --profile ci -p buzz-relay -p git-credential-nostr
|
|
cargo nextest archive \
|
|
--cargo-profile ci \
|
|
-p buzz-db \
|
|
-p buzz-relay \
|
|
-p buzz-test-client \
|
|
--lib \
|
|
--test e2e_event_reminder \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst
|
|
- name: Save relay artifacts cache
|
|
if: steps.relay-artifacts-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: |
|
|
target/ci/buzz-relay
|
|
target/ci/git-credential-nostr
|
|
target/ci/backend-integration-tests.tar.zst
|
|
key: relay-artifacts-${{ runner.os }}-${{ hashFiles('crates/**', 'migrations/**', 'Dockerfile', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo/config.toml', '.github/workflows/ci.yml') }}
|
|
- name: Upload relay artifacts
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: desktop-e2e-relay
|
|
path: |
|
|
target/ci/buzz-relay
|
|
target/ci/git-credential-nostr
|
|
target/ci/backend-integration-tests.tar.zst
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
desktop-e2e-integration-shard:
|
|
name: Desktop E2E Integration (${{ matrix.shard }}/2)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
needs: [changes, desktop-e2e-relay]
|
|
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
shard: [1, 2]
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- name: Start integration services
|
|
run: |
|
|
for attempt in 1 2 3; do
|
|
if docker compose up -d postgres redis minio minio-init; then
|
|
break
|
|
fi
|
|
if [ "$attempt" -eq 3 ]; then
|
|
echo "docker compose up failed after 3 attempts" >&2
|
|
exit 1
|
|
fi
|
|
echo "docker compose up failed (attempt $attempt), retrying in $((attempt * 5))s..." >&2
|
|
sleep $((attempt * 5))
|
|
done
|
|
- name: Get pnpm store directory
|
|
id: pnpm-cache
|
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
|
- name: Restore pnpm store cache
|
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
|
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
restore-keys: pnpm-${{ runner.os }}-
|
|
- name: Install desktop dependencies
|
|
run: just desktop-install-ci
|
|
- name: Get Playwright version
|
|
id: pw-version
|
|
run: echo "version=$(cd desktop && node -e "console.log(require('@playwright/test/package.json').version)")" >> "$GITHUB_OUTPUT"
|
|
- name: Restore Playwright browser cache
|
|
id: playwright-cache
|
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
|
|
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
|
- name: Install Playwright Chromium
|
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
run: cd desktop && pnpm exec playwright install chromium
|
|
- name: Install Playwright system dependencies
|
|
run: cd desktop && pnpm exec playwright install-deps chromium
|
|
- name: Save Playwright browser cache
|
|
if: steps.playwright-cache.outputs.cache-hit != 'true' && github.event_name == 'push' && matrix.shard == 1
|
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
|
|
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
|
- name: Desktop E2E build
|
|
run: pnpm -C desktop build:e2e
|
|
- name: Wait for integration services
|
|
run: |
|
|
wait_healthy() {
|
|
local service="$1"
|
|
local container="$2"
|
|
for attempt in $(seq 1 60); do
|
|
status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "not_found")
|
|
if [ "${status}" = "healthy" ]; then
|
|
echo "${service} is healthy"
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
docker logs "${container}" || true
|
|
return 1
|
|
}
|
|
wait_healthy "Postgres" "buzz-postgres"
|
|
wait_healthy "Redis" "buzz-redis"
|
|
wait_healthy "MinIO" "buzz-minio"
|
|
- name: Download relay binary
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: desktop-e2e-relay
|
|
path: target/ci
|
|
- name: Apply schema and seed deployment community
|
|
# MT: the relay resolves each request's tenant from the communities host
|
|
# map and fails closed on an unmapped host. The channel reconciler binds
|
|
# the deployment community ONCE at boot (outside its retry loop) and
|
|
# exits permanently on an unmapped host, so the 'localhost:3000'
|
|
# community MUST exist before the relay starts — the retry loop only
|
|
# handles late-seeded channels, not a late-seeded community. The relay
|
|
# migrates at boot via BUZZ_AUTO_MIGRATE, but that's too late for the
|
|
# pre-boot seed, so apply the schema here first (then drop AUTO_MIGRATE
|
|
# below). lower(host) is the unique index → ON CONFLICT target. psql
|
|
# isn't on PATH in hermit → exec into the buzz-postgres container.
|
|
env:
|
|
PGHOST: localhost
|
|
PGPORT: "5432"
|
|
PGUSER: buzz
|
|
PGPASSWORD: buzz_dev
|
|
PGDATABASE: buzz
|
|
# Use the already-running docker postgres for desired-state planning instead of
|
|
# downloading an embedded Postgres from Maven Central (transient-fetch flake source).
|
|
PGSCHEMA_PLAN_HOST: localhost
|
|
PGSCHEMA_PLAN_PORT: "5432"
|
|
PGSCHEMA_PLAN_DB: buzz
|
|
PGSCHEMA_PLAN_USER: buzz
|
|
PGSCHEMA_PLAN_PASSWORD: buzz_dev
|
|
run: |
|
|
./bin/pgschema apply --file schema/schema.sql --auto-approve
|
|
docker exec -i -e PGPASSWORD=buzz_dev buzz-postgres \
|
|
psql -U buzz -d buzz -v ON_ERROR_STOP=1 < scripts/attach-schema-partitions.sql
|
|
docker exec -e PGPASSWORD=buzz_dev buzz-postgres \
|
|
psql -U buzz -d buzz -qtA -c "
|
|
INSERT INTO communities (id, host)
|
|
VALUES ('00000000-0000-4000-8000-00000000c0de', 'localhost:3000')
|
|
ON CONFLICT (lower(host)) DO NOTHING
|
|
;"
|
|
- name: Start relay
|
|
run: |
|
|
chmod +x ./target/ci/buzz-relay
|
|
nohup env \
|
|
DATABASE_URL="postgres://buzz:${BUZZ_TEST_POSTGRES_PASSWORD}@localhost:5432/buzz" \
|
|
REDIS_URL=redis://localhost:6379 \
|
|
RELAY_URL=ws://localhost:3000 \
|
|
BUZZ_BIND_ADDR=0.0.0.0:3000 \
|
|
BUZZ_REQUIRE_AUTH_TOKEN=false \
|
|
BUZZ_RECONCILE_CHANNELS=true \
|
|
BUZZ_RATE_LIMIT_HUMAN_MESSAGES_PER_MIN=100000 \
|
|
BUZZ_RATE_LIMIT_HUMAN_API_CALLS_PER_MIN=100000 \
|
|
BUZZ_RATE_LIMIT_HUMAN_WS_EVENTS_PER_SEC=10000 \
|
|
BUZZ_GIT_PROBE_WRITERS=8 \
|
|
SPROUT_REMINDER_SCHEDULER_INTERVAL_SECS=1 \
|
|
./target/ci/buzz-relay > /tmp/buzz-relay.log 2>&1 &
|
|
echo $! > /tmp/buzz-relay.pid
|
|
for attempt in $(seq 1 60); do
|
|
if ! kill -0 "$(cat /tmp/buzz-relay.pid)" 2>/dev/null; then
|
|
cat /tmp/buzz-relay.log
|
|
exit 1
|
|
fi
|
|
status_code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/_readiness || true)
|
|
if [ "${status_code}" = "200" ]; then
|
|
exit 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
cat /tmp/buzz-relay.log
|
|
exit 1
|
|
- name: Seed desktop e2e data
|
|
run: bash scripts/setup-desktop-test-data.sh
|
|
- name: Desktop relay-backed e2e
|
|
run: cd desktop && pnpm exec playwright test --project=integration --shard=${{ matrix.shard }}/2
|
|
- name: Summarize flaky tests
|
|
if: ${{ !cancelled() }}
|
|
run: node scripts/summarize-flaky-tests.mjs playwright-report.json "Desktop E2E Integration (${{ matrix.shard }}/2)"
|
|
working-directory: desktop
|
|
- name: Upload desktop integration artifacts
|
|
if: ${{ !cancelled() }}
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: desktop-e2e-integration-artifacts-${{ matrix.shard }}
|
|
path: |
|
|
desktop/playwright-report
|
|
desktop/playwright-report.json
|
|
desktop/test-results
|
|
/tmp/buzz-relay.log
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|
|
- name: Save pnpm store cache
|
|
if: github.event_name == 'push'
|
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
|
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
|
|
desktop-e2e-integration:
|
|
name: Desktop E2E Integration
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
needs: [changes, desktop-e2e-integration-shard]
|
|
if: always() && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- name: Check integration shards
|
|
run: |
|
|
if [ "${{ needs.desktop-e2e-integration-shard.result }}" != "success" ]; then
|
|
echo "Desktop E2E Integration shards finished with: ${{ needs.desktop-e2e-integration-shard.result }}"
|
|
exit 1
|
|
fi
|
|
echo "Desktop E2E Integration shards passed"
|
|
|
|
backend-integration:
|
|
name: Backend Integration (relay e2e)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
needs: [changes, desktop-e2e-relay]
|
|
if: github.event_name == 'push' || needs.changes.outputs.rust == 'true'
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- name: Install cargo-nextest
|
|
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
|
with:
|
|
tool: cargo-nextest@0.9.136
|
|
- name: Start integration services
|
|
run: |
|
|
for attempt in 1 2 3; do
|
|
if docker compose up -d postgres redis minio minio-init; then
|
|
break
|
|
fi
|
|
if [ "$attempt" -eq 3 ]; then
|
|
echo "docker compose up failed after 3 attempts" >&2
|
|
exit 1
|
|
fi
|
|
echo "docker compose up failed (attempt $attempt), retrying in $((attempt * 5))s..." >&2
|
|
sleep $((attempt * 5))
|
|
done
|
|
- name: Wait for integration services
|
|
run: |
|
|
wait_healthy() {
|
|
local service="$1"
|
|
local container="$2"
|
|
for attempt in $(seq 1 60); do
|
|
status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "not_found")
|
|
if [ "${status}" = "healthy" ]; then
|
|
echo "${service} is healthy"
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
docker logs "${container}" || true
|
|
return 1
|
|
}
|
|
wait_healthy "Postgres" "buzz-postgres"
|
|
wait_healthy "Redis" "buzz-redis"
|
|
wait_healthy "MinIO" "buzz-minio"
|
|
- name: Download relay artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: desktop-e2e-relay
|
|
path: target/ci
|
|
- name: Apply schema and seed deployment community
|
|
# MT: the relay resolves each request's tenant from the communities host
|
|
# map and fails closed on an unmapped host. The reminder scheduler binds
|
|
# the deployment community ONCE at boot and exits permanently on an
|
|
# unmapped host (no retry, unlike the channel reconciler), so the
|
|
# 'localhost:3000' community MUST exist before the relay starts — seeding
|
|
# after boot leaves the scheduler dead. The relay migrates at boot via
|
|
# BUZZ_AUTO_MIGRATE, but that's too late for the pre-boot seed, so apply
|
|
# the schema here first (then drop AUTO_MIGRATE below). lower(host) is the
|
|
# unique index → ON CONFLICT target. psql isn't on PATH in hermit → exec
|
|
# into the buzz-postgres container.
|
|
env:
|
|
PGHOST: localhost
|
|
PGPORT: "5432"
|
|
PGUSER: buzz
|
|
PGPASSWORD: buzz_dev
|
|
PGDATABASE: buzz
|
|
# Use the already-running docker postgres for desired-state planning instead of
|
|
# downloading an embedded Postgres from Maven Central (transient-fetch flake source).
|
|
PGSCHEMA_PLAN_HOST: localhost
|
|
PGSCHEMA_PLAN_PORT: "5432"
|
|
PGSCHEMA_PLAN_DB: buzz
|
|
PGSCHEMA_PLAN_USER: buzz
|
|
PGSCHEMA_PLAN_PASSWORD: buzz_dev
|
|
run: |
|
|
./bin/pgschema apply --file schema/schema.sql --auto-approve
|
|
docker exec -i -e PGPASSWORD=buzz_dev buzz-postgres \
|
|
psql -U buzz -d buzz -v ON_ERROR_STOP=1 < scripts/attach-schema-partitions.sql
|
|
docker exec -e PGPASSWORD=buzz_dev buzz-postgres \
|
|
psql -U buzz -d buzz -qtA -c "
|
|
INSERT INTO communities (id, host)
|
|
VALUES ('00000000-0000-4000-8000-00000000c0de', 'localhost:3000')
|
|
ON CONFLICT (lower(host)) DO NOTHING
|
|
;"
|
|
- name: Start relay
|
|
run: |
|
|
chmod +x ./target/ci/buzz-relay
|
|
nohup env \
|
|
DATABASE_URL="postgres://buzz:${BUZZ_TEST_POSTGRES_PASSWORD}@localhost:5432/buzz" \
|
|
REDIS_URL=redis://localhost:6379 \
|
|
RELAY_URL=ws://localhost:3000 \
|
|
BUZZ_BIND_ADDR=0.0.0.0:3000 \
|
|
BUZZ_REQUIRE_AUTH_TOKEN=false \
|
|
BUZZ_RECONCILE_CHANNELS=true \
|
|
BUZZ_GIT_PROBE_WRITERS=8 \
|
|
SPROUT_REMINDER_SCHEDULER_INTERVAL_SECS=1 \
|
|
./target/ci/buzz-relay > /tmp/buzz-relay.log 2>&1 &
|
|
echo $! > /tmp/buzz-relay.pid
|
|
for attempt in $(seq 1 60); do
|
|
if ! kill -0 "$(cat /tmp/buzz-relay.pid)" 2>/dev/null; then
|
|
cat /tmp/buzz-relay.log
|
|
exit 1
|
|
fi
|
|
status_code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/_readiness || true)
|
|
if [ "${status_code}" = "200" ]; then
|
|
exit 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
cat /tmp/buzz-relay.log
|
|
exit 1
|
|
- name: Invite security tests
|
|
run: |
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E '(package(buzz-db) and test(/relay_invite::tests/)) or (package(buzz-relay) and test(/api::invites::tests/))' \
|
|
--run-ignored ignored-only
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- name: Workspace profile (kind:9033) gate tests
|
|
# Call-site integration for the 9033 authorization gate: open relay
|
|
# rosterless/steward transitions and the closed-relay admin/owner rule,
|
|
# against real Postgres. #[ignore]d in the default suite, selected
|
|
# explicitly here — see handlers::relay_admin::tests.
|
|
run: |
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E 'package(buzz-relay) and test(/handlers::relay_admin::tests/)' \
|
|
--run-ignored ignored-only
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- name: NIP-ER reminder e2e
|
|
# Feature e2e for NIP-ER (Event Reminders, kind:30300): write-path
|
|
# validation, author-only read filtering, and scheduler delivery against
|
|
# a live relay. The schema-drift / migration-version guarantee is owned
|
|
# by the buzz-db migration.rs unit tests, not this suite.
|
|
run: |
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E 'binary(e2e_event_reminder)' \
|
|
--run-ignored ignored-only
|
|
env:
|
|
RELAY_URL: ws://localhost:3000
|
|
- name: NIP-MP coordinate deletion guard
|
|
# Verifies the never-delete-newer invariant of soft_delete_by_coordinate:
|
|
# a stale tombstone (created_at earlier than the live head) spares that
|
|
# head, and an equal-timestamp tombstone deletes it.
|
|
run: |
|
|
cargo nextest run \
|
|
--archive-file target/ci/backend-integration-tests.tar.zst \
|
|
-E 'package(buzz-db) and test(coordinate_delete_spares_head_newer_than_the_deletion)' \
|
|
--run-ignored ignored-only
|
|
env:
|
|
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
|
|
- name: Upload relay log
|
|
if: failure()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: backend-integration-relay-log
|
|
path: /tmp/buzz-relay.log
|
|
if-no-files-found: ignore
|
|
|
|
relay-e2e:
|
|
name: Relay E2E
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
needs: [changes, desktop-e2e-relay]
|
|
if: github.event_name == 'push' || needs.changes.outputs.rust == 'true'
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
with:
|
|
save-if: ${{ github.event_name != 'pull_request' }}
|
|
# Reuse the relay + git-credential-nostr built by Desktop E2E Relay
|
|
# instead of compiling them a second time.
|
|
- name: Download relay binary
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: desktop-e2e-relay
|
|
path: target/ci
|
|
- name: Start relay
|
|
run: |
|
|
chmod +x ./target/ci/buzz-relay ./target/ci/git-credential-nostr
|
|
./scripts/start-relay-for-tests.sh --no-build
|
|
- name: Relay E2E tests
|
|
run: |
|
|
cargo test -p buzz-test-client --test e2e_persona --test e2e_team_catalog --test e2e_nostr_interop --test e2e_project -- --ignored --nocapture
|
|
cargo test -p buzz-test-client --test e2e_relay invite -- --ignored --nocapture
|
|
cargo test -p buzz-test-client --test e2e_relay nip43_membership_snapshots_are_rejected -- --ignored --nocapture
|
|
env:
|
|
RELAY_URL: ws://localhost:3000
|
|
GIT_CREDENTIAL_NOSTR_BIN: ${{ github.workspace }}/target/ci/git-credential-nostr
|
|
- name: Upload relay logs
|
|
if: failure()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: relay-e2e-artifacts
|
|
path: /tmp/buzz-relay.log
|
|
if-no-files-found: ignore
|
|
|
|
web:
|
|
name: Web
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.web == 'true'
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
fetch-depth: 2
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- name: Get pnpm store directory
|
|
id: pnpm-cache
|
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
|
- name: Restore pnpm store cache
|
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
|
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
restore-keys: pnpm-${{ runner.os }}-
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
- name: Web lint and format
|
|
run: just web-check
|
|
- name: Web build
|
|
run: just web-build
|
|
- name: Save pnpm store cache
|
|
if: github.event_name == 'push'
|
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
|
key: pnpm-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
|
|
mobile:
|
|
name: Mobile
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.mobile == 'true'
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
fetch-depth: 2
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- name: Compute Hermit cache key
|
|
id: hermit-bin-hash
|
|
run: |
|
|
hash="$(find ./bin ! -type d | sort | xargs openssl sha256 | openssl sha256 -r | cut -d' ' -f1)"
|
|
echo "hash=$hash" >> "$GITHUB_OUTPUT"
|
|
- name: Restore Hermit package cache
|
|
id: hermit-cache
|
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ~/.cache/hermit/pkg
|
|
key: ${{ runner.os }}-hermit-cache-${{ steps.hermit-bin-hash.outputs.hash }}
|
|
restore-keys: ${{ runner.os }}-hermit-cache-
|
|
- name: Prime Flutter SDK
|
|
run: flutter --version
|
|
- name: Save Hermit package cache
|
|
if: always() && steps.hermit-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
continue-on-error: true
|
|
with:
|
|
path: ~/.cache/hermit/pkg
|
|
key: ${{ runner.os }}-hermit-cache-${{ steps.hermit-bin-hash.outputs.hash }}
|
|
- name: Restore pub cache
|
|
id: pub-cache
|
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ~/.pub-cache
|
|
key: pub-${{ runner.os }}-${{ hashFiles('mobile/pubspec.lock') }}
|
|
restore-keys: pub-${{ runner.os }}-
|
|
- name: Install dependencies
|
|
run: cd mobile && flutter pub get
|
|
- name: Save pub cache
|
|
if: always() && steps.pub-cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
continue-on-error: true
|
|
with:
|
|
path: ~/.pub-cache
|
|
key: pub-${{ runner.os }}-${{ hashFiles('mobile/pubspec.lock') }}
|
|
- name: File size ratchet
|
|
run: node mobile/scripts/check-file-sizes.mjs
|
|
- name: Format check
|
|
run: cd mobile && dart format --output=none --set-exit-if-changed .
|
|
- name: Analyze
|
|
run: cd mobile && flutter analyze
|
|
- name: Test
|
|
run: cd mobile && flutter test
|
|
- name: Build Android debug APK
|
|
run: just mobile-build-android
|
|
|
|
security:
|
|
name: Security
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.rust == 'true'
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- name: Dependency policy
|
|
run: cargo-deny check
|
|
|
|
dead-token-guard:
|
|
name: Dead Token Reference Guard
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- name: Check for dead API token references in client code
|
|
run: |
|
|
# Fail if dead API token patterns reappear in desktop, mobile, docs, or config.
|
|
# Relay crates are excluded — they still use token auth internally.
|
|
PATTERNS='TokenScope|MintTokenResponse|hasApiToken|spr_tok_'
|
|
PATHS='desktop/src/ desktop/tests/ mobile/test/ mobile/lib/ .env.example'
|
|
EXCLUDES='--exclude-dir=node_modules --exclude-dir=.dart_tool'
|
|
if grep -rn $EXCLUDES -E "$PATTERNS" $PATHS 2>/dev/null; then
|
|
echo "::error::Dead API token references found in client code. See above."
|
|
exit 1
|
|
fi
|
|
echo "No dead token references found."
|
|
|
|
server-cross-compile:
|
|
name: Server Cross-Compile
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.rust == 'true'
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
target:
|
|
- x86_64-unknown-linux-musl
|
|
- aarch64-unknown-linux-musl
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
with:
|
|
key: cross-${{ matrix.target }}
|
|
save-if: ${{ github.event_name != 'pull_request' }}
|
|
- name: Install cross
|
|
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
|
with:
|
|
tool: cross@0.2.5
|
|
- name: Build server binaries
|
|
env:
|
|
TARGET: ${{ matrix.target }}
|
|
# PRs: compile + build-script gate only (no codegen/link). Main: full link gate.
|
|
CARGO_CMD: ${{ github.event_name == 'pull_request' && 'check' || 'build' }}
|
|
run: |
|
|
cross "$CARGO_CMD" --release --target "$TARGET" \
|
|
-p buzz-relay \
|
|
-p buzz-acp \
|
|
-p buzz-agent \
|
|
-p buzz-dev-mcp \
|
|
-p git-credential-nostr \
|
|
-p git-sign-nostr
|
|
|
|
windows-rust:
|
|
name: Windows Rust (x86_64-pc-windows-msvc)
|
|
runs-on: windows-latest
|
|
# Windows runners are slow and this compiles the workspace + Tauri crate
|
|
# cold across four steps; budget generously.
|
|
timeout-minutes: 45
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true'
|
|
permissions:
|
|
contents: read
|
|
env:
|
|
TARGET: x86_64-pc-windows-msvc
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
# MSVC needs windows.h (aws-lc-sys et al.), so this runs on a real Windows
|
|
# runner — hermit, used by the Linux jobs, does not provide MSVC. The
|
|
# toolchain (1.95.0 + clippy via profile = default) comes from the
|
|
# repo-root rust-toolchain.toml, which the runner's preinstalled rustup
|
|
# honors on demand; the host triple already is x86_64-pc-windows-msvc.
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
with:
|
|
workspaces: |
|
|
.
|
|
desktop/src-tauri
|
|
key: windows-msvc
|
|
save-if: ${{ github.event_name != 'pull_request' }}
|
|
# Tauri validates externalBin at compile time, so the Tauri-crate steps
|
|
# below fail without these stubs. Mirrors scripts/bundle-sidecars.sh's
|
|
# Windows naming (binaries/<bin>-<triple>.exe); empty files suffice for a
|
|
# type-check since nothing executes them.
|
|
- name: Create sidecar placeholders
|
|
shell: bash
|
|
run: |
|
|
mkdir -p desktop/src-tauri/binaries
|
|
for bin in buzz-acp buzz-agent buzz-dev-mcp git-credential-nostr buzz; do
|
|
touch "desktop/src-tauri/binaries/${bin}-${TARGET}.exe"
|
|
done
|
|
- name: Clippy (workspace)
|
|
run: cargo clippy --workspace --all-targets --target $env:TARGET -- -D warnings
|
|
- name: Check (workspace)
|
|
run: cargo check --workspace --all-targets --target $env:TARGET
|
|
- name: Test (buzz-dev-mcp)
|
|
# The Windows-only bash resolver lives in buzz-dev-mcp; its unit tests
|
|
# only gate if this crate is tested ON Windows.
|
|
# Serial: windows_resolver_tests mutate process-global env
|
|
# (BUZZ_SHELL/GIT_BASH/SystemRoot) that SharedState::new reads.
|
|
run: cargo test -p buzz-dev-mcp --target $env:TARGET -- --test-threads=1
|
|
# Smoke-test the new host-prereq contract: Git for Windows (which provides
|
|
# bash) is available on the runner, a shell command round-trips, and bash
|
|
# does NOT resolve from System32 (so WSL's launcher is never picked up).
|
|
# windows-latest runners have Git for Windows pre-installed; the unit tests
|
|
# above exercise the MCP resolver itself. This step verifies the host env.
|
|
- name: Smoke-test host Git Bash prereq (host env check)
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
# Git for Windows ships bash.exe under its bin/ directory; confirm it
|
|
# resolves from the standard location the runtime resolver probes first.
|
|
bash_path=$(command -v bash 2>/dev/null || true)
|
|
[[ -n "$bash_path" ]] || { echo "ERROR: bash not found on PATH — host Git for Windows missing" >&2; exit 1; }
|
|
echo "Resolved bash: $bash_path"
|
|
[[ "$bash_path" != *System32* ]] || { echo "ERROR: resolved bash is WSL's System32 launcher" >&2; exit 1; }
|
|
|
|
# Run a basic pipeline through the resolved bash (same invocation the
|
|
# agent uses: bash -c '...').
|
|
out=$(bash -c 'echo hello | tr a-z A-Z')
|
|
[[ "$out" == "HELLO" ]] || { echo "bash pipeline failed: got '$out'" >&2; exit 1; }
|
|
|
|
# Confirm git itself works — agents run git commands frequently.
|
|
git --version
|
|
repo=$(mktemp -d)
|
|
cd "$repo"
|
|
git init -q
|
|
git -c user.name=ci -c user.email=ci@example.com commit -q --allow-empty -m smoke
|
|
git log -1 --format=%s | grep -qx smoke
|
|
echo "Host bash resolved and functional; git commit round-trip passed"
|
|
- name: Check (Tauri crate)
|
|
run: cargo check --manifest-path desktop/src-tauri/Cargo.toml --target $env:TARGET
|
|
env:
|
|
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
|
|
- name: Test (Tauri crate)
|
|
run: cargo test --manifest-path desktop/src-tauri/Cargo.toml --target $env:TARGET
|
|
env:
|
|
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
|
|
|
|
desktop-build-macos:
|
|
name: Desktop Build (macOS)
|
|
runs-on: macos-latest
|
|
timeout-minutes: 45
|
|
needs: [changes]
|
|
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
|
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
|
with:
|
|
workspaces: desktop/src-tauri
|
|
save-if: ${{ github.event_name != 'pull_request' }}
|
|
- name: Install desktop dependencies
|
|
run: just desktop-install-ci
|
|
- name: Create sidecar placeholders
|
|
run: |
|
|
TARGET=$(rustc -vV | sed -n 's|host: ||p')
|
|
mkdir -p desktop/src-tauri/binaries
|
|
touch "desktop/src-tauri/binaries/buzz-acp-$TARGET"
|
|
touch "desktop/src-tauri/binaries/buzz-agent-$TARGET"
|
|
touch "desktop/src-tauri/binaries/buzz-backend-kubernetes-$TARGET"
|
|
touch "desktop/src-tauri/binaries/buzz-dev-mcp-$TARGET"
|
|
touch "desktop/src-tauri/binaries/git-credential-nostr-$TARGET"
|
|
touch "desktop/src-tauri/binaries/buzz-$TARGET"
|
|
# Mesh rev is derived from Cargo.lock so a dependency bump needs no
|
|
# lockstep edit here; the cache key tracks it automatically.
|
|
- name: Resolve mesh-llm rev
|
|
id: mesh_rev
|
|
run: |
|
|
set -euo pipefail
|
|
REV=$(python3 -c 'import tomllib; d=tomllib.load(open("Cargo.lock", "rb")); p=next(p for p in d["package"] if p["name"] == "mesh-llm-sdk"); print(p["source"].rsplit("#", 1)[1])')
|
|
[[ -n "$REV" ]] || { echo "::error::could not resolve mesh-llm rev from Cargo.lock"; exit 1; }
|
|
echo "rev=$REV" >> "$GITHUB_OUTPUT"
|
|
echo "short=${REV:0:7}" >> "$GITHUB_OUTPUT"
|
|
- name: Restore mesh llama build cache
|
|
id: llama_cache
|
|
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ github.workspace }}/.cache/mesh-llama
|
|
key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }}
|
|
- name: Build mesh llama native libraries
|
|
if: steps.llama_cache.outputs.cache-hit != 'true'
|
|
env:
|
|
MESH_REV_SHORT: ${{ steps.mesh_rev.outputs.short }}
|
|
run: |
|
|
set -euo pipefail
|
|
cargo fetch --manifest-path desktop/src-tauri/Cargo.toml
|
|
SHORT="$MESH_REV_SHORT"
|
|
MESH_ROOT=$(find "${CARGO_HOME:-$HOME/.cargo}/git/checkouts" -path "*/$SHORT" -type d -name "$SHORT" | head -1)
|
|
if [[ -z "$MESH_ROOT" ]]; then
|
|
echo "::error::mesh-llm checkout for $SHORT not found after cargo fetch"
|
|
exit 1
|
|
fi
|
|
export LLAMA_STAGE_BACKEND=metal
|
|
export LLAMA_STAGE_BUILD_DIR="$GITHUB_WORKSPACE/.cache/mesh-llama/build-stage-abi-metal"
|
|
export CMAKE_OSX_DEPLOYMENT_TARGET=10.15
|
|
"$MESH_ROOT/scripts/prepare-llama.sh" pinned
|
|
"$MESH_ROOT/scripts/build-llama.sh" -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15
|
|
- name: Save mesh llama build cache
|
|
if: steps.llama_cache.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
|
|
with:
|
|
path: ${{ github.workspace }}/.cache/mesh-llama
|
|
key: mesh-llama-${{ runner.os }}-metal-${{ steps.mesh_rev.outputs.rev }}
|
|
- name: Build Tauri app
|
|
run: cd desktop && pnpm tauri build
|
|
env:
|
|
CMAKE_POLICY_VERSION_MINIMUM: "3.5"
|
|
MACOSX_DEPLOYMENT_TARGET: "10.15"
|
|
CMAKE_OSX_DEPLOYMENT_TARGET: "10.15"
|
|
LLAMA_STAGE_BACKEND: metal
|
|
LLAMA_STAGE_BUILD_DIR: ${{ github.workspace }}/.cache/mesh-llama/build-stage-abi-metal
|
|
SKIPPY_LLAMA_AUTO_BUILD: "0"
|