@@ -44,13 +44,10 @@ def get_next_version(project_dir: Path, repo_root: Path) -> str | None:
4444 """Get the next version for a project using vcs-versioning API."""
4545 try :
4646 # Load configuration from project's pyproject.toml
47+ # All project-specific settings (tag_regex, fallback_version, etc.) are in the config files
48+ # Override local_scheme to get clean version strings
4749 pyproject = project_dir / "pyproject.toml"
48- config = Configuration .from_file (
49- pyproject ,
50- root = str (repo_root ),
51- version_scheme = "towncrier-fragments" ,
52- local_scheme = "no-local-version" ,
53- )
50+ config = Configuration .from_file (pyproject , local_scheme = "no-local-version" )
5451
5552 # Get the ScmVersion object
5653 scm_version = parse_version (config )
@@ -69,11 +66,17 @@ def get_next_version(project_dir: Path, repo_root: Path) -> str | None:
6966 return None
7067
7168
72- def run_towncrier (project_dir : Path , version : str ) -> bool :
69+ def run_towncrier (project_dir : Path , version : str , * , draft : bool = False ) -> bool :
7370 """Run towncrier build for a project."""
7471 try :
72+ cmd = ["uv" , "run" , "towncrier" , "build" , "--version" , version ]
73+ if draft :
74+ cmd .append ("--draft" )
75+ else :
76+ cmd .append ("--yes" )
77+
7578 result = subprocess .run (
76- [ "uv" , "run" , "towncrier" , "build" , "--version" , version , "--yes" ] ,
79+ cmd ,
7780 cwd = project_dir ,
7881 capture_output = True ,
7982 text = True ,
@@ -122,41 +125,63 @@ def main() -> None:
122125 parser = argparse .ArgumentParser (description = "Create release proposal" )
123126 parser .add_argument (
124127 "--event" ,
125- required = True ,
126128 help = "GitHub event type (push or pull_request)" ,
127129 )
128130 parser .add_argument (
129131 "--branch" ,
130- required = True ,
131- help = "Source branch name" ,
132+ help = "Source branch name (defaults to current branch)" ,
132133 )
133134 args = parser .parse_args ()
134135
135136 # Get environment variables
136137 token = os .environ .get ("GITHUB_TOKEN" )
137138 repo_name = os .environ .get ("GITHUB_REPOSITORY" )
138- source_branch = args .branch
139- is_pr = args .event == "pull_request"
140139
141- if not token :
142- print ("ERROR: GITHUB_TOKEN environment variable not set" , file = sys .stderr )
143- sys .exit (1 )
140+ # Determine source branch
141+ if args .branch :
142+ source_branch = args .branch
143+ else :
144+ # Get current branch from git
145+ try :
146+ result = subprocess .run (
147+ ["git" , "branch" , "--show-current" ],
148+ capture_output = True ,
149+ text = True ,
150+ check = True ,
151+ )
152+ source_branch = result .stdout .strip ()
153+ print (f"Using current branch: { source_branch } " )
154+ except subprocess .CalledProcessError :
155+ print ("ERROR: Could not determine current branch" , file = sys .stderr )
156+ sys .exit (1 )
144157
145- if not repo_name :
146- print ("ERROR: GITHUB_REPOSITORY environment variable not set" , file = sys .stderr )
147- sys .exit (1 )
158+ is_pr = args .event == "pull_request" if args .event else False
159+
160+ # GitHub integration is optional
161+ github_mode = bool (token and repo_name )
148162
149- # Initialize GitHub API
150- gh = Github (token )
151- repo = gh .get_repo (repo_name )
163+ if github_mode :
164+ # Type narrowing: when github_mode is True, both token and repo_name are not None
165+ assert token is not None
166+ assert repo_name is not None
167+ print (f"GitHub mode: enabled (repo: { repo_name } )" )
168+ # Initialize GitHub API
169+ gh = Github (token )
170+ repo = gh .get_repo (repo_name )
152171
153- # Check for existing PR (skip for pull_request events)
154- if not is_pr :
155- release_branch , existing_pr_number = check_existing_pr (repo , source_branch )
172+ # Check for existing PR (skip for pull_request events)
173+ if not is_pr :
174+ release_branch , existing_pr_number = check_existing_pr (repo , source_branch )
175+ else :
176+ release_branch = f"release/{ source_branch } "
177+ existing_pr_number = None
178+ print (
179+ f"[PR VALIDATION MODE] Validating release for branch: { source_branch } "
180+ )
156181 else :
182+ print ("GitHub mode: disabled (missing GITHUB_TOKEN or GITHUB_REPOSITORY)" )
157183 release_branch = f"release/{ source_branch } "
158184 existing_pr_number = None
159- print (f"[PR VALIDATION MODE] Validating release for branch: { source_branch } " )
160185
161186 repo_root = Path .cwd ()
162187 projects = {
@@ -179,12 +204,13 @@ def main() -> None:
179204 if not any (to_release .values ()):
180205 print ("No changelog fragments found in any project, skipping release" )
181206
182- # Write GitHub Step Summary
183- github_summary = os .environ .get ("GITHUB_STEP_SUMMARY" )
184- if github_summary :
185- with open (github_summary , "a" ) as f :
186- f .write ("## Release Proposal\n \n " )
187- f .write ("ℹ️ No changelog fragments to process\n " )
207+ # Write GitHub Step Summary (if in GitHub mode)
208+ if github_mode :
209+ github_summary = os .environ .get ("GITHUB_STEP_SUMMARY" )
210+ if github_summary :
211+ with open (github_summary , "a" ) as f :
212+ f .write ("## Release Proposal\n \n " )
213+ f .write ("ℹ️ No changelog fragments to process\n " )
188214
189215 sys .exit (0 )
190216
@@ -210,8 +236,8 @@ def main() -> None:
210236
211237 print (f"{ project_name } next version: { version } " )
212238
213- # Run towncrier
214- if not run_towncrier (project_dir , version ):
239+ # Run towncrier (draft mode for local runs)
240+ if not run_towncrier (project_dir , version , draft = not github_mode ):
215241 print (f"ERROR: Towncrier build failed for { project_name } " , file = sys .stderr )
216242 sys .exit (1 )
217243
@@ -225,17 +251,18 @@ def main() -> None:
225251 releases_str = ", " .join (releases )
226252 print (f"\n Successfully prepared releases: { releases_str } " )
227253
228- # Write GitHub Actions outputs
229- github_output = os .environ .get ("GITHUB_OUTPUT" )
230- if github_output :
231- with open (github_output , "a" ) as f :
232- f .write (f"release_branch={ release_branch } \n " )
233- f .write (f"releases={ releases_str } \n " )
234- f .write (f"labels={ ',' .join (labels )} \n " )
254+ # Write GitHub Actions outputs (if in GitHub mode)
255+ if github_mode :
256+ github_output = os .environ .get ("GITHUB_OUTPUT" )
257+ if github_output :
258+ with open (github_output , "a" ) as f :
259+ f .write (f"release_branch={ release_branch } \n " )
260+ f .write (f"releases={ releases_str } \n " )
261+ f .write (f"labels={ ',' .join (labels )} \n " )
235262
236- # Prepare PR content for workflow to use
237- pr_title = f"Release: { releases_str } "
238- pr_body = f"""## Release Proposal
263+ # Prepare PR content for workflow to use
264+ pr_title = f"Release: { releases_str } "
265+ pr_body = f"""## Release Proposal
239266
240267This PR prepares the following releases:
241268{ releases_str }
@@ -253,39 +280,43 @@ def main() -> None:
253280
254281**Merging this PR will automatically create tags and trigger PyPI uploads.**"""
255282
256- # Write outputs for workflow
257- if github_output :
258- with open (github_output , "a" ) as f :
259- # Write PR metadata (multiline strings need special encoding)
260- f .write (f"pr_title={ pr_title } \n " )
261- # For multiline, use GitHub Actions multiline syntax
262- f .write (f"pr_body<<EOF\n { pr_body } \n EOF\n " )
263- # Check if PR exists
264- if not is_pr :
265- f .write (f"pr_exists={ 'true' if existing_pr_number else 'false' } \n " )
266- f .write (f"pr_number={ existing_pr_number or '' } \n " )
267-
268- # Write GitHub Step Summary
269- github_summary = os .environ .get ("GITHUB_STEP_SUMMARY" )
270- if github_summary :
271- with open (github_summary , "a" ) as f :
272- if is_pr :
273- f .write ("## Release Proposal Validation\n \n " )
274- f .write ("✅ **Status:** Validated successfully\n \n " )
275- f .write (f"**Planned Releases:** { releases_str } \n " )
276- else :
277- f .write ("## Release Proposal\n \n " )
278- f .write (f"**Releases:** { releases_str } \n " )
279-
280- # For PR validation, we're done
281- if is_pr :
282- print (f"\n [PR VALIDATION] Release validation successful: { releases_str } " )
283- return
284-
285- # For push events, output success but don't create PR yet
286- # (workflow will create PR after pushing the branch)
287- print (f"\n [PUSH] Release preparation complete: { releases_str } " )
288- print ("[PUSH] Workflow will commit, push branch, and create/update PR" )
283+ # Write outputs for workflow
284+ if github_output :
285+ with open (github_output , "a" ) as f :
286+ # Write PR metadata (multiline strings need special encoding)
287+ f .write (f"pr_title={ pr_title } \n " )
288+ # For multiline, use GitHub Actions multiline syntax
289+ f .write (f"pr_body<<EOF\n { pr_body } \n EOF\n " )
290+ # Check if PR exists
291+ if not is_pr :
292+ f .write (f"pr_exists={ 'true' if existing_pr_number else 'false' } \n " )
293+ f .write (f"pr_number={ existing_pr_number or '' } \n " )
294+
295+ # Write GitHub Step Summary
296+ github_summary = os .environ .get ("GITHUB_STEP_SUMMARY" )
297+ if github_summary :
298+ with open (github_summary , "a" ) as f :
299+ if is_pr :
300+ f .write ("## Release Proposal Validation\n \n " )
301+ f .write ("✅ **Status:** Validated successfully\n \n " )
302+ f .write (f"**Planned Releases:** { releases_str } \n " )
303+ else :
304+ f .write ("## Release Proposal\n \n " )
305+ f .write (f"**Releases:** { releases_str } \n " )
306+
307+ # For PR validation, we're done
308+ if is_pr :
309+ print (f"\n [PR VALIDATION] Release validation successful: { releases_str } " )
310+ return
311+
312+ # For push events, output success but don't create PR yet
313+ # (workflow will create PR after pushing the branch)
314+ print (f"\n [PUSH] Release preparation complete: { releases_str } " )
315+ print ("[PUSH] Workflow will commit, push branch, and create/update PR" )
316+ else :
317+ # Local mode - just report what would be released
318+ print (f"\n [LOCAL MODE] Release proposal ready: { releases_str } " )
319+ print ("[LOCAL MODE] Review changes in CHANGELOG.md and commit manually" )
289320
290321
291322if __name__ == "__main__" :
0 commit comments