Skip to content

Commit 36aec4a

Browse files
christian-byrneConnor Byrne
andauthored
fix(cicd): don't cut a second patch when a bump is already pending (#14997)
Bug in the stranded-commits check from #14429, which is live and currently misfiring once a day. ## What happens The idempotency guard asked "is a Release bump PR open against this branch?". Once that PR merges but the tag is never cut, the answer is no, so the next scheduled run dispatches another patch release. Observed on `core/1.48`: ``` 2026-08-09 16:10 check finds e136c13 stranded, dispatches a patch 2026-08-09 16:13 #14973 "1.48.8" opened 2026-08-10 04:58 #14973 merged; draft_release fails 403, v1.48.8 never created 2026-08-10 16:20 check runs again, sees no open bump PR, dispatches 2026-08-10 16:21 #14991 "1.48.9" opened ``` `core/1.48` now has `package.json` at 1.48.8 with `v1.48.7` as its newest tag. Left alone this burns one version per day for as long as the underlying failure persists. ## Fix Compare the branch's `package.json` against its newest tag. When they differ, a bump has already landed unreleased, so the release needs recovering rather than restarting, and no dispatch is requested. Stranded commits are still reported and the job still fails, so nothing goes quiet. Verified against the live state that caused the loop: ``` [FAIL] 1 fix commit(s) sit past v1.48.7 on core/1.48 and have not shipped: e136c13 [backport core/1.48] fix(billing): stop reporting "processing" ... core/1.48 is at 1.48.8 but its newest tag is v1.48.7: a bump already landed and was never released. Recover that release rather than cutting another patch; not requesting a dispatch. ``` `needs_patch_branch` is not emitted, so the dispatch step is skipped. ## Separate, and the actual blocker `draft_release` fails with `403 Resource not accessible by integration` when creating the GitHub release. No core release can be tagged until that is resolved, and this PR does not address it. #14991 should probably be closed so the version stops advancing, leaving `core/1.48` at 1.48.8 ready to tag. --------- Co-authored-by: Connor Byrne <c.byrne@comfy.org>
1 parent dbc37c1 commit 36aec4a

2 files changed

Lines changed: 124 additions & 4 deletions

File tree

scripts/cicd/check-stranded-release-commits.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
evaluateLine,
66
minorLineOf,
77
evaluatePin,
8+
hasPendingBump,
9+
shouldRequestDispatch,
810
newestStableVersion,
911
parsePinnedVersion,
1012
releaseBranchFor
@@ -190,3 +192,54 @@ describe('evaluatePin', () => {
190192
).toBeNull()
191193
})
192194
})
195+
196+
describe('hasPendingBump', () => {
197+
it('detects a bump that landed but was never tagged', () => {
198+
// core/1.48 on 2026-08-10: #14973 merged the 1.48.8 bump, but draft_release
199+
// 403'd so v1.48.8 never existed. Dispatching again just burns 1.48.9.
200+
expect(
201+
hasPendingBump({ branchVersion: '1.48.8', latestTag: 'v1.48.7' })
202+
).toBe(true)
203+
})
204+
205+
it('is false when the branch matches its latest tag', () => {
206+
expect(
207+
hasPendingBump({ branchVersion: '1.48.7', latestTag: 'v1.48.7' })
208+
).toBe(false)
209+
})
210+
211+
it('tolerates a tag without the v prefix', () => {
212+
expect(
213+
hasPendingBump({ branchVersion: '1.48.7', latestTag: '1.48.7' })
214+
).toBe(false)
215+
})
216+
})
217+
218+
describe('shouldRequestDispatch', () => {
219+
it('requests a dispatch when the branch matches its tag', () => {
220+
expect(
221+
shouldRequestDispatch({ latestTag: 'v1.48.7', branchVersion: '1.48.7' })
222+
).toBe(true)
223+
})
224+
225+
it('declines when a bump already landed unreleased', () => {
226+
expect(
227+
shouldRequestDispatch({ latestTag: 'v1.48.7', branchVersion: '1.48.8' })
228+
).toBe(false)
229+
})
230+
231+
it('fails closed when either value is unavailable', () => {
232+
// Unreadable package.json or a tagless branch means we cannot prove a bump
233+
// is not already pending. Dispatching on an unproven assumption is what
234+
// burned 1.48.8 and 1.48.9.
235+
expect(
236+
shouldRequestDispatch({ latestTag: 'v1.48.7', branchVersion: null })
237+
).toBe(false)
238+
expect(
239+
shouldRequestDispatch({ latestTag: null, branchVersion: '1.48.7' })
240+
).toBe(false)
241+
expect(
242+
shouldRequestDispatch({ latestTag: null, branchVersion: null })
243+
).toBe(false)
244+
})
245+
})

