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
14 changes: 14 additions & 0 deletions packages/diffler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,22 @@ helpers:

cache:
ttl: 3600

statsAction:
# Include private repositories in anonymous aggregate numbers while keeping
# names, URLs, topics, IDs, and per-repository records out of public JSON.
includePrivateRepositoryMetrics: true
includePrivateRepositoryDetails: false
includePrivateCacheDetails: false
```

Private aggregate collection requires a token that can read the intended private
repositories. A repository-scoped `GITHUB_TOKEN` usually cannot access private
repositories outside its own repository; use a fine-grained PAT stored as a
repository secret. You can also enable this mode with
`STATS_INCLUDE_PRIVATE_REPOSITORY_METRICS=true` or
`diffler collect --include-private-metrics`.

---

## 🤖 GitHub Actions
Expand Down
8 changes: 8 additions & 0 deletions packages/diffler/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ program
.option("--cache-path <path>", "Path for committed stable cache state")
.option("--volatile-cache-path <path>", "Path for volatile API metadata cache")
.option("--backfill-mode <mode>", "Backfill mode: resume, refresh, or off")
.option(
"--include-private-metrics",
"Include anonymous private repository metrics without repository details"
)
.option("--include-private", "Include private repository details")
.action(async (options) => {
const token = process.env.GITHUB_TOKEN;
Expand All @@ -114,7 +118,11 @@ program
statsConfig.backfillMode = options.backfillMode;
}
}
if (options.includePrivateMetrics) {
statsConfig.includePrivateRepositoryMetrics = true;
}
if (options.includePrivate) {
statsConfig.includePrivateRepositoryMetrics = true;
statsConfig.includePrivateRepositoryDetails = true;
statsConfig.includePrivateCacheDetails = true;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/diffler/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const StatsActionConfigSchema = z.object({
minRestRemaining: z.number().int().default(750),
includeTraffic: z.boolean().default(true),
includeRestRepoStats: z.boolean().default(true),
includePrivateRepositoryMetrics: z.boolean().default(false),
includePrivateRepositoryDetails: z.boolean().default(false),
includePrivateCacheDetails: z.boolean().default(false),
backfillMode: z.enum(["resume", "refresh", "off"]).default("resume"),
Expand Down Expand Up @@ -150,6 +151,10 @@ export function buildStatsActionConfig(config: DifflerConfig): StatsActionConfig
base.minRestRemaining = envNum("min-rest-remaining", base.minRestRemaining);
base.includeTraffic = envBool("include-traffic", base.includeTraffic);
base.includeRestRepoStats = envBool("include-rest-repo-stats", base.includeRestRepoStats);
base.includePrivateRepositoryMetrics = envBool(
"include-private-repository-metrics",
base.includePrivateRepositoryMetrics
);
base.includePrivateRepositoryDetails = envBool(
"include-private-repository-details",
base.includePrivateRepositoryDetails
Expand Down
50 changes: 45 additions & 5 deletions packages/diffler/src/stats/cache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { dirname } from "node:path";
import {
type BackfillFailure,
Expand Down Expand Up @@ -46,10 +47,11 @@ export function readVolatileCache(path: string): VolatileCache {
export function writeStableCache(
path: string,
cache: StableCache,
includePrivateDetails = false
includePrivateDetails = false,
includePrivateMetrics = false
): void {
writeJsonFile(path, {
...sanitizeStableCache(cache, includePrivateDetails),
...sanitizeStableCache(cache, includePrivateDetails, includePrivateMetrics),
updatedAt: Date.now(),
});
}
Expand Down Expand Up @@ -84,7 +86,8 @@ export function cacheRepository(

export function sanitizeStableCache(
cache: StableCache,
includePrivateDetails: boolean
includePrivateDetails: boolean,
includePrivateMetrics = false
): StableCache {
if (includePrivateDetails) return cache;

Expand All @@ -94,6 +97,13 @@ export function sanitizeStableCache(
)
) as Record<string, CachedRepository>;
const publicRepositoryIds = new Set(Object.keys(repositories));
const metricCacheIds = includePrivateMetrics
? new Set([
...publicRepositoryIds,
...Object.keys(cache.contributorStats).filter(isPrivateMetricCacheKey),
...Object.keys(cache.traffic).filter(isPrivateMetricCacheKey),
])
: publicRepositoryIds;

return {
...cache,
Expand All @@ -106,9 +116,9 @@ export function sanitizeStableCache(
),
contributorStats: filterRecordByPublicRepoId(
cache.contributorStats,
publicRepositoryIds
metricCacheIds
),
traffic: filterRecordByPublicRepoId(cache.traffic, publicRepositoryIds),
traffic: filterRecordByPublicRepoId(cache.traffic, metricCacheIds),
backfill: {
pending: cache.backfill.pending.filter((item) =>
publicRepositoryIds.has(item.repoId)
Expand All @@ -119,6 +129,36 @@ export function sanitizeStableCache(
};
}

export function repositoryMetricCacheKey(
repository: RepositoryRecord,
includePrivateDetails: boolean
): string {
if (!repository.isPrivate || includePrivateDetails) return repository.id;
return `private:${hashPrivateMetricValue(repository.id)}`;
}

export function repositoryMetricVersion(
repository: RepositoryRecord,
includePrivateDetails: boolean
): string | null {
if (
!repository.defaultBranchOid ||
!repository.isPrivate ||
includePrivateDetails
) {
return repository.defaultBranchOid;
}
return `private:${hashPrivateMetricValue(repository.defaultBranchOid)}`;
}

function isPrivateMetricCacheKey(key: string): boolean {
return key.startsWith("private:");
}

function hashPrivateMetricValue(value: string): string {
return createHash("sha256").update(value).digest("hex");
}

export function shouldReuseContributionYear(
cached: CachedContributionYear | undefined,
year: number,
Expand Down
43 changes: 35 additions & 8 deletions packages/diffler/src/stats/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
cacheRepository,
mergeBackfillQueue,
recordBackfillFailure,
repositoryMetricCacheKey,
repositoryMetricVersion,
shouldReuseContributionYear,
} from "./cache.js";
import { aggregateLanguages, mergeContributionsCollections } from "./aggregate.js";
Expand Down Expand Up @@ -408,12 +410,26 @@ export function buildBackfillQueue(
const next: BackfillItem[] = [];
const forceRefresh = config.backfillMode === "refresh";
for (const repo of repositories) {
if (repo.isPrivate && !config.includePrivateRepositoryDetails) continue;
if (
repo.isPrivate &&
!config.includePrivateRepositoryMetrics &&
!config.includePrivateRepositoryDetails
) {
continue;
}

const basePriority = getRepositoryPriority(repo);
const contributorStats = cache.contributorStats[repo.id];
const metricCacheKey = repositoryMetricCacheKey(
repo,
config.includePrivateRepositoryDetails
);
const metricVersion = repositoryMetricVersion(
repo,
config.includePrivateRepositoryDetails
);
const contributorStats = cache.contributorStats[metricCacheKey];
const contributorStatsComplete =
contributorStats?.defaultBranchOid === repo.defaultBranchOid &&
contributorStats?.defaultBranchOid === metricVersion &&
["fresh", "cached"].includes(contributorStats.status);
if (
config.includeRestRepoStats &&
Expand All @@ -430,7 +446,7 @@ export function buildBackfillQueue(
});
}

const traffic = cache.traffic[repo.id];
const traffic = cache.traffic[metricCacheKey];
if (
config.includeTraffic &&
canReadTraffic(repo) &&
Expand Down Expand Up @@ -486,21 +502,32 @@ export async function processBackfillQueue(
}

try {
const metricCacheKey = repositoryMetricCacheKey(
repo,
config.includePrivateRepositoryDetails
);
if (item.type === "contributors") {
cache.contributorStats[repo.id] = await fetchContributorStats(
const contributorStats = await fetchContributorStats(
client,
scheduler,
volatileCache,
cache.contributorStats[repo.id],
cache.contributorStats[metricCacheKey],
repo,
username
);
cache.contributorStats[metricCacheKey] = {
...contributorStats,
defaultBranchOid: repositoryMetricVersion(
repo,
config.includePrivateRepositoryDetails
),
};
} else {
cache.traffic[repo.id] = await fetchTraffic(
cache.traffic[metricCacheKey] = await fetchTraffic(
client,
scheduler,
volatileCache,
cache.traffic[repo.id],
cache.traffic[metricCacheKey],
repo
);
}
Expand Down
8 changes: 7 additions & 1 deletion packages/diffler/src/stats/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export async function runStatsCollection(
repositories = mergeRepositories(
[
...Object.values(stableCache.repositories).map((entry) => entry.repository),
...repositories,
...contributions.repositories,
]
);
Expand Down Expand Up @@ -166,7 +167,12 @@ export async function runStatsCollection(
});

writeJsonOutput(config.outputPath, output);
writeStableCache(config.cachePath, stableCache, config.includePrivateCacheDetails);
writeStableCache(
config.cachePath,
stableCache,
config.includePrivateCacheDetails,
config.includePrivateRepositoryMetrics
);
writeVolatileCache(config.volatileCachePath, volatileCache);

console.log(
Expand Down
Loading
Loading