mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
* feat(github): mirror PR CI checks and merge conflict status (MUL-2228)
Surface "checks passed/failed" and "conflicts/no conflicts" badges under
each linked PR on the issue page so users can judge readiness without
flipping over to GitHub. CI state is fed by check_suite webhooks
(GitHub Actions + apps using the Checks API; legacy status events are
out of scope for MVP); conflicts are read from pull_request.mergeable_state.
Data model:
* github_pull_request: add head_sha + mergeable_state
* github_pull_request_check_suite: per-suite rows keyed by (pr_id, suite_id)
* Aggregation done at query time, filtering by current head_sha so
late-arriving suites for a stale head can't contaminate the new head's
pending view; per-app latest suite chosen first so a single app firing
multiple suites isn't counted N times.
Webhook hardening:
* synchronize/opened/reopened/edited(base) explicitly clear mergeable_state
* single-row ordering protection on the check_suite upsert prevents a
late-delivered older event from overwriting a newer one
* check_suite.pull_requests is iterated; unknown PRs are logged and dropped
UI:
* PR row shows Checks + Conflicts badges; opaque mergeable values
(blocked/behind/unstable/...) render as no badge, not as conflicts.
* Terminal PR states (merged/closed) suppress the status row entirely.
Tests: * Pure unit coverage for derivePRMergeableState + aggregateChecksConclusion
* Webhook integration tests: multi-app aggregation, old-head ignore,
late-older-event ignore, synchronize clears mergeable_state
* Vitest coverage for pull-request-list badge rendering across CI/conflict
combinations and the legacy (null) fallback.
Co-authored-by: multica-agent <github@multica.ai>
* fix(github): scope check_suite PR lookup; preserve mergeable on metadata
Addresses code review on PR #2632.
1. check_suite handler now resolves the PR through the workspace-scoped
GetGitHubPullRequest query instead of GetGitHubPullRequestByRepoNumber.
The (workspace_id, repo_owner, repo_name, pr_number) tuple is the real
uniqueness key, so a bare (owner, repo, number) lookup could return a
stale row from another workspace and either land the suite on the wrong
PR or skip the right one when the installation ids drifted. The old
unscoped query is removed.
2. derivePRMergeableState now returns (value, clear) and the upsert SQL
distinguishes three cases: state-changing actions clear the column to
NULL, non-empty payloads write the value, and metadata events with an
empty payload preserve the existing column. Previously every empty
payload became NULL, so a labeled/assigned event silently wiped a
known clean/dirty verdict in violation of the RFC's "metadata empty
payload preserves" rule.
3. ListPullRequestsByIssue narrows to the issue's PR ids before running
the per-app check_suite aggregation, avoiding a full-table scan over
github_pull_request_check_suite when only a handful of rows belong to
the requested issue.
New helper test covers labeled+empty preserves; new integration test
verifies a metadata event after a known mergeable_state keeps the value.
Co-authored-by: multica-agent <github@multica.ai>
* feat(github): PR card layout v3 increment — stats + segmented progress bar
Replaces the row + badge layout under "Pull requests" on the issue
detail sidebar with a card that mirrors the GitHub PR summary look:
title, author/avatar, +N −M · K files diff stats, segmented progress
bar (failed → pending → passed, failure leftmost), and a one-line
status caption following an explicit priority pass-through.
Backend
- Migration 092: github_pull_request adds additions / deletions /
changed_files (INT NOT NULL DEFAULT 0). Zero defaults are what the
new frontend treats as "legacy backend — hide the stats row" so old
PR rows that pre-date this migration don't render "+0 −0 · 0 files".
- pull_request webhook handler reads stats off the top-level payload.
- ListPullRequestsByIssue now surfaces per-suite counts
(checks_passed / failed / pending) alongside the existing aggregate
conclusion, so the segmented bar reuses the already-computed counts
with no new aggregation.
Frontend (packages)
- core/github/pull-request-status.{ts,test.ts}: pure-function module
for the status-kind priority table and the segment derivation; 15
cases covered, includes the "all-zero → hide stats" guard.
- views/issues/components/pull-request-list.tsx: PullRequestCard plus
a compact-row fallback used when count > 4 (first 3 as cards, the
remainder collapsed behind a Show more toggle).
- i18n: new `pull_request_card_*` keys in en + zh-Hans.
Tests
- 12 component tests covering each rule of the priority table, the
legacy-zero stats fallback, and the collapse threshold.
- Reuse of the v3 webhook handler tests confirmed.
Verification
- pnpm typecheck + pnpm test green (60 test files, 536 tests).
- go build ./... + go vet ./... clean.
- 6 demo issues (DEV-2..DEV-7) screenshotted via Playwright; see the
PR comments for the visual check matrix.
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): collapse PR cards at N>=4, not N>4
The card-vs-collapse threshold used `>` so 4 PRs slipped past it and
all rendered as full cards, contrary to RFC v3 (N >= 4 collapses to
3 cards + compact tail). Switch to `>=` and update the threshold-
boundary test to expect "Show 1 more".
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): align PR sidebar rows with existing list style
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): hide terminal PR status badges
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
92 lines
3.7 KiB
JavaScript
92 lines
3.7 KiB
JavaScript
// Standalone screenshot capture for the 6 PR-card demo issues. Logs in as
|
|
// dev@localhost via the local /auth send-code → verify-code flow, then opens
|
|
// each /dev/issues/DEV-N and saves a clipped PNG focused on the right sidebar.
|
|
//
|
|
// Run: pnpm exec node scripts/screenshot-pr-cards.mjs
|
|
// Output: ./.screenshots/pr-card-DEV-{2..7}.png
|
|
|
|
import { chromium } from "@playwright/test";
|
|
import pg from "pg";
|
|
import { mkdirSync } from "node:fs";
|
|
|
|
const FRONTEND = process.env.FRONTEND_ORIGIN || "http://localhost:13101";
|
|
const API = process.env.NEXT_PUBLIC_API_URL || "http://localhost:18181";
|
|
const DB = process.env.DATABASE_URL || "postgres://multica:multica@localhost:5432/multica_multica_101?sslmode=disable";
|
|
const EMAIL = "dev@localhost";
|
|
const SLUG = "dev";
|
|
const ISSUES = [2, 3, 4, 5, 6, 7];
|
|
|
|
async function loginAndGetToken() {
|
|
const client = new pg.Client(DB);
|
|
await client.connect();
|
|
try {
|
|
await client.query("DELETE FROM verification_code WHERE email = $1", [EMAIL]);
|
|
const sendRes = await fetch(`${API}/auth/send-code`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: EMAIL }),
|
|
});
|
|
if (!sendRes.ok) throw new Error(`send-code: ${sendRes.status}`);
|
|
const row = await client.query(
|
|
"SELECT code FROM verification_code WHERE email=$1 AND used=FALSE AND expires_at>now() ORDER BY created_at DESC LIMIT 1",
|
|
[EMAIL],
|
|
);
|
|
if (row.rows.length === 0) throw new Error("no verification code");
|
|
const verifyRes = await fetch(`${API}/auth/verify-code`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: EMAIL, code: row.rows[0].code }),
|
|
});
|
|
if (!verifyRes.ok) throw new Error(`verify-code: ${verifyRes.status}`);
|
|
const data = await verifyRes.json();
|
|
return data.token;
|
|
} finally {
|
|
await client.end();
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
mkdirSync(".screenshots", { recursive: true });
|
|
const token = await loginAndGetToken();
|
|
const browser = await chromium.launch({ headless: true });
|
|
const ctx = await browser.newContext({ viewport: { width: 1600, height: 1200 } });
|
|
const page = await ctx.newPage();
|
|
|
|
// Set token before navigation so the renderer hits an authenticated state.
|
|
await page.goto(`${FRONTEND}/login`);
|
|
await page.evaluate((t) => localStorage.setItem("multica_token", t), token);
|
|
|
|
for (const n of ISSUES) {
|
|
const url = `${FRONTEND}/${SLUG}/issues/DEV-${n}`;
|
|
console.log("→", url);
|
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
// Wait for the PR section header to appear so the card has rendered.
|
|
const prSection = page.locator("text=Pull requests").first();
|
|
await prSection.waitFor({ timeout: 15000 }).catch(() => undefined);
|
|
|
|
// Close the floating chat widget — it overlays the right sidebar and
|
|
// hides exactly the area we want to capture. The minimize button has
|
|
// aria-label or just an icon; brute-force any chat close/minimize.
|
|
const closeChat = page.locator('[aria-label="Minimize" i], [aria-label="Close" i], button[title="Minimize" i]').first();
|
|
if (await closeChat.count()) {
|
|
await closeChat.click({ trial: false }).catch(() => undefined);
|
|
}
|
|
// Fallback: click on the chat header to collapse it.
|
|
await page.locator("text=New chat").first().click({ timeout: 1000 }).catch(() => undefined);
|
|
|
|
await page.waitForTimeout(400);
|
|
// Scroll the PR section into view.
|
|
await prSection.scrollIntoViewIfNeeded().catch(() => undefined);
|
|
await page.waitForTimeout(300);
|
|
const out = `.screenshots/pr-card-DEV-${n}.png`;
|
|
await page.screenshot({ path: out, fullPage: false });
|
|
console.log(" saved", out);
|
|
}
|
|
await browser.close();
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|