scripts/cicd/check-stranded-release-commits.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,40 @@ export function evaluatePin({
137137
}
138138
}
139139

140+
/**
141+
* Whether a version bump already landed on the branch without being tagged.
142+
*
143+
* The release is then already in flight and needs recovering, not restarting:
144+
* dispatching again resolves the *next* patch and burns a version per run. On
145+
* 2026-08-10 core/1.48 sat at 1.48.8 with v1.48.7 as its newest tag, because
146+
* draft_release 403'd, and the check cut a pointless 1.48.9.
147+
*/
148+
export function hasPendingBump({
149+
branchVersion,
150+
latestTag
151+
}: {
152+
branchVersion: string
153+
latestTag: string
154+
}): boolean {
155+
return branchVersion !== latestTag.replace(/^v/, '')
156+
}
157+
158+
/**
159+
* Whether to ask CI to cut a patch. Fails closed: if either the tag or the
160+
* branch version is unreadable we cannot prove a bump is not already pending,
161+
* and dispatching on that unproven assumption is what burned 1.48.8 and 1.48.9.
162+
*/
163+
export function shouldRequestDispatch({
164+
latestTag,
165+
branchVersion
166+
}: {
167+
latestTag: string | null
168+
branchVersion: string | null
169+
}): boolean {
170+
if (!latestTag || !branchVersion) return false
171+
return !hasPendingBump({ branchVersion, latestTag })
172+
}
173+
140174
function git(...args: string[]): string {
141175
return execFileSync('git', args, { encoding: 'utf-8' }).trim()
142176
}
@@ -174,6 +208,21 @@ function latestTagOn(branch: string): string | null {
174208
}
175209
}
176210

211+
function branchPackageVersion(branch: string): string | null {
212+
try {
213+
const pkg: unknown = JSON.parse(
214+
git('show', `origin/${branch}:package.json`)
215+
)
216+
if (typeof pkg === 'object' && pkg !== null && 'version' in pkg) {
217+
const { version } = pkg as { version: unknown }
218+
return typeof version === 'string' ? version : null
219+
}
220+
return null
221+
} catch {
222+
return null
223+
}
224+
}
225+
177226
function commitsPastTag(tag: string, branch: string): Commit[] {
178227
const raw = git('log', `${tag}..origin/${branch}`, '--format=%H%x1f%s')
179228
if (!raw) return []
@@ -261,10 +310,28 @@ async function main(): Promise<void> {
261310
(f) => f.kind === 'stranded-commits' && f.severity === 'failure'
262311
)
263312
if (process.env.GITHUB_OUTPUT && strandedPinnedLine) {
264-
appendFileSync(
265-
process.env.GITHUB_OUTPUT,
266-
`needs_patch_branch=${strandedPinnedLine.branch}\n`
267-
)
313+
const { branch } = strandedPinnedLine
314+
const latestTag = latestTagOn(branch)
315+
const branchVersion = branchPackageVersion(branch)
316+
317+
if (shouldRequestDispatch({ latestTag, branchVersion })) {
318+
appendFileSync(
319+
process.env.GITHUB_OUTPUT,
320+
`needs_patch_branch=${branch}\n`
321+
)
322+
} else if (!latestTag || !branchVersion) {
323+
console.error(
324+
`Could not read ${!latestTag ? 'the newest tag' : 'package.json'} for ` +
325+
`${branch}; cannot prove a bump is not already pending, so not ` +
326+
`requesting a dispatch.`
327+
)
328+
} else {
329+
console.error(
330+
`${branch} is at ${branchVersion} but its newest tag is ${latestTag}: a ` +
331+
`bump already landed and was never released. Recover that release ` +
332+
`rather than cutting another patch; not requesting a dispatch.`
333+
)
334+
}
268335
}
269336

270337
if (allFindings.some((f) => f.severity === 'failure')) {

0 commit comments

Comments
 (0)