Author with AI
Use your favorite LLM (ChatGPT, Claude, etc.) to help you write Gruntwork Runbooks.
How to use it
- Copy the following prompt into your LLM:Write me a runbook that [describe what you want the runbook to do].Use the following context to help you write the runbook, which is the complete Runbooks documentation.
- Copy the LLM context below and paste it into the same message.
- Send it! The LLM will have full knowledge of all available blocks, props, and conventions.
For best results, be specific about what your runbook should do: which tools it installs, what inputs it collects, what scripts it runs, and what checks it performs.
LLM Context
# Gruntwork Runbooks Documentation
> Gruntwork Runbooks turns your infrastructure expertise into guided
> workflows that any developer can safely execute.
>
> This document contains the full documentation for authoring and using Runbooks.
> Paste it into your LLM for complete context when writing runbooks.
Source: https://runbooks.gruntwork.io
---
## Intro
### Overview
Gruntwork Runbooks is a desktop app that opens runbooks. A runbook is a markdown document with interactive blocks that collect input, generate files, and run scripts.
Runbooks is [open source](https://github.com/gruntwork-io/runbooks), built by [Gruntwork](https://gruntwork.io), and free to use.
## What can Runbooks do?
Runbooks loads a runbook file and:
- Renders its markdown text
- Collects user input in forms it generates from the runbook
- Uses the values the user entered to:
- Generate files and folders from templates
- Run scripts
- Run checks
## How is Runbooks useful?
Platform and DevOps engineers often know how to do an infrastructure task that application engineers need done. A runbook lets the platform engineer write that task down once, so the application engineer can complete it without waiting for help.
Runbooks is useful for:
- developer self-service
- setting up landing zones
- documenting internal processes or standard operating procedures
- codifying all the ways to use a given IaC pattern
Learn more by reading about [Runbooks use cases](/intro/use_cases/).
## What does a runbook look like?
`runbooks open /path/to/runbook` opens a window like this:




The [UI tour](/intro/ui_tour/) walks through these screens.
## How is a runbook structured?
A typical runbook directory looks like the file tree below. The file names are arbitrary, but the folder names are conventional.
- runbook.mdx
- assets/
- architecture.jpg
- checks/
- preflight_checks.sh
- scripts/
- install_mise.sh
- templates/
- consume_lambda_module/
- boilerplate.yml
- terragrunt.hcl
A `runbook.mdx` file contains markdown and interactive [blocks](/authoring/blocks/). Blocks can reference files in the `assets/`, `checks/`, `scripts/`, and `templates/` directories. See [Authoring Runbooks](/authoring/overview/) for the details.
## How do you open a runbook?
Runbooks open in the Runbooks desktop app. From the app, choose **Open Runbook** or **Open from URL** on the welcome screen. From a terminal, pass `runbooks open` a local path:
```bash
runbooks open /path/to/runbook
```
Or pass it a remote URL, without cloning the repo first:
```bash
runbooks open https://github.com/org/repo/tree/main/runbooks/launch-rds
```
Runbooks supports GitHub and GitLab browser URLs, as well as OpenTofu-style source addresses. See [`runbooks open`](/commands/open/) for all supported formats and for authentication with private repos.
## Next
Next, take the [UI tour](/intro/ui_tour/) to see a runbook in use.
---
### UI tour
This page follows a runbook consumer who opens the [lambda sample runbook](https://github.com/gruntwork-io/runbooks/tree/main/testdata/sample-runbooks/lambda). Their goal is to launch an AWS Lambda function in a way that matches their organization's standards.
There is a [written walkthrough](#written-walkthrough) and a [video walkthrough](#video-walkthrough).
## Written walkthrough
The user installs Runbooks, downloads the `lambda` sample runbook, and opens it:
```bash
runbooks open /path/to/lambda
```
The Runbooks window opens and renders the runbook.

So far the runbook is rendering markdown text.
> **Find in page:**
Runbooks can get long. Press **Cmd+F** on macOS, or **Ctrl+F** on Windows and Linux, to search the runbook and any logs or outputs you have expanded. Press **Enter** / **Shift+Enter** (or **Cmd/Ctrl+G** / **Shift+Cmd/Ctrl+G**) to move between matches, and **Esc** to close the find bar. Collapsed sections, such as logs you haven't opened, aren't searched, and neither is the text in form fields.
> **Command palette:**
Press **Cmd+K** on macOS, or **Ctrl+K** on Windows and Linux, to open the command palette, then type to filter. It holds everything in the **Menu** and the application menus: open or close a runbook, show the generated files, download logs, switch the theme or [instruction mode](/intro/instruction-mode/), open the docs, and copy the app's version for a bug report. **Jump to section** lists the runbook's headings, so you can move to any part of a long runbook without scrolling, and typing a heading's name offers it directly. Whatever you type, the last row is **Find "…" in page**, which opens the find bar on that text. Press **Backspace** in an empty search to go back to the commands, and **Esc** to close.

Next come pre-flight checks that confirm the user's machine has the right tools installed, in this case the `mise` tool version manager. When the user clicks "Check", Runbooks runs the command `mise --version && mise self-update --yes` on their machine.

The consumer uses the UI without knowing how the runbook is written. To the author, that first gray box is a [Check block](/authoring/blocks/check/), defined like this:
```mdx
```
Authors declare what they want to happen, and Runbooks renders it as an interactive UI.
Further down, the runbook generates the code needed to launch the Lambda function.

This form comes from a [Template block](/authoring/blocks/template/). To collect these values from the user, the author declared a set of variables like this:
```yaml
variables:
- name: Environment
type: enum
description: Target environment for deployment
options:
- non-prod
- prod
default: non-prod
x-section: Deployment Settings
- name: AwsRegion
type: enum
options:
- us-east-1
- us-east-2
- us-west-1
- us-west-2
- eu-central-1
- eu-west-1
- eu-west-2
description: The AWS region to deploy the Lambda function to
default: "us-west-2"
validations:
- required
x-section: Deployment Settings
- name: FunctionName
type: string
description: Name for your Lambda function (will be suffixed with environment)
default: example-lambda
validations:
- required
x-section: Function Settings
- name: Description
type: string
description: Description of the Lambda function (optional)
x-section: Function Settings
...
```
The user clicks the "Generate" button at the bottom of the form, which the screenshot does not show. Runbooks generates a set of files from the author's code template, using the values the user entered.

As the user changes values in the form, the rendered files update, so the user sees how each value changes the generated code.
Runbooks writes the generated files to the user's machine, so a script can open a GitHub pull request with them. This runbook's author included one.

The author used a [Command block](/authoring/blocks/command/) to create the pull request, and configured it to ask for a GitHub org name and repo name. The script that runs uses those values.
The last block is a Check that validates the Lambda function deployed.

## Video walkthrough
<div style="position: relative; padding-bottom: 56.25%; height: 0;"><iframe src="https://www.loom.com/embed/0848381b1e174670895e3228a69b865a" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe></div>
## Next
Next, [install Runbooks](/intro/installation/).
---
### Installation
Download the latest version of Runbooks from the [GitHub releases page](https://github.com/gruntwork-io/runbooks/releases).
#### macOS
Runbooks requires macOS 13 (Ventura) or later.
Download the `.dmg` file for your Mac from the latest GitHub release:
- Apple Silicon: `Runbooks-x.y.z-mac-arm64.dmg`
- Intel: `Runbooks-x.y.z-mac-x64.dmg`
Open the `.dmg` file and drag **Runbooks** into your **Applications** folder.
> **Tip:** To find out which chip you have, click the Apple menu and select **About This Mac**. A chip named "Apple M1" or similar needs the Apple Silicon version. "Intel" needs the Intel version.
#### Linux
Download one of the following from the latest GitHub release:
- `Runbooks-x.y.z-linux-x86_64.AppImage` runs on most Linux distributions without installation. Mark it as executable and run it:
```bash
chmod +x Runbooks-x.y.z-linux-x86_64.AppImage
./Runbooks-x.y.z-linux-x86_64.AppImage
```
- `Runbooks-x.y.z-linux-amd64.deb` is for Debian and Ubuntu. Install it with:
```bash
sudo dpkg -i Runbooks-x.y.z-linux-amd64.deb
```
After installing the `.deb` package, launch Runbooks from your application menu or by running `runbooks` in a terminal. The `.deb` package's `runbooks` command runs the app itself, so it holds the terminal and prints the app's logs until you quit Runbooks. For a `runbooks` command that gives you your prompt back right away, [install the command](#installing-the-runbooks-command) from Runbooks, then open a new terminal.
#### Windows
Download the installer, `Runbooks-x.y.z-win-x64.exe`, from the latest GitHub release.
Run the installer and follow its prompts. Then launch Runbooks from the Start menu or desktop shortcut.
## Installing the `runbooks` command
To open runbooks from a terminal with the [`runbooks` command](/commands/overview/), click **Install command line tool** on the welcome screen. You can also choose **Install 'runbooks' command in PATH** from the **Runbooks** menu on macOS, from the **Help** menu on Linux and Windows, or from the command palette (**Cmd/Ctrl+K**).
- On macOS and Linux, Runbooks writes a small launcher script to `/usr/local/bin/runbooks`. If you can't write to that folder, you're asked for your administrator password.
- On Windows, Runbooks writes `runbooks.cmd` to `%LOCALAPPDATA%\Runbooks\bin` and adds that folder to your user `PATH`. Open a new terminal to pick up the change.
The `runbooks` command starts Runbooks and gives you your prompt back right away. If yours holds the terminal and prints the app's logs until you quit Runbooks, it is an outdated launcher. Install the command again to replace it. The welcome screen shows **Install command line tool** while the launcher is outdated.
The launcher records where the app is installed. If you move Runbooks, or switch to a different AppImage file, install the command again to update it. On macOS, move Runbooks to your **Applications** folder before you install the command.
To remove the command, choose **Uninstall 'runbooks' command from PATH** from the same menu or from the command palette.
> **Caution:** If `/usr/local/bin/runbooks` already exists and Runbooks didn't create it (for example, an older Runbooks CLI), Runbooks won't overwrite or remove it. Installing shows an error instead. To install the command, remove or rename the existing file first.
## Building from source
To build the app yourself, install [mise](https://mise.jdx.dev/) first. It installs the other tools the build needs.
1. Clone the repository:
```bash
git clone https://github.com/gruntwork-io/runbooks.git
cd runbooks
```
2. Install tool versions and dependencies:
```bash
mise install
bun install
```
3. Build and package the app for your platform:
```bash
just package
```
On macOS, `just package` needs an Apple Developer ID certificate. Without one, run `just package-local`, which signs the app ad hoc.
The packaged app is written to the `out/` directory.
## Verifying installation
Open Runbooks. You should see the welcome screen.
## Next
Next, [write your first runbook](/intro/write_your_first_runbook/).
---
### Write your first runbook
This page builds a small runbook from scratch. You can also [download the complete example](/my-first-runbook.zip) as a zip file.
## Create the runbook files
1. Create a new folder called `my-first-runbook`.
1. Inside that folder, create the subfolders `checks`, `scripts` and `templates/project`.
1. Create each of the files below with the content shown.
The scripts don't need to be executable. Runbooks reads each one and runs it with the interpreter its first line names.
## Open your runbook
Save the files and open the runbook:
```bash
runbooks open my-first-runbook/
```
If your terminal can't find `runbooks`, [install the command](/intro/installation/#installing-the-runbooks-command), or choose **Open Runbook** on the app's welcome screen and pick the folder.
The Runbooks window opens and shows the runbook. Work through it from top to bottom and run each block.
## Next
This runbook generates some files. The [files workspace](/intro/files_workspace/) page explains where generated files and cloned repositories show up in the Runbooks UI.
---
### Files workspace
The files workspace is the panel on the right side of the Runbooks window, labeled **Files**. It shows two types of files:
- [Generated files](#generated-files) are new files the runbook creates. Use them when the user will do something with the files outside the runbook, such as copying them into another project.
- [Repository files](#repository-files) are a git repository the runbook cloned. Use them when later blocks create, modify, or delete files in that repo, usually before pushing the changes or opening a pull request.
> **Tip:** A runbook that only generates files shows only the **Generated files** tab. A runbook that only clones a repository shows only the **Repository** tab. When both are present, switch between them with the tabs at the top of the workspace.
## Generated files
There are two ways a runbook generates files. The workspace opens automatically when either one produces a file.
### Template blocks
The [Template](/authoring/blocks/template/) and [TemplateInline](/authoring/blocks/templateinline/) blocks generate files from [Boilerplate templates](/authoring/boilerplate/). When you fill in the form fields and click "Generate", the template engine renders the output files. The default `target` is `"generated"`.
```mdx
```
Template blocks re-render when you change input values, so the generated files match the form.
### Command and Check blocks with $GENERATED_FILES
Scripts run by [Command](/authoring/blocks/command/) and [Check](/authoring/blocks/check/) blocks can write files to the directory named by the `$GENERATED_FILES` environment variable:
```mdx
"$GENERATED_FILES/tofu-outputs.json"`}
title="Export OpenTofu outputs"
/>
```
Files written to `$GENERATED_FILES` appear in the workspace when the script exits with code 0 or 2.
### Where generated files are stored
Runbooks writes generated files to a `generated/` folder inside the folder that contains the runbook file. Template blocks and `$GENERATED_FILES` captures both write there, even after a script has `cd`'d somewhere else:
- my-runbook/
- runbook.mdx
- **generated/** created next to your runbook
- main.tf
- variables.tf
- outputs.tf
For a [remote runbook](/commands/open/#remote-sources), the runbook's folder is inside a temporary clone, so its generated files are deleted when you quit Runbooks.
### Subdirectories
A script that writes to a subdirectory of `$GENERATED_FILES` creates the subdirectory itself:
```bash
mkdir -p "$GENERATED_FILES/config"
echo '{}' > "$GENERATED_FILES/config/app.json"
```
- generated
- config/
- app.json
- main.tf
- variables.tf
- outputs.tf
## Repository files
Repository files come from a git repository that a [GitClone](/authoring/blocks/gitclone/) block cloned. When the clone completes, the workspace switches to the **Repository** tab.
The clone is stored at the local path set in the GitClone block, relative to the working directory. The working directory starts as the folder that contains `runbook.mdx`, and it [follows any `cd`](/authoring/blocks/command/#execution-context) that an earlier script made.
Later changes to the repository come from a [Template](/authoring/blocks/template/) block that sets `target="worktree"`, or from Command and Check scripts that write to the directory named by `$REPO_FILES`.
The Repository tab has two sub-tabs.
### All files
The **All files** tab shows the complete file tree of the cloned repository. Click any file to view its contents with syntax highlighting.
### Changed files
The **Changed files** tab shows a diff of the files that changed since the repository was cloned:
- Which files were added, modified, or deleted
- Line-by-line additions and deletions
- A summary of total changes at the top
> **Note:** The **Changed files** tab appears the first time Runbooks detects a change.
### Multiple repositories
If a runbook clones more than one repository, a dropdown at the top of the **Repository** tab replaces the repository and branch name and switches between them.
## Where Runbooks reads and writes files
Runbooks checks each path a block asks it to read or write. It resolves symlinks first, then allows the path only when it is inside one of these:
- the working directory
- the folder that contains the runbook
- a repository registered by a GitClone block
This covers the template folder a Template block reads, the folder Template and TemplateInline blocks write to, and the file tree, file contents, and git changes shown in the workspace. A symlink inside an allowed folder that points outside it is rejected.
A GitClone destination has a stricter rule, because cloning can delete what is already there. It must be a subdirectory of the working directory, and it must not contain the open runbook.
> **Note:** These checks apply to paths that blocks pass to Runbooks. Scripts run by Command and Check blocks have full filesystem access and can write anywhere the user has permissions.
## Next
Next, read about the [use cases](/intro/use_cases/) for Runbooks.
---
### Use cases
These are the situations where teams most often wait on an infrastructure expert.
## Developer self-service
To use an infrastructure pattern, an app developer has to look it up, learn enough of the technology to write the code, debug it, and deploy it. Many ask an infrastructure expert for help and then wait for an answer.
A runbook written by that expert lets app developers launch new infrastructure that follows your organization's practices. For example:
- Create a new K8s Service
- Scaffold a new app repo
- Launch a new database
- Launch a new static website
> **Tip:**
The [UI tour](/intro/ui_tour/) walks through how an app developer might deploy a Lambda function.
## Landing zones
Most engineering organizations eventually need a repeatable way to create new AWS accounts, GCP projects, or Azure subscriptions, organize them, and install a baseline of resources for security and governance. This is called setting up a landing zone.
Setting up a landing zone means solving three problems:
1. Define your baseline, for example as OpenTofu modules.
2. Define a way to create a new AWS account, GCP project, or Azure subscription, for example through the cloud API or an OpenTofu module.
3. Present a UI where users configure the account, project, or subscription they want.
OpenTofu modules and cloud APIs already solve the first two. A runbook solves the third by combining them. It asks the user for values such as an email, name, and description, calls the API to provision the account, and generates the code that installs the baseline.
A value the runbook asks for, such as an AWS account ID, is available to the code it generates and the scripts it runs.
The same runbook can provision SSO access, explain how to use the new resources, point the user at policies to review, and link to their account, without a custom user interface.
## Internal processes
Many internal processes are captured as static documentation. A runbook carries out the process as you read it. For example:
- Provision a new AWS Account, configure the account baseline and all networking configurations, and validate everything along the way.
- Stand up a new customer with all the infrastructure and all the information they need to get started.
## Document IaC modules
A reusable infrastructure-as-code (IaC) module needs more than its code. Users need to know how to use it, see common usage examples, and confirm that it works.
A runbook collects configuration from the user, generates the module code, and validates the result. For example:
- Create an SNS topic that notifies a Slack channel
- Create an SNS topic that notifies an email address
- Create an SNS topic that notifies a PagerDuty service
You might define one Runbook for each of these, or a single Runbook that can create any of these configurations.
## Next
Next, see how [Runbooks compares to other tools](/intro/runbooks_vs_other/).
---
### Runbooks vs. other tools
## vs. static documentation
Static documentation in Notion, Confluence, or a git repo is easy to write. It also goes out of date, has no automated validation, and leaves users to copy, paste, and adapt its code samples.
A runbook generates the files a consumer needs from values they enter in a form, runs commands for the other steps, and runs checks so the consumer can confirm each step worked.
Authors write a runbook as an MDX file, which is markdown plus a small set of components that Runbooks calls [blocks](/authoring/blocks/).
Consumers can report a missing check, command, or template input, and the author can add it to the runbook for the next consumer. A runbook can also generate automated tests alongside the code it produces.
## vs. internal developer portals
Internal developer portals (IDPs) like [Backstage](https://backstage.io/) and [Port](https://www.getport.io/) give developers one interface for service catalogs, software templates, API documentation, and dashboards.
One popular use for IDPs is template generation. Backstage, for example, uses the Scaffolder plugin for templates written in the Nunjucks templating language. It presents users with a catalog of templates to choose from, but the templating has shortcomings.
End users cannot preview the code they will generate as they fill in values, and cannot easily validate that what they generated works. Any documentation for the template is typically generated as code, separate from the form the user fills in.
Template authors have to run the same template repeatedly and work through Backstage configuration issues. Backstage itself takes real effort to set up and maintain.
With Runbooks, a consumer installs the desktop app and runs `runbooks open /path/to/runbook`, or `runbooks open https://github.com/org/repo/tree/main/path/to/runbook` for a remote runbook. In one window they read the documentation, see the files they will generate as they type, run commands, and run checks.
An author installs the same app, writes a `runbook.mdx` file, and sees each save reloaded with `runbooks open --watch /path/to/runbook`. Authors can test template generation in Runbooks, or run the [Gruntwork Boilerplate](https://github.com/gruntwork-io/boilerplate) templating engine directly.
## vs. Jupyter notebooks
Jupyter notebooks and runbooks both combine code and documentation in one document. They differ in who the document is for, what each step produces, and how much the document can do.
### Who the document is for
A Jupyter notebook is optimized for its author, who uses it as a canvas to evolve program state step by step and show their work.
A runbook is optimized for its consumer. The author writes down what they know about a specific DevOps pattern, and the consumer applies it.
Running a notebook means setting up Python and Jupyter first. Opening a runbook takes the desktop app and one command, `runbooks open /path/to/runbook`, or the same command with a remote URL such as `runbooks open https://github.com/org/repo/tree/main/runbooks/my-runbook`.
### What each step produces
Each cell in a Jupyter notebook changes the state of a Python program.
Each block in a runbook produces something outside the document: generated files, changes made by commands, and checks that confirm the step worked.
### How much the document can do
Jupyter notebooks can execute arbitrary code, draw charts, and let authors trace back and restart execution.
Runbooks does less. Blocks pass environment variables and named outputs to later blocks, but no in-memory program state. In exchange, a consumer fills in a form to generate files, run commands, or run checks.
---
### Instruction mode
By default, Runbooks is interactive. When you click **Run**, **Authenticate**,
**Generate**, **Clone**, or **Create PR**, the app performs that action on your
machine. It executes scripts, captures credentials, renders templates to disk,
and talks to git and your git host for you.
In instruction mode, Runbooks performs no actions. Nothing is executed, no
credentials are captured, and no files, clones, or pull requests are written.
The runbook renders top to bottom with the same prose, and each block shows the
command or step to perform by hand, with a button to copy it.
## When to use it
- **Windows and other non-POSIX environments.** Many runbook scripts and the
boilerplate generator assume a Unix-style shell. Instruction mode shows the
command so you can run it yourself or adapt it to PowerShell.
- **Locked-down or managed machines** where the app can't run scripts, write
files, or hold credentials.
- **Review before running.** Read each command, then run it in your own
terminal.
## Turning it on
Instruction mode is a global setting:
1. Open the **Menu** in the top-right of the window.
2. Under **Mode**, toggle **Instruction mode**.
Or press **Cmd/Ctrl+K** and run **Turn on Instruction mode** from the command
palette.
The setting is off by default and is remembered across launches. Turning it off
restores the interactive blocks with their state intact. While it is on, a
banner at the top of every runbook says that nothing runs automatically.
## What each block becomes
| Block | Interactive | Instruction mode |
|---|---|---|
| Command, Check | Run button. The app executes the script. | "Run this command:" or "Run this check:" with the resolved command in a copy block. A nested `` stays a form above the command. File-backed scripts keep the source viewer. |
| AwsAuth | SSO, profile, or key UI. Captures credentials. | "Log into AWS in the `<account>` account", with the configured SSO details. No capture. |
| GoogleAuth | Service account key, Google sign-in, or gcloud config UI. Captures credentials. | "Log into Google Cloud in the `<project>` project", noting the region, gcloud configuration, scopes, and the `gcloud auth application-default login` command. No capture. |
| GitAuth, GitHubAuth, GitLabAuth | OAuth or token entry. Holds a token. | "Log into GitHub" or "Log into GitLab", noting the scopes the block needs. For GitLab it also names `glab auth login`. No capture. |
| Inputs | Form you fill | Still a form. Its values are substituted into the displayed commands. |
| Template | Generate button. Renders boilerplate to disk. | The variable form plus a copyable `boilerplate` invocation. No files written. |
| TemplateInline | Inline rendered preview | The rendered text as a copy block. No files written. |
| GitClone | Clone button | A copyable `git clone` command, with a sparse-checkout note for sub-paths. |
| GitPullRequest, GitHubPullRequest, GitLabMergeRequest | Create PR or MR button | A copyable `gh pr create` or `glab mr create` command, plus a reminder to commit and push your branch first. |
| DirPicker | Cascading directory dropdowns | A text field for the directory path. The path you enter is substituted into later commands. |
| Admonition, Iframe | Callout, embedded page | Unchanged. |
## Filling in values
Runbooks substitutes every value you supply into the displayed commands. Each
`{{ .inputs.* }}` or `{{ .outputs.* }}` reference in a command shows either its
value or, until you supply one, a placeholder such as `<bucket>`:
- **`Inputs` form values** are substituted directly into the commands shown,
including those from an `` nested inside a Command or Check. An input
you haven't filled in yet shows as a `<name>` placeholder. A checkbox always
has a value (unchecked is `false`), so it never shows one. An input you've
left empty stays empty, so conditionals and defaults in the command work out
just as they would when it runs. An input used only in template logic, such
as the condition of an `{{ if ... }}`, gets no placeholder. Until you fill it
in, that logic is shown as written.
- **Values produced by an earlier step** (a script's outputs) can't be known by
the app, because it runs nothing. When a command needs one, instruction mode
shows a small field labeled with the output's name and the step it comes
from, for example `account_id` from step `create_account`. Run that earlier
step yourself, then paste the value in. The command updates as you type. A
[sensitive output](/authoring/inputs-and-outputs/#sensitive-outputs) always
gets a field, even if its step already ran, so the command never shows it.
## Tracking your progress
Each step has a **Mark as done** checkbox in its top-right corner. Tick it once
you've performed that step by hand and the block turns green. Your progress is
remembered per runbook across reloads, and when you turn instruction mode off
and back on.
## Notes
- Commands are resolved with the same template engine the interactive UI uses
to preview them. If that engine is unavailable or can't render a command,
Runbooks falls back to a simpler resolver and shows a small note. That
resolver fills in value references only, so template logic such as
conditionals and functions may be shown as written.
- Instruction mode changes what the app displays. It is not a headless CLI or
CI runner.
---
## App & CLI
### Overview
Runbooks is a desktop app. Launch it from its app icon or with the `runbooks` command, which takes a local path or a remote URL as the runbook source.
- [`runbooks open`](/commands/open/) opens a runbook in the desktop app.
- [Watch mode](/commands/watch/) reloads a runbook while you author it.
## Launching from the command line
```bash
runbooks open ./path/to/runbook
```
## Remote URLs
You can open a runbook from a remote URL without downloading its files first.
```bash
runbooks open https://github.com/org/repo/tree/main/runbooks/launch-rds
```
See [`runbooks open`](/commands/open/#remote-sources) for supported URL formats and authentication for private repos.
---
### open
`runbooks open` opens a runbook in the Runbooks desktop app.
## Usage
```bash
runbooks open RUNBOOK_SOURCE [flags]
```
## Arguments
`RUNBOOK_SOURCE` is a local path or remote URL that points to a `runbook.mdx` file or to the directory that contains one.
### Local paths
```bash
runbooks open ./path/to/runbook
runbooks open /absolute/path/to/runbook
runbooks open ./path/to/runbook.mdx
```
Because `open` is the command name, write a folder that is named `open` as a path: `runbooks open ./open`.
### Remote sources
You can open a runbook directly from a GitHub or GitLab repo, or from a repo on any other git host, without cloning it first. The source can point at the repo root, at a directory containing a `runbook.mdx`, or at a runbook file. The same sources work in the desktop app's **Open from URL** dialog.
#### Browser URLs
Copy the URL from your browser's address bar while viewing the runbook's directory or its `runbook.mdx`:
```bash
runbooks open https://github.com/org/repo/tree/main/runbooks/setup-vpc
runbooks open https://github.com/org/repo/blob/main/runbooks/setup-vpc/runbook.mdx
runbooks open https://gitlab.com/group/subgroup/repo/-/tree/main/runbooks/setup-vpc
```
| Format | Example |
|--------|---------|
| GitHub directory | `https://github.com/org/repo/tree/main/runbooks/setup-vpc` |
| GitHub file | `https://github.com/org/repo/blob/main/runbooks/setup-vpc/runbook.mdx` |
| GitLab directory | `https://gitlab.com/group/repo/-/tree/main/runbooks/setup-vpc` |
| GitLab file | `https://gitlab.com/group/repo/-/blob/main/runbooks/setup-vpc/runbook.mdx` |
| Repo root | `https://github.com/org/repo` |
The branch, tag or commit in the URL is used. Branch names that contain slashes (`release/v1`) work. So do commit permalinks (`/blob/<sha>/...`) and query strings or anchors the browser adds, such as GitLab's `?ref_type=heads`. For `github.com` and `gitlab.com`, the `https://` prefix is optional.
Directory and file URLs from GitHub Enterprise and self-hosted GitLab work too, and need the `https://` prefix. A repo root URL on one of those hosts works only when Runbooks can tell which kind of host it is:
- A GitHub Enterprise Cloud host on `ghe.com`, or a GitHub Enterprise Server host you logged in to with `gh auth login` or named in `GH_HOST`.
- A GitLab host with `gitlab` as a dot- or hyphen-separated part of its name, such as `gitlab.example.com` or `gitlab-ce.corp.net`.
For a repo root on any other host, use a `git::` source.
#### Git sources in go-getter syntax
Runbooks also accepts git sources written in [go-getter](https://github.com/hashicorp/go-getter) syntax, the syntax of a Terragrunt `source` and of an OpenTofu or Terraform module source. A double slash (`//`) separates the repo from the path inside it. `?ref=` picks a branch, tag or commit. Without `?ref=`, Runbooks uses the repo's default branch.
```bash
runbooks open github.com/org/repo//runbooks/setup-vpc?ref=v1.0
runbooks open github.com/org/repo//runbooks/setup-vpc/runbook.mdx
runbooks open gitlab.com/group/subgroup/repo//runbooks/setup-vpc?ref=main
runbooks open "git::https://git.example.com/org/repo.git//runbooks/setup-vpc?ref=main"
runbooks open "git@github.com:org/repo.git//runbooks/setup-vpc?ref=v1.0"
```
| Format | Example |
|--------|---------|
| GitHub shorthand | `github.com/org/repo//runbooks/setup-vpc?ref=v1.0` |
| GitLab shorthand | `gitlab.com/group/subgroup/repo//runbooks/setup-vpc?ref=v1.0` |
| Any git host over HTTPS | `git::https://git.example.com/org/repo.git//runbooks/setup-vpc?ref=main` |
| SSH | `git::ssh://git@github.com/org/repo.git//runbooks/setup-vpc` or `git@github.com:org/repo.git//runbooks/setup-vpc` |
As in go-getter, `github.com/org/repo/runbooks/setup-vpc` (without the `//`) also works, because a GitHub repo is always `owner/repo`. GitLab projects can sit under nested groups, so GitLab shorthand needs the `//` to mark where the path starts. Quote sources that contain `?` or `&` so your shell leaves them alone.
Runbooks parses these sources itself and fetches them with git. It doesn't use the go-getter library, so it differs from go-getter in a few places:
- Runbooks recognizes scheme-less shorthand for `github.com` and `gitlab.com` only. `runbooks open` reads any other scheme-less source, `bitbucket.org/org/repo` included, as a local path. For other hosts, use `git::https://`.
- `git::` takes a full `https://`, `http://` or `ssh://` URL, or a `git@host:path` address. Runbooks rejects `git::github.com/org/repo`.
- Git over HTTPS, HTTP or SSH is the only way Runbooks fetches. go-getter's other getters (`s3::`, `gcs::`, `hg::`) and archive URLs such as `https://example.com/runbook.zip` don't work, and Runbooks rejects `git::file://`.
- `?ref=` is the only query parameter Runbooks reads. It ignores `depth`, `sshkey` and any others.
- A `+` in `?ref=` stays a `+` (`?ref=v1.0.0+build.1`), where go-getter reads it as a space.
> **Note:** When you open a remote runbook, Runbooks clones the whole repository into a temporary directory, as go-getter does for a `//` subdirectory. The runbook's folder inside that clone becomes the working directory: scripts start there, and generated files are saved inside it. Because the rest of the repository is there too, a runbook can use files outside its own folder, such as templates elsewhere in the repo. The temporary directory is deleted when you quit Runbooks.
### Authentication for private repos
Public repositories need no authentication. For a private repository:
- On GitHub, set the `GITHUB_TOKEN` or `GH_TOKEN` environment variable, or run `gh auth login`.
- On GitLab, set the `GITLAB_TOKEN` environment variable, or run `glab auth login`.
A token in an environment variable is sent only to `github.com` or `gitlab.com` unless you name another host:
- For GitHub Enterprise Server, set `GH_ENTERPRISE_TOKEN` and `GH_HOST=<host>`, or run `gh auth login --hostname <host>`.
- For a GitHub Enterprise Cloud tenant on `ghe.com`, set `GITHUB_TOKEN` and `GH_HOST=<host>`.
- For self-hosted GitLab, set `GITLAB_TOKEN` and `GITLAB_HOST=<host>`, or run `glab auth login --hostname <host>`.
These tokens are used only for HTTPS sources. SSH sources (`git@...` or `ssh://...`) authenticate with your SSH keys, so the key must be loaded in your SSH agent and the host must already be in `~/.ssh/known_hosts`.
## Flags
| Flag | Effect |
|------|--------|
| `--watch` | Turns on [watch mode](/commands/watch/), which reloads the runbook when you save it. |
| `--no-telemetry` | Turns off anonymous telemetry. Setting `RUNBOOKS_TELEMETRY_DISABLE=1` does the same. |
| `--verbose` | Keeps the command attached to the terminal and prints the app's logs there until you quit Runbooks. Use it to troubleshoot a launch that shows no window. It shows logs only for a Runbooks it starts. If Runbooks is already running, the command hands the runbook to it and exits. |
## What it does
Like `code .`, `runbooks open` gives you your prompt back right away without printing anything. Runbooks then carries on in the background:
1. For a remote source, it clones the repository that holds the runbook. If the download fails, for example because a private repo has no token, the app shows an error dialog that explains why.
2. It opens the Runbooks window and renders the `runbook.mdx` file.
3. It keeps running until you quit it, even if you close the terminal you started it from.
If the app is already running, a second invocation focuses the existing window and opens the new runbook in it. A relative path is resolved from the directory you ran the command in.
> **Note:** The `runbooks` command that the Linux `.deb` package puts in `/usr/bin` runs the app itself, so it holds the terminal and prints the app's logs until you quit Runbooks. To get the command described here, [install it from Runbooks](/intro/installation/#installing-the-runbooks-command). It goes in `/usr/local/bin`, which comes before `/usr/bin` in the default `PATH`.
## Troubleshooting
### Nothing happens
The command doesn't show the app's output, so a launch that fails is silent. Run the same command again with `--verbose` to see the app's logs in the terminal. If Runbooks is already running, even with no window, quit it first, or `--verbose` hands the runbook to it and exits without showing its logs. To quit a Runbooks with no window, end its process in Activity Monitor on macOS, your system monitor on Linux, or Task Manager on Windows.
### "Runbooks was not found"
The `runbooks` command records where Runbooks is installed. If you moved or reinstalled Runbooks, open it and [install the command again](/intro/installation/#installing-the-runbooks-command).
### Runbook not found
Check that the path points to a `runbook.mdx` file or a directory containing one. For a remote source, check that the path exists at the branch, tag or commit you named. A source in go-getter syntax without `?ref=` reads the repo's default branch.
### Authentication errors for remote runbooks
If you see "authentication required", set the token for the git host as described in [Authentication for private repos](#authentication-for-private-repos).
### Run from a folder that was deleted
If your terminal is still in a folder that has since been deleted (a removed git worktree, for example), your shell prints a warning like this when you run `runbooks`:
```text
shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
```
Runbooks still starts, or focuses the window that is already open. A relative path such as `runbooks open .` usually still points into the deleted folder, so the app shows "This path no longer exists" unless a folder has been created again at that path. Some shells can't say which folder they were in (`zsh -c` started from the deleted folder, for example), and then relative paths resolve against your home folder. Either way, `cd` into a folder that exists, or pass an absolute path.
---
### Watch mode
Watch mode reloads the runbook in the app each time you save `runbook.mdx`. It is meant for runbook authors.
> **Caution:** Each reload rebuilds the [executable registry](/security/execution-model/#executable-registry) from the files on disk, so after a reload the app executes the current version of your commands and scripts. Pass `--disable-live-file-reload` to keep executing the versions that were present when the runbook was opened in this app session (opening another runbook and coming back rebuilds the registry).
Only changes to `runbook.mdx` itself trigger a reload. After you change a script that a `` or `` block runs from a file, the block shows a "Script changed" notice with the diff and keeps executing the version it loaded. Click **Reload script** in the notice to execute the new version, or save `runbook.mdx`, even unchanged: the reload rebuilds the registry and those blocks re-read their scripts, so each block shows the script it will execute. With `--disable-live-file-reload`, an unchanged save keeps the registry, so reload each changed script from its block. After you change a template or another file the runbook uses, change something in `runbook.mdx`, or close and reopen the runbook, to load it.
## Activating watch mode
Pass `--watch` when you launch the app from the command line:
```bash
runbooks open --watch RUNBOOK_SOURCE
```
Then edit `runbook.mdx` in your editor and save it. The app shows the change about 300ms later.
Watch mode stays on for every runbook you open until you quit the app.
`--watch` and `--disable-live-file-reload` are read only when they start the app. If Runbooks is already running, a second `runbooks` command opens the runbook in the existing window with that window's modes, whatever flags it passes. To change modes, quit Runbooks first.
## Arguments
`RUNBOOK_SOURCE` is a local path or remote URL that points to a `runbook.mdx` file or to the directory that contains one. See [`runbooks open`](/commands/open/#remote-sources) for the remote URL formats.
> **Note:** For a remote runbook, Runbooks watches the local copy it downloaded. It does not detect changes to the remote repository.
## Flags
`--disable-live-file-reload` keeps the executable registry that was built when the runbook was opened in this app session. Opening another runbook and coming back rebuilds it. Reloads still update what the app shows, but Runbooks keeps executing the original commands and scripts, and blocks whose script has changed show a "Script changed" warning. A block that runs a script file offers to [reload that script](/security/execution-model/#reloading-a-changed-script-file) from the warning.
## Technical details
### File watching
- Runbooks watches the directory that contains your runbook file with `chokidar`, so it still sees editors that save by replacing the file.
- It does not watch subdirectories, such as cloned repositories or generated output.
- It reacts only to write and create events for the runbook file. Changes to other files in the directory are ignored.
- It waits 300ms after the last event, so an editor that saves a file several times triggers one reload.
- Script files that blocks run are watched separately, with or without `--watch`. A change to one never reloads the runbook: its block shows the "Script changed" notice.
### Reloading
- When the file changes, the main process sends a `watch:file-change` IPC event to the renderer.
- The renderer reloads the runbook, which re-reads the file and rebuilds the executable registry unless `--disable-live-file-reload` is set.
- A reload keeps the session. Environment variables that blocks exported, and the directory a block moved to with `cd`, stay as they were. Re-opening the runbook starts its blocks from the runbook's directory again.
---
## Authoring Runbooks
### Overview
A runbook combines markdown with interactive blocks that:
- Validate the user's current state with automated checks
- Execute shell commands and scripts
- Collect user input through forms
- Generate files from templates
All of this runs locally on the user's machine in the Runbooks desktop app.
## Quick start
For a complete tutorial, see [Write your first runbook](/intro/write_your_first_runbook/).
## Section guide
- [Runbook structure](/authoring/runbook-structure/) covers the file format and folder layout.
- [Markdown](/authoring/markdown/) is the reference for supported markdown elements.
- [Inputs and outputs](/authoring/inputs-and-outputs/) explains how data flows between blocks: collecting user input, wiring it with `inputsId`, and passing runtime outputs to later blocks.
- [Boilerplate templates](/authoring/boilerplate/) covers template syntax and `boilerplate.yml` files.
- [Blocks](/authoring/blocks/) is the reference for every interactive block.
- [Opening runbooks](/authoring/opening-runbooks/) covers the ways to open a runbook, locally or from a remote URL.
- [Testing](/authoring/testing/) covers `runbook_test.yml` and the test CLI.
---
### Runbook Structure
## File format
A runbook lives in its own folder and must contain a file named `runbook.mdx`. The file is MDX (Markdown plus JSX), so it mixes [standard markdown](/authoring/markdown) (headers, paragraphs, lists, code blocks, images, links) with [blocks](/authoring/blocks) such as ``, ``, ``, and ``.
### Common structure
You can organize `runbook.mdx` however you like. A common pattern:
`````mdx
# Runbook title
Introduction paragraph explaining what this runbook does.
## Pre-flight checks
Make sure the user is set up for success.
## Execute actions
Run commands on behalf of the user to complete the runbook.
## Generate code
Generate code the user needs to accomplish their task.
## Post-flight checks
Verify everything worked.
`````
## Folder organization
Blocks reference scripts and templates by path. You can store them anywhere, but this layout is the convention:
- my-runbook/
- runbook.mdx Main runbook file
- assets/ Images and other assets
- diagram.png
- checks/ Shell scripts for `` blocks
- prereq1.sh
- prereq2.sh
- scripts/ Shell scripts for `` blocks
- deploy.sh
- cleanup.sh
- templates/ Boilerplate templates for `` blocks
- my-template/
- boilerplate.yml
- main.tf
- variables.tf
### Folder purposes
| Folder | Purpose | Used by |
|--------|---------|---------|
| `assets/` | Images, diagrams, videos, and other static files | Markdown image syntax and `<img>`, `<video>`, `<audio>` tags |
| `checks/` | Shell scripts that validate prerequisites | `` blocks |
| `scripts/` | Shell scripts that execute actions | `` blocks |
| `templates/` | Boilerplate template directories | `` blocks |
### Relative paths
Reference files relative to the runbook's folder:
```mdx

<img src="./assets/diagram.png" width="400" />
<video src="./assets/demo.mp4" poster="./assets/demo-poster.png" controls />
```
Asset paths must start with `./assets/`. Runbooks loads them from the runbook's `assets/` folder when they appear in markdown image syntax, including in a block prop that supports inline markdown such as `title` or `description`, or in one of these tag attributes:
| Tag | Attributes |
|-----|------------|
| `<img>` | `src`, `srcSet` |
| `<video>` | `src`, `poster` |
| `<audio>` | `src` |
| `<source>` | `src` (inside `<video>` or `<audio>`), `srcSet` (inside `<picture>`) |
| `<track>` | `src` |
In a `srcSet`, each candidate's path is rewritten and its descriptor (`2x`, `480w`) is kept. Write the path as a plain string: `src={"./assets/demo.mp4"}` is left as written and won't load.
Video and audio are read from the file as they play, and you can seek anywhere in them.
`assets/` must be a real folder, not a symlink. If `assets` is a symlink, Runbooks serves nothing from it and every asset fails to load, even when the symlink points inside the runbook's folder. Otherwise `assets -> .` would serve `runbook.mdx`, scripts and generated files to any page the runbook embeds. Symlinks inside `assets/` work when they point to files inside `assets/`.
The [``](/authoring/blocks/iframe/) block's `src` also takes a `./assets/` path.
---
### Markdown Support
Runbooks renders GitHub-flavored Markdown (GFM).
## Supported elements
### Headers
```markdown
# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5
###### Header 6
```
### Text formatting
```markdown
**Bold text**
*Italic text*
***Bold and italic***
~~Strikethrough~~
`Inline code`
```
### Lists
Unordered lists:
```markdown
- Item 1
- Item 2
- Nested item
- Another nested item
- Item 3
```
Ordered lists:
```markdown
1. First item
2. Second item
3. Third item
1. Nested numbered item
```
Task lists:
```markdown
- [x] Completed task
- [ ] Incomplete task
- [ ] Another task
```
### Links
```markdown
[Link text](https://example.com)
[Link with title](https://example.com "Title text")
```
### Autolinks
URLs and email addresses are automatically converted to clickable links:
```markdown
Visit https://gruntwork.io for more info.
Contact support@example.com for help.
```
### Images
```markdown


```
Image paths must start with `./assets/`. Runbooks loads them from the `assets/` folder next to your runbook file. Any other relative path, such as `images/diagram.png` or `assets/diagram.png` without the leading `./`, won't load. To set an image's size, use an `<img>` tag with the same kind of path, for example `<img src="./assets/image.png" width="400" />`. See [Relative paths](/authoring/runbook-structure/#relative-paths) for video and audio.
### Code blocks
Inline code:
```markdown
Use the `npm install` command to install dependencies.
```
Code blocks with syntax highlighting:
````markdown
```bash
echo "Hello, world!"
```
```python
def hello():
print("Hello, world!")
```
```javascript
console.log("Hello, world!");
```
````
Supported languages include bash, sh, shell, python, javascript, typescript, go, rust, java, terraform, hcl, yaml and json.
### Blockquotes
```markdown
> This is a blockquote.
> It can span multiple lines.
>
> And have multiple paragraphs.
```
### Horizontal rules
```markdown
---
***
___
```
### Tables
```markdown
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 |
```
With alignment:
```markdown
| Left-aligned | Center-aligned | Right-aligned |
|:-------------|:--------------:|--------------:|
| Left | Center | Right |
```
### Footnotes
```markdown
Here is a sentence with a footnote.[^1]
[^1]: This is the footnote content.
```
Footnotes are collected and rendered at the bottom of the document.
## MDX features
MDX adds a few things on top of markdown.
### Mix markdown and JSX
```mdx
# My Runbook
Regular markdown text here.
More markdown text.
```
### Literal values in props
Use `{...}` to pass a prop value that isn't a plain string, such as a number, a boolean, an array or an object:
```mdx
```
Runbooks never runs JavaScript from your `runbook.mdx`. The value inside `{...}` must be a literal: a string, number, boolean or `null`, a template string without `${...}` substitutions, or an array or object made only of those. You can also write `{/* comments */}`. `import` and `export` statements, JavaScript expressions such as `{new Date().toLocaleDateString()}`, and spread props such as `{...props}` are rejected with an error when the runbook opens. See the [execution security model](/security/execution-model/) for why.
### HTML
You can use HTML directly in markdown:
```markdown
<div style="color: red;">
This text will be red.
</div>
```
Because Runbooks never runs JavaScript from your `runbook.mdx`, elements and props that load scripts, embed other documents or inject raw HTML are rejected with an error. These include `<script>`, `<iframe>`, `<object>`, `<embed>`, custom elements such as `<my-widget>`, namespaced elements such as `<svg:script>`, `dangerouslySetInnerHTML`, `srcDoc` and `javascript:` URLs in props. See the [execution security model](/security/execution-model/) for the full list.
### Escaping special characters
To display a markdown character literally, escape it with a backslash:
```markdown
\* This won't be italic
\# This won't be a header
\`This won't be code\`
```
### Code blocks inside blocks
Blocks such as `` take a fenced code block as their content:
````mdx
```yaml
variables:
- name: Example
type: string
```
````
To show a fenced block inside another fenced block, as this page does, give the outer fence more backticks than the inner one. Do not escape the inner backticks.
---
### Inputs & Outputs
Every runbook has a global context, a shared set of key-value pairs that blocks read and write. The keys are variables.
Values enter the global context in two ways. [Input providers](#input-providers) such as `` and `` collect values from the user through a form, and consumer blocks pull them in with the [`inputsId` prop](#wiring-blocks-with-inputsid). Blocks that run something, such as ``, `` and ``, publish results that later blocks read through the [`outputs` namespace](#block-outputs).
## Variables
Reference a variable in templates, scripts, and block props as `{{ .inputs.VarName }}`. For example:
```mdx
```
When this block runs, Runbooks resolves `{{ .inputs.Email }}` and `{{ .inputs.AccountName }}` from the global context, producing a command like:
```bash
aws organizations create-account --email "alice@example.com" --account-name "my-account"
```
### Boilerplate variables
Runbooks uses [Gruntwork Boilerplate](/authoring/boilerplate/) as its template engine, so variables are Boilerplate variables:
- The same `{{ .inputs.VarName }}` syntax
- The same types (`string`, `int`, `float`, `bool`, `enum`, `list`, `map`)
- The same validation rules (`required`, `email`, `url`, `alpha`, `digit`, `alphanumeric`, `countrycode2`, `semver`, `regex`)
- The same `boilerplate.yml` format for defining them
- The Boilerplate helper functions and pipe modifiers (`upper`, `lower`, `snakeCase`, `camelCase`, `fromJson`, conditionals, loops)
The [Boilerplate templates](/authoring/boilerplate/) page covers the syntax, variable definitions, helper functions, and testing templates locally with the Boilerplate CLI.
#### Template syntax
Reference inputs as `{{ .inputs.VarName }}` and outputs as `{{ .outputs.blockId.outputName }}`. Runbooks also exposes every input at the root of the context, so `{{ .VarName }}` works in every block and in templates written for the Boilerplate CLI.
> **Caution:** `inputs` and `outputs` are reserved and cannot be variable names in `boilerplate.yml`.
## Input providers
An input provider is a block that collects variable values and makes them available to other blocks.
### \The [``](/authoring/blocks/inputs/) block renders a form from a `boilerplate.yml` definition. This inline definition declares one variable, `ProjectName`:
`````mdx
```yaml
variables:
- name: ProjectName
type: string
description: Name for your project
```
`````
### \The [``](/authoring/blocks/template/) block generates files from a Boilerplate template directory. It renders a form for the variables in that directory's `boilerplate.yml`, so it is an input provider too.
```mdx
```
Here the variables come from `templates/rds/boilerplate.yml`.
## Wiring blocks with `inputsId`
Any block that consumes variables (``, ``, ``, ``) accepts an `inputsId` prop naming an input provider by its `id`. This Command uses the value collected by the Inputs block:
`````mdx
```yaml
variables:
- name: ProjectName
type: string
```
`````
### Multiple sources
Pass an array of IDs to `inputsId` to merge variables from more than one input provider. Variables merge in order, so on a name conflict the later ID wins.
```mdx
```
### Embedded inputs
An `` block nested inside a `` or `` feeds the parent without `inputsId`:
`````mdx
```yaml
variables:
- name: Name
type: string
```
`````
Other blocks can still reference the embedded Inputs with `inputsId="greeting"`.
## When variables overlap
A consuming block receives every variable from its input provider.
`` also has its own `boilerplate.yml`, which can define variables too. When a Template imports from an input provider, Runbooks compares the two sets:
- A variable only the Template defines appears as an editable field in the Template's form. Use this to collect extra inputs, such as an environment name, that the provider doesn't know about.
- A variable only the input provider defines is passed to the template engine and doesn't appear in the Template's form.
- A variable both define appears in the Template's form read-only, synced to the input provider's value, so the two forms can't disagree.
## Block outputs
A block that has run can publish outputs for later blocks to read.
### Producing outputs
[``](/authoring/blocks/command/) and [``](/authoring/blocks/check/) blocks produce outputs by writing `key=value` pairs to the file specified by the `$RUNBOOK_OUTPUT` environment variable:
```bash
#!/bin/bash
ACCOUNT_ID=$(aws organizations create-account ...)
echo "account_id=$ACCOUNT_ID" >> "$RUNBOOK_OUTPUT"
echo "region=us-west-2" >> "$RUNBOOK_OUTPUT"
```
For complex data (lists, maps), serialize as JSON:
```bash
echo 'users=["alice","bob","charlie"]' >> "$RUNBOOK_OUTPUT"
```
Runbooks sets `$RUNBOOK_OUTPUT` before the script runs.
### Sensitive outputs
When an output holds a credential, such as a secret key, token or password, write `sensitive:` before its key:
```bash
echo "AWS_ACCESS_KEY_ID=$ACCESS_KEY_ID" >> "$RUNBOOK_OUTPUT"
echo "sensitive:AWS_SECRET_ACCESS_KEY=$SECRET_ACCESS_KEY" >> "$RUNBOOK_OUTPUT"
echo "sensitive:AWS_SESSION_TOKEN=$SESSION_TOKEN" >> "$RUNBOOK_OUTPUT"
```
The prefix isn't part of the key. The second output above is named `AWS_SECRET_ACCESS_KEY`, so later blocks read it as `{{ .outputs.<block_id>.AWS_SECRET_ACCESS_KEY }}`. Auth blocks that take credentials from a block, such as `' }]} />`, find it by its usual name too.
Runbooks hands a sensitive output's real value only to what uses it:
- A Command or Check script that references it in a template runs with the real value.
- A [``](/authoring/blocks/template/), or a [``](/authoring/blocks/templateinline/) with `generateFile`, writes the real value into the file it generates.
- An auth block's `detectCredentials` block source reads the real value.
- The copy button on the output's row in **View Outputs** copies the real value.
- The [test CLI](/authoring/testing/)'s `output_equals` and `output_matches` assertions compare the real value.
Everywhere Runbooks only shows the value, it masks it:
- **View Outputs** shows `••••••••`, and **Copy JSON** writes `"<redacted>"`.
- The script view of a Command or Check that references the output shows `<redacted>` in its place, though the script runs with the real value.
- A `` without `generateFile` shows `<redacted>` in its preview.
- A block prop that references the output, such as a title, a description, a clone URL or a pull request's title and body, gets `<redacted>`.
- The test CLI prints `<redacted>` in its `--verbose` output, in the rendered output of a `` and in the message of a failed `output_equals` or `output_matches` assertion.
An empty sensitive output reveals nothing, so wherever `<redacted>` would stand in for it, it stays empty. An `{{ if }}` on it then takes the same branch in the script view as when the script runs.
`<redacted>` only stands in for the value, so a template that processes the value can't process `<redacted>`. For example, the script view of `{{ (fromJson .outputs.<block_id>.credentials).user }}` shows a render error that explains this, though the script runs with the real value. A `` preview of it shows the same error.
[Instruction mode](/intro/instruction-mode/) treats a sensitive output like one that hasn't been produced yet, even if its step already ran. The command shows a `<name>` placeholder, with a field to paste the value into.
Auth blocks mark their own secrets the same way, including a credential they read from a block that marked it `sensitive:`. [``](/authoring/blocks/awsauth/) publishes `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` as sensitive outputs. [``](/authoring/blocks/gitauth/), [``](/authoring/blocks/githubauth/) and [``](/authoring/blocks/gitlabauth/) publish their token as one when their outputs include it, which they do for a personal access token or a `{ block }` source. So `{{ .outputs.<auth_block_id>.AWS_SECRET_ACCESS_KEY }}` is masked like any other sensitive output.
Once any line marks a key `sensitive:`, that output stays sensitive, even if a later line writes the same key without the prefix. The prefix is lowercase and comes first on the line.
> **Caution:** Runbooks can't mask what your scripts and templates produce from the real value:
- A block's log shows everything its script prints, so don't `echo` a sensitive value, in the script that produces it or in one that uses it.
- A file that a `` or a `` with `generateFile` writes holds the real value, and the block's preview and the generated files show it.
To use a credential in later blocks without writing it anywhere, pass it through an auth block such as [``](/authoring/blocks/awsauth/). For a credential no auth block handles, `export` it in the producing script, and later Command and Check scripts read it from the environment (see [Shell Execution Context](/security/shell-execution-context/)).
### Consuming outputs
Later blocks reference outputs through the `outputs` namespace:
```
{{ .outputs.<block-id>.<output-name> }}
```
For example, a Command whose `id` is `create-account` that outputs `account_id` is referenced as:
```
{{ .outputs.create_account.account_id }}
```
This syntax works in `` and `` scripts, `` files, and `` content.
> **Note:** Block IDs can use hyphens or underscores (`id="create-account"` or `id="create_account"`). In template syntax, use underscores, because Go templates read a hyphen as subtraction. IDs that normalize to the same value, such as `create-account` and `create_account`, cannot coexist in one runbook.
### Iterating over complex outputs
Use `fromJson` to parse JSON output values, then `range` to iterate:
```
{{- range (fromJson .outputs.list_users.users) }}
- {{ . }}
{{- end }}
```
### Output dependencies
When a block references outputs from a block that hasn't run yet, a warning names the blocks that need to run first, the Run or Generate button is disabled, and the template shows the raw `{{ }}` syntax.
A Command or Check block's button is also disabled while the upstream block is running again. See [Run order](/authoring/blocks/command/#run-order).
After a block runs, **View Outputs** below the log viewer lists its outputs in a table. [Sensitive outputs](#sensitive-outputs) are masked there.
> **Note:** Block outputs live in memory and are cleared when the runbook reloads.
### Outputs a block emits only sometimes
Some outputs exist only some of the time. `` publishes `org_id` and `repo_id` only when it can look the repository up on GitHub, and a script can write an output only when it has something to report. Templates treat a missing key as an error, so a block that reads such an output waits until the output exists, which may be never.
To read an output that may be missing, check for it with `hasKey` and read it only inside that branch:
```
{{ if hasKey .outputs.clone_repo "org_id" }}
org_id = "{{ .outputs.clone_repo.org_id }}"
{{ end }}
```
When every reference to an output sits inside such a branch, the block waits for the upstream block to run, but not for that output. The branch is skipped when the output is missing. If the upstream block later withdraws its outputs, for example when a `` is reset or a `` fails, the block waits again.
A guard covers only the branch it opens. A reference anywhere else makes the block wait for the output again, whether it comes after `{{ end }}`, in an `{{ else }}` branch, or in another file of the same template. These forms count as a guard:
- `{{ if hasKey .outputs.<block_id> "<output>" }}`, or `$.outputs` in place of `.outputs` inside `range` or `with`
- `{{ if and (hasKey .outputs.<block_id> "<output>") ... }}`, which also guards the arguments of `and` that follow it
- The `{{ else }}` branch of `{{ if not (hasKey .outputs.<block_id> "<output>") }}`
Other forms aren't recognized, such as a guard on a variable (`{{ if hasKey $repo "org_id" }}`), so the block waits for the output.
> **Caution:** - **The upstream block has to publish at least one output.** A `` that succeeded without writing any outputs looks the same as one that failed, so a block that reads its outputs only behind guards keeps waiting.
- **Guards work only where the template engine renders.** That covers `` and `` scripts, `` files and `` content. Props the app fills in itself can't evaluate `if`, so every output they reference is required: the `prefilled*` props of `` and ``, and the `outputPath` of ``.
### Combining inputs and outputs
Inputs and block outputs can appear in the same template:
```hcl
# From an Inputs block (via inputsId)
environment = "{{ .inputs.Environment }}"
# From a Command block's outputs
account_id = "{{ .outputs.create_account.account_id }}"
```
## Example
The [block-outputs feature demo](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/block-outputs) passes inputs and outputs between Command, Check, Template, and TemplateInline blocks.
To run it directly:
```bash
runbooks open https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/block-outputs
```
---
### Opening Runbooks
See the [open command reference](/commands/open/) for the full list of flags and supported URL formats.
## Ways to open a runbook
1. Double-click a `.mdx` file. The desktop app registers the `.mdx` file association on macOS and Windows.
2. Paste a local path or remote URL into **Open from URL** in the desktop app.
3. Use the command line:
```bash
# Local
runbooks open ./runbooks/deploy-rds
# Remote (GitHub URL copied from the browser)
runbooks open https://github.com/org/repo/tree/main/runbooks/deploy-rds
# Remote (git source in go-getter syntax, as in a Terragrunt source)
runbooks open github.com/org/repo//runbooks/deploy-rds?ref=v1.0
```
For remote sources, Runbooks clones the repository that holds the runbook and opens the runbook in the desktop app. The whole repository is cloned, so a runbook can use files outside its own directory.
---
### Boilerplate Templates
[Gruntwork Boilerplate](https://github.com/gruntwork-io/boilerplate) generates files and folders from templates. Runbooks takes two things from it: the `boilerplate.yml` schema that `` and `` build their forms from, and the Go template syntax that ``, ``, ``, and `` render.
This page covers the parts of Boilerplate that runbook authors use. The [Boilerplate repo](https://github.com/gruntwork-io/boilerplate) has the complete documentation.
## Boilerplate in a nutshell
Boilerplate is a template engine, similar to [Jinja](https://jinja.palletsprojects.com/en/stable/), [Nunjucks](https://mozilla.github.io/nunjucks/), or [Cookiecutter](https://cookiecutter.readthedocs.io/en/stable/).
### Why Boilerplate
Boilerplate is built for DevOps and infrastructure work:
- As a CLI, it prompts for the variables defined in `boilerplate.yml`, or takes them from command-line options so it can run in automated tests.
- Templates are Go templates, with conditionals, loops, and helpers for loading another file, running a shell command, and including partial templates.
- A template can depend on other templates and include them conditionally on variable values.
- Variables are typed and can have validations.
- Hooks run arbitrary scripts.
- It is a standalone binary for macOS, Linux, and Windows.
### Quick example
Say you want to generate a README for new projects. Create a template folder:
- my-template/
- boilerplate.yml
- README.md
`boilerplate.yml` defines the variables:
```yaml
variables:
- name: ProjectName
type: string
description: Name of the project
- name: Author
type: string
description: Who is the author?
default: Anonymous
```
`README.md` is the template file:
```markdown
# {{ .ProjectName }}
Created by {{ .Author }}.
```
Run Boilerplate on the command line:
```bash
boilerplate \
--template-url ./my-template \
--output-folder ./output \
--var ProjectName="My Cool App" \
--var Author="Jane Doe"
```
The result is `output/README.md`:
```markdown
# My Cool App
Created by Jane Doe.
```
## The `boilerplate.yml` file
Every template directory needs a `boilerplate.yml` file that defines its variables. Runbooks renders a form from it.
### Basic structure
```yaml
variables:
- name: ProjectName
type: string
description: What would you like to call your project?
default: my-project
validations:
- required
- name: Environment
type: enum
description: Which environment is this for?
options:
- dev
- staging
- prod
default: dev
```
This generates a form with a text input for `ProjectName` and a dropdown for `Environment`.
### Variable properties
Each variable supports these properties:
| Property | Required | Description |
|----------|----------|-------------|
| `name` | Yes | Variable name (used in templates as `.Name`) |
| `type` | No | Data type: `string`, `int`, `float`, `bool`, `enum`, `list`, `map` (defaults to `string`) |
| `description` | No | Help text shown in the form |
| `default` | No | Default value |
| `options` | For `enum` | List of allowed values for dropdowns |
| `validations` | No | [Validation rules](#validations) |
| `sensitive` | No | For a `string`, masks the field like a password input, with a button to show the value |
### Defaults that use other variables
A `default` can use other variables, with the same [template syntax](#template-syntax) as your template files:
```yaml
variables:
- name: ProjectName
type: string
default: acme
- name: Environment
type: enum
options: [dev, prod]
default: dev
- name: BucketName
type: string
default: "{{ .ProjectName }}-state"
- name: InstanceSize
type: string
default: '{{ if eq .Environment "prod" }}large{{ else }}small{{ end }}'
- name: CatalogRepositories
type: list
default:
- github.com/gruntwork-io/terraform-aws-service-catalog
- "github.com/{{ .ProjectName }}/modules"
- name: DefaultTags
type: map
default:
"{{ .ProjectName }}:Team": DevOps
```
Runbooks fills these in, using whatever the other fields hold at that moment, in two places: when it renders the body of a `` or `` block, and when a `` or `` script reads the variable through `inputsId`.
Other block props that use the variable through `{{ .inputs.X }}`, like ``'s `prefilledUrl` or ``'s `outputPath`, get the expression as written, not the value it resolves to. Give those props a variable with a plain default.
The form doesn't show the `{{ }}` syntax. A linked default shows the value it comes to, with a link icon, and follows the fields it uses as you edit them. Here `BucketName` shows `acme-state`, and `InstanceSize` shows `small` until you pick `prod`. In a ``, a default can also use the values the block gets through `inputsId`.
When the form can't work out the value yet, it shows a token for each variable the default uses instead, named the way that variable's field is labelled: a **Project Name** token followed by `-state`. A default that does more than insert a variable shows one token that says what it's based on, like **Based on Environment**. If the default uses a block output that hasn't been produced yet, its token is yellow; hover over it to see which block has to run first. If no block on the page has the id the default names, the value can never be filled in: the token is red, the field shows an error, the block is red and won't generate until you replace the value, and the error is counted in the runbook's error summary.
A sensitive field, and any default built from a sensitive variable or output, always shows tokens, never the value.
In a text field:
- Hover over the value to see its expression.
- Click it to edit the expression. When you leave the field, a value that is still an expression is shown as linked again.
- Click the **X** to clear it and type your own value.
The [template-defaults demo runbook](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/template-defaults) uses each kind.
## Variable types
### `string`
Text input field.
```yaml
- name: BucketName
type: string
description: Name for the S3 bucket
default: my-bucket
```
### `int`
Numeric input field.
```yaml
- name: InstanceCount
type: int
description: Number of instances to deploy
default: 3
```
### `float`
Numeric input field that accepts decimals.
```yaml
- name: ScalingThreshold
type: float
description: CPU utilization that triggers scaling
default: 0.75
```
### `bool`
Checkbox toggle. Without a `default`, the checkbox starts unchecked, which is the value `false`.
```yaml
- name: EnableLogging
type: bool
description: Enable CloudWatch logging?
default: true
```
### `enum`
Dropdown select from predefined options. Without a `default`, the dropdown starts empty and the variable has no value until the user picks one. With the `required` validation, the form asks for a pick before it can be submitted. Boilerplate itself needs a value for every variable without a default, so give an optional enum a `default` (`""` works if `""` is one of the options).
```yaml
- name: Region
type: enum
description: AWS region for deployment
options:
- us-east-1
- us-west-2
- eu-west-1
default: us-east-1
```
### `list`
List of values. Users can add and remove items.
```yaml
- name: AllowedIPs
type: list
description: IP addresses to allow
default: []
```
### `map`
Key-value pairs. Users can add and remove entries.
```yaml
- name: Tags
type: map
description: Resource tags
default: {}
```
For structured values, see [`x-schema`](#x-schema).
## Validations
Add validation rules to a variable:
```yaml
- name: ProjectName
type: string
validations:
- required
- name: ContactEmail
type: string
validations:
- email
- name: WebsiteURL
type: string
validations:
- url
- name: Identifier
type: string
validations:
- alphanumeric
- name: CountryCode
type: string
validations:
- countrycode2
- name: Version
type: string
validations:
- semver
- name: ManyValidations
type: string
validations:
- "required"
- "email"
```
### Custom error messages
Use the object form with `type` and `message`:
```yaml
variables:
- name: Email
type: string
validations:
- type: required
message: Email is required
- type: email
message: Must be a valid email address
```
### Validation types
| Validation | Description |
|------------|-------------|
| `required` | Field cannot be empty |
| `email` | Must be a valid email address |
| `url` | Must be a valid URL |
| `alpha` | Only letters allowed (no numbers or special characters) |
| `digit` | Only digits allowed (0-9) |
| `alphanumeric` | Only letters and numbers allowed |
| `countrycode2` | Must be a valid two-letter country code (ISO 3166-1 alpha-2) |
| `semver` | Must be a valid semantic version (e.g., `1.0.0`, `2.1.3-beta`) |
| `length(min, max)` | Value must be between `min` and `max` characters long |
| `regex("pattern")` | Value must match the given regular expression pattern |
#### Parameterized validations
```yaml
- name: ProjectSlug
type: string
validations:
- regex("^[a-z][a-z0-9-]+$")
- name: Description
type: string
validations:
- length(0, 256)
```
## Template syntax
Template files use Go template syntax. Reference a variable from `boilerplate.yml` with dot notation. In a runbook, the same variable is also available as `{{ .inputs.BucketName }}`, as [Inputs and outputs](/authoring/inputs-and-outputs/#template-syntax) describes.
### Variable substitution
```hcl
resource "aws_s3_bucket" "main" {
bucket = "{{ .BucketName }}-{{ .Environment }}"
}
```
### Conditionals
```hcl
{{- if .EnableLogging }}
resource "aws_cloudwatch_log_group" "main" {
name = "/app/{{ .ProjectName }}"
}
{{- end }}
```
### Comparisons
```hcl
instance_type = "{{ if eq .Environment "prod" }}t3.large{{ else }}t3.micro{{ end }}"
```
### Loops
```hcl
{{- range .AllowedIPs }}
- {{ . }}
{{- end }}
```
### Maps
```hcl
tags = {
{{- range $key, $value := .Tags }}
{{ $key }} = "{{ $value }}"
{{- end }}
}
```
### Whitespace control
Use `-` to trim whitespace around template directives:
- `{{-` trims whitespace before
- `-}}` trims whitespace after
```hcl
{{- if .Description }}
description = "{{ .Description }}"
{{- end }}
```
## Helper functions
Boilerplate includes helper functions for common transformations:
```yaml
# String case transformations
{{ .ProjectName | snakeCase }} # my_project
{{ .ProjectName | camelCase }} # myProject
{{ .ProjectName | pascalCase }} # MyProject
{{ .ProjectName | kebabCase }} # my-project
{{ .ProjectName | upper }} # MY-PROJECT
{{ .ProjectName | lower }} # my-project
# String checks
{{ hasPrefix "prod" .Environment }} # true if starts with "prod"
{{ hasSuffix "-dev" .Name }} # true if ends with "-dev"
```
## Dynamic file names
File and folder names can use template syntax:
- templates/
- boilerplate.yml
- `{{ .ProjectName }}.tf`
- `{{ .Environment }}/`
- config.yaml
With `ProjectName: "vpc"` and `Environment: "prod"`, this generates:
- vpc.tf
- prod/
- config.yaml
## Runbooks extensions
Runbooks reads extra YAML properties, prefixed with `x-`, that change how the form renders. The Boilerplate CLI ignores them.
### `x-section`
`x-section` groups fields under a named heading. This form renders two sections, Basic Settings and Advanced Settings:
```yaml
variables:
- name: FunctionName
type: string
x-section: Basic Settings
- name: Runtime
type: enum
options: [python3.12, nodejs20.x]
x-section: Basic Settings
- name: MemorySize
type: int
default: 128
x-section: Advanced Settings
- name: Timeout
type: int
default: 30
x-section: Advanced Settings
```
Variables without `x-section` appear in an unnamed section at the top.
### `x-options`
Lists the allowed values for a `list` variable. The form then renders a multi-select picker. The Boilerplate CLI doesn't check the values against the list.
```yaml
- name: Regions
type: list
description: Regions to deploy to
x-options:
- us-east-1
- us-west-2
- eu-west-1
```
### `x-schema`
A `map` variable collects key-value pairs where each value is a string. When each key needs several values, such as an email address, account ID, and name for each AWS account, give the map an `x-schema`. Runbooks then renders a structured form for each entry:
```yaml
- name: AWSAccounts
type: map
description: AWS account configuration
x-schema:
email: string
name: string
id: string
```
Each map entry renders with those three fields.
### `x-schema-instance-label`
Sets the label for each entry of a schema-based map:
```yaml
- name: AWSAccounts
type: map
description: AWS account configuration
x-schema-instance-label: AWS Account Name
x-schema:
email: string
environment: string
id: string
```
## Authoring templates with the CLI
You can run the Boilerplate CLI directly to test templates during development.
### Install Boilerplate
```bash
# macOS
brew install gruntwork-io/tap/boilerplate
# Or download from GitHub releases
# https://github.com/gruntwork-io/boilerplate/releases
```
### Generate files
```bash
boilerplate \
--template-url ./templates/vpc \
--output-folder ./output \
--var VpcName="my-vpc" \
--var Environment="dev"
```
### Interactive mode
Without `--var` flags, Boilerplate prompts for values interactively:
```bash
boilerplate \
--template-url ./templates/vpc \
--output-folder ./output
```
### Non-interactive mode
Use `--non-interactive` with a vars file for CI/CD:
```bash
boilerplate \
--template-url ./templates/vpc \
--output-folder ./output \
--var-file vars.yml \
--non-interactive
```
## Example template
A template directory for a Lambda function:
- templates/
- lambda/
- boilerplate.yml
- main.tf
- variables.tf
- outputs.tf
`boilerplate.yml`:
```yaml
variables:
- name: FunctionName
type: string
description: Name for the Lambda function
validations:
- required
x-section: Basic Settings
- name: Runtime
type: enum
description: Lambda runtime
options:
- python3.12
- nodejs20.x
default: python3.12
x-section: Basic Settings
- name: MemorySize
type: int
description: Memory in MB (128-10240)
default: 128
x-section: Advanced Settings
- name: EnableLogging
type: bool
description: Create CloudWatch log group?
default: true
x-section: Advanced Settings
- name: Tags
type: map
description: Resource tags
default: {}
x-section: Advanced Settings
```
`main.tf`:
```hcl
resource "aws_lambda_function" "main" {
function_name = "{{ .FunctionName }}"
runtime = "{{ .Runtime }}"
memory_size = {{ .MemorySize }}
handler = "index.handler"
role = aws_iam_role.lambda.arn
tags = {
{{- range $key, $value := .Tags }}
{{ $key }} = "{{ $value }}"
{{- end }}
}
}
{{- if .EnableLogging }}
resource "aws_cloudwatch_log_group" "lambda" {
name = "/aws/lambda/{{ .FunctionName }}"
retention_in_days = 14
}
{{- end }}
```
## Using templates in runbooks
Reference a template directory from a runbook with ``:
```mdx
# Deploy a Lambda Function
Configure your Lambda function below:
After generating, review the files in the file tree on the right.
```
For a template that doesn't need its own directory, pair `` with ``:
`````mdx
```yaml
variables:
- name: BucketName
type: string
```
```hcl
resource "aws_s3_bucket" "main" {
bucket = "{{ .BucketName }}"
}
```
`````
---
### Testing Runbooks
A runbook test runs the runbook's blocks from the command line and checks their results. You describe the test cases in a `runbook_test.yml` file next to `runbook.mdx`, then run `runbooks-cli test /path/to/runbook`, or `runbooks-cli test ./...` for every runbook under the current directory. Tests run locally or in CI.
The test runner is a standalone CLI in the [Runbooks repository](https://github.com/gruntwork-io/runbooks). From a checkout, `just compile-test-cli` builds it into a single binary at `resources/bin/runbooks-test`, which runs without Node.js or Bun. You can also run it from source with `bun cli/index.ts test`. The examples below use `runbooks-cli` as shorthand for either invocation.
## Quick start
1. Create a `runbook_test.yml` file next to your `runbook.mdx`. This minimal configuration runs every block in document order and expects each one to succeed:
```yaml
version: 1
tests:
- name: happy-path
```
Add inputs, steps, and assertions as you need them. For a fuller example to start from, see [`testdata/sample-runbooks/my-first-runbook/runbook_test.yml`](https://github.com/gruntwork-io/runbooks/blob/main/testdata/sample-runbooks/my-first-runbook/runbook_test.yml) in the Runbooks repository.
2. Run the tests:
```bash
runbooks-cli test ./my-runbook
```
## Test configuration
A fuller `runbook_test.yml`:
```yaml
version: 1
settings:
# use_temp_working_dir: true # Default: use isolated temp directory
# working_dir: . # Alternative: use runbook's directory
# output_path: generated # Where to write generated files (relative to working_dir)
timeout: 5m # Limit for each script and git command
parallelizable: true # Can run in parallel with other runbooks
tests:
- name: happy-path
description: Standard successful execution
inputs:
project.Name: "test-project"
project.Language: "python"
steps:
- block: check-requirements
expect: success
- block: create-project
expect: success
assertions:
- type: file_exists
path: README.md
- type: file_contains
path: README.md
contains: "test-project"
cleanup:
- command: rm -rf /tmp/test-resources
```
### Top-level structure
```yaml
version: 1 # Required. Must be 1.
settings: { ... } # Optional. Global settings for all tests.
tests: [ ... ] # Required. Array of test cases (minimum 1).
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `version` | integer | Yes | Configuration version. Must be `1`. |
| `settings` | object | No | Global settings that apply to all tests. |
| `tests` | array | Yes | Array of runbook tests to run. At least one test case is required. |
### Settings object
```yaml
settings:
use_temp_working_dir: true # boolean, default: true
working_dir: "." # string, default: current directory
output_path: "generated" # string, default: "generated"
timeout: "5m" # duration string, default: "5m"
parallelizable: true # boolean, default: true
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `use_temp_working_dir` | boolean | `true` | Run each test case in its own fresh temporary directory, which is removed when that test case ends. Overrides `working_dir` when `true`. |
| `working_dir` | string | current directory | Base directory for script execution and file generation. Use `.` for the runbook's directory. Any other relative path resolves against the runbook's directory. Ignored when `use_temp_working_dir` is `true`. All test cases share this directory, so files one test case generates or clones are still there for the next. |
| `output_path` | string | `"generated"` | Directory where generated files are written, relative to the working directory. |
| `timeout` | string | `"5m"` | Time limit for each script a block runs and each git command a `` runs, as a duration such as `500ms`, `30s`, `5m`, or `1h`. There is no limit on the test case as a whole. A `script` assertion is limited to this value or 30 seconds, whichever is lower, and each cleanup action to 30 seconds. |
| `parallelizable` | boolean | `true` | Whether this runbook's tests can run in parallel with tests from other runbooks. Runbooks are currently tested one at a time, so this only affects order: parallelizable runbooks run first. |
### Test case object
```yaml
tests:
- name: "test-name" # string, required
description: "..." # string, optional
env: { ... } # map[string]string, optional
inputs: { ... } # map[string]InputValue, optional
steps: [ ... ] # array of Step objects, optional
assertions: [ ... ] # array of Assertion objects, optional
cleanup: [ ... ] # array of CleanupAction objects, optional
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Unique identifier for the test case. |
| `description` | string | No | Human-readable description of what the test validates. |
| `env` | map | No | Environment variables to set for all blocks, `script` assertions and cleanup actions in this test. Keys and values are strings. |
| `inputs` | map | No | Variable values for the test. Keys use `inputsID.varName` format. Values can be literals or fuzz configs. |
| `steps` | array | No | Blocks to execute, in the order listed. A block can be listed more than once, and blocks that aren't listed don't run. A step whose `block` matches no block in the runbook fails the test. If empty, every block runs once in document order and is expected to succeed, except [pull request blocks](#testing-pull-request-blocks), which are expected to skip. |
| `assertions` | array | No | Validations to run after all steps complete. |
| `cleanup` | array | No | Actions to run after the test's blocks and assertions, even if one of them fails. |
#### Test case environment
Each test case starts from the environment the `test` command was started with. Within a test case, a bash block that succeeds or warns passes the variables it exports and the directory it `cd`s into on to the blocks after it, as in the app, and an auth block adds its credentials. The test case's `env` is applied on top of all of that. Blocks, `script` assertions, and cleanup actions all see this environment.
None of it carries over into the next test case. With `use_temp_working_dir`, each test case also gets its own empty working directory.
### Step object
```yaml
steps:
- block: "block-id" # string, required
expect: success # ExpectedStatus, default: "success"
env_prefix: "PREFIX_" # string, optional
missing_outputs: ["..."] # array of strings, optional
error_contains: "..." # string, optional
assertions: [ ... ] # array of Assertion objects, optional
set_outputs: { ... } # map of strings, optional (Iframe blocks only)
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `block` | string | Yes | ID of the block to execute. |
| `expect` | string | No | Expected execution status. Default: `success`. See [Expected status values](#expected-status-values). |
| `env_prefix` | string | No | Prefix for the environment variables an auth block reads its credentials from. See [Testing AwsAuth blocks](#testing-awsauth-blocks). |
| `missing_outputs` | array | No | Expected missing outputs (used with `blocked` status to verify why a block is blocked). Write each one as templates reference it, with hyphens in the block ID turned into underscores: `outputs.create_account.account_id` for block `create-account`. Don't list an output the block reads only behind a [`hasKey` guard](/authoring/inputs-and-outputs/#outputs-a-block-emits-only-sometimes): once its block has run, the app doesn't wait for it, but the test would still report the block as blocked. |
| `error_contains` | string | No | Expected error message substring (used with `config_error` status). |
| `assertions` | array | No | Assertions to run immediately after this step completes. |
| `set_outputs` | map | No | For an `` block, the outputs its page would set. Each name must be in the block's `outputs` prop, and each value must be a string. See [Testing Iframe blocks](#testing-iframe-blocks). |
#### Expected status values
| Status | Description |
|--------|-------------|
| `success` | Block completed successfully (exit code 0). |
| `fail` | Block failed (any exit code other than 0 or 2). |
| `warn` | Block completed with warning (exit code 2). |
| `blocked` | Block is blocked: an auth block it depends on hasn't run or was skipped, a block its `dependsOn` names hasn't finished with a success or a warning, or the outputs listed in `missing_outputs` don't exist yet. The block doesn't run. |
| `skip` | Don't run this block, for example an `` in a test that has no AWS credentials. [Pull request blocks](#testing-pull-request-blocks) can only be tested with `skip` or `blocked`. |
| `config_error` | Block has a configuration error, such as a missing prop. |
A script that runs past the `timeout` setting gets the status `timeout`. No `expect` value matches it, so the step fails.
#### Block IDs
Each step references a block by the value of its `id` prop. For example, `` is `block: verify-install`.
### Assertion object
All assertions have a `type` field that determines which other fields are required.
```yaml
assertions:
- type: "assertion_type" # AssertionType, required
# Additional fields depend on the assertion type
```
| Assertion type | Required fields | Optional fields | Description |
|----------------|-----------------|-----------------|-------------|
| `file_exists` | `path` | none | Check that a file exists at the given path. |
| `file_not_exists` | `path` | none | Check that a file does not exist. |
| `file_contains` | `path`, `contains` | none | Check that a file contains a substring. |
| `file_not_contains` | `path`, `contains` | none | Check that a file does not contain a substring. |
| `file_matches` | `path`, `pattern` | none | Check that file content matches a regex pattern. |
| `file_equals` | `path`, `value` | none | Check that file content exactly equals a value. |
| `dir_exists` | `path` | none | Check that a directory exists. |
| `dir_not_exists` | `path` | none | Check that a directory does not exist. |
| `output_equals` | `block`, `output`, `value` | none | Check that a block output equals a value. |
| `output_matches` | `block`, `output`, `pattern` | none | Check that a block output matches a regex pattern. |
| `output_exists` | `block`, `output` | none | Check that a block output exists (is not empty). |
| `files_generated` | `block` | `min_count` | Check that a block generated files. |
| `script` | `command` | none | Run a shell command. Exit code 0 passes. |
`files_generated` counts only the files the named block wrote during the test case: the files a `` rendered, the file a `` with `generateFile` wrote, or the files a `` or `` wrote to `$GENERATED_FILES`. Templates with `target="worktree"` count too, though they write into the cloned repository. Files that other blocks or earlier test cases left in the output directory don't count.
#### Assertion fields
| Field | Type | Description |
|-------|------|-------------|
| `type` | string | The assertion type. Required for all assertions. |
| `path` | string | File or directory path, relative to the output directory (`<working_dir>/<output_path>`, which is `generated/` in the working directory by default). An absolute path is used as is. |
| `contains` | string | Substring to search for in file content. |
| `pattern` | string | Regular expression pattern. |
| `value` | string | Expected exact value. |
| `block` | string | Block ID for output assertions or `files_generated`. |
| `output` | string | Output name to check. |
| `min_count` | integer | Minimum number of files generated (for `files_generated`). Default: 1. |
| `command` | string | Shell command for a `script` assertion. It runs in bash from the output directory. |
### Cleanup action object
```yaml
cleanup:
- command: "rm -rf /tmp/test" # string, optional (inline command)
path: "cleanup/teardown.sh" # string, optional (script file path)
```
| Field | Type | Description |
|-------|------|-------------|
| `command` | string | Inline shell command to execute. |
| `path` | string | Path to a script file to execute, relative to the runbook's directory. |
When both are set, `command` runs and `path` is ignored.
### Input values
An input value is a literal or a fuzz configuration.
#### Literal values
```yaml
inputs:
project.Name: "my-project" # string literal
config.Port: 8080 # integer literal
config.Enabled: true # boolean literal
```
#### Fuzz values
A fuzz value is generated at random within the given constraints, anew on each test run.
```yaml
inputs:
project.Name:
fuzz:
type: string # FuzzType, required
# Additional fields depend on the fuzz type
```
#### Variables a test doesn't set
A test only needs to set the variables it cares about. Every other variable of an `` or `` block gets the value the app's form starts it with, so a field a user can leave untouched in the app can be left out of the test too:
- Its `default`, if it has one.
- Otherwise, the value its field shows before anyone touches it: `false` for a `bool` (an unchecked checkbox), and for a `list` with an `x-schema` (a fixed row of fields), one element per schema key, `false` for a `bool` element and `""` for any other.
- Any other variable without a default stays unset, as it does in the form, so set it in the test if a block uses it.
When a `` imports a block with `inputsId` and both define a variable, the app shows that variable read-only with the imported block's value, so the test gives it that value too. For example, setting `config.Environment` also sets `vpc.Environment` for ``. With several `inputsId`s, the last one that has a value wins.
A value the test sets always wins over one filled in this way.
### Fuzz configuration
The `fuzz` object specifies how to generate random values.
| Field | Type | Applies to | Description |
|-------|------|------------|-------------|
| `type` | string | All | Required. One of the [fuzz types](#fuzz-types). |
| `length` | integer | `string` | Exact length of the generated string, not counting `prefix` and `suffix`. Overrides `minLength`/`maxLength`. `0` gives an empty string. |
| `minLength` | integer | `string`, `list`, `map` | Minimum length of the string, of each `list` item, or of each `map` value (without `schema`). Default: 8 for strings, 5 for list items and map values, or `maxLength` when only a smaller `maxLength` is set. |
| `maxLength` | integer | `string`, `list`, `map` | Maximum length. Must be at least `minLength`. Default: `minLength + 10` for strings, `minLength + 7` for list items and map values. |
| `prefix` | string | `string` | Prefix to prepend to the generated string. |
| `suffix` | string | `string` | Suffix to append to the generated string. |
| `includeSpaces` | boolean | `string` | Include space characters. Default: `false`. |
| `includeSpecialChars` | boolean | `string` | Include special characters. Default: `false`. |
| `min` | number | `int`, `float` | Minimum numeric value. Default: 0, or `max - 100` when only a zero or negative `max` is set. |
| `max` | number | `int`, `float` | Maximum numeric value. Must be at least `min`; equal to `min` for a fixed value. Default: `min + 100`. For `int`, fractional bounds narrow to the integers between them, and the test case fails if there are none. |
| `options` | array | `enum` | Array of valid enum options. Required for `enum` type. |
| `domain` | string | `email`, `url` | Domain to use. Default: random from `example.com`, `test.org`, etc. |
| `minDate` | string | `date`, `timestamp` | Minimum date (supports RFC3339, YYYY-MM-DD). Default: 365 days ago. |
| `maxDate` | string | `date`, `timestamp` | Maximum date. Default: now. |
| `format` | string | `date`, `timestamp` | Output format as a Go reference layout, evaluated in UTC. Supported tokens: `2006` (year), `01` (month), `02` (day), `15` (hour), `04` (minute), `05` (second), and the zones `Z07:00` and `Z0700` (written as `Z`), `-07:00` (`+00:00`), `-0700` (`+0000`) and `MST` (`UTC`). Other text is copied as is. Default: `2006-01-02` for date, RFC3339 for timestamp. |
| `wordCount` | integer | `words` | Exact number of words. Overrides `minWordCount`/`maxWordCount`. `0` gives an empty string. |
| `minWordCount` | integer | `words` | Minimum word count. Default: 2, or `maxWordCount` when only a smaller `maxWordCount` is set. |
| `maxWordCount` | integer | `words` | Maximum word count. Must be at least `minWordCount`. Default: `minWordCount + 3`. |
| `count` | integer | `list`, `map` | Exact number of items/entries. Overrides `minCount`/`maxCount`. `0` gives an empty list or map. |
| `minCount` | integer | `list`, `map` | Minimum items/entries. Default: 2, or `maxCount` when only a smaller `maxCount` is set. |
| `maxCount` | integer | `list`, `map` | Maximum items/entries. Must be at least `minCount`. Default: `minCount + 3` for lists, `minCount + 2` for maps. |
| `schema` | array | `map` | Field names for nested map values (generates `map[string]map[string]string`). |
A maximum below its minimum fails the test case with an error such as `fuzz string: maxLength (3) is less than minLength (5)`. Setting a minimum and maximum to the same value gives exactly that value. A field left empty (`max:` or `max: ~`) counts as unset.
### Fuzz types
| Type | Output | Description |
|------|--------|-------------|
| `string` | `string` | Alphanumeric string with optional spaces and special characters. |
| `int` | `integer` | Random integer within min/max range. |
| `float` | `float` | Random float within min/max range. |
| `bool` | `boolean` | Random `true` or `false`. |
| `enum` | `string` | Random selection from `options` array. |
| `email` | `string` | Valid email address (e.g., `abc123@example.com`). |
| `url` | `string` | Valid URL (e.g., `https://example.com/path`). |
| `uuid` | `string` | UUID v4 (e.g., `550e8400-e29b-41d4-a716-446655440000`). |
| `date` | `string` | Date string. Default format: `YYYY-MM-DD`. |
| `timestamp` | `string` | Timestamp string. Default format: RFC3339. |
| `words` | `string` | Space-separated random words. |
| `list` | `string` | JSON array of strings (e.g., `["abc", "def"]`). |
| `map` | `string` or `map` | JSON object or nested map depending on `schema`. |
### Complete example
```yaml
version: 1
settings:
use_temp_working_dir: true
output_path: generated
timeout: 10m
parallelizable: true
tests:
- name: happy-path
description: Standard successful execution with all features
env:
LOG_LEVEL: debug
RUNBOOK_DRY_RUN: "false"
inputs:
# Literal values
project.Name: "test-project"
config.Port: 8080
# Fuzz values
project.Description:
fuzz: { type: words, minWordCount: 3, maxWordCount: 6 }
user.Email:
fuzz: { type: email, domain: "test.example.com" }
resource.ID:
fuzz: { type: uuid }
config.Region:
fuzz: { type: enum, options: ["us-east-1", "us-west-2", "eu-west-1"] }
steps:
- block: validate-inputs
expect: success
- block: create-resources
expect: success
assertions:
- type: output_exists
block: create-resources
output: resource_id
- block: generate-config
expect: success
assertions:
- type: file_exists
path: config.json
- type: file_contains
path: config.json
contains: "test-project"
- type: file_matches
path: config.json
pattern: '"version":\s*"\d+\.\d+\.\d+"'
- type: output_matches
block: create-resources
output: resource_id
pattern: "^[a-f0-9]{12}$"
cleanup:
- command: rm -rf /tmp/test-resources
- name: missing-dependency
description: Test that blocks are properly blocked
steps:
- block: create-resources
expect: blocked
missing_outputs:
- outputs.validate_inputs.validated
- block: validate-inputs
expect: success
- block: create-resources
expect: success
```
## Cleanup
Cleanup actions run after the test's blocks and assertions, even if a block or assertion fails:
```yaml
cleanup:
# Inline command
- command: rm -rf /tmp/test-resources
# Script file
- path: cleanup/teardown.sh
```
Both kinds of action run in bash from the output directory (`<working_dir>/<output_path>`), which is created first if no block has generated files yet. A `path` script is read relative to the runbook's directory. They see the [test case environment](#test-case-environment), and each is limited to 30 seconds. A failing cleanup action doesn't fail the test and doesn't stop the remaining actions, but it is printed as a warning.
A test that fails before any block runs skips cleanup, since nothing has run yet. That happens when the runbook has a block type the runner doesn't know, when the test's inputs don't validate, and when a step names a block the runbook doesn't have.
## Testing AwsAuth blocks
For an `` block, the test runner looks for AWS credentials in environment variables. It never contacts AWS. It checks these sources in order:
1. `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, with `AWS_SESSION_TOKEN` and `AWS_REGION` if they are set
2. `AWS_PROFILE`
3. `AWS_ROLE_ARN` together with `AWS_WEB_IDENTITY_TOKEN_FILE`
Static keys go into the test case environment, so every later block sees them. A profile, or a role and token file, is passed only to blocks that reference the auth block with `awsAuthId`. If none of the three is set, the step is skipped and blocks that reference the auth block are blocked.
### Using a test prefix
To keep test credentials apart from your local AWS credentials, set `env_prefix` on the test step:
```yaml
# runbook_test.yml
tests:
- name: happy-path
steps:
- block: aws-auth
env_prefix: RUNBOOKS_TEST_
expect: success
```
The runner then reads:
- `RUNBOOKS_TEST_AWS_ACCESS_KEY_ID`
- `RUNBOOKS_TEST_AWS_SECRET_ACCESS_KEY`
- `RUNBOOKS_TEST_AWS_SESSION_TOKEN`
- `RUNBOOKS_TEST_AWS_REGION`
If the prefixed key pair isn't set, it falls back to the unprefixed variables. The prefix applies only to these four. `AWS_PROFILE`, `AWS_ROLE_ARN`, and `AWS_WEB_IDENTITY_TOKEN_FILE` are read without it.
### CI example
In GitHub Actions:
```yaml
# .github/workflows/test.yml
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-west-2
- name: Run runbook tests
run: runbooks-cli test ./runbooks/...
env:
RUNBOOKS_TEST_AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
RUNBOOKS_TEST_AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
RUNBOOKS_TEST_AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }}
RUNBOOKS_TEST_AWS_REGION: ${{ env.AWS_REGION }}
```
## Testing GoogleAuth blocks
For a `` block, the test runner looks for Google Cloud credentials in environment variables and adds them to the test case environment. As with AwsAuth, it never contacts Google, so a test shows that the block is wired up and says nothing about whether the credential is valid.
It checks these credential variables in order:
- `GOOGLE_APPLICATION_CREDENTIALS` (path to a credentials JSON file, either a service account key or an `authorized_user` document)
- `GOOGLE_CREDENTIALS` (inline credentials JSON)
- `GOOGLE_OAUTH_ACCESS_TOKEN`
- `CLOUDSDK_AUTH_ACCESS_TOKEN`
If none is set, it falls back to gcloud's `application_default_credentials.json`, the block's `'adc'` detection source. If that is missing too, the step is skipped and every block that references it with `googleAuthId` is blocked.
It also reads these variables, none of which counts as a credential:
- Project: `CLOUDSDK_CORE_PROJECT`, `GOOGLE_CLOUD_PROJECT`, `GOOGLE_PROJECT`, `GCLOUD_PROJECT`
- Region: `CLOUDSDK_COMPUTE_REGION`, `GOOGLE_CLOUD_REGION`
- Zone: `CLOUDSDK_COMPUTE_ZONE`
When none of those is set, the block's own `project`, `defaultRegion`, and `defaultZone` props are used.
On success the runner writes the same variables the app's session environment writes, so scripts see every name that gcloud, the client libraries, and the OpenTofu `google` provider each look for: `GOOGLE_CLOUD_PROJECT` / `CLOUDSDK_CORE_PROJECT` / `GOOGLE_PROJECT`, `GOOGLE_CLOUD_REGION` / `CLOUDSDK_COMPUTE_REGION` / `GOOGLE_REGION`, and `CLOUDSDK_COMPUTE_ZONE` / `GOOGLE_ZONE`. A credentials file also sets `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE`, so `gcloud` uses that file and ignores any account from `gcloud auth login`. An access token clears it.
### Using a test prefix
As with AwsAuth, set `env_prefix` on the test step to keep test credentials apart from your local ones:
```yaml
# runbook_test.yml
tests:
- name: happy-path
steps:
- block: google-auth
env_prefix: RUNBOOKS_TEST_
expect: success
```
The runner then checks:
- `RUNBOOKS_TEST_GOOGLE_APPLICATION_CREDENTIALS`
- `RUNBOOKS_TEST_GOOGLE_CREDENTIALS`
- `RUNBOOKS_TEST_GOOGLE_OAUTH_ACCESS_TOKEN`
- `RUNBOOKS_TEST_CLOUDSDK_AUTH_ACCESS_TOKEN`
- `RUNBOOKS_TEST_CLOUDSDK_CORE_PROJECT` (and the other project/region/zone names above, prefixed)
If no prefixed credential is found, it falls back to the unprefixed variables.
### CI example
```yaml
# .github/workflows/test.yml
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/123/locations/global/workloadIdentityPools/ci/providers/github
service_account: runbooks-ci@my-project.iam.gserviceaccount.com
- name: Run runbook tests
run: runbooks-cli test ./runbooks/...
env:
RUNBOOKS_TEST_GOOGLE_APPLICATION_CREDENTIALS: ${{ steps.auth.outputs.credentials_file_path }}
RUNBOOKS_TEST_CLOUDSDK_CORE_PROJECT: my-project
RUNBOOKS_TEST_CLOUDSDK_COMPUTE_REGION: us-central1
```
## Testing GitHubAuth blocks
For a `` block, the test runner looks for a GitHub token in these environment variables, in order:
1. `RUNBOOKS_GITHUB_TOKEN` (recommended for tests)
2. `GITHUB_TOKEN`
3. `GH_TOKEN`
### Using a test prefix
With `env_prefix` on the step, the runner reads the prefixed names:
```yaml
# runbook_test.yml
tests:
- name: happy-path
steps:
- block: gh-auth
env_prefix: CI_
expect: success
```
This step reads `CI_GITHUB_TOKEN` and `CI_GH_TOKEN`.
### CI example
```yaml
# .github/workflows/test.yml
- name: Run runbook tests
run: runbooks-cli test ./runbooks/...
env:
RUNBOOKS_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
## Testing GitLabAuth and GitAuth blocks
For a `` block, the test runner looks for a GitLab token in these environment variables, in order:
1. `GITLAB_TOKEN`
2. `GITLAB_ACCESS_TOKEN`
3. `OAUTH_TOKEN`
With `env_prefix` on the step, it checks the prefixed names instead (`CI_GITLAB_TOKEN`, `CI_GITLAB_ACCESS_TOKEN`, `CI_OAUTH_TOKEN`).
On success, the blocks after it see `GITLAB_TOKEN` and `GITLAB_HOST`. As in the app, a token from the environment belongs to one GitLab host: the one `GITLAB_HOST` (or `GITLAB_URI`, `GL_HOST`) names, or `gitlab.com` if none is set. A block that pins another host with `instanceUrl` or `host` skips unless `GITLAB_HOST` names that host too, and a block whose pinned value isn't a valid URL or host always skips.
The test runner never sends the token anywhere, so a fake one is enough to test that the block authenticates:
```yaml
# runbook_test.yml
tests:
- name: detects-env-token
env:
GITLAB_TOKEN: fake-token
steps:
- block: gitlab-auth
expect: success
```
A `` block is tested like `` or ``, depending on its `provider` prop (`github` by default).
### Cloning with a token
A `` that references an auth block with `gitAuthId` or `githubAuthId` clones an `https` URL with that block's token when the URL is on the host the block authenticated to. That includes GitHub Enterprise Server and self-managed GitLab hosts. The username is `oauth2` for GitLab and `x-access-token` for GitHub.
Without a reference, the runner recognizes `github.com`, `*.ghe.com`, and `gitlab.com` URLs and uses that provider's token from the [test case environment](#test-case-environment): `GITHUB_TOKEN` or `GH_TOKEN` for GitHub, `GITLAB_TOKEN` for GitLab. A token already set where the tests run counts too, so to clone without one, blank it in the test case's `env`:
```yaml
tests:
- name: anonymous-clone
env:
GITHUB_TOKEN: ""
GH_TOKEN: ""
```
## Testing pull request blocks
``, ``, and `` blocks never run in tests, since they would push a branch and open a real pull request. A step that names one must use `expect: skip`, or `expect: blocked` when the auth block it references hasn't run or was skipped. A test with no `steps` expects them to skip.
```yaml
# runbook_test.yml
tests:
- name: skip-without-credentials
steps:
- block: gh-auth
expect: skip
- block: create-pr
expect: skip
```
## Testing Iframe blocks
Tests can't run the page an [``](/authoring/blocks/iframe/) block shows, so a step for the block stands in for the page: its `set_outputs` become the block's outputs, as if the page had set them. Only an `` with an `id` can be a step.
```yaml
# runbook_test.yml
tests:
- name: deploy-to-picked-region
steps:
- block: region-picker
set_outputs:
region: eu-west-1
- block: deploy
expect: success
```
A step whose `set_outputs` names an output the block's `outputs` prop doesn't list fails, just as the app refuses that output from the page. Without `set_outputs`, the step succeeds and sets nothing, so blocks that read the block's outputs stay blocked. A test with no `steps` runs each `` with an `id` that way.
## Running tests
The runner executes each test case's blocks in a temporary directory by default, captures script output and generated files, and checks them against the test's assertions. For a failed test it prints which block failed and why.
It exits with status 0 if every test passed. It exits with 1 if any test failed, a `runbook_test.yml` couldn't be loaded, or no runbook has the test case named with `--test`.
### Single runbook
```bash
# Run all tests for a runbook
runbooks-cli test ./my-runbook
# Run a specific test case
runbooks-cli test ./my-runbook --test happy-path
# Verbose output
runbooks-cli test ./my-runbook -v
```
### Multiple runbooks
End the path with `/...` to test every runbook under a directory:
```bash
# Test all runbooks in a directory tree
runbooks-cli test ./runbooks/...
```
Runbooks are tested one at a time. The `--max-parallel` flag is accepted but currently has no effect.
### CI output
To write a JUnit XML report:
```bash
runbooks-cli test ./runbooks/... --output junit --output-file results.xml
```
The file is written before the command exits, including when tests fail. If it can't be written, the CLI prints the error and writes the report to stdout instead.
## Out-of-order testing
To run blocks in a different order than the runbook defines them, list them under `steps`. Steps run in the order listed, and a block listed more than once runs once per step, each time with that step's expectation.
A block added to the runbook later is not tested until you add it to `steps`. Leave `steps` empty where you can, so new blocks are covered without a change to the test.
```yaml
tests:
- name: out-of-order
description: Test dependency enforcement
steps:
# This should be blocked because create-account hasn't run
- block: create-resources
expect: blocked
missing_outputs:
- outputs.create_account.account_id
# Run the dependency
- block: create-account
expect: success
# Now this should succeed
- block: create-resources
expect: success
```
## Example workflow
These jobs run in your own repository, which has no copy of the test runner. So each one checks out the Runbooks repository into `.runbooks`, builds the runner there, and then runs it against the runbooks in your repository's `runbooks` directory. Point the runner at that directory rather than `./...`, which would also find the test runbooks in `.runbooks`.
#### GitHub Actions
```yaml
# .github/workflows/test-runbooks.yml
name: Test Runbooks
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
# The test runner is built from the Runbooks repository.
# Set `ref` to a release tag to pin the version.
- uses: actions/checkout@v4
with:
repository: gruntwork-io/runbooks
path: .runbooks
- uses: jdx/mise-action@v2
with:
working_directory: .runbooks
- name: Build the test runner
working-directory: .runbooks
run: |
bun install --frozen-lockfile
just compile-test-cli
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- name: Run runbook tests
run: .runbooks/resources/bin/runbooks-test test ./runbooks/... --output junit --output-file results.xml
env:
RUNBOOKS_TEST_AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
RUNBOOKS_TEST_AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
RUNBOOKS_TEST_AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }}
RUNBOOKS_TEST_AWS_REGION: ${{ vars.AWS_REGION }}
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: test-results
path: results.xml
```
#### GitLab CI
```yaml
# .gitlab-ci.yml
test-runbooks:
stage: test
variables:
RUNBOOKS_TEST_AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID
RUNBOOKS_TEST_AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY
RUNBOOKS_TEST_AWS_REGION: $AWS_REGION
before_script:
# The test runner is built from the Runbooks repository.
# Add `--branch <release tag>` to pin the version.
- git clone --depth 1 https://github.com/gruntwork-io/runbooks.git .runbooks
- curl -fsSL https://mise.jdx.dev/install.sh | sh
- export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH"
- cd .runbooks
- mise install
- bun install --frozen-lockfile
- just compile-test-cli
- cd "$CI_PROJECT_DIR"
script:
- .runbooks/resources/bin/runbooks-test test ./runbooks/... --output junit --output-file results.xml
artifacts:
reports:
junit: results.xml
```
## Testing with external dependencies
For runbooks that call external services such as GitHub or AWS, test in tiers. Each tier covers more than the one before it and is slower and harder to set up, as in the [test pyramid](https://martinfowler.com/articles/practical-test-pyramid.html).
### Tier 1: template validation
Tier 1 checks that templates generate files and makes no external calls.
```yaml
tests:
- name: template-validation
description: Validate template generation
inputs:
config.ProjectName: "test-project"
config.Region: "us-east-1"
steps:
- block: generate-config # Template block
expect: success
- block: deploy-resources # Skip external calls
expect: skip
assertions:
- type: files_generated
block: generate-config
min_count: 1
```
### Tier 2: dry-run mode
Tier 2 runs scripts with their external calls switched off. The script checks an environment variable, and the test sets it. `RUNBOOK_DRY_RUN` is a convention, and any variable name works.
In the Command or Check script:
```bash
#!/bin/bash
set -e
# Dry-run support
DRY_RUN="${RUNBOOK_DRY_RUN:-false}"
if [[ "$DRY_RUN" == "true" ]]; then
echo "[DRY-RUN] Would create PR in $GITHUB_ORG/$REPO_NAME"
echo "[DRY-RUN] gh pr create --title '$TITLE'"
exit 0
fi
# Real execution
gh pr create --title "$TITLE" --body "$BODY"
```
In the test configuration:
```yaml
tests:
- name: dry-run-flow
description: Test full flow without real API calls
env:
RUNBOOK_DRY_RUN: "true"
steps:
- block: generate-config
expect: success
- block: create-pr
expect: success # Succeeds in dry-run mode
```
### Tier 3: integration tests
Tier 3 runs against real services. Set credentials as environment variables in CI, and run these tests manually or on a schedule:
```yaml
tests:
- name: integration-test
description: Full integration (requires credentials)
env:
# Credentials set in the CI environment are available to the test,
# so they don't need to be set here.
steps:
- block: deploy-resources
expect: success
cleanup:
- path: scripts/cleanup-test-resources.sh
```
## Best practices
- Start with one test case and no `steps`, so every block runs in document order. Add inputs, steps, and assertions from there.
- Cover the failure paths too: `expect: fail` for a script that should exit non-zero, `expect: blocked` with `missing_outputs` for a block whose dependency hasn't run, and `expect: config_error` with `error_contains` for a misconfigured block.
---
### Blocks
#### Overview
Blocks are the interactive components of a `runbook.mdx` file. Write them as JSX tags within your markdown.
## Available blocks
- [Admonition](/authoring/blocks/admonition)
- [AwsAuth](/authoring/blocks/awsauth)
- [Check](/authoring/blocks/check)
- [Command](/authoring/blocks/command)
- [DirPicker](/authoring/blocks/dirpicker)
- [GitAuth](/authoring/blocks/gitauth)
- [GitClone](/authoring/blocks/gitclone)
- [GitHubAuth](/authoring/blocks/githubauth)
- [GitHubPullRequest](/authoring/blocks/githubpullrequest)
- [GitLabAuth](/authoring/blocks/gitlabauth)
- [GitLabMergeRequest](/authoring/blocks/gitlabmergerequest)
- [GitPullRequest](/authoring/blocks/gitpullrequest)
- [GoogleAuth](/authoring/blocks/googleauth)
- [Iframe](/authoring/blocks/iframe)
- [Inputs](/authoring/blocks/inputs)
- [Template](/authoring/blocks/template)
- [TemplateInline](/authoring/blocks/templateinline)
## Block IDs
Interactive blocks take an `id` prop, a unique identifier for the block. When one block references another, it does so by the other block's ID:
- `inputsId` uses the values collected by an `` or `` block. See [Wiring blocks with `inputsId`](/authoring/inputs-and-outputs/#wiring-blocks-with-inputsid).
- `awsAuthId`, `googleAuthId`, `gitAuthId` and `githubAuthId` choose which auth block's credentials a block uses.
- `gitCloneId` roots a `` in the repository a `` block cloned.
- `{{ .outputs.<block_id>.<output_name> }}` reads the outputs of a block that has already run. In template syntax, write any hyphens in the ID as underscores. See [Block outputs](/authoring/inputs-and-outputs/#block-outputs).
Each ID must be unique within a runbook. IDs that differ only in hyphens versus underscores (such as `create-account` and `create_account`) count as the same ID.
Runbook tests use IDs too: each step in `runbook_test.yml` names the block it runs by its ID. See [Testing](/authoring/testing/#block-ids).
In Runbooks, most blocks show a small **ID** badge in their top-right corner. `` doesn't, and neither does an `` block nested inside another block. Hover over the badge (or Tab to it) to see the block's ID, and click it to copy the ID to your clipboard.
---
#### <Admonition>
The `` block renders a callout box for a note, tip, warning, or danger message.
## Basic usage
```mdx
```
## Props
### Required props
- `type` (string): `"note"`, `"info"`, `"warning"`, or `"danger"`
### Optional props
- `title` (string): title for the callout box. Defaults based on type. Supports inline markdown (bold, italic, links, code).
- `description` (string): the message to display. Supports inline markdown.
- `inputsId` (string | string[]): ID of one or more [Inputs](/authoring/blocks/inputs/) blocks. `title`, `description`, and `confirmationText` resolve `{{ .inputs.VarName }}` expressions against those inputs, so a callout can name the value the user entered.
- `closable` (boolean): whether users can close the admonition. Default `false`.
- `confirmationText` (string): if set, shows a confirmation button with this label that users must click to dismiss the admonition.
- `allowPermanentHide` (boolean): with `confirmationText`, adds a "Don't show me this again" checkbox.
- `storageKey` (string): unique key for localStorage. Required with `allowPermanentHide`.
## Types
### Note (gray)
```mdx
```
### Info (blue)
```mdx
```
### Warning (yellow)
```mdx
```
### Danger (red)
```mdx
```
## Inline content
You can pass the content as children in place of the `description` prop:
```mdx
Before proceeding, you need:
- AWS CLI installed
- OpenTofu v1.6+
- Valid AWS credentials configured
```
The content supports inline markdown:
```mdx
Review the [deployment guide](https://example.com/guide) before running these commands. **Do not** run this in production without testing first.
```
## Closable admonitions
```mdx
```
## Confirmation button
Require users to acknowledge before dismissing:
```mdx
```
## Don't show again
Let users hide the admonition permanently:
```mdx
```
---
#### <AwsAuth>
The `` block authenticates the user to AWS with static credentials, AWS SSO (IAM Identity Center), or a local AWS profile. Once authenticated, the credentials are available to later [Command](/authoring/blocks/command/) and [Check](/authoring/blocks/check/) blocks.
By default, AwsAuth detects credentials in environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and so on), validates them, and asks the user to confirm the account before using them.
## Basic usage
```mdx
```
## Authentication methods
| Method | Description |
|--------|-------------|
| **Static Credentials** | Enter Access Key ID and Secret Access Key directly |
| **AWS SSO** | Use AWS IAM Identity Center (formerly AWS SSO) |
| **Local Profile** | Use a profile from `~/.aws/credentials` |
Static Credentials is the tab users land on. Set `defaultTab` to open on a different one:
```mdx
```
The user can still switch tabs. `defaultTab` only chooses the starting one.
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `id` | string | required | Unique identifier for this component |
| `inputsId` | string or string[] | - | ID of one or more [Inputs](/authoring/blocks/inputs/) blocks. `title` and `description` resolve `{{ .inputs.VarName }}` expressions against them |
| `title` | string | "AWS Authentication" | Display title shown in the UI |
| `description` | string | - | Description of the authentication purpose |
| `defaultRegion` | string | "us-east-1" | Default AWS region for CLI commands. Sets `AWS_REGION` environment variable. GovCloud regions (`us-gov-east-1`, `us-gov-west-1`) are supported. When the credentials don't name a region themselves, this region also decides which partition (commercial or GovCloud) they are validated in |
| `defaultTab` | `'credentials'`, `'sso'`, or `'profile'` | `'credentials'` | Which authentication tab the block opens on. An unrecognized value falls back to `'credentials'` |
| `detectCredentials` | `false` or `CredentialSource[]` | `['env']` | Whether and how to detect existing credentials. See [Credential detection](#credential-detection) |
| `ssoStartUrl` | string | - | AWS SSO start URL (for example `https://my-company.awsapps.com/start`). Required for SSO |
| `ssoRegion` | string | "us-east-1" | AWS region where IAM Identity Center is configured. Every SSO call goes to this region. For a GovCloud Identity Center this is `us-gov-east-1` or `us-gov-west-1` |
| `ssoAccountId` | string | - | Pre-select a specific AWS account after SSO authentication. Takes effect only together with `ssoRoleName` |
| `ssoRoleName` | string | - | Pre-select a specific IAM role to assume. Takes effect only together with `ssoAccountId` |
### AWS partitions
Credentials are validated in the partition their region belongs to: the commercial partition, GovCloud (US), China, the European Sovereign Cloud, or an isolated (ISO) partition. The region picker lists commercial and GovCloud regions. A region in any other partition works when it comes from the environment's credentials, a profile's `region`, or a block's `AWS_REGION` output, and is validated against that partition's own STS endpoint.
## Environment variables
When authentication succeeds, these environment variables are set for later Command and Check blocks:
| Variable | Description |
|----------|-------------|
| `AWS_ACCESS_KEY_ID` | The AWS access key |
| `AWS_SECRET_ACCESS_KEY` | The AWS secret key |
| `AWS_SESSION_TOKEN` | Session token (for temporary credentials from SSO or assume role) |
| `AWS_REGION` | The selected default region |
They are set in the session environment, so later blocks get them without setting `awsAuthId`.
## Using with Commands and Checks
A Command or Check block that doesn't set `awsAuthId` uses the session environment, which has the credentials of the most recently authenticated AwsAuth block. Each new sign-in replaces them.
Clicking **Re-authenticate** on the success card withdraws the block's credentials, so blocks that reference it with `awsAuthId` (below) report it as not authenticated until the user signs in again. The session environment variables keep their previous values until the next sign-in replaces them.
To make a Command or Check block use one specific AwsAuth block, regardless of which signed in last, set `awsAuthId`:
```mdx
```
## Pre-selecting the SSO account and role
After the user approves the sign-in in their browser, AwsAuth lists the AWS accounts they can access and asks them to pick an account and then a role. If they can access exactly one account with exactly one role, that role is used without asking.
To skip the account and role selection, set both `ssoAccountId` and `ssoRoleName`. Setting only one has no effect.
```mdx
```
## Multiple AWS accounts
A runbook can have several AwsAuth blocks, one per AWS account:
```mdx
```
> **Caution:** With several AwsAuth blocks, the session environment variables hold the most recently authenticated credentials. Set `awsAuthId` on each command so it runs against the account you intend.
## Local profile support
The Local Profile tab shows profiles from your `~/.aws/credentials` and `~/.aws/config` files, or from the files that `AWS_SHARED_CREDENTIALS_FILE` and `AWS_CONFIG_FILE` point to. The files are read the way the AWS CLI reads them: `[profile staging]` in config and `[staging]` in credentials are one profile, so keys in one file and a region in the other work together. Two profile types are supported:
| Profile type | Description |
|--------------|-------------|
| **Static Credentials** | Profiles with `aws_access_key_id` and `aws_secret_access_key` |
| **Assume Role** | Profiles with a `role_arn` and either a `source_profile` or a `credential_source` |
Other profile types, such as `credential_process` profiles, are not listed.
**Refresh profiles** re-reads both files. The selected profile stays selected as long as it is still listed.
A profile's own `region` is the one its credentials are validated in and the one set as `AWS_REGION`. A profile that sets no region uses the region selected in the block instead, so pick a `us-gov-*` region for a GovCloud profile without one.
> **Note:** SSO profiles (those with `sso_session` or `sso_start_url`) are not shown in the Local Profile tab. Use the AWS SSO tab instead to authenticate with SSO.
## Credential detection
By default, AwsAuth detects existing credentials in environment variables. It shows the user the AWS account ID, the account name if available, and the identity (for example an IAM role). The user must confirm before the credentials are used.
> **Note:** GitHubAuth uses detected credentials without asking. AwsAuth waits for the user to confirm an account, and until then no credentials are available to later blocks. See [Credential protection](#credential-protection).
### Default behavior
With no configuration, AwsAuth checks for credentials in standard AWS environment variables:
```mdx
{/* Same as detectCredentials={['env']} */}
```
If credentials are found and valid, the confirmation prompt shows:
- AWS Account ID
- AWS Account Name (if available)
- IAM ARN (identity)
- Default Region
- Whether credentials are temporary or static
The region comes from `AWS_REGION`, then `AWS_DEFAULT_REGION`, then the block's `defaultRegion`. It is the value set as `AWS_REGION` once the user confirms, and it decides the partition the credentials are validated in, so GovCloud keys need a `us-gov-*` region. If none of the three is set, detection reports an error instead of assuming a region.
The user can then choose to "Use These Credentials" or "Use Different Credentials".
When the user clicks "Use These Credentials", AwsAuth reads and validates the environment's credentials again before setting them in the session. If they now belong to a different AWS account than the prompt showed, the prompt switches to that account and the user has to confirm again.
### Credential sources
| Source | Description |
|--------|-------------|
| `'env'` | Check standard AWS env vars (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and so on) |
| `{ env: { prefix: 'PREFIX_' } }` | Check prefixed env vars (`PREFIX_AWS_ACCESS_KEY_ID` and so on) |
| `{ block: 'block-id' }` | Use credentials from a Command block's output |
### Disabling detection
To show only the manual authentication tabs:
```mdx
```
### Custom detection with prefix
A prefixed source checks for the standard AWS environment variables under a custom prefix. This configuration checks for:
- `PROD_AWS_ACCESS_KEY_ID`
- `PROD_AWS_SECRET_ACCESS_KEY`
- `PROD_AWS_SESSION_TOKEN`
- `PROD_AWS_REGION` (or `PROD_AWS_DEFAULT_REGION`)
```mdx
```
A prefixed source reads only the prefixed variables. It never falls back to the unprefixed `AWS_*` credentials, so a `PROD_` block can't pick up your development keys by accident. When you confirm, the credentials are set in the session under the standard names (`AWS_ACCESS_KEY_ID`, `AWS_REGION`, and so on).
Prefixes must follow these rules:
- Uppercase letters, numbers, and underscores only
- Must start with a letter
- Must end with an underscore, as in `PROD_` or `MY_APP_`
### Test prefix
To use a different prefix in automated tests, for example to avoid conflicts with local credentials, set `env_prefix` on the step in `runbook_test.yml`:
```yaml
# runbook_test.yml
steps:
- block: aws-auth
env_prefix: CI_ # Checks CI_AWS_ACCESS_KEY_ID, CI_AWS_SECRET_ACCESS_KEY, and so on
expect: success
```
See [Testing AwsAuth blocks](/authoring/testing/#testing-awsauth-blocks).
### From Command output
Use credentials generated by a previous Command block:
```mdx
```
The Command script should output credentials in standard format:
```bash
#!/usr/bin/env bash
CREDS=$(aws sts assume-role --role-arn "$ROLE_ARN" --role-session-name runbook)
echo "AWS_ACCESS_KEY_ID=$(jq -r '.Credentials.AccessKeyId' <<< "$CREDS")" >> "$RUNBOOK_OUTPUT"
echo "sensitive:AWS_SECRET_ACCESS_KEY=$(jq -r '.Credentials.SecretAccessKey' <<< "$CREDS")" >> "$RUNBOOK_OUTPUT"
echo "sensitive:AWS_SESSION_TOKEN=$(jq -r '.Credentials.SessionToken' <<< "$CREDS")" >> "$RUNBOOK_OUTPUT"
echo "AWS_REGION=us-west-2" >> "$RUNBOOK_OUTPUT" # Optional: override region
```
The `sensitive:` prefix masks the secret key and session token in the Command's **View Outputs**. AwsAuth still reads them as `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN`. See [Sensitive outputs](/authoring/inputs-and-outputs/#sensitive-outputs).
The `AWS_REGION` output is optional. If not provided, the AwsAuth block's `defaultRegion` prop is used.
When the user clicks "Use These Credentials", AwsAuth validates the Command's current outputs again and registers the result. If the Command was re-run in the meantime and its credentials now belong to a different AWS account, the prompt switches to that account and the user has to confirm again. If the credentials no longer validate, for example because temporary credentials have expired, the block shows the error and registers nothing.
### When users reject detected credentials
Detection is read-only. It validates the credentials and shows the user which account they belong to, but does not register them to the session, so scripts can't use detected credentials until the user confirms them.
When a user clicks "Use Different Credentials":
1. The manual authentication UI is shown (Static Credentials, SSO, or Local Profile tabs)
2. The user must authenticate manually before any commands can access AWS
Credentials for the wrong account, such as production when the user expected development, were not registered, so no script runs with them.
## Security
### Credential protection
When a runbook contains an `` block, Runbooks strips AWS credentials from the session at startup:
1. Before confirmation, scripts in the runbook cannot read `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` or `AWS_SESSION_TOKEN`, even if they are set in your terminal.
2. When AwsAuth checks for credentials, it reads them from your environment without registering them to the session.
3. Clicking "Use These Credentials" adds the credentials to the session, where later scripts can use them.
For example, with production credentials set in your terminal, opening a runbook meant for development does not let its scripts run against production.
> **Tip:** A runbook with no AwsAuth block leaves environment credentials in place, and all scripts can use them.
### Why confirmation is required
AWS operations often take effect immediately and can't be undone. They create resources that cost money, change production data, or delete infrastructure. Confirmation matters most when users have access to several AWS accounts, or when a tool such as aws-vault or granted has set credentials in the environment.
### Credential handling
- Authentication happens between your machine and AWS. Credentials are not sent to Gruntwork or any other server unless a script in your runbook sends them.
- Credentials are stored only in your local Runbooks session and are not written to disk.
- Credentials are validated with STS GetCallerIdentity before they are registered to the session.
## Detailed example
The [aws-auth feature demo](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/aws-auth) is a runbook that walks through the AwsAuth block.
---
#### <Check>
The `` block runs a shell command or script and reports whether the user's system is in the expected state. Use it for pre-flight checks before a Command, to validate what a Command did, or as a smoke test at the end of a runbook.
## Basic usage
```mdx
```
## vs. Command
Check and [Command](/authoring/blocks/command/) blocks share most of their features. A Check reads the state of the world and validates it. A Command changes it.
A Check has a **Check** button where a Command has **Run**. It also accepts a `warnMessage` and shows exit code 2 as a warning, where a Command shows it as a failure.
## Props
### Required props
- `id` (string): unique identifier for this check block
### Optional props
- `title` (string): display title shown in the UI. Supports inline markdown (bold, italic, links, code).
- `description` (string): longer description of what's being checked. Supports inline markdown.
- `command` (string): inline command to execute. Use either this or `path`.
- `path` (string): path to a shell script file relative to the runbook. Use either this or `command`.
- `inputsId` (string | string[]): ID of an [Inputs](/authoring/blocks/inputs/) block to get template variables from. Can be a single ID or an array of IDs. When multiple IDs are provided, variables are merged in order, and later IDs override earlier ones.
- `awsAuthId` (string): ID of an [AwsAuth](/authoring/blocks/awsauth/) block for AWS credentials. The credentials are passed as environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`). The Check button is disabled until authentication completes.
- `gitAuthId` (string): ID of a [GitAuth](/authoring/blocks/gitauth/), [GitHubAuth](/authoring/blocks/githubauth/) or [GitLabAuth](/authoring/blocks/gitlabauth/) block, for either provider. Its credentials are passed as environment variables (`GITHUB_TOKEN` and `GITHUB_USER`, or `GITLAB_TOKEN` and `GITLAB_USER`). The Check button is disabled until authentication completes.
- `githubAuthId` (string): ID of a [GitHubAuth](/authoring/blocks/githubauth/) block. GitHub only. The credentials are passed as environment variables (`GITHUB_TOKEN`, `GITHUB_USER`). The Check button is disabled until authentication completes. Prefer `gitAuthId`.
- `googleAuthId` (string): ID of a [GoogleAuth](/authoring/blocks/googleauth/) block for Google Cloud credentials. The block's credentials are passed as environment variables (`GOOGLE_APPLICATION_CREDENTIALS`, `GOOGLE_CLOUD_PROJECT`, `CLOUDSDK_CORE_PROJECT`, `GOOGLE_PROJECT`, `CLOUDSDK_CORE_ACCOUNT`, and the region/zone variables), overriding the session environment for this execution only. The Check button is disabled until authentication completes.
- `successMessage` (string): message shown when the check succeeds. Default "Success". Supports inline markdown.
- `warnMessage` (string): message shown on warning. Default "Warning". Supports inline markdown.
- `failMessage` (string): message shown when the check fails. Default "Failed". Supports inline markdown.
- `runningMessage` (string): message shown while running. Default "Checking...". Supports inline markdown.
- `timeoutMs` (number): maximum time, in milliseconds, the script may run before it is killed and the block is marked as failed. Defaults to `3600000` (60 minutes). Lower it so a hung check fails sooner, for example `timeoutMs={120000}` for a 2-minute probe, or raise it for checks that run longer.
- `dependsOn` (string | string[]): ID of a Command or Check block, or an array of IDs, that must finish with a success or a warning before this block can run. See [Run order](/authoring/blocks/command/#run-order).
- `exclusive` (boolean): whether this block runs alone. Defaults to `false`. While it runs, no other Command or Check block can start, and it can't start while another one is running. See [Run order](/authoring/blocks/command/#run-order).
### Inline content
You can nest an `` component directly inside the Check:
````mdx
```yaml
variables:
- name: BucketName
type: string
description: Name of the S3 bucket to check
validations:
- required
```
````
The embedded `` renders inside the Check block, so users fill in variables before running the check. Other blocks can reference it by `inputsId` like any standalone Inputs block.
## Writing scripts
Scripts can be defined inline with the `command` prop or stored in external files referenced by the `path` prop.
### Defining scripts
#### Inline scripts
For simple checks, define the script directly in the `command` prop:
```mdx
```
Inline scripts suit one-liners. For anything longer, use an external script file.
#### External scripts
```mdx
```
External scripts are plain bash scripts. The referenced `checks/aws-authenticated.sh` might look like:
```bash
#!/bin/bash
log_info "Checking AWS authentication..."
if aws sts get-caller-identity &>/dev/null; then
log_info "AWS credentials are valid"
exit 0
else
log_error "Not authenticated to AWS"
exit 1
fi
```
If you edit the script file while the runbook is open, the block shows a "Script changed" notice with the diff and keeps running the version it loaded. Click **Reload script** in the notice to run the new version. See [Reloading a changed script file](/security/execution-model/#reloading-a-changed-script-file).
### Exit codes
| Exit code | Result |
|-----------|--------|
| `0` | Success (green), with `successMessage` |
| `2` | Warning (yellow), with `warnMessage` |
| Anything else | Failure (red), with `failMessage` |
A script killed by `timeoutMs` is a failure.
### Logging
Check scripts get the same `log_info`, `log_warn`, `log_error` and `log_debug` functions and log files as Command scripts. See [Logging](/authoring/blocks/command/#logging) in the Command block docs, including [Log files](/authoring/blocks/command/#log-files) and [Local development](/authoring/blocks/command/#local-development).
### With variables
A Check takes variables the same way a Command does: from `inputsId`, from several `inputsId` values, or from a nested ``. Reference a value as `{{ .inputs.VariableName }}` in the command, the script, or the message props.
````mdx
```yaml
variables:
- name: AwsRegion
type: string
description: AWS region to check
default: us-east-1
```
````
See [With variables](/authoring/blocks/command/#with-variables) in the Command block docs for merging several Inputs blocks, and [How values are inserted](/authoring/blocks/command/#how-values-are-inserted) for quoting and for passing `{{ }}` through to another tool.
### Execution context
Check scripts run in the same persistent, non-interactive shell environment as Command scripts. A check that exits 0 or 2 carries its `export`, `unset` and `cd` changes forward to later blocks. See [Execution context](/authoring/blocks/command/#execution-context).
### Examples
#### Basic validation script
```bash
#!/bin/bash
# checks/tofu-installed.sh
log_info "Checking for OpenTofu installation..."
if command -v tofu &> /dev/null; then
log_info "OpenTofu is installed: $(tofu version | head -1)"
exit 0
else
log_error "OpenTofu is not installed"
exit 1
fi
```
#### Script with warning
```bash
#!/bin/bash
# checks/disk-space.sh
log_info "Checking available disk space..."
available=$(df -h / | awk 'NR==2 {print $4}' | sed 's/G//')
log_debug "Available space: ${available}GB"
if [ "$available" -lt 1 ]; then
log_error "Less than 1GB available"
exit 1
elif [ "$available" -lt 5 ]; then
log_warn "Less than 5GB available"
exit 2
else
log_info "Disk space OK: ${available}GB available"
exit 0
fi
```
#### Parameterized script
```bash
#!/bin/bash
# checks/s3-bucket-exists.sh
BUCKET_NAME="{{ .inputs.BucketName }}"
log_info "Checking if S3 bucket exists..."
log_debug "Bucket name: ${BUCKET_NAME}"
if aws s3 ls "s3://${BUCKET_NAME}" &> /dev/null; then
log_info "Bucket ${BUCKET_NAME} exists"
exit 0
else
log_error "Bucket ${BUCKET_NAME} does not exist"
exit 1
fi
```
## Viewing logs
A check's output streams into the same **View Logs** panel as a command's. See [Viewing logs](/authoring/blocks/command/#viewing-logs).
## Block outputs
A check script can write `key=value` pairs to the `$RUNBOOK_OUTPUT` file, and later blocks reference them as `{{ .outputs.<block-id>.<key> }}`:
```bash
#!/bin/bash
# Verify an account and output its ID
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
echo "account_id=$ACCOUNT_ID" >> "$RUNBOOK_OUTPUT"
```
A Check that references another block's outputs keeps its Check button disabled, and names the blocks that need to run first, until those outputs exist. The button is also disabled while the upstream block is running again. See [Run order](/authoring/blocks/command/#run-order).
See [Block outputs](/authoring/inputs-and-outputs/#block-outputs) in Inputs and outputs. The [block-outputs feature demo](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/block-outputs) includes a Check that consumes outputs from upstream Command blocks.
## Capturing output files
A check script can write to `$GENERATED_FILES` and read or modify `$REPO_FILES` the same way a command script does. Files in `$GENERATED_FILES` are captured when the check exits 0 or 2, so a check that ends in a warning still keeps its files. See [Capturing output files](/authoring/blocks/command/#capturing-output-files).
A check more often reads `$REPO_FILES` than writes to it:
```bash
#!/bin/bash
# Validate a config file in the cloned repo
if [ -z "${REPO_FILES:-}" ]; then
log_error "No git worktree available. Clone a repo first."
exit 1
fi
if [ -f "$REPO_FILES/terragrunt.hcl" ]; then
log_info "Found terragrunt.hcl"
exit 0
else
log_error "Missing terragrunt.hcl"
exit 1
fi
```
---
#### <Command>
The `` block runs a shell command or script, with user input substituted in. Use it for deployments, resource creation, and anything else that changes state.
## Basic usage
```mdx
```
## vs. Check
Command and [Check](/authoring/blocks/check/) blocks share most of their features. A Check reads the state of the world and validates it. A Command changes it.
## Props
### Required props
- `id` (string): unique identifier for this command block
### Optional props
- `title` (string): display title shown in the UI. Supports inline markdown (bold, italic, links, code).
- `description` (string): longer description of what the command does. Supports inline markdown.
- `command` (string): inline command to execute. Use either this or `path`.
- `path` (string): path to a shell script file relative to the runbook. Use either this or `command`.
- `inputsId` (string | string[]): ID of an [Inputs](/authoring/blocks/inputs/) block to get template variables from. Can be a single ID or an array of IDs. When multiple IDs are provided, variables are merged in order, and later IDs override earlier ones.
- `awsAuthId` (string): ID of an [AwsAuth](/authoring/blocks/awsauth/) block for AWS credentials. The credentials are passed as environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`). The Run button is disabled until authentication completes.
- `gitAuthId` (string): ID of a [GitAuth](/authoring/blocks/gitauth/), [GitHubAuth](/authoring/blocks/githubauth/) or [GitLabAuth](/authoring/blocks/gitlabauth/) block, for either provider. Its credentials are passed as environment variables (`GITHUB_TOKEN` and `GITHUB_USER`, or `GITLAB_TOKEN` and `GITLAB_USER`). The Run button is disabled until authentication completes.
- `githubAuthId` (string): ID of a [GitHubAuth](/authoring/blocks/githubauth/) block. GitHub only. The credentials are passed as environment variables (`GITHUB_TOKEN`, `GITHUB_USER`). The Run button is disabled until authentication completes. Prefer `gitAuthId`.
- `googleAuthId` (string): ID of a [GoogleAuth](/authoring/blocks/googleauth/) block for Google Cloud credentials. The block's credentials are passed as environment variables (`GOOGLE_APPLICATION_CREDENTIALS`, `GOOGLE_CLOUD_PROJECT`, `CLOUDSDK_CORE_PROJECT`, `GOOGLE_PROJECT`, `CLOUDSDK_CORE_ACCOUNT`, and the region/zone variables), overriding the session environment for this execution only. The Run button is disabled until authentication completes.
- `successMessage` (string): message shown when the command succeeds. Default "Success". Supports inline markdown.
- `failMessage` (string): message shown when the command fails. Default "Failed". Supports inline markdown.
- `runningMessage` (string): message shown while running. Default "Running...". Supports inline markdown.
- `timeoutMs` (number): maximum time, in milliseconds, the script may run before it is killed and the block is marked as failed. Defaults to `3600000` (60 minutes). Raise it for scripts that run longer, for example `timeoutMs={7200000}` for a 2-hour deployment, or lower it so a hung script fails sooner.
- `dependsOn` (string | string[]): ID of a Command or Check block, or an array of IDs, that must finish with a success or a warning before this block can run. See [Run order](#run-order).
- `exclusive` (boolean): whether this block runs alone. Defaults to `false`. While it runs, no other Command or Check block can start, and it can't start while another one is running. See [Run order](#run-order).
### Inline content
You can nest an `` component directly inside the Command:
````mdx
```yaml
variables:
- name: Name
type: string
description: Your name
validations:
- required
```
````
The embedded `` renders inside the Command block, so users fill in variables before running the command. Other blocks can reference it by `inputsId` like any standalone Inputs block.
## Writing scripts
Scripts can be defined inline with the `command` prop or stored in external files referenced by the `path` prop.
### Defining scripts
#### Inline scripts
For simple commands, define the script directly in the `command` prop:
```mdx
```
Inline scripts suit one-liners. For anything longer, use an external script file.
#### External scripts
```mdx
```
External scripts are plain bash scripts. The referenced `scripts/deploy.sh` might look like:
```bash
#!/bin/bash
log_info "Starting deployment..."
kubectl apply -f deployment.yaml
if kubectl rollout status deployment/myapp; then
log_info "Deployment complete!"
exit 0
else
log_error "Deployment failed"
exit 1
fi
```
If you edit the script file while the runbook is open, the block shows a "Script changed" notice with the diff and keeps running the version it loaded. Click **Reload script** in the notice to run the new version. See [Reloading a changed script file](/security/execution-model/#reloading-a-changed-script-file).
### Exit codes
Exit code 0 renders as success (green). Any other exit code renders as failure (red).
Exit code 2 is a warning for [Check](/authoring/blocks/check/#exit-codes) blocks. A Command that exits 2 is shown as failed, but Runbooks still treats the run as a warning internally. Files written to `$GENERATED_FILES` are [captured](#capturing-output-files), and the script's environment changes [carry forward](#execution-context).
### Logging
Runbooks sources a [logging.sh file](https://github.com/gruntwork-io/runbooks/blob/main/scripts/logging.sh) into every bash script, which defines a set of logging functions. Using them lets the Runbooks UI parse log levels for filtering and export.
#### Log levels
| Function | Output | Description |
|----------|--------|-------------|
| `log_info "msg"` | `[timestamp] [INFO] msg` | General informational messages |
| `log_warn "msg"` | `[timestamp] [WARN] msg` | Warning conditions |
| `log_error "msg"` | `[timestamp] [ERROR] msg` | Error messages |
| `log_debug "msg"` | `[timestamp] [DEBUG] msg` | Debug output (only when `DEBUG=true`) |
The logging functions append to a [log file](#log-files) that Runbooks creates for each run, and Runbooks shows those lines in the block's logs as your script writes them, in the order it wrote them. They never write to stdout, so you can call them inside a function whose output you capture with `$(...)`. The captured value holds only what the function prints.
```bash
get_account_id() {
log_info "Looking up the AWS account ID..."
aws sts get-caller-identity --query Account --output text
}
ACCOUNT_ID=$(get_account_id)
```
#### Usage example
```bash
#!/bin/bash
log_info "Starting deployment..."
log_debug "Target environment: $ENVIRONMENT"
if [ -z "$API_KEY" ]; then
log_warn "API_KEY not set, some features may be unavailable"
fi
if ! deploy_application; then
log_error "Deployment failed"
exit 1
fi
log_info "Deployment complete"
```
#### Log files
Each run gets a set of log files. Runbooks puts their paths in environment variables, the way `$RUNBOOK_OUTPUT` holds the path of the [outputs](#block-outputs) file:
| Variable | Written by | Level in the logs |
|----------|------------|-------------------|
| `RUNBOOK_LOG` | `log_info`, `log_warn`, `log_error`, `log_debug` | The level each line names |
| `RUNBOOK_INFO_LOG` | Your commands | `INFO` |
| `RUNBOOK_WARN_LOG` | Your commands | `WARN` |
| `RUNBOOK_ERROR_LOG` | Your commands | `ERROR` |
| `RUNBOOK_DEBUG_LOG` | Your commands | `DEBUG` |
All four logging functions append to `$RUNBOOK_LOG`, so their lines keep the order your script wrote them in, whatever their level. Runbooks shows the lines in `$RUNBOOK_LOG` as they are written, so a line you add there yourself should name its level the same way, for example `[WARN] Quota is almost used up`.
The per-level files are for output that doesn't name a level, such as a tool's stderr. Any command can append to them directly. Runbooks adds a timestamp and the file's level to each line that doesn't already start with them, so the line shows up at that level:
```bash
# tofu's errors appear as [ERROR] lines; its JSON output stays in the variable
PLAN_JSON=$(tofu show -json plan.tfplan 2>>"$RUNBOOK_ERROR_LOG")
```
Append with `>>`. Overwriting a log file with `>` can drop or repeat lines.
Scripts in other languages get the same variables. For example, in Python:
```python
import os
with open(os.environ["RUNBOOK_WARN_LOG"], "a") as log:
print("Quota is almost used up", file=log)
```
Runbooks reads the log files while your script runs, so their lines appear next to its stdout and stderr, normally in the order the script wrote them. Lines written to different log files close together can appear grouped by file, with the lines from `$RUNBOOK_LOG` first. `log_debug` only writes when `DEBUG=true`, but every line in `$RUNBOOK_DEBUG_LOG` is shown, whoever wrote it.
The files are deleted when the run ends, so lines that a [background job](#background-processes) writes after the script exits don't appear. From then on, the logging functions write to stderr instead, so the job's later log lines go wherever you redirected its output. Don't redirect a background job's output to the log files: once they are deleted, a command whose output is redirected to one of them fails without running.
#### Local development
Outside the Runbooks UI, the logging functions aren't injected. To simulate the Runbooks environment, run this one-liner in your terminal session (bash or zsh):
```bash
mkdir -p ~/.config/runbooks && curl -fsSL https://raw.githubusercontent.com/gruntwork-io/runbooks/main/scripts/logging.sh -o ~/.config/runbooks/logging.sh && export BASH_ENV=~/.config/runbooks/logging.sh
```
This sets [`BASH_ENV`](https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html), which tells bash to source the logging functions before running any script. It works from bash and zsh parent shells and lasts until the terminal is closed. The same script then works in both environments.
Outside Runbooks `RUNBOOK_LOG` isn't set, so the logging functions write to stderr instead. The one-liner overwrites `~/.config/runbooks/logging.sh`, so re-run it to pick up changes to the logging functions. Copies downloaded before the log files existed write to stdout, where the log lines end up in `$(...)` captures.
> **Fish shell:**
The one-liner above uses `export` and `&&`, which are not fish syntax. Fish also does not read `BASH_ENV` itself, but child `bash` processes will if the variable is exported. Use the fish equivalent:
```fish
mkdir -p ~/.config/runbooks; and curl -fsSL https://raw.githubusercontent.com/gruntwork-io/runbooks/main/scripts/logging.sh -o ~/.config/runbooks/logging.sh; and set -gx BASH_ENV ~/.config/runbooks/logging.sh
```
To make this persistent across sessions, add the following to a fish startup file such as `~/.config/fish/conf.d/runbooks.fish`:
```fish
if test -f ~/.config/runbooks/logging.sh
set -gx BASH_ENV ~/.config/runbooks/logging.sh
end
```
### With variables
#### Using inputsId
The command or script pulls its values from a separate Inputs block.
````mdx
```yaml
variables:
- name: OrgName
type: string
description: GitHub organization name
- name: RepoName
type: string
description: Repository name
```
````
#### Using inline inputs
The Command collects input values directly. Other blocks can read them like any standalone Inputs block.
````mdx
```yaml
variables:
- name: Name
type: string
description: Your name
validations:
- required
```
````
#### Using multiple inputsIds
Pass an array of IDs to reference several Inputs blocks. Variables are merged in order, and later IDs override earlier ones:
````mdx
```yaml
variables:
- name: GithubOrgName
type: string
description: GitHub organization name
- name: GithubRepoName
type: string
description: Repository name
```
````
If both blocks define a variable with the same name, the value from `repo-config` wins.
#### How values are inserted
Variable values are inserted into the script verbatim, with no quoting or escaping. Quote a reference where the shell needs it, for example `REGION="{{ .inputs.AwsRegion }}"`, and compare strings directly, for example `{{ if eq .inputs.Language "JavaScript" }}`.
> **Caution:**
Quoting keeps a value with spaces in one piece, but it does not make an untrusted value safe to run. Double quotes still expand `$(...)`. For values you don't control, such as another block's outputs, have the upstream block `export` an environment variable and read that instead.
If the script can't be rendered, for example because it calls an unknown function or contains a `{{ }}` expression meant for another tool, the block fails with the template error and the script does not run. To pass `{{ }}` through to a tool such as `docker ps --format`, write it as a raw string: ``--format '{{`{{.Names}}`}}'``.
> **Note:**
[Runbook tests](/authoring/testing/) use a simpler template renderer that doesn't handle `eq` comparisons or the raw-string form yet. It leaves `{{ if eq ... }}` unrendered and reports `{{.Names}}` as an undefined variable, so a script that uses either works in the app but not under test.
### Execution context
Scripts run in a persistent environment. Environment variable changes (`export`, `unset`) and working directory changes (`cd`) made by a script that exits 0 or 2 carry forward to later blocks, so earlier steps can set up the environment for later ones.
Scripts run in a non-interactive shell, so shell aliases such as `ll` and shell functions such as `nvm` or `rvm` are not available.
See [Shell execution context](/security/shell-execution-context/) for the details.
#### Run order
Command and Check blocks can run at the same time. A block's Run button is disabled while the block has to wait for another one:
- **It uses another block's outputs.** A block that references `{{ .outputs.build.tag }}` can't run until `build` has produced that output, and can't run while `build` is running again.
- **It names another block in `dependsOn`.** Use this when a script needs something an earlier script did that isn't an output, such as a variable it exported, a directory it changed into, or a resource it created. The block can't run until every block it names has finished with a success or a warning. If one of them later fails or is stopped, the block waits again.
- **It would overlap with an `exclusive` block.** No other Command or Check block can start while an `exclusive` block runs, and an `exclusive` block can't start while another one is running. Use this for a script that can't share its resources, such as one that takes a state lock.
```mdx
```
A block that is waiting names the blocks it is waiting for. Click a name to scroll to that block. For a block that is running, there is also a button to stop it.
#### Background processes
A block finishes once its script has exited and no process it started still has the script's output open. To leave a process running for later blocks, such as a port-forward or a local dev server, start it in the background with its output redirected:
```bash
nohup kubectl port-forward svc/web 8080:80 > /tmp/port-forward.log 2>&1 &
```
Without the redirect, the block keeps waiting on the background process until `timeoutMs` kills it. Stopping a running block, or letting it hit `timeoutMs`, terminates the script together with every process it started. Quitting Runbooks while a block is running sends them the same termination signal.
### Examples
#### Simple deployment script
```bash
#!/bin/bash
# scripts/deploy.sh
set -e # Exit on error
log_info "Starting deployment..."
kubectl apply -f deployment.yaml
kubectl rollout status deployment/myapp
log_info "Deployment complete!"
```
#### Parameterized script
```bash
#!/bin/bash
# scripts/create-vpc.sh
REGION="{{ .inputs.AwsRegion }}"
VPC_NAME="{{ .inputs.VpcName }}"
CIDR_BLOCK="{{ .inputs.CidrBlock }}"
log_info "Creating VPC $VPC_NAME in $REGION..."
log_debug "CIDR block: $CIDR_BLOCK"
aws ec2 create-vpc \
--cidr-block "$CIDR_BLOCK" \
--tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=$VPC_NAME}]" \
--region "$REGION"
log_info "VPC created successfully!"
```
#### Script with error handling
```bash
#!/bin/bash
# scripts/safe-deploy.sh
set -e
function cleanup {
log_info "Cleaning up..."
# Cleanup code here
}
trap cleanup EXIT
log_info "Running pre-deployment checks..."
./check-prerequisites.sh || { log_error "Pre-checks failed"; exit 1; }
log_info "Deploying..."
./deploy.sh
log_info "Running post-deployment validation..."
./validate-deployment.sh || { log_error "Validation failed"; exit 1; }
log_info "Deployment successful!"
```
## Capturing output files
Scripts can write files to two destinations:
- [Generated files](/intro/files_workspace/#generated-files)
- [Repository files](/intro/files_workspace/#repository-files)
See the [Files workspace](/intro/files_workspace/) page for more.
### Writing to generated files
Files written to `$GENERATED_FILES` appear in the file panel after the script exits 0 or 2.
#### Basic example
```bash
#!/bin/bash
# Export OpenTofu outputs to generated files
tofu output -json > "$GENERATED_FILES/tf-outputs.json"
# Copy a config file
cp config.yaml "$GENERATED_FILES/"
```
Or as an inline command:
```mdx
"$GENERATED_FILES/greeting.txt"`}
successMessage="File created successfully!"
/>
```
#### Organizing files with subdirectories
```bash
#!/bin/bash
mkdir -p "$GENERATED_FILES/opentofu"
mkdir -p "$GENERATED_FILES/config"
tofu output -json > "$GENERATED_FILES/opentofu/outputs.json"
echo '{"env": "production"}' > "$GENERATED_FILES/config/settings.json"
```
This creates:
- generated/
- opentofu/
- outputs.json
- config/
- settings.json
#### How it works
1. Before your script runs, Runbooks creates a temporary capture directory
2. The `$GENERATED_FILES` environment variable points to this directory
3. Your script writes files to `$GENERATED_FILES`
4. If the script exits 0 or 2, the files are copied to the generated files directory
5. The temporary directory is removed
> **Tip:** Files are only captured when the script exits 0 or 2. On any other exit code, files written to `$GENERATED_FILES` are discarded.
> **Note:** If multiple commands run concurrently and write files with the same name, the last command to finish overwrites the file. Use unique filenames or subdirectories to avoid conflicts.
### Writing to repository files
Once a `` block has registered a repository, the `$REPO_FILES` environment variable points to its local path. With several repositories registered, it points to the one selected in the workspace's repository dropdown, or to the most recently registered one if the user hasn't selected any. Scripts can modify files inside the repository directly:
```bash
#!/bin/bash
# Modify a file in the cloned repository
echo "new setting = true" >> "$REPO_FILES/config.hcl"
```
Writes to `$REPO_FILES` are not captured to a temporary directory. They happen directly on the filesystem, and the **Changed files** tab shows them via `git diff`.
> **Note:** `$REPO_FILES` is only set when a `` block has registered a repository. Otherwise it is unset, so check for it in your scripts:
```bash
if [ -z "${REPO_FILES:-}" ]; then
echo "No git worktree available. Clone a repo first."
exit 1
fi
```
## Viewing logs
While a command runs, its output streams into the collapsible **View Logs** panel under the block. The panel's toolbar offers:
- **Copy log file path.** Every run also writes its output to a log file on disk, which you can open or tail with other tools.
- **Copy prompt for LLM.** Copies a ready-to-paste prompt for an AI assistant. It names the block's ID, the runbook file and the log file, and ends with an instruction that matches the block's status. When the block has failed, for example, it asks the assistant to analyze the failure and suggest a fix. The prompt points at the log file rather than including the logs, so it works best with assistants that can read local files, such as Claude Code or Cursor.
- **Wrap** long lines, **copy** the logs as plain text, or **download** them as raw (`.log`) or structured (`.json`) logs.
- **Maximize** the logs to fill the screen.
Blocks that don't write a log file, such as [``](/authoring/blocks/gitclone/) and [``](/authoring/blocks/gitpullrequest/), put the last 200 lines of their logs in the prompt instead.
## Block outputs
Commands can produce outputs that later blocks consume. Scripts write `key=value` pairs to the `$RUNBOOK_OUTPUT` file, and later blocks reference them via `{{ .outputs.<block-id>.<key> }}`.
```bash
#!/bin/bash
ACCOUNT_ID=$(aws organizations create-account ...)
echo "account_id=$ACCOUNT_ID" >> "$RUNBOOK_OUTPUT"
```
After a block runs, click **View Outputs** below the log viewer to inspect its outputs. To keep a credential off screen there, write `sensitive:` before its key, as in `echo "sensitive:API_TOKEN=$TOKEN" >> "$RUNBOOK_OUTPUT"`. See [Sensitive outputs](/authoring/inputs-and-outputs/#sensitive-outputs).
For producing outputs, consuming them in templates, dependency behavior, and complex data types, see [Block outputs](/authoring/inputs-and-outputs/#block-outputs) in Inputs and outputs.
---
#### <DirPicker>
The `` block renders a cascading set of dropdowns for selecting a directory path.
For example, if the user cloned a repository with this directory structure:
- dev/
- region-1/
- region-2/
- prod/
- region-1/
- region-2/
- region-3/
The DirPicker block could be configured with these props:
```mdx
```
Users then get a form with one dropdown for each directory level.

The options in each dropdown are the immediate subdirectories of the directory selected above it. If the user selects "dev" in the first dropdown, the second offers "region-1" and "region-2".
Users can also type a path directly into the editable text input below the dropdowns, labelled "Target Path" in the screenshot above.
The selected path is registered as a block output, available to later `` blocks as `{{ .outputs.<id>.PATH }}`.
### Basic usage
DirPicker needs a root directory to browse. You can provide one in two ways.
With `rootDir`, pass an explicit directory path:
```mdx
```
With `gitCloneId`, reference a `` block. DirPicker waits for the clone to complete, then uses the cloned repository as its root directory:
```mdx
```
At least one of `rootDir` or `gitCloneId` must be provided. If both are set, `rootDir` takes precedence.
### Use cases
We built the DirPicker block for developer self-service.
Suppose a developer who doesn't know infrastructure-as-code well wants to deploy a new application to a specific AWS account. The DirPicker block gives them a guided UI for picking the account, region, and application name, and produces the right path.
More generally, the block helps users select any path when the meaning of each directory level is known in advance.
### Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `string` | Yes | Unique block identifier. Later blocks use it to reference outputs. |
| `rootDir` | `string` | No* | Absolute path to the root directory to browse. When set, the block uses this path directly. |
| `gitCloneId` | `string` | No* | ID of a `` block. DirPicker waits for the clone to complete, then uses the cloned repository as its root directory. |
| `inputsId` | `string \| string[]` | No | ID of one or more `` blocks, for `{{ .inputs.* }}` expressions in props. |
| `title` | `string` | No | Display title. Supports inline markdown. Defaults to "Select Directory". |
| `description` | `string` | No | Description text below the title. Supports inline markdown. Defaults to "Choose a target directory". |
| `dirLabels` | `string[]` | No | Ordered labels for each directory level (for example `['Account', 'Region']`). Also caps the number of dropdowns at `dirLabels.length` unless `dirLabelsExtra` is true. When omitted, depth is unbounded and every level is labelled "Level N". |
| `dirLabelsExtra` | `boolean` | No | When `true`, allow navigating deeper than `dirLabels.length`. Extra levels are labelled "Level N". Defaults to `false`. |
| `pathLabel` | `string` | No | Label for the editable path text input. Defaults to "Target Path". |
| `pathLabelDescription` | `string` | No | Description text shown below the path label. Supports inline markdown. |
\* At least one of `rootDir` or `gitCloneId` is required. If both are set, `rootDir` takes precedence.
### Outputs
| Output | Description |
|--------|-------------|
| `PATH` | The selected path, relative to the root directory, with the dropdown selections joined by `/`. Updated on every dropdown selection or manual edit. When the path is empty (the first dropdown is reset to its placeholder or the text input is cleared), `PATH` is removed, so blocks that reference it wait until a path is chosen again. The same happens when the `` block's clone path changes: the dropdowns and the path start over from the new root. |
### Directory-level labels
`dirLabels` names each cascading directory level. Each entry labels the corresponding depth of the tree: the dropdown shows a header such as "Account" and its placeholder reads "Select account...".
Without `dirLabels`, every dropdown is labelled "Level N" and the user can drill as deep as the tree goes. With `dirLabels`, the number of dropdowns is capped at the number of labels unless `dirLabelsExtra={true}`, in which case deeper levels are labelled "Level N".
### Using outputs in templates
The DirPicker outputs `PATH`, which can be referenced in a `` block's `outputPath` prop:
```mdx
```
The `outputPath` resolves `{{ .outputs.*.* }}` expressions client-side using block outputs, so the file is written to the directory the user selected. When the user picks or types a different directory, the block cleans up the file it wrote for the earlier selection. If the block created that file, it is removed. If the earlier directory already had a `terragrunt.hcl`, for example because it is an existing unit, that file is put back the way it was. A file edited since the block wrote it is left as it is.
### How it works
1. DirPicker resolves its root directory from `rootDir` (if set) or by reading the `clone_path` output of the referenced `` block once the clone completes.
2. It lists the immediate subdirectories of the root directory to fill the first dropdown.
3. Each dropdown selection triggers a fetch for the next level's subdirectories, building a cascading drill-down.
4. An editable text input below the dropdowns shows the composed path. Users can edit it directly.
5. On every change (dropdown or manual edit), the block registers `PATH` as an output. Clearing the path removes the output.
Hidden directories (those starting with `.`) are excluded from the dropdown options.
---
#### <GitAuth>
The `` block authenticates the user to GitHub or GitLab. A dropdown on the block selects the provider.
It detects whether the user is already authenticated by checking their environment variables and the provider's CLI (`gh` for GitHub, `glab` for GitLab). Users can also authenticate manually. GitHub supports OAuth and a Personal Access Token (PAT). GitLab supports a PAT. Once authenticated, the credentials are available to later [Command](/authoring/blocks/command/), [Check](/authoring/blocks/check/), and [GitClone](/authoring/blocks/gitclone/) blocks.
### Basic usage
Default to GitHub and let the user pick the provider from the dropdown:
```mdx
```
Pre-select GitLab and hide the dropdown so the runbook is GitLab-only:
```mdx
```
By default, Runbooks detects and uses credentials from these sources, in order:
1. Environment variables. For GitHub, `GITHUB_TOKEN` or `GH_TOKEN`, and `GH_ENTERPRISE_TOKEN` or `GITHUB_ENTERPRISE_TOKEN` for GitHub Enterprise Server. For GitLab, `GITLAB_TOKEN`.
2. The provider CLI. `gh auth token --hostname <host>` for GitHub, or `glab auth token` for GitLab. For GitLab, Runbooks also reads glab's `config.yml`, where `glab auth login` stores the token, when `glab auth token` returns nothing, for example when the `glab` binary is not on `PATH`.
If no credentials are detected, users authenticate manually.
To read prefixed variables such as `CI_GITHUB_TOKEN` or `CI_GITLAB_TOKEN`, add `{ env: { prefix: 'CI_' } }` to `detectCredentials`. See [``](/authoring/blocks/githubauth/#detection-sources) and [``](/authoring/blocks/gitlabauth/#prefixed-environment-variables) for the variables each provider reads.
`detectCredentials` can also list `{ block: 'block-id' }` to take the token another block wrote to its outputs (`GITHUB_TOKEN`/`GH_TOKEN` or `GITLAB_TOKEN`), or the credential another `` block authenticated. Sources are tried in the order you list them. If that block has not run yet, the block shows *Waiting for "block-id" to run...* and detection pauses there without moving to a later source. If the block ran without leaving a usable token, detection moves on to the next source.
### Disabling detection
Set `detectCredentials={false}` to have the user always sign in on the block itself:
```mdx
```
The block then never uses a token from an environment variable or from the `gh` or `glab` CLI, even when the user is already logged in with one, and it never suggests logging in with the CLI. It tells the user that the runbook doesn't use existing credentials and to sign in below, with no **Check again** button and no help on authenticating automatically. The host picker still lists the hosts Runbooks knows about, so the user can choose where to sign in, but it has no **Reload** button and doesn't mark which hosts have credentials.
### Providers
| Provider | Methods | CLI | Token env var | OAuth |
| --- | --- | --- | --- | --- |
| `github` (default) | OAuth, PAT, CLI, env | `gh` | `GITHUB_TOKEN` or `GH_TOKEN`, and `GH_ENTERPRISE_TOKEN` for GHES | Yes (enterprise hosts need `oauthClientId`) |
| `gitlab` | PAT, CLI, env | `glab` | `GITLAB_TOKEN` | No |
> **Note:** There is no registered Gruntwork GitLab app, so GitLab has no OAuth flow and authenticates via a Personal Access Token, the `glab` CLI, or the `GITLAB_TOKEN` environment variable. When GitLab reports a token's scopes, the success card lists them and warns if the token has neither `api` nor `write_repository`, since pushing branches and opening merge requests may fail.
### Hosts
Both providers support more than their public SaaS host. Set `host` to pin the block to one host and hide the host picker. With `host` unset, the block offers a picker whenever more than one host is available.
| Provider | Default host | Other hosts | Picker lists |
| --- | --- | --- | --- |
| `github` | `github.com` | GitHub Enterprise Server (such as `github.example.com`) and GHE.com data-residency tenants (such as `acme.ghe.com`) | Hosts from gh's `hosts.yml` (`gh auth login --hostname <host>`), `GH_HOST`, the session host, recently used hosts |
| `gitlab` | `gitlab.com` | Self-managed GitLab instances (such as `gitlab.example.com`) | Hosts from glab's `config.yml`, hosts named by environment variables, the session host, recently used hosts |
```mdx
```
For GitHub, the picker's initial host is the host the user last picked if a credential for it is still available, then `GH_HOST`, then `github.com`. Each environment token is bound to a single host. `GITHUB_TOKEN` and `GH_TOKEN` go to `GH_HOST` when it is `github.com` or a `*.ghe.com` tenant, and to `github.com` otherwise. `GH_ENTERPRISE_TOKEN` and `GITHUB_ENTERPRISE_TOKEN` go only to the Enterprise Server host `GH_HOST` names.
The Gruntwork OAuth app only exists on `github.com`. On an enterprise host, **Sign in with GitHub** needs `oauthClientId` set to the client ID of an OAuth app registered on that instance with device flow enabled. Without one, the block offers a personal access token and the GitHub CLI, or only the token when `detectCredentials={false}`, and explains why.
See [GitHub Enterprise support](/authoring/blocks/githubauth/#github-enterprise-support) on the `` page for the host, environment variable, API URL, and OAuth details.
### Self-managed GitLab
A GitLab block can authenticate to your own GitLab instance in three ways:
- **Host picker.** The block lists the GitLab hosts it knows about: the hosts you are logged into with glab (`glab auth login --hostname gitlab.example.com`), the host your environment names, and instances you entered before. It detects credentials for the host you pick and remembers the pick.
- **Instance URL field.** The Personal Access Token form has an editable *GitLab Instance URL* field, for example `https://gitlab.example.com`. Leave it blank for `gitlab.com`. Picking **Other instance** in the host picker takes you to it.
- **Props.** `host` pins the block to one instance and hides the host picker. `instanceUrl` pre-fills the Instance URL field. See [Props](#props).
A filled-in instance URL, whether typed or set with `instanceUrl`, takes precedence over `host` and the host picker. Picking a host in the picker clears the field.
The `GITLAB_TOKEN` environment variable is used for one instance only. That instance is the host named by `GITLAB_HOST`, `GITLAB_URI` or `GL_HOST`, checked in that order, or `gitlab.com` when none is set. Runbooks never sends the token to any other host, or over plain `http://`, so to have it detected for a self-managed instance, also set `GITLAB_HOST`. A prefixed token (`{ env: { prefix } }`) is bound by the prefixed host variables. See [``](/authoring/blocks/gitlabauth/#prefixed-environment-variables).
## Props
| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | Yes | - | Unique block ID. |
| `provider` | `'github' \| 'gitlab'` | No | `'github'` | The initially selected provider. |
| `hideProviderSelect` | `boolean` | No | `false` | When `true`, hide the provider dropdown and lock the provider to `provider`. |
| `title` | `string` | No | `"Git Authentication"` | Block title (supports template expressions). |
| `description` | `string` | No | - | Block description (supports template expressions). |
| `host` | `string` | No | - | Pins the host and hides the host picker. GitHub: `github.com`, a GitHub Enterprise Server host (`github.example.com`), or a GHE.com tenant (`acme.ghe.com`). GitLab: `gitlab.com` or a self-managed instance. See [Hosts](#hosts). |
| `oauthClientId` | `string \| Record<string, string>` | No | Gruntwork's app (`github.com` only) | Custom GitHub OAuth App client ID. A string applies to `host` when set, otherwise to `github.com`. A map is keyed by host. Required for OAuth on GitHub Enterprise hosts. GitHub only. |
| `oauthScopes` | `string[]` | No | `['repo']` (GitHub) | OAuth scopes to request. |
| `instanceUrl` | `string` | No | - | GitLab only. The starting value of the editable *GitLab Instance URL* field, such as `https://gitlab.example.com`. Credentials are detected and tokens validated against this instance. Takes precedence over `host`. |
| `detectCredentials` | `false \| GitCredentialSource[]` | No | `['env', 'cli']` | Credential detection sources, applied to the selected provider. Set to `false` to disable. See [Disabling detection](#disabling-detection). |
| `defaultTab` | `'oauth' \| 'pat'` | No | `'oauth'` (GitHub), `'pat'` (GitLab) | Which authentication tab the block opens on. GitLab has no OAuth flow, so `'oauth'` falls back to `'pat'` there. Re-applied when the user switches providers. |
| `inputsId` | `string \| string[]` | No | - | ID of one or more `` blocks. `title` and `description` resolve `{{ .inputs.VarName }}` expressions against them. |
## Usage
On successful authentication, the block sets these session environment variables, which later blocks inherit:
| Provider | Variables |
| --- | --- |
| `github` | `GITHUB_TOKEN`, `GITHUB_USER`, `GITHUB_HOST`, `GH_HOST` (plus `GH_ENTERPRISE_TOKEN` on GitHub Enterprise Server) |
| `gitlab` | `GITLAB_TOKEN`, `GITLAB_USER`, `GITLAB_HOST` |
`GITLAB_HOST` is the instance the token belongs to, either `gitlab.com` or your self-managed host.
A [``](/authoring/blocks/gitclone/) of a private `github.com` or `gitlab.com` repository uses the session `GITHUB_TOKEN` or `GITLAB_TOKEN`. To clone from a self-managed GitLab instance, set `gitAuthId` on the GitClone block so it uses the session `GITLAB_TOKEN`. Runbooks sends a session token only over HTTPS to the host it belongs to, never to another host or over plain `http://`, and not at all once a later script points `GITHUB_HOST` or `GITLAB_HOST` at a different host. For GitHub, `GITHUB_HOST` and `GH_HOST` record which host the token belongs to, so `gh` commands in later [Command](/authoring/blocks/command/) and [Check](/authoring/blocks/check/) scripts target the same host.
### Explicit block references
Blocks can reference an auth block by ID so they wait for authentication and receive its credentials for that execution only:
- `gitAuthId` references a `` block of either provider and injects the token and user it produced. Use this for GitLab.
- `githubAuthId` is GitHub only. It reads `GITHUB_TOKEN` and `GITHUB_USER`, so it cannot reference a GitLab block.
> **Caution:** A GitLab `` block cannot be referenced via `githubAuthId`. Use `gitAuthId`, or rely on the session environment, from which any `` or `` inherits `GITLAB_TOKEN` without an explicit reference.
## Provider-locked aliases
Two aliases lock `` to a single provider and hide the dropdown:
- [``](/authoring/blocks/githubauth/) is equivalent to ``.
- [``](/authoring/blocks/gitlabauth/) is equivalent to ``.
## Detailed example
The [git-auth demo](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/git-auth) is a complete GitLab example.
---
#### <GitClone>
The `` block brings a git repository into a runbook, either by cloning it or by pointing at a checkout the user already has on disk. It works with any git upstream. When a GitHub token is available, it can also browse GitHub organizations, repositories, and branches.
Compared with cloning from a [Command](/authoring/blocks/command/) block, GitClone adds a form for the clone, search of the GitHub API for orgs, repos, branches, and tags, and the [file workspace](/intro/files_workspace/), which shows the contents of the repository and any changes to it.
### Basic usage
At its simplest, the block is a single text input for a git URL:
```mdx
```
### With GitHub authentication
When paired with a `` block, the GitClone block enables a "Browse GitHub repositories" dropdown for discovering repos by organization and name, and a ref selector for choosing a branch or tag to clone. The GitHub token is also used when cloning GitHub URLs, which gives access to private repositories.
```mdx
```
### Using a local checkout
Users often already have the repository cloned, such as a long-lived `infrastructure-live` checkout or a large monorepo. The block's source picker lets them choose **Use local checkout** and select that directory.
Choosing a directory only inspects it. The block resolves the repository root, so any subdirectory of the checkout works. It then reads the repository's remote and current branch and counts its tracked files. Nothing is fetched, pulled, modified, or registered at this point.
Confirming with **Use This Repo** registers the checkout and produces the block's outputs, as a completed clone does. Until then, any later block referencing its outputs reports them as missing. To undo the choice, use **Stop using this repo** in the result panel (see [Cancelling and starting over](#cancelling-and-starting-over)).
Inspecting a directory needs no credentials, so browsing works while a linked auth block is still pending. Confirming waits on the auth block, as cloning does, because the GitHub `org_id` and `repo_id` outputs are resolved from the session token at that moment.
```mdx
```
Setting `prefilledRepoDir` starts the block on the local source. Use `source` to choose the starting source explicitly, and `hideSourceSelect` to remove the choice altogether:
```mdx
{/* Local checkouts only, with no cloning offered */}
{/* Cloning only */}
```
Later blocks behave the same with either source. They get the same `clone_path`, `repo_owner`, and `repo_name` outputs, the same `$REPO_FILES` variable, the same workspace file tree, and the same [``](/authoring/blocks/gitpullrequest) integration. A pull request opens against the checkout's remote and current branch.
> **Caution:** A checkout with no remote at all can still be selected, but it produces no `repo_owner` or `repo_name`, nor the GitHub IDs derived from them, and blocks that open a pull request need one. The block says so inline when it finds no remote. `origin` is preferred; if the checkout names its remote something else, that one is used.
### Pre-filled values
You can pre-fill the URL, ref, sparse checkout path, and local path fields. Users can still edit these values before cloning.
```mdx
```
### Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `string` | Yes | Unique block identifier. Later blocks use it to reference outputs. |
| `title` | `string` | No | Display title for the block header. Supports inline markdown. Defaults to "Clone Repository". |
| `description` | `string` | No | Description text below the title. Supports inline markdown. |
| `inputsId` | `string \| string[]` | No | ID of one or more [``](/authoring/blocks/inputs) blocks. `title`, `description`, and the `prefilled*` props resolve `{{ .inputs.VarName }}` expressions against them. |
| `githubAuthId` | `string` | No | Reference to a [``](/authoring/blocks/githubauth) block. When set, the block waits for authentication to complete and uses the token for GitHub API access and clone authentication. |
| `gitAuthId` | `string` | No | Reference to a [``](/authoring/blocks/gitauth), [``](/authoring/blocks/githubauth) or [``](/authoring/blocks/gitlabauth) block, for either provider. When set, the block waits for authentication to complete and clones with that provider's session token, including from a self-managed GitLab instance. |
| `prefilledUrl` | `string` | No | Pre-fills the Git URL input field. The user can edit this value before cloning. |
| `prefilledRef` | `string` | No | Pre-fills the ref (branch or tag) to clone. When set, the specified ref is passed to `git clone --branch`. Defaults to the repository's default branch if empty. |
| `prefilledRepoPath` | `string` | No | Pre-fills the sparse checkout path: a directory relative to the repository root, such as `modules/vpc`. When set, only that directory is checked out, plus the loose files above it such as a top-level `README.md`. Empty or `.` clones the whole repository. See [Sparse checkout](#sparse-checkout). |
| `prefilledLocalPath` | `string` | No | Pre-fills the local path (relative to the current working directory) where files will be cloned. Defaults to the repository name if empty. |
| `showFileTree` | `boolean` | No | Whether to show the cloned repository's file tree in the workspace panel after cloning. Defaults to `true`. When enabled, the **All files** and **Changed files** tabs display the cloned files and any later modifications. |
| `source` | `'clone' \| 'local'` | No | Which source the block starts on: clone a remote repo, or use an existing local checkout. Defaults to `local` when `prefilledRepoDir` is set, otherwise `clone`. |
| `hideSourceSelect` | `boolean` | No | Hides the source picker and locks the block to `source`. Defaults to `false`. |
| `prefilledRepoDir` | `string` | No | Pre-fills the local checkout directory. Any directory inside the checkout works, because the repository root is resolved from it. |
### Ref selection
The "Ref" field lets users specify a branch or tag to clone in place of the default branch. There are two ways to set it:
1. **GitHub browser.** When a GitHub token is available and a repo is selected in the GitHub browser, a searchable "Ref" dropdown appears listing all branches and tags in that repo, with the default branch marked by a "default" badge. Picking a repo in the browser clears the "Ref" field and then selects that repo's default branch (a repo with no commits yet has none, so the field stays empty). The repo the browser opens on from `prefilledUrl` keeps `prefilledRef`.
2. **Text input.** The "Ref" field below the GitHub browser accepts any branch or tag name directly, independent of GitHub authentication. Use the `prefilledRef` prop to set this in advance.
The ref is passed to `git clone --branch`, which accepts both branch names and tag names.
### File workspace integration
When `showFileTree` is `true` (the default), the GitClone block registers the cloned repository with the workspace panel. After a successful clone:
- The **All files** tab shows the full file tree of the cloned repository. Click any file to view its contents.
- The **Changed files** tab shows a diff view, in the style of a GitHub pull request, of any files modified after cloning, for example by Command or Template blocks.
- If multiple `` blocks are used in a single runbook, a dropdown in the workspace header lets the user switch between repositories.
Set `showFileTree={false}` to keep the cloned repository out of the workspace panel, for example for a helper repository the user doesn't need to browse.
A local checkout registers with the workspace the same way. The **Changed files** tab then also shows any uncommitted changes the checkout already had.
### Repositories with no commits
A repository that was created but never pushed to has no branches. No later block can open a pull request against it, because the base branch a pull request needs does not exist. The provider rejects the request as invalid, and only after the runbook has committed and pushed its work.
GitClone detects this case for both sources. When the repo has no commits, the block:
- renders in the warning color,
- withholds its outputs (including `clone_path`) and does not register the repository with the workspace, so blocks that depend on it stay blocked,
- offers a **Create default branch** button that pushes a single empty commit to the branch the remote advertises as its default. The name is editable before you press it.
Once the branch exists, the block releases its outputs and registers the workspace as it does for a repository that already had commits, and the runbook continues.
The seeded commit is empty. It gives the default branch something to point at, so the branch the runbook pushes later shares an ancestor with it and opens as a reviewable diff. Files already written into the work tree are left untracked.
> **Note:** Seeding requires a linked auth block, since it pushes to the remote. The branch is created locally, committed, and pushed to `origin` in one step.
### Accepted git URL formats
| Format | Example | Notes |
|--------|---------|-------|
| HTTPS | `https://github.com/org/repo.git` | Recommended. The token is used for URLs on the authenticated GitHub host. |
| HTTPS (no .git) | `https://github.com/org/repo` | Also works without the `.git` suffix. |
| Any host | `https://gitlab.com/org/repo.git` | Works with any git hosting provider. |
| SSH | `git@github.com:org/repo.git` | Token auth does not apply to SSH URLs. Use HTTPS for token-based auth. |
| SSH (other user) | `gitlab@gitlab.example.com:group/repo.git` | Any SSH user works, for self-managed instances whose SSH user isn't `git`. |
| SSH, bracketed host | `git@[2001:db8::1]:org/repo.git`, `git@[git.example.com:2222]:org/repo.git` | As in git, an IPv6 address, or a host with a non-default SSH port, goes in brackets. Without brackets, the text after the colon is always the repository path. |
### GitHub authentication
The GitClone block resolves GitHub credentials in this order:
1. `githubAuthId`. If set, the block uses the `GITHUB_TOKEN` from the referenced GitHubAuth block's outputs, for the host that block authenticated to.
2. The session environment. The block looks for a GitHub token bound to the host (see [environment variables and hosts](/authoring/blocks/githubauth/#environment-variables-and-hosts)).
3. No token. The "Browse GitHub repositories" section is hidden. The user can still clone public repos or use SSH URLs.
When a GitHub token is available and the user enters an HTTPS URL on the host that token belongs to, Runbooks authenticates the clone with the token.
#### GitHub Enterprise
GitClone follows the GitHub host of the linked auth block (its `GITHUB_HOST` output, `github.com` by default). When the auth block authenticated to a GitHub Enterprise Server host such as `github.example.com`, or a GHE.com tenant such as `acme.ghe.com`:
- The **Browse GitHub repositories** dropdown lists organizations, repositories, and refs from that host's API.
- Repositories picked in the browser are cloned from `https://<host>/<org>/<repo>`, and the URL placeholder shows that host.
- The token is only injected into clone URLs on that same host. A URL on any other host (including `github.com`) is cloned without it.
```mdx
```
A `prefilledUrl` on `github.com`, a `*.ghe.com` tenant, or the auth block's host is recognized as a GitHub repository, so its organization and repository are pre-selected in the browser.
The token is passed to git only for the clone itself (through the environment, which requires git 2.31 or later), so it is never saved in the checkout's `.git/config` or shown as part of its remote URL. A script that later runs `git pull`, `git fetch`, or `git push` in the checkout needs its own git credentials, such as a configured credential helper (`gh auth setup-git`), an SSH remote, or a one-off helper that reads the token from the environment:
```bash
git -c credential.helper= \
-c "credential.https://${GITHUB_HOST:-github.com}.helper="'!f() { echo username=x-access-token; echo "password=${GITHUB_TOKEN}"; }; f' \
pull
```
The helper is scoped to `https://` on the token's own host (`GITHUB_HOST`, set by the GitHub Auth block), so git never offers the token to another remote or over plain `http://`. For a GitLab checkout, scope it to your GitLab host, such as `credential.https://gitlab.com.helper`, and use `username=oauth2` and `${GITLAB_TOKEN}` in the helper.
### Sparse checkout
Use the `prefilledRepoPath` prop (or the "Repo Path" input field) to check out only one directory of a large repository:
```mdx
```
This clones the history without file contents (`git clone --filter=blob:none --no-checkout`), then checks out the path with a cone-mode `git sparse-checkout`. File contents are downloaded only for the files that are checked out. Sparse checkout works together with `prefilledRef`.
Cone mode checks out more than the directory itself. It also includes the files directly inside each directory above it, up to the repository root. For `modules/vpc-app`, that is everything under `modules/vpc-app/`, plus the files (but not the subdirectories) in `modules/` and at the root, such as a top-level `README.md`.
The path is relative to the repository root. An absolute path or one containing `..` fails the clone, and an empty path or `.` clones the whole repository.
[``](/authoring/blocks/gitpullrequest) commits every file that later blocks write in the checkout, including files outside the sparse checkout: new files, and changes to files the repository already has there. Files outside the sparse checkout that no block wrote stay out of the commit, and are not committed as deleted. Staging uses `git add --sparse`, which needs git 2.34 or later. With an older git, a pull request from a sparse checkout fails with a message that says so.
### Custom local path
The `prefilledLocalPath` prop (or the "Local Path" input field) controls where files are cloned, relative to the current working directory. If not set, the repository name is used.
The local path must be a subdirectory of the working directory, because "Delete & Clone" removes an existing directory there before cloning. The block shows an error instead of cloning when the path is the working directory itself (for example `.`), when it leads outside the working directory (including through a symlink), or when it contains the runbook.
The block shows the destination relative to the working directory, both under the "Local Path" field and once the clone or local checkout completes. Hover over the path to see the absolute path. The copy button next to it copies the absolute path. A local checkout outside the working directory has no shorter form, so it is shown by its absolute path.
### Environment variables
When a `` block registers a repository, Runbooks sets the `$REPO_FILES` environment variable for later Command and Check blocks. It points to the repository's local path.
```bash
#!/bin/bash
# In a Command or Check script after GitClone
echo "Cloned repo is at: $REPO_FILES"
# Modify files directly in the cloned repo
echo "new_setting = true" >> "$REPO_FILES/config.hcl"
```
> **Note:** With several `` blocks, `$REPO_FILES` points to the repository selected in the workspace's repository dropdown, or to the most recently registered one if the user hasn't selected any.
Template and TemplateInline blocks can also write directly into the cloned repository by setting `target="worktree"`:
```mdx
```
### Block outputs
After a successful clone, the GitClone block produces these outputs:
| Output | Description | Example |
|--------|-------------|---------|
| `clone_path` | Absolute path where files were cloned | `/Users/josh/Code/my-repo` |
| `repo_owner` | The organization or user that owns the repo, parsed from the repository URL | `acme-corp` |
| `repo_name` | The repository name, parsed from the repository URL | `infrastructure-live` |
| `org_id` | GitHub numeric ID of the owning org or user. Only for GitHub repositories, when a GitHub token is available | `12345678` |
| `repo_id` | GitHub numeric ID of the repository. Only for GitHub repositories, when a GitHub token is available | `87654321` |
`repo_owner` and `repo_name` are omitted when the URL can't be parsed into an owner and a name. The numeric IDs don't change when a repository is renamed or transferred.
For a local checkout, `clone_path` is the repository root the user selected, and `repo_owner` and `repo_name` come from its remote, `origin` when present, otherwise whichever remote the checkout has. They are omitted when the repo has no remote, and so are `org_id` and `repo_id`, which are looked up from the owner and name.
Because `org_id` and `repo_id` (and, for a checkout without a remote, `repo_owner` and `repo_name`) can be missing, read them behind a `hasKey` guard. Otherwise a block that references them waits until they exist, which without a GitHub token is never. See [Outputs a block emits only sometimes](/authoring/inputs-and-outputs/#outputs-a-block-emits-only-sometimes).
```
{{ if hasKey .outputs.clone_repo "org_id" }}--org-id {{ .outputs.clone_repo.org_id }}{{ end }}
```
Reference outputs in later blocks with template expressions:
```mdx
```
> **Note:** Block IDs with hyphens are converted to underscores in template variables. For example, `clone-repo` becomes `clone_repo`.
### Cancelling and starting over
**Cancel** stops the running `git` process. A cancelled clone produces no outputs and is not added to the workspace. Once `git` has exited, the block removes the directory the clone created, including a [sparse checkout](#sparse-checkout) stopped partway and a checkout that `git` had already finished. The block shows **Cancelling...** until that is done, or for at most about ten seconds, and then offers **Clone** again.
The block only removes a directory that the clone created. **Delete & Clone** deletes the old directory before the clone starts, so that directory is gone either way.
A directory can still be left behind. A cancel that arrives just as the clone completes is too late to undo it. The checkout stays and, as after any completed clone, `$REPO_FILES` may point at it. A directory that can't be removed also stays, as can happen on Windows while a process still has a file in it open. Cloning to the same path again offers **Delete & Clone** to replace it.
**Clone again** (or **Stop using this repo** for a local checkout), next to the result's heading, withdraws the block's outputs and takes the repository out of the workspace panel until the next clone or checkout completes. Nothing on disk changes. For a local checkout, the block returns to its form with the same directory filled in, so the user can use it again or pick another one. Blocks that read this block's outputs wait for that next clone rather than using the previous repository's values. [``](/authoring/blocks/gitpullrequest) stops targeting the previous repository too. It waits, or uses the repository of another `` block in the runbook until this one completes again. A repository with no commits chosen next is held back as described in [Repositories with no commits](#repositories-with-no-commits).
`$REPO_FILES` and `target="worktree"` writes are not held back. When no other `` block has a repository in the workspace, they keep pointing at the previous repository until the next clone or checkout completes.
### View logs
The GitClone block has a collapsible **View Logs** section, like the [Command](/authoring/blocks/command/#viewing-logs) and [Check](/authoring/blocks/check/) blocks. It streams the `git clone` output during the clone, including progress, transfer statistics, and error details.
### Example: full workflow
```mdx
```
---
#### <GitHubAuth>
The `` block authenticates the user to GitHub.
> **Tip:** `` is an alias of [``](/authoring/blocks/gitauth/) locked to GitHub. It is equivalent to ``. Use `` if you also want to support GitLab.
It detects whether the user is already authenticated to GitHub by checking their environment variables and the GitHub CLI. Users can also authenticate manually with OAuth or a Personal Access Token (PAT). Once authenticated, the credentials are available to later [Command](/authoring/blocks/command/) and [Check](/authoring/blocks/check/) blocks.
Scripts in a runbook without this block still see a `GITHUB_TOKEN` set in the environment, but the block is the recommended way to authenticate.
### Basic usage
```mdx
```
By default, Runbooks detects and uses credentials from:
1. Environment variables: `GITHUB_TOKEN` or `GH_TOKEN`, and `GH_ENTERPRISE_TOKEN` or `GITHUB_ENTERPRISE_TOKEN` for a GitHub Enterprise Server host. See [Environment variables and hosts](#environment-variables-and-hosts).
2. The GitHub CLI (`gh auth token --hostname <host>`)
If no credentials are detected, users authenticate manually with OAuth or a PAT.
### Authentication methods
| Method | Description |
|--------|-------------|
| **Environment Variable** | Detected from `GITHUB_TOKEN` or `GH_TOKEN` |
| **GitHub CLI** | Detected from `gh auth token` |
| **OAuth** | Sign in via GitHub's device authorization flow (recommended) |
| **Personal Access Token** | Enter a PAT (classic or fine-grained) directly in the UI |
### GitHub Enterprise support
The GitHubAuth block works with three kinds of GitHub host:
| Host kind | Example host | REST API base URL |
|-----------|--------------|-------------------|
| **GitHub.com** (default) | `github.com` | `https://api.github.com` |
| **GitHub Enterprise Cloud with data residency** (GHE.com) | `acme.ghe.com` | `https://api.acme.ghe.com` |
| **GitHub Enterprise Server** (GHES, self-hosted) | `github.example.com` | `https://github.example.com/api/v3` |
Any host that is not `github.com` or a `*.ghe.com` tenant is treated as GitHub Enterprise Server. GitHub is always reached over HTTPS, and the OAuth device flow always uses the host's web origin (`https://<host>/login/device`). GitHub Enterprise Cloud without data residency lives on `github.com`, so it needs no host configuration.
#### Pinning the host
Set the `host` prop to pin the block to one GitHub host. The host picker is hidden, and detection, validation, OAuth and every API call target that host only:
```mdx
{/* GitHub Enterprise Server */}
{/* GitHub Enterprise Cloud with data residency */}
```
`host` accepts a bare hostname, optionally with a port, or a URL such as `https://github.example.com`, of which only the host part is kept. An API origin is mapped back to its web host, so `api.github.com` becomes `github.com` and `api.acme.ghe.com` becomes `acme.ghe.com`. A `host` value that cannot be parsed is reported as a configuration error. The block never falls back to `github.com`.
#### Picking a host
Without a `host` prop, the block offers a **GitHub host** picker listing every GitHub host Runbooks knows about:
- Hosts you are logged into with the GitHub CLI (from gh's `hosts.yml`, which `gh auth login --hostname <host>` writes to)
- The host named by the `GH_HOST` environment variable
- The host the current session is already authenticated to
- Enterprise hosts you recently authenticated to in Runbooks
- `github.com`, always
The picker only appears when more than one host is available. The initially selected host is, in order of precedence:
1. The host the user last picked, if a credential for it is still available
2. The host named by `GH_HOST`
3. `github.com`
To make an enterprise host show up in the picker, log in to it once with the GitHub CLI:
```bash
gh auth login --hostname github.example.com
```
#### Environment variables and hosts
Runbooks follows the GitHub CLI's environment variable conventions, but binds every token to one host and never sends it to any other:
| Variable | Used for |
|----------|----------|
| `GITHUB_TOKEN`, `GH_TOKEN` | The host named by `GH_HOST` when it is `github.com` or a `*.ghe.com` tenant, and `github.com` otherwise |
| `GH_ENTERPRISE_TOKEN`, `GITHUB_ENTERPRISE_TOKEN` | Only the GitHub Enterprise Server host named by `GH_HOST`. Not used otherwise |
For example:
| Environment | `github.com` | `acme.ghe.com` | `github.example.com` (GHES) |
|-------------|--------------|----------------|-----------------------------|
| `GITHUB_TOKEN` set, no `GH_HOST` | `GITHUB_TOKEN` | - | - |
| `GITHUB_TOKEN` set, `GH_HOST=acme.ghe.com` | - | `GITHUB_TOKEN` | - |
| `GITHUB_TOKEN` and `GH_ENTERPRISE_TOKEN` set, `GH_HOST=github.example.com` | `GITHUB_TOKEN` | - | `GH_ENTERPRISE_TOKEN` |
| `GH_ENTERPRISE_TOKEN` set, no `GH_HOST` | - | - | - |
When both variables of a pair are set, `GITHUB_TOKEN` wins over `GH_TOKEN`, and `GH_ENTERPRISE_TOKEN` wins over `GITHUB_ENTERPRISE_TOKEN`. A `GH_HOST` that cannot be parsed binds no environment token. For any other host, use the GitHub CLI, OAuth, or a personal access token.
> **Note:** This is stricter than the GitHub CLI, which sends `GITHUB_TOKEN` to both `github.com` and `*.ghe.com` hosts, and `GH_ENTERPRISE_TOKEN` to any Enterprise Server host. Because a runbook can choose the host, Runbooks only releases an environment token to the one host the user configured it for.
GitHub CLI detection runs `gh auth token --hostname <host>` for the selected host, so it returns the token gh stored for that host regardless of gh's own default host.
#### OAuth on enterprise hosts
The built-in Gruntwork Runbooks OAuth app only exists on `github.com`. To offer **Sign in with GitHub** on an enterprise host, register an OAuth app on that instance or tenant with device flow enabled, and pass its client ID in `oauthClientId`.
`oauthClientId` accepts a string or a map of host to client ID. A string applies to the block's `host` when one is set, otherwise to `github.com` only. Use a map when the host picker is shown and users may pick different hosts.
```mdx
{/* Pinned host: the string applies to github.example.com */}
{/* Host picker: one OAuth app per host */}
```
`github.com` keeps using the Gruntwork app unless the map, or a string without `host`, overrides it. For an enterprise host with no client ID, the OAuth tab is disabled with an explanation, and users can still sign in with a personal access token or `gh auth login --hostname <host>`. With `detectCredentials={false}` the block ignores the `gh` login, so only the personal access token is offered. Runbooks never sends the `github.com` client ID to an enterprise host.
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `id` | string | required | Unique identifier for this component |
| `title` | string | "GitHub Authentication" | Display title shown in the UI |
| `description` | string | - | Description of the authentication purpose |
| `inputsId` | string or string[] | - | ID of one or more [Inputs](/authoring/blocks/inputs/) blocks. `title` and `description` resolve `{{ .inputs.VarName }}` expressions against them |
| `detectCredentials` | `false` or `CredentialSource[]` | `['env', 'cli']` | Whether and how the block detects credentials in the user's environment |
| `host` | string | - | Pins the GitHub host (`github.com`, a GHE.com tenant such as `acme.ghe.com`, or a GitHub Enterprise Server host such as `github.example.com`) and hides the host picker. See [GitHub Enterprise support](#github-enterprise-support) |
| `oauthClientId` | string or `Record<string, string>` | Gruntwork default (github.com only) | Custom OAuth App client ID. A string applies to `host`, or to `github.com` when `host` is unset. A map is keyed by host. Required for OAuth on enterprise hosts |
| `oauthScopes` | string[] | `["repo"]` | OAuth scopes to request |
| `defaultTab` | `'oauth'` or `'pat'` | `'oauth'` | Which authentication tab the block opens on |
## Usage
When authentication succeeds, these environment variables are set in the session:
| Variable | Description |
|----------|-------------|
| `GITHUB_TOKEN` | The GitHub access token |
| `GITHUB_USER` | The authenticated user's login name |
| `GITHUB_HOST` | The GitHub host the token belongs to (such as `github.com`, `acme.ghe.com`, or `github.example.com`) |
| `GH_HOST` | Same as `GITHUB_HOST`, so `gh` commands in later scripts target the same host |
| `GH_ENTERPRISE_TOKEN` | Set on GitHub Enterprise Server hosts only. Holds the same token, because it is the only token variable `gh` reads for Enterprise Server |
Later Command and Check blocks have access to these credentials. If you authenticate more than once, the most recent credentials become the default.
### Explicit block references
A Command or Check block can reference a GitHubAuth block with the `githubAuthId` prop. Its Run or Check button is then disabled until the referenced block has valid credentials. When several GitHubAuth blocks exist, the block runs with the credentials of the one you named.
```mdx
```
#### How environment variables work with references
When a GitHubAuth block authenticates, it sets the session environment variables (`GITHUB_TOKEN`, `GITHUB_USER`, `GITHUB_HOST`, `GH_HOST`) for all later blocks. It also stores its credentials so that blocks can request them with `githubAuthId`.
A block that sets `githubAuthId` gets the referenced block's credentials as environment variables that override the session values for that block only. This holds when several GitHubAuth blocks have different credentials, when another auth block set the session `GITHUB_TOKEN`, and when the user later re-authenticated with different credentials.
Without `githubAuthId`, a block uses whatever `GITHUB_TOKEN` is in the session environment, set by the most recent authentication.
> **Tip:** We recommend `githubAuthId` even with a single GitHubAuth block. It stops users from running a command before authenticating, and it helps with automated testing.
### Credential detection
By default, GitHubAuth detects existing credentials from environment variables and the GitHub CLI. The `detectCredentials` prop changes this.
#### Default behavior
With no configuration, GitHubAuth tries these sources in order:
```mdx
{/* Same as detectCredentials={['env', 'cli']} */}
```
1. Environment variables. `GITHUB_TOKEN` or `GH_TOKEN`, or `GH_ENTERPRISE_TOKEN` or `GITHUB_ENTERPRISE_TOKEN` for a GitHub Enterprise Server host, following the [host binding rules](#environment-variables-and-hosts).
2. The GitHub CLI. `gh auth token --hostname <host>`.
If either source provides valid credentials, authentication succeeds without user input. If neither does, users see the manual OAuth and PAT interface.
#### Detection sources
| Source | Description |
|--------|-------------|
| `'env'` | Check standard env vars (`GITHUB_TOKEN`, `GH_TOKEN`, and `GH_ENTERPRISE_TOKEN`, `GITHUB_ENTERPRISE_TOKEN` for GitHub Enterprise Server) |
| `{ env: { prefix: 'PREFIX_' } }` | Check prefixed env vars (`PREFIX_GITHUB_TOKEN`, `PREFIX_GH_TOKEN`, and so on), bound to a host by `PREFIX_GH_HOST` |
| `'cli'` | Check GitHub CLI (`gh auth token --hostname <host>`) |
| `{ block: 'block-id' }` | Use token from a Command block's output |
#### Custom detection order
Check only environment variables:
```mdx
```
Check prefixed environment variables such as `PROD_GITHUB_TOKEN`:
```mdx
```
A prefix must follow these rules:
- Uppercase letters, numbers, and underscores only
- Must start with a letter
- Must end with an underscore, as in `PROD_`
A prefixed source reads only `PREFIX_GITHUB_TOKEN` or `PREFIX_GH_TOKEN`, or `PREFIX_GH_ENTERPRISE_TOKEN` or `PREFIX_GITHUB_ENTERPRISE_TOKEN` for a GitHub Enterprise Server host. `PREFIX_GH_HOST`, and not the unprefixed `GH_HOST`, decides which host they are bound to, by the same rules as for the unprefixed variables.
#### Test prefix
To use a different prefix in automated tests, for example to avoid conflicts with local credentials, set `env_prefix` on the step in `runbook_test.yml`:
```yaml
# runbook_test.yml
steps:
- block: gh-auth
env_prefix: CI_ # Checks CI_GITHUB_TOKEN, CI_GH_TOKEN
expect: success
```
Without `env_prefix`, the test framework checks `RUNBOOKS_GITHUB_TOKEN` first, which lets you keep test credentials separate from the ones in your local environment. See [Testing GitHubAuth blocks](/authoring/testing/#testing-githubauth-blocks).
#### Disabling detection
To have the user always sign in on the block:
```mdx
```
The block then ignores environment tokens such as `GITHUB_TOKEN` and the user's `gh` login, and doesn't offer to check for them again. The user signs in with OAuth or a personal access token. See [Disabling detection](/authoring/blocks/gitauth/#disabling-detection).
#### From Command output
Use a token generated by a previous Command block:
```mdx
```
The Command script should output the token. The [`sensitive:` prefix](/authoring/inputs-and-outputs/#sensitive-outputs) masks it in the Command's **View Outputs**, and GitHubAuth still reads it as `GITHUB_TOKEN`:
```bash
#!/usr/bin/env bash
# Example: fetch token from a secrets manager
TOKEN=$(vault read -field=token secret/github)
echo "sensitive:GITHUB_TOKEN=$TOKEN" >> "$RUNBOOK_OUTPUT"
```
Sources are tried in the order you list them. If the referenced block has not run yet, GitHubAuth shows *Waiting for "fetch-token" to run...* and detection pauses there without moving to a later source. If the block ran without writing a usable `GITHUB_TOKEN`, for example because the script failed, detection moves on to the next source, and the manual sign-in form appears if none of them finds a credential.
## Permissions
A GitHub token's permissions control what it can do. How they are set depends on the authentication method:
| Method | How permissions are set |
|--------|------------------------|
| **Environment Variable** | Permissions are inherited from however the token was originally created |
| **GitHub CLI** | Permissions are inherited from `gh auth login`. Runbooks warns if the `repo` scope is missing |
| **OAuth** | The runbook author specifies OAuth scopes with the `oauthScopes` prop, and the user approves them on GitHub |
| **Personal Access Token (PAT)** | User configures permissions when creating the token on GitHub |
### OAuth scopes
When users authenticate via OAuth, the `oauthScopes` prop controls what permissions are requested. Users see these scopes on GitHub's authorization page and must approve them.
The default is `["repo"]`, which grants full read/write access to repositories. Request narrower scopes if your runbook needs less:
| Scope | Description |
|-------|-------------|
| `repo` | Full access to private and public repositories (default) |
| `public_repo` | Access to public repositories only |
| `repo:status` | Read/write commit status (useful for CI) |
| `read:org` | Read organization membership |
For a complete list, see [GitHub's OAuth scopes documentation](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps).
```mdx
```
> **Caution:** OAuth scopes are coarse-grained. The smallest OAuth scope that Runbooks can use is `repo`, which grants full read/write access to all repositories. To narrow the permissions further, create a fine-grained token and either set it as an environment variable or enter it in the UI.
### Personal access tokens
When users authenticate with a PAT, the token's permissions are determined by how the user created it on GitHub. Runbook authors cannot control PAT permissions.
Classic PATs use the same scopes as OAuth, such as `repo` and `read:org`. Fine-grained PATs set permissions per repository.
If your runbook requires specific permissions, list them in the runbook so users know what to select when creating their PAT.
The block's "create a token" link follows the selected host: `https://<host>/settings/personal-access-tokens/new` on `github.com` and GHE.com tenants, and `https://<host>/settings/tokens/new` on GitHub Enterprise Server.
### Auto-detected credentials
When GitHubAuth detects credentials from environment variables or GitHub CLI, those tokens retain whatever permissions they were created with. If a detected token lacks required permissions, operations may fail at runtime.
> **Note:** When detecting credentials from GitHub CLI, Runbooks checks whether the token has the `repo` scope and displays a warning if it's missing.
## Security
### Principles
1. **Gruntwork never sees your credentials.** Authentication happens between your machine and GitHub. Tokens are not sent to Gruntwork servers.
2. **Tokens stay with their host.** A token is sent only to the GitHub host it was issued for.
3. **Credentials stay in memory.** Tokens are stored only in your local Runbooks session and are not written to disk.
### How credentials stay local
| Method | How it works |
|--------|-------------|
| **Environment Variable** | Runbooks reads `GITHUB_TOKEN` from your local environment. |
| **GitHub CLI** | Runbooks calls `gh auth token` locally, which reads the token from the CLI's own storage. |
| **OAuth** | Uses GitHub's [Device Authorization Grant](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow). GitHub sends the token to the Runbooks app on your machine. No Gruntwork server is involved. |
| **Personal Access Token** | You paste the token into the Runbooks app, which sends it only to the GitHub host shown in the block. |
### OAuth flow details
When you authenticate via OAuth:
1. Runbooks, running on your machine, requests a device code from GitHub
2. You open `https://<host>/login/device` (github.com/login/device by default) and enter the code. GitHub keeps the code valid for 15 minutes by default, and Runbooks keeps checking for your approval until it expires. If it expires before you approve, start the sign-in again.
3. You see "Gruntwork Runbooks wants to access your account" (or the name of your enterprise's OAuth app) and approve
4. GitHub sends the token to the Runbooks app on your machine
Gruntwork has no server in this flow.
### Threat analysis
#### Malicious runbook with custom `oauthClientId`
**Scenario.** An attacker publishes a runbook with `oauthClientId="attacker-app-id"`.
**What happens.** GitHub's authorization page shows "Attacker's App" where it would normally show "Gruntwork Runbooks". If you approve, the token still goes to your machine. The attacker registered an OAuth app but has no server receiving tokens.
**Risk.** None. The token goes from GitHub to your machine, so the attacker cannot intercept it. The only effect is that "Attacker's App" appears in your GitHub authorized apps list, where you can revoke it.
**Mitigation.** A custom client ID can still indicate a suspicious runbook, so Runbooks displays a warning when `oauthClientId` differs from the default. Check the app name on GitHub's authorization page before approving.
#### Malicious runbook with custom base URL
**Scenario.** An attacker publishes a runbook that tries to redirect OAuth to a fake GitHub login page, for example with `oauthBaseUrl="https://github-login.attacker.com"`.
**What happens.** Nothing. `oauthBaseUrl` is not a prop. GitHubAuth only communicates with the GitHub host shown in the block (`github.com` unless `host` or the host picker says otherwise), always over HTTPS.
**Risk.** None.
#### Malicious runbook with custom `host`
**Scenario.** An attacker publishes a runbook with `host="github-login.attacker.com"`, hoping to collect tokens from your environment or GitHub CLI.
**What happens.** The block shows the host it is authenticating to. Environment tokens are only released to the host they are bound to: `GITHUB_TOKEN` to `github.com` or the `*.ghe.com` host in `GH_HOST`, and `GH_ENTERPRISE_TOKEN` only to the Enterprise Server host in `GH_HOST`. GitHub CLI detection only returns a token you stored for that exact host with `gh auth login --hostname`. OAuth needs an app registered on that host.
**Risk.** Low. Only a token you type or paste into the block could reach the attacker's host.
**Mitigation.** Check the host shown in the block before pasting a personal access token or approving a sign-in.
#### Scope escalation
**Scenario.** A runbook requests excessive permissions with `oauthScopes={["repo", "admin:org", "delete_repo"]}`.
**What happens.** Runbooks displays the requested scopes in the UI before you authenticate. GitHub's authorization page displays them too. If you approve without reviewing either, you grant broader access than needed.
**Risk.** Low. Both Runbooks and GitHub display the requested scopes before you grant access.
**Mitigation.** Review the scopes shown in Runbooks before clicking "Sign in with GitHub", and check them again on GitHub's authorization page. Be suspicious of runbooks requesting `admin:*`, `delete_repo`, or `write:*` scopes beyond the default `repo`.
### Managing your authorizations
Gruntwork never has your tokens, so there is nothing to revoke from Gruntwork. Revoking at GitHub invalidates the token itself, which is useful when you are done with a runbook or suspect a token was exposed.
You can manage authorizations at:
- [github.com/settings/applications](https://github.com/settings/applications) to revoke OAuth authorizations
- [github.com/settings/tokens](https://github.com/settings/tokens) to revoke personal access tokens
On an enterprise host, use the same paths on that host, for example `https://github.example.com/settings/tokens`.
Closing your Runbooks session also discards any tokens stored locally.
---
#### <GitHubPullRequest>
The `` block commits the changes made in a repository cloned by [``](/authoring/blocks/gitclone/), pushes them to a new branch, and opens a GitHub pull request. It is a GitHub-locked alias of [``](/authoring/blocks/gitpullrequest/), equivalent to ``.
### Basic usage
Pair it with `` and ``:
```mdx
```
### Pre-filled values
You can pre-populate the PR title, description, labels, branch name, and commit message. Users can still edit these values before creating the PR.
```mdx
```
> **Note:** [``](/authoring/blocks/gitlabmergerequest/) and `` accept the same prefilled props, including `prefilledCommitMessage`.
### Template expressions
The `prefilledPullRequestTitle`, `prefilledPullRequestDescription`, `prefilledBranchName`, and `prefilledCommitMessage` props support template expressions that reference inputs and outputs from other blocks. Escape sequences like `\n` are converted to real newlines, which is useful for multi-line PR descriptions:
```mdx
```
Template expressions are resolved in real-time as upstream blocks produce outputs. If the block references outputs from blocks that haven't run yet, it displays a warning and disables the "Create Pull Request" button until all dependencies are satisfied.
### Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `string` | Yes | Unique block identifier. Used by downstream blocks to reference outputs. |
| `title` | `string` | No | Display title for the block header. Supports inline markdown and template expressions. |
| `description` | `string` | No | Description text below the title. Supports inline markdown and template expressions. |
| `inputsId` | `string \| string[]` | No | Reference to one or more [``](/authoring/blocks/inputs) blocks by ID for template variable substitution. When multiple IDs are provided, variables are merged in order (later IDs override earlier ones). |
| `githubAuthId` | `string` | No | Reference to a [``](/authoring/blocks/githubauth) block. When set, the block waits for authentication to complete and uses the token for GitHub API access. |
| `prefilledPullRequestTitle` | `string` | No | Pre-fills the PR title field. Supports template expressions. |
| `prefilledPullRequestDescription` | `string` | No | Pre-fills the PR description field. Supports template expressions, markdown, and `\n` for newlines. |
| `prefilledPullRequestLabels` | `string[]` | No | Pre-selects labels by name. Labels must exist in the repository. |
| `prefilledBranchName` | `string` | No | Pre-fills the branch name. Supports template expressions. Defaults to `runbook/<timestamp>`. |
| `prefilledCommitMessage` | `string` | No | Pre-fills the commit message field. Supports template expressions. |
### Block outputs
After a successful PR creation, the block produces outputs that can be referenced by downstream blocks:
| Output | Description | Example |
|--------|-------------|---------|
| `PR_ID` | The pull request number | `42` |
| `PR_URL` | The full URL of the created pull request | `https://github.com/org/repo/pull/42` |
Reference outputs in downstream blocks using template variables:
```mdx
```
> **Note:** Block IDs with hyphens are converted to underscores in template variables. For example, `create-pr` becomes `create_pr`.
### Git Push
After creating a PR, the block shows a **Git Push** button. It runs `git push -u origin <branch>` for the PR's branch with the authenticated token. It does not stage or commit anything, so use it to push commits made since the PR was created, for example by a Command block that ran `git commit`. To commit uncommitted changes onto the open PR, choose "create another PR" and enter the open PR's branch name. See [Creating another PR](#creating-another-pr).
### Retrying after a failure
If creation fails after the branch was created (for example, the push is rejected or the API call fails), fix the cause and create the PR again with the same branch name. The block picks up on the existing branch: it commits any new changes and pushes, then opens the PR.
You can also retry under a new branch name. The new branch starts from the branch the failed attempt left checked out, so if that attempt's push failed, its commits are pushed under the new name, along with any new changes.
If a local branch with that name already exists but isn't checked out (for example, one left over from an earlier run), the block offers to delete it and retry. The delete is refused if the branch has commits that haven't been pushed or merged; choose a new branch name instead.
The branch name can't be the base branch or a protected name (`main`, `master`, `develop`, `dev`, `staging`, `release`, `prod`, `production`).
### Creating another PR
After a PR is created, its branch stays checked out, and "create another" fills in a new branch name such as `runbook/<timestamp>`. What the next create does depends on the branch name and on whether anything changed since:
- With a new name and new changes, the new branch starts from the open PR's branch, so the new PR also contains the open one's commits.
- With a new name and no new changes, the create is refused with "Nothing to commit". The open PR's commits are already on GitHub, and a new PR would only repeat them. Nothing is pushed, and the open PR's branch stays checked out.
- With the open PR's branch name, the block picks up on that branch, commits any new changes and pushes them to it, so they become part of the open PR, and then reports that the PR already exists.
The block shows **Git Push** until you choose "create another".
### Prerequisites
The block needs a completed `` block, for the token, and a `` block that has cloned a repository, which the PR is created against. It checks both and shows an amber warning for whichever is missing. The Create PR button stays disabled until both are met.
For authentication, the block looks for a `GITHUB_TOKEN` output from the block referenced by `githubAuthId`. When that block picked up existing credentials (environment variables or the GitHub CLI) rather than running its OAuth flow, it registers an `__AUTHENTICATED` marker instead, which also satisfies the check. Until one is present, the block displays "Waiting for GitHub authentication" and names the block to complete.
For the repository, the block checks whether a `GitClone` block has registered an active worktree. Until one has, it displays "No repository available".
### Labels
The block fetches available labels from the repository and presents them in a searchable multi-select dropdown. Labels can also be pre-selected using the `prefilledPullRequestLabels` prop.
Labels are applied after the PR opens. If applying them fails, the PR is still created and the logs show a warning.
### GitHub Enterprise
The pull request is opened against the host of the cloned repository's `origin` remote: `github.com`, a GitHub Enterprise Server host such as `github.example.com`, or a GHE.com tenant such as `acme.ghe.com`. The block uses that host's API (`https://api.github.com`, `https://<host>/api/v3`, or `https://api.<tenant>.ghe.com`) and fetches labels from it.
That host must be the one the linked auth block authenticated to (its `GITHUB_HOST` output). If they differ, for example a repository cloned from `github.example.com` while the auth block signed in to `github.com`, the block fails with an error and sends the token nowhere. Links to token settings shown by the block also follow the authenticated host.
```mdx
```
See [GitHub Enterprise support](/authoring/blocks/githubauth/#github-enterprise-support) for configuring the host.
### Example: full workflow
```mdx
> config.hcl"
/>
```
---
#### <GitLabAuth>
The `` block authenticates to GitLab.
> **Tip:** `` is a GitLab-locked alias of the generic [``](/authoring/blocks/gitauth/) block, equivalent to ``. Use [``](/authoring/blocks/gitauth/) directly if you want a provider dropdown so users can pick GitHub or GitLab.
It detects an existing login from the `GITLAB_TOKEN` environment variable or the GitLab CLI. Users can also authenticate manually with a Personal Access Token. Once authenticated, the `GITLAB_TOKEN`/`GITLAB_USER` credentials are available to subsequent [Command](/authoring/blocks/command/), [Check](/authoring/blocks/check/), and [GitClone](/authoring/blocks/gitclone/) blocks.
GitLab has no OAuth device flow here (there is no registered Gruntwork GitLab app), so authentication is via PAT, the `glab` CLI, or the `GITLAB_TOKEN` environment variable.
### Basic usage
```mdx
```
By default, Runbooks detects and uses credentials from, in order:
1. The `GITLAB_TOKEN` environment variable
2. The GitLab CLI (`glab auth token`). `glab auth login` writes the token to glab's `config.yml`, which Runbooks reads directly when `glab auth token` returns nothing, for example when the `glab` binary is not on `PATH`.
If no credentials are detected, the user authenticates manually with a Personal Access Token. Set `detectCredentials={false}` to always require one, even when the user is logged in with `glab`. See [Disabling detection](/authoring/blocks/gitauth/#disabling-detection).
Runbooks sends an environment token, prefixed or not, only over HTTPS. A `glab` token goes only to the host glab stored it for, and only over HTTPS unless glab itself uses HTTP for that host (`api_protocol: http`). For a plain `http://` instance the environment token is skipped, and so is the `glab` token unless glab uses HTTP for that host. The user then pastes a Personal Access Token.
The block can authenticate to a plain `http://` instance. [GitClone](/authoring/blocks/gitclone/) and [GitLabMergeRequest](/authoring/blocks/gitlabmergerequest/), including its label lookup, send the session token only over HTTPS. On such an instance the token is available only to [Command](/authoring/blocks/command/) and [Check](/authoring/blocks/check/) scripts as `GITLAB_TOKEN`.
### Prefixed environment variables
Add `{ env: { prefix: 'PREFIX_' } }` to `detectCredentials` to read a namespaced token instead of the standard variables. For example, you can use a CI token so it is never confused with a personal `GITLAB_TOKEN`:
```mdx
```
With a prefix, Runbooks reads only `CI_GITLAB_TOKEN`, then `CI_GITLAB_ACCESS_TOKEN`. It never falls back to the unprefixed variables. The prefix may contain only uppercase letters, digits and underscores. It must start with a letter and end with an underscore (for example `CI_` or `PROD_`). Any other prefix is rejected.
An environment token is only ever sent to one GitLab host. For a prefixed token, that host comes from the prefixed host variables: `CI_GITLAB_HOST`, then `CI_GITLAB_URI`, then `CI_GL_HOST`. If none is set, the host is `gitlab.com`. The unprefixed `GITLAB_HOST` does not apply. If the block authenticates to any other host, the prefixed token is skipped.
The prefixed host variables only limit where the token may be sent. They do not choose the host the block authenticates to, which still comes from the host picker or the `host` prop. For a self-managed instance, set `host` to the same host as `CI_GITLAB_HOST` so the prefixed token is used.
On success the token is saved to the session as `GITLAB_TOKEN`, as with any other source, so downstream blocks don't need to know the prefix.
## Props
`` accepts the same props as [``](/authoring/blocks/gitauth/#props) except `provider` and `hideProviderSelect` (which are fixed to `gitlab` and `true`). The most common are `id` (required), `title`, `description`, `detectCredentials`, and `inputsId`. `title` defaults to `GitLab Authentication`. For a self-managed GitLab instance, add `host` or `instanceUrl`; see [Self-managed GitLab](/authoring/blocks/gitauth/#self-managed-gitlab).
## Downstream references
Reference the block from other blocks with `gitAuthId` (provider-agnostic). The `githubAuthId` prop is GitHub-only and cannot reference a GitLab block. See [Explicit block references](/authoring/blocks/gitauth/#explicit-block-references).
For full details on detection, scopes, and security, see the [``](/authoring/blocks/gitauth/) documentation.
---
#### <GitLabMergeRequest>
The `` block creates a GitLab merge request from changes made during a runbook. It is a GitLab-locked alias of [``](/authoring/blocks/gitpullrequest/), equivalent to ``, and accepts the same props.
### Basic usage
```mdx
```
### Pre-filled values
You can pre-populate the MR title, description, labels, branch name, and commit message. Users can still edit these values before creating the MR.
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `string` | Yes | Unique block identifier. |
| `title` | `string` | No | Display title. Defaults to `Create Merge Request`. |
| `description` | `string` | No | Description text below the title. |
| `gitAuthId` | `string` | No | Reference to a [``](/authoring/blocks/gitauth) or [``](/authoring/blocks/gitlabauth) block. |
| `inputsId` | `string \| string[]` | No | Reference to one or more [``](/authoring/blocks/inputs) blocks by ID. The template expressions in the props below can use their values. |
| `prefilledPullRequestTitle` | `string` | No | Pre-fills the MR title field. Supports template expressions. |
| `prefilledPullRequestDescription` | `string` | No | Pre-fills the MR description field. Supports template expressions. |
| `prefilledPullRequestLabels` | `string[]` | No | Pre-selects labels by name. |
| `prefilledBranchName` | `string` | No | Pre-fills the branch name. Supports template expressions. |
| `prefilledCommitMessage` | `string` | No | Pre-fills the commit message used when committing changes before opening the MR. Supports template expressions. |
> **Note:** The `prefilledPullRequest*` prop names are shared with `` and apply to merge requests the same way.
---
#### <GitPullRequest>
The `` block commits the changes made in a repository cloned by [``](/authoring/blocks/gitclone/), pushes them to a new branch, and opens a GitHub pull request or a GitLab merge request. One block covers both providers.
### Basic usage
Pair it with `` and ``. The provider comes from the linked `` block, so no `provider` prop is needed:
```mdx
```
### Provider detection
`` determines which provider (GitHub or GitLab) to talk to in this order:
1. An explicit `provider` prop, if set.
2. The `GIT_PROVIDER` output of the block referenced by `gitAuthId` (or `githubAuthId`).
3. The host of the cloned repository's remote URL. `github.com` and `*.ghe.com` tenants are recognized as GitHub and `gitlab.com` as GitLab. A GitHub Enterprise Server or self-managed GitLab host is recognized only through the linked auth block.
4. Otherwise, `github`.
> **Note:** If the linked auth block authenticated to a different provider than the block resolves to (for example, a GitLab `` block linked while the cloned repo is on GitHub), the block shows a blocking error.
### GitLab instance
A merge request is opened on the GitLab instance that the repository's `origin` remote points at, whether that is gitlab.com or a self-managed host:
- An `https://` remote keeps its port.
- An SSH remote (`git@gitlab.example.com:group/repo.git`, `gitlab@gitlab.example.com:group/repo.git` or `ssh://git@gitlab.example.com:2222/group/repo.git`) maps to `https://gitlab.example.com`. The port in an SSH URL belongs to the SSH server, not the web server.
- A plain `http://` remote is refused. The GitLab token is sent only over HTTPS (see [GitHub Enterprise hosts](#github-enterprise-hosts) below).
The block never falls back to gitlab.com. If the checkout has no `origin` remote, or its URL has no host, the block fails before your GitLab token is sent anywhere.
### Pre-filled values
You can pre-populate the PR/MR title, description, labels, branch name, and commit message. Users can still edit these values before creating it.
```mdx
```
> **Note:** The `prefilledPullRequest*` props apply the same way on GitLab. `prefilledPullRequestTitle` becomes the MR title, and so on.
### Template expressions
The `prefilledPullRequestTitle`, `prefilledPullRequestDescription`, `prefilledBranchName`, and `prefilledCommitMessage` props support template expressions that reference inputs and outputs from other blocks. Escape sequences like `\n` are converted to real newlines, which is useful for multi-line descriptions:
```mdx
```
Template expressions are resolved in real-time as upstream blocks produce outputs. If the block references outputs from blocks that haven't run yet, it displays a warning and disables the create button until all dependencies are satisfied.
### Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `string` | Yes | Unique block identifier. Used by downstream blocks to reference outputs. |
| `title` | `string` | No | Display title for the block header. Supports inline markdown and template expressions. Defaults per provider (`Create Pull Request` / `Create Merge Request`). |
| `description` | `string` | No | Description text below the title. Supports inline markdown and template expressions. |
| `provider` | `'github' \| 'gitlab'` | No | Locks the provider. Left unset, the provider is derived as described in [Provider detection](#provider-detection). |
| `inputsId` | `string \| string[]` | No | Reference to one or more [``](/authoring/blocks/inputs) blocks by ID for template variable substitution. When multiple IDs are provided, variables are merged in order (later IDs override earlier ones). |
| `gitAuthId` | `string` | No | Reference to a [``](/authoring/blocks/gitauth) block (either provider). The block waits for authentication to complete and uses its token/provider for this execution. |
| `githubAuthId` | `string` | No | Reference to a [``](/authoring/blocks/githubauth) block. GitHub only. Prefer `gitAuthId`. |
| `prefilledPullRequestTitle` | `string` | No | Pre-fills the title field. Supports template expressions. |
| `prefilledPullRequestDescription` | `string` | No | Pre-fills the description field. Supports template expressions, markdown, and `\n` for newlines. |
| `prefilledPullRequestLabels` | `string[]` | No | Pre-selects labels by name. Labels must exist in the repository. |
| `prefilledBranchName` | `string` | No | Pre-fills the branch name. Supports template expressions. Defaults to `runbook/<timestamp>`. |
| `prefilledCommitMessage` | `string` | No | Pre-fills the commit message field. Supports template expressions. |
### Block outputs
After a successful create, the block produces outputs that can be referenced by downstream blocks:
| Output | Description | Example |
|--------|-------------|---------|
| `PR_ID` | The pull request number, or the GitLab merge request **iid** | `42` |
| `PR_URL` | The full URL of the created pull/merge request | `https://github.com/org/repo/pull/42` |
Reference outputs in downstream blocks using template variables:
```mdx
```
> **Note:** Block IDs with hyphens are converted to underscores in template variables. For example, `create-pr` becomes `create_pr`.
### Git Push
After creating a PR/MR, the block shows a **Git Push** button. It runs `git push -u origin <branch>` for the PR/MR's branch with the linked auth block's token. It does not stage or commit anything, so use it to push commits made since the PR/MR was created, for example by a Command block that ran `git commit`. To commit uncommitted changes onto the open PR/MR, choose "create another" and enter its branch name. See [Creating another PR/MR](#creating-another-prmr).
### Retrying after a failure
If creation fails after the branch was created (for example, the push is rejected or the API call fails), fix the cause and create again with the same branch name. The block picks up on the existing branch: it commits any new changes and pushes, then opens the PR/MR.
You can also retry under a new branch name. The new branch starts from the branch the failed attempt left checked out, so if that attempt's push failed, its commits are pushed under the new name, along with any new changes.
If a local branch with that name already exists but isn't checked out (for example, one left over from an earlier run), the block offers to delete it and retry. The delete is refused if the branch has commits that haven't been pushed or merged; choose a new branch name instead.
The branch name can't be the base branch or a protected name (`main`, `master`, `develop`, `dev`, `staging`, `release`, `prod`, `production`).
### Creating another PR/MR
After a PR/MR is created, its branch stays checked out, and "create another" fills in a new branch name such as `runbook/<timestamp>`. What the next create does depends on the branch name and on whether anything changed since:
- With a new name and new changes, the new branch starts from the open PR/MR's branch, so the new PR/MR also contains the open one's commits.
- With a new name and no new changes, the create is refused with "Nothing to commit". The open PR/MR's commits are already on the remote, and a new PR/MR would only repeat them. Nothing is pushed, and the open PR/MR's branch stays checked out.
- With the open PR/MR's branch name, the block picks up on that branch, commits any new changes and pushes them to it, so they become part of the open PR/MR, and then reports that the PR/MR already exists.
The block shows **Git Push** until you choose "create another".
### Prerequisites
The block needs a completed `` or `` block, for the token, and a `` block that has cloned a repository, which the PR/MR is created against. It checks both and shows an amber warning for whichever is missing. The create button stays disabled until both are met.
For authentication, the block looks for a token output from the block referenced by `gitAuthId` or `githubAuthId`. When that block picked up existing credentials (environment variables or a CLI) rather than running its interactive flow, it registers an `__AUTHENTICATED` marker instead, which also satisfies the check. Until one is present, the block displays "Waiting for <provider> authentication" and names the block to complete.
For the repository, the block checks whether a `GitClone` block has registered an active worktree. Until one has, it displays "No repository available".
### Labels
The block fetches available labels from the repository and presents them in a searchable multi-select dropdown. Labels can also be pre-selected using the `prefilledPullRequestLabels` prop.
On GitHub, labels are applied after the pull request opens. If applying them fails, the pull request is still created and the logs show a warning.
### GitHub Enterprise hosts
GitLab merge requests use the instance of the repository's remote, as described above. On GitHub, the pull request is opened against the host of the cloned repository's `origin` remote: `github.com`, a GitHub Enterprise Server host such as `github.example.com`, or a GHE.com tenant such as `acme.ghe.com`. The block uses that host's API (`https://api.github.com`, `https://<host>/api/v3`, or `https://api.<tenant>.ghe.com`) and fetches labels from it.
That host must be the one the linked auth block authenticated to (its `GITHUB_HOST` output). If they differ, for example a repository cloned from `github.example.com` while the auth block signed in to `github.com`, the block fails with an error. Links to token settings shown by the block also follow the authenticated host.
For both providers, the token is sent only over HTTPS, and only to the host the linked auth block authenticated to (the `GITHUB_HOST` or `GITLAB_HOST` it set). Runbooks never assumes `github.com` or `gitlab.com`. The block fails before sending the token if `origin` is on another host, is a plain `http://` URL, is missing, or names no host Runbooks can read (a local path, or an address it can't parse). Point `origin` at the repository on your GitHub or GitLab host (an `https://` URL or a `git@<host>:<owner>/<repo>.git` address) and try again.
```mdx
```
See [GitHub Enterprise support](/authoring/blocks/githubauth/#github-enterprise-support) for configuring the host.
## Provider-locked aliases
Two aliases lock `` to a single provider:
- [``](/authoring/blocks/githubpullrequest/) is ``.
- [``](/authoring/blocks/gitlabmergerequest/) is ``.
### Example: full workflow
```mdx
> config.hcl"
/>
```
---
#### <GoogleAuth>
The `` block authenticates to Google Cloud with a service account key, an interactive Google sign-in, or an existing local gcloud configuration. Once authenticated, the credentials are available to subsequent [Command](/authoring/blocks/command/) and [Check](/authoring/blocks/check/) blocks.
By default, GoogleAuth looks for existing credentials in environment variables (`GOOGLE_APPLICATION_CREDENTIALS`, `GOOGLE_CREDENTIALS`, and others) and then in the Application Default Credentials file. If it finds any, it validates them and asks the user to confirm before using them. See [Credential detection](#credential-detection).
## Basic usage
```mdx
```
## Authentication methods
The block has three tabs:
| Tab | Description |
|-----|-------------|
| **Service Account Key** | Paste (or load from disk) a service account JSON key |
| **Google Sign-In** | Interactive browser sign-in that produces user credentials, the same thing `gcloud auth application-default login` produces. Requires an OAuth client. See [Google Sign-In](#google-sign-in) |
| **gcloud Config** | Reuse a configuration and Application Default Credentials that already exist on the machine |
All three tabs also offer an optional **Default Region** picker, which seeds the region environment variables for subsequent commands. It starts on `defaultRegion`; choosing **No default region** sets no region at all (on the gcloud Config tab, the configuration's own `compute/region` then applies).
Service Account Key is the tab users land on. Set `defaultTab` to open on a different one:
```mdx
```
The user can still switch tabs.
> **Note:** Runbooks never shells out to the `gcloud` binary. Service account keys are validated by minting a real access token, sign-in uses a loopback OAuth redirect, and the gcloud tab reads gcloud's own configuration files directly. You do not need the Google Cloud CLI installed for any of the three tabs. The gcloud Config tab needs the configuration files a previous `gcloud` run created.
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `id` | `string` | required | Unique identifier for this component |
| `title` | `string` | `"Google Cloud Authentication"` | Display title shown in the UI (supports template expressions) |
| `description` | `string` | - | Description of the authentication purpose (supports template expressions) |
| `project` | `string` | - | Pin a Google Cloud project. When set, project selection is skipped and this project is used (supports template expressions) |
| `defaultRegion` | `string` | - | Default compute region for subsequent commands. Sets `GOOGLE_CLOUD_REGION`, `CLOUDSDK_COMPUTE_REGION`, and `GOOGLE_REGION` |
| `defaultZone` | `string` | - | Default compute zone for subsequent commands. Sets `CLOUDSDK_COMPUTE_ZONE` and `GOOGLE_ZONE` |
| `gcloudConfiguration` | `string` | - | Pre-select a named gcloud configuration in the gcloud Config tab (supports template expressions) |
| `defaultTab` | `'service_account'`, `'oauth'`, or `'gcloud'` | `'service_account'` | Which authentication tab the block opens on. An unrecognized value falls back to `'service_account'` |
| `scopes` | `string[]` | `cloud-platform`, `userinfo.email`, `openid` | OAuth scopes requested by Google Sign-In. When set, also required of any auto-detected or gcloud user ADC this block will accept (service-account keys are exempt) |
| `oauthClientId` | `string` | - | Client ID of a Google Cloud "Desktop app" OAuth client. Must be paired with `oauthClientSecret`. Mutually exclusive with `oauthClientFile`. See [Using your own OAuth client](#using-your-own-oauth-client) |
| `oauthClientSecret` | `string` | - | Client secret issued alongside the Desktop OAuth client. Required whenever `oauthClientId` is set. Per RFC 8252 this value is not confidential. Google issues one with every Desktop client |
| `oauthClientFile` | `string` | - | Path to a Google Cloud Console Desktop-app client JSON download (`client_secret_*.json` with an `installed` object). `~` is expanded. Mutually exclusive with `oauthClientId` / `oauthClientSecret`. Read in the main process only |
| `detectCredentials` | `false \| GoogleCredentialSource[]` | `['env', 'adc']` | Whether and how to detect existing credentials. See [Credential detection](#credential-detection) |
| `inputsId` | `string \| string[]` | - | Reference one or more [Inputs](/authoring/blocks/inputs/) blocks for template expressions in props |
> **Caution:** `detectCredentials` takes a JSX expression. Write `detectCredentials={false}`. `detectCredentials="false"` is the truthy string `"false"` and does not disable detection.
## Environment variables
When authentication succeeds, the following environment variables are set in the session environment for subsequent Command and Check blocks:
| Variable | Value |
|----------|-------|
| `GOOGLE_APPLICATION_CREDENTIALS` | Absolute path to the credentials file backing this session |
| `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE` | The same path, in the property the `gcloud` CLI reads. See [Bridging to the gcloud CLI](#bridging-to-the-gcloud-cli) |
| `GOOGLE_CLOUD_PROJECT` | The selected project ID |
| `CLOUDSDK_CORE_PROJECT` | The selected project ID (read by the `gcloud` CLI) |
| `GOOGLE_PROJECT` | The selected project ID (read by the OpenTofu/Terraform `google` provider) |
| `CLOUDSDK_CORE_ACCOUNT` | The authenticated principal, a service account or user email address |
These are set only when the corresponding value is known:
| Variable | Source |
|----------|--------|
| `GOOGLE_CLOUD_REGION`, `CLOUDSDK_COMPUTE_REGION`, `GOOGLE_REGION` | `defaultRegion`, the Default Region picker, or else the gcloud configuration's `compute/region` (or, for a credential detected in environment variables, their region) |
| `CLOUDSDK_COMPUTE_ZONE`, `GOOGLE_ZONE` | `defaultZone`, or else the gcloud configuration's `compute/zone` (or the detected environment's zone) |
| `CLOUDSDK_ACTIVE_CONFIG_NAME` | gcloud Config tab only. The name of the configuration you authenticated with |
Google client libraries and the OpenTofu/Terraform `google` provider read `GOOGLE_APPLICATION_CREDENTIALS`. The `gcloud` CLI reads `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE`, which GoogleAuth also sets. See [Bridging to the gcloud CLI](#bridging-to-the-gcloud-cli).
> **Note:** Runbooks does not write key material into an environment variable. `GOOGLE_APPLICATION_CREDENTIALS` points at a file: either a credentials file that already existed on your machine, or a `0600` file written to a private temporary directory that is deleted when the app quits. The exception is a credential that is only an access token (see [Access token credentials](#access-token-credentials)).
### Bridging to the gcloud CLI
Application Default Credentials (`GOOGLE_APPLICATION_CREDENTIALS`) and the `gcloud` CLI's own login state (`gcloud auth login`, stored in a local `credentials.db`) are two separate credential stores. When the CLI has any account configured, it prefers that account over ADC and ignores `GOOGLE_APPLICATION_CREDENTIALS`. Without the bridge, a bare `gcloud` command in a `` block would use that CLI login, which may have expired, and fail with an error such as:
```
ERROR: (gcloud.organizations.list) UNAUTHENTICATED: Request had invalid authentication
credentials. Expected OAuth 2 access token, login cookie or other valid authentication
credential.
```
Whenever GoogleAuth writes or points at a credentials file, it also sets `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE` to the same path, so the CLI uses that file and ignores its own login state. Re-authenticating, including with a different credential type, updates it.
> **Caution:** The bridge needs a credentials file, and an [access-token-only credential](#access-token-credentials) has none. In that case, bare `gcloud` commands use whatever CLI login is already configured on the machine. Do not work around this by exporting a bearer token as `CLOUDSDK_AUTH_ACCESS_TOKEN` yourself. Some older Google APIs (for example Cloud Resource Manager v1's `Organizations.SearchOrganizations`, which `gcloud organizations list` calls) reject a bearer presented that way with `ACCESS_TOKEN_TYPE_UNSUPPORTED`.
## Block outputs
The block also publishes its result as block outputs, so a specific GoogleAuth block can be referenced by ID:
| Output | Value |
|--------|-------|
| `GOOGLE_APPLICATION_CREDENTIALS` | Path to the credentials file for this block |
| `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE` | The same path, in the property the `gcloud` CLI reads. Blank for an [access-token-only](#access-token-credentials) credential |
| `GOOGLE_CLOUD_PROJECT`, `CLOUDSDK_CORE_PROJECT`, `GOOGLE_PROJECT` | The selected project ID |
| `CLOUDSDK_CORE_ACCOUNT` | The authenticated principal |
| `GOOGLE_CLOUD_REGION`, `CLOUDSDK_COMPUTE_REGION`, `GOOGLE_REGION` | The region the block wrote to the session (see [above](#environment-variables)), if any |
| `CLOUDSDK_COMPUTE_ZONE`, `GOOGLE_ZONE` | The zone the block wrote to the session, if any |
| `GOOGLE_AUTH_TYPE` | How the credential was obtained: `service_account`, `authorized_user`, `external_account`, `impersonated_service_account`, or `access_token` |
| `__AUTHENTICATED` | Always `true` once the block has authenticated |
## Using with Commands and Checks
Subsequent Command and Check blocks get the [environment variables above](#environment-variables), so they use the most recently authenticated GoogleAuth block. A later Google Cloud authentication becomes the new default and overwrites the variables its own credential sets. So does picking a project with **Change project** on a block's success card. Variables it does not set can remain from an earlier block. See [Multiple projects](#multiple-projects).
To bind a block to one specific GoogleAuth block, use the `googleAuthId` prop. The referenced block's outputs are injected into that execution only, and the **Run** button stays disabled until that block has authenticated:
```mdx
```
> **Tip:** Without `googleAuthId`, subsequent blocks still have Google Cloud access through the session environment variables.
## Multiple projects
You can include multiple GoogleAuth blocks in a single runbook to authenticate against different projects, or with different principals:
```mdx
```
> **Caution:** With multiple GoogleAuth blocks, the block that most recently authenticated or changed project is the session default. Becoming the default switches only the session variables that block's own credential sets:
- Its credentials file and `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE`. For an [access-token-only](#access-token-credentials) credential, its access token is set and the override is cleared.
- Its account, and its project if it has one.
- The region and zone it authenticated with.
- `CLOUDSDK_ACTIVE_CONFIG_NAME`, if it used a gcloud configuration.
Variables that block does not set stay from whichever block set them last:
- `GOOGLE_OAUTH_ACCESS_TOKEN` and `CLOUDSDK_AUTH_ACCESS_TOKEN` from an access-token block. The OpenTofu/Terraform `google` provider prefers that token over `GOOGLE_APPLICATION_CREDENTIALS`.
- `CLOUDSDK_ACTIVE_CONFIG_NAME` from a block that used a gcloud configuration.
- `GOOGLE_CLOUD_PROJECT`, `CLOUDSDK_CORE_PROJECT` and `GOOGLE_PROJECT`, when the block authenticated with no project (for example, Google Sign-In with no `project` prop and no project the account can list).
- A region or zone the block did not set.
- For an access-token block, the `GOOGLE_APPLICATION_CREDENTIALS` of the last file-backed credential (another block's, or its own before it switched to a token), which Google client libraries read.
So with several GoogleAuth blocks, use `googleAuthId` on every command. It injects the referenced block's credentials file, `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE`, account, and any project, region or zone it set into that command. It does not unset the variables listed above, so a runbook that mixes an access-token credential with file-backed ones can still hand a `googleAuthId` command another block's credential. Each block keeps its own credentials file, so the two never collide.
## Service Account Key
The **Service Account Key** tab accepts the full JSON key file that Google Cloud produces for a service account, two ways:
- **Paste it** into the field, which is masked with a reveal toggle.
- **Choose key file...** opens a native file picker. Only the path is recorded. The app's main process reads and validates the key, and the interface never loads it.
The two are alternatives: choosing a file clears a pasted key, and typing into the field clears a chosen file.
Two optional fields refine the result:
- **Project ID** overrides the project. It starts out showing the `project` prop, if set. Leave it blank to use the key's own `project_id`.
- **Default Region** sets the region environment variables.
The key is validated by exchanging it for a real access token, so an expired, revoked, disabled, or malformed key fails immediately with the reason shown inline. On success, `GOOGLE_APPLICATION_CREDENTIALS` points at the key: a file you chose is used where it already lives, and a pasted key is written to a private `0600` file that is deleted when Runbooks quits.
> **Caution:** A service account key is long-lived key material. Prefer **Google Sign-In** or the **gcloud Config** tab for interactive use, and reserve key files for automation contexts where nothing else is available.
## Google Sign-In
> **Caution:** This tab requires a Google Cloud "Desktop app" OAuth client, and Runbooks builds include none. Until you supply one through props, a client JSON file, operator environment variables, or the in-session file picker, the tab is labeled *(needs OAuth client)* and sign-in cannot start. See [Using your own OAuth client](#using-your-own-oauth-client). The Service Account Key and gcloud Config tabs need no client.
The **Google Sign-In** tab performs the same loopback OAuth flow as `gcloud auth application-default login`:
1. Runbooks opens a listener bound to `127.0.0.1` on an ephemeral port and opens your browser at Google's consent screen (PKCE, S256).
2. You approve the requested scopes in the browser.
3. Google redirects back to the loopback listener, Runbooks exchanges the code for a refresh token, writes a user-credentials file, and closes the listener.
The requested scopes default to:
- `https://www.googleapis.com/auth/cloud-platform`
- `https://www.googleapis.com/auth/userinfo.email`
- `openid`
Override them with the `scopes` prop. The tab's **What permissions does this grant?** disclosure lists what will be requested.
When `scopes` is set, existing credentials must grant them too. Auto-detected env/ADC credentials and the gcloud Config tab refuse a user credential whose tokeninfo grant is missing any listed scope. The block shows the missing scopes and offers **Sign in with required scopes**, or a copyable `gcloud auth application-default login --client-id-file="$GOOGLE_OAUTH_CLIENT_CREDENTIALS" --scopes=...` command when Sign-In has no Desktop client. The default scopes apply to Sign-In requests only and are not enforced on existing ADC.
```mdx
```
The flow returns a refresh token, so the credential does not expire mid-runbook. The refresh token is written to a `0600` file and is not sent to the UI.
### Using your own OAuth client
Sign-in requires a registered Google Cloud Desktop app OAuth client. The main process looks for one in this order:
1. Author props `oauthClientId` + `oauthClientSecret`
2. Author prop `oauthClientFile` (path to the Console download JSON)
3. Operator env `GOOGLE_OAUTH_CLIENT_CREDENTIALS` (path to the same JSON)
4. Operator env `GOOGLE_OAUTH_CLIENT_ID` + `GOOGLE_OAUTH_CLIENT_SECRET`
5. A client compiled into the build. Runbooks builds include none.
To pass the ID and secret as props:
```mdx
```
Both props are required together. `oauthClientId` on its own is refused with an error. Google issues a client secret with every Desktop client, and a user-credentials file written without it cannot be refreshed, so every later `gcloud`, client-library, and OpenTofu call would fail at the first token refresh.
To pass the client JSON file, the Desktop-app download from Google Cloud Console (`{ "installed": { "client_id", "client_secret", ... } }`):
```mdx
```
`oauthClientFile` is mutually exclusive with `oauthClientId` / `oauthClientSecret`. The main process reads the file (`~` is expanded), and the secret is not sent to the UI. Web-client downloads (`{ "web": ... }`) are rejected because Sign-In uses a loopback redirect registered for Desktop clients.
The operator can set a client in the environment of the machine, with no change to the runbook:
| Variable | Value |
|----------|-------|
| `GOOGLE_OAUTH_CLIENT_CREDENTIALS` | Absolute or `~/...` path to a Desktop-app `client_secret_*.json` |
| `GOOGLE_OAUTH_CLIENT_ID` | Desktop-app client ID (must be paired with the secret) |
| `GOOGLE_OAUTH_CLIENT_SECRET` | Desktop-app client secret |
These are separate from `GOOGLE_APPLICATION_CREDENTIALS`, which points at the user or service-account credentials produced by authentication.
When no client is configured, the **Google Sign-In** tab stays selectable and is labeled *(needs OAuth client)*. Its panel offers **Choose Desktop OAuth client JSON**, which takes the same Console download as `oauthClientFile` and `GOOGLE_OAUTH_CLIENT_CREDENTIALS`. The interface keeps only the path. The main process reads `installed.client_id` and `installed.client_secret` when sign-in starts.
## gcloud Config
The **gcloud Config** tab lists the configurations found in your gcloud configuration directory:
| Platform | Location |
|----------|----------|
| macOS / Linux | `~/.config/gcloud` |
| Windows | `%APPDATA%\gcloud` |
| Any | `$CLOUDSDK_CONFIG`, when set |
Each row shows the configuration's account, project, and a badge describing what it can authenticate with:
| Badge | Meaning |
|-------|---------|
| **User ADC** | Application Default Credentials from `gcloud auth application-default login` |
| **Service Account ADC** | Application Default Credentials backed by a service account key |
| **Federated ADC** | Workforce-pool sign-in or an impersonated service account. Workload identity federation that sources its subject token from a file, a URL, or an executable (`credential_source`) is listed but rejected on use. See [Federated credential limits](#federated-credential-limits) |
| **No ADC** | A configuration exists, but there are no Application Default Credentials to use. Not selectable |
| **Unsupported** | Nothing usable could be read from this configuration |
A gcloud configuration records which account and project to use. The credential itself is in `application_default_credentials.json`. Rows marked **No ADC** are listed but not selectable, with a hint to run:
```bash
gcloud auth application-default login
```
Authenticating from this tab copies nothing. `GOOGLE_APPLICATION_CREDENTIALS` points at your existing credentials file, and `CLOUDSDK_ACTIVE_CONFIG_NAME` records the configuration name.
Pin a configuration with the `gcloudConfiguration` prop:
```mdx
```
> **Note:** Only `application_default_credentials.json` is read. The `gcloud` CLI keeps its own login state in a `credentials.db` SQLite database, which Runbooks does not open, so being logged in with `gcloud auth login` alone is not enough. Run `gcloud auth application-default login` as well.
## Project selection
The project decides what your commands operate on.
- If the `project` prop is set, that project is pinned and no picker is shown.
- Otherwise, on the Service Account Key tab the key's own `project_id` is used when present.
- Otherwise, if the credential can see exactly one project, it is selected automatically.
- Otherwise, a searchable project picker is shown before the block reports success.
Once authenticated, the success card offers **Change project** to switch to another project the same credential can see, and **Re-authenticate** to start over.
Picking a project makes the block the session default again. The session variables its own credential sets switch together: its credentials file and `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE` (or its access token), its account, and the new project. The block keeps the region and zone it authenticated with. Variables it does not set can remain from another GoogleAuth block. See [Multiple projects](#multiple-projects).
**Cancel** in a picker opened by **Change project** returns to the success card and keeps the current project. If the project list cannot be loaded, the picker shows why (*"Could not list projects: ..."*).
> **Note:** Listing projects requires the `resourcemanager.projects.list` permission on some resource the credential can see. A service account without it authenticates but shows an empty picker. In that case, set the `project` prop or type the project ID on the Service Account Key tab.
## Credential detection
By default GoogleAuth checks for credentials that already exist on the machine, then asks the user to confirm them. Detection is read-only. Nothing is written to the session, and no outputs are published, until the user clicks **Use These Credentials**.
The confirmation card shows the project, the principal, the credential type, the quota project when known, and where the credential came from (the environment variable name, the credentials file path, or the gcloud configuration name).
> **Note:** GitAuth uses detected credentials without asking. GoogleAuth always asks for confirmation, so the user sees which project they are about to operate in.
### Credential sources
`detectCredentials` accepts an array of sources, tried in the order written until one succeeds:
| Source | Reads |
|--------|-------|
| `'env'` | Environment variables (see below) |
| `{ env: { prefix: 'PREFIX_' } }` | The same variables with a prefix, e.g. `PREFIX_GOOGLE_APPLICATION_CREDENTIALS` |
| `'adc'` | The well-known `application_default_credentials.json` under the gcloud configuration directory |
| `'gcloud'` | The active gcloud configuration: its account, project, and compute defaults, with that same credentials file |
| `{ block: 'block-id' }` | Credentials published by an earlier Command block. Only one block source is allowed per GoogleAuth block |
The `'env'` source reads, in precedence order:
| Purpose | Variables |
|---------|-----------|
| Credential | `GOOGLE_APPLICATION_CREDENTIALS` (a path), then `GOOGLE_CREDENTIALS` (inline JSON), then `GOOGLE_OAUTH_ACCESS_TOKEN`, then `CLOUDSDK_AUTH_ACCESS_TOKEN` |
| Project | `CLOUDSDK_CORE_PROJECT`, `GOOGLE_CLOUD_PROJECT`, `GOOGLE_PROJECT`, `GCLOUD_PROJECT` |
| Region | `CLOUDSDK_COMPUTE_REGION`, `GOOGLE_CLOUD_REGION` |
| Zone | `CLOUDSDK_COMPUTE_ZONE` |
A project ID on its own is not a credential. Detection reports a find only when one of the credential variables is set. A credentials file may hold either a service account key or user credentials. The file's `type` field decides which.
### Default behavior
With no configuration, GoogleAuth checks environment variables and then Application Default Credentials:
```mdx
{/* Same as detectCredentials={['env', 'adc']} */}
```
### Disable auto-detection
Force manual authentication only:
```mdx
```
### Include the active gcloud configuration
```mdx
```
### Custom detection with a prefix
Check the standard variables with a custom prefix. The example below looks for `PROD_GOOGLE_APPLICATION_CREDENTIALS`, `PROD_GOOGLE_CREDENTIALS`, `PROD_CLOUDSDK_CORE_PROJECT`, and so on:
```mdx
```
Prefixes must follow these rules:
- Uppercase letters, numbers, and underscores only
- Must start with a letter
- Must end with a trailing underscore (e.g. `PROD_`, `MY_APP_`)
An invalid prefix is rejected and reported inline. The block does not fall back to the unprefixed variables.
### Test prefix
To use a different prefix for automated tests, set `env_prefix` on the test step in `runbook_test.yml`:
```yaml
# runbook_test.yml
steps:
- block: google-auth
env_prefix: CI_ # Checks CI_GOOGLE_APPLICATION_CREDENTIALS, CI_GOOGLE_CLOUD_PROJECT, etc.
expect: success
```
See [Testing](/authoring/testing/) for details.
### From Command output
Use credentials produced by a previous Command block, for example a script that impersonates a service account or exchanges a workload identity token:
```mdx
```
The script writes credentials to the runbook output file:
```bash
#!/usr/bin/env bash
set -euo pipefail
gcloud iam service-accounts keys create /tmp/deploy-key.json \
--iam-account "deployer@my-project-123456.iam.gserviceaccount.com"
echo "GOOGLE_APPLICATION_CREDENTIALS=/tmp/deploy-key.json" >> "$RUNBOOK_OUTPUT"
echo "GOOGLE_CLOUD_PROJECT=my-project-123456" >> "$RUNBOOK_OUTPUT"
```
Accepted output keys are `GOOGLE_APPLICATION_CREDENTIALS` (a path), `GOOGLE_CREDENTIALS` (inline JSON), or `GOOGLE_OAUTH_ACCESS_TOKEN` / `CLOUDSDK_AUTH_ACCESS_TOKEN`. The project falls back to `CLOUDSDK_CORE_PROJECT`, `GOOGLE_CLOUD_PROJECT`, `GOOGLE_PROJECT`, and finally the `project` prop.
When a script writes the credential itself, rather than a path to it, mark it sensitive, as in `echo "sensitive:GOOGLE_OAUTH_ACCESS_TOKEN=$TOKEN" >> "$RUNBOOK_OUTPUT"`. The Command's **View Outputs** then masks it, and GoogleAuth still reads it by its plain name. See [Sensitive outputs](/authoring/inputs-and-outputs/#sensitive-outputs).
If the referenced block has not run yet, GoogleAuth shows *Waiting for "impersonate-sa" to run...* and pauses detection there. It does not skip to a later source.
> **Caution:** Only one `{ block: 'id' }` entry is allowed per GoogleAuth block. More than one is a configuration error, and the block renders an error card in place of the authentication UI.
### Access token credentials
If the only credential found is an access token (`GOOGLE_OAUTH_ACCESS_TOKEN` or `CLOUDSDK_AUTH_ACCESS_TOKEN`), confirming it re-exports that same value under both canonical names and writes no `GOOGLE_APPLICATION_CREDENTIALS`. Runbooks does not mint a bearer token of its own, because access tokens expire after about an hour, which would break a long runbook halfway through.
With no credentials file, GoogleAuth cannot [bridge to the `gcloud` CLI](#bridging-to-the-gcloud-cli). `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE` is left unset, and a bare `gcloud` command uses whatever CLI login is already configured on the machine. Client libraries, OpenTofu/Terraform, and any script that reads `GOOGLE_OAUTH_ACCESS_TOKEN` directly are unaffected.
### When users reject detected credentials
Detected credentials are not registered as this block's credentials until the user confirms them. When a user clicks **Use Different Credentials**:
1. The manual authentication tabs are shown
2. The user must authenticate manually before the block reports success, publishes outputs, or satisfies a `googleAuthId` dependency
A **Try auto-detection again** link re-runs detection. If nothing is found, a *No credentials found* hint appears.
If a credential is found but does not validate, the block shows *"Invalid credentials detected: Application Default Credentials are invalid or expired"*, followed by the underlying reason in parentheses (for example, a file that does not exist), and falls through to manual authentication.
## Instruction mode
In [instruction mode](/intro/instruction-mode/), GoogleAuth captures nothing. It renders a single instruction, *"Log into Google Cloud"*, or *"Log into Google Cloud in the `my-project-123456` project"* when `project` is set. The configured project, region, gcloud configuration, and scopes are listed as notes, along with the equivalent command to run by hand. When the author sets `scopes`, that command includes `--scopes=...`:
```bash
gcloud auth application-default login --scopes=https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/userinfo.email,openid
```
No detection runs, no browser opens, and no credentials are read or written.
## Security
### Credential handling
- **Credentials stay local.** All authentication happens between your machine and Google. Nothing is transmitted to Gruntwork.
- **Key material never reaches the UI.** Service account keys, refresh tokens, and access tokens are handled entirely in the app's main process. Every result the interface receives is metadata: a principal, a project, a credential type, and a file path.
- **Credentials are files.** `GOOGLE_APPLICATION_CREDENTIALS` points at either a file you already had, or a `0600` file in a private temporary directory. Those temporary files are deleted when Runbooks quits, and re-authenticating a block replaces that block's previous file. Files are tracked per block, so one GoogleAuth block never releases a file another block is still publishing.
- **Validation before use.** Every credential is exchanged for a real access token before it is registered, so an expired or revoked credential fails at authentication.
- **Federated credentials are checked before they are used.** A service account key and a user credential are inert data, but an `external_account` or `impersonated_service_account` document names the endpoints Google's auth library must call and the local file it must read. Runbooks rejects any such document that points anywhere other than `*.googleapis.com` over HTTPS, and rejects `credential_source` entirely, so a credentials file from an untrusted source cannot make the block send a request or read a file on someone else's behalf. Workforce pool sign-in (`gcloud auth application-default login`) and impersonation chains are unaffected. File-, URL-, and executable-sourced workload identity federation is not accepted.
- **Errors are redacted.** Error messages that could echo key material are scrubbed before they are displayed.
### Confirmation, and what it does not do
Confirmation controls what this block registers and publishes. No session variables are written and no outputs are produced until the user confirms.
> **Caution:** It does not remove Google Cloud variables that your shell already exported. If you launch Runbooks from a terminal where `GOOGLE_APPLICATION_CREDENTIALS` is set, scripts inherit that value like any other environment variable, whether or not you confirm anything. To guarantee a command runs with the credentials a specific block produced, reference it with `googleAuthId`.
Within a Runbooks session, GoogleAuth updates `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE` every time it authenticates, including when it switches between a file-backed credential and an access token, so re-authenticating leaves no value from an earlier credential the block set. The same caveat applies to this variable. A value already present in the shell Runbooks was launched from stays in the session until a GoogleAuth block in this runbook authenticates and overwrites or clears it.
### Federated credential limits
`external_account`, `external_account_authorized_user` and `impersonated_service_account` documents are instructions. The document names the URL the auth library must call, the headers it must send, and, through `credential_source`, the local file it must read or the program it must run. The library performs no validation of its own, so Runbooks validates them before they are used:
- Every absolute URL anywhere in the document must be `https:` and must resolve to a host under `googleapis.com` (`accounts.google.com` is also permitted, since every issued service account key carries it as `auth_uri`).
- `universe_domain` must be `googleapis.com`, at every level of a `source_credentials` chain.
- `credential_source` is rejected outright.
Workload identity federation that sources its subject token from a file, a URL, or an executable will not authenticate here. Accepting it would let a pasted document read an arbitrary local file and send it to a host of its choosing. Workforce-pool sign-in (`gcloud auth application-default login` against a workforce pool) has a refresh token and no `credential_source`, so it works, as does an impersonation chain over a service account key.
If a federated configuration you rely on is refused, authenticate with a service account key or `gcloud auth application-default login` instead.
## Troubleshooting
| Symptom | Cause and fix |
|---------|---------------|
| **Google Sign-In** tab labeled *(needs OAuth client)* | No OAuth client is configured yet (build default, author props/file, or `GOOGLE_OAUTH_CLIENT_*` env). Choose a Desktop client JSON in the panel, set env, or use the Service Account Key / gcloud Config tab |
| *"oauthClientId was supplied without oauthClientSecret"* | A custom Desktop client needs both. Google issues a secret with every Desktop client; without it the credential cannot be refreshed |
| *"Supply either oauthClientId/oauthClientSecret or oauthClientFile"* | Those props are mutually exclusive. Pick one |
| *"OAuth client credentials file is a Web client"* | Download a **Desktop app** client JSON from Google Cloud Console (`installed`), not a Web client (`web`) |
| *No gcloud configurations found* | There is no gcloud configuration directory at the reported path. Run `gcloud init`, or set `CLOUDSDK_CONFIG` to the directory you use |
| A configuration shows **No ADC** and cannot be selected | The configuration has no Application Default Credentials. Run `gcloud auth application-default login` |
| *Not a service account key (expected type: service_account)* | The pasted JSON is user credentials or an OAuth client file, not a service account key. Use the file that Google Cloud produced from **IAM & Admin > Service Accounts > Keys** |
| *...which Runbooks does not accept* / *...is not a Google API endpoint* | The credentials document is a workload identity federation config that fetches its subject token from a file, a URL, or a command, or that points at a host outside `*.googleapis.com`. Runbooks does not run those instructions. Use a service account key, **Google Sign-In**, or `gcloud auth application-default login` |
| *Invalid credentials detected: ... are invalid or expired (...)* | The detected credential does not authenticate; the text in parentheses says why. For a missing or unreadable file, fix the path the variable points at. For an expired or revoked credential, refresh it (`gcloud auth application-default login`). Or authenticate manually |
| *Credentials missing required scopes* | The block's `scopes` prop lists scopes the detected user ADC does not grant (for example Admin SDK scopes). Use **Sign in with required scopes**, or run the shown `gcloud auth application-default login --client-id-file="$GOOGLE_OAUTH_CLIENT_CREDENTIALS" --scopes=...` command and try auto-detection again |
| gcloud Config / confirm fails with *missing required OAuth scopes* | Same check as detection: the selected ADC is too narrow for this block. Re-authenticate with the required scopes via Sign-In or `gcloud auth application-default login --client-id-file=... --scopes=...` |
| The project picker is empty | The credential cannot list projects. Grant `resourcemanager.projects.list`, or set the `project` prop / type the project ID directly |
| *Could not list projects: ...* | Google refused or failed the project listing (for example, the Cloud Resource Manager API is disabled). The current authentication is untouched: **Cancel** returns to the success card. Fix the cause shown, or set the `project` prop |
| *Project ... is not accessible with these credentials* | Advisory warning: the credential authenticated, but Google definitively refused (404/permission denied) on that project. Check the project ID and the principal's IAM roles. An inconclusive answer, such as a disabled Cloud Resource Manager API or a network error, produces no warning, because it says nothing about whether your commands will work |
| *Authenticated, but no Google Cloud project is set* | The credential is valid but nothing named a project: no `project` prop, no `core/project` in the gcloud configuration, and no project the principal can enumerate. Commands that need one will fail with *"The project property must be set"*, unless an earlier GoogleAuth block set a project: then they use that block's project, even with `googleAuthId` (see [Multiple projects](#multiple-projects)). Set the `project` prop, run `gcloud config set project`, or use **Change project** |
| *Authenticated, but the credential could not be saved to the session* | The credential is valid but the session write failed. Re-run the block; blocks that consume it may not see the credentials until you do |
| A Command's **Run** button stays disabled | It has a `googleAuthId` pointing at a GoogleAuth block that has not authenticated yet, or at an ID that does not exist. Check the spelling |
| Commands use the wrong project | The session's project variables come from the GoogleAuth block that most recently authenticated with a project or changed project. A block that authenticated with none leaves the previous block's project in place, and a `googleAuthId` command that references it gets that project too, because a blank project is not injected. Add `googleAuthId` to every command that must use a specific block, and make sure that block has a project: set its `project` prop or use **Change project** |
| `gcloud` commands ignore the selected project | Some scripts pass `--project` explicitly. Runbooks sets `CLOUDSDK_CORE_PROJECT`, which only applies when the flag is absent |
| Bare `gcloud` commands fail with *"UNAUTHENTICATED"* or use the wrong account | The machine has a pre-existing `gcloud auth login` session that the CLI prefers over ADC. GoogleAuth sets `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE` to prevent this. See [Bridging to the gcloud CLI](#bridging-to-the-gcloud-cli). This can still happen for an [access-token-only](#access-token-credentials) credential, which has no file to bridge with |
## Detailed example
See the [google-auth feature demo](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/google-auth) for a complete walkthrough of the GoogleAuth block.
---
#### <Iframe>
The `` block embeds a web page in your runbook. It can show an external site, such as a dashboard or a local dev server, or an HTML page that ships with the runbook in its `assets/` folder.
The page doesn't load when the runbook opens. The block shows a **Load page** button, so a runbook can't run its author's scripts until the user asks for it. Once loaded, the page stays loaded until the app quits, including across live reloads while you edit the runbook.
The page has its own browser session. It can't read what you type into the runbook's other blocks, and it has no access to the Runbooks app.
## Basic usage
Embed an external site:
```mdx
```
Embed a page from the runbook's `assets/` folder:
```mdx
```
## Props
### Required props
- `src` (string). The page to load: an `https://` URL, an `http://` URL on `localhost` or `127.0.0.1` (such as a local dev server), or a path that starts with `./assets/` for a file in the runbook's `assets/` folder.
### Optional props
- `title` (string). Label shown above the frame. Screen readers announce it too. The title bar always shows the frame's host, or its `./assets/` path, next to the label.
- `height` (number or string). Height of the frame. A number, or a string of digits, is a pixel count (`height={600}` or `height="600"`). Any other string must be a CSS length (`height="70vh"`). Defaults to 500 pixels.
- `id` (string). Block ID. Required with `outputs`, because later blocks read the page's outputs through it.
- `inputsId` (string or array). [Inputs](/authoring/blocks/inputs/) block IDs whose values a local page receives. See [Exchanging values with a local page](#exchanging-values-with-a-local-page).
- `outputs` (array of strings). Names of the outputs a local page may set, such as `outputs={["region"]}`. Names use letters, digits and underscores, and start with a letter or underscore.
The frame always fills the width of the runbook.
## Local pages
A local page loads with every file it references, so a static site built into a folder works as long as it sits under `assets/`:
```
my-runbook/
├── runbook.mdx
└── assets/
└── dashboard/
├── index.html
├── app.js
└── style.css
```
Relative references such as `<script src="app.js">` or `href="./style.css"` resolve against the page's own folder. A path that starts with `/` resolves against the `assets/` folder instead, so `/app.js` in the example above loads `assets/app.js`, not `assets/dashboard/app.js`. Build the site with relative paths. For a Vite build, set `base: './'`.
A local page can only load files inside the runbook's `assets/` folder. It can't read `runbook.mdx`, generated files, or anything else in the runbook's folder. It can link to other pages in `assets/`, but not navigate to an external site.
`assets/` must be a real folder. If `assets` is a symlink, Runbooks serves nothing from it, so local pages, images and videos all fail to load. A symlink to a file inside `assets/` works as long as it points to another file inside `assets/`.
Each runbook's local pages get their own origin. A page can keep data in `localStorage` or other browser storage, and it's still there the next time the runbook opens, but pages from other runbooks can't read it.
## Exchanging values with a local page
A page from `assets/` can receive values from the runbook and send values back, so it can act as a custom form or picker. External sites can't do either, and setting `inputsId` or `outputs` on a block with an external `src` is a configuration error.
````mdx
```yaml
variables:
- name: environment
type: string
default: staging
```
````
### Receiving input values
With `inputsId`, the page receives the values of those Inputs blocks as a message. It arrives when the page loads, again whenever the values change, and whenever the page asks for it:
```js
window.addEventListener("message", (event) => {
// Only the runbook sends input values, but a frame inside this page, such
// as a site it embeds, can post to it too.
if (event.source !== parent) return
if (event.data?.type === "runbooks:inputs") {
console.log(event.data.inputs.environment) // "staging"
}
})
// Ask for the current values, for example once a script loaded with `async` or `type="module"` is ready.
parent.postMessage({ type: "runbooks:get-inputs" }, "*")
```
A standalone `` block sends a change after the user clicks **Submit**.
### Setting outputs
The page sets outputs by posting a message to the runbook:
```js
parent.postMessage({ type: "runbooks:set-outputs", outputs: { region: "eu-west-1" } }, "*")
```
Later blocks read them like any block's outputs: `{{ .outputs.region_picker.region }}`, with hyphens in the block ID turned into underscores. Each message adds to or replaces the outputs the page set before. The block shows the current outputs under the frame.
The block refuses a whole message, and shows why under the frame, when any of these is true:
- It names an output that the `outputs` prop doesn't list.
- A value isn't a string.
- A value is longer than 65,536 characters.
The block only accepts messages that the page itself posts. Messages from a frame inside the page, such as a site it embeds, are ignored, and that frame receives no input values.
The page gets the input values it asks for, and it can send them anywhere. Only give a page the Inputs it needs.
To test a runbook whose later blocks read an Iframe's outputs, see [Testing Iframe blocks](/authoring/testing/#testing-iframe-blocks).
## External sites
An external page can navigate to other `https://` pages, and to `http://` pages on `localhost` or `127.0.0.1`. It can't load files from the runbook's `assets/` folder.
External pages share one browser session, kept between app restarts, so signing in to a site in one runbook signs you in wherever it's embedded. That session is separate from your web browser's.
Runbooks loads the page as a top-level page rather than a frame, so sites that forbid framing with an `X-Frame-Options` or `Content-Security-Policy: frame-ancestors` header still load.
## Behavior
- The title bar shows the host the frame started on. The page can navigate itself elsewhere after it loads.
- A host too long for the title bar is shortened from the start, so its end, which names the site, stays visible.
- The title bar has a **Reload** button that loads `src` again, and for external sites an **Open in browser** button.
- The embedded page gets keyboard input only after you click into it. It can't move focus into itself while you type elsewhere in the runbook.
- The embedded page can't open new windows. Links with `target="_blank"` and calls to `window.open` do nothing. Use the **Open in browser** button to open an external site in your default browser.
- The embedded page can't show dialogs or start downloads. `alert()`, `confirm()` and `prompt()` return at once, and a download is cancelled.
- The embedded page runs its own scripts, but it can't navigate the runbook away or call into the Runbooks app.
- The embedded page gets no browser permissions. Requests for the microphone, camera, location, notifications, fullscreen, USB devices and the like are denied without asking.
- Runbooks never sends a client certificate, so a site that requires one to sign in won't load.
---
#### <Inputs>
The `` block renders a form to collect user input. [Command](/authoring/blocks/command/), [Check](/authoring/blocks/check/), and [Template](/authoring/blocks/template/) blocks use the collected values for variable substitution.
## Basic usage
`````mdx
```yaml
variables:
- name: ProjectName
type: string
description: Name of your project
validations:
- required
```
`````
## vs. Template
Inputs and [Template](/authoring/blocks/template/) both render forms to collect variable values:
| Feature | `` | `` |
|---------|------------|--------------|
| Collects variables | Yes | Yes |
| Generates files | No | Yes |
| Button text | "Submit" | "Generate" |
| Use case | Collect values for other blocks | Generate files from a Boilerplate template directory |
## Props
### Required props
- `id` (string). Unique identifier for this component. Other blocks reference this ID via `inputsId` to access the collected values.
### Optional props
- `path` (string). Path to a directory containing a `boilerplate.yml` file, relative to the runbook. Required unless the block has inline YAML.
- `prefilledVariables` (object). An object of variable names to values that pre-populate the form fields. These values override any `default` values defined in the YAML.
## Declaring variables
Variables use the [Boilerplate variable syntax](/authoring/boilerplate/#the-boilerplateyml-file). Declare them as inline YAML or load them with `path`. A block takes one or the other.
### Inline YAML
Write the variable definitions inside the `` tags in the style of a `boilerplate.yml` file, wrapped in a ` ```yaml ` code fence:
`````mdx
```yaml
variables:
- name: Username
type: string
description: Your username
validations:
- required
- name: NotifyByEmail
type: bool
description: Receive email notifications
default: true
```
`````
The code fence is required. Without it, MDX parses the YAML as Markdown paragraphs and lists and loses its indentation, so the block shows an "Invalid inline boilerplate configuration format" error asking you to wrap the YAML in a code fence.
### Loading from a path
Load variables from an external `boilerplate.yml` file:
```mdx
```
This loads variable definitions from `templates/vpc/boilerplate.yml` relative to your runbook file.
## Prefilled variables
Pre-populate form fields with specific values. These override any `default` values:
`````mdx
`````
## Using variables in Commands and Checks
Command, Check and Template blocks read the values of an Inputs block through `inputsId`.
### Reference by `inputsId`
`````mdx
```yaml
variables:
- name: VpcName
type: string
description: Name for the VPC
- name: CidrBlock
type: string
description: CIDR block for the VPC
default: "10.0.0.0/16"
```
`````
### Reference multiple Inputs
You can reference multiple Inputs blocks by passing an array of IDs. Variables are merged in order, with later IDs overriding earlier ones:
`````mdx
```yaml
variables:
- name: Environment
type: enum
options: [dev, staging, prod]
default: dev
```
```yaml
variables:
- name: AppName
type: string
- name: Port
type: int
default: 8080
```
`````
### Embedded in Command or Check
You can embed Inputs directly inside a Command or Check block:
`````mdx
```yaml
variables:
- name: Name
type: string
description: Your name
validations:
- required
```
`````
When embedded, the Inputs form renders inline within the parent block without a separate submit button, and the parent can use its variables.
Other blocks can still reference embedded Inputs using the standard `inputsId` pattern.
## Sections
Group related variables under section headers using the `x-section` extension:
```yaml
variables:
- name: FunctionName
type: string
description: Name of the Lambda function
x-section: Basic Configuration
- name: Runtime
type: enum
options: [python3.12, nodejs20.x]
x-section: Basic Configuration
- name: MemorySize
type: int
description: Memory allocation in MB
default: 128
x-section: Advanced Settings
- name: Timeout
type: int
description: Timeout in seconds
default: 30
x-section: Advanced Settings
```
Variables with the same `x-section` value are grouped together in the form under a section header.
## Complete example
`````mdx
# Create a GitHub Repository
Enter your repository settings:
```yaml
variables:
- name: OrgName
type: string
description: GitHub organization name
validations:
- required
- name: RepoName
type: string
description: Repository name
validations:
- required
- name: Visibility
type: enum
description: Repository visibility
options:
- private
- public
default: private
```
First, verify you're authenticated to GitHub:
Now create the repository:
Verify the repository was created:
`````
---
#### <Template>
The `` block generates files from a [Gruntwork Boilerplate](/authoring/boilerplate/) template directory. It renders a form for any variables defined in the template, and saves generated files to the workspace.
## Basic usage
```mdx
```
This loads the `boilerplate.yml` file from `templates/vpc/boilerplate.yml` (relative to your runbook file), renders a form for the variables, and generates files when the user clicks "Generate".
Once the files have been generated, the block turns green and regenerates them automatically as the form changes. If generation fails, the error is shown above the form and the block is not green while that error is shown. A template error in one of the template's files fails generation before any file is written, so the generated files stay as they were. If the first generation fails, the Generate button stays available so the user can retry.
## vs. TemplateInline
Template and [TemplateInline](/authoring/blocks/templateinline/) both render Boilerplate templates:
| Feature | `` | `` |
|---------|-------------|-------------------|
| Template source | Directory with `boilerplate.yml` | Inline in runbook |
| Form rendered | Yes, from `boilerplate.yml` | No form (uses [Inputs](/authoring/blocks/inputs/)) |
| File generation | Always saves to workspace | Optional (`generateFile={true}`) |
| Use case | Generate files from a template directory with a `boilerplate.yml` | Preview or generate a single file without a template directory |
## Props
### Required props
- `id` (string). Unique identifier for this component. Other blocks use it to reference this Template's variables.
- `path` (string). Path to the boilerplate template directory, relative to the runbook file. The directory must contain a `boilerplate.yml` file.
### Optional props
- `inputsId` (string | string[]). ID of Inputs or Template block(s) to import variable values from. When multiple IDs are provided as an array, variables are merged in order (later IDs override earlier ones).
- `target` (`"generated"` | `"worktree"`). Where template output is written. Defaults to `"generated"`, which writes to the generated files directory (`$GENERATED_FILES`). `"worktree"` writes into the active git worktree and requires a `` block to have run first. The active worktree is the one selected in the workspace, or else the most recently cloned repository.
> **Tip:** Use `target="worktree"` to generate files into a cloned repository, for example to add new modules or configuration files to an existing repo.
## Directory structure
The `path` prop should point to a directory containing a `boilerplate.yml` file and any template files:
```mdx
```
Expected directory structure:
- templates/
- vpc/
- boilerplate.yml Variable definitions (required)
- arbitrary_template_files.txt Template file
The `boilerplate.yml` file defines the variables that will be collected from the user. Template files use [Boilerplate's Go template syntax](/authoring/boilerplate/#template-syntax) for variable substitution.
## With variables
### Standalone Template
The Template displays a form with all variables defined in its `boilerplate.yml`:
```mdx
```
### Using `inputsId`
Import variables from a separate Inputs block:
`````mdx
```yaml
variables:
- name: Environment
type: enum
options: [dev, staging, prod]
default: dev
- name: Region
type: string
default: us-east-1
```
`````
Variables from `config` are merged with the template's own variables. Shared variables become read-only in the Template form.
### Using multiple `inputsId` values
You can reference multiple Inputs blocks by passing an array of IDs. Variables are merged in order, with later IDs overriding earlier ones:
`````mdx
```yaml
variables:
- name: OrgName
type: string
```
```yaml
variables:
- name: Environment
type: enum
options: [dev, prod]
```
`````
## Variable categories
A Template has its own `boilerplate.yml` and can also import variables via `inputsId`, so a variable can be defined on either side or both. Variables only the Template defines are editable. Variables only the imported block defines are passed to the template without appearing in the form. Variables defined in both places are read-only in the Template form. See [When variables overlap](/authoring/inputs-and-outputs/#when-variables-overlap).
## Template file syntax
Template files use [Boilerplate's Go template syntax](/authoring/boilerplate/#template-syntax):
```hcl
# main.tf
resource "aws_vpc" "main" {
cidr_block = "{{ .CidrBlock }}"
tags = {
Name = "{{ .VpcName }}"
Environment = "{{ .Environment }}"
}
}
{{- if .EnableFlowLogs }}
resource "aws_flow_log" "main" {
vpc_id = aws_vpc.main.id
# ...
}
{{- end }}
```
## Using block outputs
Template files can reference outputs from other blocks (e.g., [Command](/authoring/blocks/command/), [Check](/authoring/blocks/check/), [DirPicker](/authoring/blocks/dirpicker/)) using the `outputs` namespace:
```hcl
account_id = "{{ .outputs.create_account.account_id }}"
```
Runbooks scans template files for output references and disables the Generate button until all upstream blocks have run.
For producing outputs, consuming them, dependency behavior, and ID naming rules, see [Block outputs](/authoring/inputs-and-outputs/#block-outputs).
## Complete example
`````mdx
# Deploy a VPC
First, configure your environment:
```yaml
variables:
- name: Environment
type: enum
options: [dev, staging, prod]
default: dev
- name: Region
type: string
default: us-east-1
```
Now configure your VPC. The Environment and Region will be inherited from above:
`````
---
#### <TemplateInline>
The `` block renders a Boilerplate template written in the runbook file itself and shows the rendered output inline. [``](/authoring/blocks/template/) loads its templates from a directory.
## Basic usage
`````mdx
```yaml
variables:
- name: Name
type: string
default: World
```
```txt
Hello, {{ .inputs.Name }}!
```
`````
## vs. Template
TemplateInline and [Template](/authoring/blocks/template/) both render Boilerplate templates:
| Feature | `` | `` |
|---------|-------------------|--------------|
| Template source | Inline in runbook | Directory with `boilerplate.yml` |
| Form rendered | No (uses [Inputs](/authoring/blocks/inputs/)) | Yes, from `boilerplate.yml` |
| File generation | Optional (`generateFile={true}`) | Always saves to workspace |
| Use case | Preview or generate a single file without a template directory | Generate files from a template directory with a `boilerplate.yml` |
## Props
### Required props
- `id` (string). Unique identifier for this block. See [Block IDs](/authoring/blocks/#block-ids).
- Inline template content. The template to render, written as a fenced code block inside the `` tags. Give the code block a language hint (e.g., `hcl`, `yaml`, `dockerfile`) for syntax highlighting.
### Optional props
- `inputsId` (string | string[]). ID of the Inputs block(s) to get variable values from. When multiple IDs are provided as an array, variables are merged in order (later IDs override earlier ones). If not provided, the template renders without variable substitution.
- `outputPath` (string). File path for the rendered output (e.g., `config.yaml` or `docs/account.hcl`). This appears as the filename in the code block header. With `generateFile={true}`, it is also where the file is saved, relative to the output directory. Missing parent directories are created.
- `generateFile` (boolean). Whether to save the rendered file to the workspace. When `false` (the default), the block only shows the rendered output inline.
- `target` (`"generated"` | `"worktree"`). Where template output is written when `generateFile={true}`. Defaults to `"generated"`, which writes to the generated files directory (`$GENERATED_FILES`). `"worktree"` writes into the active git worktree: the one selected in the workspace, or else the most recently cloned repository. It requires a `` block to have run first. Until then, the block shows an error in place of the preview.
> **Note:** `outputPath` must be a relative path. Absolute paths and paths that leave the output directory (e.g., `../`) are rejected.
## With variables
### Using `inputsId`
Reference a separate Inputs block to get variable values:
`````mdx
```yaml
variables:
- name: AppName
type: string
- name: NodeVersion
type: string
default: "18"
```
```dockerfile
FROM node:{{ .inputs.NodeVersion }}-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
```
`````
### Using multiple `inputsId` values
You can reference multiple Inputs blocks by passing an array of IDs. Variables are merged in order, with later IDs overriding earlier ones:
`````mdx
```yaml
variables:
- name: Environment
type: enum
options: [dev, staging, prod]
default: dev
```
```yaml
variables:
- name: ServiceName
type: string
- name: Port
type: int
default: 8080
```
```hcl
resource "aws_ecs_service" "main" {
name = "{{ .inputs.ServiceName }}-{{ .inputs.Environment }}"
load_balancer {
container_port = {{ .inputs.Port }}
}
}
```
`````
## Multiple files
You can have multiple `` blocks referencing the same ``:
`````mdx
```yaml
variables:
- name: AppName
type: string
- name: Environment
type: enum
options: [dev, prod]
```
```yaml
app:
name: {{ .inputs.AppName }}
environment: {{ .inputs.Environment }}
```
```bash
APP_NAME={{ .inputs.AppName }}
ENVIRONMENT={{ .inputs.Environment }}
```
`````
## With Boilerplate logic
Templates can use Boilerplate template syntax, including conditionals and loops:
`````mdx
```yaml
variables:
- name: Environment
type: enum
options: [dev, prod]
- name: EnableMonitoring
type: bool
default: false
```
```hcl
resource "aws_instance" "app" {
ami = "ami-12345678"
instance_type = "{{ if eq .inputs.Environment "prod" }}t3.large{{ else }}t3.micro{{ end }}"
{{- if .inputs.EnableMonitoring }}
monitoring = true
{{- end }}
}
```
`````
See [Template syntax](/authoring/boilerplate/#template-syntax).
## Using block outputs
TemplateInline can reference outputs from [Command](/authoring/blocks/command/) or [Check](/authoring/blocks/check/) blocks using the `outputs` namespace:
`````mdx
```txt
Account ID: {{ .outputs.create_account.account_id }}
Region: {{ .outputs.create_account.region }}
```
`````
Runbooks scans the template content for output references and shows a warning until all upstream blocks have run.
For producing outputs, consuming them, dependency behavior, and ID naming rules, see [Block outputs](/authoring/inputs-and-outputs/#block-outputs).
## Generating files
By default, `` shows the rendered output and saves no file. To also save it, set `generateFile={true}`. The file is saved at `outputPath` inside the generated files directory (or inside the active git worktree with `target="worktree"`), and it is rewritten each time the block re-renders, for example when its inputs change:
`````mdx
```yaml
variables:
- name: ProjectName
type: string
```
```markdown
# {{ .inputs.ProjectName }}
Welcome to {{ .inputs.ProjectName }}!
```
`````
If `outputPath` changes between renders, for example because it uses a [``](/authoring/blocks/dirpicker/) output, the block saves the file at the new path and cleans up the one it saved at the old path:
- If the block created the old file, it is removed, along with any directories that removal leaves empty.
- If a file was already at the old path before the block wrote to it, the block puts back its earlier content. The block never deletes a file it did not create.
- If the old file no longer holds what the block wrote, for example because you edited it, it is left as it is.
Cleanup only happens inside the directory the block is writing to now. With `target="worktree"`, a file the block wrote into an earlier repo stays there after another `` makes a different repo the active worktree.
## Complete example
`````mdx
# Configure Your Docker Service
Enter your service configuration:
```yaml
variables:
- name: ServiceName
type: string
description: Name of your service
validations:
- required
- name: Port
type: int
description: Port to expose
default: 8080
- name: Environment
type: enum
description: Deployment environment
options: [dev, staging, prod]
default: dev
```
Here's what your Docker Compose file will look like:
```yaml
version: "3.8"
services:
{{ .inputs.ServiceName }}:
build: .
ports:
- "{{ .inputs.Port }}:{{ .inputs.Port }}"
environment:
- NODE_ENV={{ .inputs.Environment }}
```
And your environment file:
```bash
SERVICE_NAME={{ .inputs.ServiceName }}
PORT={{ .inputs.Port }}
ENVIRONMENT={{ .inputs.Environment }}
```
`````
---
## Security
### Execution security model
Runbooks runs the commands and shell scripts in a runbook directly on your computer, with the environment variables the app was started with. This page describes the checks Runbooks applies before a script runs.
## Trust warning
Every runbook opens with a banner asking you to confirm that you trust it. The banner has an "I trust this Runbook" checkbox and an option to hide it for that runbook path.
## Opening a runbook never runs its code
A `runbook.mdx` file is compiled and rendered inside the Runbooks app, so Runbooks only accepts MDX that is purely declarative. If a runbook contains any of the following, Runbooks shows an error instead of rendering it:
- `import` or `export` statements
- JavaScript expressions, such as `{new Date().toString()}` or `command={buildCommand()}`
- Spread props, such as ``
- `<script>` elements, and elements that embed another document: `<iframe>`, `<frame>`, `<frameset>`, `<object>`, `<embed>` and `<webview>`
- Props that inject raw HTML: `dangerouslySetInnerHTML` and `srcDoc`
- Custom elements (element names with a `-`, such as `<my-widget>`), dotted element names (such as ``) and namespaced element names (such as `<svg:script>`)
- `__proto__` as a prop name or object key
- `javascript:` URLs in any prop, such as `<a href="javascript:...">`
You can still use `{...}` for literal values: strings, numbers (including a leading `-` or `+`), booleans, `null`, regex literals, template strings without `${...}` substitutions, and arrays or objects made only of those. For example, `detectCredentials={['env']}` and `detectCredentials={[{ env: { prefix: 'PROD_' } }, 'env']}` are allowed, and so are `{/* comments */}`.
So opening a runbook, from your machine or from a remote URL, runs no code of its own. The scripts in `` and `` blocks run when you click Run.
## Executable registry
When a runbook opens, the main process reads `runbook.mdx`, finds every `` and ``, and stores each script (the `command` prop, or the file named by `path`) in an in-memory registry under a unique executable ID, along with the block ID and its template variables.
When you click Run, the renderer sends an execution request with the executable ID, the template variable values, environment variable overrides from the auth blocks the block references, and a timeout. It never sends script content. The main process looks the ID up in the registry, renders the stored script with the given variables, and runs it. A request for an ID that is not in the registry is refused, so a manipulated IPC message cannot pick a script the runbook did not contain. The registry holds the scripts the runbook had when it was loaded, and any script file you [reloaded yourself](#reloading-a-changed-script-file) after reviewing a change. Template values are inserted into the script verbatim and environment overrides are applied as sent, so a manipulated request can change what the stored script does, though not which script runs.
## Electron settings
The renderer runs with `sandbox: true`, `contextIsolation: true` and `nodeIntegration: false`. It has no access to Node.js APIs or the file system and talks to the main process only through the API that the preload script exposes with `contextBridge`. Script execution, file access and environment management all happen in the main process, which is a separate OS process from the renderer.
`webviewTag` is enabled so the [Iframe block](/authoring/blocks/iframe/) can embed pages in `<webview>` guests. Before a guest attaches, the main process removes its preload script and denies dialogs, downloads, `window.open` and permission requests from the embedded page.
## When the registry is built
Every script Runbooks runs comes from the registry. The ways of opening a runbook differ in when the registry is rebuilt from the files on disk.
### Opening a runbook
```bash
runbooks open path/to/runbook.mdx
```
The registry is built once, when the runbook opens. Changes you make to the runbook or its scripts afterwards do not run until you close and reopen the runbook, or until you [reload a changed script file](#reloading-a-changed-script-file) from its block. This is the mode for consumers who want to run exactly what the author wrote.
### Watch mode
```bash
runbooks open --watch path/to/runbook.mdx
```
Runbooks watches the runbook file and reloads the UI when it changes. Each reload rebuilds the registry from the runbook and the scripts it references as they are on disk at that moment. Execution still goes through the registry, but whatever is on disk at reload becomes approved, so anything that can write to the runbook's files while you work can change what runs. This is the mode for authors editing their own files.
### Freezing the registry
```bash
runbooks open --watch --disable-live-file-reload path/to/runbook.mdx
```
`--disable-live-file-reload` keeps the registry built when the runbook was opened in this app session. Watch mode still reloads what the app shows, but Runbooks keeps executing the scripts that were present at open, and blocks whose script has changed show a "Script changed" warning. Opening a different runbook builds its registry as usual, and coming back to this one rebuilds its registry.
Freezing the registry does not stop you from [reloading a changed script file](#reloading-a-changed-script-file) from its block.
### Reloading a changed script file
Runbooks watches the script files that `` and `` blocks reference with `path`, in every mode. When one changes on disk after the registry was built, nothing about what runs changes. Its block shows a "Script changed" notice with a diff between the version in the registry and the version on disk, and a **Reload script** button.
**How it works:**
1. Until you click **Reload script**, Run executes the version in the registry, which is also the version under "View Source Code"
2. The click sends the main process the block's ID and the hash of the version you reviewed, never a path or script content
3. The main process reads the file again from the path the registry entry was built from, and replaces that one entry only if the content has the hash you reviewed
4. If the file changed again in between, the reload is refused and the notice shows the latest diff
**Security:**
- A changed script is never executed without a click on a notice that shows what changed
- The reload approves the content you saw. A write that lands between the diff and the click is rejected
- Only script files are covered. A changed inline `command` is part of `runbook.mdx`, so it needs the runbook to be reloaded
## How scripts run
1. The main process checks that the executable ID is in the registry.
2. It renders template variables such as `{{ .VarName }}` into the script.
3. It writes the script to a temporary file.
4. It reads the shebang line to pick the interpreter, defaulting to `bash`.
5. It runs the interpreter on the file in a non-interactive shell.
6. It streams stdout and stderr to the renderer over IPC.
7. It deletes the temporary file.
Scripts run with your user's permissions and full environment. Only run runbooks you trust.
For interpreter detection, shell limitations and how environment changes persist between scripts, see [Shell execution context](/security/shell-execution-context/).
---
### Shell execution context
## Environment persists between blocks
Scripts in Check and Command blocks share one session. When a Bash script exits with code 0 or 2, Runbooks applies its environment changes to the session, and later blocks see them.
| What persists | Example |
|---------------|---------|
| Environment variables | `export AWS_PROFILE=prod` stays set for later blocks |
| Working directory | `cd /path/to/project` changes where later scripts run |
| Unset variables | `unset DEBUG` removes the variable for later blocks |
Changes made by a script that exits with any other code, times out or is cancelled are discarded.
### Bash scripts only
> **Environment persistence requires Bash:**
Only scripts with a `#!/bin/bash` or `#!/bin/sh` shebang, or no shebang, can change the session. Scripts in other languages can read the session's environment variables, but a change such as `os.environ["VAR"] = "value"` in Python does not reach later blocks.
| Script type | Can read env vars | Can set persistent env vars |
|-------------|-------------------|-----------------------------|
| Bash (`#!/bin/bash`) | Yes | Yes |
| Sh (`#!/bin/sh`) | Yes | Yes |
| Python (`#!/usr/bin/env python3`) | Yes | No |
| Ruby (`#!/usr/bin/env ruby`) | Yes | No |
| Node.js (`#!/usr/bin/env node`) | Yes | No |
| Other interpreters | Yes | No |
Runbooks wraps each Bash script in Bash code that writes the environment and working directory to temporary files when the script exits. That wrapper cannot run under another interpreter, and a child process cannot change its parent's environment.
Because the wrapper is Bash code, scripts with a `#!/bin/sh` shebang run under `bash`. On Debian and Ubuntu, `sh` is `dash`, which can't run the wrapper. Bash runs POSIX `sh` scripts as they are. For `#!/bin/sh` scripts Runbooks also turns on Bash's `xpg_echo` option, so `echo "a\nb"` prints two lines, as `sh` does on macOS, Debian and Ubuntu. Scripts with a `#!/bin/bash` shebang keep Bash's default, where `echo` prints `\n` literally unless you pass `-e`.
### Multiline environment variables
Values with embedded newlines, such as RSA keys and JSON, persist across blocks unchanged. Runbooks captures the environment with NUL-terminated output (`env -0`).
```bash
#!/bin/bash
export SSH_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"
export JSON_CONFIG='{
"database": "postgres",
"settings": { "timeout": 30 }
}'
```
### `trap` handlers
Scripts can set their own `EXIT` trap for cleanup.
```bash
#!/bin/bash
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT
# Your script logic...
export RESULT="computed value"
```
Runbooks captures the environment in its own `EXIT` handler, so it intercepts `trap` calls that name `EXIT` and saves your handler. The usual `trap` forms all work: `trap -- cleanup EXIT`, `trap cleanup INT EXIT` (the `INT` handler is installed as usual), and resets such as `trap - EXIT` or `trap EXIT`. When your script exits:
1. Your trap handler runs.
2. Runbooks captures the environment and working directory.
3. The script exits with its original exit code.
### One session per runbook
The app has one window and one session. Every script in a runbook shares it. The session lasts until you open a different runbook or quit the app.
### Starting environment
The session starts from the environment the Runbooks app was started with.
If you start Runbooks from a terminal or an SSH session, it uses that terminal's environment as it is. A variable you set on the command line that starts Runbooks, such as `AWS_PROFILE=prod`, is the value your scripts see.
If you start Runbooks from Finder, the Dock or a desktop launcher on macOS or Linux, it first runs your login shell once as `$SHELL -ilc` and copies the environment that shell ends up with. Because the shell is both a login and an interactive shell, it reads your profile and rc files, so your `PATH` and the variables they export are available to scripts.
On Windows, Runbooks uses the environment it was started with.
### How script changes are applied
When a Bash script exits with code 0 or 2, Runbooks applies only what that script changed:
- Variables the script exported or changed are set to their new values.
- Variables the script unset are removed from the session.
- The working directory changes only if the script changed directory.
Everything else in the session is left as it is. If the session changed while the script was running, for example because an auth block added credentials, those changes are kept and the script's changes are applied on top of them. If the script and something else both changed the same variable, the script's value wins.
If you open a different runbook while a script is running, the finished script's changes are discarded. The same applies to a sign-in or a `` still in progress when you switch: its credentials and checkout are not added to the new runbook's session. Sign in or clone again from the new runbook.
### Running scripts at the same time
You can click Run on a block while another block's script is still going, and both keep running. Runbooks holds a block back when it can tell the block has to wait: the block uses the outputs of a block that is running, names a block in `dependsOn` that hasn't succeeded, or would overlap with an `exclusive` block. See [Run order](/authoring/blocks/command/#run-order).
Runbooks can't tell that one script reads a variable or a directory another script sets. A script gets the session as it stood when you clicked Run, and Runbooks applies its changes when it finishes, so two scripts running together don't see each other's changes. If a script needs what an earlier block exported, name the earlier block in its `dependsOn`. When overlapping scripts set the same variable, or both change directory, the last one to finish wins.
Runbooks discards the environment changes of a script you stop.
## Built-in environment variables
Runbooks sets the following environment variables for every script:
| Variable | Description |
|----------|-------------|
| `GENERATED_FILES` | Path to a temporary directory where scripts can write files to be captured. Files written here appear in the **Generated files** tab when the script exits with code 0 or 2. |
| `REPO_FILES` | Path to the active git worktree: the one you selected in the workspace, or the most recently registered one if you selected none. Unset if no repository has been cloned. |
| `RUNBOOK_OUTPUT` | Path to a file where scripts can write `key=value` pairs to produce [block outputs](/authoring/blocks/command/#block-outputs) for later blocks. Write `sensitive:key=value` to mask a credential in the UI (see [Sensitive outputs](/authoring/inputs-and-outputs/#sensitive-outputs)). |
Each script also gets log files. The `log_info`, `log_warn`, `log_error` and `log_debug` functions append to `RUNBOOK_LOG`, and each of their lines names its level. `RUNBOOK_INFO_LOG`, `RUNBOOK_WARN_LOG`, `RUNBOOK_ERROR_LOG` and `RUNBOOK_DEBUG_LOG` take lines that don't name a level, which show up at the file's level. Any command or script can append to them all, and their lines appear in the block's logs as the script writes them. The files are deleted when the run ends. See [Log files](/authoring/blocks/command/#log-files).
### Capturing output files
Write files to `$GENERATED_FILES` to save them to the generated files directory:
```bash
#!/bin/bash
# Generate a config and capture it
tofu output -json > "$GENERATED_FILES/outputs.json"
# Create subdirectories as needed
mkdir -p "$GENERATED_FILES/config"
echo '{"env": "production"}' > "$GENERATED_FILES/config/settings.json"
```
Files are captured only when the script exits with code 0 or 2. If the script fails, files written to `$GENERATED_FILES` are discarded.
See [Capturing output files](/authoring/blocks/command/#capturing-output-files) for more.
### Modifying cloned repositories
If a `` block has cloned a repository, use `$REPO_FILES` to modify files in it:
```bash
#!/bin/bash
if [ -n "${REPO_FILES:-}" ]; then
echo "Modifying files in cloned repo: $REPO_FILES"
echo "new config" >> "$REPO_FILES/settings.hcl"
else
echo "No git worktree available"
fi
```
Writes to `$REPO_FILES` go straight to the checkout on disk, whatever the script's exit code. They show up in the **Changed files** tab.
## Non-interactive shell
Scripts run in a non-interactive shell, which limits what they can use:
| Feature | Available | Notes |
|---------|-----------|-------|
| Environment variables | Yes | The session's environment, including changes from earlier blocks |
| Binaries in `$PATH` | Yes | `git`, `aws`, `tofu` and so on |
| Shell aliases | No | `ll`, `la`, custom aliases |
| Shell functions | No | `nvm`, `rvm`, `assume` and so on |
| RC files | No | Scripts do not source `.bashrc` or `.zshrc`. On a desktop launch Runbooks reads them once at startup for exported variables only (see [Starting environment](#starting-environment)). |
### Aliases and binaries
```bash
# Fails: ll is usually a shell alias for "ls -l"
# Works: ls is a binary
```
### Tools that are shell functions
Some developer tools are shell functions defined in your rc files (`.bashrc`, `.zshrc`), so they exist only in interactive shells:
- `nvm`, Node Version Manager
- `rvm`, Ruby Version Manager
- `pyenv` shell integration
- `conda activate`
- `assume`, from [Granted](https://docs.commonfate.io/granted/introduction)
They are functions because they change the current shell's environment, such as `$PATH`, which a child process cannot do.
### Workarounds
For a tool that is a shell function, check for its installation:
```bash
#!/bin/bash
# "nvm --version" fails here, so check that nvm is installed
if [ -d "$HOME/.nvm" ] && [ -s "$HOME/.nvm/nvm.sh" ]; then
echo "nvm is installed"
exit 0
else
echo "nvm is not installed"
exit 1
fi
```
If a script needs the function itself, source the rc file in the script. This ties the script to one shell's configuration.
```bash
#!/bin/bash
source ~/.bashrc 2>/dev/null || source ~/.zshrc 2>/dev/null
nvm --version
```
## Interpreter detection
If a script starts with a shebang such as `#!/bin/bash` or `#!/usr/bin/env python3`, Runbooks runs it with that interpreter. Without a shebang it uses `bash`.
| Shebang | Interpreter |
|---------|-------------|
| `#!/bin/bash` | Bash |
| `#!/bin/sh` | Bash (see [Bash scripts only](#bash-scripts-only)) |
| `#!/bin/zsh` | Zsh |
| `#!/usr/bin/env python3` | Python 3 |
| `#!/usr/bin/env node` | Node.js |
Start each script with a shebang so the interpreter does not depend on the default:
```bash
#!/bin/bash
set -e
# Your script here...
```
## Demo runbooks
The Runbooks repository has demo runbooks for these features.
[`runbook-execution-model`](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/runbook-execution-model) covers environment persistence:
- Setting and reading environment variables across blocks
- Working directory persistence
- Multiline environment variables (RSA keys, JSON)
- Non-Bash scripts reading persistent environment variables
[`capture-files-from-scripts`](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/capture-files-from-scripts) covers file capture:
- Using `$GENERATED_FILES` to capture generated files
- Creating OpenTofu configs from environment variables set in earlier blocks
[`file-workspace`](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/file-workspace) covers the file workspace:
- Cloning a repository with `` and browsing its files
- Using `$REPO_FILES` to modify files in a cloned repo
- Writing templates directly into a worktree with `target="worktree"`
- Viewing changes in the **Changed files** diff view
---
### Telemetry
Runbooks can send usage events to [Mixpanel](https://mixpanel.com/), a third-party analytics service. Telemetry is on unless you [opt out](#how-to-disable-telemetry).
## When telemetry is sent
Runbooks sends events only when it has a Mixpanel project token in two places: `VITE_MIXPANEL_TOKEN` when the app is built, and `MIXPANEL_TOKEN` in the app's environment when it runs. If either is missing, nothing is sent. The release workflow in this repository sets neither, and a build from source sets neither unless you do.
## What is sent
The renderer sends two events.
| Event | When | Extra properties |
|-------|------|------------------|
| `app_loaded` | The app window loads | None |
| `runbook_loaded` | Half a second after the blocks of a runbook finish rendering | `block_counts`, the number of blocks of each type in the runbook (such as `Command: 3`), and `total_blocks` |
Block counts cover Command, Check, Inputs, Template, TemplateInline, DirPicker, GitClone, the auth blocks and the pull request blocks. Admonition and Iframe blocks are not counted.
Every event also has these properties:
| Property | Value |
|----------|-------|
| `distinct_id`, `$user_id` | The [install identifier](#the-install-identifier) |
| `$device_id` | A random ID the Mixpanel library generates and keeps in the app's local storage |
| `version` | The Runbooks version |
| `platform` | Always `electron` |
| `$os`, `$browser`, `$browser_version` | Read from the app's user agent string by the Mixpanel library |
| `$screen_width`, `$screen_height` | Your screen size in pixels |
| `$current_url` | The `file://` URL of the app's own page |
| `$initial_referrer`, `$initial_referring_domain` | Always `$direct`, because the app has no referrer |
| `mp_lib`, `$lib_version`, `$insert_id`, `time` | The Mixpanel library name and version, a random event ID and a timestamp |
`$current_url` is the path of the installed app, so it includes your OS username if you installed Runbooks under your home directory. It also includes the `#anchor` of the last in-page link you clicked in a runbook.
## What is not sent
- Runbook text, scripts and commands, apart from the anchor described above
- The location of your runbooks on disk
- Values you type into inputs
- Script output
- Your IP address. The Mixpanel client is configured with `ip: false`, which tells Mixpanel not to store the address the request came from.
## The install identifier
Events are tied together by a SHA-256 hash of `hostname:username`. Mixpanel receives only the hash. The hash is the same every time you run Runbooks as that user on that machine.
The hash is a pseudonym. Anyone who knows or guesses your hostname and username can compute it and match it to your events.
## How to disable telemetry
Pass `--no-telemetry` when you start Runbooks:
```bash
runbooks open --no-telemetry path/to/runbook
```
Or set `RUNBOOKS_TELEMETRY_DISABLE` to `1`, `true` or `yes`:
```bash
# Add to your shell profile (~/.bashrc, ~/.zshrc) to opt out permanently
export RUNBOOKS_TELEMETRY_DISABLE=1
```
## Where the data goes
Events go to Mixpanel over HTTPS and are kept according to Mixpanel's data retention policies. The Gruntwork team has access to them.
## Source
The telemetry code is in two files:
- Main process: [`src/telemetry.ts`](https://github.com/gruntwork-io/runbooks/blob/main/src/telemetry.ts)
- Renderer: [`web/src/contexts/IpcTelemetryContext.tsx`](https://github.com/gruntwork-io/runbooks/blob/main/web/src/contexts/IpcTelemetryContext.tsx)
Gruntwork uses the data to see which block types runbooks use and which Runbooks versions are in use. If you have questions about telemetry, [open an issue](https://github.com/gruntwork-io/runbooks/issues).
---
## Development
### Development workflow
This page is for people working on the Runbooks app itself.
## Prerequisites
[mise](https://mise.jdx.dev/) manages the tool versions. Install mise, then run:
```bash
mise install
```
This installs Node.js, bun, and the [just](https://github.com/casey/just) command runner.
## Running the app in dev mode
```bash
just dev
```
`just dev-runbook <path>` does the same and opens the runbook at that path. Without a path it opens `testdata/sample-runbooks/my-first-runbook`.
This runs `electron-vite` in dev mode with hot module replacement (HMR) for the renderer process.
It first runs `just fetch-boilerplate`, which downloads the pinned [Boilerplate](https://github.com/gruntwork-io/boilerplate) release (CLI + WASM build) into `resources/`. The app renders templates with this vendored copy and ignores any `boilerplate` on your `PATH`, so you develop against the version that ships in the packaged app. To bump it, change `boilerplate_version` in the `justfile` and re-run `just fetch-boilerplate`.
It also checks that the Electron binary for your platform is in `node_modules/electron/dist`, and downloads it if not. `bun install` doesn't download it (Electron stopped doing that in version 42), so the first `just dev` after an install or an Electron upgrade needs network access. `bun run dev` and `bun run preview` run the same check.
To test a custom Boilerplate build, set `RUNBOOKS_BOILERPLATE_BIN` (path to the CLI) and/or `RUNBOOKS_BOILERPLATE_WASM_DIR` (directory containing `boilerplate-full.wasm.br` and `wasm_exec.js`) before launching. The main process logs a warning at startup whenever an override is active. The CLI and WASM build must come from the same Boilerplate source, or renders will differ between the warm (WASM) and cold (CLI) paths.
## Making changes
| You edit | What happens |
|----------|--------------|
| `web/src/` | The renderer picks up the change through HMR. No restart. |
| `src/` or `electron/` | The main process rebuilds and restarts. |
| The runbook you're testing with | Nothing, until you reload the window with **View > Reload**. |
## Testing
```bash
# Everything below, in this order
just test
# Unit tests (Bun test runner for src/, Vitest for web/ and test/integration/)
just test-unit
# End-to-end tests (Playwright). Builds the app first.
just test-e2e
# Runbook tests: runs the compiled test CLI on every runbook under testdata/
just test-runbooks
# Docs: spellcheck, build, and link check
just test-docs
```
## Building
```bash
# Build the app (electron-vite build)
just build
# Package distributable (electron-builder)
just package
```
## Code quality
```bash
# Lint (oxlint)
just lint
# Format (oxfmt). `just fmt-check` reports without writing.
just fmt
# Unused files, exports and dependencies (knip)
just knip
# Type checking (tsc -b)
just typecheck
# lint, fmt-check, knip and typecheck together
just check
```
## Adding dependencies
The app has one `package.json` and one `bun.lock`, both at the repo root. They cover the Electron main process, the preload script, the test CLI in `cli/` and the React renderer in `web/`. Neither `cli/` nor `web/` has a `package.json` of its own, so run `bun add` from the repo root. The docs site in `docs/` is the one exception: it is a separate project with its own `package.json` and `bun.lock`.
If your checkout has a `web/node_modules` or `cli/node_modules` with packages in it, delete it. It shadows the root packages for imports from that directory. These commands stop with an error until it is gone:
- `just dev`, `just dev-runbook`, `just build`, `just typecheck`, `just compile-test-cli`, `just test-backend`, `just test-web`, `just test-integration` and `just test-e2e-run`, and every recipe that depends on one of them, such as `just package` and `just test`.
- `bun run dev`, `bun run build`, `bun run preview`, `bun run typecheck`, `bun run test`, `bun run test:backend`, `bun run test:web`, `bun run test:watch`, `bun run test:coverage`, `bun run test:integration`, `bun run test:e2e` and `bun run test:electron-e2e`. Running `electron-vite` directly, or `vitest` or Playwright on the tests in `web/`, is covered too, because their config files (`electron.vite.config.ts`, `web/vitest.config.ts` and `web/playwright.config.ts`) run the check.
Running `tsc`, `bun test` or `bun cli/index.ts` directly skips the check. `bun scripts/no-nested-node-modules.ts` runs it on its own.
The section a package goes in decides whether it ships inside the packaged app:
- `dependencies` is for packages that `electron/`, `src/` or `cli/` import at runtime. electron-builder copies these into the app's `node_modules`. Add them with `bun add <package>`.
- `devDependencies` is for packages that only `web/` imports, plus build and test tools. Vite bundles the renderer's packages into `dist/renderer`, so the app doesn't need its own copy of them. Add them with `bun add -d <package>`.
## Adding shadcn/ui components
The renderer's UI components come from [shadcn/ui](https://ui.shadcn.com/). To add one, run the shadcn CLI from the repo root, where `components.json` lives:
```bash
bunx shadcn@latest add <component-name>
```
The CLI writes the component to `web/src/components/ui/`. It installs any packages a component needs with `bun add`, which puts them under `dependencies`. Move them to `devDependencies`, because only the renderer uses them.
---
## Runbooks Pro
### Overview
Runbooks open source has limits. If you need any of the following, [contact Gruntwork sales](https://www.gruntwork.io/contact) and tell us how you'd like to use Runbooks in a commercial or enterprise setting.
- A web-based way to browse your Runbooks
- A hosted environment for running Runbooks
- Dropdowns populated from your own environment, such as your AWS accounts, or from a provider, such as the latest RDS engine versions
- Centrally managed scripts shared across your Runbooks
- Audit logs
- Guarantees around which Runbooks you can trust
- Security scanning for Runbooks
- Integration with your existing tools
- Something else the open source edition doesn't support
---