Examples
The following examples illustrate both the usage and the utility of
staged-script. They build on each other in complexity, so it will
make the most sense to start here at the top and work your way down
through each in turn.
The Basics
We’ll start with what amounts to a “Hello World” example, with two simple stages to say “hello” and “goodbye”.
example/ex_0_the_basics.py 1import sys
2
3from staged_script import StagedScript
4
5
6class MyScript(StagedScript):
7 @StagedScript.stage("hello", "Greeting the user")
8 def say_hello(self) -> None:
9 self.run("echo 'Hello World'", shell=True)
10
11 @StagedScript.stage("goodbye", "Bidding farewell")
12 def say_goodbye(self) -> None:
13 self.run("echo 'Goodbye World'", shell=True)
14
15 def main(self, argv: list[str]) -> None:
16 self.parse_args(argv)
17 try:
18 self.say_hello()
19 self.say_goodbye()
20 finally:
21 self.print_script_execution_summary()
22
23
24if __name__ == "__main__":
25 my_script = MyScript({"hello", "goodbye"})
26 my_script.main(sys.argv[1:])
The two methods say_hello() and say_goodbye() are stand-ins for
whatever you might want to do in the stages of your script. Note that
in this case they are simply running commands in the underlying
shell, but you could instead include
whatever Python code you want in here.
The main() method specifies the general form of the script, where
you first parse the command line arguments, then try to execute a series of
stages (lines 20–21), and finally print the script execution
summary, regardless of whether
anything went wrong in any of the stages. You’re welcome to construct
your scripts however you like—this general form is just a
recommendation.
Running the script, and passing the --help argument to it, yields
the following:
$ python3 ../../example/ex_0_the_basics.py --help
usage: ex_0_the_basics.py [-h] [--stage {goodbye,hello} [{goodbye,hello} ...]]
[--dry-run]
[--goodbye-retry-attempts GOODBYE_RETRY_ATTEMPTS]
[--goodbye-retry-delay GOODBYE_RETRY_DELAY]
[--goodbye-retry-timeout GOODBYE_RETRY_TIMEOUT]
[--hello-retry-attempts HELLO_RETRY_ATTEMPTS]
[--hello-retry-delay HELLO_RETRY_DELAY]
[--hello-retry-timeout HELLO_RETRY_TIMEOUT]
This is the description of the ArgumentParser in the StagedScript base class. This should be overridden in your subclass. See the docstring for details.
options:
-h, --help show this help message and exit
--stage {goodbye,hello} [{goodbye,hello} ...]
Which stages to run. (default: None)
--dry-run If specified, don't actually run the commands in the
shell; instead print the commands that would have been
executed. (default: False)
retry:
Additional options for retrying stages.
--goodbye-retry-attempts GOODBYE_RETRY_ATTEMPTS
How many times to retry the 'goodbye' stage. (default:
0)
--goodbye-retry-delay GOODBYE_RETRY_DELAY
How long to wait (in seconds) before retrying the
'goodbye' stage. (default: 0)
--goodbye-retry-timeout GOODBYE_RETRY_TIMEOUT
How long to wait (in seconds) before giving up on
retrying the 'goodbye' stage. (default: 60)
--hello-retry-attempts HELLO_RETRY_ATTEMPTS
How many times to retry the 'hello' stage. (default:
0)
--hello-retry-delay HELLO_RETRY_DELAY
How long to wait (in seconds) before retrying the
'hello' stage. (default: 0)
--hello-retry-timeout HELLO_RETRY_TIMEOUT
How long to wait (in seconds) before giving up on
retrying the 'hello' stage. (default: 60)
If you tell it you only want to run the hello stage, you’ll see
$ python3 ../../example/ex_0_the_basics.py --stage hello
[15:27:44] ╭──────────────────────────────────────────────╮ staged_script.py:934
│ Greeting the user │
╰──────────────────────────────────────────────╯
Executing: echo 'Hello World' staged_script.py:824
Hello World
`hello` stage duration: 0:00:00.004040 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Bidding farewell │
╰──────────────────────────────────────────────╯
Skipping this stage. staged_script.py:438
`goodbye` stage duration: 0:00:00.001172 staged_script.py:473
───────────────── ex_0_the_basics.py Script Execution Summary ──────────────────
staged_script.py:917
➤ Ran the following:
ex_0_the_basics.py \
--stage hello \
--goodbye-retry-attempts 0 \
--goodbye-retry-delay 0 \
--goodbye-retry-timeout 60 \
--hello-retry-attempts 0 \
--hello-retry-delay 0 \
--hello-retry-timeout 60
➤ Commands executed:
echo 'Hello World'
➤ Timing results:
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Stage ┃ Duration ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ hello │ 0:00:00.004040 │
│ goodbye │ 0:00:00.001172 │
├─────────┼────────────────┤
│ Total │ 0:00:00.007240 │
└─────────┴────────────────┘
➤ Script result:
Success
─────────────── End ex_0_the_basics.py Script Execution Summary ────────────────
Note
It’s worth noting that StagedScript uses a
rich.console.Console for all output, so you may want to
familiarize yourself with the Rich documentation for the sake of
customizing the StagedScript.console attribute.
Customizing the Parser
Removing the Retry Arguments
For a script as simple as this one, all of the retry business in the
help text and script execution summary are rather distracting, because
our two stages don’t automatically retry themselves. We can
customize the argument parser
by adding the following to the MyScript class:
example/ex_1_removing_the_retry_arguments.py 1 @functools.cached_property
2 def parser(self) -> ArgumentParser:
3 my_parser = super().parser
4 my_parser.description = "Demonstrate removing the retry arguments."
5 self.retry_arg_group.title = argparse.SUPPRESS
6 self.retry_arg_group.description = argparse.SUPPRESS
7 self.hello_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
8 self.hello_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
9 self.hello_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
10 self.goodbye_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
11 self.goodbye_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
12 self.goodbye_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
13 return my_parser
Note
An upcoming release will refactor the retry argument attributes so
mypy will be happy with them. For now, just use the type:
ignore[attr-defined] comments.
Now when we look at the --help text, we see:
$ python3 ../../example/ex_1_removing_the_retry_arguments.py --help
usage: ex_1_removing_the_retry_arguments.py [-h]
[--stage {hello,goodbye} [{hello,goodbye} ...]]
[--dry-run]
Demonstrate removing the retry arguments.
options:
-h, --help show this help message and exit
--stage {hello,goodbye} [{hello,goodbye} ...]
Which stages to run. (default: None)
--dry-run If specified, don't actually run the commands in the
shell; instead print the commands that would have been
executed. (default: False)
This is much nicer from the user perspective. Now if we run the script with the same arguments as last time, we see:
$ python3 ../../example/ex_1_removing_the_retry_arguments.py --stage hello
[15:27:45] ╭──────────────────────────────────────────────╮ staged_script.py:934
│ Greeting the user │
╰──────────────────────────────────────────────╯
Executing: echo 'Hello World' staged_script.py:824
Hello World
`hello` stage duration: 0:00:00.003975 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Bidding farewell │
╰──────────────────────────────────────────────╯
Skipping this stage. staged_script.py:438
`goodbye` stage duration: 0:00:00.001131 staged_script.py:473
──────── ex_1_removing_the_retry_arguments.py Script Execution Summary ─────────
staged_script.py:917
➤ Ran the following:
ex_1_removing_the_retry_arguments.py \
--stage hello
➤ Commands executed:
echo 'Hello World'
➤ Timing results:
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Stage ┃ Duration ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ hello │ 0:00:00.003975 │
│ goodbye │ 0:00:00.001131 │
├─────────┼────────────────┤
│ Total │ 0:00:00.007118 │
└─────────┴────────────────┘
➤ Script result:
Success
────── End ex_1_removing_the_retry_arguments.py Script Execution Summary ───────
Running Certain Stages by Default
By default, a staged script won’t run any stages unless you tell it to.
Keep in mind, this package was designed for improving replicability for
infrastructure automation, so we err on the side of explicit is better
than implicit. That said, you or your users may find this
inconvenient, and would prefer to make it such that certain stages run
by default when you don’t specify --stage on the command line. In
that case, you can add the highlighted line:
example/ex_2_running_certain_stages_by_default.py1 @functools.cached_property
2 def parser(self) -> ArgumentParser:
3 my_parser = super().parser
4 my_parser.set_defaults(stage=list(self.stages))
5 return my_parser
In this case I’m telling it to default the --stage argument to the
list of all the stages registered when instantiating a StagedScript
subclass. However, you could just as easily specify whatever subset of
stages you like here with, e.g., stage=["stage-1", "stage-2"]. Now,
if we run the script without any arguments, we see:
$ python3 ../../example/ex_2_running_certain_stages_by_default.py
[15:27:45] ╭──────────────────────────────────────────────╮ staged_script.py:934
│ Greeting the user │
╰──────────────────────────────────────────────╯
Executing: echo 'Hello World' staged_script.py:824
Hello World
`hello` stage duration: 0:00:00.004034 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Bidding farewell │
╰──────────────────────────────────────────────╯
Executing: echo 'Goodbye World' staged_script.py:824
Goodbye World
`goodbye` stage duration: 0:00:00.001811 staged_script.py:473
────── ex_2_running_certain_stages_by_default.py Script Execution Summary ──────
staged_script.py:917
➤ Ran the following:
ex_2_running_certain_stages_by_default.py \
--stage goodbye hello
➤ Commands executed:
echo 'Hello World'
echo 'Goodbye World'
➤ Timing results:
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Stage ┃ Duration ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ hello │ 0:00:00.004034 │
│ goodbye │ 0:00:00.001811 │
├─────────┼────────────────┤
│ Total │ 0:00:00.007932 │
└─────────┴────────────────┘
➤ Script result:
Success
──── End ex_2_running_certain_stages_by_default.py Script Execution Summary ────
Note
It’s worth noting that the order of the stage names passed to the
--stages argument on the command line does not affect the order
in which the stages are run. That is governed by the contents of the
main() method, as shown in the basic example.
Adding Arguments
Now let’s see about adding some arguments to the parser beyond what
StagedScript provides. We can do this by adding arguments to the
argparse.ArgumentParser as you normally would.
example/ex_3_adding_arguments.py 1 @functools.cached_property
2 def parser(self) -> ArgumentParser:
3 my_parser = super().parser
4 my_parser.add_argument(
5 "--some-file",
6 required=True,
7 type=Path,
8 help="Some file your users need to point to for the script "
9 "to run.",
10 )
11 my_parser.add_argument(
12 "--some-flag",
13 action="store_true",
14 help="Some flag your users can toggle on if they like.",
15 )
16 return my_parser
Beyond that, though, you likely also want to augment the parsing of the arguments to handle these new options. You can do so by extending the parse_args() method provided by the base class.
example/ex_3_adding_arguments.py 1 def parse_args(self, argv: list[str]) -> None:
2 # The base class saves the parsed arguments as `self.args`.
3 super().parse_args(argv)
4
5 # If you like, you may wish to transfer some subset of the added
6 # arguments to instance attributes for convenience.
7 self.flag = self.args.some_flag
8
9 # You may also wish to do additional post-processing of certain
10 # arguments, whether you save them as instance attributes or
11 # not.
12 self.args.some_file = self.args.some_file.resolve()
Note
Here’s we’re taking the file the user gives us on the command line and resolving it to an absolute path. This is again in service of improving replicability, as the script output will then point us directly to the exact file used at execution time, instead of just giving us a file name or relative path.
For the sake of using these new arguments in our script, let’s modify the two stages to take them into account.
example/ex_3_adding_arguments.py 1 @StagedScript.stage("hello", "Greeting the user")
2 def say_hello(self) -> None:
3 self.run("echo 'Hello World'", shell=True)
4 self.console.log(f"Processing file: {self.args.some_file}")
5
6 @StagedScript.stage("goodbye", "Bidding farewell")
7 def say_goodbye(self) -> None:
8 self.run("echo 'Goodbye World'", shell=True)
9 self.console.log(
10 "Some flag was " + ("not " if not self.flag else "") + "set!"
11 )
Now if we run the script, passing in a file on the command line, we see:
$ python3 ../../example/ex_3_adding_arguments.py --some-file foo.txt
[15:27:45] ╭──────────────────────────────────────────────╮ staged_script.py:934
│ Greeting the user │
╰──────────────────────────────────────────────╯
Executing: echo 'Hello World' staged_script.py:824
Hello World
Processing file: ex_3_adding_arguments.py:23
/home/docs/checkouts/readthedocs.org/user
_builds/staged-script/checkouts/v2.0.5/do
c/source/foo.txt
`hello` stage duration: 0:00:00.004816 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Bidding farewell │
╰──────────────────────────────────────────────╯
Executing: echo 'Goodbye World' staged_script.py:824
Goodbye World
Some flag was not set! ex_3_adding_arguments.py:28
`goodbye` stage duration: 0:00:00.002375 staged_script.py:473
────────────── ex_3_adding_arguments.py Script Execution Summary ───────────────
staged_script.py:917
➤ Ran the following:
ex_3_adding_arguments.py \
--stage hello goodbye \
--some-file
/home/docs/checkouts/readthedocs.org/user_bu
ilds/staged-script/checkouts/v2.0.5/doc/sour
ce/foo.txt
➤ Commands executed:
echo 'Hello World'
echo 'Goodbye World'
➤ Timing results:
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Stage ┃ Duration ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ hello │ 0:00:00.004816 │
│ goodbye │ 0:00:00.002375 │
├─────────┼────────────────┤
│ Total │ 0:00:00.009338 │
└─────────┴────────────────┘
➤ Script result:
Success
──────────── End ex_3_adding_arguments.py Script Execution Summary ─────────────
Customizing Stage Behavior
For All Stages
We now move beyond customizing the parser to adjusting the behavior of the stages as they run. Recall from The Conceptual Stage that a stage is broken down into a number of phases, each of which has a corresponding method. These phase methods have reasonable default implementations in the base class, but may be extended or overridden in your subclasses.
example/ex_4_customizing_stage_behavior.py 1 def _run_pre_stage_actions(self) -> None:
2 # You can extend the default implementation by calling it via
3 # `super()` first.
4 super()._run_pre_stage_actions()
5 self.console.log("Checking to make sure it's safe to run a stage...")
6
7 def _skip_stage(self) -> None:
8 # You can override the default implementation if you don't like
9 # it by simply omitting the `super()` call.
10 self.console.log(
11 f"You didn't tell me to run the '{self.current_stage}' stage."
12 )
13
14 def _end_stage(self) -> None:
15 super()._end_stage()
16 self.print_heading(f"Finished stage '{self.current_stage}'.")
17
18 def _run_post_stage_actions(self) -> None:
19 super()._run_post_stage_actions()
20 self.console.log(
21 "Checking to make sure all is well after running the stage..."
22 )
Note
The _begin_stage() method is not included above, meaning it is
neither extended nor overridden, so the base class implementation
will be used.
Now when we run the script, we can see the changed behavior:
$ python3 ../../example/ex_4_customizing_stage_behavior.py --some-file foo.txt --some-flag --stage goodbye
[15:27:45] Checking to make sure it's safe ex_4_customizing_stage_behavior.py:76
to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Greeting the user │
╰──────────────────────────────────────────────╯
You didn't tell me to run the ex_4_customizing_stage_behavior.py:81
'hello' stage.
`hello` stage duration: 0:00:00.001387 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished stage 'hello'. │
╰──────────────────────────────────────────────╯
Checking to make sure all is ex_4_customizing_stage_behavior.py:91
well after running the stage...
Checking to make sure it's safe ex_4_customizing_stage_behavior.py:76
to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Bidding farewell │
╰──────────────────────────────────────────────╯
Executing: echo 'Goodbye World' staged_script.py:824
Goodbye World
Some flag was set! ex_4_customizing_stage_behavior.py:28
`goodbye` stage duration: 0:00:00.002573 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished stage 'goodbye'. │
╰──────────────────────────────────────────────╯
Checking to make sure all is ex_4_customizing_stage_behavior.py:91
well after running the stage...
───────── ex_4_customizing_stage_behavior.py Script Execution Summary ──────────
staged_script.py:917
➤ Ran the following:
ex_4_customizing_stage_behavior.py \
--stage goodbye \
--some-file
/home/docs/checkouts/readthedocs.org/user_bu
ilds/staged-script/checkouts/v2.0.5/doc/sour
ce/foo.txt \
--some-flag
➤ Commands executed:
echo 'Goodbye World'
➤ Timing results:
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Stage ┃ Duration ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ hello │ 0:00:00.001387 │
│ goodbye │ 0:00:00.002573 │
├─────────┼────────────────┤
│ Total │ 0:00:00.011464 │
└─────────┴────────────────┘
➤ Script result:
Success
─────── End ex_4_customizing_stage_behavior.py Script Execution Summary ────────
You’re free to customize the phases of the stage however you like, but generally speaking the phases are useful for doing the following:
Phase |
Actions |
|---|---|
Pre-Stage Actions |
Checking pre-conditions and erroring appropriately if not met |
Begin Stage Actions |
Telling the user what’s about to happen and capturing state |
Skip Stage Actions |
Telling the user what’s happening and why |
End Stage Actions |
Capturing state; potentially displaying information |
Post-Stage Actions |
Checking post-conditions and erroring appropriately if not met |
For Individual Stages
In addition to tweaking the phase implementations for all stages, you
also have the flexibility to tailor things on a stage-by-stage
basis. This can be particularly helpful, e.g.,
in the Pre- and Post-Stage Actions for checking the pre- and
post-conditions specific to each stage. To customize a phase method for
a particular stage, you just need to define a phase method as before,
but then append _STAGE_NAME to the method name, where STAGE_NAME
is the name of the stage as provided to the StagedScript.stage() decorator.
example/ex_5_customizing_individual_stages.py 1 def _begin_stage_hello(self, heading: str) -> None:
2 # You can use whatever `_begin_stage()` method already exists,
3 # either whatever's been overridden or extended in the current
4 # class, or whatever's provided by the base class, and then
5 # extend it.
6 self._begin_stage(heading)
7 self.console.log("The first stage is underway...")
8
9 def _end_stage_goodbye(self) -> None:
10 # Or you can ignore whatever's been overridden/extended in the
11 # current class, and fall back to what's provided by the base
12 # class, and then extend it.
13 super()._end_stage()
14 self.print_heading(f"Finished the final stage: {self.current_stage}")
15
16 # You can also override things completely by omitting any `self`
17 # or `super()` calls to the default method for the corresponding
18 # phase, if you like.
Now when we run both stages we see:
$ python3 ../../example/ex_5_customizing_individual_stages.py --some-file foo.txt
[15:27:45] Checking to make sure it's ex_5_customizing_individual_stages.py:76
safe to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Greeting the user │
╰──────────────────────────────────────────────╯
The first stage is ex_5_customizing_individual_stages.py:101
underway...
Executing: echo 'Hello World' staged_script.py:824
Hello World
Processing file: ex_5_customizing_individual_stages.py:23
/home/docs/checkouts/readthe
docs.org/user_builds/staged-
script/checkouts/v2.0.5/doc/
source/foo.txt
`hello` stage duration: 0:00:00.003595 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished stage 'hello'. │
╰──────────────────────────────────────────────╯
Checking to make sure all is ex_5_customizing_individual_stages.py:91
well after running the
stage...
Checking to make sure it's ex_5_customizing_individual_stages.py:76
safe to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Bidding farewell │
╰──────────────────────────────────────────────╯
Executing: echo 'Goodbye World' staged_script.py:824
Goodbye World
Some flag was not set! ex_5_customizing_individual_stages.py:28
`goodbye` stage duration: 0:00:00.002314 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished the final stage: goodbye │
╰──────────────────────────────────────────────╯
Checking to make sure all is ex_5_customizing_individual_stages.py:91
well after running the
stage...
──────── ex_5_customizing_individual_stages.py Script Execution Summary ────────
staged_script.py:917
➤ Ran the following:
ex_5_customizing_individual_stages.py \
--stage goodbye hello \
--some-file
/home/docs/checkouts/readthedocs.org/user_bu
ilds/staged-script/checkouts/v2.0.5/doc/sour
ce/foo.txt
➤ Commands executed:
echo 'Hello World'
echo 'Goodbye World'
➤ Timing results:
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Stage ┃ Duration ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ hello │ 0:00:00.003595 │
│ goodbye │ 0:00:00.002314 │
├─────────┼────────────────┤
│ Total │ 0:00:00.013484 │
└─────────┴────────────────┘
➤ Script result:
Success
────── End ex_5_customizing_individual_stages.py Script Execution Summary ──────
For Retryable Stages
Now that we’re familiar with the process of customizing stage behavior, we can move on to introducing a retryable phase. For demonstration purposes, we’ll simply add a stage that fails twice before it passes, but in reality you would want to check on how things went when executing a particular stage, and if something happens where a human would look at it and say, “Yes, I understand what happened here; we just need to try again and it should pass,” you can program that logic into the stage itself.
Warning
You don’t want to use this feature to hide problems with your actual code base. If there’s flakiness in your application or infrastructure that’s your fault, you want those problems in your face to incentivize fixing them. However, if there’s flakiness due to various things outside your control (e.g., corporate or external infrastructure that may be unreachable from time to time), this can be a helpful way of hiding such problems from your team and avoiding the “everyone hates CI because it randomly fails” problem.
There’s a good deal to add to our script at this point, so let’s walk
through the pieces one at a time. First we’ll add an __init__()
method, but that’s just so we can keep track of the number of times our
flaky stage has been run.
example/ex_6_creating_retryable_stages.py 1 def __init__(
2 self,
3 stages: set[str],
4 *,
5 console_force_terminal: Optional[bool] = None,
6 console_log_path: bool = True,
7 print_commands: bool = True,
8 ) -> None:
9 super().__init__(
10 stages,
11 console_force_terminal=console_force_terminal,
12 console_log_path=console_log_path,
13 print_commands=print_commands,
14 )
15 self.num_times_flaky_run = 0
Next we’ll add the flaky stage itself. The keys here are raising the
RetryStage exception whenever you detect
something where a human would say, “Just try again,” and setting
self.script_success appropriately.
example/ex_6_creating_retryable_stages.py 1 @StagedScript.stage("flaky", "Trying an error-prone operation")
2 def try_error_prone_operation(self) -> None:
3 self.num_times_flaky_run += 1
4 num_times_to_fail = 2
5 if self.num_times_flaky_run <= num_times_to_fail:
6 self.console.log("[red]Oh no! Something went horribly wrong!")
7 self.script_success = False
8 raise RetryStage
9 self.console.log("[green]Thank goodness, everything worked this time.")
10 self.script_success = True
Next we need to adjust the parser to account for this new stage.
example/ex_6_creating_retryable_stages.py 1 @functools.cached_property
2 def parser(self) -> ArgumentParser:
3 my_parser = super().parser
4 my_parser.description = "Demonstrate adding arguments to the parser."
5 self.hello_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
6 self.hello_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
7 self.hello_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
8 self.goodbye_retry_attempts_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
9 self.goodbye_retry_delay_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
10 self.goodbye_retry_timeout_arg.help = argparse.SUPPRESS # type: ignore[attr-defined]
11 my_parser.set_defaults(
12 stage=list(self.stages),
13 flaky_retry_attempts=5,
14 flaky_retry_delay=1,
15 )
16 my_parser.add_argument(
17 "--some-file",
18 required=True,
19 type=Path,
20 help="Some file your users need to point to for the script "
21 "to run.",
22 )
23 my_parser.add_argument(
24 "--some-flag",
25 action="store_true",
26 help="Some flag your users can toggle on if they like.",
27 )
28 return my_parser
Note that we’ve removed the lines
self.retry_arg_group.title = argparse.SUPPRESS
self.retry_arg_group.description = argparse.SUPPRESS
so that when we look at the help text, we see:
$ python3 ../../example/ex_6_creating_retryable_stages.py --help
usage: ex_6_creating_retryable_stages.py [-h]
[--stage {goodbye,hello,flaky} [{goodbye,hello,flaky} ...]]
[--dry-run]
[--flaky-retry-attempts FLAKY_RETRY_ATTEMPTS]
[--flaky-retry-delay FLAKY_RETRY_DELAY]
[--flaky-retry-timeout FLAKY_RETRY_TIMEOUT]
--some-file SOME_FILE [--some-flag]
Demonstrate adding arguments to the parser.
options:
-h, --help show this help message and exit
--stage {goodbye,hello,flaky} [{goodbye,hello,flaky} ...]
Which stages to run. (default: ['goodbye', 'hello',
'flaky'])
--dry-run If specified, don't actually run the commands in the
shell; instead print the commands that would have been
executed. (default: False)
--some-file SOME_FILE
Some file your users need to point to for the script
to run. (default: None)
--some-flag Some flag your users can toggle on if they like.
(default: False)
retry:
Additional options for retrying stages.
--flaky-retry-attempts FLAKY_RETRY_ATTEMPTS
How many times to retry the 'flaky' stage. (default:
5)
--flaky-retry-delay FLAKY_RETRY_DELAY
How long to wait (in seconds) before retrying the
'flaky' stage. (default: 1)
--flaky-retry-timeout FLAKY_RETRY_TIMEOUT
How long to wait (in seconds) before giving up on
retrying the 'flaky' stage. (default: 60)
Now when we run all the stages, we see:
$ python3 ../../example/ex_6_creating_retryable_stages.py --some-file foo.txt
[15:27:46] Checking to make sure it's safe ex_6_creating_retryable_stages.py:108
to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Greeting the user │
╰──────────────────────────────────────────────╯
The first stage is underway... ex_6_creating_retryable_stages.py:133
Executing: echo 'Hello World' staged_script.py:824
Hello World
Processing file: ex_6_creating_retryable_stages.py:42
/home/docs/checkouts/readthedocs
.org/user_builds/staged-script/c
heckouts/v2.0.5/doc/source/foo.t
xt
`hello` stage duration: 0:00:00.003583 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished stage 'hello'. │
╰──────────────────────────────────────────────╯
Checking to make sure all is ex_6_creating_retryable_stages.py:123
well after running the stage...
Checking to make sure it's safe ex_6_creating_retryable_stages.py:108
to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Trying an error-prone operation │
╰──────────────────────────────────────────────╯
Oh no! Something went horribly ex_6_creating_retryable_stages.py:49
wrong!
`flaky` stage duration: 0:00:00.001217 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished stage 'flaky'. │
╰──────────────────────────────────────────────╯
Preparing to retry the 'flaky' stage... staged_script.py:543
<RetryCallState 128507770346848: attempt #1;
slept for 1.0; last result: failed (RetryStage
)>
[15:27:47] ╭──────────────────────────────────────────────╮ staged_script.py:934
│ Trying an error-prone operation │
╰──────────────────────────────────────────────╯
Oh no! Something went horribly ex_6_creating_retryable_stages.py:49
wrong!
`flaky` stage duration: 0:00:00.001692 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished stage 'flaky'. │
╰──────────────────────────────────────────────╯
Preparing to retry the 'flaky' stage... staged_script.py:543
<RetryCallState 128507770346848: attempt #2;
slept for 2.0; last result: failed (RetryStage
)>
[15:27:48] ╭──────────────────────────────────────────────╮ staged_script.py:934
│ Trying an error-prone operation │
╰──────────────────────────────────────────────╯
Thank goodness, everything ex_6_creating_retryable_stages.py:52
worked this time.
`flaky` stage duration: 0:00:00.001668 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished stage 'flaky'. │
╰──────────────────────────────────────────────╯
Checking to make sure all is ex_6_creating_retryable_stages.py:123
well after running the stage...
Checking to make sure it's safe ex_6_creating_retryable_stages.py:108
to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Bidding farewell │
╰──────────────────────────────────────────────╯
Executing: echo 'Goodbye World' staged_script.py:824
Goodbye World
Some flag was not set! ex_6_creating_retryable_stages.py:58
`goodbye` stage duration: 0:00:00.002742 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished the final stage: goodbye │
╰──────────────────────────────────────────────╯
Checking to make sure all is ex_6_creating_retryable_stages.py:123
well after running the stage...
────────── ex_6_creating_retryable_stages.py Script Execution Summary ──────────
staged_script.py:917
➤ Ran the following:
ex_6_creating_retryable_stages.py \
--stage flaky goodbye hello \
--flaky-retry-attempts 5 \
--flaky-retry-delay 1 \
--flaky-retry-timeout 60 \
--some-file
/home/docs/checkouts/readthedocs.org/user_bu
ilds/staged-script/checkouts/v2.0.5/doc/sour
ce/foo.txt
➤ Commands executed:
echo 'Hello World'
echo 'Goodbye World'
➤ Timing results:
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Stage ┃ Duration ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ hello │ 0:00:00.003583 │
│ flaky │ 0:00:00.001217 │
│ flaky │ 0:00:00.001692 │
│ flaky │ 0:00:00.001668 │
│ goodbye │ 0:00:00.002742 │
├─────────┼────────────────┤
│ Total │ 0:00:02.025783 │
└─────────┴────────────────┘
➤ Script result:
Success
──────── End ex_6_creating_retryable_stages.py Script Execution Summary ────────
Note that it took three tries before the flaky stage finally passed. If, however, the user is impatient and doesn’t want to retry the stage, we’d see:
$ python3 ../../example/ex_6_creating_retryable_stages.py --some-file foo.txt --flaky-retry-attempts 0
[15:27:48] Checking to make sure it's safe ex_6_creating_retryable_stages.py:108
to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Greeting the user │
╰──────────────────────────────────────────────╯
The first stage is underway... ex_6_creating_retryable_stages.py:133
Executing: echo 'Hello World' staged_script.py:824
Hello World
Processing file: ex_6_creating_retryable_stages.py:42
/home/docs/checkouts/readthedocs
.org/user_builds/staged-script/c
heckouts/v2.0.5/doc/source/foo.t
xt
`hello` stage duration: 0:00:00.003525 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished stage 'hello'. │
╰──────────────────────────────────────────────╯
Checking to make sure all is ex_6_creating_retryable_stages.py:123
well after running the stage...
Checking to make sure it's safe ex_6_creating_retryable_stages.py:108
to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Trying an error-prone operation │
╰──────────────────────────────────────────────╯
Oh no! Something went horribly ex_6_creating_retryable_stages.py:49
wrong!
`flaky` stage duration: 0:00:00.001175 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished stage 'flaky'. │
╰──────────────────────────────────────────────╯
Checking to make sure all is ex_6_creating_retryable_stages.py:123
well after running the stage...
Checking to make sure it's safe ex_6_creating_retryable_stages.py:108
to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Bidding farewell │
╰──────────────────────────────────────────────╯
Executing: echo 'Goodbye World' staged_script.py:824
Goodbye World
Some flag was not set! ex_6_creating_retryable_stages.py:58
`goodbye` stage duration: 0:00:00.002330 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished the final stage: goodbye │
╰──────────────────────────────────────────────╯
Checking to make sure all is ex_6_creating_retryable_stages.py:123
well after running the stage...
────────── ex_6_creating_retryable_stages.py Script Execution Summary ──────────
staged_script.py:917
➤ Ran the following:
ex_6_creating_retryable_stages.py \
--stage hello goodbye flaky \
--flaky-retry-attempts 0 \
--flaky-retry-delay 1 \
--flaky-retry-timeout 60 \
--some-file
/home/docs/checkouts/readthedocs.org/user_bu
ilds/staged-script/checkouts/v2.0.5/doc/sour
ce/foo.txt
➤ Commands executed:
echo 'Hello World'
echo 'Goodbye World'
➤ Timing results:
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Stage ┃ Duration ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ hello │ 0:00:00.003525 │
│ flaky │ 0:00:00.001175 │
│ goodbye │ 0:00:00.002330 │
├─────────┼────────────────┤
│ Total │ 0:00:00.016877 │
└─────────┴────────────────┘
➤ Script result:
Failure
──────── End ex_6_creating_retryable_stages.py Script Execution Summary ────────
Note that the flaky stage ran only once, and the script tells us of the overall failure at the end of the summary.
Note
The default retry behavior is governed by the retry phase
methods, which can be customized like
any other phase methods in the same way as demonstrated above. The
retry behavior is handled by tenacity.Retrying, so you might want
to familiarize yourself with the Tenacity documentation.
Customizing the Script Execution Summary
The purpose of the script execution summary is to give the user an
overview of what happened while the script was running, providing
sufficient details for them or their teammates to replicate what was run
and ease any debugging that may be necessary. By default,
StagedScript provides all the details you’ve seen in the examples
above, but you have the flexibility to extend the behavior for your subclasses.
example/ex_7_customizing_the_summary.py 1 def print_script_execution_summary(
2 self,
3 extra_sections: Optional[dict[str, str]] = None,
4 ) -> None:
5 extras = {
6 "Machine details": (
7 f"hostname: {socket.gethostname()}\n"
8 f"platform: {platform.platform()}"
9 ),
10 }
11 if extra_sections is not None:
12 extras |= extra_sections
13 super().print_script_execution_summary(extra_sections=extras)
In this case we’re adding a new section to the summary with some details about the machine the script was run on. Now when we run just the first stage, we see:
$ python3 ../../example/ex_7_customizing_the_summary.py --some-file foo.txt --stage hello
[15:27:48] Checking to make sure it's safe ex_7_customizing_the_summary.py:110
to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Greeting the user │
╰──────────────────────────────────────────────╯
The first stage is underway... ex_7_customizing_the_summary.py:135
Executing: echo 'Hello World' staged_script.py:824
Hello World
Processing file: ex_7_customizing_the_summary.py:44
/home/docs/checkouts/readthedocs.o
rg/user_builds/staged-script/check
outs/v2.0.5/doc/source/foo.txt
`hello` stage duration: 0:00:00.003639 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished stage 'hello'. │
╰──────────────────────────────────────────────╯
Checking to make sure all is well ex_7_customizing_the_summary.py:125
after running the stage...
Checking to make sure it's safe ex_7_customizing_the_summary.py:110
to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Trying an error-prone operation │
╰──────────────────────────────────────────────╯
You didn't tell me to run the ex_7_customizing_the_summary.py:115
'flaky' stage.
`flaky` stage duration: 0:00:00.001219 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished stage 'flaky'. │
╰──────────────────────────────────────────────╯
Checking to make sure all is well ex_7_customizing_the_summary.py:125
after running the stage...
Checking to make sure it's safe ex_7_customizing_the_summary.py:110
to run a stage...
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Bidding farewell │
╰──────────────────────────────────────────────╯
You didn't tell me to run the ex_7_customizing_the_summary.py:115
'goodbye' stage.
`goodbye` stage duration: 0:00:00.001112 staged_script.py:473
╭──────────────────────────────────────────────╮ staged_script.py:934
│ Finished the final stage: goodbye │
╰──────────────────────────────────────────────╯
Checking to make sure all is well ex_7_customizing_the_summary.py:125
after running the stage...
─────────── ex_7_customizing_the_summary.py Script Execution Summary ───────────
staged_script.py:917
➤ Ran the following:
ex_7_customizing_the_summary.py \
--stage hello \
--flaky-retry-attempts 5 \
--flaky-retry-delay 1 \
--flaky-retry-timeout 60 \
--some-file
/home/docs/checkouts/readthedocs.org/user_bu
ilds/staged-script/checkouts/v2.0.5/doc/sour
ce/foo.txt
➤ Commands executed:
echo 'Hello World'
➤ Timing results:
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃ Stage ┃ Duration ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│ hello │ 0:00:00.003639 │
│ flaky │ 0:00:00.001219 │
│ goodbye │ 0:00:00.001112 │
├─────────┼────────────────┤
│ Total │ 0:00:00.017037 │
└─────────┴────────────────┘
➤ Script result:
Success
➤ Machine details:
hostname:
build-30026792-project-1073165-staged-script
platform:
Linux-6.8.0-1029-aws-x86_64-with-glibc2.35
───────── End ex_7_customizing_the_summary.py Script Execution Summary ─────────
Real-World Example: System-Level Testing of a Kubernetes Application
The Geophysical Monitoring System (GMS) is a suite of applications
that deploys via Kubernetes. The scripting to automatically run its
system-level testing on a cluster, as the applications would run in
production, was built up via a hierarchy of StagedScript subclasses:
%%{init: {"theme": "neutral"}}%%
flowchart TD
GMSSystemTest -. uses .-> IANSimDeploy
GMSSystemTest -- includes a --> SimulatorMixin
GMSSystemTest == is a ==> GMSKubeWrapper
IANSimDeploy -- includes a --> SimulatorMixin
IANSimDeploy == is a ==> GMSKubeWrapper
SimulatorMixin -. uses .-> GMSKubeWrapper
SimulatorMixin -. uses .-> StagedScript
GMSKubeWrapper == is a ==> StagedScript
Note
The links below use a prior version of the package. Anywhere you see
driver_script or DriverScript, understand that those are
essentially staged_script and StagedScript. There were a few
breaking changes made in the midst of open-sourcing, but that
shouldn’t impact your ability to understand this example.
GMSKubeWrapper: Instances of GMS are deployed to a cluster with thegmskubeutility, which essentially just executes a series of Helm commands under the hood.GMSKubeWrapperis aStagedScriptthat provides some stages for standardgmskubecommands. It’s not really intended to be run as a script by itself, though it could be; rather, it exists as a base class for more substantial scripts to inherit from.SimulatorMixin: To facilitate testing without live data, GMS includes a simulator that can be applied to a running instance of the system.SimulatorMixinprovides stages for interacting with the simulator.IANSimDeploy: One of the GMS applications is the Interactive Analysis (IAN) view.IANSimDeployis aStagedScriptthat allows both developers and CI services to deploy an instance of this application, with the simulator included, consistently. It can be run as a script by itself, but also supplies some of its functionality toGMSSystemTest. The other GMS applications don’t require their own specialized script for standing them up.GMSSystemTest: GMS’ automated testing framework allows the user to stand up an instance of the system, run a series of tests against it, and then tear down the instance.GMSSystemTestis aStagedScriptthat pulls everything together from all the other classes to make this happen. It’s primarily intended to be run in CI, though developers can run it locally as well to replicate exactly what happens in CI.
The details of GMS and how it’s deployed aren’t important; rather, this
example showcases the flexibility of the framework provided by
staged-script.
Adding Stages
The stages provided by the classes are the following:
GMSKubeWrapper:SimulatorMixin:GMSSystemTest:sleep: Wait for some amount of time. (This is historically present to overlook transient system instability.)
test: Run a test augmentation against the system and collect the results.
Customizing the Parser
The argument parser for GMSSystemTest is built up, bit by bit, by
the different classes.
-
--instance: The name of the GMS instance.--save-logs,--no-save-logs: Whether to save container logs before tearing everything down.--log-dir: The directory in which to save container logs.--tag: The container image tag to use.--wait-timeout: How long to wait for the pods to reach a ready state.
-
--keycloak,--no-keycloak: Whether to use KeyCloak authentication.--node-env: Which Node environment to use (development or production).--state-timeout: How long to wait for the simulator to transition between states.A variety of options to pass on to the
gmskube installcommand.A variety of options for starting the simulator.
-
--type: Which of the GMS applications to stand up.--sleep: How long to wait between the pods reaching the ready state and starting the test.--test: Which test augmentation to apply to the system.--env: Environment variables to set in the test environment.--parallel: How many identical test augmentation pods to launch in parallel.
With all of these additions, if we run ./gms_system_test.py --help,
we see:
usage: gms_system_test.py [-h] [--stage {init,start,sleep,install,wait,test,uninstall} [{init,start,sleep,install,wait,test,uninstall} ...]]
[--dry-run] [--install-retry-attempts INSTALL_RETRY_ATTEMPTS] [--install-retry-delay INSTALL_RETRY_DELAY]
[--install-retry-timeout INSTALL_RETRY_TIMEOUT] [--test-retry-attempts TEST_RETRY_ATTEMPTS]
[--test-retry-delay TEST_RETRY_DELAY] [--test-retry-timeout TEST_RETRY_TIMEOUT] [--instance INSTANCE] [--log-dir LOG_DIR]
[--save-logs | --no-save-logs] [--tag TAG] [--wait-timeout WAIT_TIMEOUT] [--type {ian,keycloak,sb,logging}] [--sleep SLEEP]
[--test TEST] [--env ENV] [--parallel {1,2,3,4,5,6,7,8,9,10}] [--values VALUES]
This script:
* stands up a temporary instance of the GMS system,
* waits for all the pods to be up and running,
* sleeps a given amount of time to wait for the application to be ready,
* runs a test augmentation against it, and
* tears down the temporary instance after testing completes.
Test augmentations, which run in a pod on a Kubernetes cluster, copy their test results to a MinIO test reporting service so that they can be gathered back to the machine on which this script was executed. Final reports will be gathered in a ``gms_system_test-reports-{timestamp}-{unique-str}`` directory under the current working directory. Additionally a ``gms_system_test-container-logs-{timestamp}-{unique-str}`` directory will contain logs from all the containers run as part of the testing.
options:
-h, --help show this help message and exit
--stage {init,start,sleep,install,wait,test,uninstall} [{init,start,sleep,install,wait,test,uninstall} ...]
Which stages to run. (default: {'init', 'start', 'sleep', 'install', 'wait', 'test', 'uninstall'})
--dry-run If specified, don't actually run the commands in the shell; instead print the commands that would have been executed.
(default: False)
--instance INSTANCE The name of the GMS instance. (default: None)
--log-dir LOG_DIR The directory in which to save the container logs. Defaults to `gms_system_test-container-logs-<timestamp>-<unique-str>`.
(default: None)
--save-logs, --no-save-logs
Whether to save the container logs. (default: True)
--tag TAG Tag name, which corresponds to the docker tag of the images. The value entered will automatically be transformed according to
the definition of the gitlab `CI_COMMIT_REF_SLUG` variable definition (lowercase, shortened to 63 characters, and with
everything except `0-9` and `a-z` replaced with `-`, no leading / trailing `-`). (default: None)
--wait-timeout WAIT_TIMEOUT
How long to wait (in seconds) for all the pods in the instance to be ready. (default: 900)
--type {ian,keycloak,sb,logging}
The type of instance. (default: None)
--sleep SLEEP How long to wait between the pods reaching a 'Ready' state and starting the test. (default: 0)
--test TEST The name of a test to run (see ``gmskube augment catalog --tag <reference>``). (default: None)
--env ENV Set environment variables in the test environment. This argument can be specified multiple times to specify multiple values.
Example: ``--env FOO=bar`` will set ``FOO=bar`` for the test. (default: None)
--parallel {1,2,3,4,5,6,7,8,9,10}
How many identical test augmentation pods to launch in parallel. (default: 1)
retry:
Additional options for retrying stages.
--install-retry-attempts INSTALL_RETRY_ATTEMPTS
How many times to retry the 'install' stage. (default: 2)
--install-retry-delay INSTALL_RETRY_DELAY
How long to wait (in seconds) before retrying the 'install' stage. (default: 0)
--install-retry-timeout INSTALL_RETRY_TIMEOUT
How long to wait (in seconds) before giving up on retrying the 'install' stage. (default: 600)
--test-retry-attempts TEST_RETRY_ATTEMPTS
How many times to retry the 'test' stage. (default: 5)
--test-retry-delay TEST_RETRY_DELAY
How long to wait (in seconds) before retrying the 'test' stage. (default: 0)
--test-retry-timeout TEST_RETRY_TIMEOUT
How long to wait (in seconds) before giving up on retrying the 'test' stage. (default: 1200)
install:
Additional options to pass on to ``gmskube install``.
--values VALUES Set override values in the chart using a YAML file. The chart `values.yaml` is always included first, existing values second
(for upgrade), followed by any override file(s). This file should only include the specific values you want to override; it
should not be the entire `values.yaml` from the chart. This flag can be used multiple times to specify multiple files. The
priority will be given to the last (right-most) file specified. (default: None)
examples:
Here are some standard use cases.
Run the ``jest`` test against a ``sb`` instance deployed from the ``develop`` branch::
gms_system_test.py --type sb --tag develop --test jest
Verify that it's possible to install/uninstall ``ian``, but don't test anything::
gms_system_test.py --type ian --tag develop --stage install wait init start uninstall
Customizing Stage Behavior
The transitions between the stages are customized as follows:
GMSKubeWrapper:There are some general pre-stage actions defined (ensure we have an instance name, that the instance exists, and that we have a
KubeCtlobject ready to talk to it) that may be used by any number of stages defined here or in subclasses.If the install stage fails, uninstall the instance before retrying it. If it still hasn’t worked after the retries have been exhausted, record the script failure and skip all future stages.
Before the wait stage, run the general pre-stage actions. If skipping this stage, assume all the pods are ready. If the pods aren’t ready, record the script failure.
Before the uninstall stage, run the general pre-stage actions, and then save the container logs.
SimulatorMixin: For each stage defined, run the general pre-stage actions fromGMSKubeWrapper.IANSimDeploy: If the pods aren’t ready after the wait stage, skip over all future stages.GMSSystemTest:Before the install stage, if an instance name wasn’t supplied, create a unique one.
If the pods aren’t ready after the wait stage, skip all future stages.
Before running the test stage, ensure the instance tag is set, and create the test reports directory. After it completes, set the script success based on the test results. If the tests fail, before retrying the stage, save the container logs and clean things up.
Customizing the Script Execution Summary
Each class in the hierarchy can add details to the script execution summary, to better communicate to users what was just run, and to ease debugging of any failures, either locally or in CI.
GMSKubeWrapper: Adds the container logs directory and name of the current Kubernetes cluster.IANSimDeploy: Adds the URL for the user interface, so users can quickly pull it up in their browser.GMSSystemTest: Adds the test reports directory, so users can quickly see how things went.