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 {hello,goodbye} [{hello,goodbye} ...]]
                          [--dry-run]
                          [--hello-retry-attempts HELLO_RETRY_ATTEMPTS]
                          [--hello-retry-delay HELLO_RETRY_DELAY]
                          [--hello-retry-timeout HELLO_RETRY_TIMEOUT]
                          [--goodbye-retry-attempts GOODBYE_RETRY_ATTEMPTS]
                          [--goodbye-retry-delay GOODBYE_RETRY_DELAY]
                          [--goodbye-retry-timeout GOODBYE_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 {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)

retry:
  Additional options for retrying stages.

  --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)
  --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)

If you tell it you only want to run the hello stage, you’ll see

$ python3 ../../example/ex_0_the_basics.py --stage hello
[19:26:19] ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Greeting the user                            │                     
           ╰──────────────────────────────────────────────╯                     
           Executing:  echo 'Hello World'                   staged_script.py:827
Hello World
           `hello` stage duration:  0:00:00.005360          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Bidding farewell                             │                     
           ╰──────────────────────────────────────────────╯                     
           Skipping this stage.                             staged_script.py:440
           `goodbye` stage duration:  0:00:00.001810        staged_script.py:475
───────────────── ex_0_the_basics.py Script Execution Summary ──────────────────
                                                            staged_script.py:920
           ➤ Ran the following:                                                 
                                                                                
               ex_0_the_basics.py \                                             
                   --stage hello \                                              
                   --hello-retry-attempts 0 \                                   
                   --hello-retry-delay 0 \                                      
                   --hello-retry-timeout 60 \                                   
                   --goodbye-retry-attempts 0 \                                 
                   --goodbye-retry-delay 0 \                                    
                   --goodbye-retry-timeout 60                                   
                                                                                
           ➤ Commands executed:                                                 
                                                                                
               echo 'Hello World'                                               
                                                                                
           ➤ Timing results:                                                    
                                                                                
               ┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓                                     
               ┃ Stage   ┃ Duration       ┃                                     
               ┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩                                     
               │ hello   │ 0:00:00.005360 │                                     
               │ goodbye │ 0:00:00.001810 │                                     
               ├─────────┼────────────────┤                                     
               │ Total   │ 0:00:00.010238 │                                     
               └─────────┴────────────────┘                                     
                                                                                
           ➤ 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
[19:26:20] ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Greeting the user                            │                     
           ╰──────────────────────────────────────────────╯                     
           Executing:  echo 'Hello World'                   staged_script.py:827
Hello World
           `hello` stage duration:  0:00:00.005227          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Bidding farewell                             │                     
           ╰──────────────────────────────────────────────╯                     
           Skipping this stage.                             staged_script.py:440
           `goodbye` stage duration:  0:00:00.001839        staged_script.py:475
──────── ex_1_removing_the_retry_arguments.py Script Execution Summary ─────────
                                                            staged_script.py:920
           ➤ 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.005227 │                                     
               │ goodbye │ 0:00:00.001839 │                                     
               ├─────────┼────────────────┤                                     
               │ Total   │ 0:00:00.010058 │                                     
               └─────────┴────────────────┘                                     
                                                                                
           ➤ 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.py
1    @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
[19:26:20] ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Greeting the user                            │                     
           ╰──────────────────────────────────────────────╯                     
           Executing:  echo 'Hello World'                   staged_script.py:827
Hello World
           `hello` stage duration:  0:00:00.005235          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Bidding farewell                             │                     
           ╰──────────────────────────────────────────────╯                     
           Executing:  echo 'Goodbye World'                 staged_script.py:827
Goodbye World
           `goodbye` stage duration:  0:00:00.002672        staged_script.py:475
────── ex_2_running_certain_stages_by_default.py Script Execution Summary ──────
                                                            staged_script.py:920
           ➤ Ran the following:                                                 
                                                                                
               ex_2_running_certain_stages_by_default.py \                      
                   --stage hello goodbye                                        
                                                                                
           ➤ Commands executed:                                                 
                                                                                
               echo 'Hello World'                                               
               echo 'Goodbye World'                                             
                                                                                
           ➤ Timing results:                                                    
                                                                                
               ┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓                                     
               ┃ Stage   ┃ Duration       ┃                                     
               ┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩                                     
               │ hello   │ 0:00:00.005235 │                                     
               │ goodbye │ 0:00:00.002672 │                                     
               ├─────────┼────────────────┤                                     
               │ Total   │ 0:00:00.011002 │                                     
               └─────────┴────────────────┘                                     
                                                                                
           ➤ 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
