New to pantograph? Click here to open the installation & setup instructions first
1) Install the latest Xcode command line tools
xcode-select --install
# Install ruby via homebrew (macOS & linux only) brew install ruby # Set ruby in your shell path (example uses Zsh) echo 'export PATH="/usr/local/opt/ruby/bin:$PATH"' >> ~/.zshrc # Using RubyGems gem install pantograph
3) Navigate to your project and run
pantograph init
Actions
User input and output
The PantographCore::UI
utility may be used to display output to the user and also
request input from an action.
UI
includes a number of methods to customize the output for different purposes:
UI.message('Hello from my_new_action.') UI.important('Warning: This is a new action.') UI.error('Something unexpected happened in my_new_action. Attempting to continue.')
method | description |
---|---|
error | Displays an error message in red |
important | Displays a warning or other important message in yellow |
success | Displays a success message in green |
message | Displays a message in the default output color |
deprecated | Displays a deprecation message in bold blue |
command | Displays a command being executed in cyan |
command_output | Displays command output in magenta |
verbose | Displays verbose output in the default output color |
header | Displays a message in a box for emphasis |
Methods ending in an exclamation point (!
) terminate execution of the current
lane and report an error:
UI.user_error!("Could not open file #{file_path}")
method | description |
---|---|
crash! | Report a catastrophic error |
user_error! | Rescue an exception in your action and report a nice message to the user |
shell_error! | Report failure of a shell command |
build_failure! | Report a build failure |
test_failure! | Report a test failure |
abort_with_message! | Report a failure condition that prevents continuing |
Note that these methods raise exceptions that are rescued in the runner context for the lane. This terminates further lane execution, so it is not necessary to return.
# No need for "and return" here UI.user_error!("Could not open file #{file_path}") and return if file.nil?
The following methods may be used to prompt the user for input.
if UI.interactive? name = UI.input('Please enter your name: ') is_correct = UI.confirm('Is this correct? ') choice = UI.select('Please choose from the following list:', %w{alpha beta gamma}) password = UI.password('Please enter your password: ') end
method | description |
---|---|
interactive? | Indicates whether interactive input is possible |
input | Prompt the user for string input |
confirm | Ask the user a binary question |
select | Prompt the user to select from a list of options |
password | Prompt the user for a password (masks output) |
Run actions directly
If you just want to try an action without adding them to your Pantfile
yet, you can use
pantograph run notification message:"My Text" title:"The Title"
To get the available options for any action run pantograph action [action_name]
. You might not be able to set some kind of parameters using this method.
Shell values
You can get value from shell commands:
output = sh('ls -a')
Building Actions
Using PantographCore::Configuration
Most actions accept one or more parameters to customize their behavior. Actions define their
parameters in an available_options
method. This method returns an array of PantographCore::ConfigItem
objects to describe supported options. Each option is declared by creating a new
ConfigItem
, e.g.:
PantographCore::ConfigItem.new( key: :file, env_name: 'MY_NEW_ACTION_FILE', description: 'A file to operate on', type: String, optional: false )
This declares a file
option for use with the action in a Pantfile, e.g.:
my_new_action(file: 'file.txt')
If the optional env_name
is present, an environment variable with the specified
name may also be used in place of an option in the Pantfile:
MY_NEW_ACTION_FILE=file.txt pantograph run my_new_action
The type
argument to the PantographCore::ConfigItem
initializer specifies the
name of a Ruby class representing a standard
data type. Supplied arguments will be coerced to the specified type. Some standard types
support default conversions.
Boolean parameters
Ruby does not have a single class to represent a Boolean type. When specifying
Boolean parameters, use is_string: false
, without specifying a type
, e.g.:
PantographCore::ConfigItem.new( key: :commit, env_name: 'MY_NEW_ACTION_COMMIT', description: 'Commit the results if true', optional: true, default_value: false, is_string: false )
When passing a string value, e.g. from an environment variable, certain set string values are recognized:
MY_NEW_ACTION_COMMIT=true MY_NEW_ACTION_COMMIT=false MY_NEW_ACTION_COMMIT=yes MY_NEW_ACTION_COMMIT=no
These values may also be passed in all caps, e.g. MY_NEW_ACTION_COMMIT=YES
.
Array parameters
If a parameter is declared with type: Array
and a String
argument is passed,
an array will be produced by splitting the string using the comma character
as a delimiter:
PantographCore::ConfigItem.new( key: :files, env_name: 'MY_NEW_ACTION_FILES', description: 'One or more files to operate on', type: Array, optional: false )
my_new_action(files: 'file1.txt,file2.txt')
This is received by the action as ['file1.txt', 'file2.txt']
.
This also means a parameter that accepts an array may take a single string as an argument:
my_new_action(files: 'file.txt')
This is received by the action as ['file.txt']
.
Comma-separated lists are particularly useful when using environment variables:
export MY_NEW_ACTION_FILES=file1.txt,file2.txt
Polymorphic parameters
To allow for different types to be passed to a parameter (beyond what is
provided above), specify is_string: false
without a type
field. Use
an optional verify_block
argument (see below) or verify the argument within
your action. If the block does not raise, the option is considered verified.
The UI.user_error!
method is a convenient way to handle verification failure.
PantographCore::ConfigItem.new( key: :polymorphic_option, is_string: false, verify_block: ->(value) { verify_option(value) } ) def verify_option(value) case value when String @polymorphic_option = value when Array @polymorphic_option = value.join(' ') when Hash @polymorphic_option = value.to_s else UI.user_error! "Invalid option: #{value.inspect}" end end
Callback parameters
If your action needs to provide a callback, specify Proc
for the type
field.
PantographCore::ConfigItem.new( key: :callback, description: 'Optional callback argument', optional: true, type: Proc )
To invoke the callback in your action, use the Proc#call
method and pass
any arguments:
params[:callback].call(result) if params[:callback]
To notify the user of success or failure, it's usually best just to return a
value such as true
or false
from your action. Use a callback for contextual
error handling. For example, the built-in sh
action passes the entire command
output to an optional error_callback
:
callback = lambda do |result| handle_missing_file if result =~ /file not found/i handle_auth_failure if result =~ /login failed/i end sh('some_cmd', error_callback: callback)
Note on Procs
When passing a block as a parameter to an action or ConfigItem, use a Proc object. There are three ways to create an instance of Proc in Ruby.
Using the lambda
operator:
verify_block = lambda do |value| ... end
Using Proc.new
:
verify_block = Proc.new do |value| ... end
Using the Proc
literal notation:
verify_block = ->(value) { ... }
Note that you cannot pass a block literal as a Proc
.
Verify blocks
Use a verify_block
argument with your ConfigItem
to provide special
argument verification:
verify_block = lambda do |value| # Has to be a String to get this far uri = URI(value) UI.error("Invalid scheme #{uri.scheme}" unless uri.scheme == 'http' || uri.scheme == 'https') end PantographCore::ConfigItem.new( key: :url, type: String, verify_block: verify_block )
The verify_block
requires a Proc
argument (see above).
Conflicting options
If your action includes multiple conflicting options, use conflicting_options
in the ConfigItem
for each. Make sure conflicting options are optional.
PantographCore::ConfigItem.new( key: :text, type: String, optional: true, conflicting_options: [:text_file] ), PantographCore::ConfigItem.new( key: :text_file, type: String, optional: true, conflicting_options: [:text] )
You can also pass a conflict_block
(a Proc
, see above) if you want to
implement special handling of conflicting options:
conflict_block = Proc.new do |other| UI.user_error!("Unexpected conflict with option #{other}") unless [:text, :text_file].include?(other) UI.message('Ignoring :text_file in favor of :text') end PantographCore::ConfigItem.new( key: :text, type: String, optional: true, conflicting_options: [:text_file], conflict_block: conflict_block ), PantographCore::ConfigItem.new( key: :text_file, type: String, optional: true, conflicting_options: [:text], conflict_block: conflict_block )
Optional parameters
Parameters with optional: true
will be nil
unless a default_value
field
is present. Make sure the default_value
is reasonable unless it's acceptable
for the key to be absent.
PantographCore::ConfigItem.new( key: :build_configuration, description: 'Which build configuration to use', type: String, optional: true, default_value: 'Release' ), PantographCore::ConfigItem.new( key: :offset, description: 'Offset to start from', type: Integer, optional: true, default_value: 0 ), PantographCore::ConfigItem.new( key: :workspace, description: 'Optional workspace path', type: String, optional: true # Not every project has a workspace, so nil is a good default value here. )
Within the action params[:build_configuration]
will never be nil. Specifying
the default_value
is preferable to something in code like:
config = params[:build_configuration] || 'Release'
Default values are included in the documentation for action parameters.
Configuration files
Pantograph also supports configuration files (MyNewActionFile
).
This is useful for actions with many options.
To add support for a configuration file to a custom action, call load_configuration_file
early, usually as the first line of run
:
def self.run(params) params.load_configuration_file('MyNewActionfile') # ...
This will load any parameters specified in MyNewActionfile
. This method looks for
the specified file in ./pantograph
, ./.pantograph
and .
, in that order. The
file is evaluated by the Ruby interpreter.
You may specify they key
from any PantographCore::ConfigItem
as a method call in the
configuration file:
command 'ls -la' files %w{file1.txt file2.txt}
Resolution order
Parameters are resolved from different sources in the following order:
- A parameter directly passed to an action using the
key
, usually from thePantfile
. - An environment variable, if the
env_name
is set. - A configuration file used in
load_configuration_file
. - The
default_value
of theConfigItem
. If not explicitly set, this will benil
.
Invoking shell commands
If your action needs to run a shell command, there are several methods. You can easily determine the exit status of the command and capture all terminal output from the command.
Using Kernel#system
Use the Ruby system
method call to invoke a command string. This does not
redirect stdin, stdout or stderr, so output formatting will be unaffected. It executes
the command in a subshell.
system('cat pantograph/Pantfile')
Upon command completion, the method returns true or false to indicate completion
status. The $?
global variable will also indicate the exit status of the
command.
system('cat pantograph/Pantfile') UI.user_error!('Could not execute command') unless $?.exitstatus == 0
If the command to be executed is not found, system will return nil
, and
$?.exitstatus
will be nonzero.
Using backticks
To capture the output of a command, enclose the command in backticks:
pod_cmd = `which pod` UI.important('"pod" command not found') if pod_cmd.empty?
Because you are capturing stdout, the command output will not appear at
the terminal unless you log it using UI
. Formatting may be lost when
capturing command output. The entire output to stdout will be captured after the
command returns. Output to stderr is not captured or redirected. The $?
global variable will indicate the completion status of the command.
If the command to be executed is not found, Errno::ENOENT
is raised.
Using the sh method
You can also use the built-in sh
method:
sh('pwd')
This is called the same way in an action as in a Pantfile. This provides consistent
logging of command output. All output to stdout and stderr is logged via UI
.
The sh
method can accept a block, which will receive the Process::Status
returned by the command, the complete output of the command, and an equivalent
shell command upon completion of the command.
sh("ls', '-la") do |status, result, command| unless status.success? UI.error("Command #{command} (pid #{status.pid}) failed with status #{status.exitstatus}") end UI.message("Output is #{result.inspect}") end
To be notified only when an error occurs, use the error_callback
parameter
(a Proc):
success = true sh('pwd', error_callback: ->(result) { success = false }) UI.user_error("Command failed") unless success
The result
argument to the error_callback
is the entire string output
of the command.
If the command to be executed is not found, Errno::ENOENT
is raised without
calling the block or error_callback
.
If an error_callback
or block is not provided, and the command executes and
returns an error, an exception is raised, and lane execution is terminated
unless the exception is rescued. The exit status of the command will be available in $?
. It is also available
as the first argument to a block.
The return value of the method is the output of the command, unless a block is
given. Then the output is available within the block, and the return value of
sh
is the return value of the block. This enables usage like:
if sh command { |s| s.success? } UI.success('Command succeeded') else UI.error('Command failed') end
Anywhere other than an action or a Pantfile (e.g. in helper code), you can
invoke this method as Actions.sh
.
Escaping in shell commands
Use shellwords
to escape arguments to shell commands.
`git commit -aqm #{Shellwords.escape commit_message}`
system("cat #{path.shellescape}")
When using system
or sh
, pass a list of arguments instead of shell-escaping
individual arguments.
sh('git', 'commit', '-aqm', commit_message) system('cat', path)
Calling other actions
Some built-in utility actions, such as sh
, may be used in custom actions (e.g., in
plugins). It's not generally a good idea to call a complex action from another action.
In particular:
- If you're calling one plugin action from another plugin action, you should probably refactor your plugin helper to be more easily called from all actions in the plugin.
- Avoid wrapping complex built-in actions like deliver and gym.
- There can be issues with one plugin depending on another plugin.
- Certain simple built-in utility actions may be used with
other_action
in your action, such as:other_action.git_add
,other_action.git_commit
. - Think twice before calling an action from another action. There is often a better solution.