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
90 changes: 56 additions & 34 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ jobs:
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 }}
rust: ${{ steps.selection.outputs.rust }}
desktop: ${{ steps.selection.outputs.desktop }}
desktop-rust: ${{ steps.selection.outputs.desktop-rust }}
web: ${{ steps.selection.outputs.web }}
mobile: ${{ steps.selection.outputs.mobile }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
Expand All @@ -35,7 +35,11 @@ jobs:
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
with:
token: ''
# PR files come from GitHub's PR diff. A tokenless comparison uses the
# checked-out merge commit and can include newer, unrelated base changes.
token: ${{ github.token }}
Comment thread
brow marked this conversation as resolved.
Comment thread
brow marked this conversation as resolved.
# Root and docs/ Markdown do not select runtime suites. Markdown under
# runtime directories remains covered by the directory filters below.
filters: |
rust:
- 'crates/**'
Expand Down Expand Up @@ -84,8 +88,13 @@ jobs:
- '.github/workflows/mobile-release-candidate.yml'
- '.github/workflows/ci.yml'
- '.github/workflows/_ci-*.yml'
- name: Select runtime suites conservatively at the PR file-list limit
id: selection
env:
FILTER_OUTPUTS: ${{ toJSON(steps.filter.outputs) }}
run: node scripts/ci-runtime-selection.mjs
- name: Validate PostgreSQL test discovery
if: github.event_name == 'push' || steps.filter.outputs.rust == 'true'
if: github.event_name == 'push' || steps.selection.outputs.rust == 'true'
run: |
scripts/test-postgres-test-discovery.sh
scripts/test-postgres-test-wrapper.sh
Expand Down Expand Up @@ -116,7 +125,7 @@ jobs:
- name: CI required-context isolation contract
run: scripts/test-ci-required-context-isolation.sh
- name: CI path selection regression tests
run: node --test scripts/ci-selection.test.mjs
run: node --test scripts/ci-selection.test.mjs scripts/ci-required-gates.test.mjs
- name: File size policy
run: just file-size-check

Expand Down Expand Up @@ -244,169 +253,182 @@ jobs:

rust-lint:
name: Rust Lint
if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true')
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true')))
needs: [changes, rust]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Rust Lint result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.rust.outputs.rust_lint_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

unit-tests:
name: Unit Tests
if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true')
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.rust == 'true')))
needs: [changes, rust]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Unit Tests result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.rust.outputs.unit_tests_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

windows-rust:
name: Windows Rust (x86_64-pc-windows-msvc)
if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true')
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop-rust == 'true')))
needs: [changes, rust]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Windows Rust result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.rust.outputs.windows_rust_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

desktop:
name: Desktop
if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')))
needs: [changes, desktop-domain]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Desktop result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.desktop-domain.outputs.desktop_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

desktop-build-macos:
name: Desktop Build (macOS)
if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')))
needs: [changes, desktop-macos-domain]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Desktop Build (macOS) result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.desktop-macos-domain.outputs.desktop_macos_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

desktop-e2e-relay:
name: Desktop E2E Relay
if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')))
needs: [changes, relay-artifacts-domain]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Desktop E2E Relay result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.relay-artifacts-domain.outputs.desktop_e2e_relay_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

desktop-e2e-integration:
name: Desktop E2E Integration
if: always() && needs.changes.result == 'success' && needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')
if: always() && (needs.changes.result != 'success' || (needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')))
needs: [changes, relay-artifacts-domain, relay-domain]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Desktop E2E Integration result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.relay-domain.outputs.desktop_e2e_integration_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

backend-integration:
name: Backend Integration (relay e2e)
if: always() && needs.changes.result == 'success' && needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true')
if: always() && (needs.changes.result != 'success' || (needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true')))
needs: [changes, relay-artifacts-domain, relay-domain]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Backend Integration result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.relay-domain.outputs.backend_integration_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

postgres-tests:
name: PostgreSQL Tests
if: always() && needs.changes.result == 'success' && needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true')
if: always() && (needs.changes.result != 'success' || (needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true')))
needs: [changes, relay-artifacts-domain, postgres-domain]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check PostgreSQL Tests result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.postgres-domain.outputs.postgres_tests_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

relay-e2e:
name: Relay E2E
if: always() && needs.changes.result == 'success' && needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true')
if: always() && (needs.changes.result != 'success' || (needs.relay-artifacts-domain.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true')))
needs: [changes, relay-artifacts-domain, relay-domain]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Relay E2E result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.relay-domain.outputs.relay_e2e_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

web:
name: Web
if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.web == 'true')
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.web == 'true')))
needs: [changes, clients]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Web result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.clients.outputs.web_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

mobile:
name: Mobile
if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.mobile == 'true')
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.mobile == 'true')))
needs: [changes, clients]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Mobile result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.clients.outputs.mobile_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success

