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
117 changes: 117 additions & 0 deletions .agents/skills/update-deps/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
name: update-deps
description: >
Update this GraphRAG uv-workspace monorepo's dependencies and repair the resulting
breakages so every check and test passes again. Use when asked to update, upgrade, or
bump dependencies/packages/versions, refresh or regenerate the lockfile, migrate to a new
major of pandas/numpy/pydantic/pyarrow, resolve dependency-related test or lint failures,
or run a "dependency sweep" — even if the user only names a single package. Covers editing
pyproject.toml version specifiers across the root dev-deps and every packages/* member,
re-locking with uv, and fixing code and tests for library API changes (e.g. pandas 3.0).
USE FOR: update dependencies, upgrade packages, bump versions, dependency sweep, uv lock,
uv sync, refresh lockfile, migrate pandas/numpy, "deps broke the tests", pyproject bump.
user-invocable: true
---

# Update Dependencies (GraphRAG monorepo)

## Goal

Raise dependency versions across this uv workspace, re-lock, and fix any code or test
fallout until `uv run poe check` and `uv run poe test_unit` are both green — without
touching the version machinery that the release process owns.

## Layout facts

- This is a **uv workspace monorepo**. The root [`pyproject.toml`](../../../pyproject.toml)
holds the `dev` group under `[dependency-groups]` and the `[tool.poe.tasks]` commands.
- Runtime dependencies live in each member's `packages/*/pyproject.toml` under
`[project] dependencies`.
- Workspace members are wired via `[tool.uv.sources]` (`{ workspace = true }`) and pinned to
each other with `graphrag-*==X.Y.Z` lines.
- Package resolution goes through a Microsoft-internal index (`[[tool.uv.index]]`); expect
that feed to be used, not public PyPI directly.

## Do NOT edit these (release-owned)

1. **Cross-package pins** `graphrag-cache==...`, `graphrag-llm==...`, etc. in any package.
These are rewritten automatically by
[`scripts/update_workspace_dependency_versions.py`](../../../scripts/update_workspace_dependency_versions.py)
from the semversioner version. Hand-editing them causes drift.
2. **`[project] version` fields** — managed by semversioner ("do not change the version
here manually").
3. **`graspologic-native>=1.2,<1.3`** — held below 1.3 on purpose; 1.3.x changes Leiden
clustering output and breaks golden regression data. Only bump with a deliberate
golden-data refresh, and say so explicitly.

## Process

1. **Baseline first.** Confirm a clean working tree and that checks/tests already pass
before changing anything, so later failures are attributable to the bump:
- `uv run poe check`
- `uv run poe test_unit`
Prefer a dedicated branch (e.g. `dep-sweep`).

2. **Decide the scope.** Either a targeted set of packages the user named, or a full sweep.
Edit the `~=`/`>=`/`<` specifiers in the relevant `[project] dependencies`
(`packages/*/pyproject.toml`) and the root `dev` group. Leave the release-owned lines
above untouched.

3. **Resolve and lock.**
- For a full "get latest allowed" pass: `uv lock --upgrade`.
- For targeted bumps after editing specifiers: `uv lock`.
- Then install: `uv sync --all-packages`.
If resolution fails, read the conflict, relax/adjust the offending specifier, and re-lock.
Do not delete `uv.lock` to force it.

4. **Static checks.** Run `uv run poe check` (this is `ruff format --check` + `ruff check` +
`pyright`). Apply safe autofixes with `uv run poe fix`; format with `uv run poe format`.
Fix remaining lint/type errors by hand — see Gotchas and the migration reference.

5. **Tests.** Run `uv run poe test_unit` (NOT `uv run poe test`, which runs the full coverage
suite). Run `uv run poe test_verbs` and `uv run poe test_integration` when the change is
broad or touches indexing/query. Investigate every new failure.

6. **Repair breakages.** For test/type failures caused by a library's API change, load
[`references/migration-gotchas.md`](references/migration-gotchas.md) and apply the
documented pattern. Keep fixes minimal and consistent with sibling code; prefer a real
fix over a `# noqa`.

7. **Record the change.** Add a changelog entry:
`uv run semversioner add-change -t patch -d "<short description>"` (use `minor`/`major`
only if the user's intent warrants it).

8. **Final verification.** Re-run `uv run poe check` and `uv run poe test_unit`; both must be
green (see the known-flake note below before calling a failure a regression).

## Gotchas (this repo)

- **`test_unit`, not `test`.** `poe test` runs coverage over everything and is slow; use
`test_unit` for the fast feedback loop.
- **Ruff runs in preview mode** (`preview = true`, `target-version = "py310"`). Preview-only
rules such as `RUF069` (float equality) and `ASYNC119` fire here even though they may not
in other repos.
- **Known pre-existing flake:**
`tests/unit/indexing/test_profiling.py::TestWorkflowProfiler::test_handles_exception_in_context`
is timing-sensitive and can fail intermittently — it is not a dependency regression.
- **`uv sync --all-packages`** (not bare `uv sync`) to install every workspace member.
- **pandas is on the 3.0 line and numpy on 2.x.** Their major-version API changes are the
usual source of post-bump breakage — see the reference file.
- Version-bump edits touch many `pyproject.toml` files; make sure you did not accidentally
modify a `graphrag-*==` pin or a `version` field while editing nearby specifiers.

## Load-on-demand reference

When a bump breaks tests or type-checking with a library API change (especially pandas or
numpy), read [`references/migration-gotchas.md`](references/migration-gotchas.md) for
verified, repo-specific fix patterns before improvising.

## Completion checklist

- [ ] Only intended specifiers changed; no `graphrag-*==` pin or `version` field edited.
- [ ] `uv.lock` regenerated via `uv lock`/`uv lock --upgrade` (not hand-edited or deleted).
- [ ] `uv run poe check` passes (ruff format, ruff lint, pyright).
- [ ] `uv run poe test_unit` passes (ignoring only the known profiling flake).
- [ ] Broader suites run if the change was broad (`test_verbs`/`test_integration`).
- [ ] A semversioner changelog entry was added.
- [ ] Any risky/held pin (e.g. `graspologic-native`) left in place unless explicitly bumped.
81 changes: 81 additions & 0 deletions .agents/skills/update-deps/references/migration-gotchas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Migration Gotchas — verified fixes for this repo

Load this when a dependency bump breaks tests or `pyright`/`ruff` with a library API change.
Each entry is a pattern that was actually hit and fixed in this codebase. Apply the minimal
fix and keep it consistent with sibling code.

## pandas 3.0

### `DataFrame.swapaxes` removed → `np.array_split(df, n)` returns ndarrays

`np.array_split` internally calls `np.swapaxes`, which used to delegate to
`DataFrame.swapaxes` and return DataFrames with column names intact. In pandas 3.0
`swapaxes` was removed (deprecated in 2.1), so `np.array_split(df, n)` now yields plain
numpy arrays. Rebuilding with `pd.DataFrame(fold)` produces integer `RangeIndex` columns,
so later `df["some_column"]` raises `KeyError`.

Symptom: `KeyError: '<column>'` with a traceback ending in
`pandas/core/indexes/range.py ... get_loc`.

Fix — split positional indices instead of the frame, then select rows with `iloc`:

```python
# Broken under pandas 3.0
return [pd.DataFrame(fold) for fold in np.array_split(reports, n)]

# Fixed — preserves columns, dtypes, and even fold sizes
return [
reports.iloc[indices]
for indices in np.array_split(np.arange(len(reports)), n)
]
```

### `copy=` keyword removed from `merge`/`concat`/`join`/`set_axis` etc.

pandas 3.0 makes Copy-on-Write the default and drops the `copy=` parameter. Calls like
`df.merge(other, copy=False)` or `pd.concat([...], copy=False)` raise `TypeError`.
Fix: delete the `copy=` argument — CoW already avoids the unnecessary copy.

### Chained-assignment / `inplace` under Copy-on-Write

With CoW, mutating a slice (`df[mask]["col"] = x`) no longer writes back and may warn/error.
Assign through `.loc`: `df.loc[mask, "col"] = x`. Reassign results of `inplace=True`-style
operations rather than relying on in-place mutation of a view.

## numpy 2.x

- Removed aliases (`np.float_`, `np.int0`, `np.bool8`, `np.object0`, etc.) — use the builtin
or the explicit sized dtype (`np.float64`, `np.bool_`).
- `np.array_split` on a DataFrame no longer preserves the frame (see the pandas entry above).
- Some functions moved out of the top-level namespace; import from the documented submodule.

## ruff (preview mode active in this repo)

- **RUF069 (float-equality-comparison):** `x == 0.0` / `!= 0.0` is flagged. Prefer a
non-equality guard when semantically valid (`x <= 0.0` for a non-positive divide guard) or
`math.isclose(...)` for tolerance checks. Avoid a blanket `# noqa` when a real fix exists.
- **ASYNC119 (yield in context manager in async generator):** do not `yield` while holding a
`with`/`async with` in an async generator. Materialize inside the block, then yield after
it closes — matching the sibling providers:

```python
with Path.open(path, "r", encoding=enc) as f:
rows = list(csv.DictReader(f))
for row in rows:
yield transform(row)
```

## pyright

- Type stubs travel with majors: the `dev` group pins `pandas-stubs~=3.0`. When bumping
pandas, bump the matching stubs so `pyright` reflects the new API.
- After dependency changes, `pyright` may surface new optional/overload errors from updated
stubs; fix at the call site rather than suppressing, unless the stub is demonstrably wrong.

## General approach

1. Read the actual traceback/diagnostic to the leaf frame — the failing library call and the
changed symbol are usually right there.
2. Check how a sibling module in the same package already handles the pattern and match it.
3. Prefer a real, minimal fix over suppression; re-run `uv run poe check` and
`uv run poe test_unit` after each fix to confirm.
25 changes: 25 additions & 0 deletions .github/workflows/close-dependabot-prs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Close Dependabot PRs

on:
schedule:
- cron: "0 0 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
readme:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14

- name: Close Dependabot PRs
env:
GH_APP_ACCESS_TOKEN: ${{ secrets.GH_APP_ACCESS_TOKEN }}
run: bun run ./scripts/close-dependabot-prs.ts
25 changes: 25 additions & 0 deletions .github/workflows/update-deps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Update Dependencies

on:
schedule:
- cron: "0 0 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
readme:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14

- name: Create Update Dependencies Sweep Issue
env:
GH_APP_ACCESS_TOKEN: ${{ secrets.GH_APP_ACCESS_TOKEN }}
run: bun run ./scripts/open-deps-update-issue.ts
70 changes: 70 additions & 0 deletions scripts/close-dependabot-prs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
export { };

const access_token = process.env.GH_APP_ACCESS_TOKEN;
if (!access_token) {
throw new Error("GH_APP_ACCESS_TOKEN is not set");
}

const OWNER = "microsoft";
const REPO = "graphrag";
const DEPENDABOT_LOGIN = "dependabot[bot]";

const headers = {
Authorization: `Bearer ${access_token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2026-03-10",
};

type PullRequest = {
number: number;
title: string;
user: { login: string } | null;
};

const dependabotPrs: PullRequest[] = [];
for (let page = 1; ; page++) {
const res = await fetch(
`https://api.github.com/repos/${OWNER}/${REPO}/pulls?state=open&per_page=100&page=${page}`,
{ headers },
);

if (!res.ok) {
throw new Error(
`Failed to list pull requests. ${res.status} - ${res.statusText}`,
);
}

const pulls = (await res.json()) as PullRequest[];
if (pulls.length === 0) {
break;
}

dependabotPrs.push(
...pulls.filter((pr) => pr.user?.login === DEPENDABOT_LOGIN),
);
}

if (dependabotPrs.length === 0) {
console.log("No open dependabot pull requests found.");
} else {
for (const pr of dependabotPrs) {
const res = await fetch(
`https://api.github.com/repos/${OWNER}/${REPO}/pulls/${pr.number}`,
{
method: "PATCH",
headers,
body: JSON.stringify({ state: "closed" }),
},
);

if (!res.ok) {
throw new Error(
`Failed to close PR #${pr.number}. ${res.status} - ${res.statusText}`,
);
}

console.log(`Closed PR #${pr.number}: ${pr.title}`);
}

console.log(`Closed ${dependabotPrs.length} dependabot pull request(s).`);
}
44 changes: 44 additions & 0 deletions scripts/open-deps-update-issue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export { };

const access_token = process.env.GH_APP_ACCESS_TOKEN;
if (!access_token) {
throw new Error("GH_APP_ACCESS_TOKEN is not set");
}

const OWNER = "microsoft";
const REPO = "graphrag";

const body = {
title: "Update Dependencies Sweep",
body: "Update dependencies to the latest versions. This is an automated sweep to ensure that all dependencies are up-to-date.",
labels: ["dependencies"],
assignees: ["copilot-swe-agent[bot]"],
agent_assignment: {
target_repo: `${OWNER}/${REPO}`,
base_branch: "main",
custom_instructions:
"Use the update-deps skill to update all dependencies to their latest versions.",
model: "claude-opus-4.8",
},
};

const res = await fetch(
`https://api.github.com/repos/${OWNER}/${REPO}/issues`,
{
method: "POST",
headers: {
Authorization: `Bearer ${access_token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2026-03-10",
},
body: JSON.stringify(body),
},
);

if (!res.ok) {
throw new Error(`Failed to open issue. ${res.status} - ${res.statusText}`);
}

const data = await res.json();

console.log(JSON.stringify(data, null, 2));
Loading