Skip to content

Commit eb79f72

Browse files
committed
Release Wizard improvements for 9.10.0
- support for persisting vars in commands - changed logchange steps
1 parent ab3667c commit eb79f72

File tree

2 files changed

+225
-80
lines changed

2 files changed

+225
-80
lines changed

dev-tools/scripts/releaseWizard.py

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
import holidays
5757
import yaml
5858
from ics import Calendar, Event
59-
from jinja2 import Environment
59+
from jinja2 import Environment, Undefined
6060
except:
6161
print("You lack some of the module dependencies to run this script.")
6262
print("Please run 'pip3 install -r requirements.txt' and try again.")
@@ -74,6 +74,25 @@
7474
templates = None
7575
solr_news_file = None
7676

77+
78+
class ReadableUndefined(Undefined):
79+
"""Custom Undefined handler that renders undefined variables as {{ varname }}
80+
81+
This allows users to see which variables are not yet defined when displaying
82+
command templates before execution, particularly useful for persist_vars
83+
that haven't been captured yet.
84+
"""
85+
def __str__(self):
86+
return "{{ %s }}" % self._undefined_name
87+
88+
def __getattr__(self, name):
89+
# Handle special Python attributes normally
90+
if name[:2] == '__':
91+
raise AttributeError(name)
92+
# Chain undefined attribute access for nested vars like {{ todo_id.var_name }}
93+
return ReadableUndefined(name="%s.%s" % (self._undefined_name, name))
94+
95+
7796
# Edit this to add other global jinja2 variables or filters
7897
def expand_jinja(text, vars=None):
7998
global_vars = OrderedDict({
@@ -141,7 +160,7 @@ def expand_jinja(text, vars=None):
141160
filled = replace_templates(text)
142161

143162
try:
144-
env = Environment(lstrip_blocks=True, keep_trailing_newline=False, trim_blocks=True)
163+
env = Environment(lstrip_blocks=True, keep_trailing_newline=False, trim_blocks=True, undefined=ReadableUndefined)
145164
env.filters['path_join'] = lambda paths: os.path.join(*paths)
146165
env.filters['expanduser'] = lambda path: os.path.expanduser(path)
147166
env.filters['formatdate'] = lambda date: (datetime.strftime(date, "%-d %B %Y") if date else "<date>" )
@@ -1420,19 +1439,22 @@ def tail_file(file, lines):
14201439
break
14211440

14221441

1423-
def run_with_log_tail(command, cwd, logfile=None, tail_lines=10, tee=False, live=False, shell=None):
1442+
def run_with_log_tail(command, cwd, logfile=None, tail_lines=10, tee=False, live=False, shell=None, capture_output=False):
14241443
fh = sys.stdout
14251444
if logfile:
14261445
logdir = os.path.dirname(logfile)
14271446
if not os.path.exists(logdir):
14281447
os.makedirs(logdir)
14291448
fh = open(logfile, 'w')
1430-
rc = run_follow(command, cwd, fh=fh, tee=tee, live=live, shell=shell)
1449+
1450+
rc, captured_output = run_follow(command, cwd, fh=fh, tee=tee, live=live, shell=shell, capture_output=capture_output)
1451+
14311452
if logfile:
14321453
fh.close()
14331454
if not tee and tail_lines and tail_lines > 0:
14341455
tail_file(logfile, tail_lines)
1435-
return rc
1456+
1457+
return rc, captured_output
14361458

14371459

14381460
def ask_yes_no(text):
@@ -1463,7 +1485,7 @@ def print_line_cr(line, linenum, stdout=True, tee=False):
14631485
print(line.rstrip())
14641486

14651487

1466-
def run_follow(command, cwd=None, fh=sys.stdout, tee=False, live=False, shell=None):
1488+
def run_follow(command, cwd=None, fh=sys.stdout, tee=False, live=False, shell=None, capture_output=False):
14671489
doShell = '&&' in command or '&' in command or shell is not None
14681490
if not doShell and not isinstance(command, list):
14691491
command = shlex.split(command)
@@ -1479,6 +1501,7 @@ def run_follow(command, cwd=None, fh=sys.stdout, tee=False, live=False, shell=No
14791501

14801502
endstdout = endstderr = False
14811503
errlines = []
1504+
captured_lines = [] if capture_output else None
14821505
while not (endstderr and endstdout):
14831506
lines_before = lines_written
14841507
if not endstdout:
@@ -1490,6 +1513,8 @@ def run_follow(command, cwd=None, fh=sys.stdout, tee=False, live=False, shell=No
14901513
else:
14911514
fh.write(chars)
14921515
fh.flush()
1516+
if capture_output:
1517+
captured_lines.append(chars)
14931518
if '\n' in chars:
14941519
lines_written += 1
14951520
else:
@@ -1499,6 +1524,8 @@ def run_follow(command, cwd=None, fh=sys.stdout, tee=False, live=False, shell=No
14991524
else:
15001525
fh.write("%s\n" % line.rstrip())
15011526
fh.flush()
1527+
if capture_output:
1528+
captured_lines.append(line)
15021529
lines_written += 1
15031530
print_line_cr(line, lines_written, stdout=(fh == sys.stdout), tee=tee)
15041531

@@ -1536,7 +1563,12 @@ def run_follow(command, cwd=None, fh=sys.stdout, tee=False, live=False, shell=No
15361563
for line in errlines:
15371564
fh.write("%s\n" % line.rstrip())
15381565
fh.flush()
1539-
return rc
1566+
1567+
captured_output = None
1568+
if capture_output and captured_lines is not None:
1569+
captured_output = "".join(captured_lines)
1570+
1571+
return rc, captured_output
15401572

15411573

15421574
def is_windows():
@@ -1637,6 +1669,7 @@ def run(self): # pylint: disable=inconsistent-return-statements # TODO
16371669
logfilename = cmd.logfile
16381670
logfile = None
16391671
cmd_to_run = "%s%s" % ("echo Dry run, command is: " if dry_run else "", cmd.get_cmd())
1672+
need_capture = cmd.persist_vars and not dry_run
16401673
if cmd.redirect:
16411674
try:
16421675
out = run(cmd_to_run, cwd=cwd)
@@ -1645,6 +1678,7 @@ def run(self): # pylint: disable=inconsistent-return-statements # TODO
16451678
outfile.write(out)
16461679
outfile.flush()
16471680
print("Wrote %s bytes to redirect file %s" % (len(out), cmd.get_redirect()))
1681+
cmd_output = out
16481682
except Exception as e:
16491683
print("Command %s failed: %s" % (cmd_to_run, e))
16501684
success = False
@@ -1668,8 +1702,8 @@ def run(self): # pylint: disable=inconsistent-return-statements # TODO
16681702
if cmd.comment:
16691703
print("# %s\n" % cmd.get_comment())
16701704
start_time = time.time()
1671-
returncode = run_with_log_tail(cmd_to_run, cwd, logfile=logfile, tee=cmd.tee, tail_lines=25,
1672-
live=cmd.live, shell=cmd.shell)
1705+
returncode, cmd_output = run_with_log_tail(cmd_to_run, cwd, logfile=logfile, tee=cmd.tee, tail_lines=25,
1706+
live=cmd.live, shell=cmd.shell, capture_output=need_capture)
16731707
elapsed = time.time() - start_time
16741708
if not returncode == 0:
16751709
if cmd.should_fail:
@@ -1684,9 +1718,23 @@ def run(self): # pylint: disable=inconsistent-return-statements # TODO
16841718
print("Expected command to fail, but it succeeded.")
16851719
success = False
16861720
break
1687-
else:
1688-
if elapsed > 30:
1689-
print("Command completed in %s seconds" % elapsed)
1721+
1722+
# Handle persist_vars: capture stdout and parse for --wizard-var markers
1723+
if cmd.persist_vars and not dry_run and cmd_output:
1724+
try:
1725+
parsed_vars = parse_wizard_vars(cmd_output)
1726+
if parsed_vars:
1727+
todo = state.get_todo_by_id(self.todo_id)
1728+
if todo:
1729+
for var_name, var_value in parsed_vars.items():
1730+
todo.state[var_name] = var_value
1731+
state.save()
1732+
for var_name, var_value in parsed_vars.items():
1733+
print("Saved variable '%s' = '%s'" % (var_name, var_value))
1734+
except Exception as e:
1735+
print("WARNING: Failed to persist variables: %s" % e)
1736+
if elapsed > 30:
1737+
print("Command completed in %s seconds" % elapsed)
16901738
if not success:
16911739
print("WARNING: One or more commands failed, you may want to check the logs")
16921740
return success
@@ -1719,7 +1767,7 @@ def jinjaify(self, data, join=False):
17191767
return None
17201768
v = self.get_vars()
17211769
if self.todo_id:
1722-
v.update(state.get_todo_by_id(self.todo_id).get_vars())
1770+
v.update(state.get_todo_by_id(self.todo_id).get_vars_and_state())
17231771
if isinstance(data, list):
17241772
if join:
17251773
return expand_jinja(" ".join(data), v)
@@ -1744,11 +1792,37 @@ def abbreviate_homedir(line):
17441792
return re.sub(r'([^/]|\b)%s' % os.path.expanduser('~'), "\\1~", line)
17451793

17461794

1795+
def parse_wizard_vars(stdout_text):
1796+
"""Parse --wizard-var markers from command stdout.
1797+
1798+
Format: --wizard-var KEY=VALUE
1799+
1800+
Returns a dict of extracted variables, with last value winning for duplicates.
1801+
"""
1802+
variables = {}
1803+
if not stdout_text:
1804+
return variables
1805+
1806+
for line in stdout_text.splitlines():
1807+
# Check if line starts with --wizard-var marker
1808+
if line.startswith("--wizard-var "):
1809+
# Extract the KEY=VALUE part
1810+
var_part = line[len("--wizard-var "):].strip()
1811+
if '=' in var_part:
1812+
key, _, value = var_part.partition('=')
1813+
key = key.strip()
1814+
value = value.strip()
1815+
if key: # Only store if key is not empty
1816+
variables[key] = value
1817+
1818+
return variables
1819+
1820+
17471821
class Command(SecretYamlObject):
17481822
yaml_tag = u'!Command'
17491823
hidden_fields = ['todo_id']
17501824
def __init__(self, cmd, cwd=None, stdout=None, logfile=None, tee=None, live=None, comment=None, vars=None,
1751-
todo_id=None, should_fail=None, redirect=None, redirect_append=None, shell=None):
1825+
todo_id=None, should_fail=None, redirect=None, redirect_append=None, shell=None, persist_vars=None):
17521826
self.cmd = cmd
17531827
self.cwd = cwd
17541828
self.comment = comment
@@ -1762,6 +1836,7 @@ def __init__(self, cmd, cwd=None, stdout=None, logfile=None, tee=None, live=None
17621836
self.todo_id = todo_id
17631837
self.redirect_append = redirect_append
17641838
self.redirect = redirect
1839+
self.persist_vars = persist_vars
17651840
if tee and stdout:
17661841
self.stdout = None
17671842
print("Command %s specifies 'tee' and 'stdout', using only 'tee'" % self.cmd)
@@ -1806,7 +1881,7 @@ def __str__(self):
18061881
def jinjaify(self, data, join=False):
18071882
v = self.get_vars()
18081883
if self.todo_id:
1809-
v.update(state.get_todo_by_id(self.todo_id).get_vars())
1884+
v.update(state.get_todo_by_id(self.todo_id).get_vars_and_state())
18101885
if isinstance(data, list):
18111886
if join:
18121887
return expand_jinja(" ".join(data), v)

0 commit comments

Comments
 (0)