[19:26:20] ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Greeting the user                            │                     
           ╰──────────────────────────────────────────────╯                     
           Executing:  echo 'Hello World'                   staged_script.py:827
Hello World
           Processing file:                          ex_3_adding_arguments.py:23
           /home/docs/checkouts/readthedocs.org/user                            
           _builds/staged-script/checkouts/v2.0.0/do                            
           c/source/foo.txt                                                     
           `hello` stage duration:  0:00:00.006279          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Bidding farewell                             │                     
           ╰──────────────────────────────────────────────╯                     
           Executing:  echo 'Goodbye World'                 staged_script.py:827
Goodbye World
           Some flag was not set!                    ex_3_adding_arguments.py:28
           `goodbye` stage duration:  0:00:00.003392        staged_script.py:475
────────────── ex_3_adding_arguments.py Script Execution Summary ───────────────
                                                            staged_script.py:920
           ➤ 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.0/doc/sour                     
               ce/foo.txt                                                       
                                                                                
           ➤ Commands executed:                                                 
                                                                                
               echo 'Hello World'                                               
               echo 'Goodbye World'                                             
                                                                                
           ➤ Timing results:                                                    
                                                                                
               ┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓                                     
               ┃ Stage   ┃ Duration       ┃                                     
               ┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩                                     
               │ hello   │ 0:00:00.006279 │                                     
               │ goodbye │ 0:00:00.003392 │                                     
               ├─────────┼────────────────┤                                     
               │ Total   │ 0:00:00.012740 │                                     
               └─────────┴────────────────┘                                     
                                                                                
           ➤ 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
[19:26:20] Checking to make sure it's safe ex_4_customizing_stage_behavior.py:76
           to run a stage...                                                    
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ 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.002341          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ 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:937
           │ Bidding farewell                             │                     
           ╰──────────────────────────────────────────────╯                     
           Executing:  echo 'Goodbye World'                 staged_script.py:827
Goodbye World
           Some flag was set!              ex_4_customizing_stage_behavior.py:28
           `goodbye` stage duration:  0:00:00.003552        staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ 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:920
           ➤ 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.0/doc/sour                     
               ce/foo.txt \                                                     
                   --some-flag                                                  
                                                                                
           ➤ Commands executed:                                                 
                                                                                
               echo 'Goodbye World'                                             
                                                                                
           ➤ Timing results:                                                    
                                                                                
               ┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓                                     
               ┃ Stage   ┃ Duration       ┃                                     
               ┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩                                     
               │ hello   │ 0:00:00.002341 │                                     
               │ goodbye │ 0:00:00.003552 │                                     
               ├─────────┼────────────────┤                                     
               │ Total   │ 0:00:00.017351 │                                     
               └─────────┴────────────────┘                                     
                                                                                
           ➤ 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
[19:26:21] Checking to make sure it's   ex_5_customizing_individual_stages.py:76
           safe to run a stage...                                               
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Greeting the user                            │                     
           ╰──────────────────────────────────────────────╯                     
           The first stage is          ex_5_customizing_individual_stages.py:101
           underway...                                                          
           Executing:  echo 'Hello World'                   staged_script.py:827
Hello World
           Processing file:             ex_5_customizing_individual_stages.py:23
           /home/docs/checkouts/readthe                                         
           docs.org/user_builds/staged-                                         
           script/checkouts/v2.0.0/doc/                                         
           source/foo.txt                                                       
           `hello` stage duration:  0:00:00.004889          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ 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:937
           │ Bidding farewell                             │                     
           ╰──────────────────────────────────────────────╯                     
           Executing:  echo 'Goodbye World'                 staged_script.py:827
