2222from packaging import version
2323from pathlib import Path
2424import requests
25+ import yaml
2526
2627
2728REGISTRY_URL = "https://pypi.riseproject.dev/simple/"
3334# build — a distinct ABI from "3.14", so it needs its own wheel.
3435TARGET_PYTHON_VERSIONS = ["3.12" , "3.13" , "3.14" , "3.14t" ]
3536
36- # Cap on how many new versions get dispatched per upgrade PR. When the registry
37+ # Cap on how many new versions get declared per upgrade PR. When the registry
3738# drifts far behind PyPI, an unbounded loop could kick off dozens of workflow
3839# runs per package. Three keeps the retry cost bounded while still covering the
3940# common "we missed one or two point releases" case.
@@ -341,38 +342,16 @@ def create_deprecation_pr(package: str, reason: str) -> Optional[str]:
341342 return None
342343
343344
344- def dispatch_workflow (workflow : str , ref : str , version_input : str ) -> bool :
345- """
346- Dispatch a workflow_dispatch workflow with a version input.
347-
348- Strips any leading `v`/`V` from version_input; build workflows already
349- prepend `v` when constructing git refs (e.g. `ref: v${{ env.VERSION }}`),
350- so passing `v2.5.1` would produce `vv2.5.1`.
351- """
352- version_input = version_input .lstrip ("vV" )
353- try :
354- subprocess .run ([
355- "gh" , "workflow" , "run" , workflow ,
356- "--repo" , REPO ,
357- "--ref" , ref ,
358- "-f" , f"version={ version_input } " ,
359- ], check = True , capture_output = True , text = True )
360- return True
361- except subprocess .CalledProcessError as e :
362- print (f" [!] Failed to dispatch { workflow } : { e .stderr or e } " )
363- return False
364-
365-
366345def get_new_versions (package_info : Dict , registry_version : str ) -> List [str ]:
367346 """
368347 Return sorted list of stable PyPI versions strictly greater than
369348 registry_version. Skips prereleases, dev releases, and fully yanked
370349 versions.
371350
372351 Result is truncated to the newest MAX_NEW_VERSIONS_PER_PR entries so a
373- stale registry does not spawn dozens of workflow dispatches per package.
374- Older skipped versions can still be triggered manually by editing the PR
375- body to add more `Trigger:` directives .
352+ stale registry does not queue dozens of builds per package.
353+ Older skipped versions can still be built by declaring them in
354+ docs/packages/<pkg>.yaml by hand .
376355 """
377356 try :
378357 reg_ver = version .parse (registry_version )
@@ -397,61 +376,33 @@ def get_new_versions(package_info: Dict, registry_version: str) -> List[str]:
397376 return result [- MAX_NEW_VERSIONS_PER_PR :]
398377
399378
400- def find_workflow_default_version (content : str ) -> Optional [str ]:
401- """
402- Return the value of `default:` under `workflow_dispatch.inputs.version`.
403-
404- Uses a simple heuristic: the first `default:` after `workflow_dispatch:`.
405- This holds for the current build-<pkg>.yml template.
406- """
407- marker = re .search (r"workflow_dispatch:" , content )
408- if not marker :
409- return None
410- m = re .search (
411- r"^\s*default:\s*(['\"]?)([^'\"\n\r]+?)\1\s*$" ,
412- content [marker .end ():],
413- re .MULTILINE ,
414- )
415- return m .group (2 ).strip () if m else None
416-
417-
418- def bump_workflow_version (path : Path , new_version : str ) -> bool :
379+ def declare_versions (yaml_file : Path , new_versions : List [str ]) -> List [str ]:
419380 """
420- Update the workflow_dispatch default version in a workflow file, and
421- replace every quoted literal of the old version elsewhere in the file
422- (concurrency group, env defaults, job names).
381+ Append a bare `- version:` entry for each version not yet listed in the
382+ package YAML, and return the versions that were added.
423383
424- Returns True if the file was modified.
384+ An entry without `tag`/`files` is what the build workflow builds, so this
385+ is all an upgrade PR has to change.
425386 """
426- content = path .read_text ()
427- current = find_workflow_default_version (content )
428- if current is None or current == new_version :
429- return False
430- updated = re .sub (
431- r"(['\"])" + re .escape (current ) + r"\1" ,
432- lambda m : f"{ m .group (1 )} { new_version } { m .group (1 )} " ,
433- content ,
434- )
435- if updated == content :
436- return False
437- path .write_text (updated )
438- return True
387+ content = yaml_file .read_text ()
388+ declared = set (re .findall (r"^- version: ['\"]?([^'\"\n]+)" , content , re .M ))
389+ added = [v for v in new_versions if v not in declared ]
390+ if not added :
391+ return []
392+ lines = [content .rstrip ("\n " )]
393+ for v in added :
394+ lines .append (yaml .safe_dump ([{"version" : v }], default_flow_style = False ).rstrip ("\n " ))
395+ yaml_file .write_text ("\n " .join (lines ) + "\n " )
396+ return added
439397
440398
441399def create_upgrade_pr (package : str , package_info : Dict , new_versions : List [str ]) -> Optional [str ]:
442400 """
443- Create a PR to trigger builds for each new package version.
444-
445- - Bumps the `default:` version in build-<pkg>.yml (and test-<pkg>.yml if
446- present) to the latest new version.
447- - PR body includes a `Trigger: <package>:<version>` line for every new
448- version so the pr-trigger workflow (or a human editing the body) can
449- re-dispatch builds.
450- - Script also directly dispatches build-<pkg>.yml (and test-<pkg>.yml if
451- present) once per new version, because PRs authored by GITHUB_TOKEN do
452- not fire pull_request workflows.
453- - If build workflow is missing, PR body contains boilerplate asking the
454- reviewer to create it.
401+ Create a PR that declares each new version in docs/packages/<pkg>.yaml.
402+
403+ The PR's own pull_request run of build-<pkg>.yml builds every version the
404+ YAML declares without a release, so nothing is dispatched here. If the
405+ build workflow is missing, the PR body asks the reviewer to create it.
455406 """
456407 if not new_versions :
457408 return None
@@ -470,76 +421,45 @@ def create_upgrade_pr(package: str, package_info: Dict, new_versions: List[str])
470421 git_run ("fetch" , "origin" )
471422 git_run ("switch" , "main" )
472423
424+ yaml_file = Path (f"docs/packages/{ package } .yaml" )
425+ if not yaml_file .exists ():
426+ print (f" [!] { yaml_file } does not exist" )
427+ return None
428+
473429 git_run ("switch" , "-c" , branch )
474430
475431 pypi_package_url = get_pypi_package_url (package_info )
476432 build_workflow = Path (f".github/workflows/build-{ package } .yml" )
477- test_workflow = Path (f".github/workflows/test-{ package } .yml" )
478-
479- bumped : List [Path ] = []
480- if build_workflow .exists () and bump_workflow_version (build_workflow , latest_version ):
481- git_run ("add" , str (build_workflow ))
482- bumped .append (build_workflow )
483- if test_workflow .exists () and bump_workflow_version (test_workflow , latest_version ):
484- git_run ("add" , str (test_workflow ))
485- bumped .append (test_workflow )
486-
487- if bumped :
488- commit_message = f"{ package } : upgrade to v{ latest_version } "
489- git_run ("commit" , "-m" , commit_message )
490- else :
491- # This block should only run if either:
492- #
493- # 1. No workflows exist for the package
494- # 2. The workflows don't need a bump for the detected version
495- #
496- # In the latter case, we want an empty PR to indicate that a new
497- # build should be triggered to add the latest version to the
498- # registry. Trigger directives are added for the empty PR so we can
499- # run a test build and ensure no tweaks need to be made before
500- # running the workflow from main.
501- commit_message = f"DO NOT MERGE: { package } : trigger builds for v{ latest_version } "
502- git_run ("commit" , "--allow-empty" , "-m" , commit_message )
503433
434+ added = declare_versions (yaml_file , new_versions )
435+ if not added :
436+ print (f" [=] { package } : { ', ' .join (new_versions )} already declared in { yaml_file } " )
437+ return None
438+
439+ git_run ("add" , str (yaml_file ))
440+ git_run ("commit" , "-m" , f"{ package } : upgrade to v{ latest_version } " )
504441 git_run ("push" , "origin" , branch )
505442
506- versions_list = ", " .join (f"v{ v } " for v in new_versions )
443+ versions_list = ", " .join (f"v{ v } " for v in added )
507444 body_lines = [
508445 f"Automatically generated PR to upgrade { package } — new versions detected: { versions_list } ." ,
509446 "" ,
510447 f"Link to [PyPI]({ pypi_package_url } )." ,
511448 "" ,
449+ f"Declares { versions_list } in `{ yaml_file } `; the build workflow builds every declared version that has no release yet." ,
450+ "" ,
512451 ]
513452
514- if bumped :
515- bumped_list = ", " .join (f"`{ p } `" for p in bumped )
516- body_lines += [
517- f"Bumped default `version` input to v{ latest_version } in { bumped_list } ." ,
518- "" ,
519- ]
520-
521453 if build_workflow .exists ():
522454 body_lines += [
523- "Build (and test, if present) workflows dispatched automatically for each version listed below." ,
524- "" ,
525- "Trigger directives (re-dispatch by editing the PR body):" ,
526- "" ,
527- ]
528- body_lines += [f"Trigger: { package } :{ v } " for v in new_versions ]
529- body_lines += [
530- "" ,
531- "If all builds succeed without modification, merge this PR and re-trigger the workflow(s) from `main` to publish." ,
532- "If changes are needed, force-push this branch and re-dispatch." ,
455+ "If all builds succeed without modification, merge this PR: the push to `main` publishes the wheels and opens the docs PR." ,
456+ "If changes are needed, force-push this branch." ,
533457 ]
534458 else :
535459 body_lines += [
536460 f"No build workflow found at `.github/workflows/build-{ package } .yml`." ,
537461 "Please create one (and optionally a matching `test-*.yml`) before merging." ,
538- "" ,
539- "Once the workflow exists, edit this PR body to re-fire the `Trigger:` directives:" ,
540- "" ,
541462 ]
542- body_lines += [f"Trigger: { package } :{ v } " for v in new_versions ]
543463
544464 body = "\n " .join (body_lines ) + "\n "
545465
@@ -553,19 +473,7 @@ def create_upgrade_pr(package: str, package_info: Dict, new_versions: List[str])
553473 "--body" , body ,
554474 ], capture_output = True , text = True , check = True )
555475
556- pr_url = extract_pr_url (result .stdout ) or f"PR created for { package } (URL not found in output)"
557-
558- if build_workflow .exists ():
559- for v in new_versions :
560- print (f" [+] Dispatching build-{ package } .yml on { branch } for v{ v } " )
561- dispatch_workflow (f"build-{ package } .yml" , branch , v )
562- if test_workflow .exists ():
563- print (f" [+] Dispatching test-{ package } .yml on { branch } for v{ v } " )
564- dispatch_workflow (f"test-{ package } .yml" , branch , v )
565- else :
566- print (f" [!] No build workflow for { package } ; skipping dispatch" )
567-
568- return pr_url
476+ return extract_pr_url (result .stdout ) or f"PR created for { package } (URL not found in output)"
569477
570478 except subprocess .CalledProcessError as e :
571479 print (f" [X] Error creating upgrade PR for { package } : { e .stderr or e } " )
0 commit comments