Skip to content

Commit 020efe2

Browse files
Copilotrpothin
andauthored
Handle missing GraphQL aliases as non-blocking repository skips (#68)
* Initial plan * Handle missing GraphQL aliases as soft skips Agent-Logs-Url: https://github.com/rpothin/PowerPlatform-OpenSource-Hub/sessions/2104b38a-97ba-4bfc-aa87-bc5bc7cc5b07 Co-authored-by: rpothin <23240245+rpothin@users.noreply.github.com> * Remove accidental parity report artifact Agent-Logs-Url: https://github.com/rpothin/PowerPlatform-OpenSource-Hub/sessions/2104b38a-97ba-4bfc-aa87-bc5bc7cc5b07 Co-authored-by: rpothin <23240245+rpothin@users.noreply.github.com> * Trim missing-repo warning output Agent-Logs-Url: https://github.com/rpothin/PowerPlatform-OpenSource-Hub/sessions/2104b38a-97ba-4bfc-aa87-bc5bc7cc5b07 Co-authored-by: rpothin <23240245+rpothin@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rpothin <23240245+rpothin@users.noreply.github.com>
1 parent d5884f1 commit 020efe2

6 files changed

Lines changed: 88 additions & 1 deletion

File tree

.github/workflows/update-github-repositories-details.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,17 @@ jobs:
260260
if (metricsPath && fs.existsSync(metricsPath)) {
261261
const metrics = JSON.parse(fs.readFileSync(metricsPath, 'utf8'));
262262
const failures = metrics.detailFailures ?? 0;
263+
const missingRepoSkips = metrics.missingRepoSkips ?? 0;
264+
if (missingRepoSkips > 0) {
265+
const missingRepoSkipNames = metrics.missingRepoSkipNames ?? [];
266+
const displayedNames = missingRepoSkipNames.slice(0, 5).join(', ');
267+
const moreCount = Math.max(0, missingRepoSkipNames.length - 5);
268+
const affectedRepos =
269+
displayedNames.length > 0
270+
? `${displayedNames}${moreCount > 0 ? `, and ${moreCount} more` : ''}`
271+
: 'names not recorded';
272+
console.log(`::warning::TypeScript pipeline skipped ${missingRepoSkips} repo(s) whose GraphQL alias was absent (likely deleted/private). Affected: ${affectedRepos}`);
273+
}
263274
if (failures > 0) {
264275
blockingFailures.push(`TypeScript pipeline had ${failures} repository detail hydration failure(s); all must succeed in production live mode`);
265276
}

Pipeline/src/generator.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { loadSearchCriteria } from "./config.js";
66
import { writeGeneratedRepositoryFilesAtomically } from "./generatedFiles.js";
77
import { deduplicateByFullName, isRecentlyUpdated, normalizeRepositoryRecord, serializeRecords, sortByPopularity } from "./normalization.js";
88
import { validateRecordsWithSchema } from "./schema.js";
9+
import { isMissingAliasError } from "./types.js";
910
import type { CandidateProvider, GenerateResult, PipelineMetrics, RepositoryRecord } from "./types.js";
1011

1112
export interface GenerateOptions {
@@ -99,6 +100,14 @@ export async function generateRepositoryDetails(options: GenerateOptions): Promi
99100
return null;
100101
}
101102
if (result instanceof Error) {
103+
if (isMissingAliasError(result)) {
104+
metrics.missingRepoSkips += 1;
105+
metrics.missingRepoSkipNames.push(repository.fullName);
106+
metrics.warnings.push(
107+
`Skipping '${repository.fullName}' because it is no longer accessible (GraphQL alias absent): ${result.message}`
108+
);
109+
return null;
110+
}
102111
if (isPatPolicyError(result)) {
103112
metrics.patPolicyFailures += 1;
104113
metrics.patPolicyFailureNames.push(repository.fullName);
@@ -172,6 +181,8 @@ function createMetrics(criteriaCount: number): PipelineMetrics {
172181
activeRepositories: 0,
173182
detailRequests: 0,
174183
detailFailures: 0,
184+
missingRepoSkips: 0,
185+
missingRepoSkipNames: [],
175186
patPolicyFailures: 0,
176187
patPolicyFailureNames: [],
177188
detailBatchCalls: 0,

Pipeline/src/providers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,12 @@ export class OctokitRepositoryProvider implements CandidateProvider {
303303
result.set(repo.fullName, new Error(rootLevelErrors.map((e) => e.message).join("; ")));
304304
} else if (node === undefined) {
305305
// Alias absent with no error — unexpected, likely a malformed response.
306-
result.set(repo.fullName, new Error(`GraphQL response missing alias '${alias}' for ${repo.fullName}`));
306+
result.set(
307+
repo.fullName,
308+
Object.assign(new Error(`GraphQL response missing alias '${alias}' for ${repo.fullName}`), {
309+
isMissingAlias: true as const
310+
})
311+
);
307312
} else {
308313
// GraphQL returned null with no error. All repos in this batch were found by the
309314
// search API so they exist. GitHub can silently null-out repos for PAT-policy

Pipeline/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,14 @@ export interface CandidateProvider {
198198
batchGetRepositoryDetails?(repos: SearchRepository[]): Promise<Map<string, RepositoryDetails | Error>>;
199199
}
200200

201+
export type MissingAliasError = Error & {
202+
isMissingAlias: true;
203+
};
204+
205+
export function isMissingAliasError(error: unknown): error is MissingAliasError {
206+
return error instanceof Error && (error as Partial<MissingAliasError>).isMissingAlias === true;
207+
}
208+
201209
export interface PipelineMetrics {
202210
criteriaCount: number;
203211
searchRequests: number;
@@ -206,6 +214,8 @@ export interface PipelineMetrics {
206214
activeRepositories: number;
207215
detailRequests: number;
208216
detailFailures: number;
217+
missingRepoSkips: number;
218+
missingRepoSkipNames: string[];
209219
patPolicyFailures: number;
210220
patPolicyFailureNames: string[];
211221
detailBatchCalls: number;

Pipeline/tests/generator.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ describe("generateRepositoryDetails", () => {
166166
activeRepositories: 2,
167167
detailRequests: 2,
168168
detailFailures: 0,
169+
missingRepoSkips: 0,
170+
missingRepoSkipNames: [],
169171
generatedRecords: 2
170172
});
171173

@@ -329,6 +331,34 @@ describe("generateRepositoryDetails", () => {
329331
expect(result.metrics.patPolicyFailures).toBe(0);
330332
});
331333

334+
it("routes missing GraphQL aliases in batch path to missingRepoSkips", async () => {
335+
const configPath = path.join(outputRoot, "criteria-batch-missing-alias.json");
336+
const outputPath = path.join(outputRoot, "details-batch-missing-alias.json");
337+
await writeFile(configPath, JSON.stringify([{ topic: "powerplatform", searchLimit: 3 }]), "utf8");
338+
339+
const missingAliasError = Object.assign(new Error("GraphQL response missing alias 'repo1' for owner/missing"), {
340+
isMissingAlias: true as const
341+
});
342+
343+
const result = await generateRepositoryDetails({
344+
configPath,
345+
outputPath,
346+
schemaPath,
347+
provider: new BatchFakeProvider(
348+
{ powerplatform: [searchRepository("owner/ok"), searchRepository("owner/missing")] },
349+
{ "owner/ok": repositoryDetails(20), "owner/missing": missingAliasError }
350+
),
351+
now: new Date("2026-01-01T00:00:00Z"),
352+
workflowRunId: "test-run"
353+
});
354+
355+
expect(result.records.map((r) => r.fullName)).toEqual(["owner/ok"]);
356+
expect(result.metrics.missingRepoSkips).toBe(1);
357+
expect(result.metrics.missingRepoSkipNames).toEqual(["owner/missing"]);
358+
expect(result.metrics.detailFailures).toBe(0);
359+
expect(result.metrics.warnings.join("\n")).toContain("owner/missing");
360+
});
361+
332362
it("routes PAT-policy errors in batch path to patPolicyFailures", async () => {
333363
const configPath = path.join(outputRoot, "criteria-batch-pat.json");
334364
const outputPath = path.join(outputRoot, "details-batch-pat.json");

Pipeline/tests/providers.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,26 @@ describe("OctokitRepositoryProvider.batchGetRepositoryDetails", () => {
202202
expect((bad as Error).message).toContain("Could not resolve to a Repository");
203203
});
204204

205+
it("tags a missing-alias error when GraphQL omits a repo alias with no errors", async () => {
206+
const client = makeClient([
207+
{
208+
data: {
209+
rateLimit: RATE_LIMIT_OK,
210+
repo0: goodNode()
211+
},
212+
errors: []
213+
}
214+
]);
215+
216+
const provider = new OctokitRepositoryProvider(client);
217+
const results = await provider.batchGetRepositoryDetails([repo("owner/ok"), repo("owner/missing")]);
218+
219+
const r = results.get("owner/missing");
220+
expect(r).toBeInstanceOf(Error);
221+
expect((r as Error).message).toContain("GraphQL response missing alias 'repo1' for owner/missing");
222+
expect((r as any).isMissingAlias).toBe(true);
223+
});
224+
205225
// ---------------------------------------------------------------------------
206226
// Root-level GraphQL errors (PAT-policy scenario)
207227
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)