@@ -133,6 +133,8 @@ export interface CliOptions {
133133 number ?: number ;
134134 since ?: string ;
135135 until ?: string ;
136+ applicationReleaseBaseSha ?: string ;
137+ applicationReleaseHeadSha ?: string ;
136138}
137139
138140interface ChangelogState {
@@ -194,15 +196,44 @@ export function parseArgs(argv: string[]): CliOptions {
194196 continue ;
195197 }
196198
199+ if ( value === '--application-release-base-sha' || value === '--application-release-head-sha' ) {
200+ const sha = requireOptionValue ( argv , index , value ) ;
201+ if ( ! / ^ [ 0 - 9 a - f ] { 40 } $ / i. test ( sha ) ) {
202+ throw new Error ( `Invalid SHA for ${ value } : ${ sha } ` ) ;
203+ }
204+
205+ if ( value === '--application-release-base-sha' ) {
206+ options . applicationReleaseBaseSha = sha ;
207+ } else {
208+ options . applicationReleaseHeadSha = sha ;
209+ }
210+ index += 1 ;
211+ continue ;
212+ }
213+
197214 throw new Error ( `Unknown argument: ${ value } ` ) ;
198215 }
199216
200217 if ( options . preview ) {
201- if ( options . number === undefined || ! options . since || ! options . until ) {
202- throw new Error ( 'Preview mode requires --number, --since, and --until.' ) ;
218+ if (
219+ options . number === undefined ||
220+ ! options . since ||
221+ ! options . until ||
222+ ! options . applicationReleaseBaseSha ||
223+ ! options . applicationReleaseHeadSha
224+ ) {
225+ throw new Error (
226+ 'Preview mode requires --number, --since, --until, --application-release-base-sha, and --application-release-head-sha.' ,
227+ ) ;
203228 }
204- } else if ( options . number !== undefined ) {
205- throw new Error ( '--number can only be used with --preview.' ) ;
229+ } else if (
230+ options . number !== undefined ||
231+ options . applicationReleaseBaseSha ||
232+ options . applicationReleaseHeadSha
233+ ) {
234+ throw new Error (
235+ '--number, --application-release-base-sha, and --application-release-head-sha can only be used with --preview.' ,
236+ ) ;
206237 }
207238
208239 return options ;
@@ -554,10 +585,18 @@ async function fetchSubmoduleShaAtRef(
554585}
555586
556587class NonAheadCompareError extends Error {
557- constructor ( repoConfig : ChangelogRepository , baseSha : string , headSha : string , status : string ) {
588+ readonly status : GitHubCompareResponse [ 'status' ] ;
589+
590+ constructor (
591+ repoConfig : ChangelogRepository ,
592+ baseSha : string ,
593+ headSha : string ,
594+ status : GitHubCompareResponse [ 'status' ] ,
595+ ) {
558596 super (
559597 `Expected an ahead range for ${ repoConfig . owner } /${ repoConfig . repo } , received ${ status } for ${ baseSha } ...${ headSha } .` ,
560598 ) ;
599+ this . status = status ;
561600 }
562601}
563602
@@ -570,7 +609,7 @@ async function fetchComparedCommits(
570609 const commits : GitHubListCommit [ ] = [ ] ;
571610 let totalCommits : number | null = null ;
572611
573- for ( let page = 1 ; page <= 3 ; page += 1 ) {
612+ for ( let page = 1 ; ; page += 1 ) {
574613 const url = new URL (
575614 `https://api.github.com/repos/${ repoConfig . owner } /${ repoConfig . repo } /compare/${ baseSha } ...${ headSha } ` ,
576615 ) ;
@@ -665,9 +704,9 @@ export async function expandReleasedBrowserCommits(
665704 token ,
666705 ) ;
667706 } catch ( error ) {
668- // A rolled-back or rewritten submodule pointer must not block the whole draft; the
669- // application release comparison still fails loudly on the same condition .
670- if ( error instanceof NonAheadCompareError ) {
707+ // A pure rollback contains no newly released browser commits. Diverged ranges may contain
708+ // head-only commits, so they must fail instead of silently dropping release evidence .
709+ if ( error instanceof NonAheadCompareError && error . status === 'behind' ) {
671710 console . warn (
672711 `Skipping submodule expansion for ${ hostCommit . owner } /${ hostCommit . repo } @${ hostCommit . shortSha } : ${ error . message } ` ,
673712 ) ;
@@ -1330,8 +1369,8 @@ async function writePreviewDraftArtifacts(input: {
13301369 ] ) ;
13311370}
13321371
1333- async function main ( ) {
1334- const options = parseArgs ( process . argv . slice ( 2 ) ) ;
1372+ export async function main ( argv = process . argv . slice ( 2 ) ) {
1373+ const options = parseArgs ( argv ) ;
13351374 const githubToken = getGithubToken ( ) ;
13361375 let nextNumber : number ;
13371376 let windowSelection : WindowSelection ;
@@ -1344,18 +1383,8 @@ async function main() {
13441383 since : options . since as string ,
13451384 until : options . until as string ,
13461385 } ) ;
1347- [ applicationReleaseBaseSha , applicationReleaseSha ] = await Promise . all ( [
1348- fetchBranchHeadAtOrBefore (
1349- CHANGELOG_APPLICATION_REPOSITORY ,
1350- githubToken ,
1351- windowSelection . since ,
1352- ) ,
1353- fetchBranchHeadAtOrBefore (
1354- CHANGELOG_APPLICATION_REPOSITORY ,
1355- githubToken ,
1356- windowSelection . until ,
1357- ) ,
1358- ] ) ;
1386+ applicationReleaseBaseSha = options . applicationReleaseBaseSha as string ;
1387+ applicationReleaseSha = options . applicationReleaseHeadSha as string ;
13591388 } else {
13601389 const state = await readChangelogState ( ) ;
13611390 windowSelection = await selectWindow ( options , state ) ;
@@ -1444,16 +1473,6 @@ async function main() {
14441473 ) ;
14451474 }
14461475
1447- if ( eligible . length === 0 ) {
1448- console . log (
1449- 'The source window contains no public changelog facts. Treating it as a quiet week.' ,
1450- ) ;
1451- if ( ! options . preview ) {
1452- await appendGithubOutput ( 'has_changes' , 'false' ) ;
1453- }
1454- return ;
1455- }
1456-
14571476 const previewWorkspace = options . preview
14581477 ? await createPreviewWorkspace ( {
14591478 number : nextNumber ,
@@ -1468,6 +1487,16 @@ async function main() {
14681487 console . log ( `Prepared isolated preview evidence at ${ previewWorkspace . directory } ` ) ;
14691488 }
14701489
1490+ if ( eligible . length === 0 ) {
1491+ console . log (
1492+ 'The source window contains no public changelog facts. Treating it as a quiet week.' ,
1493+ ) ;
1494+ if ( ! options . preview ) {
1495+ await appendGithubOutput ( 'has_changes' , 'false' ) ;
1496+ }
1497+ return ;
1498+ }
1499+
14711500 const openAiToken = getOpenAiToken ( ) ;
14721501 const draft = await requestDraftFromModel ( eligible , nextNumber , windowSelection , openAiToken ) ;
14731502 if ( ! draft . sections . some ( ( section ) => section . entries . length > 0 ) ) {
0 commit comments