@@ -58,6 +58,53 @@ def fetch_ref(repository: str, ref: str) -> str:
5858 return git (('rev-parse' , 'FETCH_HEAD^{commit}' )).stdout .decode ().strip ()
5959
6060
61+ def release_version (tag : str ) -> tuple [int , int , int ] | None :
62+ if tag .startswith ('refs/tags/' ):
63+ tag = tag .removeprefix ('refs/tags/' )
64+ if not tag .startswith ('v' ):
65+ return None
66+
67+ parts = tag .removeprefix ('v' ).split ('.' )
68+ if len (parts ) != 3 or not all (part .isdecimal () for part in parts ):
69+ return None
70+
71+ major , minor , patch = parts
72+ return (int (major ), int (minor ), int (patch ))
73+
74+
75+ def latest_stable_release (repository : str ) -> str :
76+ output = git (('ls-remote' , '--tags' , '--refs' , repository , 'v*.*.*' )).stdout .decode ()
77+ releases : list [tuple [tuple [int , int , int ], str ]] = []
78+ for line in output .splitlines ():
79+ try :
80+ _ , ref = line .split (None , maxsplit = 1 )
81+ except ValueError :
82+ continue
83+
84+ version = release_version (ref )
85+ if version is not None :
86+ releases .append ((version , ref .removeprefix ('refs/tags/' )))
87+
88+ if not releases :
89+ raise SyncError (f'could not find stable Node.js release tags in { repository } ' )
90+
91+ return max (releases )[1 ]
92+
93+
94+ def resolve_target_ref (repository : str , ref : str ) -> str :
95+ if not ref :
96+ return latest_stable_release (repository )
97+
98+ return ref
99+
100+
101+ def target_version (ref : str , sha : str ) -> str :
102+ if release_version (ref ) is not None :
103+ return ref .removeprefix ('refs/tags/' )
104+
105+ return sha [:12 ]
106+
107+
61108def load_state (path : Path ) -> str | None :
62109 if not path .exists ():
63110 return None
@@ -180,8 +227,10 @@ def sync(args: argparse.Namespace) -> int:
180227 if base_ref is None :
181228 raise SyncError (f'{ state_path } does not record a node_commit; pass --base-node-ref to bootstrap the sync' )
182229
230+ target_ref = resolve_target_ref (args .node_repository , args .node_ref )
183231 base_sha = fetch_ref (args .node_repository , base_ref )
184- target_sha = fetch_ref (args .node_repository , args .node_ref )
232+ target_sha = fetch_ref (args .node_repository , target_ref )
233+ target_node_version = target_version (target_ref , target_sha )
185234
186235 check_unmapped_files (target_sha )
187236
@@ -196,7 +245,7 @@ def sync(args: argparse.Namespace) -> int:
196245 )
197246
198247 conflicts : list [str ] = []
199- would_change = current_state != target_sha
248+ would_change_mapped_files = False
200249 with tempfile .TemporaryDirectory (prefix = 'sync-node-ncrypto-' ) as temporary_directory_name :
201250 temporary_directory = Path (temporary_directory_name )
202251 for source , destination in MAPPINGS .items ():
@@ -208,30 +257,35 @@ def sync(args: argparse.Namespace) -> int:
208257 temporary_directory = temporary_directory ,
209258 )
210259 if destination .read_bytes () != merged :
211- would_change = True
260+ would_change_mapped_files = True
212261 if not args .dry_run :
213262 destination .write_bytes (merged )
214263 if conflicted :
215264 conflicts .append (str (destination ))
216265
217- if not args .dry_run :
266+ paths = list (MAPPINGS .values ())
267+ changed = would_change_mapped_files if args .dry_run else has_changes (paths )
268+
269+ if not args .dry_run and changed :
218270 write_state (state_path , target_sha )
219271
220- paths = [* MAPPINGS .values (), state_path ]
221- changed = would_change if args .dry_run else has_changes (paths )
222272 outputs = {
223273 'base_sha' : base_sha ,
224274 'target_sha' : target_sha ,
225275 'target_short_sha' : target_sha [:12 ],
276+ 'target_ref' : target_ref ,
277+ 'target_version' : target_node_version ,
226278 'has_changes' : changed ,
227279 'has_conflicts' : bool (conflicts ),
228280 'conflicts' : conflicts ,
229- 'branch_name' : f'sync-node-ncrypto/{ target_sha [: 12 ] } ' ,
281+ 'branch_name' : f'sync-node-ncrypto/{ target_node_version } ' ,
230282 }
231283 write_github_output (outputs )
232284
233285 print (f'Base node commit: { base_sha } ' )
234286 print (f'Target node commit: { target_sha } ' )
287+ print (f'Target node ref: { target_ref } ' )
288+ print (f'Target Node.js: { target_node_version } ' )
235289 print (f'Changed files: { str (changed ).lower ()} ' )
236290 print (f'Conflicts: { str (bool (conflicts )).lower ()} ' )
237291 for path in conflicts :
@@ -243,7 +297,11 @@ def sync(args: argparse.Namespace) -> int:
243297def parse_args (argv : Sequence [str ]) -> argparse .Namespace :
244298 parser = argparse .ArgumentParser (description = 'Sync nodejs/node deps/ncrypto into standalone ncrypto.' )
245299 parser .add_argument ('--node-repository' , default = NODE_REPOSITORY )
246- parser .add_argument ('--node-ref' , default = 'main' )
300+ parser .add_argument (
301+ '--node-ref' ,
302+ default = '' ,
303+ help = 'nodejs/node ref to sync from; defaults to latest stable release' ,
304+ )
247305 parser .add_argument ('--base-node-ref' , default = '' )
248306 parser .add_argument ('--state-file' , default = str (STATE_FILE ))
249307 parser .add_argument ('--dry-run' , action = 'store_true' )
0 commit comments