Goodbye World
           Some flag was not set!       ex_5_customizing_individual_stages.py:28
           `goodbye` stage duration:  0:00:00.003308        staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ 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:920
           ➤ Ran the following:                                                 
                                                                                
               ex_5_customizing_individual_stages.py \                          
                   --stage hello goodbye \                                      
                   --some-file                                                  
               /home/docs/checkouts/readthedocs.org/user_bu                     
               ilds/staged-script/checkouts/v2.0.0/doc/sour                     
               ce/foo.txt                                                       
                                                                                
           ➤ Commands executed:                                                 
                                                                                
               echo 'Hello World'                                               
               echo 'Goodbye World'                                             
                                                                                
           ➤ Timing results:                                                    
                                                                                
               ┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓                                     
               ┃ Stage   ┃ Duration       ┃                                     
               ┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩                                     
               │ hello   │ 0:00:00.004889 │                                     
               │ goodbye │ 0:00:00.003308 │                                     
               ├─────────┼────────────────┤                                     
               │ Total   │ 0:00:00.019669 │                                     
               └─────────┴────────────────┘                                     
                                                                                
           ➤ 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    ):
 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 {flaky,goodbye,hello} [{flaky,goodbye,hello} ...]]
                                         [--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 {flaky,goodbye,hello} [{flaky,goodbye,hello} ...]
                        Which stages to run. (default: ['flaky', 'goodbye',
                        'hello'])
  --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
[19:26:21] Checking to make sure it's safe ex_6_creating_retryable_stages.py:106
           to run a stage...                                                    
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Greeting the user                            │                     
           ╰──────────────────────────────────────────────╯                     
           The first stage is underway...  ex_6_creating_retryable_stages.py:131
           Executing:  echo 'Hello World'                   staged_script.py:827
Hello World
           Processing file:                 ex_6_creating_retryable_stages.py:40
           /home/docs/checkouts/readthedocs                                     
           .org/user_builds/staged-script/c                                     
           heckouts/v2.0.0/doc/source/foo.t                                     
           xt                                                                   
           `hello` stage duration:  0:00:00.004805          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Finished stage 'hello'.                      │                     
           ╰──────────────────────────────────────────────╯                     
           Checking to make sure all is    ex_6_creating_retryable_stages.py:121
           well after running the stage...                                      
           Checking to make sure it's safe ex_6_creating_retryable_stages.py:106
           to run a stage...                                                    
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Trying an error-prone operation              │                     
           ╰──────────────────────────────────────────────╯                     
           Oh no!  Something went horribly  ex_6_creating_retryable_stages.py:47
           wrong!                                                               
           `flaky` stage duration:  0:00:00.002024          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Finished stage 'flaky'.                      │                     
           ╰──────────────────────────────────────────────╯                     
           Preparing to retry the 'flaky' stage...          staged_script.py:546
           <RetryCallState 139940538969568: attempt #1;                         
           slept for 1.0; last result: failed (RetryStage                       
           )>                                                                   
[19:26:22] ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Trying an error-prone operation              │                     
           ╰──────────────────────────────────────────────╯                     
           Oh no!  Something went horribly  ex_6_creating_retryable_stages.py:47
           wrong!                                                               
           `flaky` stage duration:  0:00:00.002173          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Finished stage 'flaky'.                      │                     
           ╰──────────────────────────────────────────────╯                     
           Preparing to retry the 'flaky' stage...          staged_script.py:546
           <RetryCallState 139940538969568: attempt #2;                         
           slept for 2.0; last result: failed (RetryStage                       
           )>                                                                   
[19:26:23] ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Trying an error-prone operation              │                     
           ╰──────────────────────────────────────────────╯                     
           Thank goodness, everything       ex_6_creating_retryable_stages.py:50
           worked this time.                                                    
           `flaky` stage duration:  0:00:00.002208          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Finished stage 'flaky'.                      │                     
           ╰──────────────────────────────────────────────╯                     
           Checking to make sure all is    ex_6_creating_retryable_stages.py:121
           well after running the stage...                                      
           Checking to make sure it's safe ex_6_creating_retryable_stages.py:106
           to run a stage...                                                    
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Bidding farewell                             │                     
           ╰──────────────────────────────────────────────╯                     
           Executing:  echo 'Goodbye World'                 staged_script.py:827
Goodbye World
           Some flag was not set!           ex_6_creating_retryable_stages.py:56
           `goodbye` stage duration:  0:00:00.003583        staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Finished the final stage:  goodbye           │                     
           ╰──────────────────────────────────────────────╯                     
           Checking to make sure all is    ex_6_creating_retryable_stages.py:121
           well after running the stage...                                      
────────── ex_6_creating_retryable_stages.py Script Execution Summary ──────────
                                                            staged_script.py:920
           ➤ 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.0/doc/sour                     
               ce/foo.txt                                                       
                                                                                
           ➤ Commands executed:                                                 
                                                                                
               echo 'Hello World'                                               
               echo 'Goodbye World'                                             
                                                                                
           ➤ Timing results:                                                    
                                                                                
               ┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓                                     
               ┃ Stage   ┃ Duration       ┃                                     
               ┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩                                     
               │ hello   │ 0:00:00.004805 │                                     
               │ flaky   │ 0:00:00.002024 │                                     
               │ flaky   │ 0:00:00.002173 │                                     
               │ flaky   │ 0:00:00.002208 │                                     
               │ goodbye │ 0:00:00.003583 │                                     
               ├─────────┼────────────────┤                                     
               │ Total   │ 0:00:02.037255 │                                     
               └─────────┴────────────────┘                                     
                                                                                
           ➤ 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
[19:26:23] Checking to make sure it's safe ex_6_creating_retryable_stages.py:106
           to run a stage...                                                    
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Greeting the user                            │                     
           ╰──────────────────────────────────────────────╯                     
           The first stage is underway...  ex_6_creating_retryable_stages.py:131
           Executing:  echo 'Hello World'                   staged_script.py:827
Hello World
           Processing file:                 ex_6_creating_retryable_stages.py:40
           /home/docs/checkouts/readthedocs                                     
           .org/user_builds/staged-script/c                                     
           heckouts/v2.0.0/doc/source/foo.t                                     
           xt                                                                   
           `hello` stage duration:  0:00:00.004851          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Finished stage 'hello'.                      │                     
           ╰──────────────────────────────────────────────╯                     
           Checking to make sure all is    ex_6_creating_retryable_stages.py:121
           well after running the stage...                                      
           Checking to make sure it's safe ex_6_creating_retryable_stages.py:106
           to run a stage...                                                    
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Trying an error-prone operation              │                     
           ╰──────────────────────────────────────────────╯                     
           Oh no!  Something went horribly  ex_6_creating_retryable_stages.py:47
           wrong!                                                               
           `flaky` stage duration:  0:00:00.001978          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Finished stage 'flaky'.                      │                     
           ╰──────────────────────────────────────────────╯                     
           Checking to make sure all is    ex_6_creating_retryable_stages.py:121
           well after running the stage...                                      
           Checking to make sure it's safe ex_6_creating_retryable_stages.py:106
           to run a stage...                                                    
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Bidding farewell                             │                     
           ╰──────────────────────────────────────────────╯                     
           Executing:  echo 'Goodbye World'                 staged_script.py:827
Goodbye World
           Some flag was not set!           ex_6_creating_retryable_stages.py:56
           `goodbye` stage duration:  0:00:00.003355        staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Finished the final stage:  goodbye           │                     
           ╰──────────────────────────────────────────────╯                     
           Checking to make sure all is    ex_6_creating_retryable_stages.py:121
           well after running the stage...                                      
────────── ex_6_creating_retryable_stages.py Script Execution Summary ──────────
                                                            staged_script.py:920
           ➤ Ran the following:                                                 
                                                                                
               ex_6_creating_retryable_stages.py \                              
                   --stage flaky hello goodbye \                                
                   --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.0/doc/sour                     
               ce/foo.txt                                                       
                                                                                
           ➤ Commands executed:                                                 
                                                                                
               echo 'Hello World'                                               
               echo 'Goodbye World'                                             
                                                                                
           ➤ Timing results:                                                    
                                                                                
               ┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓                                     
               ┃ Stage   ┃ Duration       ┃                                     
               ┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩                                     
               │ hello   │ 0:00:00.004851 │                                     
               │ flaky   │ 0:00:00.001978 │                                     
               │ goodbye │ 0:00:00.003355 │                                     
               ├─────────┼────────────────┤                                     
               │ Total   │ 0:00:00.025395 │                                     
               └─────────┴────────────────┘                                     
                                                                                
           ➤ 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
[19:26:23] Checking to make sure it's safe   ex_7_customizing_the_summary.py:108
           to run a stage...                                                    
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Greeting the user                            │                     
           ╰──────────────────────────────────────────────╯                     
           The first stage is underway...    ex_7_customizing_the_summary.py:133
           Executing:  echo 'Hello World'                   staged_script.py:827
Hello World
           Processing file:                   ex_7_customizing_the_summary.py:42
           /home/docs/checkouts/readthedocs.o                                   
           rg/user_builds/staged-script/check                                   
           outs/v2.0.0/doc/source/foo.txt                                       
           `hello` stage duration:  0:00:00.004936          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Finished stage 'hello'.                      │                     
           ╰──────────────────────────────────────────────╯                     
           Checking to make sure all is well ex_7_customizing_the_summary.py:123
           after running the stage...                                           
           Checking to make sure it's safe   ex_7_customizing_the_summary.py:108
           to run a stage...                                                    
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Trying an error-prone operation              │                     
           ╰──────────────────────────────────────────────╯                     
           You didn't tell me to run the     ex_7_customizing_the_summary.py:113
           'flaky' stage.                                                       
           `flaky` stage duration:  0:00:00.002185          staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Finished stage 'flaky'.                      │                     
           ╰──────────────────────────────────────────────╯                     
           Checking to make sure all is well ex_7_customizing_the_summary.py:123
           after running the stage...                                           
           Checking to make sure it's safe   ex_7_customizing_the_summary.py:108
           to run a stage...                                                    
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Bidding farewell                             │                     
           ╰──────────────────────────────────────────────╯                     
           You didn't tell me to run the     ex_7_customizing_the_summary.py:113
           'goodbye' stage.                                                     
           `goodbye` stage duration:  0:00:00.001912        staged_script.py:475
           ╭──────────────────────────────────────────────╮ staged_script.py:937
           │ Finished the final stage:  goodbye           │                     
           ╰──────────────────────────────────────────────╯                     
           Checking to make sure all is well ex_7_customizing_the_summary.py:123
           after running the stage...                                           
─────────── ex_7_customizing_the_summary.py Script Execution Summary ───────────
                                                            staged_script.py:920
           ➤ 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.0/doc/sour                     
               ce/foo.txt                                                       
                                                                                
           ➤ Commands executed:                                                 
                                                                                
               echo 'Hello World'                                               
                                                                                
           ➤ Timing results:                                                    
                                                                                
               ┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┓                                     
               ┃ Stage   ┃ Duration       ┃                                     
               ┡━━━━━━━━━╇━━━━━━━━━━━━━━━━┩                                     
               │ hello   │ 0:00:00.004936 │                                     
               │ flaky   │ 0:00:00.002185 │                                     
               │ goodbye │ 0:00:00.001912 │                                     
               ├─────────┼────────────────┤                                     
               │ Total   │ 0:00:00.025507 │                                     
               └─────────┴────────────────┘                                     
                                                                                
           ➤ Script result:                                                     
                                                                                
               Success                                                          
                                                                                
           ➤ Machine details:                                                   
                                                                                
               hostname:                                                        
               build-26475439-project-1073165-staged-script                     
               platform:                                                        
               Linux-5.19.0-1028-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 the gmskube utility, which essentially just executes a series of Helm commands under the hood. GMSKubeWrapper is a StagedScript that provides some stages for standard gmskube commands. 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. SimulatorMixin provides stages for interacting with the simulator.

  • IANSimDeploy: One of the GMS applications is the Interactive Analysis (IAN) view. IANSimDeploy is a StagedScript that 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 to GMSSystemTest. 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. GMSSystemTest is a StagedScript that 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:

    • install: Stand up an instance of one of the GMS applications on a cluster.

    • wait: Wait for all the pods to reach a ready state.

    • uninstall: Tear down the instance.

  • SimulatorMixin:

    • init: Initialize the simulator.

    • start: Start data flowing.

    • stop: Stop the data flow.

    • clean: Clean up the simulator and return it to the uninitialized state.

    • status: Get the current status of the simulator.

  • 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.

  • GMSKubeWrapper adds:

    • --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.

  • IANSimDeploy adds:

    • --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 install command.

    • A variety of options for starting the simulator.

  • GMSSystemTest adds:

    • --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:

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.