diff --git a/packages/cli/src/commands/review/publish-assets.test.ts b/packages/cli/src/commands/review/publish-assets.test.ts index 92b25748fb7..fc91cf525be 100644 --- a/packages/cli/src/commands/review/publish-assets.test.ts +++ b/packages/cli/src/commands/review/publish-assets.test.ts @@ -306,6 +306,32 @@ describe('publish-assets', () => { expect(ghWithInputPlainMock).not.toHaveBeenCalled(); }); + it('warns when the assets repo is the reviewed repo, case-insensitively', () => { + process.env['QWEN_REVIEW_ASSETS_REPO'] = 'qwenlm/qwen-code'; + ghMock.mockImplementation((...args: string[]) => { + if (args[0] === 'repo' && args[1] === 'view') { + return JSON.stringify({ + owner: { login: 'AaronZ345' }, + name: 'qwen-code', + url: 'https://github.com/AaronZ345/qwen-code', + parent: { owner: { login: 'QwenLM' }, name: 'qwen-code' }, + }); + } + return args.includes('.object.sha') ? 'headsha1234567890' : '{}'; + }); + ghWithInputMock.mockImplementation(() => '{}'); + + run({ files: [pngFile('evidence.png')] }); + + expect(process.exitCode).toBeUndefined(); + const stderr = (stderrSpy.mock.calls.map((c) => c[0]) as string[]).join( + '\n', + ); + expect(stderr).toContain( + 'QWEN_REVIEW_ASSETS_REPO points at the reviewed repository', + ); + }); + it('creates the branch when missing, from the default branch head', () => { // First ref lookup throws (missing branch); the creation path then asks // for the default branch and its head; the post-upload head read follows. diff --git a/packages/cli/src/commands/review/publish-assets.ts b/packages/cli/src/commands/review/publish-assets.ts index 31c43628ad2..193e3a5dbae 100644 --- a/packages/cli/src/commands/review/publish-assets.ts +++ b/packages/cli/src/commands/review/publish-assets.ts @@ -67,6 +67,48 @@ interface PublishAssetsArgs { defaultComment?: boolean; } +interface GhRepoView { + owner?: { login?: string }; + name?: string; + parent?: { + owner?: { login?: string }; + name?: string; + }; +} + +function resolveReviewedRepoForSelfTargetWarning( + args: PublishAssetsArgs, +): string | undefined { + if (args.reviewedRepo !== undefined) { + return args.reviewedRepo; + } + try { + const view = JSON.parse( + gh('repo', 'view', '--json', 'owner,name,url,parent'), + ) as GhRepoView; + const target = view.parent ?? view; + const owner = target.owner?.login; + const name = target.name; + return owner && name ? `${owner}/${name}` : undefined; + } catch { + return undefined; + } +} + +function warnIfAssetsRepoTargetsReviewedRepo( + assetsRepo: string, + args: PublishAssetsArgs, +): void { + const reviewedRepo = resolveReviewedRepoForSelfTargetWarning(args); + if (reviewedRepo?.toLowerCase() !== assetsRepo.toLowerCase()) return; + writeStderrLine( + 'publish-assets: warning — QWEN_REVIEW_ASSETS_REPO points at the reviewed ' + + 'repository. Evidence images will still be published because the ' + + 'destination was explicitly designated, but this recreates pr-assets/* refs ' + + 'in the repository under review.', + ); +} + /** The Contents-API dance for one file: create, or update when it exists. */ function putContent( repo: string, @@ -469,6 +511,7 @@ export function runPublishAssets(args: PublishAssetsArgs): void { const outPath = resolve(args.out); mkdirSync(dirname(outPath), { recursive: true }); writeFileSync(outPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); + warnIfAssetsRepoTargetsReviewedRepo(repo, args); // ── Pipeline mode: write the URLs back into the findings artifact ───────── if (findings && !args.findingsOut) {