3737 "Compatibility" ,
3838]
3939
40+ TRUTHY_ENV_VALUES = {"1" , "true" , "yes" , "on" }
41+ FALSY_ENV_VALUES = {"" , "0" , "false" , "no" , "off" }
42+
43+ NEXT_STEP_STAGES = (
44+ "crate-after-bump" ,
45+ "crate-after-check-release" ,
46+ "crate-after-tag" ,
47+ "crate-after-push-tag" ,
48+ "schema-after-bump" ,
49+ "schema-after-check-schema" ,
50+ "schema-after-tag" ,
51+ "schema-after-push-tag" ,
52+ )
53+
54+ RELEASE_WORKFLOW_URL = (
55+ "https://github.com/smorinlabs/envgen/actions/workflows/release.yml"
56+ )
57+
4058
4159class BumpError (RuntimeError ):
4260 """Raised when the bump flow should fail with a user-facing error."""
@@ -46,6 +64,145 @@ def fail(message: str) -> None:
4664 raise BumpError (message )
4765
4866
67+ def env_var_truthy (name : str ) -> bool :
68+ value = os .environ .get (name )
69+ if value is None :
70+ return False
71+ return value .strip ().lower () not in FALSY_ENV_VALUES
72+
73+
74+ def parse_hint_override () -> bool | None :
75+ value = os .environ .get ("ENVGEN_HINTS" )
76+ if value is None :
77+ return None
78+
79+ normalized = value .strip ().lower ()
80+ if normalized in TRUTHY_ENV_VALUES :
81+ return True
82+ if normalized in FALSY_ENV_VALUES :
83+ return False
84+ return None
85+
86+
87+ def hints_enabled () -> bool :
88+ override = parse_hint_override ()
89+ if override is not None :
90+ return override
91+ if env_var_truthy ("CI" ):
92+ return False
93+ return sys .stdout .isatty ()
94+
95+
96+ def render_next_step (
97+ stage : str ,
98+ * ,
99+ crate_version : str | None = None ,
100+ schema_version : str | None = None ,
101+ tag_name : str | None = None ,
102+ ) -> tuple [str , list [str ]]:
103+ if stage == "crate-after-bump" :
104+ resolved_crate_version = crate_version or read_cargo_version ()
105+ return (
106+ f"Crate release prep updated to v{ resolved_crate_version } ." ,
107+ ["$ make check-release" ],
108+ )
109+
110+ if stage == "crate-after-check-release" :
111+ resolved_crate_version = crate_version or read_cargo_version ()
112+ return (
113+ f"Release readiness checks passed for crate v{ resolved_crate_version } ." ,
114+ [
115+ "$ git add Cargo.toml Cargo.lock CHANGELOG.md" ,
116+ f'$ git commit -m "chore(release): bump crate to v{ resolved_crate_version } "' ,
117+ "$ git push origin main" ,
118+ "$ make tag-crate" ,
119+ ],
120+ )
121+
122+ if stage == "crate-after-tag" :
123+ resolved_crate_version = crate_version or read_cargo_version ()
124+ resolved_tag_name = tag_name or f"v{ resolved_crate_version } "
125+ return (
126+ f"Local crate tag created: { resolved_tag_name } ." ,
127+ ["$ make push-tag-crate" ],
128+ )
129+
130+ if stage == "crate-after-push-tag" :
131+ resolved_crate_version = crate_version or read_cargo_version ()
132+ resolved_tag_name = tag_name or f"v{ resolved_crate_version } "
133+ return (
134+ f"Crate tag pushed to origin: { resolved_tag_name } ." ,
135+ [
136+ "Release workflow should trigger automatically from this tag push." ,
137+ f"Monitor: { RELEASE_WORKFLOW_URL } " ,
138+ ],
139+ )
140+
141+ if stage == "schema-after-bump" :
142+ resolved_schema_version = schema_version or read_schema_version_file ()
143+ return (
144+ f"Schema release prep updated to v{ resolved_schema_version } ." ,
145+ ["$ make check-schema" ],
146+ )
147+
148+ if stage == "schema-after-check-schema" :
149+ resolved_schema_version = schema_version or read_schema_version_file ()
150+ schema_file = f"schemas/envgen.schema.v{ resolved_schema_version } .json"
151+ return (
152+ f"Schema checks passed for artifact v{ resolved_schema_version } ." ,
153+ [
154+ f"$ git add SCHEMA_VERSION SCHEMA_CHANGELOG.md { schema_file } " ,
155+ f'$ git commit -m "chore(schema): schema-v{ resolved_schema_version } "' ,
156+ "$ git push origin main" ,
157+ "$ make tag-schema" ,
158+ ],
159+ )
160+
161+ if stage == "schema-after-tag" :
162+ resolved_schema_version = schema_version or read_schema_version_file ()
163+ resolved_tag_name = tag_name or f"schema-v{ resolved_schema_version } "
164+ return (
165+ f"Local schema tag created: { resolved_tag_name } ." ,
166+ ["$ make push-tag-schema" ],
167+ )
168+
169+ if stage == "schema-after-push-tag" :
170+ resolved_schema_version = schema_version or read_schema_version_file ()
171+ resolved_tag_name = tag_name or f"schema-v{ resolved_schema_version } "
172+ return (
173+ f"Schema tag pushed to origin: { resolved_tag_name } ." ,
174+ [
175+ "Schema tag pushes do not trigger crates.io publishing." ,
176+ "Create and push a crate tag (vX.Y.Z) when you want a crate release." ,
177+ ],
178+ )
179+
180+ fail (f"Unsupported next-step stage: { stage } " )
181+
182+
183+ def emit_next_step (
184+ stage : str ,
185+ * ,
186+ crate_version : str | None = None ,
187+ schema_version : str | None = None ,
188+ tag_name : str | None = None ,
189+ ) -> None :
190+ if not hints_enabled ():
191+ return
192+
193+ summary , lines = render_next_step (
194+ stage ,
195+ crate_version = crate_version ,
196+ schema_version = schema_version ,
197+ tag_name = tag_name ,
198+ )
199+ print ("" )
200+ print (f"Hint: { summary } " )
201+ print ("Next:" )
202+ for line in lines :
203+ print (f" { line } " )
204+
205+
49206def write_atomic (path : Path , content : str ) -> None :
50207 tmp_path = path .with_suffix (path .suffix + ".tmp" )
51208 tmp_path .write_text (content , encoding = "utf-8" )
@@ -344,6 +501,7 @@ def do_bump_crate(args: argparse.Namespace) -> None:
344501 print (f"crate version: { old } -> { new } " )
345502 print (f"updated: { CARGO_TOML } " )
346503 print (f"updated: { CHANGELOG } " )
504+ emit_next_step ("crate-after-bump" , crate_version = new )
347505
348506
349507def do_bump_schema (args : argparse .Namespace ) -> None :
@@ -386,34 +544,43 @@ def do_bump_schema(args: argparse.Namespace) -> None:
386544 print (f"updated: { SCHEMA_VERSION_FILE } " )
387545 print (f"updated: { SCHEMA_CHANGELOG } " )
388546 print (f"renamed: { old_schema_path } -> { new_schema_path } " )
547+ emit_next_step ("schema-after-bump" , schema_version = new )
389548
390549
391550def do_tag_crate (args : argparse .Namespace ) -> None :
392551 version = resolve_tag_crate_version (require_release_section = True )
393552 tag_name = f"v{ version } "
394553 create_tag (tag_name , f"release { tag_name } " , args .dry_run )
395554 print (f"created local tag: { tag_name } " )
555+ emit_next_step ("crate-after-tag" , crate_version = version , tag_name = tag_name )
396556
397557
398558def do_push_tag_crate (args : argparse .Namespace ) -> None :
399559 version = resolve_tag_crate_version (require_release_section = False )
400560 tag_name = f"v{ version } "
401561 push_tag (tag_name , args .dry_run )
402562 print (f"pushed tag: { tag_name } " )
563+ emit_next_step ("crate-after-push-tag" , crate_version = version , tag_name = tag_name )
403564
404565
405566def do_tag_schema (args : argparse .Namespace ) -> None :
406567 version = resolve_tag_schema_version (require_release_section = True )
407568 tag_name = f"schema-v{ version } "
408569 create_tag (tag_name , f"schema release { tag_name } " , args .dry_run )
409570 print (f"created local tag: { tag_name } " )
571+ emit_next_step ("schema-after-tag" , schema_version = version , tag_name = tag_name )
410572
411573
412574def do_push_tag_schema (args : argparse .Namespace ) -> None :
413575 version = resolve_tag_schema_version (require_release_section = False )
414576 tag_name = f"schema-v{ version } "
415577 push_tag (tag_name , args .dry_run )
416578 print (f"pushed tag: { tag_name } " )
579+ emit_next_step ("schema-after-push-tag" , schema_version = version , tag_name = tag_name )
580+
581+
582+ def do_next_step (args : argparse .Namespace ) -> None :
583+ emit_next_step (args .stage )
417584
418585
419586def build_parser () -> argparse .ArgumentParser :
@@ -423,6 +590,13 @@ def build_parser() -> argparse.ArgumentParser:
423590 status = subparsers .add_parser ("status" , help = "Show current crate/schema versions" )
424591 status .set_defaults (func = do_status )
425592
593+ next_step = subparsers .add_parser (
594+ "next-step" ,
595+ help = "Print guided next-step release hints for a flow stage" ,
596+ )
597+ next_step .add_argument ("--stage" , required = True , choices = NEXT_STEP_STAGES )
598+ next_step .set_defaults (func = do_next_step )
599+
426600 bump_crate = subparsers .add_parser ("bump-crate" , help = "Bump crate version + CHANGELOG" )
427601 bump_crate .add_argument ("--level" , choices = ["patch" , "minor" , "major" ])
428602 bump_crate .add_argument ("--version" )
0 commit comments