@@ -16,6 +16,7 @@ const rePrereleaseIdNum = /^([a-zA-Z0-9-]+)\.(\d+)$/;
1616const reDatePattern = / (?< = [ ^ 0 - 9 ] | ^ ) [ 0 - 9 ] { 4 } - [ 0 - 9 ] { 2 } - [ 0 - 9 ] { 2 } (? = [ ^ 0 - 9 ] | $ ) / g;
1717const reDate = new RegExp ( reDatePattern . source ) ;
1818const reReplaceString = / ^ s # ( [ ^ # ] + ) # ( [ ^ # ] + ) # ( .* ) $ / ;
19+ const pyprojectVersionSections : readonly string [ ] = [ "project" , "tool.poetry" ] ;
1920
2021function stripV ( str : string ) : string {
2122 return str [ 0 ] === "v" ? str . slice ( 1 ) : str ;
@@ -88,9 +89,11 @@ export function readVersionFromPackageJson(projectRoot: string): string | null {
8889
8990export function readVersionFromPyprojectToml(projectRoot: string): string | null {
9091 return readVersionFile("pyproject.toml", projectRoot, content => {
91- const project = tomlGetString(content, "project", "version");
92- if (project && isSemver(project)) return project;
93- return tomlGetString(content, "tool.poetry", "version");
92+ for (const section of pyprojectVersionSections) {
93+ const v = tomlGetString(content, section, "version");
94+ if (v && isSemver(v)) return v;
95+ }
96+ return undefined;
9497 });
9598}
9699
@@ -189,17 +192,38 @@ export function getFileChanges({file, baseVersion, newVersion, replacements, dat
189192
190193 let newData: string;
191194 if (fileName === "package.json") {
192- newData = oldData.replace(/("version":[^]*?")\d+\.\d+\.\d+(?:[^"\d][^"]*)?(")/,
193- (_, p1, p2) => ` $ { p1 } ${newVersion } ${p2 } `);
195+ // regex replace would corrupt nested "version" fields (overrides, resolutions, scripts.version)
196+ const pkg = JSON.parse(oldData);
197+ pkg.version = newVersion;
198+ const indent = /^\{\r?\n([ \t]+)/.exec(oldData)?.[1] ?? " ";
199+ newData = JSON.stringify(pkg, null, indent) + (oldData.endsWith("\n") ? "\n" : "");
194200 } else if (fileName === "package-lock.json") {
195201 // regex replace would corrupt nested dependency versions
196202 const lockFile = JSON.parse(oldData);
197203 if (lockFile.version) lockFile.version = newVersion; // v1 and v2
198204 if (lockFile?.packages?.[""]?.version) lockFile.packages[""].version = newVersion; // v2 and v3
199205 newData = ` $ { JSON . stringify ( lockFile , null , 2 ) } \n`;
200206 } else if ( fileName === "pyproject . toml ") {
201- newData = oldData . replace ( / ( ^ v e r s i o n ? = ? [ " ' ] ) \d + \. \d + \. \d + (?: [ ^ " ' \d ] [ ^ " ' ] * ) ? ( [ " ' ] .* ) / gm,
202- ( _ , p1 , p2 ) => `${ p1 } ${ newVersion } ${ p2 } ` ) ;
207+ // scope to [project] / [tool.poetry] — other sections may have unrelated `version` keys
208+ const eol = / \r?\n/ . exec ( oldData ) ?. [ 0 ] ?? "\n" ;
209+ const lines = oldData . split ( reNewline ) ;
210+ const versionLine = / ^ ( v e r s i o n ? = ? [ " ' ] ) \d + \. \d + \. \d + (?: [ ^ " ' \d ] [ ^ " ' ] * ) ? ( [ " ' ] .* ) $ / ;
211+ let section : string | null = null ;
212+ for ( let i = 0 ; i < lines . length ; i ++ ) {
213+ const trimmed = lines [ i ] . trim ( ) ;
214+ if ( ! trimmed || trimmed [ 0 ] === "#" ) continue ;
215+ if ( trimmed [ 0 ] === "[" ) {
216+ section = / ^ \[ ( [ ^ [ \] ] + ) \] / . exec ( trimmed ) ?. [ 1 ] . trim ( ) ?? null ;
217+ continue ;
218+ }
219+ if ( ! section || ! pyprojectVersionSections . includes ( section ) ) continue ;
220+ const m = versionLine . exec ( lines [ i ] ) ;
221+ if ( m ) {
222+ lines [ i ] = `${ m [ 1 ] } ${ newVersion } ${ m [ 2 ] } ` ;
223+ break ;
224+ }
225+ }
226+ newData = lines . join ( eol ) ;
203227 } else if ( fileName === "uv.lock" ) {
204228 const projStr = readFileSync ( file . replace ( / u v \. l o c k $ / , "pyproject.toml" ) , "utf8" ) ;
205229 const name = tomlGetString ( projStr , "project" , "name" ) ?? tomlGetString ( projStr , "tool.poetry" , "name" ) ;
@@ -316,6 +340,10 @@ async function forgeFetch(method: string, url: string, authHeader: string, jsonB
316340// Thrown by attempt callbacks of withTokens to signal that the next token should be tried.
317341class AuthRetryable extends Error { }
318342
343+ function authOrError ( status : number , message : string ) : Error {
344+ return status === 401 || status === 403 ? new AuthRetryable ( message ) : new Error ( message ) ;
345+ }
346+
319347async function withTokens < T > (
320348 isGithub : boolean ,
321349 tokens : string [ ] ,
@@ -343,7 +371,7 @@ async function deleteMatchingDrafts(apiUrl: string, authHeader: string, tagName:
343371 throw new Error ( `Failed to list releases: ${ err . cause ?. message || err . message || "Unknown error" } ` ) ;
344372 }
345373 if ( ! listResponse . ok ) {
346- throw new Error ( `Failed to list releases: ${ listResponse . status } ${ listResponse . statusText } \n${ await listResponse . text ( ) } ` ) ;
374+ throw authOrError ( listResponse . status , `Failed to list releases: ${ listResponse . status } ${ listResponse . statusText } \n${ await listResponse . text ( ) } ` ) ;
347375 }
348376 const releases = await listResponse . json ( ) as Array < { id : number ; tag_name: string ; draft: boolean } > ;
349377 const drafts = releases . filter ( r => r . draft && r . tag_name === tagName ) ;
@@ -355,7 +383,7 @@ async function deleteMatchingDrafts(apiUrl: string, authHeader: string, tagName:
355383 throw new Error ( `Failed to delete draft release ${ draft . id } : ${ err . cause ?. message || err . message || "Unknown error" } ` ) ;
356384 }
357385 if ( ! deleteResponse . ok && deleteResponse . status !== 404 ) {
358- throw new Error ( `Failed to delete draft release ${ draft . id } : ${ deleteResponse . status } ${ deleteResponse . statusText } \n${ await deleteResponse . text ( ) } ` ) ;
386+ throw authOrError ( deleteResponse . status , `Failed to delete draft release ${ draft . id } : ${ deleteResponse . status } ${ deleteResponse . statusText } \n${ await deleteResponse . text ( ) } ` ) ;
359387 }
360388 console . info ( `Deleted stale draft release for ${ tagName } ` ) ;
361389 }
@@ -377,8 +405,7 @@ export async function deleteForgeRelease(repoInfo: RepoInfo, releaseId: number,
377405 throw new Error ( `Failed to delete release ${ releaseId } : ${ err . cause ?. message || err . message || "Unknown error" } ` ) ;
378406 }
379407 if ( response . ok || response . status === 404 ) return ;
380- const message = `Failed to delete release ${ releaseId } : ${ response . status } ${ response . statusText } \n${ await response . text ( ) } ` ;
381- throw response . status === 401 || response . status === 403 ? new AuthRetryable ( message ) : new Error ( message ) ;
408+ throw authOrError ( response . status , `Failed to delete release ${ releaseId } : ${ response . status } ${ response . statusText } \n${ await response . text ( ) } ` ) ;
382409 } ) ;
383410}
384411
@@ -419,8 +446,7 @@ export async function createForgeRelease(repoInfo: RepoInfo, tagName: string, bo
419446 return typeof result . id === "number" ? { id : result . id , html_url : result . html_url } : null ;
420447 }
421448
422- const message = `Failed to create release: ${ response . status } ${ response . statusText } \n${ await response . text ( ) } ` ;
423- throw response . status === 401 || response . status === 403 ? new AuthRetryable ( message ) : new Error ( message ) ;
449+ throw authOrError ( response . status , `Failed to create release: ${ response . status } ${ response . statusText } \n${ await response . text ( ) } ` ) ;
424450 } ) ;
425451}
426452
@@ -619,7 +645,11 @@ async function main(): Promise<void> {
619645 return { path, original, entry : processed . entry , updated : processed . updated } ;
620646 } ) ( ) ;
621647
622- const allFiles = changelogInfo ?. updated ? [ ...files , relative ( pwd , changelogInfo . path ) ] : files ;
648+ // generic baseVersion replacement would rewrite prior version headings in CHANGELOG.md
649+ const changelogRel = changelogInfo ? relative ( pwd , changelogInfo . path ) : null ;
650+ if ( changelogRel ) files = files . filter ( file => file !== changelogRel ) ;
651+
652+ const allFiles = changelogInfo ?. updated ? [ ...files , changelogRel ! ] : files ;
623653 const filesToAddPromise = ( ! args . gitless && ! args . all && allFiles . length ) ? removeIgnoredFiles ( allFiles ) : null ;
624654 const changelogPromise = ( ! args . gitless && ! args . dry ) ? ( async ( ) => {
625655 if ( changelogInfo ) {
0 commit comments