Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions .github/workflows/nuncio-crew-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,193 @@ jobs:
if-no-files-found: ignore
retention-days: 7

# Advisory only (D-047 / D-032 precedent). continue-on-error keeps the signal
# visible without blocking merges. Do not add this job to `gate.needs` or
# JOB_RELEVANCE — relay flakiness and inherited Buzz integration coverage must
# not red-wall Crew PRs. Two shards match upstream wall-clock (~6–7m each with
# a warm rust cache); a single serial job would exceed ~12m once the full
# integration suite is included. Each shard builds its own buzz-relay so the
# lane stays self-contained and never becomes a hard dependency of Gate.
desktop-e2e-integration:
name: Desktop E2E Integration (${{ matrix.shard }}/2)
needs: changes
if: needs.changes.outputs.desktop == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
continue-on-error: true
strategy:
fail-fast: false
matrix:
shard: [1, 2]
env:
BUZZ_TEST_POSTGRES_PASSWORD: buzz_dev
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1
- name: Restore Rust cache
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: nuncio-desktop-e2e-integration
save-if: ${{ github.event_name != 'pull_request' }}
- 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 _ 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: Build buzz-relay
run: cargo build --profile ci -p buzz-relay
- 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. 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.
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_S3_ENDPOINT=http://localhost:9000 \
BUZZ_S3_ACCESS_KEY=buzz_dev \
BUZZ_S3_SECRET_KEY=buzz_dev_secret \
BUZZ_S3_BUCKET=buzz-media \
BUZZ_S3_REGION=us-east-1 \
BUZZ_S3_ADDRESSING_STYLE=path \
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 _ 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