security:
name: Security
if: always() && needs.changes.result == 'success' && (github.event_name == 'push' || needs.changes.outputs.rust == 'true')
if: always() && (needs.changes.result != 'success' || ((github.event_name == 'push' || needs.changes.outputs.rust == 'true')))
needs: [changes, security-domain]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: Check Security result
env:
SELECTION_RESULT: ${{ needs.changes.result }}
RESULT: ${{ needs.security-domain.outputs.security_result }}
run: test "$RESULT" = success
run: test "$SELECTION_RESULT" = success && test "$RESULT" = success
12 changes: 10 additions & 2 deletions desktop/tests/e2e/persistent-agent-audience.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,15 +601,23 @@ test("the mention button opens settings and can undo an address", async ({
await expect(
composer.getByRole("button", { name: "Mention someone" }),
).toBeVisible();
await input.fill("");
await expect(
composer.getByTestId(`composer-address-lock-${AGENT_A}`),
).toHaveCount(0);
// Clear through editor transactions before selecting a manual mention.
await input.press("ControlOrMeta+A");
await input.press("Backspace");
await expect(input).toHaveText("");

await menu
.getByRole("button", { name: "Mention Morgarita", exact: true })
.click();
await expect(input).toHaveText("@Morgarita ");
// Explicitly opting out keeps subsequent mentions manual. The old avatar's
// exit animation must not be mistaken for a newly pinned recipient.
await expect(
composer.getByTestId(`composer-address-lock-${AGENT_A}`),
).toBeVisible();
).toHaveCount(0);

await input.type("later");
await input.press("Enter");
Expand Down
90 changes: 90 additions & 0 deletions scripts/ci-required-gates.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { test } from "node:test";
import { runInNewContext } from "node:vm";

const workflow = readFileSync(
new URL("../.github/workflows/ci.yml", import.meta.url),
"utf8",
);
const gates = [
"rust-lint",
"unit-tests",
"windows-rust",
"desktop",
"desktop-build-macos",
"desktop-e2e-relay",
"desktop-e2e-integration",
"backend-integration",
"postgres-tests",
"relay-e2e",
"web",
"mobile",
"security",
];
for (const gate of gates) {
const body = workflow.match(
new RegExp(`^ ${gate}:\\n([\\s\\S]*?)(?=^ [\\w-]+:|$(?![\\s\\S]))`, "m"),
)[1];
const condition = body.match(/^ {4}if: (.+)$/m)[1];
const command = body.match(/^ {8}run: (.+)$/m)[1];
function shouldRun(
selection,
selected = false,
event = "pull_request",
artifacts = "skipped",
) {
// These workflow conditions use only booleans, string equality and grouping.
// Evaluate the actual expression after substituting its GitHub context values.
assert.match(
condition,
/\balways\(\)/,
"Required wrapper must override GitHub implicit success()",
);
const expression = condition
.replace(/always\(\)/g, "true")
Comment thread
brow marked this conversation as resolved.
.replace(
/github\.event_name|needs\.[\w-]+\.(?:result|outputs\.[\w-]+)/g,
(key) => {
if (key === "github.event_name") return JSON.stringify(event);
if (key === "needs.changes.result") return JSON.stringify(selection);
if (key === "needs.relay-artifacts-domain.result")
return JSON.stringify(artifacts);
assert.match(key, /^needs\.changes\.outputs\./);
return JSON.stringify(selected ? "true" : "false");
},
);
return runInNewContext(expression, {}, { timeout: 100 });
}
function check(selection, result) {
assert.match(body, /SELECTION_RESULT: \$\{\{ needs.changes.result \}\}/);
assert.match(body, /RESULT: \$\{\{ needs\.[\w-]+\.outputs\.[\w_]+ \}\}/);
return spawnSync("bash", ["-c", command], {
env: { ...process.env, SELECTION_RESULT: selection, RESULT: result },
timeout: 1000,
}).status;
}
test(`${gate}: selector failures run and fail the required check`, () => {
for (const selection of ["failure", "cancelled", "skipped"]) {
for (const artifacts of ["skipped", "success"]) {
assert.equal(
shouldRun(selection, false, "pull_request", artifacts),
true,
);
}
for (const result of ["", "skipped", "success"]) {
assert.notEqual(check(selection, result), 0);
}
}
});
test(`${gate}: successful selection preserves path gating and suite results`, () => {
assert.equal(shouldRun("success"), false);
assert.equal(shouldRun("success", true, "pull_request", "success"), true);
assert.equal(shouldRun("success", false, "push", "success"), true);
assert.equal(check("success", "success"), 0);
for (const result of ["", "failure", "cancelled", "skipped"]) {
assert.notEqual(check("success", result), 0);
}
});
}
Loading
Loading