https://github.com/withgraphite/graphite-transitive-dependencies
Compute affected packages in your monorepo CI. Used by Graphite's merge queue to determine which packages need validation when PRs are queued to merge.
https://github.com/withgraphite/graphite-transitive-dependencies
Last synced: 5 months ago
JSON representation
Compute affected packages in your monorepo CI. Used by Graphite's merge queue to determine which packages need validation when PRs are queued to merge.
- Host: GitHub
- URL: https://github.com/withgraphite/graphite-transitive-dependencies
- Owner: withgraphite
- Created: 2025-12-16T19:11:46.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2025-12-23T18:14:28.000Z (5 months ago)
- Last Synced: 2025-12-24T13:46:40.677Z (5 months ago)
- Language: TypeScript
- Size: 44.9 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# graphite-transitive-dependencies
Compute affected packages in a monorepo by traversing the dependency graph. Used to partition PRs into merge queue zones and protect against semantic conflicts.
## Installation
```bash
npm install
```
## Quick Start
```typescript
import { Splog } from "@monologue/splog";
// 1. Implement the StorageClient interface for your storage backend
const storageClient: StorageClient = {
getObjectOrNull: async (key: string) => {
// Your S3/GCS/filesystem implementation here
// Return { data: string | Buffer } or null if not found
},
};
const splog = new Splog();
// 2. Fetch cached targets for baseline (main) and PR commits
const baselineTargets = await getCachedTargetsForCommit({
commitSha: "main-branch-sha",
kind: "full",
storageClient,
splog,
});
const prTargets = await getCachedTargetsForCommit({
commitSha: "pr-branch-sha",
kind: "partial",
storageClient,
splog,
});
// 3. Build the hydrated DAG
const hydratedDag = buildHydratedDag({
baselineTargets,
additionalTargets: [prTargets],
splog,
});
// 4. Compute affected packages for merge queue partitioning
const affectedTargets = computeTransitiveTargets({
directPackageNames: prTargets.targetIds, // Use targetIds from cached data
hydratedDag,
splog,
});
// 5. Use affected packages to determine MQ zone
const affectedPackageNames = [...affectedTargets].map((t) => t.name);
```
## How It Works
1. **Cache dependency graphs per commit** - Store the full DAG for main branch commits, and filtered DAGs for PR commits
2. **Merge DAGs** - Combine baseline (main) with PR branch changes to get complete picture
3. **Compute transitive closure** - Given directly changed packages, find all packages that depend on them
4. **Partition PRs** - PRs affecting overlapping packages go in the same MQ zone to catch semantic conflicts
## API Reference
### Schemas
All schemas use Zod for runtime validation.
#### `CachedBuildTargets` (v2)
```typescript
type CachedBuildTargets =
| {
version: 2;
mode: "full-dag";
headSha: string;
targetIds: string[]; // List of affected package names
graph: Target[]; // Build targets with dependencies
}
| {
version: 2;
mode: "filtered";
baseSha: string;
headSha: string;
targetIds: string[]; // List of affected package names
graph: Target[]; // Build targets with dependencies
};
```
#### `Target`
```typescript
type Target = {
targetId: string; // Unique identifier (e.g., "@myorg/server#build")
targetName?: string; // Human-readable name (e.g., "@myorg/server")
dependencies: string[]; // Target IDs this target depends on
dependents: string[]; // Target IDs that depend on this target
};
```
#### `HydratedDag`
```typescript
type HydratedDag = {
targetIdToDependentIds: Map>; // Target ID to its dependents
targetIdToName: Map; // Target ID to human-readable name
nameToTargetIds: Map>; // Package name to target IDs
};
```
#### `ComputedTarget`
```typescript
type ComputedTarget = {
id: string; // Stable target ID
name: string; // Human-readable package name
};
```
### Functions
#### `getCachedTargetsForCommit`
Fetch cached build targets for a single commit.
```typescript
function getCachedTargetsForCommit(params: {
commitSha: string;
kind: "full" | "partial";
storageClient: StorageClient;
splog: Splog;
keyGenerator?: (commitSha: string, kind: "full" | "partial") => string;
}): Promise;
```
#### `getCachedTargetsForCommits`
Batch fetch cached build targets for multiple commits.
```typescript
function getCachedTargetsForCommits(params: {
commitShas: [string, ...string[]];
kind: "full" | "partial";
storageClient: StorageClient;
splog: Splog;
batchSize?: number; // Default: 50
keyGenerator?: (commitSha: string, kind: "full" | "partial") => string;
}): Promise>;
```
#### `buildHydratedDag`
Build a hydrated DAG by merging baseline targets with additional targets. Returns a `HydratedDag` with targetId-based mappings.
```typescript
function buildHydratedDag(params: {
baselineTargets: CachedBuildTargets; // Must be "full-dag" mode
additionalTargets: CachedBuildTargets[]; // PRs in queue
splog: Splog;
}): HydratedDag;
```
#### `computeTransitiveTargets`
Compute all packages affected by changes to the given packages. Returns targets with both stable ID and human-readable name.
```typescript
function computeTransitiveTargets(params: {
directPackageNames: string[]; // Package names directly changed
hydratedDag: HydratedDag; // From buildHydratedDag
splog: Splog;
}): Set;
```
### Error Classes
- `CachedTargetsNotFoundError` - Storage returned null for the commit
- `InvalidCachedTargetsError` - Cached data failed schema validation
- `FailedToFetchCachedTargetsError` - One or more batch fetches failed
## Storage Key Format
By default, cached targets are stored at:
```
commit-targets/{kind}-{commitSha}.json
```
Override with a custom `keyGenerator` function.
## Example: Storage Backends
### AWS S3
```typescript
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
const client = new S3Client({ region: "us-east-1" });
const storageClient: StorageClient = {
getObjectOrNull: async (key: string) => {
try {
const response = await client.send(
new GetObjectCommand({ Bucket: "your-bucket", Key: key })
);
const data = await response.Body?.transformToString();
return data ? { data } : null;
} catch (err) {
if ((err as any).name === "NoSuchKey") return null;
throw err;
}
},
};
```
### File System
```typescript
import * as fs from "fs/promises";
import * as path from "path";
const storageClient: StorageClient = {
getObjectOrNull: async (key: string) => {
try {
const data = await fs.readFile(path.join("/cache", key), "utf-8");
return { data };
} catch (err) {
if ((err as any).code === "ENOENT") return null;
throw err;
}
},
};
```
## Running Tests
```bash
npm test
```