Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d211fe8
feat(deps): adds dependency detection module and config schema
wgordon17 May 3, 2026
ac8d2d1
feat(deps): add dependencies tab filter schema and TabBar integration
wgordon17 May 3, 2026
6b1184e
feat(deps): add dependencies settings section
wgordon17 May 3, 2026
5882d74
feat(deps): integrate pre-exclusivity dep PR detection into Dashboard…
wgordon17 May 3, 2026
bba5cfb
fix(deps): address review findings from Phase 4
wgordon17 May 3, 2026
2aaa827
test(deps): add coverage for fetchDashboardIssueBodies, ignore/track,…
wgordon17 May 3, 2026
2af72dd
refactor(deps): removes redundant filters and slop comments
wgordon17 May 3, 2026
a15ccdd
perf(deps): removes redundant state filter in bot options memo
wgordon17 May 3, 2026
c60c361
fix(deps): removes dead props, adds dependencies to settings export
wgordon17 May 3, 2026
4a36c0d
feat(deps): resolves 12 UAT findings
wgordon17 May 3, 2026
ab5313f
fix(deps): fixes unknown bot detection bugs
wgordon17 May 3, 2026
f095717
fix(deps): fixes header styling, title parsing, and sort
wgordon17 May 3, 2026
6212f93
fix(deps): matches tracked bots with and without [bot] suffix
wgordon17 May 3, 2026
4455ea7
feat(deps): triggers refresh after tracking a bot
wgordon17 May 3, 2026
828abc5
feat(deps): body fallback for update type, ignored visibility
wgordon17 May 4, 2026
feb9ad6
fix(deps): body version priority, category badges, IgnoreBadge
wgordon17 May 4, 2026
e6e42dd
fix(deps): preserves body across poll cycles
wgordon17 May 4, 2026
f58382e
fix(deps): category sort is always primary, not just default
wgordon17 May 4, 2026
b0507fb
fix: resolves rebase artifacts (missing brace, unused import)
wgordon17 May 4, 2026
cf8731a
fix(deps): uses reactive signal for body data, not store mutation
wgordon17 May 4, 2026
a3a0332
feat(deps): persist parsed dep metadata to localStorage
wgordon17 May 4, 2026
0c32880
fix(deps): address PR review findings
wgordon17 May 4, 2026
a538b94
fix(deps): fixes post-deploy dependency tab issues
wgordon17 May 5, 2026
30da872
fix(deps): migrates tracked user logins to strip [bot] suffix
wgordon17 May 5, 2026
d4684bb
fix(deps): constrains compact-mode repo badge width for long names
wgordon17 May 5, 2026
ba5362b
fix(deps): uses inline-block for subtleRepo badge truncation
wgordon17 May 5, 2026
0e60a79
fix(deps): uses inline style for repo badge max-width
wgordon17 May 5, 2026
bf13350
fix(deps): replaces inline styles with Tailwind max-w classes
wgordon17 May 5, 2026
db3fca4
fix(deps): shows short repo name in comfortable subtleRepo mode
wgordon17 May 5, 2026
f3473c3
fix(deps): uses fixed-width repo badge column for consistent alignment
wgordon17 May 5, 2026
c5c1db2
fix(deps): narrows repo badge column to 11rem for tighter layout
wgordon17 May 5, 2026
cb3100e
fix(deps): narrows compact repo badge column to 9rem
wgordon17 May 5, 2026
6f3f3a6
fix(deps): tighten title pattern and address PR review findings
wgordon17 May 5, 2026
148a08e
fix(deps): restores missing prop and test after rebase
wgordon17 May 5, 2026
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
7 changes: 4 additions & 3 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,11 +287,12 @@ Unlike the Pull Requests tab (which groups by repo), the Dependencies tab groups

| Group | Criteria |
|-------|----------|
| **Needs Review** | CI passing (all checks green), PR not yet approved — these are ready to merge |
| **Waiting** | CI pending, checks still running, or PR is a draft — not yet actionable |
| **Mergeable** | CI passing (all checks green), not yet approved — awaiting review |
| **Pending Rebase** | PR has the configured rebase label — waiting for the bot to rebase |
| **Needs Action** | CI pending, checks still running, or PR is a draft — not yet actionable |
| **Stale** | PR has been open more than 14 days without merging — may need a rebase or manual review |

