Automatically import and update project repository skills (#16066)

Automatically import skills from new GitHub project repositories and update enabled sources in the background. Preserve project-derived access, saved selections, immutable execution versions and explicit pins; add legacy adoption, refresh and connection repair in Sources.

Include durable scheduling and bounded work credits, production Storybook journeys, real GitHub-to-routine acceptance receipts and final-head CI/security/Greptile qualification.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-13 16:11:43 -05:00
1 parent de335b42d4
commit 473c80d13f
101 files changed
+5991 -186

No files matched your search

+27 -2
View File
@@ -14,9 +14,10 @@ export interface GitHubRead {
withScan?: <T>(operation: () => Promise<T>) => Promise<T>;
openSnapshot: (input: { repositoryUrl: string; ref: string; commitSha?: string }, options?: GitSkillSnapshotOptions) => Promise<GitSkillSnapshot>;
readonly connectionId?: string | null;
readonly grantId?: string | null;
}
export type DiscoveredSkill = SkillSourceCandidate & { files: CompanySkillVersionFileInventoryEntry[] };
export type ScannedSkillSource = SkillSourceDiscovery & { skills: DiscoveredSkill[]; defaultBranch: string; repositoryFiles?: CompanySkillVersionFileInventoryEntry[] };
export type ScannedSkillSource = SkillSourceDiscovery & { skills: DiscoveredSkill[]; defaultBranch: string; treeSha?: string; downloadBytes?: number; scanBytes?: number; repositoryFiles?: CompanySkillVersionFileInventoryEntry[] };
const MAX_FILE_BYTES = 1024 * 1024;
const MAX_SCAN_BYTES = 100 * 1024 * 1024;
const MAX_SCAN_PACKAGES = 1_000;
@@ -42,6 +43,28 @@ export function scanGitHubSkills(input: SkillScanInput, providerRead: GitHubRead
return providerRead.withScan ? providerRead.withScan(scan) : scan();
}
/** Oversized repositories may still contain valid, bounded skill packages. */
export async function scanGitHubSkillsWithPackageFallback(input: SkillScanInput, providerRead: GitHubRead, options: SkillScanOptions = {}): Promise<ScannedSkillSource> {
try { return await scanGitHubSkills(input, providerRead, options); }
catch (error) {
if (input.packageMode !== 'repository' || !(error instanceof Error) || !/^Repository package exceeds/.test(error.message)) throw error;
options.signal?.throwIfAborted();
const scan = await scanGitHubSkills({ ...input, packageMode: 'skills' }, providerRead, options);
rejectIncompleteSkillPackages(scan);
scan.warnings.push('Only skill packages were imported because the repository exceeds the supported package size.');
return scan;
}
}
export function rejectIncompleteSkillPackages(scan: ScannedSkillSource): void {
for (const skill of scan.skills) {
if (!skill.error && skill.inspection?.references.length) {
skill.error = 'This skill depends on files that its imported package cannot provide. Its dependencies could not be preserved. Import a supported repository package or include the dependencies in this skill package.';
}
}
scan.candidates = scan.skills.map(({ files: _files, ...candidate }) => candidate);
}
async function scanRepository(input: SkillScanInput, providerRead: GitHubRead, options: SkillScanOptions): Promise<ScannedSkillSource> {
const read = async (apiPath: string) => {
options.signal?.throwIfAborted();
@@ -249,7 +272,9 @@ async function scanRepository(input: SkillScanInput, providerRead: GitHubRead, o
if (skill.inspection) skill.inspection.references = skill.inspection.references.filter(ref => !included.has(ref.resolvedPath));
}
}
return { ...(input.packageMode ? { packageMode: input.packageMode } : {}), ...(repositoryPackage ? { repositoryPackage } : {}),
return { treeSha: snapshot.treeSha, downloadBytes: snapshot.downloadBytes,
scanBytes: Math.min(MAX_SCAN_BYTES, snapshot.entries.reduce((sum, entry) => sum + (entry.type === 'blob' ? entry.size ?? MAX_SCAN_BYTES : 0), 0)),
...(input.packageMode ? { packageMode: input.packageMode } : {}), ...(repositoryPackage ? { repositoryPackage } : {}),
...(repositoryFiles && options.retainFiles !== false ? { repositoryFiles } : {}),
connectionId: providerRead.connectionId ?? null, repositoryId: String(repo.id), repositoryUrl: `https://github.com/${repo.full_name.toLowerCase()}`, fullName: repo.full_name, trackingRef, commitSha: commit.sha,
defaultBranch: repo.default_branch, candidates: skills.map(({ files: _files, ...candidate }) => candidate), warnings, skills };