New to pantograph? Click here to open the installation & setup instructions first

1) Install the latest Xcode command line tools

xcode-select --install
Install _pantograph_ using Homebrew & Rubygems
# 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

More Details

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:

  1. A parameter directly passed to an action using the key, usually from the Pantfile.
  2. An environment variable, if the env_name is set.
  3. A configuration file used in load_configuration_file.
  4. The default_value of the ConfigItem. If not explicitly set, this will be nil.

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.