ReviewOS

also looking at this

stacks/buddy

fix: prevent generated dependency cache PRs

#1366
Merged chris wants to merge codex/prevent-generated-pantry-prs into main
16 files +356 -72
src/git/github-provider.tsmodified+27-4
Changes to src/git/github-provider.ts
@@ -3,6 +3,7 @@ import type { FileChange, GitProvider, Issue, IssueOptions, PullRequest, PullReq
33import { Buffer } from 'node:buffer'
44import { spawn } from 'node:child_process'
55import process from 'node:process'
6import { assertUpdateTargetsExist, FileChangeValidationError, normalizeRepositoryPath } from '../utils/file-changes'
67import { detectRequiredPackageManagers, getAllLockFilePaths, regenerateLockFile } from '../utils/lock-file'
78
89// Match GitHub token formats (ghp_*, gho_*, ghs_*, ghu_*, ghr_*, github_pat_*)
@@ -141,7 +142,7 @@ export class GitHubProvider implements GitProvider {
141142 // Lockfile regeneration failures must NOT fall through to the API path —
142143 // the API path can't regenerate lockfiles either, so it would just produce
143144 // a PR with an updated manifest and stale lockfile. Bubble the error up.
144 if (gitError instanceof LockfileRegenerationError)
145 if (gitError instanceof LockfileRegenerationError || gitError instanceof FileChangeValidationError)
145146 throw gitError
146147
147148 console.warn(`⚠️ Git CLI commit failed, falling back to GitHub API: ${gitError}`)
@@ -240,9 +241,19 @@ export class GitHubProvider implements GitProvider {
240241 await this.runCommand('git', ['clean', '-fd'])
241242 console.log(`✅ Reset ${branchName} to origin/${baseBranch}`)
242243
244 await assertUpdateTargetsExist(files, async (cleanPath) => {
245 try {
246 await this.runCommand('git', ['cat-file', '-e', `origin/${baseBranch}:${cleanPath}`])
247 return true
248 }
249 catch {
250 return false
251 }
252 })
253
243254 // Apply file changes
244255 for (const file of files) {
245 const cleanPath = file.path.replace(/^\.\//, '').replace(/^\/+/, '')
256 const cleanPath = normalizeRepositoryPath(file.path)
246257
247258 // Safety check: prevent writing to sensitive files during tests
248259 if (process.env.NODE_ENV === 'test' || process.env.BUN_ENV === 'test') {
@@ -395,11 +406,23 @@ export class GitHubProvider implements GitProvider {
395406 const baseCommit = await this.apiRequest(`GET /repos/${this.owner}/${this.repo}/git/commits/${baseSha}`)
396407 const baseTreeSha = baseCommit.tree.sha
397408
409 await assertUpdateTargetsExist(files, async (cleanPath) => {
410 const encodedPath = cleanPath.split('/').map(segment => encodeURIComponent(segment)).join('/')
411 try {
412 await this.apiRequest(`GET /repos/${this.owner}/${this.repo}/contents/${encodedPath}?ref=${encodeURIComponent(baseBranch)}`)
413 return true
414 }
415 catch (error: any) {
416 if (error.message?.includes('404'))
417 return false
418 throw error
419 }
420 })
421
398422 // Create new tree with file changes
399423 const tree = []
400424 for (const file of files) {
401 // Ensure path doesn't start with ./ or have leading slashes (GitHub API requires clean relative paths)
402 const cleanPath = file.path.replace(/^\.\//, '').replace(/^\/+/, '')
425 const cleanPath = normalizeRepositoryPath(file.path)
403426
404427 if (file.type === 'delete') {
405428 tree.push({