gate:
name: NuncioCrew Gate
if: ${{ always() }}
Expand Down
1 change: 0 additions & 1 deletion desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ export default defineConfig({
"**/profile.spec.ts",
"**/sidebar.spec.ts",
"**/sidebar-relay-card.spec.ts",
"**/tokens.spec.ts",
"**/persona-env-vars.spec.ts",
"**/persona-sync.spec.ts",
"**/team-snapshot.spec.ts",
Expand Down
47 changes: 43 additions & 4 deletions desktop/src/testing/nuncio-crew-ci-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,15 @@ test("desktop smoke e2e runs on PRs as an advisory signal until flakes are triag
const ci = workflow("nuncio-crew-ci.yml");
const smokeStart = ci.indexOf("desktop-smoke-e2e:");
assert.ok(smokeStart > 0, "desktop-smoke-e2e job must exist");
const nextJob = ci.indexOf("\n desktop-e2e-integration:", smokeStart);
const gateStart = ci.indexOf("\n gate:", smokeStart);
const smoke = ci.slice(
smokeStart,
gateStart > smokeStart ? gateStart : undefined,
);
const smokeEnd =
nextJob > smokeStart
? nextJob
: gateStart > smokeStart
? gateStart
: undefined;
const smoke = ci.slice(smokeStart, smokeEnd);

assert.match(smoke, /name:\s*Desktop Smoke E2E/);
assert.match(smoke, /continue-on-error:\s*true/);
Expand All @@ -155,6 +159,41 @@ test("desktop smoke e2e runs on PRs as an advisory signal until flakes are triag
assert.doesNotMatch(gateHelper, /desktop-smoke-e2e/);
});

test("desktop e2e integration runs on PRs as an advisory relay-backed lane", () => {
const ci = workflow("nuncio-crew-ci.yml");
const integStart = ci.indexOf("desktop-e2e-integration:");
assert.ok(integStart > 0, "desktop-e2e-integration job must exist");
const gateStart = ci.indexOf("\n gate:", integStart);
const integ = ci.slice(
integStart,
gateStart > integStart ? gateStart : undefined,
);

assert.match(integ, /name:\s*Desktop E2E Integration/);
assert.match(integ, /continue-on-error:\s*true/);
assert.match(integ, /shard:\s*\[1,\s*2\]/);
assert.match(integ, /docker compose up -d postgres redis minio minio-init/);
assert.match(integ, /cargo build --profile ci -p buzz-relay/);
assert.match(integ, /BUZZ_RECONCILE_CHANNELS=true/);
assert.match(integ, /setup-desktop-test-data\.sh/);
assert.match(integ, /pnpm -C desktop build:e2e/);
assert.match(
integ,
/playwright test --project=integration --shard=\$\{\{ matrix\.shard \}\}\/2/,
);
assert.match(integ, /needs\.changes\.outputs\.desktop == 'true'/);
// Advisory (D-047): must not be registered in the merge gate.
assert.doesNotMatch(ci, /needs\.desktop-e2e-integration\.result/);
const gateHelper = readFileSync(gateHelperPath, "utf8");
assert.doesNotMatch(gateHelper, /desktop-e2e-integration/);
// Gate needs list must stay free of the integration job id.
const gateNeeds = ci.match(
/name:\s*NuncioCrew Gate[\s\S]*?needs:\s*\[([^\]]+)\]/,
);
assert.ok(gateNeeds, "gate needs list must be present");
assert.doesNotMatch(gateNeeds[1], /desktop-e2e-integration/);
});

test("upstream compatibility is explicit and manual", () => {
const upstream = workflow("nuncio-crew-upstream-sync.yml");
const trigger = upstream.slice(
Expand Down
11 changes: 7 additions & 4 deletions docs/crew/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ The gate always appears. It requires `CI Policy` and accepts a deliberately
skipped conditional job only when the path classifier says that surface is
unchanged.

A green `NuncioCrew Gate` is not evidence that the Desktop Smoke E2E suite
passed.
A green `NuncioCrew Gate` is not evidence that the Desktop Smoke E2E suite or
the Desktop E2E Integration suite passed.

This advisory posture is recorded in [`verification/0007`](verification/0007-gate-e2e-shard-relationship.md)
and D-032; revisit making the shards required once #109 and #110 are closed.
Smoke advisory posture is recorded in
[`verification/0007`](verification/0007-gate-e2e-shard-relationship.md) and
D-032. Integration advisory posture is recorded in D-047 (#147); revisit making
either lane required only by an explicit founder decision.

| Job | Runs when | Proves |
| --- | --- | --- |
Expand All @@ -27,6 +29,7 @@ and D-032; revisit making the shards required once #109 and #110 are closed.
| `macOS ARM Package` | Same desktop boundary as Desktop Fast | Unsigned `aarch64-apple-darwin` Tauri package with Nuncio identity |
| `Project Relay` | Project, relay, schema, or Nostr paths change | Kind `30617` local-path lifecycle against an isolated real relay |
| `Desktop Smoke E2E` | Desktop paths change | **nothing that blocks merge** — advisory (`continue-on-error`), excluded from the gate by design (#36/#37) |
| `Desktop E2E Integration` | Same desktop boundary as Desktop Smoke E2E | **nothing that blocks merge** — advisory (`continue-on-error`), two shards, real relay + Postgres/Redis/MinIO, `playwright --project=integration` (D-047 / #147). Includes the Crew-owned `evidence-reactions-relay` proof and inherited Buzz relay-backed specs |

The PR package uses placeholder sidecars only to satisfy Tauri's packaging
shape. The manual release workflow builds real sidecars, signs the app,
Expand Down
31 changes: 31 additions & 0 deletions docs/crew/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -839,3 +839,34 @@ classifications.

This decision changes no reaction semantics, event kinds, or evidence tag
schema. D-036 remains authoritative for evidence tags and reaction behavior.

## D-047 — Keep Desktop E2E Integration advisory until Crew owns more of the lane

- **Status:** Accepted
- **Date:** 2026-08-12
- **Issue:** #147
- **Precedent:** D-032 (Desktop Smoke E2E advisory)

The founder decided that the relay-backed Desktop E2E Integration lane
(`playwright test --project=integration` in `nuncio-crew-ci.yml`) is **advisory**:
`continue-on-error: true`, excluded from `NuncioCrew Gate` `needs`, and absent
from `JOB_RELEVANCE` in `check-nuncio-crew-ci-results.mjs`. A green Gate is not
evidence that integration passed.

Rationale matches D-032's trade-off, applied to a heavier lane:

1. Only one configured integration spec is Crew-specific today
(`evidence-reactions-relay.spec.ts` from #133 / PR #146); the rest are
inherited Buzz coverage that Crew deliberately stopped requiring when the
upstream `CI` workflow was disabled.
2. The lane needs Postgres, Redis, MinIO, a built `buzz-relay`, schema and
community seed, `scripts/setup-desktop-test-data.sh`, Playwright, and
`BUZZ_RECONCILE_CHANNELS=true`. Relay and service flakiness must not block
every desktop merge.
3. Restoring the lane as advisory recovers continuous signal (including the
D-042 headless click → real kind-7 proof) without red-walling merges.

Promotion path: making the lane required is an **explicit future founder
decision**, not an automatic follow-up when a single spec turns green. Revisit
once Crew owns a meaningful share of the integration suite and the lane's
failure rate on `main` is attributed and stable enough to gate on.
4 changes: 4 additions & 0 deletions docs/crew/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ Out of scope for this slice:
baseline bump while D-022 governs Crew-authored growth.
- Automatic checks: desktop fast gate, unsigned macOS ARM64 package, and a
path-filtered real-relay Project contract.
- Advisory (non-blocking) on desktop path changes: Desktop Smoke E2E (D-032)
and Desktop E2E Integration — real relay + Postgres/Redis/MinIO,
`playwright --project=integration`, two shards (D-047 / #147). A green Gate
is not evidence either advisory lane passed.
- Web, mobile, Windows, Linux distribution, Docker publishing, Helm, Sprig,
and optional mesh-llm builds are outside automatic Crew CI.
- Core root and desktop Tauri Rust format, lint, unit, and dependency-policy
Expand Down
Loading
Loading