Within each group, PRs are sorted by updated date (most recent first).
Within each group, PRs are sorted by repository name, then update category (maintenance, pin, patch, minor, major), then update date.

### Abandoned Dependencies

Expand Down
23 changes: 19 additions & 4 deletions src/app/components/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ onAuthCleared(() => {
setDepMeta(new Map());
localStorage.removeItem?.(DEP_META_STORAGE_KEY);
_fetchingDashboardBodies = false;
_fetchingDepBodies = false;
resetAbandonedPatternCache();
const coord = _coordinator();
if (coord) {
Expand Down Expand Up @@ -1278,10 +1279,10 @@ export default function DashboardPage() {
isRefreshing={_coordinator()?.isRefreshing() ?? dashboardData.loading}
lastRefreshedAt={_coordinator()?.lastRefreshAt() ?? dashboardData.lastRefreshedAt}
onRefresh={() => _coordinator()?.manualRefresh()}
sortOptions={globalSortOptions}
sortValue={viewState.globalSort.field}
sortDirection={viewState.globalSort.direction}
onSortChange={(field, dir) => setSortPreference(field, dir)}
sortOptions={activeTab() === "dependencies" ? undefined : globalSortOptions}
sortValue={activeTab() === "dependencies" ? undefined : viewState.globalSort.field}
sortDirection={activeTab() === "dependencies" ? undefined : viewState.globalSort.direction}
onSortChange={activeTab() === "dependencies" ? undefined : (field, dir) => setSortPreference(field, dir)}
hideOrgRepo={!isBuiltinTab(activeTab()) || activeTab() === "dependencies"}
/>
</div>
Expand Down Expand Up @@ -1331,6 +1332,20 @@ export default function DashboardPage() {
onRefresh={() => _coordinator()?.manualRefresh()}
/>
</Match>
<Match when={activeTab() === "dependencies"}>
<DependenciesTab
pullRequests={dependencyPullRequests()}
loading={dashboardData.loading}
abandonedDepsMap={abandonedDepsMap()}
dashboardIssueUrls={dashboardIssueUrls()}
hotPollingPRIds={hotPollingPRIds()}
refreshTick={refreshTick()}
rebaseLabel={config.dependencies.rebaseLabel}
userLogin={userLogin()}
trackedBotLogins={trackedBotLogins()}
onRefresh={() => _coordinator()?.manualRefresh()}
/>
</Match>
<Match when={activeTab() === "tracked"}>
{/* TrackedTab intentionally receives unfiltered dashboardData — it bypasses exclusivity */}
<TrackedTab
Expand Down
28 changes: 4 additions & 24 deletions src/app/components/dashboard/DependenciesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,36 +200,16 @@ export default function DependenciesTab(props: DependenciesTabProps) {
});

const sortedPRs = createMemo(() => {
const { field, direction } = viewState.globalSort;
const items = [...classifiedPRs()];
const dir = direction === "asc" ? 1 : -1;

items.sort((a, b) => {
// User-selected sort as primary
let cmp = 0;
switch (field) {
case "repo": cmp = a.pr.repoFullName.localeCompare(b.pr.repoFullName); break;
case "title": cmp = a.pr.title.localeCompare(b.pr.title); break;
case "author": cmp = a.pr.userLogin.localeCompare(b.pr.userLogin); break;
case "comments": cmp = a.pr.comments - b.pr.comments; break;
case "checkStatus": cmp = (a.pr.checkStatus ?? "").localeCompare(b.pr.checkStatus ?? ""); break;
case "reviewDecision": cmp = (a.pr.reviewDecision ?? "").localeCompare(b.pr.reviewDecision ?? ""); break;
case "size": cmp = (a.pr.additions + a.pr.deletions) - (b.pr.additions + b.pr.deletions); break;
case "createdAt": cmp = a.pr.createdAt.localeCompare(b.pr.createdAt); break;
case "updatedAt":
default:
cmp = a.pr.updatedAt.localeCompare(b.pr.updatedAt);
break;
}
if (cmp !== 0) return cmp * dir;

// Category as secondary (safest → least safe within each group)
const repoCmp = a.pr.repoFullName.localeCompare(b.pr.repoFullName);
if (repoCmp !== 0) return repoCmp;

const catCmp = CATEGORY_SORT_ORDER[a.category] - CATEGORY_SORT_ORDER[b.category];
if (catCmp !== 0) return catCmp;

return a.pr.repoFullName.localeCompare(b.pr.repoFullName);
return a.pr.updatedAt.localeCompare(b.pr.updatedAt);
});

return items;
});

Expand Down
15 changes: 8 additions & 7 deletions src/app/components/dashboard/ItemRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,23 @@ export default function ItemRow(props: ItemRowProps) {
when={isCompact()}
fallback={
<span
class={`shrink-0 inline-flex items-center rounded-full font-mono font-medium text-xs px-2.5 py-1 ${
class={`shrink-0 font-mono font-medium text-xs px-2.5 py-1 ${
props.subtleRepo
? "min-w-[9.5rem] text-base-content/50"
: "bg-primary/10 text-primary"
? "inline-block w-[11rem] truncate align-middle text-base-content/50"
: "inline-flex items-center rounded-full bg-primary/10 text-primary"
}`}
title={props.subtleRepo ? props.repo : undefined}
>
{props.repo}
{props.subtleRepo ? repoShortName() : props.repo}
</span>
}
>
<Tooltip content={props.repo} class="shrink-0 relative z-10">
<span
class={`shrink-0 inline-flex items-center font-mono font-medium text-xs ${
class={`shrink-0 font-mono font-medium text-xs ${
props.subtleRepo
? "min-w-[9.5rem] text-base-content/50"
: "rounded-full bg-primary/10 text-primary px-2 py-0.5"
? "inline-block w-[9rem] truncate align-middle text-base-content/50"
: "inline-flex items-center rounded-full bg-primary/10 text-primary px-2 py-0.5"
}`}
>
{repoShortName()}
Expand Down
18 changes: 12 additions & 6 deletions src/app/lib/dependency-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const DEP_BRANCH_PREFIXES = [
"pyup-update-",
];

export const DEP_TITLE_PATTERN = /^(Bump |Update dependency |chore\(deps|fix\(deps|build\(deps|\[Snyk\])/i;
export const DEP_TITLE_PATTERN = /^(Bump |Update dependency |\[Snyk\]|(?:chore|fix|build)\(deps[^)]*\):\s*(?:update |pin |lock file |bump ))/i;

export const DEP_TOOL_LABEL_NAMES = new Set([
"dependencies",
Expand Down Expand Up @@ -72,8 +72,14 @@ export function isDependencyPr(pr: PullRequest, trackedBotLogins: Set<string>):
return false;
}

const VERSION_SPECIFIER_RE = /^[><=!~^]+/;

function stripVersionSpecifier(v: string): string {
return v.replace(VERSION_SPECIFIER_RE, "");
}

function parseSemver(v: string): [number, number, number] | null {
const cleaned = v.replace(/^v/, "");
const cleaned = stripVersionSpecifier(v).replace(/^v/, "");
const parts = cleaned.split(".");
if (parts.length < 2) return null;
const nums = parts.slice(0, 3).map(Number);
Expand Down Expand Up @@ -122,8 +128,8 @@ export function extractVersionInfo(title: string): VersionInfo | null {
// "update X requirement from >=A to >=B" (Dependabot Python)
const reqMatch = /^update\s+(.+?)\s+requirement\s+from\s+([^\s]+)\s+to\s+([^\s]+)/i.exec(body);
if (reqMatch) {
const from = reqMatch[2]!.replace(/^[><=!~^]+/, "");
const to = reqMatch[3]!.replace(/^[><=!~^]+/, "");
const from = stripVersionSpecifier(reqMatch[2]!);
const to = stripVersionSpecifier(reqMatch[3]!);
return { packageName: reqMatch[1]!, from, to, updateType: semverUpdateType(from, to) ?? undefined };
}

Expand Down Expand Up @@ -197,13 +203,13 @@ export function parseRenovateBody(body: string): VersionInfo | null {

if (changeIdx >= 0 && changeIdx < cells.length) {
const m = VERSION_ARROW_RE.exec(cells[changeIdx]);
if (m) { result.from = m[1]; result.to = m[2]; }
if (m) { result.from = stripVersionSpecifier(m[1]); result.to = stripVersionSpecifier(m[2]); }
}

if (!result.from) {
for (const cell of cells) {
const m = VERSION_ARROW_RE.exec(cell);
if (m) { result.from = m[1]; result.to = m[2]; break; }
if (m) { result.from = stripVersionSpecifier(m[1]); result.to = stripVersionSpecifier(m[2]); break; }
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,7 +922,7 @@ async function graphqlLightCombinedSearch(
return {
issues: [],
pullRequests: [],
errors: [{ repo: "search", statusCode: null, message: "Invalid userLogin", retryable: false }],
errors: [{ repo: "search", statusCode: null, message: `Invalid userLogin: "${userLogin}"`, retryable: false }],
};
}

Expand Down
11 changes: 11 additions & 0 deletions src/app/stores/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ export function loadConfig(): Config {
(parsed as Record<string, unknown>).theme = "auto";
}
}
// Migrate tracked user logins: strip [bot] suffix from stored logins.
// handleTrackBot now stores base names only, but pre-migration data may
// have "renovate[bot]" which causes a doubled "renovate[bot][bot]" variant.
if (parsed && typeof parsed === "object" && Array.isArray((parsed as Record<string, unknown>).trackedUsers)) {
const users = (parsed as Record<string, unknown>).trackedUsers as { login?: string }[];
for (const u of users) {
if (typeof u.login === "string" && /\[bot\]$/i.test(u.login)) {
u.login = u.login.replace(/\[bot\]$/i, "");
}
}
}
const result = ConfigSchema.safeParse(parsed);
if (result.success) {
const data = result.data;
Expand Down
53 changes: 53 additions & 0 deletions tests/components/IssuesTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -573,3 +573,56 @@ describe("IssuesTab", () => {
expect(link.getAttribute("rel")).toBe("noopener noreferrer");
});
});

describe("IssuesTab — hideDepDashboard + dependencies.enabled interaction", () => {
it("shows Dependency Dashboard in custom tab even when hideDepDashboard=true and deps disabled", () => {
viewStore.updateViewState({ hideDepDashboard: true });
updateConfig({ dependencies: { enabled: false, rebaseLabel: "rebase" } });
const issues = [
makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo", userLogin: "me" }),
];
setAllExpanded("my-custom-tab", ["org/repo"], true);
render(() => <IssuesTab issues={issues} userLogin="me" customTabId="my-custom-tab" />);
screen.getByText("Dependency Dashboard");
});
});

describe("IssuesTab — hideDepDashboard + dependencies.enabled", () => {
it("hides Dependency Dashboard issue when hideDepDashboard=true and dependencies.enabled=false", () => {
viewStore.updateViewState({ hideDepDashboard: true });
updateConfig({ dependencies: { enabled: false, rebaseLabel: "rebase" } });
const issues = [
makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo" }),
makeIssue({ id: 2, title: "Regular issue", repoFullName: "org/repo" }),
];
setAllExpanded("issues", ["org/repo"], true);
render(() => <IssuesTab issues={issues} userLogin="" />);
expect(screen.queryByText("Dependency Dashboard")).toBeNull();
screen.getByText("Regular issue");
});

it("shows Dependency Dashboard issue when hideDepDashboard=true but dependencies.enabled=true", () => {
viewStore.updateViewState({ hideDepDashboard: true });
updateConfig({ dependencies: { enabled: true, rebaseLabel: "rebase" } });
const issues = [
makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo" }),
makeIssue({ id: 2, title: "Regular issue", repoFullName: "org/repo" }),
];
setAllExpanded("issues", ["org/repo"], true);
render(() => <IssuesTab issues={issues} userLogin="" />);
screen.getByText("Dependency Dashboard");
screen.getByText("Regular issue");
});

it("shows Dependency Dashboard issue when hideDepDashboard=false regardless of dependencies.enabled", () => {
viewStore.updateViewState({ hideDepDashboard: false });
updateConfig({ dependencies: { enabled: false, rebaseLabel: "rebase" } });
const issues = [
makeIssue({ id: 1, title: "Dependency Dashboard", repoFullName: "org/repo" }),
];
setAllExpanded("issues", ["org/repo"], true);
render(() => <IssuesTab issues={issues} userLogin="" />);
screen.getByText("Dependency Dashboard");
});

});
Loading