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
Each page below starts with a top-level heading naming its place in the docs.
## Contents
- Intro › Overview
- Intro › UI tour
- Intro › Installation
- Intro › Write your first runbook
- Intro › Files workspace
- Intro › Use cases
- Intro › Runbooks vs. other tools
- Intro › Instruction mode
- App & CLI › Overview
- App & CLI › open
- App & CLI › Sessions
- App & CLI › Watch mode
- Authoring Runbooks › Overview
- Authoring Runbooks › Runbook Structure
- Authoring Runbooks › Markdown Support
- Authoring Runbooks › Inputs & Outputs
- Authoring Runbooks › Opening Runbooks
- Authoring Runbooks › Boilerplate Templates
- Authoring Runbooks › Testing Runbooks
- Authoring Runbooks › Blocks › Overview
- Authoring Runbooks › Blocks › <Admonition>
- Authoring Runbooks › Blocks › <AwsAuth>
- Authoring Runbooks › Blocks › <Chapter>
- Authoring Runbooks › Blocks › <Check>
- Authoring Runbooks › Blocks › <Command>
- Authoring Runbooks › Blocks › <DirPicker>
- Authoring Runbooks › Blocks › <Finish>
- Authoring Runbooks › Blocks › <GitAuth>
- Authoring Runbooks › Blocks › <GitClone>
- Authoring Runbooks › Blocks › <GitHubAuth>
- Authoring Runbooks › Blocks › <GitHubPullRequest>
- Authoring Runbooks › Blocks › <GitLabAuth>
- Authoring Runbooks › Blocks › <GitLabMergeRequest>
- Authoring Runbooks › Blocks › <GitPullRequest>
- Authoring Runbooks › Blocks › <GoogleAuth>
- Authoring Runbooks › Blocks › <Iframe>
- Authoring Runbooks › Blocks › <Inputs>
- Authoring Runbooks › Blocks › <Prerequisites>
- Authoring Runbooks › Blocks › <Template>
- Authoring Runbooks › Blocks › <TemplateInline>
- Security › Execution security model
- Security › Shell execution context
- Security › Telemetry
- Development › Development workflow
- Runbooks Pro › Overview
---
# 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.
---
# Intro › 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. In a runbook split into [chapters](/authoring/blocks/chapter/), only the chapter on screen is searched.
> **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. In a runbook split into [chapters](/authoring/blocks/chapter/), **Go to chapter** lists its chapters the same way. 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.
> **Progress:**
> The bar along the bottom of the title bar fills in as you work through the runbook, and the count next to the session name says how many of its steps are done. Each block you act on is a step, such as a Check that passed or an account you signed in to. See [Progress](/authoring/blocks/#progress) for when each block counts as done.

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
<Check
id="check-mise"
command="mise --version && mise self-update --yes;"
title="Check mise Installation"
description="We recommend `mise` as a tool version manager that can install and manage Terragrunt, OpenTofu, and other tools. This checks that mise is installed and up to date."
successMessage="mise is installed and up to date!"
failMessage="mise is not installed. Install it from https://mise.jdx.dev/getting-started.html"
/>
```
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/).
---
# Intro › 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/).
---
# Intro › 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.
**`runbook.mdx`**
````mdx
# My First Runbook
Welcome to your first runbook! Let's walk through the key features of Runbooks.
## Full markdown support
The first thing to point out is that you have access to the _full set_ ~~of~~ **markdown capabilities** ❤️. Here are some commonly used elements, though almost all of [GitHub-Flavored Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) is supported:
| Element | Syntax | Description |
|---------|--------|-------------|
| Headings | `# H1` `## H2` `### H3` | Create section titles and hierarchy |
| Bold | `**text**` | Emphasize important text |
| Italic | `*text*` or `_text_` | Add subtle emphasis |
| Links | `[text](url)` | Create clickable hyperlinks |
| Images | `` | Embed images in your document |
### Admonitions
In addition to full markdown support, you can also include an [Admonition block](https://runbooks.gruntwork.io/authoring/blocks/admonition/) like this:
<Admonition type="info" title="Before You Begin" description="This is an Admonition block. Notice that it even supports _inline markdown_!" />
## Checks
When users are consuming a runbook, It can be helpful to "check" that they are prepared to succeed, or after using a runbook, that they did in fact succeed. This is the function of [Check blocks](https://runbooks.gruntwork.io/authoring/blocks/check/).
In this example, we're going to verify that git is installed:
<Check
id="check-git"
command="git --version"
title="Check if Git is installed"
description="We need Git for version control"
successMessage="Git is installed!"
failMessage="Git is not installed. Please install it from https://git-scm.com/"
/>
That was a pretty simple check, so now let's load our check from a script, and allow users to _customize_ that script on the fly.
<Check
id="check-git-version"
path="checks/check-git-version.sh"
env={{ EXPECTED_VERSION: "{{ .inputs.GitVersion }}" }}
title="Check Git Version"
description="Verify that you're running a specific version of git"
failMessage="Hmm, your git version seems off. Try again."
successMessage="Success! Well now, look who's got the expected git version."
>
<Inputs id="git-version-inputs">
```yaml
variables:
- name: GitVersion
type: string
description: "The exact git version you expect to have installed (e.g., 2.39.0)"
validations:
- required
default: "2.39.0"
```
</Inputs>
</Check>
If you look at the source code for this runbook, you'll see that we declare the inputs (or variables) we want, and Runbooks renders a web form automatically:
```yaml
variables:
- name: GitVersion
type: string
description: "The exact git version you expect to have installed (e.g., 2.39.0)"
validations:
- required
default: "2.39.0"
```
We declare the variables using [Gruntwork Boilerplate syntax](https://runbooks.gruntwork.io/authoring/boilerplate/), and we have significant control over how the webform renders based on what the [Inputs block](https://runbooks.gruntwork.io/authoring/blocks/inputs/) supports.
The Check's `env` prop hands the value to the script as an environment variable:
```mdx
env={{ EXPECTED_VERSION: "{{ .inputs.GitVersion }}" }}
```
The script reads it as `$EXPECTED_VERSION`. The Check lists the variable with its value, so you can see what the script will get before you run it.
## Generate files
Now let's generate some files. We're going to specify a Gruntwork Boilerplate template, that includes a declaration of the variables in that template, and the template files themselves.
Runbooks will then render a web form automatically and generate the files for us!
<Template id="project" path="templates/project" />
<Admonition type="info" title="Templates update in real time" description="Try updating the author name and programming language, and notice how the rendered template changes in real time." />
On your local machine, all the newly generated files are saved in a subfolder of the folder that contains this runbook.
## Run commands
Runbooks also gives you the [Command block](https://runbooks.gruntwork.io/authoring/blocks/command/), which allows you to run arbitrary scripts.
You can even parameterize the scripts with a custom set of inputs, or any of the inputs we collected above. This one uses the Template's inputs, through its `env` prop:
<Command
id="run-setup"
path="scripts/setup.sh"
inputsId="project"
env={{
PROJECT_NAME: "{{ .inputs.ProjectName }}",
AUTHOR: "{{ .inputs.Author }}",
LANGUAGE: "{{ .inputs.Language }}",
}}
title="Run Project Setup"
description="Creates your project structure"
successMessage="Project setup complete!"
failMessage="Setup failed. Check the logs for details."
/>
Commands are useful when you need to do things like:
- Make an API call
- Install tools locally
- Run commands locally
Whereas Checks blocks are focused on _reading_ the current state of the world, Command blocks are focused on _mutating_ the state of the world.
## Congratulations!
<Command
id="final-message"
command="echo '🎉 Congratulations! You finished using your first runbook!'"
title="Show Success Message"
/>
**Great job!**
Everything above was built in a text editor. I typed in some markdown and blocks (e.g. `<Check>` or `<Template>`), reloaded the runbook, and verified that things looked the way I wanted. To have Runbooks reload the runbook each time you save, open it with `runbooks open --watch`.
If you'd like to learn more about Runbooks, head back to the [Runbooks docs](https://runbooks.gruntwork.io).
````
**`checks/check-git-version.sh`**
```bash
#!/bin/bash
# Git Version Validation Script
# This script checks if the user has the expected version of git installed.
# The Check's env prop sets EXPECTED_VERSION from the GitVersion input.
echo "🔍 Checking git version..."
echo "📋 Expected version: $EXPECTED_VERSION"
# Get the installed git version (extract just the version number, e.g., "2.39.5")
INSTALLED_VERSION=$(git --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [ -z "$INSTALLED_VERSION" ]; then
echo "❌ Git is not installed"
exit 1
fi
echo "💻 Installed version: $INSTALLED_VERSION"
# Compare versions
if [ "$INSTALLED_VERSION" = "$EXPECTED_VERSION" ]; then
echo "✅ You are running the exact expected git version!"
exit 0
else
echo "⚠️ Version mismatch: expected $EXPECTED_VERSION, but found $INSTALLED_VERSION"
exit 1
fi
```
**`scripts/setup.sh`**
```bash
#!/bin/bash
# The Command's env prop sets PROJECT_NAME, AUTHOR and LANGUAGE from the
# project Template's inputs
echo "🚀 Setting up project: $PROJECT_NAME"
echo ""
echo "👋 Welcome, $AUTHOR!"
echo "📦 Primary language: $LANGUAGE"
echo ""
echo "✅ Project setup complete!"
```
**`templates/project/boilerplate.yml`**
```yaml
variables:
- name: ProjectName
type: string
description: What would you like to call your project?
default: my-awesome-project
validations:
- required
- name: Author
type: string
description: What's your first name?
- name: Language
type: enum
description: What's your favorite programming language?
options:
- Go
- Python
- JavaScript
- TypeScript
- Rust
default: Go
```
**`templates/project/README.md`**
```markdown
# {{ .inputs.ProjectName }}
Created by **{{ .inputs.Author }}**
## About
This project is built with {{ .inputs.Language }}.
{{ if eq .inputs.Language "Go" -}}
Go excels at cross-platform compilation and building fast, reliable CLI tools and APIs.
{{- else if eq .inputs.Language "Python" -}}
Python's readability and vast ecosystem make it ideal for data science and rapid prototyping.
{{- else if eq .inputs.Language "JavaScript" -}}
JavaScript powers the web and runs everywhere from browsers to servers to mobile apps.
{{- else if eq .inputs.Language "TypeScript" -}}
TypeScript adds static typing to JavaScript, catching bugs at compile time.
{{- else if eq .inputs.Language "Rust" -}}
Rust delivers C-level performance with memory safety guaranteed at compile time.
{{- end }}
## Getting Started
1. Clone this repository
2. Install dependencies
3. Start building!
```
**`templates/project/.gitignore`**
```text
# Build outputs
dist/
build/
*.exe
```
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.
---
# Intro › 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.
Drag the divider between the runbook and the workspace to change how wide each one is. Double-click the divider to put back the default widths.
> **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/). A Template renders its files when you fill in its form and click **Generate**. A TemplateInline has no form: with `generateFile={true}`, it writes its file once every input it uses has a value. The default `target` is `"generated"`.
```mdx
<Template id="vpc-setup" path="templates/vpc" />
```
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/), [Check](/authoring/blocks/check/) and [Finish](/authoring/blocks/finish/) blocks can write files to the directory named by the `$GENERATED_FILES` environment variable:
```mdx
<Command
id="export-tofu"
command={`tofu output -json > "$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.
### Reviewing changed files
The workspace highlights each generated file that changed since you last reviewed it. A Template you have already generated renders again when you change an input, so the highlights show what that change did to the files.
- In the file tree, a file marked **A** was added and a file marked **M** was modified. A dot next to a folder means a file inside it changed.
- In the code, the file has a colored border and an **Added** or **Modified** label with its count of added and removed lines. The changed lines are highlighted, and a red line marks where lines were removed.
- The header above the files shows how many changed.
Click a modified file in the tree to jump to its first change. It stays highlighted while it's selected, and counts as reviewed once you select another file. Changes to the file while it's selected, such as a Template rendering again as you edit its form, show up in its highlights as they happen and count as reviewed with the rest. Scrolling down past a file's changes also counts as reviewing it: once its last changed line, or the end of a file that was added, goes past the top of the workspace. While files are left to review, there's room below the last file to scroll it past the top too. Only your own scrolling counts, not a jump to a file you clicked in the tree, and a collapsed file isn't reviewed until you expand it. To stop highlighting a file without scrolling past it, click **Dismiss** on the file, or **Dismiss all** in the header.
Which files are left to review is saved with the [session](/commands/sessions/), so a resumed session still highlights them.
### Where generated files are stored
Runbooks writes generated files to a `generated/` folder inside the runbook's [session directory](/commands/sessions/), which Runbooks keeps in its own data folder. Nothing is written to 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:
- sessions/dirs/
- 0199a5c2-7e3b-7c4d-9a1f-3b2c4d5e6f70/ The runbook's session directory
- **generated/**
- main.tf
- variables.tf
- outputs.tf
The files workspace shows the folder as `./generated`, and the copy button next to it copies the full path. Opening the runbook again resumes the same session, so the files are still there. This holds for a [remote runbook](/commands/open/#remote-sources) too: only its clone is temporary.
### 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, or from a local checkout the user selected in one. 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 runbook's [session directory](/commands/sessions/), 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/) or [TemplateInline](/authoring/blocks/templateinline/) block that sets `target="worktree"`, or from Command, Check and Finish 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's `<GitClone>` blocks add more than one repository, a dropdown at the top of the **Repository** tab replaces the repository and branch name and switches between them. The selected repository is the one `$REPO_FILES` points to, that `target="worktree"` writes to, and that a [GitPullRequest](/authoring/blocks/gitpullrequest/) block works on. Until the user selects another, it's the first one added, whether by a clone or a [local checkout](/authoring/blocks/gitclone/#using-a-local-checkout).
## 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, the directories a [DirPicker](/authoring/blocks/dirpicker/) lists, 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, Check and Finish 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.
---
# Intro › 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/).
---
# Intro › 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.
---
# Intro › 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. | The block's title, or "Run this command:" or "Run this check:" if it has none, with the resolved command in a copy block. A nested `<Inputs>` stays a form above the command. File-backed scripts keep the source viewer. The block's [`env`](/authoring/blocks/command/#passing-values-as-environment-variables) variables appear as `export` lines before the command, or in a copy block of their own before a script. |
| Finish | Finish button. Runs the optional final check, then celebrates and shows the next steps. | The block's title, "Finish this runbook" unless you set another, with the final check in a copy block, if the block has one, and the next steps, always shown. Ticking **Mark as done** celebrates and [marks the session finished](/commands/sessions/#finishing-a-session). |
| Prerequisites | Check button. Looks for each tool on PATH and checks its version. | The block's title, or "Make sure these tools are installed:" if it has none, with each tool's version and install hint, and the commands that check them in a copy block. |
| 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. A block that uses a local checkout shows a `cd` to its `prefilledRepoDir` instead. |
| 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 or in its `env`
`export` lines 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 `<Inputs>` 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.
The bar along the bottom of the title bar, and the count next to the session
name, show how many of the steps are done. An `<Inputs>` form has a **Submit**
button rather than a checkbox, and counts as done once you submit it.
A step whose block sets [`autoCollapse`](/authoring/blocks/#folding-done-blocks)
folds down to its title, and its **Done** checkbox, once you mark it as done.
Click it to show the step in full again.
## 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.
- [Sessions](/commands/sessions/) covers what Runbooks saves for each runbook and how it resumes.
## Launching from the command line
```bash
runbooks open ./path/to/runbook
```
Run `runbooks` with no arguments to resume the [session](/commands/sessions/#resuming-a-session) you last launched from the current directory.
## 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.
---
# App & CLI › 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. 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. Scripts and generated files don't use that clone: like any runbook, a remote one runs in the directory of its [session](/commands/sessions/), which is kept.
### 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` | Has no effect, because Runbooks sends no usage data (see [Telemetry](/security/telemetry/)). It is still accepted so existing launch commands keep working, as is the `RUNBOOKS_TELEMETRY_DISABLE` environment variable. |
| `--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.
---
# App & CLI › Sessions
Runbooks keeps a session for every runbook you open. A session is what the runbook's blocks build up as you run them:
- A directory of its own. Scripts start there, [<GitClone>](/authoring/blocks/gitclone/) blocks clone into it, and [generated files](/intro/files_workspace/#generated-files) are written to it.
- The environment variables that scripts exported and auth blocks set.
- The directory the last script left off in.
- The repositories its <GitClone> blocks cloned or opened.
- Its history: what you did in its blocks. The values you typed into forms, every script run with its status, logs and outputs, the repositories you cloned or selected, the pull requests you opened, and the accounts you signed in to.
Runbooks saves the session after every change, so quitting the app keeps it.
## Session names
Every session has a name made of an adjective and a noun, such as `elegant-elephant`. The title bar shows it after the runbook's name, labelled **Session**, along with [how far through the runbook you are](/authoring/blocks/#progress). The window title starts with the session's name too.
A runbook's name is the name of the folder its `runbook.mdx` is in. A remote runbook at the root of its repository is named after the repository. The button before the runbook's name copies the full path of its file, or for a remote runbook the URL you opened it from.
The copy button before the session's name copies it. The folder button after the name copies the path of the session's directory. It is the session's directory for every runbook, including a remote one, whose temporary clone it does not point to.
Paths on screen are shortened: a path in the session's directory starts with `session/`, as in `session/generated`, and any other path in your home directory starts with `~/`. Hover over one to see it in full. Copy buttons copy the full path, and scripts and other blocks get the full path too.
Runbooks picks the name when it creates the session, and the session keeps it until you rename it. No two sessions share a name. If a random pick is already taken, Runbooks picks again, and after ten tries it adds a number, as in `elegant-elephant-2`.
### Renaming a session
Click the name in the title bar, or choose **File > Rename Session…** or **Rename Session** in the app's menu. Type the new name and press <kbd>Enter</kbd>. Press <kbd>Esc</kbd> or click elsewhere to keep the name as it was.
A name follows these rules:
- It is 1 to 63 characters long.
- It uses lowercase letters (`a` to `z`), digits and hyphens. Uppercase letters are lowercased as you type.
- It starts and ends with a letter or digit, and has no two hyphens in a row.
- No other session has it.
`prod-deploy` and `release-2026-10-02` are allowed. `Prod Deploy`, `prod_deploy` and `-prod` are not. When a name is refused, the title bar says why and the session keeps its name.
Renaming changes only what the session is called. Its directory, files and environment stay as they are.
## Resuming a session
Opening a runbook resumes its most recent session, unless you [finished the runbook](#finishing-a-session) in it. Scripts run with the environment variables and working directory the earlier run left, and the session's files are where they were. That holds for a runbook you close and open again without quitting the app, too.
A notice at the top of the runbook names the session it resumed and says when you last used it. Click **Start new session** in it to [reset the session](#resetting-a-session) instead. A session with nothing in its history gets no notice: there is nothing of it to come back.
The blocks come back as you left them, and so does the runbook's [progress](/authoring/blocks/#progress). Each block shows what the session's history has for it at once. A block whose state depends on something outside the app then checks it in the background, and starts over, saying why, if it no longer holds:
| Block | What comes back | What is checked |
|-------|-----------------|-----------------|
| [<Inputs>](/authoring/blocks/inputs/) | The values you typed, and the values you [entered in place of a linked value's chips](/authoring/boilerplate/#defaults-that-use-other-variables). A form you had submitted is still submitted. | |
| [<Command>](/authoring/blocks/command/) and [<Check>](/authoring/blocks/check/) | The last run: its status, logs and outputs. A block that reads another block's output runs without that block running again. The [outputs you entered by hand](/authoring/blocks/command/#entering-an-output-by-hand). | |
| [<Finish>](/authoring/blocks/finish/) | Whether you finished the runbook, with the last run of its final check if it has one. It doesn't celebrate again. | |
| [<Prerequisites>](/authoring/blocks/prerequisites/) | The last check: what it found for each tool, its logs and its outputs. It doesn't check again. | |
| [<Template>](/authoring/blocks/template/) | The values you typed, the values you [entered in place of a linked value's chips](/authoring/boilerplate/#defaults-that-use-other-variables), and whether you had generated its files. | |
| [<GitClone>](/authoring/blocks/gitclone/) | The repository you cloned or selected, with its outputs. | That it is still a git repository on disk. |
| [<DirPicker>](/authoring/blocks/dirpicker/) | The path, with a dropdown selection for each of its directories that is still there. | |
| [<GitPullRequest>](/authoring/blocks/gitpullrequest/), [<GitHubPullRequest>](/authoring/blocks/githubpullrequest/) and [<GitLabMergeRequest>](/authoring/blocks/gitlabmergerequest/) | The pull or merge request you opened, with its outputs, and the form fields you edited. | |
| [<Iframe>](/authoring/blocks/iframe/) | Not the outputs its page set. Blocks that read them wait until the page sets them again. | |
| [<AwsAuth>](/authoring/blocks/awsauth/) | The account you signed in to. | That its credentials still work. Temporary ones expire. |
| [<GitAuth>](/authoring/blocks/gitauth/), [<GitHubAuth>](/authoring/blocks/githubauth/) and [<GitLabAuth>](/authoring/blocks/gitlabauth/) | The account you signed in to. The session's token is still sent only to the host you signed in to. | That the token still works for that host, and still belongs to that account. |
| [<GoogleAuth>](/authoring/blocks/googleauth/) | The account you signed in to, when the sign-in can be done again. | That the credential still signs in as that account. Runbooks then signs in again the same way. |
A check that can't reach AWS, GitHub or GitLab, because you are offline for example, leaves the sign-in as it is.
Credentials that work now can still expire later in the session. When the provider says when, the history saves it with the sign-in, and the block turns red and asks you to sign in again from five minutes before then. That works the same in a resumed session as in a new one. Each auth block's page lists which credentials have an expiry: [<AwsAuth>](/authoring/blocks/awsauth/#expiring-credentials), [<GitAuth>](/authoring/blocks/gitauth/#expiring-tokens) and [<GoogleAuth>](/authoring/blocks/googleauth/#access-token-credentials).
A resumed <GitClone> also checks which branch the repository is on. If it is no longer on the branch the session cloned or found it on, the block turns red and names both branches, and Runbooks leaves the repository as it is. Check out the session's branch in the repository, or clone again.
A resumed <Template> or <TemplateInline> writes its files only when they would come out differently than it last wrote them. A change made to a generated file since stays until something the template uses changes.
A script that was still running when Runbooks quit shows as not run, with a notice that its last run did not finish.
<GoogleAuth> can sign in again from a gcloud configuration, from credentials it detected in the environment or in gcloud's application default credentials, from a key file you chose, or from another block's outputs. It can't from a key you pasted or from **Sign in with Google**: Runbooks deletes the credential files it writes for those when it quits. A block signed in that way says so and asks you to sign in again.
A <GoogleAuth> credential that now signs in as another account is not used: the block asks you to sign in again, and later resumes start over. When signing in again fails for any other reason, such as a key file that was moved or no network, the block asks you to sign in, and the next resume tries again.
See [The session's history](#the-sessions-history) for what is saved and when.
Which session opens depends on how you launch Runbooks:
| Launch | What opens |
|--------|------------|
| `runbooks open SOURCE` | `SOURCE`, in its most recent session. A runbook you have not opened before gets a new one. |
| `runbooks` with no arguments, in a terminal | The session last launched from the terminal's current directory. If there is none, the welcome screen. |
| The dock, the Start menu, or a desktop shortcut | The most recent session. If there is none, the welcome screen. |
A session you finished the runbook in is never what opens: its runbook opens in another session, as [Finishing a session](#finishing-a-session) describes.
Runbooks records the directory you ran `runbooks open SOURCE` from, which is how `runbooks` alone finds the session later. Your home directory counts as no directory: `runbooks` run there opens the most recent session.
A [remote runbook](/commands/open/#remote-sources) is cloned again every time it opens, and resumes the session of the source you opened it with.
If a resumed session's working directory or one of its cloned repositories has been deleted, the session starts in its own directory and drops the missing repository.
## Resetting a session
Choose **File > Reset Session**, or **Reset Session** in the app's menu, to start the open runbook over. Its blocks go back to their initial state, and scripts run in an empty directory with the environment Runbooks was launched with.
A reset replaces the session with a new one, so the name in the title bar changes. The runbook resumes the new session from then on.
Nothing is deleted. The session that was replaced keeps its directory, files and history on disk. [Switch to it](#switching-sessions) to pick it up again, or [delete it](#deleting-sessions).
## Finishing a session
A runbook can end with a [<Finish>](/authoring/blocks/finish/) block. When you click **Finish**, and its final check passes if it has one, you have finished the runbook, and Runbooks marks the session finished. In [instruction mode](/intro/instruction-mode/), ticking **Mark as done** on the <Finish> block does the same. A session is marked finished once: finishing the runbook again in it, or a later failed final check, changes nothing.
Runbooks doesn't resume a finished session on its own. The next time you open its runbook, in the running app or after a restart, the runbook opens in a new session. A notice at the top of the runbook names the finished session, with a **Switch session…** button to go back to it or to another session.
A finished session keeps its directory, files and history, and **Switch Session…** lists it with a **Finished** label. Switching to it opens it as you left it, the <Finish> block finished too, and the notice that it was resumed says when you finished it. A [watch mode](/commands/watch/) reload keeps it open.
Switching to a finished session doesn't make it the one the runbook opens in: it stays finished. If you started another session of the runbook after you last finished it, opening the runbook again resumes the most recent of those that isn't finished. Otherwise it starts a new session.
## Switching sessions
Choose **File > Switch Session…**, or **Switch Session…** in the app's menu, to see the saved sessions. They are grouped by runbook, the open runbook first, and each shows when it was last used and how many scripts it ran. A session you [finished the runbook in](#finishing-a-session) has a **Finished** label. Type in the filter box to narrow the list by session name or runbook.
Click a session to open its runbook in it. Its blocks, environment and working directory come back as [Resuming a session](#resuming-a-session) describes. A remote runbook is cloned again first. A session whose local runbook file has been deleted is listed but can't be opened.
If a script is running, Runbooks asks before switching, since the switch stops every running script.
The session you switch to counts as launched again. Launching Runbooks from the dock resumes it next, and so does `runbooks` run in the directory it was last launched from, unless it is a finished session.
## The session's history
Runbooks adds an entry to the session's history each time you act on a block:
- You change a value in a form, submit the form, or generate its files. The entry has the form's values. Changes to one form that follow each other are one entry, so the history has the form as you left it and not every keystroke. The same goes for a <DirPicker> path and the fields of a <GitPullRequest> form.
- A <Command> or <Check> script, the final check of a <Finish> block, or the check of a <Prerequisites> block, starts, and it ends. The entry for the end has the status, the exit code, the outputs, and the newest 500 lines of the log, up to 64 KiB of text, and the path of the full log. Outputs larger than 512 KiB in total are left out. The resumed block says so, and the blocks that read them wait until it runs again.
- You click **Finish** on a <Finish> block without a final check. The entry is a run that succeeded, with no log.
- A <Template> or <TemplateInline> writes its files. The entry has a hash of what it wrote, not the files.
- A <GitClone> block gets a repository, or you start it over.
- A <GitPullRequest> block opens a pull or merge request, or you set it aside to open another.
- An auth block signs in, or you sign it out.
- The generated files left to [review](/intro/files_workspace/#reviewing-changed-files) change, because a file changed or you reviewed one, and then stay the same for a second. The entry lists them, with what each modified file was when you last reviewed it, up to 512 KiB in total. Past that, a file is kept as modified without what it was, so a resumed session highlights it but not its lines.
The history keeps every entry in order. The app reads only the latest entry of each block, which is what a resumed session shows.
A block also resumes from its latest entry when the runbook is reloaded in [watch mode](/commands/watch/), or closed and opened again without quitting the app.
A resumed session's **Generated** panel lists the files its blocks wrote, and highlights the ones you had yet to review. Runbooks doesn't offer to delete them when the runbook opens: they are the session's own.
## Where sessions are stored
Sessions live in the `v0/sessions` folder of the app's data directory:
| OS | Path |
|----|------|
| macOS | `~/Library/Application Support/Runbooks/v0/sessions` |
| Linux | `~/.config/Runbooks/v0/sessions` |
| Windows | `%APPDATA%\Runbooks\v0\sessions` |
- sessions/
- db/
- sessions.db A SQLite database of the sessions and their histories
- dirs/
- 0199a5c2-7e3b-7c4d-9a1f-3b2c4d5e6f70/ One directory per session
- .runbooks/
- logs/
- deploy/ One directory per block
- 2026-10-02T14-03-11-482Z-k3x9qa.log The full log of each script run
- generated/
- my-cloned-repo/
Every <Command> and <Check> run, and every final check of a <Finish> block, writes its full log to `.runbooks/logs` in the session's directory, in a directory named after the block's `id`. **Copy log file path** on a block copies the path of its last run's log, in a resumed session too. Runbooks keeps the logs as long as the session.
The database records each session's name, its runbook, its directory, its working directory, its cloned repositories, the directory it was last launched from, the host each of its git tokens belongs to, when you finished its runbook, its environment, and its history. A session's directory is named after its id, a UUID that never changes, not after its name. To get the directory of the open session, click the folder button next to its name in the title bar. For any other session, look its name up:
```bash
sqlite3 sessions/db/sessions.db "SELECT dir FROM sessions WHERE name = 'elegant-elephant'"
```
## What is encrypted
The environment can hold credentials: cloud keys an auth block exported, or a token a script set. So can the history: a password typed into a form, a script's sensitive output, a secret a script printed to its log, or the keys and token an auth block signed in with. Runbooks encrypts the environment and the content of every history entry with the operating system's credential store before writing them to the database: Keychain on macOS, DPAPI on Windows, and a secret service such as GNOME Keyring or KWallet on Linux.
On Linux without a secret service, Runbooks saves neither. A resumed session there starts from the environment Runbooks was launched with and with its blocks as new, and keeps its directory and files.
The rest of the database is not encrypted, including which block each history entry belongs to and when it was added. Neither are the files in a session's directory. Those include the full log of every script run, with anything the script printed.
> **Note:**
> [<GoogleAuth>](/authoring/blocks/googleauth/) writes credentials to a temporary file that is deleted when Runbooks quits, and the history keeps no Google credential. A resumed session drops the variables that point to that file. The block signs in again where it can, as [Resuming a session](#resuming-a-session) describes.
## Deleting sessions
Runbooks keeps every session until you delete it. Their cloned repositories add up, and so do their histories and their logs: every script run adds a history entry and a log file.
To delete a session, open **Switch Session…**, click the trash icon next to it, and click the red trash icon to confirm. Its history is deleted. Its directory, with the files its blocks generated, the repositories they cloned and the logs of its script runs, goes to the Trash (the Recycle Bin on Windows), named after the session's id. Restore it from there if you deleted the session by mistake; the files come back, but the session doesn't. If the directory can't be moved to the Trash, Runbooks says why and keeps the session. The open session can't be deleted: switch to another one first.
To remove every session, quit Runbooks and delete the `sessions` folder.
> **Caution:**
> Uninstalling Runbooks on Windows deletes its data directory, sessions included. Push or copy out any work in a session's cloned repositories first.
---
# App & CLI › 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 `<Command>`, `<Check>` or `<Finish>` 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](/commands/sessions/). Environment variables that blocks exported, and the directory a block moved to with `cd`, stay as they were.
- The blocks resume from the [session's history](/commands/sessions/#the-sessions-history), so a reload keeps the values you typed, each script's last run, cloned repositories and sign-ins. A reload that changes the runbook stops a script that is running, and its block then says the run did not finish.
---
# 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.
---
# Authoring Runbooks › 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 `<Check>`, `<Command>`, `<Inputs>`, and `<Template>`.
### 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 has the tools the runbook needs.
<Prerequisites id="pre-flight-checks">
```yaml
tools:
- name: tool
version: ">= 1.0"
install: "[Install the tool](https://example.com/install)"
```
</Prerequisites>
## Execute actions
Run commands on behalf of the user to complete the runbook.
<Command
id="do-something"
env={{ PROJECT_NAME: "{{ .inputs.ProjectName }}" }}
command='echo "$PROJECT_NAME"'
inputsId="config"
title="Execute action"
successMessage="Done!"
/>
## Generate code
Generate code the user needs to accomplish their task.
<Template
id="infra-code"
path="templates/infra"
/>
## Post-flight checks
Verify everything worked.
<Check
id="post-flight-checks"
command="curl https://some-endpoint.com"
title="Check if deployment is successful"
successMessage="Deployment successful"
failMessage="Something went wrong"
/>
## Finish
Celebrate, and tell the user what to do next.
<Finish id="finish">
Next, share the endpoint with your team.
</Finish>
`````
### Chapters
A long runbook that's best worked through in phases can split its content into [chapters](/authoring/blocks/chapter/), which Runbooks shows one at a time, with a table of contents to move between them:
```mdx
# Runbook title
Introduction paragraph, shown with every chapter.
<Chapter title="Pre-flight checks">
...
</Chapter>
<Chapter title="Deploy">
...
</Chapter>
```
Chapters only change what's on screen: a block in one chapter can use the inputs and outputs of a block in any other.
## Front matter
`runbook.mdx` can start with YAML front matter, between two `---` lines. Runbooks doesn't show it.
```mdx
---
title: Deploy the service
runbooks: ">= 0.28"
---
# Deploy the service
```
### Requiring a version of Runbooks
`runbooks` in the front matter names the versions of Runbooks the runbook needs, such as the version that added a block or prop it uses. The [release notes](https://github.com/gruntwork-io/runbooks/releases) list what each version added.
A version of Runbooks that doesn't meet the requirement doesn't open the runbook. Instead, it says which versions the runbook needs and which version it is, and to update Runbooks when a later version would meet the requirement. [Runbook tests](/authoring/testing/#running-tests) fail the same way.
The requirement takes the syntax of a [`<Prerequisites>` tool's `version`](/authoring/blocks/prerequisites/#version-constraints), such as `">= 0.28"`, `"~> 0.28"`, or `">= 0.28, < 1.0"`. Quote it: YAML reads an unquoted `0.30` as the number 0.3. A requirement Runbooks can't read, or front matter that isn't valid YAML, stops the runbook from opening, with an error that says what's wrong.
A version of Runbooks that doesn't have a block the runbook uses doesn't open it either, with or without a requirement: it names the block, and says to update Runbooks or check the block's name. A requirement still helps with a prop, which a version of Runbooks that doesn't have it may ignore, so that the block runs without it.
Versions of Runbooks before 0.28 don't read the requirement, and refuse a runbook with a block they don't have with an error that only says the component isn't defined.
## 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 `<Check>` blocks
- prereq1.sh
- prereq2.sh
- scripts/ Shell scripts for `<Command>` blocks
- deploy.sh
- cleanup.sh
- templates/ Boilerplate templates for `<Template>` 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 | `<Check>` blocks |
| `scripts/` | Shell scripts that execute actions | `<Command>` blocks |
| `templates/` | Boilerplate template directories | `<Template>` blocks |
### Relative paths
Reference files relative to the runbook's folder:
```mdx
<Check id="prereq" path="checks/prereq1.sh" />
<Template id="infra" path="templates/my-template" />

<img src="./assets/diagram.png" width="400" />
<video src="./assets/demo.mp4" poster="./assets/demo-poster.png" controls />
<Iframe src="./assets/dashboard/index.html" />
```
A script `path` on a `<Command>`, `<Check>` or `<Finish>` block must stay inside the runbook's folder: a path with a `..` segment fails.
These paths are for block props. A script itself starts in the [session's directory](/commands/sessions/), not in the runbook's folder, so it can't use them to find files next to the runbook. See [Execution context](/authoring/blocks/command/#execution-context).
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` and its scripts to any page the runbook embeds. Symlinks inside `assets/` work when they point to files inside `assets/`.
The [`<Iframe>`](/authoring/blocks/iframe/) block's `src` also takes a `./assets/` path.
---
# Authoring Runbooks › 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.
<Admonition type="info" title="Note" description="This is a React component!" />
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
<Command id="build" command="make build" timeoutMs={300000} />
<AwsAuth id="prod-auth" detectCredentials={[{ env: { prefix: 'PROD_' } }, 'env']} />
```
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:
```mdx
<div style={{ color: "red" }}>
This text will be red.
</div>
```
MDX reads HTML as JSX, so it follows JSX's rules rather than HTML's:
- Close every element, including void elements such as `<br />`, `<hr />` and `<img ... />`. A bare `<br>` fails with "Expected a closing tag".
- Give `style` an object, as above. A string such as `style="color: red;"` fails when the runbook renders.
- Write comments as `{/* comment */}`. An HTML comment, `<!-- comment -->`, fails to compile.
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\`
```
In MDX, `<` before a letter starts a tag and `{` starts an expression, so escape those too, or put them in inline code:
```mdx
Use the \<Inputs\> block to collect values.
Write `{{ .inputs.ProjectName }}` in a block's props to use the value.
```
Unescaped, `Use the <Inputs> block` fails with "Expected a closing tag for `<Inputs>`", and `{{ .inputs.ProjectName }}` fails with "Could not parse expression with acorn". Template expressions aren't resolved in prose. To show a value the user entered, use a block that resolves it, such as an [`<Admonition>`](/authoring/blocks/admonition/) with `inputsId`.
### Code blocks inside blocks
Blocks such as `<Inputs>` take a fenced code block as their content:
````mdx
<Inputs id="my-form">
```yaml
variables:
- name: Example
type: string
```
</Inputs>
````
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.
---
# Authoring Runbooks › 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 `<Inputs>` and `<Template>` collect values from the user through a form, and consumer blocks pull them in with the [`inputsId` prop](#wiring-blocks-with-inputsid). Other blocks, such as `<Command>`, `<Check>`, `<GitClone>` and `<DirPicker>`, 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 }}`. A block gets the values from the input providers it names with [`inputsId`](#wiring-blocks-with-inputsid). For example:
`````mdx
<Inputs id="account">
```yaml
variables:
- name: Email
type: string
- name: AccountName
type: string
```
</Inputs>
<Command
id="create-account"
inputsId="account"
env={{ EMAIL: "{{ .inputs.Email }}", ACCOUNT_NAME: "{{ .inputs.AccountName }}" }}
command='aws organizations create-account --email "$EMAIL" --account-name "$ACCOUNT_NAME"'
/>
`````
When this block runs, Runbooks fills in `{{ .inputs.Email }}` and `{{ .inputs.AccountName }}` with the values entered in the `account` form, such as `alice@example.com` and `my-account`, and the command gets them as the environment variables `EMAIL` and `ACCOUNT_NAME`. See [Passing values as environment variables](/authoring/blocks/command/#passing-values-as-environment-variables). A script can also reference `{{ .inputs.Email }}` itself, which inserts the value into the script's source; see [Templates in the script](/authoring/blocks/command/#templates-in-the-script).
### 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`, `length`, `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 }}`. In the files a `<Template>` renders, and in variable defaults that are themselves templates, every input is also available at the root of the context, so templates written for the Boilerplate CLI can keep using `{{ .VarName }}`. Everywhere else, such as Command and Check scripts, `<TemplateInline>` content and block props, `{{ .VarName }}` fails. Write `{{ .inputs.VarName }}` there.
> **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.
### <Inputs>
The [`<Inputs>`](/authoring/blocks/inputs/) block renders a form from a `boilerplate.yml` definition. This inline definition declares one variable, `ProjectName`:
`````mdx
<Inputs id="config">
```yaml
variables:
- name: ProjectName
type: string
description: Name for your project
```
</Inputs>
`````
### <Template>
The [`<Template>`](/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
<Template id="infra" path="templates/rds" />
```
Here the variables come from `templates/rds/boilerplate.yml`.
## Wiring blocks with `inputsId`
Any block that consumes variables, such as `<Command>`, `<Check>`, `<Finish>`, `<Template>` or `<TemplateInline>`, accepts an `inputsId` prop naming an input provider by its `id`. This Command uses the value collected by the Inputs block:
`````mdx
<Inputs id="config">
```yaml
variables:
- name: ProjectName
type: string
```
</Inputs>
<Command id="create-dir" inputsId="config" env={{ PROJECT: "{{ .inputs.ProjectName }}" }} command='mkdir "$PROJECT"' />
`````
Values reach a block only through `inputsId` or a nested `<Inputs>`. A Command or Check that references `{{ .inputs.* }}` without either shows a **Configuration Required** message in place of its button.
### 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
<Command
id="deploy"
inputsId={["global-config", "local-config"]}
env={{ ENVIRONMENT: "{{ .inputs.Environment }}", APP_NAME: "{{ .inputs.AppName }}" }}
command='deploy --env "$ENVIRONMENT" --name "$APP_NAME"'
/>
```
### Embedded inputs
An `<Inputs>` block nested inside a `<Command>` or `<Check>` feeds the parent without `inputsId`:
`````mdx
<Command id="greet" env={{ NAME: "{{ .inputs.Name }}" }} command='echo "Hello, $NAME!"'>
<Inputs id="greeting">
```yaml
variables:
- name: Name
type: string
```
</Inputs>
</Command>
`````
Other blocks can still reference the embedded Inputs with `inputsId="greeting"`.
## When variables overlap
A consuming block receives every variable from its input provider.
`<Template>` 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
Scripts in [`<Command>`](/authoring/blocks/command/), [`<Check>`](/authoring/blocks/check/) and [`<Finish>`](/authoring/blocks/finish/) blocks produce outputs by writing `key=value` lines 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, and reads the file when the script exits with code 0 or 2. Each line is one output:
- The key starts with a letter or underscore and contains only letters, digits and underscores. Runbooks ignores a line with any other key, such as `account-id=...`, and a line without an `=`.
- The value is everything after the first `=`, up to the end of the line. Put a multi-line value on one line, for example as JSON or base64.
- When a script writes the same key more than once, the last value wins.
Other blocks publish outputs of their own, which their pages list: the auth blocks, such as [`<AwsAuth>`](/authoring/blocks/awsauth/) and [`<GitAuth>`](/authoring/blocks/gitauth/), [`<GitClone>`](/authoring/blocks/gitclone/), [`<GitPullRequest>`](/authoring/blocks/gitpullrequest/) and its wrappers, [`<DirPicker>`](/authoring/blocks/dirpicker/), and an [`<Iframe>`](/authoring/blocks/iframe/) whose local page sets outputs.
A [`<Prerequisites>`](/authoring/blocks/prerequisites/) block publishes where it found each tool and the versions it read, such as `{{ .outputs.<block_id>.tofu_version }}`. See [Outputs](/authoring/blocks/prerequisites/#outputs).
### 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 `<AwsAuth detectCredentials={[{ block: '<block-id>' }]} />`, find it by its usual name too.
Runbooks hands a sensitive output's real value only to what uses it:
- A Command, Check or Finish script that references it in a template runs with the real value.
- A [`<Template>`](/authoring/blocks/template/), or a [`<TemplateInline>`](/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 `<TemplateInline>` 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 `<TemplateInline>` 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 `<TemplateInline>` 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:`. [`<AwsAuth>`](/authoring/blocks/awsauth/) publishes `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` as sensitive outputs. [`<GitAuth>`](/authoring/blocks/gitauth/), [`<GitHubAuth>`](/authoring/blocks/githubauth/) and [`<GitLabAuth>`](/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 `<Template>` or a `<TemplateInline>` 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 [`<AwsAuth>`](/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 the `env` prop and the scripts of `<Command>`, `<Check>` and `<Finish>` blocks, `<Template>` files, and `<TemplateInline>` content.
An output often holds text from outside the runbook, such as a PR title or an API response. To use one in a script, pass it with [`env`](/authoring/blocks/command/#passing-values-as-environment-variables): the script gets the value as data, so nothing in it runs, whatever it holds.
```mdx
<Command
id="tag-account"
env={{ ACCOUNT_ID: "{{ .outputs.create_account.account_id }}" }}
command='aws organizations tag-resource --resource-id "$ACCOUNT_ID" --tags Key=team,Value=platform'
/>
```
> **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 (click a name to scroll to that block), 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).
A Command, Check or Finish block that reads an output through its `env` prop lets you enter the value yourself instead, and then doesn't wait for the upstream block. See [Entering an output by hand](/authoring/blocks/command/#entering-an-output-by-hand). So does a form field whose default uses an output: see [Defaults that use other variables](/authoring/boilerplate/#defaults-that-use-other-variables).
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 are saved in the runbook's [session](/commands/sessions/). When the runbook reloads or the session resumes, blocks that ran publish their outputs again, except an `<Iframe>`. See [Resuming a session](/commands/sessions/#resuming-a-session).
### Outputs a block emits only sometimes
Some outputs exist only some of the time. `<GitClone>` 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 `<GitClone>` is reset or a `<Command>` 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 `<Command>` 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 `<Command>` and `<Check>` scripts and their `env` values, `<Template>` files and `<TemplateInline>` content. Each `env` value is its own template, so a guard in one doesn't cover a reference in another or in the script. Props the app fills in itself can't evaluate `if`, so every output they reference is required: the `prefilled*` props of `<GitClone>` and `<GitPullRequest>`, and the `outputPath` of `<TemplateInline>`.
### 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
```
---
# Authoring Runbooks › 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.
---
# Authoring Runbooks › 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 `<Inputs>` and `<Template>` build their forms from, and the Go template syntax that `<Template>`, `<TemplateInline>`, `<Command>`, and `<Check>` 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 (off in Runbooks; see [Helper functions](#helper-functions)), 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, referenced as `{{ .inputs.Name }}`, or as `{{ .Name }}` in the files a `<Template>` renders |
| `type` | No | Data type: `string`, `int`, `float`, `bool`, `enum`, `list`, `map` (defaults to `string`). Write it in lowercase. The form shows a field of any other type as text, but Boilerplate rejects it, so a `<Template>` that uses it fails |
| `description` | No | Help text shown in the form |
| `default` | No | Default value. Once a field's value differs from it, a **Reset** button next to the field's name puts it back |
| `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 |
| `order` | No | An integer that places the field in the form, lowest first. See [Field order](#field-order) |
### Field order
The form shows fields in the order `boilerplate.yml` declares them. To place a field elsewhere, give it an `order`, the integer Boilerplate also prompts for variables by. Fields appear lowest `order` first, a variable without one counts as `0`, and fields with the same `order` keep the order they're declared in:
```yaml
variables:
- name: Region
type: string
default: us-east-1
- name: AccountName
type: string
order: -1
- name: Tags
type: map
default: {}
order: 10
```
This form shows Account Name, then Region, then Tags.
In a form with [sections](#x-section), `order` sorts the fields within each section, and a section appears where its first field would. Fields without an `x-section` stay at the top.
### 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 `<Template>` or `<TemplateInline>` block, and when a `<Command>` or `<Check>` script reads the variable through `inputsId`.
Other block props that use the variable through `{{ .inputs.X }}`, like `<GitClone>`'s `prefilledUrl` or `<TemplateInline>`'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. It shows a linked default as its text, with a chip for each variable or block output it uses. A chip shows the value it links to, with a link icon, and follows that field as you edit it. Here `BucketName` shows an `acme` chip followed by `-state`. A default that does more than insert a variable or an output shows as one chip with the value it comes to, so `InstanceSize` shows `small` until you pick `prod`. In a `<Template>`, a default can also use the values the block gets through `inputsId`.
Until a chip's value is known, it names what it links to instead: a variable, labelled the way its field is, or an output, as `create_account.account_id`. A chip for a default that does more says what it's based on, like **Based on Environment**. A chip for a block output that hasn't been produced yet 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 chip 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 shows one chip that names the variables its default uses, never its text or its value. A chip for a sensitive variable shows only its name, and one for a sensitive output shows `<redacted>`.
To change a linked value:
- **Enter a value for a chip.** Click the chip and it turns into a field, in its place, where you can type another value. Press Enter, or click away after changing it, to use it. Press Escape to get the chip back. The field then uses that value everywhere it used the chip's variable or output, and the rest stays linked. A value entered for an output means the field no longer waits for, or needs, the block that produces it.
- **Reset goes back to the link.** A value you entered stays in its field, where you can change it, with a reset button next to it that brings the chip back.
- **Click the X** to clear the whole value and type your own.
- **Click Reset** next to the field's name to go back to the default, as the runbook wrote it, chips and all.
- **Hover** over the value to see its expression.
A value you enter for a sensitive field, variable or output is hidden as you type it. A [resumed session](/commands/sessions/#resuming-a-session) brings back the values you entered.
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
```
Only an `<Inputs>` block's `boilerplate.yml` can use this form. Boilerplate itself accepts only the string form, so a `<Template>` whose `boilerplate.yml` uses the object form fails with "validation rule at index 0 must be a string".
### 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)
```
The form and [`runbooks test`](/authoring/testing/) don't check `length(...)` and `regex(...)` rules. Boilerplate checks them when a `<Template>` renders, so a value that breaks one fails generation there, and in an `<Inputs>` block the rules have no effect.
## Template syntax
Template files use Go template syntax. In the files a `<Template>` renders, reference a variable from `boilerplate.yml` with dot notation, as in `{{ .BucketName }}`, or as `{{ .inputs.BucketName }}`. Everywhere else in a runbook, such as a `<TemplateInline>` template, a `<Command>` or `<Check>` script, or a block's props, only `{{ .inputs.BucketName }}` works. See [Inputs and outputs](/authoring/inputs-and-outputs/#template-syntax).
### 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. With `ProjectName` set to `my-project`:
```yaml
# String case transformations
{{ .ProjectName | snakeCase }} # my_project
{{ .ProjectName | camelCase }} # MyProject
{{ .ProjectName | camelCaseLower }} # myProject
{{ .ProjectName | dasherize }} # 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"
```
Runbooks turns off the `shell` helper. In a runbook, `{{ shell "..." }}` doesn't run its command and gives `replace-me` instead. Boilerplate renders variable values as templates, including the values people type and the outputs of earlier blocks, so with `shell` on, a value could run a command. Run commands in a [`<Command>`](/authoring/blocks/command/) block, and pass what they produce to the template as a block output.
## 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.
Together with [`order`](#field-order), they lay the form out for the person filling it in: [`x-section`](#x-section) groups fields under headings, [`x-sections`](#x-sections) folds the groups they rarely need, and [`x-locked`](#x-locked) and [`x-hidden`](#x-hidden) keep them from changing values the runbook sets itself. The [form-layout demo runbook](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/form-layout) uses each one.
### `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. The other sections follow in the order of their first field, which [`order`](#field-order) can change.
### `x-sections`
A top-level list of settings for the sections that `x-section` names. `collapsed: true` folds a section under its heading until someone clicks it open, so fields whose defaults rarely need changing stay out of the way:
```yaml
x-sections:
- name: Advanced Settings
collapsed: true
variables:
- name: FunctionName
type: string
x-section: Basic Settings
- name: MemorySize
type: int
default: 128
x-section: Advanced Settings
- name: Timeout
type: int
default: 30
x-section: Advanced Settings
```
The fields of a folded section keep their values, and the block uses them as it does any other field's. If one of them has an error, such as a required field that is still empty when you click **Generate**, the section opens to show it. So give the fields of a folded section defaults that work.
### `x-locked`
`x-locked: true` shows a field read-only, marked **(locked)**. Its value is the variable's `default`, or in a `<Template>`, the value the block [imports through `inputsId`](/authoring/blocks/template/#variable-categories). Nobody filling in the form can change it.
A locked `default` that [uses other variables or block outputs](#defaults-that-use-other-variables) still follows them. This field shows the provider that the `<GitAuth>` block with the id `git-auth` signed in to, from its [`GIT_PROVIDER` output](/authoring/blocks/gitauth/#block-outputs):
```yaml
- name: GitProvider
type: enum
options: [github, gitlab]
default: "{{ .outputs.git_auth.GIT_PROVIDER }}"
x-locked: true
```
When a session is resumed, a locked field starts from the runbook's value, not from the one the session saved, because the runbook may have changed it since. If a locked field's value is invalid, for example because its `default` uses a block that isn't on the page, the field shows the error, and the runbook itself has to be fixed.
### `x-hidden`
`x-hidden: true` leaves a field out of the form. A hidden field is locked too: the block uses its `default`, or the value it imports, as is. Use it for values the runbook works out by itself, such as the provider above when there's no need to show it:
```yaml
- name: GitProvider
type: enum
options: [github, gitlab]
default: "{{ .outputs.git_auth.GIT_PROVIDER }}"
x-hidden: true
```
A hidden field is still validated. If its value is invalid, for example because it's `required` and empty, the form shows the field, locked, with its error, so the block never fails on something you can't see. A section whose fields are all hidden doesn't appear.
### `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. A field's type sets its control: `bool` renders a true/false dropdown, `int`, `float` and `number` render a number input, and any other type renders a text input.
On a `list` variable, `x-schema` makes the list a fixed-length tuple. Its keys are the element positions, `"0"`, `"1"` and so on, and its values are the element types, with the same controls as a map's fields:
```yaml
- name: Endpoint
type: list
description: Host and port
x-schema:
"0": string
"1": int
```
The form renders one input per element, in position order.
### `x-schema-instance-label`
Sets the label for each entry of a schema-based map. It defaults to "Entry name":
```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 `<Template>`:
```mdx
# Deploy a Lambda Function
Configure your Lambda function below:
<Template id="lambda-config" path="templates/lambda" />
After generating, review the files in the file tree on the right.
```
For a template that doesn't need its own directory, pair `<Inputs>` with `<TemplateInline>`:
`````mdx
<Inputs id="config">
```yaml
variables:
- name: BucketName
type: string
```
</Inputs>
<TemplateInline id="bucket-preview" inputsId="config" outputPath="bucket.tf">
```hcl
resource "aws_s3_bucket" "main" {
bucket = "{{ .inputs.BucketName }}"
}
```
</TemplateInline>
`````
---
# Authoring Runbooks › 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 `<GitClone>` runs, as a duration such as `500ms`, `30s`, `5m`, or `1h`. Tests ignore a block's `timeoutMs`, so this limit applies to every script. 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 `<Iframe>` 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 `<AwsAuth>` 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, `<Check id="verify-install" ... />` 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 `<Template>` rendered, the file a `<TemplateInline>` with `generateFile` wrote, or the files a `<Command>` or `<Check>` 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 `<Inputs>` or `<Template>` block gets the value the app's form starts it with, with the two exceptions below, 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.
Set the variable in the test in these two cases, where the runner doesn't start it the way the form does:
- The `<Inputs>` block gives it a value with `prefilledVariables`. The runner ignores that prop.
- Its `default` is built from other variables, such as `"{{ .ProjectName }}-state"`. The app fills in the other variables' values, but the runner uses the text as written.
When a `<Template>` 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 `<Template id="vpc" inputsId="config" />`. 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.
#### Which blocks get a value
As in the app, a block's `{{ .inputs.Name }}` comes only from the blocks it imports. Those are the blocks its `inputsId` names, merged in order so that a later block's value wins, and then an `<Inputs>` nested in a `<Check>` or `<Command>`. A `<Template>` adds its own variables on top of the ones it imports, and a block whose `inputsId` names a Template gets both.
So when two `<Inputs>` blocks define `Region`, setting `config.Region` changes it only for the blocks that import `config`. A block that imports nothing has no inputs: if its script reads one, the test fails, and the app shows **Configuration Required** instead of running it.
### 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.
## How tests differ from the app
The runner runs your scripts and renders your templates, but not always as the app does. A test can pass while the runbook fails in the app, or the other way around:
- **Template syntax.** The runner renders scripts, `env` values, templates and block props with its own renderer. It handles only references such as `{{ .inputs.Name }}` and `{{ .outputs.block_id.name }}`, `{{ if .inputs.Flag }}...{{ else }}...{{ end }}` without nesting, and `{{ fromJson .outputs.block_id.name }}`. Anything else is left in the result as written, including `eq` and the other functions, pipes such as `| upper`, `range`, `with`, the raw-string form, and the `{{-` and `-}}` markers. A reference inside one of those, such as the output behind a `hasKey` guard, is still filled in, or reported as undefined.
- **Working directory.** In the app, scripts start in the [session's directory](/commands/sessions/), never in the runbook's folder. Under test they start in the [working directory](#settings-object): a fresh temporary directory by default, or the runbook's folder with `working_dir: .`. So a script that reads a file next to `runbook.mdx` can pass with `working_dir: .` and fail in the app.
- **Script paths.** The app refuses a `path` with a `..` segment, such as `../shared/setup.sh`, and shows an error in place of the block. The runner reads the file anyway.
- **Timeouts.** A block's `timeoutMs` doesn't apply. Every script gets the [`timeout` setting](#settings-object), 5 minutes by default.
- **A Command that exits 2.** The runner reports it as `warn`, and the blocks whose `dependsOn` names it can run. In the app, the Command shows as failed and those blocks keep waiting. A `<Check>` that exits 2 lets them run in both.
- **Auth blocks.** The runner ignores `detectCredentials`. It reads the environment variables listed in each auth block's section below, and for `<GoogleAuth>` gcloud's ADC file, whatever the prop says. A block with `detectCredentials={false}` still succeeds when they are set, a `{ block: ... }` source is never used, and a prefix in an `{ env: { prefix } }` source has no effect, since only the step's `env_prefix` sets one. The runner also checks only that an auth block has an `id`. Configuration errors that the app reports for an auth block, such as two `{ block }` sources, don't appear, and an `expect: config_error` step for one fails.
- **Template blocks.** A `<Template>` renders each file with the runner's renderer. File and directory names aren't rendered, files and directories whose names start with `.` aren't written, and `boilerplate.yml` is read only for its variables, so settings such as `skip_files` are ignored.
- **TemplateInline blocks.** The runner reports a `<TemplateInline>` without `outputPath` as a configuration error. In the app, `outputPath` defaults to `template.txt`.
## Testing AwsAuth blocks
For an `<AwsAuth>` 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.
The runner sets `AWS_REGION` only from the `AWS_REGION` variable. The app falls back to `AWS_DEFAULT_REGION` and then to the block's `defaultRegion`, but the runner doesn't, so under test a script that reads `$AWS_REGION` gets nothing when `AWS_REGION` isn't set.
### 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. A prefixed source in the app never falls back like this, so a test whose prefixed variables are missing can still pass, using whatever AWS credentials the machine running it has.
### 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 `<GoogleAuth>` 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. The app works the other way around: the `project` prop wins over a detected project, and the region and zone come from the block's region picker, which starts at `defaultRegion`, and from `defaultZone`, not from these variables.
On success the runner writes the project, region, and zone under every name that gcloud, the client libraries, and the OpenTofu `google` provider each look for, as the app does: `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 is written as both `GOOGLE_OAUTH_ACCESS_TOKEN` and `CLOUDSDK_AUTH_ACCESS_TOKEN`, and clears the override.
Two things differ from the session environment the app writes:
- Inline JSON in `GOOGLE_CREDENTIALS` is passed on as it is, and `GOOGLE_APPLICATION_CREDENTIALS` and `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE` are cleared. So only tools that read `GOOGLE_CREDENTIALS`, such as the OpenTofu `google` provider, find the credential, and `gcloud` and the client libraries don't. The app writes the JSON to a file and points both variables at it.
- `CLOUDSDK_CORE_ACCOUNT` isn't set.
### 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, and then to gcloud's ADC file. A prefixed source in the app never falls back, so a test whose prefixed variables are missing can still pass on your own Google credentials.
### CI example
```yaml
# .github/workflows/test.yml
- name: Authenticate to Google Cloud
id: auth
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 `<GitHubAuth>` block, the test runner looks for a token for the block's host: the one its `host` prop names, or `github.com` without one. It ties each environment variable to a host the way the app does, so a token is only used for its own host:
- For `github.com`: `RUNBOOKS_GITHUB_TOKEN` (recommended for tests), then `GITHUB_TOKEN`, then `GH_TOKEN`. The last two count only while `GH_HOST` is unset or names `github.com` or a GitHub Enterprise Server host.
- For a `*.ghe.com` tenant: `GITHUB_TOKEN`, then `GH_TOKEN`, when `GH_HOST` names the tenant.
- For a GitHub Enterprise Server host: `GH_ENTERPRISE_TOKEN`, then `GITHUB_ENTERPRISE_TOKEN`, when `GH_HOST` names the host.
On success, the blocks after it see `GITHUB_TOKEN` and `GITHUB_HOST`. The app also sets `GITHUB_USER` and `GH_HOST`, and for an Enterprise Server host `GH_ENTERPRISE_TOKEN`, so under test a script has those only if the environment the tests run in sets them.
A `host` that isn't a valid host is a configuration error in the app. Under test, the block uses `github.com` instead.
### 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`, or for an Enterprise Server host `CI_GH_ENTERPRISE_TOKEN` and `CI_GITHUB_ENTERPRISE_TOKEN`. `CI_GH_HOST` ties them to a host, the way `GH_HOST` does for the unprefixed names. `RUNBOOKS_GITHUB_TOKEN` isn't read with a prefix.
### 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 `<GitLabAuth>` 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`). The app reads only the first two of those with a prefix.
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. Unlike the app, the runner ties a prefixed token to a host with these unprefixed variables too. The app uses the prefixed ones, such as `CI_GITLAB_HOST`.
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 `<GitAuth>` block is tested like `<GitHubAuth>` or `<GitLabAuth>`, depending on its `provider` prop (`github` by default).
## Testing GitClone blocks
A `<GitClone>` clones its `prefilledUrl` into the working directory. With `source="local"` or a `prefilledRepoDir`, it uses that checkout instead. A block without the prop it needs has nothing to clone, so its step is skipped. A test with no `steps` expects every block to succeed, so give the block its `prefilledUrl` or `prefilledRepoDir`, or list it as a step with `expect: skip`.
The block's outputs under test differ from the app's:
- It sets `clone_path` and `file_count`, and `ref` for a clone with a `prefilledRef`. `file_count` and `ref` exist only under test.
- It doesn't set `repo_owner`, `repo_name`, `org_id` or `repo_id`. A block that references one of them fails under test, even behind a `hasKey` guard.
Each `<GitClone>` that succeeds also becomes the repository that `$REPO_FILES` and `target="worktree"` use, so with several of them the last one to run wins. In the app, the first repository added, whether by a clone or a [local checkout](/authoring/blocks/gitclone/#using-a-local-checkout), stays in use until the user selects another.
### Cloning with a token
A `<GitClone>` 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
`<GitPullRequest>`, `<GitHubPullRequest>`, and `<GitLabMergeRequest>` 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 Finish blocks
A `<Finish>` with a final check (`command` or `path`) runs it like a `<Check>`: exit code 0 is `success`, 2 is `warn`, and anything else is `fail`. A `<Finish>` without a final check has nothing to run, so its step succeeds straight away. Either way, the test can't see the celebration.
```yaml
# runbook_test.yml
tests:
- name: final-check-fails-without-setup
steps:
- block: setup
expect: skip
- block: finish
expect: fail
```
## Testing Prerequisites blocks
A step for a `<Prerequisites>` block looks for its tools and reads their versions as the app does, on the PATH the test runs with: the environment `runbooks test` started with, the test's `env`, and what earlier steps exported. The step's status is the check's: `success` when every tool is found (one tool of an [`anyOf` list](/authoring/blocks/prerequisites/#alternatives) is enough), `warn` when only optional ones aren't, and `fail` when a required one isn't. A step that succeeds or warns publishes the block's [outputs](/authoring/blocks/prerequisites/#outputs).
Unlike the app, the test doesn't run your login shell to read PATH again when a tool is missing. To test a check that a runbook's install step makes pass, run the block before and after that step:
```yaml
# runbook_test.yml
tests:
- name: install-then-check-again
steps:
- block: tools
expect: fail
- block: install-tool
expect: success
- block: tools
expect: success
```
## Testing Iframe blocks
Tests can't run the page an [`<Iframe>`](/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 `<Iframe>` 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 `<Iframe>` with an `id` that way.
## Testing DirPicker blocks
The runner doesn't run [`<DirPicker>`](/authoring/blocks/dirpicker/) blocks. A step that names one fails the test with `references unknown block`, and a test with no `steps` leaves the block out. Its `PATH` output never exists under test, so a block that reads it can only be tested with `expect: blocked` or `expect: skip`.
## 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.
If `runbook.mdx` isn't valid MDX, or contains something that [Runbooks refuses to render](/security/execution-model/#opening-a-runbook-never-runs-its-code), the runner fails it before any block runs and names the line with the error.
The runner is the same version as the app it comes with, which `runbooks-cli --version` prints. Like the app, it fails a runbook whose front matter [requires another version of Runbooks](/authoring/runbook-structure/#requiring-a-version-of-runbooks) before any block runs, and fails every test of a runbook with a block this version doesn't have.
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.
### Warnings
The runner warns about each `<Command>`, `<Check>` and `<Finish>` script that inserts `{{ .inputs.X }}` or `{{ .outputs.B.K }}` into its source, since [`env`](/authoring/blocks/command/#passing-values-as-environment-variables) passes those values as data. The warnings appear under the runbook's name and are counted in the summary line. In a JUnit report they go in the suite's `<system-err>`. A warning doesn't fail a test.
### 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.
---
# Authoring Runbooks › Blocks › Overview
Blocks are the interactive components of a `runbook.mdx` file. Write them as JSX tags within your markdown.
## Available blocks
These are all the blocks a runbook can use. A component that isn't listed here, such as `<Tabs>`, fails to render.
### Collect input and generate files
- [`<Inputs>`](/authoring/blocks/inputs) renders a form whose values other blocks use in their scripts, templates and props.
- [`<Template>`](/authoring/blocks/template) generates files from a Boilerplate template directory, with a form for the template's variables.
- [`<TemplateInline>`](/authoring/blocks/templateinline) renders a Boilerplate template written in the runbook itself and shows the result inline.
- [`<DirPicker>`](/authoring/blocks/dirpicker) selects a directory path with a cascading set of dropdowns.
### Run scripts
- [`<Command>`](/authoring/blocks/command) runs a shell command or script that changes state, such as a deployment.
- [`<Check>`](/authoring/blocks/check) runs a shell command or script that reports whether the user's system is in the expected state.
- [`<Prerequisites>`](/authoring/blocks/prerequisites) checks that the tools the runbook needs are installed, at the versions it needs, and says how to install the ones that aren't.
- [`<Finish>`](/authoring/blocks/finish) marks the end of the runbook. It can run a final check, then shows your next steps.
### Authenticate
- [`<AwsAuth>`](/authoring/blocks/awsauth) signs in to AWS with static credentials, AWS SSO, or a local AWS profile.
- [`<GoogleAuth>`](/authoring/blocks/googleauth) signs in to Google Cloud with a service account key, a Google sign-in, or a gcloud configuration.
- [`<GitAuth>`](/authoring/blocks/gitauth) signs in to GitHub or GitLab, chosen from a dropdown.
- [`<GitHubAuth>`](/authoring/blocks/githubauth) is `<GitAuth>` locked to GitHub.
- [`<GitLabAuth>`](/authoring/blocks/gitlabauth) is `<GitAuth>` locked to GitLab.
### Work with git repositories
- [`<GitClone>`](/authoring/blocks/gitclone) clones a git repository, or uses a checkout the user already has on disk.
- [`<GitPullRequest>`](/authoring/blocks/gitpullrequest) commits the changes in a cloned repository and opens a GitHub pull request or a GitLab merge request.
- [`<GitHubPullRequest>`](/authoring/blocks/githubpullrequest) is `<GitPullRequest>` locked to GitHub.
- [`<GitLabMergeRequest>`](/authoring/blocks/gitlabmergerequest) is `<GitPullRequest>` locked to GitLab.
### Show content
- [`<Admonition>`](/authoring/blocks/admonition) renders a note, info, warning, or danger callout.
- [`<Chapter>`](/authoring/blocks/chapter) splits a long runbook into chapters, shown one at a time with a table of contents.
- [`<Iframe>`](/authoring/blocks/iframe) embeds a web page: an external site, or an HTML page from the runbook's `assets/` folder.
## 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 `<Inputs>` or `<Template>` 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.
- `dependsOn` names the `<Command>` or `<Check>` blocks that must succeed before a `<Command>`, `<Check>` or `<Finish>` can run. A `<Check>` that ends with a warning counts too. See [Run order](/authoring/blocks/command/#run-order).
- `gitCloneId` roots a `<DirPicker>` in the repository a `<GitClone>` 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. `<TemplateInline>` doesn't, and neither does an `<Inputs>` 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.
## Progress
The title bar shows how far through the runbook the user is: a bar along its bottom edge fills in, and a count next to the session name, such as "2 of 5 steps done", says how many steps are done. A window narrower than 1024 pixels shows only the bar.
Each block the user acts on is a step. It is done once the block shows it's done, and stops being done if the block goes back:
| Block | Done when |
|-------|-----------|
| `<Command>` | Its last run succeeded. |
| `<Check>` | Its last run succeeded, or ended with a warning, which also lets the blocks that depend on it run. |
| `<Prerequisites>` | Its last check found every required tool. A missing optional tool doesn't matter. |
| `<Finish>` | The runbook is finished. A final check that ends with a warning leaves it unfinished. |
| `<Inputs>` | Its form was submitted, and is green. |
| `<Template>` | Its files were generated, and its form is green. |
| `<AwsAuth>`, `<GoogleAuth>`, `<GitAuth>`, `<GitHubAuth>` and `<GitLabAuth>` | It is signed in, and green: credentials that expire within five minutes turn it red. |
| `<GitClone>` | It cloned or found its repository, which is still on the session's branch. |
| `<GitPullRequest>`, `<GitHubPullRequest>` and `<GitLabMergeRequest>` | It opened its pull or merge request. |
| `<DirPicker>` | It has a path. |
`<TemplateInline>`, `<Admonition>`, `<Chapter>` and `<Iframe>` aren't steps, as there is nothing in them for the user to do. The blocks inside a `<Chapter>` are steps as they would be anywhere else, and its table of contents counts them per chapter. Neither is an `<Inputs>` nested in a `<Command>` or `<Check>`, which is part of that block, nor a block that shows a configuration error, such as a duplicate ID, in its place.
A [resumed session](/commands/sessions/#resuming-a-session) brings the blocks back as the user left them, so their steps are done as they were. In [instruction mode](/intro/instruction-mode/#tracking-your-progress), a step is done once the user marks it as done.
## Folding done blocks
A block that the user acts on once and then doesn't need to look at again, such as a `<Command>` that installs tools or a sign-in, can fold out of the way once it's done. Set `autoCollapse` on it:
```mdx
<Command id="install-tools" title="Install the tools" command="brew bundle" autoCollapse />
```
Once the block is done, as in the [Progress](#progress) table, it folds down to its title: its status icon, title and **ID** badge stay where they were, and the rest is hidden. Click anywhere on it to show the block in full, and the arrow beside its **ID** badge to fold it again. A `<Command>` or `<Check>` without a title shows its command, or its script's path, in place of one.
The block folds each time it becomes done, such as when a run succeeds again, and shows in full whenever it isn't done, such as a sign-in whose credentials are about to expire. A [resumed session](/commands/sessions/#resuming-a-session) brings its done blocks back folded. Folding only hides a block: its logs, outputs and values stay as they were, for the blocks that use them.
`autoCollapse` works on `<Command>`, `<Check>`, `<Prerequisites>`, `<AwsAuth>`, `<GoogleAuth>`, `<GitAuth>`, `<GitHubAuth>`, `<GitLabAuth>`, `<GitClone>`, `<GitPullRequest>`, `<GitHubPullRequest>` and `<GitLabMergeRequest>`. In [instruction mode](/intro/instruction-mode/#tracking-your-progress), these blocks fold once the user marks them as done.
---
# Authoring Runbooks › Blocks › <Admonition>
The `<Admonition>` block renders a callout box for a note, info, warning, or danger message.
## Basic usage
```mdx
<Admonition
type="info"
title="Important Note"
description="Make sure you have AWS credentials configured before proceeding."
/>
```
## 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
<Admonition
type="note"
title="Note"
description="This is a general note for additional context."
/>
```
### Info (blue)
```mdx
<Admonition
type="info"
title="Helpful Tip"
description="You can use environment variables instead of hardcoding values."
/>
```
### Warning (yellow)
```mdx
<Admonition
type="warning"
title="Caution"
description="This operation cannot be undone. Make sure you have backups."
/>
```
### Danger (red)
```mdx
<Admonition
type="danger"
title="Danger"
description="This will delete all data in your production database!"
/>
```
## Markdown content
You can pass the content as children in place of the `description` prop. Children support full markdown, such as lists, code blocks, and links:
````mdx
<Admonition type="info" title="Prerequisites">
Before proceeding, you need:
- AWS CLI installed
- OpenTofu v1.6+
- Valid AWS credentials configured
Check your credentials with:
```bash
aws sts get-caller-identity
```
</Admonition>
````
Put the content on lines of its own, between the opening and closing tags. Content that starts on the same line as the opening tag must end on that line too, and is read as inline markdown:
```mdx
<Admonition type="warning">**Do not** run this in production without testing first.</Admonition>
```
## Closable admonitions
```mdx
<Admonition
type="info"
title="Did you know?"
description="You can skip optional checks using the Skip checkbox."
closable={true}
/>
```
## Confirmation button
Require users to acknowledge before dismissing:
```mdx
<Admonition
type="danger"
title="Destructive Operation"
description="This will permanently delete all resources."
confirmationText="I understand this cannot be undone"
/>
```
## Don't show again
Let users hide the admonition permanently:
```mdx
<Admonition
type="info"
title="Welcome!"
description="This is your first time running this runbook."
confirmationText="I've read the introduction"
allowPermanentHide={true}
storageKey="welcome-message"
/>
```
> **See it in action:**
> The [admonition feature demo](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/admonition) shows each type, an admonition written in markdown, and both ways to dismiss one.
---
# Authoring Runbooks › Blocks › <AwsAuth>
The `<AwsAuth>` 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
<AwsAuth
id="aws-auth"
title="Authenticate to AWS"
description="Choose your preferred authentication method"
defaultRegion="us-west-2"
/>
```
## 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
<AwsAuth
id="aws-auth"
defaultTab="sso"
ssoStartUrl="https://my-company.awsapps.com/start"
/>
```
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 |
| `autoCollapse` | boolean | `false` | Folds the block down to its title once it's signed in. See [Folding done blocks](/authoring/blocks/#folding-done-blocks) |
| `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`.
## Block outputs
The block also publishes its credentials as block outputs, so a specific AwsAuth block can be referenced by ID:
| Output | Value |
|--------|-------|
| `AWS_ACCESS_KEY_ID` | The AWS access key |
| `AWS_SECRET_ACCESS_KEY` | The AWS secret key, as a [sensitive output](/authoring/inputs-and-outputs/#sensitive-outputs) |
| `AWS_SESSION_TOKEN` | The session token, as a sensitive output. Empty for credentials without one |
| `AWS_REGION` | The selected region |
Clicking **Re-authenticate** replaces them with a single output, `__AUTHENTICATED`, set to `false`, until the user signs in again.
## 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
<AwsAuth
id="aws-auth"
title="Authenticate to AWS"
defaultRegion="us-west-2"
ssoStartUrl="https://my-company.awsapps.com/start"
/>
<Check
id="verify-identity"
title="Verify AWS Identity"
command="aws sts get-caller-identity"
awsAuthId="aws-auth"
successMessage="Successfully authenticated!"
/>
<Command
id="list-buckets"
title="List S3 Buckets"
command="aws s3 ls"
awsAuthId="aws-auth"
successMessage="Buckets listed!"
/>
```
A block that sets `awsAuthId` gets the referenced block's four [outputs](#block-outputs) as environment variables for that run, in place of the session values. `AWS_SESSION_TOKEN` is set even when it is empty, so a session token from another sign-in is never paired with keys that don't use one. The block's Run or Check button stays disabled until the referenced block has published credentials.
## 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
<AwsAuth
id="aws-auth"
title="Authenticate to Production"
ssoStartUrl="https://my-company.awsapps.com/start"
ssoRegion="us-east-1"
ssoAccountId="123456789012"
ssoRoleName="AdministratorAccess"
defaultRegion="us-west-2"
/>
```
## Multiple AWS accounts
A runbook can have several AwsAuth blocks, one per AWS account:
```mdx
<AwsAuth
id="source-account"
title="Source Account (Development)"
defaultRegion="us-west-2"
/>
<Command
id="export-data"
title="Export Data from Source"
command="aws s3 cp s3://source-bucket/data.json /tmp/data.json"
awsAuthId="source-account"
/>
<AwsAuth
id="target-account"
title="Target Account (Production)"
defaultRegion="us-east-1"
/>
<Command
id="import-data"
title="Import Data to Target"
command="aws s3 cp /tmp/data.json s3://target-bucket/data.json"
awsAuthId="target-account"
/>
```
> **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']} */}
<AwsAuth id="aws-auth" />
```
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`, which is `us-east-1` unless you set it. It is the value set as `AWS_REGION` once the user confirms, and it decides the partition the credentials are validated in. GovCloud keys need a `us-gov-*` region. Set `AWS_REGION` to one in the environment, or give the block a `us-gov-*` `defaultRegion`, which applies only when neither `AWS_REGION` nor `AWS_DEFAULT_REGION` is set. Otherwise they are validated in the commercial partition, and validation fails.
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. Only one block source is allowed per AwsAuth block. See [From Command output](#from-command-output) |
Sources are tried in the order you list them.
### Disabling detection
To show only the manual authentication tabs:
```mdx
<AwsAuth
id="aws-auth"
detectCredentials={false}
/>
```
### 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
<AwsAuth
id="prod-auth"
detectCredentials={[{ env: { prefix: 'PROD_' } }]}
/>
```
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
version: 1
tests:
- name: happy-path
steps:
- block: aws-auth
env_prefix: CI_ # Checks CI_AWS_ACCESS_KEY_ID, CI_AWS_SECRET_ACCESS_KEY, and so on
expect: success
```
Unlike the block, the test runner falls back to the unprefixed `AWS_*` variables when the prefixed key pair isn't set. See [Testing AwsAuth blocks](/authoring/testing/#testing-awsauth-blocks).
### From Command output
Use credentials generated by a previous Command block:
```mdx
<Command
id="assume-cross-account"
path="scripts/assume-role.sh"
title="Assume Cross-Account Role"
/>
<AwsAuth
id="cross-account-auth"
title="Cross-Account Access"
detectCredentials={[{ block: 'assume-cross-account' }]}
/>
<Command
id="list-buckets"
command="aws s3 ls"
awsAuthId="cross-account-auth"
/>
```
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
echo "AWS_CREDENTIAL_EXPIRATION=$(jq -r '.Credentials.Expiration' <<< "$CREDS")" >> "$RUNBOOK_OUTPUT" # Optional
```
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.
The `AWS_CREDENTIAL_EXPIRATION` output is optional too. With it, the block warns before the credentials expire, as [Expiring credentials](#expiring-credentials) describes. AwsAuth reads `AWS_CREDENTIAL_EXPIRATION` from the environment as well, with the source's prefix if it has one.
If the referenced block has not run yet, AwsAuth shows *Waiting for assume-cross-account to run...*, with a link to that block, and pauses detection there. It does not skip to a later source. If the block ran but its outputs hold no valid credentials, detection moves on to the next source.
> **Caution:**
> Only one `{ block: 'id' }` entry is allowed per AwsAuth block. More than one is a configuration error, and the block renders an error card in place of the authentication UI.
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
A line above the sign-in tabs says what the user can do, followed by a **Check again** button that re-runs detection. It reads *No existing credentials found* when detection found nothing, asks the user to fix the credentials when the ones it found didn't validate, and says to sign in with different credentials after the user turned detected credentials down or clicked **Re-authenticate**. With `detectCredentials={false}` the line and the button are hidden.
Credentials for the wrong account, such as production when the user expected development, were not registered, so no script runs with them.
## Expiring credentials
Temporary credentials expire. AwsAuth knows when for:
- an AWS SSO sign-in
- a local profile with temporary credentials, such as one that assumes a role
- credentials from the environment or from a Command's outputs that come with `AWS_CREDENTIAL_EXPIRATION`, the variable the AWS SDKs read
Five minutes before the credentials expire, the block turns red, says when they expire, and offers **Sign in again**. Once they have expired, the block says so. It stays signed in until you sign in again, so a script that runs in the meantime gets the expired credentials. Keys entered on the **Static Credentials** tab come with no expiry, so the block never warns about them.
## Security
### Credential protection
When a runbook contains an `<AwsAuth>` 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.
Only those three variables are withheld. The AWS CLI and SDKs can still find credentials elsewhere, such as in a profile in `~/.aws` (the `default` profile, or the one `AWS_PROFILE` names) or through a web identity token file. For example, with production keys exported in your terminal, opening a runbook meant for development does not let its scripts run against production. A production `AWS_PROFILE` exported in your terminal still would.
[Runbook tests](/authoring/testing/) don't withhold any of these variables.
> **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 in your local Runbooks session, which Runbooks saves encrypted with the operating system's credential store. See [What is encrypted](/commands/sessions/#what-is-encrypted).
- 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.
---
# Authoring Runbooks › Blocks › <Chapter>
The `<Chapter>` block splits a long runbook into chapters, which Runbooks shows one at a time. Use it for a runbook that's best worked through in phases, such as setting up, deploying, and checking a service.
## Basic usage
```mdx
# Deploy the service
This runbook sets up the service, deploys it, and checks that it's up.
<Chapter title="Set up">
## Configure the service
<Inputs id="service" path="templates/service" />
</Chapter>
<Chapter title="Deploy">
## Deploy
<Command id="deploy" path="scripts/deploy.sh" inputsId="service" />
</Chapter>
<Chapter title="Verify">
## Check the service
<Check id="health" command="curl --fail https://my-service.example.com/health" />
</Chapter>
```
Leave a blank line after the opening tag and before the closing tag, so that the content between them is read as markdown.
Write each `<Chapter>` at the top level of the runbook. A `<Chapter>` inside another block or another chapter shows a configuration error, with its content where it is.
## Moving between chapters
In a runbook with two or more chapters, the chapter on screen starts with a bar that stays at the top of the runbook as you scroll it:
- **Contents** opens the table of contents, which lists every chapter by number and title, with how many of its [steps](/authoring/blocks/#progress) are done, such as "1/3". A chapter whose steps are all done gets a check mark. Pick a chapter to go to it.
- The bar names the chapter on screen, such as "Chapter 2 of 3 · Deploy".
- The **‹** and **›** buttons go to the previous and next chapters.
Each chapter ends with links to the previous and next chapters. The [command palette](/intro/ui_tour/) has **Go to chapter…**, which lists the chapters, and typing a chapter's title there offers it directly.
A runbook with no chapters, or with only one, has none of these: it shows all of its content at once, as it would without the block.
A runbook opens at its first chapter, and so does a new or [resumed](/commands/sessions/#resuming-a-session) session. A resumed session's blocks come back as the user left them, so the table of contents shows which chapters are done. Editing the runbook while [watch mode](/commands/watch/) reloads it keeps the chapter on screen.
## Content outside chapters
Content outside every chapter shows with each of them, where it is in the runbook. Put the runbook's title and introduction above the first chapter, so they head every chapter. A block outside the chapters, such as a `<Prerequisites>` above the first one, is on screen whichever chapter is.
## Blocks in chapters
Chapters only change what's on screen. A hidden chapter's blocks stay loaded, so:
- Block IDs are shared by the whole runbook: each must be unique across every chapter.
- A block can use the [inputs and outputs](/authoring/inputs-and-outputs/) of a block in any other chapter, and the credentials of an auth block in another chapter.
- A block keeps its state while another chapter is on screen: a form keeps its values, and a command that's running keeps running.
- The [progress](/authoring/blocks/#progress) bar in the title bar counts the steps of every chapter.
A link to something in another chapter, such as the link a block shows to a block it's waiting for, goes to that chapter first.
Find in page (**Cmd+F** on macOS, or **Ctrl+F** on Windows and Linux) searches the chapter on screen and the content outside the chapters, as does the command palette's **Jump to section**.
## Props
### Required props
- `title` (string): the chapter's name, shown in the chapter bar, the table of contents, and the command palette. A chapter without one shows a configuration error, and is named by its number, such as "Chapter 2".
## Instruction mode
Chapters work the same in [instruction mode](/intro/instruction-mode/). The table of contents counts each chapter's steps as the user marks them done.
## Testing
[Runbook tests](/authoring/testing/) don't see chapters. They run the blocks in document order, whichever chapter each one is in, and a step can use the outputs of a block in an earlier chapter.
> **See it in action:**
> The [chapters feature demo](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/chapters) splits a runbook into three chapters, with a command in one chapter that reads the output of a command in another.
---
# Authoring Runbooks › Blocks › <Check>
The `<Check>` 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
<Check
id="check-git"
command="git --version"
title="Check if Git is installed"
description="We need Git for version control"
successMessage="Git is installed!"
failMessage="Git is not installed. Please install it from https://git-scm.com/"
/>
```
## 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.
- `autoCollapse` (boolean): whether the block folds down to its title once it's done. Defaults to `false`. Click it to show the block in full again. See [Folding done blocks](/authoring/blocks/#folding-done-blocks).
- `command` (string): inline command to execute. Set exactly one of `command` and `path`. If both are set, `command` runs.
- `path` (string): path to a script file, relative to the runbook's folder, such as `scripts/check.sh`. The file must be inside that folder, so a path with a `..` segment fails. 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. A block that uses `{{ .inputs.* }}`, in `env` or in the script, needs this prop or a nested Inputs block. See [With variables](#with-variables).
- `env` (object): environment variables the script runs with, by name, such as `env={{ BUCKET_NAME: "{{ .inputs.BucketName }}" }}`. Each value can reference inputs and block outputs. See [Passing values as environment variables](/authoring/blocks/command/#passing-values-as-environment-variables).
- `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`, `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE`, `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. [Runbook tests](/authoring/testing/) ignore it and apply the test's `timeout` setting instead.
- `dependsOn` (string | string[]): ID of a Command, Check or [Prerequisites](/authoring/blocks/prerequisites/) block, or an array of IDs, that must succeed before this block can run. A Check or Prerequisites block that ends with a warning counts too. 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 `<Inputs>` component directly inside the Check:
````mdx
<Check
id="check-s3-bucket"
path="checks/s3-bucket-exists.sh"
env={{ BUCKET_NAME: "{{ .inputs.BucketName }}" }}
title="Verify S3 Bucket Exists"
>
<Inputs id="bucket-config">
```yaml
variables:
- name: BucketName
type: string
description: Name of the S3 bucket to check
validations:
- required
```
</Inputs>
</Check>
````
The embedded `<Inputs>` 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
<Check
id="check-git"
command="git --version"
title="Check if Git is installed"
successMessage="Git is installed!"
failMessage="Git is not installed."
/>
```
Inline scripts suit one-liners. For anything longer, use an external script file. See [Inline scripts](/authoring/blocks/command/#inline-scripts) in the Command block docs for commands with quotes or several lines.
#### External scripts
```mdx
<Check
id="check-aws-auth"
path="checks/aws-authenticated.sh"
title="Verify AWS Authentication"
description="Checking if you're authenticated to AWS"
successMessage="AWS credentials are valid!"
failMessage="AWS authentication failed. Run 'aws configure' to set up credentials."
/>
```
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 `<Inputs>`. Pass a value to the script with the `env` prop, and reference it as `{{ .inputs.VariableName }}` in the message props.
````mdx
<Inputs id="region-config">
```yaml
variables:
- name: AwsRegion
type: string
description: AWS region to check
default: us-east-1
```
</Inputs>
<Check
id="check-region"
env={{ REGION: "{{ .inputs.AwsRegion }}" }}
command='aws ec2 describe-availability-zones --region "$REGION"'
inputsId="region-config"
title="Check AWS Region Accessibility"
successMessage="Region {{ .inputs.AwsRegion }} is accessible!"
failMessage="Cannot access region {{ .inputs.AwsRegion }}"
/>
````
See [With variables](/authoring/blocks/command/#with-variables) in the Command block docs for merging several Inputs blocks and for what `env` does, and [Templates in the script](/authoring/blocks/command/#templates-in-the-script) for referencing values in the script itself.
### 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. Like a Command script, it starts in the session's directory rather than the runbook's folder, and runs without a terminal or any input. 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
The `check-s3-bucket` block in [Inline content](#inline-content) sets `BUCKET_NAME` for this script with its `env` prop.
```bash
#!/bin/bash
# checks/s3-bucket-exists.sh
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> }}`, or pass them to a script with [`env`](/authoring/blocks/command/#passing-values-as-environment-variables):
```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"
```
Write one output per line. A key starts with a letter or underscore and contains only letters, digits and underscores. Runbooks ignores a line with any other key. See [Producing outputs](/authoring/inputs-and-outputs/#producing-outputs) for the format.
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
```
---
# Authoring Runbooks › Blocks › <Command>
The `<Command>` 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
<Command
id="trigger-deploy"
command="curl -X POST https://api.example.com/deploy"
title="Trigger Deployment"
successMessage="Deployment triggered!"
failMessage="Failed to trigger deployment"
/>
```
## 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.
- `autoCollapse` (boolean): whether the block folds down to its title once it's done. Defaults to `false`. Click it to show the block in full again. See [Folding done blocks](/authoring/blocks/#folding-done-blocks).
- `command` (string): inline command to execute. Set exactly one of `command` and `path`. If both are set, `command` runs.
- `path` (string): path to a script file, relative to the runbook's folder, such as `scripts/deploy.sh`. The file must be inside that folder, so a path with a `..` segment fails. 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. A block that uses `{{ .inputs.* }}`, in `env` or in the script, needs this prop or a nested Inputs block. See [With variables](#with-variables).
- `env` (object): environment variables the script runs with, by name, such as `env={{ BUCKET: "{{ .inputs.BucketName }}" }}`. Each value can reference inputs and block outputs. See [Passing values as environment variables](#passing-values-as-environment-variables).
- `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`, `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE`, `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. [Runbook tests](/authoring/testing/) ignore it and apply the test's `timeout` setting instead.
- `dependsOn` (string | string[]): ID of a Command, Check or [Prerequisites](/authoring/blocks/prerequisites/) block, or an array of IDs, that must succeed before this block can run. A Check or Prerequisites block that ends with a warning counts too. 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 `<Inputs>` component directly inside the Command:
````mdx
<Command
id="echo-message"
env={{ NAME: "{{ .inputs.Name }}" }}
command='echo "Hello, $NAME!"'
title="Print Greeting"
>
<Inputs id="inline-greeting">
```yaml
variables:
- name: Name
type: string
description: Your name
validations:
- required
```
</Inputs>
</Command>
````
The embedded `<Inputs>` 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
<Command
id="deploy-trigger"
command="curl -X POST https://api.example.com/deploy"
title="Trigger Deployment"
successMessage="Deployment triggered!"
failMessage="Failed to trigger deployment"
/>
```
Inline scripts suit one-liners. For anything longer, use an external script file.
A command that contains both kinds of quotes, or spans several lines, can go in a template literal: ``command={`echo "It's done"`}``. Two things work differently there:
- `${...}` is a JavaScript substitution, which Runbooks rejects when the runbook opens. Write `$VAR` or `\${VAR}`, not `${VAR}`.
- JavaScript reads backslash escapes, as in any JavaScript string: `\n` is a line break, `` \` `` a backtick, and so on. Write `\\` for each backslash the shell should see, such as `grep '\\.tf$'`.
#### External scripts
```mdx
<Command
id="deploy-app"
path="scripts/deploy.sh"
title="Deploy Application"
description="This will deploy your application to production"
successMessage="Deployment successful!"
failMessage="Deployment failed. Check the logs for details."
/>
```
External scripts are plain bash scripts. The referenced `scripts/deploy.sh` might look like:
```bash
#!/bin/bash
log_info "Starting deployment..."
kubectl apply -f "$REPO_FILES/k8s/deployment.yaml"
if kubectl rollout status deployment/myapp; then
log_info "Deployment complete!"
exit 0
else
log_error "Deployment failed"
exit 1
fi
```
Scripts start in the session's directory, not in the runbook's folder, so this one reads its manifest from the repository a [`<GitClone>`](/authoring/blocks/gitclone/) block cloned. See [Execution context](#execution-context).
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). Blocks whose [`dependsOn`](#run-order) names it keep waiting, as they would after a failure.
### 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
A script gets the values it needs as environment variables, which the [`env`](#passing-values-as-environment-variables) prop sets from inputs and block outputs. The examples below take inputs from different places.
#### Using inputsId
The command or script pulls its values from a separate Inputs block.
````mdx
<Inputs id="repo-config">
```yaml
variables:
- name: OrgName
type: string
description: GitHub organization name
- name: RepoName
type: string
description: Repository name
```
</Inputs>
<Command
id="create-repo"
inputsId="repo-config"
env={{ ORG: "{{ .inputs.OrgName }}", REPO: "{{ .inputs.RepoName }}" }}
command='gh repo create "$ORG/$REPO" --private'
title="Create GitHub Repository"
successMessage="Repository {{ .inputs.RepoName }} created!"
failMessage="Failed to create repository"
/>
````
The message props can reference inputs and outputs directly, since Runbooks only shows them.
#### Using inline inputs
The Command collects input values directly. Other blocks can read them like any standalone Inputs block.
````mdx
<Command
id="echo-message"
env={{ NAME: "{{ .inputs.Name }}" }}
command='echo "Hello, $NAME!"'
title="Print Greeting"
>
<Inputs id="inline-greeting">
```yaml
variables:
- name: Name
type: string
description: Your name
validations:
- required
```
</Inputs>
</Command>
````
#### 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
<Inputs id="lambda-config" templatePath="templates/lambda" />
<Inputs id="repo-config">
```yaml
variables:
- name: GithubOrgName
type: string
description: GitHub organization name
- name: GithubRepoName
type: string
description: Repository name
```
</Inputs>
<Command
id="deploy-lambda"
path="scripts/deploy.sh"
inputsId={["lambda-config", "repo-config"]}
env={{ ORG: "{{ .inputs.GithubOrgName }}", REPO: "{{ .inputs.GithubRepoName }}" }}
title="Deploy Lambda Function"
description="Deploy the Lambda function using variables from both inputs"
/>
````
If both blocks define a variable with the same name, the value from `repo-config` wins.
#### Passing values as environment variables
The `env` prop sets environment variables for the script, by name. Each value is a template, rendered with the block's inputs and the outputs of other blocks, and the script reads the result like any other environment variable:
```mdx
<Command
id="label-pr"
inputsId="pr-config"
env={{
REPO: "{{ .inputs.RepoName }}",
PR_TITLE: "{{ .outputs.fetch_pr.title }}",
DRY_RUN: false,
}}
command='gh pr edit --repo "$REPO" --title "[infra] $PR_TITLE"'
/>
```
Runbooks gives the script the values as data. The shell never runs anything in them, so a value can hold quotes, `$(...)` or backticks, as a PR title or an API response can. Quote each variable where you use it, as above, so a value with spaces stays in one piece.
- **The block shows the values.** Before you click Run, the block lists each variable with the value the script will get, once the inputs and outputs it references are set. The block waits for them, as it waits for the values its script references. A [sensitive output](/authoring/inputs-and-outputs/#sensitive-outputs) shows as `<redacted>`, and the script gets its real value.
- **You can enter an output's value yourself.** See [Entering an output by hand](#entering-an-output-by-hand).
- **Instruction mode exports them.** In [instruction mode](/intro/instruction-mode/), the block shows an `export` line for each variable before the command, quoted for the shell, so you can copy and run them together.
- **They are this block's.** The variables are set for this run only, and later blocks don't see them. If the script changes one, the new value carries forward like any other [export](#execution-context). To pass a value to later blocks, write it as a [block output](#block-outputs).
- **They override other variables.** A variable in `env` replaces one of the same name from the session or from an auth block, so `env={{ AWS_REGION: "{{ .inputs.Region }}" }}` picks the region for a block that uses `awsAuthId`.
A name has letters, digits and underscores, and doesn't start with a digit. A value is a string, a number or a boolean. Runbooks sets `GENERATED_FILES`, `REPO_FILES` and every name that starts with `RUNBOOK_` itself, so `env` can't use them. A block whose `env` breaks these rules shows the error and can't run.
#### Entering an output by hand
In the block's list of variables, each output a value reads is a chip. It shows the value the script will get, or, before the block that produces it has run, which output it is. Click the chip and it turns into a field, in its place, where you can type another value. Press Enter, or click away after changing it, to use it. Press Escape to get the chip back. The block then uses that value in place of the output, in its `env` and in its script, and doesn't wait for the block that produces it. Use this to run the block when that block can't run on your machine, or to try another value.
- **Reset goes back to the output.** A value you entered stays in its field, where you can change it, with a reset button next to it that brings the chip back.
- **The value is this block's.** Other blocks that read the same output still get the output.
- **A secret stays a secret.** A value you enter for a [sensitive output](/authoring/inputs-and-outputs/#sensitive-outputs) is sensitive too. The field hides what you type, and starts empty rather than showing the output or the value you entered before.
- **The session keeps it.** A [resumed session](/commands/sessions/#resuming-a-session) brings back the values you entered.
- **`dependsOn` still applies.** A block that names the producing block in [`dependsOn`](#run-order) waits for it to run anyway.
Chips appear for values made of text and plain references, such as `"{{ .outputs.fetch_pr.title }}"` or `"s3://{{ .outputs.create_bucket.name }}/logs"`. A value with other template code, such as an input, a pipe or a condition, shows the value the script will get, without chips.
#### Templates in the script
A script can also reference values directly, as `{{ .inputs.Name }}` or `{{ .outputs.build.tag }}`. Runbooks inserts them into the script's source before it runs, 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, use [`env`](#passing-values-as-environment-variables).
A block whose script references inputs or outputs shows a note above the script that names them, and [runbook tests](/authoring/testing/#warnings) print a warning for it. The script still runs.
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. It handles references such as `{{ .inputs.Name }}` and `{{ .outputs.build.tag }}`, `{{ fromJson .outputs.build.data }}`, and an `{{ if .inputs.Flag }}...{{ else }}...{{ end }}` that isn't nested. It leaves anything else in the script as written, including functions and pipes such as `upper` or `hasKey`, `eq` comparisons, `range`, `with`, and the `{{-` and `-}}` trim markers, and it reports the `{{.Names}}` in the raw-string form as an undefined variable. A script that uses any of these 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.
Scripts start in the [session's directory](/commands/sessions/), not in the runbook's folder. A `cd` carries forward as described above, so later scripts start wherever it left off. No environment variable names the runbook's folder, so a script can't call another script or read a file that ships with the runbook. Give each script file its own block with `path`. To work with a cloned repository's files, use [`$REPO_FILES`](#writing-to-repository-files) or the `<GitClone>` block's `clone_path` output.
Scripts run without a terminal, and their standard input is empty. A command that asks for input, such as `read`, a confirmation prompt, or a `sudo` password prompt, gets none and usually fails. Use non-interactive flags, such as `tofu apply -auto-approve` or `apt-get install -y`. Tools that check for a terminal may also turn off colors and progress bars.
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 succeeded, or for a Check, ended with a warning. A Command that exits 2 doesn't count. 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
<Command id="login" command="export API_TOKEN=$(vault read -field=token secret/api)" />
<Command id="deploy" dependsOn="login" path="scripts/deploy.sh" />
<Command id="migrate" dependsOn={["login", "deploy"]} exclusive path="scripts/migrate.sh" />
```
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.
Each ID in `dependsOn` must name a Command, Check or [Prerequisites](/authoring/blocks/prerequisites/) block in the runbook. Runbooks doesn't report an ID that names another kind of block, or no block at all. The block just keeps waiting for 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 "$REPO_FILES/k8s/deployment.yaml"
kubectl rollout status deployment/myapp
log_info "Deployment complete!"
```
#### Parameterized script
```mdx
<Command
id="create-vpc"
path="scripts/create-vpc.sh"
inputsId="vpc-config"
env={{
REGION: "{{ .inputs.AwsRegion }}",
VPC_NAME: "{{ .inputs.VpcName }}",
CIDR_BLOCK: "{{ .inputs.CidrBlock }}",
}}
/>
```
```bash
#!/bin/bash
# scripts/create-vpc.sh
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..."
kubectl cluster-info > /dev/null || { log_error "Pre-checks failed"; exit 1; }
log_info "Deploying..."
kubectl apply -f "$REPO_FILES/k8s/"
log_info "Running post-deployment validation..."
kubectl rollout status deployment/myapp --timeout=5m || { 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 file from the cloned repository
cp "$REPO_FILES/config.yaml" "$GENERATED_FILES/"
```
Or as an inline command:
```mdx
<Command
id="create-file"
title="Create a greeting file"
command={`echo "Hello from the command!" > "$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 `<GitClone>` block has registered a repository, the `$REPO_FILES` environment variable points to its local path. With several repositories, it points to the one selected in the workspace's repository dropdown: the first one added, whether by a clone or a [local checkout](/authoring/blocks/gitclone/#using-a-local-checkout), until the user selects another. 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 `<GitClone>` 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 in the [session's directory](/commands/sessions/#where-sessions-are-stored), 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 [`<GitClone>`](/authoring/blocks/gitclone/) and [`<GitPullRequest>`](/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> }}`. A later block passes one to its script with [`env`](#passing-values-as-environment-variables), as in `env={{ ACCOUNT_ID: "{{ .outputs.create_account.account_id }}" }}`.
```bash
#!/bin/bash
ACCOUNT_ID=$(aws organizations create-account ...)
echo "account_id=$ACCOUNT_ID" >> "$RUNBOOK_OUTPUT"
```
Write one output per line. A key starts with a letter or underscore and contains only letters, digits and underscores. Runbooks ignores a line with any other key. See [Producing outputs](/authoring/inputs-and-outputs/#producing-outputs) for the full format.
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.
---
# Authoring Runbooks › Blocks › <DirPicker>
The `<DirPicker>` 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
<GitClone id="clone-repo" title="Clone Repository" />
<DirPicker
id="target-path"
gitCloneId="clone-repo"
dirLabels={['Account', 'Region']}
/>
```
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 `<TemplateInline>` blocks as `{{ .outputs.<id>.PATH }}`.
### Basic usage
DirPicker needs a root directory to browse. You can provide one in two ways.
With `rootDir`, pass a directory path. A relative path is resolved against the runbook's folder, so `rootDir="."` browses the folder the runbook is in:
```mdx
<DirPicker
id="target-path"
rootDir="."
dirLabels={['Account', 'Region', 'Category']}
title="Select deployment path"
description="Choose where to place the generated configuration"
/>
```
With `gitCloneId`, reference a `<GitClone>` block. DirPicker waits for the clone to complete, then uses the cloned repository as its root directory:
```mdx
<GitClone id="clone-repo" title="Clone Repository" />
<DirPicker
id="target-path"
gitCloneId="clone-repo"
dirLabels={['Account', 'Region', 'Category']}
title="Select deployment path"
description="Choose where to place the generated configuration"
/>
```
At least one of `rootDir` or `gitCloneId` must be provided. If both are set, `rootDir` takes precedence.
The root must be inside the runbook's folder, the [session's directory](/commands/sessions/), the directory the last script left off in, or a repository a `<GitClone>` block cloned or opened. Any other root shows an error in the block.
### 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* | Path to the root directory to browse. A relative path is resolved against the runbook's folder. The root must be in one of the [allowed places](#basic-usage). `{{ }}` expressions in it aren't resolved. |
| `gitCloneId` | `string` | No* | ID of a `<GitClone>` 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 `<Inputs>` blocks, for `{{ .inputs.* }}` expressions in `title`, `description`, `pathLabel` and `pathLabelDescription`. |
| `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 `<GitClone>` 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 `<TemplateInline>` block's `outputPath` prop:
````mdx
<GitClone id="clone-repo" title="Clone Repository" />
<DirPicker
id="target-path"
gitCloneId="clone-repo"
dirLabels={['Account', 'Region']}
/>
<TemplateInline
id="module-config"
outputPath="{{ .outputs.target_path.PATH }}/terragrunt.hcl"
generateFile={true}
target="worktree"
>
```hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
```
</TemplateInline>
````
The `outputPath` resolves `{{ .outputs.*.* }}` expressions client-side using block outputs. `PATH` is relative to the DirPicker's root, and with `target="worktree"` the `outputPath` is relative to the active repository: the one selected in the workspace's repository dropdown, which is the first repository added, whether by a clone or a [local checkout](/authoring/blocks/gitclone/#using-a-local-checkout), until the user selects another. So the file is written to the directory the user selected when `gitCloneId` names the `<GitClone>` block of the active repository. 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 `<GitClone>` 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.
---
# Authoring Runbooks › Blocks › <Finish>
The `<Finish>` block marks the end of a runbook. When the user clicks **Finish**, it can run a final check to confirm the runbook did its job. Once the runbook is finished, the block celebrates with a burst of confetti, shows your next steps, and marks the [session](/commands/sessions/) finished, so Runbooks won't resume that session when the runbook opens again.
Put it at the end of the runbook, after the last block the user runs.
## Basic usage
```mdx
<Finish id="finish">
You've set up your new service. Here's what to do next:
- Open a pull request with the generated code.
- Tell your team the service is live.
</Finish>
```
Clicking **Finish** finishes the runbook straight away: there is no final check, so nothing runs. The block turns green, shows the success message, and reveals the next steps.
Leave a blank line after the opening tag and before the closing tag, so that the content between them is read as markdown.
To keep the **Finish** button disabled until other blocks have run, name them in `dependsOn`:
```mdx
<Finish id="finish" dependsOn={["deploy", "smoke-test"]}>
Your service is deployed and answering requests.
</Finish>
```
## With a final check
To confirm the runbook worked before celebrating, give the block a final check with `command` or `path`, exactly as you would a [Check](/authoring/blocks/check/):
```mdx
<Finish
id="finish"
command="curl --fail --silent https://my-service.example.com/health"
description="Checks that the new service answers its health check."
>
Your service is live. Next, [set up alerting](https://example.com/alerts).
</Finish>
```
Clicking **Finish** runs the check, and its exit code decides what happens:
- **Exit code 0**: the runbook is finished. The block celebrates and shows the next steps.
- **Exit code 1** (or any code other than 0 and 2): the check failed. The block shows `failMessage` and the logs, and the user can fix the problem and click **Finish** again.
- **Exit code 2**: a warning. The block shows `warnMessage` and doesn't celebrate, since something still needs attention.
The final check runs like any other script: it has the logging helpers, the session's environment, `$RUNBOOK_OUTPUT` and `$GENERATED_FILES`, and it waits for the Inputs, outputs and authentication it references. See [Check](/authoring/blocks/check/#writing-scripts) for how to write one.
## Finishing the session
Finishing the runbook also marks the session it runs in as finished. A final check that ends in a warning or a failure doesn't finish the runbook, so it leaves the session as it was.
A finished session keeps its directory, files and history, but Runbooks doesn't resume it on its own again. The next time the runbook opens, in the running app or after a restart, it opens in a new session, with a notice that names the finished session and a **Switch session…** button to go back to it. Switching to the finished session shows the `<Finish>` block finished, without the confetti. It doesn't make Runbooks resume the finished session: the next time the runbook opens, it resumes the session the user started after finishing, or starts a new one if there is none. See [Finishing a session](/commands/sessions/#finishing-a-session).
## Props
### Required props
- `id` (string): unique identifier for this block
### Optional props
- `title` (string): heading shown in the block. Default "Finish this runbook". Supports inline markdown.
- `description` (string): text shown under the title. Supports inline markdown.
- `command` (string): inline final check to run. Use either this or `path`, or neither for a block without a final check. If both are set, `command` runs.
- `path` (string): path to a final-check script, relative to the runbook's folder. The file must be inside that folder, so a path with a `..` segment fails. 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. A final check that uses `{{ .inputs.* }}` needs this prop.
- `env` (object): environment variables the final check runs with, by name, such as `env={{ URL: "{{ .outputs.deploy.url }}" }}`. Each value can reference inputs and block outputs. See [Passing values as environment variables](/authoring/blocks/command/#passing-values-as-environment-variables).
- `awsAuthId` (string): ID of an [AwsAuth](/authoring/blocks/awsauth/) block whose credentials the final check needs. The Finish 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 whose credentials the final check needs. The Finish button is disabled until authentication completes.
- `githubAuthId` (string): ID of a [GitHubAuth](/authoring/blocks/githubauth/) block whose credentials the final check needs. GitHub only. The Finish button is disabled until authentication completes. Prefer `gitAuthId`.
- `googleAuthId` (string): ID of a [GoogleAuth](/authoring/blocks/googleauth/) block whose credentials the final check needs. The Finish button is disabled until authentication completes.
- `successMessage` (string): message shown once the runbook is finished. Default "You finished this runbook!". Supports inline markdown.
- `warnMessage` (string): message shown when the final check exits with code 2. Default "Warning". Supports inline markdown.
- `failMessage` (string): message shown when the final check fails. Default "The final check failed. Check the logs, fix the problem, and run it again.". Supports inline markdown.
- `runningMessage` (string): message shown while the final check runs. Default "Running the final check...". Supports inline markdown.
- `timeoutMs` (number): maximum time, in milliseconds, the final check may run before it is killed and the block is marked as failed. Defaults to `3600000` (60 minutes). [Runbook tests](/authoring/testing/) ignore it and apply the test's `timeout` setting instead.
- `dependsOn` (string | string[]): ID of a Command, Check or [Prerequisites](/authoring/blocks/prerequisites/) block, or an array of IDs, that must succeed before the runbook can be finished. A Check or Prerequisites block that ends with a warning counts too. See [Run order](/authoring/blocks/command/#run-order).
- `exclusive` (boolean): whether the final check runs alone. Defaults to `false`. While it runs, no Command or Check block can start, and it can't start while one is running. See [Run order](/authoring/blocks/command/#run-order).
### Next steps
The content between `<Finish>` and `</Finish>` is the author's next steps. It is hidden until the runbook is finished, then shown inside the block. It can hold any markdown: paragraphs, lists, links and code blocks.
Unlike `<Check>` and `<Command>`, a `<Finish>` can't hold a nested `<Inputs>`, because its content only appears once the runbook is finished. Use `inputsId` to reference an Inputs block elsewhere in the runbook instead.
## The celebration
When the runbook is finished, confetti bursts from the bottom corners of the window for about three seconds.
- It never blocks the page: clicks go straight through it.
- Press **Escape**, or click anywhere, to clear it at once.
- It is skipped when the operating system is set to reduce motion (`prefers-reduced-motion`). The block still turns green and shows the next steps.
- It happens each time the block goes from not finished to finished, including a successful re-run of the final check. It doesn't replay when a session is resumed: the block comes back finished, without the confetti.
A runbook can have more than one `<Finish>`, for example one at the end of each alternative path. Finishing any of them finishes the session.
## Instruction mode
In [instruction mode](/intro/instruction-mode/), a `<Finish>` shows its title, the final check (if it has one) as a command to run by hand, and the next steps, which are always visible since nothing is gated. Ticking **Mark as done** celebrates and marks the session finished.
## Testing
In a [runbook test](/authoring/testing/#testing-finish-blocks), a `<Finish>` with a final check runs it like a `<Check>`: exit code 0 is `success`, 2 is `warn`, and anything else is `fail`. A `<Finish>` without one runs nothing, and its step succeeds.
> **See it in action:**
> The [finish feature demo](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/finish) shows a `<Finish>` whose final check depends on an earlier block.
---
# Authoring Runbooks › Blocks › <GitAuth>
The `<GitAuth>` 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
<GitAuth id="git-auth" title="Authenticate to Git" />
```
Pre-select GitLab and hide the dropdown so the runbook is GitLab-only:
```mdx
<GitAuth
id="git-auth"
provider="gitlab"
hideProviderSelect
title="Authenticate to GitLab"
/>
<GitClone
id="clone-repo"
gitAuthId="git-auth"
title="Clone Repository"
prefilledUrl="https://gitlab.com/acme-corp/infrastructure-live"
/>
```
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`, then `GITLAB_ACCESS_TOKEN`, then `OAUTH_TOKEN`.
2. The provider CLI. `gh auth token --hostname <host>` for GitHub, or `glab config get token --host <host>` for GitLab. When the CLI isn't installed, Runbooks reads the token from its configuration file instead: gh's `hosts.yml` or glab's `config.yml`, where `gh auth login` and `glab auth login` store it unless they keep it in the system keyring.
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 [`<GitHubAuth>`](/authoring/blocks/githubauth/#detection-sources) and [`<GitLabAuth>`](/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 `<GitAuth>` 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...*, with a link to that block, 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
<GitAuth id="git-auth" detectCredentials={false} />
```
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`, `GITLAB_ACCESS_TOKEN` or `OAUTH_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 an environment variable such as `GITLAB_TOKEN`. 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
<GitAuth
id="git-auth"
provider="github"
hideProviderSelect
host="github.example.com"
oauthClientId="0a1b2c3d4e5f6a7b8c9d"
/>
```
`host` belongs to the provider set by `provider`:
- With `provider` unset, `host` is a GitHub host, so `<GitAuth host="gitlab.example.com" />` treats a GitLab instance as GitHub Enterprise Server. Set `provider="gitlab"` with a GitLab host.
- If the user switches providers in the dropdown, the block ignores `host` for the other provider.
- A GitHub `host` that can't be parsed is a configuration error. A GitLab `host` or `instanceUrl` that can't be parsed falls back to `gitlab.com`, with no error.
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 `<GitHubAuth>` 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.
An unprefixed GitLab token from the environment (`GITLAB_TOKEN`, `GITLAB_ACCESS_TOKEN` or `OAUTH_TOKEN`) 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 [`<GitLabAuth>`](/authoring/blocks/gitlabauth/#prefixed-environment-variables).
### Expiring tokens
The block warns before a token expires when the provider says when that is. GitHub says so for a token that has an expiration date, such as a fine-grained personal access token. GitLab says so for a personal access token with an expiry date, and for an OAuth token such as the one `glab auth login` stores.
Five minutes before the token expires, the block turns red, says when it expires, and offers **Sign in again**. Once it has expired, the block says so. It stays signed in until you sign in again, so a script that runs in the meantime gets the expired token.
## 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). |
| `autoCollapse` | `boolean` | No | `false` | Folds the block down to its title once it's signed in. See [Folding done blocks](/authoring/blocks/#folding-done-blocks). |
| `host` | `string` | No | - | Pins the host for the provider set by `provider` 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 on GitHub. GitLab has no OAuth flow, so for GitLab this only changes the scopes that [instruction mode](/intro/instruction-mode/) lists, `read_repository` and `write_repository` by default. |
| `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 `<Inputs>` 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 [`<GitClone>`](/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.
### Block outputs
The block also publishes block outputs, which later blocks can read as `{{ .outputs.<block_id>.<name> }}`:
| Output | Value |
| --- | --- |
| `GITHUB_USER` or `GITLAB_USER` | The authenticated user's login name |
| `GITHUB_HOST` or `GITLAB_HOST` | The host the credential belongs to |
| `GIT_PROVIDER` | `github` or `gitlab` |
| `__AUTHENTICATED` | `true` once the block has authenticated |
Don't read the token from the outputs. Scripts get it as `GITHUB_TOKEN` or `GITLAB_TOKEN` through `gitAuthId` or `githubAuthId` (below), or from the session environment.
### 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 `<GitAuth>` 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 `<GitAuth>` block cannot be referenced via `githubAuthId`. Use `gitAuthId`, or rely on the session environment, from which any `<Command>` or `<Check>` inherits `GITLAB_TOKEN` without an explicit reference.
## Provider-locked aliases
Two aliases lock `<GitAuth>` to a single provider and hide the dropdown:
- [`<GitHubAuth>`](/authoring/blocks/githubauth/) is equivalent to `<GitAuth provider="github" hideProviderSelect />`.
- [`<GitLabAuth>`](/authoring/blocks/gitlabauth/) is equivalent to `<GitAuth provider="gitlab" hideProviderSelect />`.
## Detailed example
The [git-auth demo](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/git-auth) is a complete GitLab example.
---
# Authoring Runbooks › Blocks › <GitClone>
The `<GitClone>` 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
<GitClone
id="clone-repo"
title="Clone Repository"
description="Enter a git URL to clone"
/>
```
### With GitHub authentication
When paired with a `<GitHubAuth>` 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
<GitHubAuth
id="gh-auth"
title="Connect to GitHub"
description="Authenticate to access private repos"
/>
<GitClone
id="clone-repo"
githubAuthId="gh-auth"
title="Clone a Repo"
description="Browse or enter a git URL to clone"
/>
```
### 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
<GitClone
id="repo"
title="Select Your Infrastructure Repo"
prefilledRepoDir="/Users/you/dev/infrastructure-live"
/>
```
`prefilledRepoDir` takes an absolute path, or a path relative to the working directory rather than to the runbook. `~` isn't expanded, so a value such as `~/dev/infrastructure-live` doesn't find the checkout.
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 */}
<GitClone id="repo" source="local" hideSourceSelect />
{/* Cloning only */}
<GitClone id="repo" source="clone" hideSourceSelect prefilledUrl="https://github.com/acme/infra" />
```
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 [`<GitPullRequest>`](/authoring/blocks/gitpullrequest) integration. A pull request opens against the checkout's `origin` 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, the block reads `repo_owner` and `repo_name` from that one, but blocks that open a pull request still fail, because they push to and open the pull request against a remote named `origin`.
### 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
<GitClone
id="clone-docs"
title="Clone Runbooks Docs"
prefilledUrl="https://github.com/gruntwork-io/runbooks"
prefilledRef="v1.0.0"
prefilledRepoPath="docs"
prefilledLocalPath="./runbooks-docs"
/>
```
### 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. |
| `autoCollapse` | `boolean` | No | Folds the block down to its title once the repository is cloned. Defaults to `false`. See [Folding done blocks](/authoring/blocks/#folding-done-blocks). |
| `inputsId` | `string \| string[]` | No | ID of one or more [`<Inputs>`](/authoring/blocks/inputs) blocks. `title`, `description`, and the `prefilled*` props resolve `{{ .inputs.VarName }}` expressions against them. |
| `githubAuthId` | `string` | No | Reference to a [`<GitHubAuth>`](/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 [`<GitAuth>`](/authoring/blocks/gitauth), [`<GitHubAuth>`](/authoring/blocks/githubauth) or [`<GitLabAuth>`](/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. A repository kept out of the workspace can't be the target of a [`<GitPullRequest>`](/authoring/blocks/gitpullrequest) block. |
| `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. Use an absolute path, or one relative to the working directory rather than to the runbook. `~` isn't expanded. |
### 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 `<GitClone>` 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 `<GitPullRequest>` block can't open a pull request from a repository kept out of the workspace. It shows "No repository available" until another `<GitClone>` block adds one.
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. |
| `ssh://` URL | `ssh://git@github.com/org/repo.git` | Not accepted. The block refuses it as an invalid git URL. Use the `git@host:org/repo.git` form above, with the host in brackets for a non-default port. |
### GitHub authentication
The GitClone block resolves GitHub credentials in this order:
1. `githubAuthId`. If set, the block uses the token the referenced GitHubAuth block signed in with, 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
<GitHubAuth
id="gh-auth"
title="Connect to GitHub Enterprise"
host="github.example.com"
/>
<GitClone
id="clone-repo"
githubAuthId="gh-auth"
title="Clone a Repo"
prefilledUrl="https://github.example.com/platform/infrastructure-live"
/>
```
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
<GitClone
id="clone-vpc-module"
title="Clone VPC Module"
prefilledUrl="https://github.com/gruntwork-io/terraform-aws-vpc"
prefilledRepoPath="modules/vpc-app"
prefilledLocalPath="./vpc-module"
/>
```
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.
[`<GitPullRequest>`](/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 working directory starts as the runbook's [session directory](/commands/sessions/), so by default a clone lands there and not next to the runbook.
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 `<GitClone>` 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 repositories, `$REPO_FILES` points to the one selected in the workspace's repository dropdown: the first one added, whether by a clone or a [local checkout](#using-a-local-checkout), until the user selects another.
Template and TemplateInline blocks can also write directly into the cloned repository by setting `target="worktree"`:
```mdx
<Template id="new-module" path="templates/module" target="worktree" />
```
### 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).
```mdx
<Command
id="register-repo"
env={{
ORG_ID: '{{ if hasKey .outputs.clone_repo "org_id" }}{{ .outputs.clone_repo.org_id }}{{ end }}',
}}
command='my-tool register ${ORG_ID:+--org-id "$ORG_ID"}'
/>
```
`ORG_ID` is empty when the output is missing, and `${ORG_ID:+...}` then leaves out the flag.
Pass outputs to a later block's script with its [`env`](/authoring/blocks/command/#passing-values-as-environment-variables) prop:
```mdx
<Command
id="list-files"
title="List Cloned Files"
env={{ REPO_DIR: "{{ .outputs.clone_repo.clone_path }}" }}
command='ls -la "$REPO_DIR"'
/>
```
> **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. [`<GitPullRequest>`](/authoring/blocks/gitpullrequest) stops targeting the previous repository too. It waits, or uses the repository of another `<GitClone>` 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 `<GitClone>` 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
id="gh-auth"
title="Authenticate to GitHub"
/>
<GitClone
id="clone-infra"
githubAuthId="gh-auth"
title="Clone Infrastructure Repo"
description="Clone the infrastructure repository to make changes"
prefilledUrl="https://github.com/acme-corp/infrastructure-live"
prefilledRef="release/v2"
prefilledLocalPath="./infra"
/>
<Command
id="apply-changes"
githubAuthId="gh-auth"
title="Apply OpenTofu Changes"
env={{ REPO_DIR: "{{ .outputs.clone_infra.clone_path }}" }}
command='cd "$REPO_DIR" && tofu apply -auto-approve'
/>
```
---
# Authoring Runbooks › Blocks › <GitHubAuth>
The `<GitHubAuth>` block authenticates the user to GitHub.
> **Tip:**
> `<GitHubAuth>` is an alias of [`<GitAuth>`](/authoring/blocks/gitauth/) locked to GitHub. It is equivalent to `<GitAuth provider="github" hideProviderSelect />`. Use `<GitAuth>` 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/), [Check](/authoring/blocks/check/), and [GitClone](/authoring/blocks/gitclone/) 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
<GitHubAuth
id="gh-auth"
title="Authenticate to GitHub"
description="Sign in to access private repositories"
/>
<GitClone
id="clone-repo"
githubAuthId="gh-auth"
title="Clone Repository"
prefilledUrl="https://github.com/acme-corp/infrastructure-live"
/>
```
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>`). When `gh` isn't installed, Runbooks reads the token from gh's `hosts.yml` instead, unless gh keeps it in the system keyring.
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 */}
<GitHubAuth
id="ghes-auth"
title="Authenticate to GitHub Enterprise"
host="github.example.com"
/>
{/* GitHub Enterprise Cloud with data residency */}
<GitHubAuth
id="ghe-auth"
title="Authenticate to Acme's GitHub"
host="acme.ghe.com"
/>
```
`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 */}
<GitHubAuth
id="ghes-auth"
host="github.example.com"
oauthClientId="0a1b2c3d4e5f6a7b8c9d"
/>
{/* Host picker: one OAuth app per host */}
<GitHubAuth
id="gh-auth"
oauthClientId={{
"github.example.com": "0a1b2c3d4e5f6a7b8c9d",
"acme.ghe.com": "9f8e7d6c5b4a39281706",
}}
/>
```
`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 |
| `autoCollapse` | boolean | `false` | Folds the block down to its title once it's signed in. See [Folding done blocks](/authoring/blocks/#folding-done-blocks) |
| `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.
### Block outputs
The block also publishes `GITHUB_USER`, `GITHUB_HOST`, `GIT_PROVIDER` (`github`) and `__AUTHENTICATED` as [block outputs](/authoring/blocks/gitauth/#block-outputs). Don't read the token from the outputs. Scripts get it as `GITHUB_TOKEN` through `githubAuthId` or `gitAuthId`, or from the session environment.
### 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
<GitHubAuth
id="gh-auth"
title="Authenticate to GitHub"
/>
<Check
id="verify-auth"
title="Verify GitHub Access"
command="gh auth status"
githubAuthId="gh-auth"
successMessage="Successfully authenticated!"
/>
<Command
id="list-repos"
title="List Repositories"
command="gh repo list --limit 10"
githubAuthId="gh-auth"
successMessage="Repositories listed!"
/>
```
`gh` reads `GITHUB_TOKEN`, but `git` doesn't. To clone a private repository, use a [`<GitClone>`](/authoring/blocks/gitclone/) block with `githubAuthId`, as in [Basic usage](#basic-usage).
#### 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']} */}
<GitHubAuth id="gh-auth" />
```
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
<GitHubAuth
id="gh-auth"
detectCredentials={['env']}
/>
```
Check prefixed environment variables such as `PROD_GITHUB_TOKEN`:
```mdx
<GitHubAuth
id="gh-auth"
detectCredentials={[{ env: { prefix: 'PROD_' } }, 'cli']}
/>
```
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
version: 1
tests:
- name: happy-path
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 for a `github.com` block, 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
<GitHubAuth
id="gh-auth"
detectCredentials={false}
/>
```
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
<Command
id="fetch-token"
path="scripts/get-token.sh"
title="Fetch GitHub Token"
/>
<GitHubAuth
id="dynamic-auth"
title="GitHub Access"
detectCredentials={[{ block: 'fetch-token' }]}
/>
```
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...*, with a link to that block, 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
<GitHubAuth
id="gh-auth"
title="Limited GitHub Access"
oauthScopes={["public_repo", "read:org"]}
/>
```
> **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.
A token with an expiration date gets a warning on the block before it expires; see [Expiring tokens](/authoring/blocks/gitauth/#expiring-tokens).
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 are encrypted at rest.** Tokens are stored in your local Runbooks session, which Runbooks saves encrypted with the operating system's credential store. See [What is encrypted](/commands/sessions/#what-is-encrypted).
### 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.** GitHub's authorization page displays the requested scopes. Runbooks doesn't show them before you sign in. If you approve without reviewing them, you grant broader access than needed.
**Risk.** Low. GitHub displays the requested scopes before you grant access.
**Mitigation.** Review the scopes on GitHub's authorization page before approving. 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`.
Runbooks keeps a token in your local session, encrypted, until you [delete the session](/commands/sessions/#deleting-sessions). Quitting Runbooks or resetting the session doesn't delete it.
---
# Authoring Runbooks › Blocks › <GitHubPullRequest>
The `<GitHubPullRequest>` block commits the changes made in a repository cloned by [`<GitClone>`](/authoring/blocks/gitclone/), pushes them to a new branch, and opens a GitHub pull request. It is a GitHub-locked alias of [`<GitPullRequest>`](/authoring/blocks/gitpullrequest/), equivalent to `<GitPullRequest provider="github" />`.
### Basic usage
Pair it with `<GitHubAuth>` and `<GitClone>`:
```mdx
<GitHubAuth
id="gh-auth"
title="Connect to GitHub"
/>
<GitClone
id="clone-repo"
githubAuthId="gh-auth"
title="Clone Repository"
prefilledUrl="https://github.com/acme-corp/infrastructure"
/>
<GitHubPullRequest
id="create-pr"
githubAuthId="gh-auth"
title="Open Pull Request"
description="Create a PR with the changes made in this runbook"
/>
```
### 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
<GitHubPullRequest
id="create-pr"
githubAuthId="gh-auth"
prefilledPullRequestTitle="Add VPC module configuration"
prefilledPullRequestDescription="## Changes\n- Added VPC module\n- Configured NAT gateway"
prefilledPullRequestLabels={["enhancement", "terraform"]}
prefilledBranchName="feature/add-vpc"
prefilledCommitMessage="Add VPC module configuration"
/>
```
> **Note:**
> [`<GitLabMergeRequest>`](/authoring/blocks/gitlabmergerequest/) and `<GitPullRequest>` 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
<GitHubPullRequest
id="create-pr"
githubAuthId="gh-auth"
prefilledPullRequestTitle="Add {{ .outputs.config.MODULE_NAME }} module"
prefilledBranchName="feature/{{ .outputs.config.MODULE_NAME }}"
/>
```
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. |
| `autoCollapse` | `boolean` | No | Folds the block down to its title once the pull request is opened. Defaults to `false`. See [Folding done blocks](/authoring/blocks/#folding-done-blocks). |
| `inputsId` | `string \| string[]` | No | Reference to one or more [`<Inputs>`](/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 [`<GitHubAuth>`](/authoring/blocks/githubauth) block. When set, the block waits for authentication to complete and uses the token for GitHub API access. |
| `gitAuthId` | `string` | No | Reference to a [`<GitAuth>`](/authoring/blocks/gitauth) or [`<GitHubAuth>`](/authoring/blocks/githubauth) block. Works like `githubAuthId`. If the linked block signed in to GitLab, the block shows a blocking error. |
| `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` |
Pass an output to a later block's script with its [`env`](/authoring/blocks/command/#passing-values-as-environment-variables) prop:
```mdx
<Command
id="notify"
title="Post PR Link"
env={{ PR_URL: "{{ .outputs.create_pr.PR_URL }}" }}
command='echo "PR created: $PR_URL"'
/>
```
> **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 `<GitClone>` block that has cloned or selected a repository, which the PR is created against. With `githubAuthId` or `gitAuthId`, it also needs the auth block it names, a `<GitHubAuth>` block or a `<GitAuth provider="github">` block, to have signed in. 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 waits for the `__AUTHENTICATED` output that the block referenced by `githubAuthId` or `gitAuthId` sets once it signs in, whichever way the user signed in. Until then, the block displays "Waiting for GitHub authentication" and links to the block to complete. Without `githubAuthId` or `gitAuthId`, the block doesn't wait for an auth block. It creates the PR with the session's GitHub token, and fails if the session has none.
For the repository, the block uses the workspace's active repository: the one selected in the workspace's repository dropdown, which is the first one added, whether by a clone or a [local checkout](/authoring/blocks/gitclone/#using-a-local-checkout), until the user selects another. No prop picks a `<GitClone>` block. Until a `<GitClone>` block has added a repository to the workspace, the block displays "No repository available". A `<GitClone>` block with `showFileTree={false}` doesn't add its repository.
### 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
<GitHubAuth
id="gh-auth"
host="github.example.com"
/>
<GitClone
id="clone-repo"
githubAuthId="gh-auth"
prefilledUrl="https://github.example.com/platform/infrastructure-live"
/>
<GitHubPullRequest
id="create-pr"
githubAuthId="gh-auth"
title="Open Pull Request"
/>
```
See [GitHub Enterprise support](/authoring/blocks/githubauth/#github-enterprise-support) for configuring the host.
### Example: full workflow
```mdx
<GitHubAuth
id="gh-auth"
title="Authenticate to GitHub"
/>
<GitClone
id="clone-infra"
githubAuthId="gh-auth"
title="Clone Infrastructure Repo"
prefilledUrl="https://github.com/acme-corp/infrastructure-live"
/>
<Command
id="make-changes"
title="Apply Configuration"
env={{ REPO_DIR: "{{ .outputs.clone_infra.clone_path }}" }}
command='echo "new_setting = true" >> "$REPO_DIR/config.hcl"'
/>
<GitHubPullRequest
id="open-pr"
githubAuthId="gh-auth"
title="Open Pull Request"
prefilledPullRequestTitle="Update infrastructure configuration"
prefilledPullRequestLabels={["infrastructure"]}
prefilledCommitMessage="Update infrastructure configuration"
/>
```
---
# Authoring Runbooks › Blocks › <GitLabAuth>
The `<GitLabAuth>` block authenticates to GitLab.
> **Tip:**
> `<GitLabAuth>` is a GitLab-locked alias of the generic [`<GitAuth>`](/authoring/blocks/gitauth/) block, equivalent to `<GitAuth provider="gitlab" hideProviderSelect />`. Use [`<GitAuth>`](/authoring/blocks/gitauth/) directly if you want a provider dropdown so users can pick GitHub or GitLab.
It detects an existing login from environment variables 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 an environment variable such as `GITLAB_TOKEN`.
### Basic usage
```mdx
<GitLabAuth
id="git-auth"
title="Authenticate to GitLab"
description="Sign in to access private repositories"
/>
<GitClone
id="clone-repo"
gitAuthId="git-auth"
title="Clone Repository"
prefilledUrl="https://gitlab.com/acme-corp/infrastructure-live"
/>
```
By default, Runbooks detects and uses credentials from, in order:
1. The `GITLAB_TOKEN`, `GITLAB_ACCESS_TOKEN` or `OAUTH_TOKEN` environment variable, checked in that order
2. The GitLab CLI (`glab config get token --host <host>`). When `glab` isn't installed, Runbooks reads the token from glab's `config.yml` instead, where `glab auth login` stores it unless glab keeps it in the system keyring.
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
<GitLabAuth
id="gitlab-auth"
detectCredentials={[{ env: { prefix: 'CI_' } }, 'cli']}
/>
```
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
`<GitLabAuth>` accepts the same props as [`<GitAuth>`](/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).
The block also publishes `GITLAB_USER`, `GITLAB_HOST`, `GIT_PROVIDER` (`gitlab`) and `__AUTHENTICATED` as [block outputs](/authoring/blocks/gitauth/#block-outputs). Don't read the token from the outputs. Scripts get it as `GITLAB_TOKEN` through `gitAuthId`, or from the session environment.
For full details on detection, scopes, and security, see the [`<GitAuth>`](/authoring/blocks/gitauth/) documentation.
---
# Authoring Runbooks › Blocks › <GitLabMergeRequest>
The `<GitLabMergeRequest>` block creates a GitLab merge request from changes made during a runbook. It is a GitLab-locked alias of [`<GitPullRequest>`](/authoring/blocks/gitpullrequest/), equivalent to `<GitPullRequest provider="gitlab" />`, and accepts the same props except `provider`. The provider is always `gitlab`, and a `provider` prop you set is ignored.
### Basic usage
```mdx
<GitLabAuth
id="gl-auth"
title="Connect to GitLab"
/>
<GitClone
id="clone-repo"
gitAuthId="gl-auth"
title="Clone Repository"
prefilledUrl="https://gitlab.com/acme-corp/infrastructure"
/>
<GitLabMergeRequest
id="create-mr"
gitAuthId="gl-auth"
title="Create Merge Request"
prefilledBranchName="upgrade-pipelines-v4"
prefilledCommitMessage="Upgrade Gruntwork Pipelines from v3 to v4 [skip ci]"
prefilledPullRequestTitle="[skip ci] Upgrade Gruntwork Pipelines from v3 to v4"
prefilledPullRequestDescription="Upgrades Pipelines configuration from v3 to v4."
/>
```
### 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. |
| `autoCollapse` | `boolean` | No | Folds the block down to its title once the merge request is opened. Defaults to `false`. See [Folding done blocks](/authoring/blocks/#folding-done-blocks). |
| `gitAuthId` | `string` | No | Reference to a [`<GitAuth>`](/authoring/blocks/gitauth) or [`<GitLabAuth>`](/authoring/blocks/gitlabauth) block. |
| `inputsId` | `string \| string[]` | No | Reference to one or more [`<Inputs>`](/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 `<GitPullRequest>` and apply to merge requests the same way.
### Block outputs
After a successful create, the block produces outputs that can be referenced by downstream blocks. Their names are the same as on [`<GitPullRequest>`](/authoring/blocks/gitpullrequest/#block-outputs), so there is no `MR_ID` or `MR_URL`:
| Output | Description | Example |
|--------|-------------|---------|
| `PR_ID` | The merge request **iid**, its number within the project | `42` |
| `PR_URL` | The full URL of the created merge request | `https://gitlab.com/acme-corp/infrastructure/-/merge_requests/42` |
Pass an output to a later block's script with its [`env`](/authoring/blocks/command/#passing-values-as-environment-variables) prop:
```mdx
<Command
id="notify"
title="Post MR Link"
env={{ MR_URL: "{{ .outputs.create_mr.PR_URL }}" }}
command='echo "MR created: $MR_URL"'
/>
```
> **Note:**
> Block IDs with hyphens are converted to underscores in template variables. For example, `create-mr` becomes `create_mr`.
---
# Authoring Runbooks › Blocks › <GitPullRequest>
The `<GitPullRequest>` block commits the changes made in a repository cloned by [`<GitClone>`](/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 `<GitAuth>` and `<GitClone>`. The provider comes from the linked `<GitAuth>` block, so no `provider` prop is needed:
```mdx
<GitAuth
id="git-auth"
title="Connect to Git"
/>
<GitClone
id="clone-repo"
gitAuthId="git-auth"
title="Clone Repository"
prefilledUrl="https://github.com/acme-corp/infrastructure"
/>
<GitPullRequest
id="create-pr"
gitAuthId="git-auth"
title="Open Pull Request"
description="Create a PR with the changes made in this runbook"
/>
```
### Provider detection
`<GitPullRequest>` 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 one the block is locked to, the block shows a blocking error. The provider is locked by the `provider` prop or by the [provider-locked aliases](#provider-locked-aliases), so a GitLab `<GitAuth>` block linked to a `<GitHubPullRequest>` block shows the error. Without `provider`, the linked auth block decides the provider, so there is no mismatch to show. A GitLab `<GitAuth>` block linked while the cloned repo is on GitHub fails when you create the merge request instead, before anything is committed, because the repository's `origin` isn't on the auth block's GitLab host.
### 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
<GitPullRequest
id="create-pr"
gitAuthId="git-auth"
prefilledPullRequestTitle="Add VPC module configuration"
prefilledPullRequestDescription="## Changes\n- Added VPC module\n- Configured NAT gateway"
prefilledPullRequestLabels={["enhancement", "terraform"]}
prefilledBranchName="feature/add-vpc"
prefilledCommitMessage="Add VPC module configuration"
/>
```
> **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
<GitPullRequest
id="create-pr"
gitAuthId="git-auth"
prefilledPullRequestTitle="Add {{ .outputs.config.MODULE_NAME }} module"
prefilledBranchName="feature/{{ .outputs.config.MODULE_NAME }}"
/>
```
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. |
| `autoCollapse` | `boolean` | No | Folds the block down to its title once the pull or merge request is opened. Defaults to `false`. See [Folding done blocks](/authoring/blocks/#folding-done-blocks). |
| `provider` | `'github' \| 'gitlab'` | No | Locks the provider. Left unset, the provider is derived as described in [Provider detection](#provider-detection). Write the value in lowercase. Any other value, such as `"GitHub"`, makes the block fail to render. |
| `inputsId` | `string \| string[]` | No | Reference to one or more [`<Inputs>`](/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 [`<GitAuth>`](/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 [`<GitHubAuth>`](/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` |
Pass an output to a later block's script with its [`env`](/authoring/blocks/command/#passing-values-as-environment-variables) prop:
```mdx
<Command
id="notify"
title="Post PR Link"
env={{ PR_URL: "{{ .outputs.create_pr.PR_URL }}" }}
command='echo "PR created: $PR_URL"'
/>
```
> **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 `<GitClone>` block that has cloned or selected a repository, which the PR/MR is created against. With `gitAuthId` or `githubAuthId`, it also needs the auth block it names to have signed in. 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 waits for the `__AUTHENTICATED` output that the block referenced by `gitAuthId` or `githubAuthId` sets once it signs in, whichever way the user signed in. Until then, the block displays "Waiting for <provider> authentication" and links to the block to complete. Without `gitAuthId` or `githubAuthId`, the block doesn't wait for an auth block. It creates the PR/MR with the session's token for the provider, and fails if the session has none.
For the repository, the block uses the workspace's active repository: the one selected in the workspace's repository dropdown, which is the first one added, whether by a clone or a [local checkout](/authoring/blocks/gitclone/#using-a-local-checkout), until the user selects another. No prop picks a `<GitClone>` block. Until a `<GitClone>` block has added a repository to the workspace, the block displays "No repository available". A `<GitClone>` block with `showFileTree={false}` doesn't add its repository.
### 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
<GitAuth
id="git-auth"
provider="github"
host="github.example.com"
/>
<GitClone
id="clone-repo"
gitAuthId="git-auth"
prefilledUrl="https://github.example.com/platform/infrastructure-live"
/>
<GitPullRequest
id="create-pr"
gitAuthId="git-auth"
title="Open Pull Request"
/>
```
See [GitHub Enterprise support](/authoring/blocks/githubauth/#github-enterprise-support) for configuring the host.
## Provider-locked aliases
Two aliases lock `<GitPullRequest>` to a single provider:
- [`<GitHubPullRequest>`](/authoring/blocks/githubpullrequest/) is `<GitPullRequest provider="github" />`.
- [`<GitLabMergeRequest>`](/authoring/blocks/gitlabmergerequest/) is `<GitPullRequest provider="gitlab" />`.
### Example: full workflow
```mdx
<GitAuth
id="git-auth"
title="Authenticate to Git"
/>
<GitClone
id="clone-infra"
gitAuthId="git-auth"
title="Clone Infrastructure Repo"
prefilledUrl="https://github.com/acme-corp/infrastructure-live"
/>
<Command
id="make-changes"
title="Apply Configuration"
env={{ REPO_DIR: "{{ .outputs.clone_infra.clone_path }}" }}
command='echo "new_setting = true" >> "$REPO_DIR/config.hcl"'
/>
<GitPullRequest
id="open-pr"
gitAuthId="git-auth"
title="Open Pull Request"
prefilledPullRequestTitle="Update infrastructure configuration"
prefilledPullRequestLabels={["infrastructure"]}
prefilledCommitMessage="Update infrastructure configuration"
/>
```
---
# Authoring Runbooks › Blocks › <GoogleAuth>
The `<GoogleAuth>` 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
<GoogleAuth
id="google-auth"
title="Authenticate to Google Cloud"
description="Choose your preferred authentication method"
defaultRegion="us-central1"
/>
```
## 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
<GoogleAuth id="gcp-auth" defaultTab="gcloud" />
```
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) |
| `autoCollapse` | `boolean` | `false` | Folds the block down to its title once it's signed in. See [Folding done blocks](/authoring/blocks/#folding-done-blocks) |
| `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. It doesn't change which configuration detection reads. See [gcloud Config](#gcloud-config) (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 and impersonated service accounts 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, and template expressions are resolved. 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}`, or an array such as `detectCredentials={['env']}`. A string, such as `detectCredentials="env"`, is not an array, so the block detects nothing.
## 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 `<Command>` 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
<GoogleAuth
id="google-auth"
title="Authenticate to Google Cloud"
project="my-project-123456"
defaultRegion="us-central1"
/>
<Check
id="verify-identity"
title="Verify Google Cloud Identity"
command="gcloud auth list --filter=status:ACTIVE --format='value(account)'"
googleAuthId="google-auth"
successMessage="Successfully authenticated!"
/>
<Command
id="list-buckets"
title="List Storage Buckets"
command="gcloud storage ls"
googleAuthId="google-auth"
successMessage="Buckets listed!"
/>
```
> **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
<GoogleAuth
id="source-project"
title="Source Project (Development)"
project="acme-dev-123456"
defaultRegion="us-central1"
/>
<Command
id="export-data"
title="Export Data from Source"
command="gcloud storage cp gs://acme-dev-data/export.json /tmp/export.json"
googleAuthId="source-project"
/>
<GoogleAuth
id="target-project"
title="Target Project (Production)"
project="acme-prod-987654"
defaultRegion="europe-west1"
/>
<Command
id="import-data"
title="Import Data to Target"
command="gcloud storage cp /tmp/export.json gs://acme-prod-data/import.json"
googleAuthId="target-project"
/>
```
> **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. It also accepts the other Google credential documents, such as user credentials (`authorized_user`), and validates them as Application Default Credentials. It takes them 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
<GoogleAuth
id="google-auth"
title="Sign in to Google Cloud"
scopes={[
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/admin.directory.rolemanagement',
'https://www.googleapis.com/auth/userinfo.email',
'openid',
]}
/>
```
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
<GoogleAuth
id="google-auth"
oauthClientId="123456789012-abcdefghijklmnop.apps.googleusercontent.com"
oauthClientSecret="GOCSPX-example-not-a-real-secret"
/>
```
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
<GoogleAuth
id="google-auth"
oauthClientFile="~/.config/gcloud/client_secret_example.json"
/>
```
`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.
To pre-select a configuration on this tab, set the `gcloudConfiguration` prop. Set `defaultTab="gcloud"` too, so the block opens on the tab:
```mdx
<GoogleAuth
id="google-auth"
title="Use your local gcloud setup"
defaultTab="gcloud"
gcloudConfiguration="production"
/>
```
The user can still pick another configuration. `gcloudConfiguration` doesn't affect [credential detection](#credential-detection): the `'gcloud'` source always reads the active configuration.
> **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']} */}
<GoogleAuth id="google-auth" />
```
### Disable auto-detection
Force manual authentication only:
```mdx
<GoogleAuth
id="google-auth"
detectCredentials={false}
/>
```
### Include the active gcloud configuration
```mdx
<GoogleAuth
id="google-auth"
detectCredentials={['env', 'gcloud', 'adc']}
/>
```
### 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
<GoogleAuth
id="prod-auth"
detectCredentials={[{ env: { prefix: 'PROD_' } }]}
/>
```
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
version: 1
tests:
- name: happy-path
steps:
- block: google-auth
env_prefix: CI_ # Checks CI_GOOGLE_APPLICATION_CREDENTIALS, CI_GOOGLE_CLOUD_PROJECT, etc.
expect: success
```
Unlike the block, the test runner falls back to the unprefixed variables when it finds no prefixed credential. See [Testing GoogleAuth blocks](/authoring/testing/#testing-googleauth-blocks).
### 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
<Command
id="impersonate-sa"
path="scripts/impersonate-sa.sh"
title="Impersonate Deployment Service Account"
/>
<GoogleAuth
id="impersonated-auth"
title="Deployment Service Account"
detectCredentials={[{ block: 'impersonate-sa' }]}
/>
<Command
id="deploy"
command="gcloud run deploy api --source ."
googleAuthId="impersonated-auth"
/>
```
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 is the `project` prop when it is set. Otherwise it is the first of `CLOUDSDK_CORE_PROJECT`, `GOOGLE_CLOUD_PROJECT` and `GOOGLE_PROJECT` that the outputs set.
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...*, with a link to that block, 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.
Google says when an access token expires. Five minutes before then, the block turns red, says when it expires, and offers **Sign in again**. Once it has expired, the block says so. It stays signed in until you sign in again. The other credential types renew their own tokens, so the block never warns about them.
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 line above the sign-in tabs says what the user can do, followed by a **Check again** button that re-runs detection. It reads *No existing credentials found* when detection found nothing, asks the user to fix the credentials when the ones it found didn't validate, and says to sign in with different credentials after the user turned detected credentials down or clicked **Re-authenticate**. With `detectCredentials={false}` the line and the button are hidden.
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. The command reads the Desktop OAuth client from `GOOGLE_OAUTH_CLIENT_CREDENTIALS`, so the user sets that variable to the path of a Desktop-app client JSON first. See [Using your own OAuth client](#using-your-own-oauth-client). When the author sets `scopes`, the command includes `--scopes=...`:
```bash
gcloud auth application-default login --client-id-file="$GOOGLE_OAUTH_CLIENT_CREDENTIALS" --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 JSON has no `type` field, as in an OAuth client file, or it is a service account key that is missing a field such as `private_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 click **Check 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.
---
# Authoring Runbooks › Blocks › <Iframe>
The `<Iframe>` 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, except the values of the Inputs blocks you pass to a local page with [`inputsId`](#exchanging-values-with-a-local-page), and it has no access to the Runbooks app.
## Basic usage
Embed an external site:
```mdx
<Iframe src="https://runbooks.gruntwork.io/authoring/blocks/command/" title="Command block docs" />
```
Embed a page from the runbook's `assets/` folder:
```mdx
<Iframe src="./assets/dashboard/index.html" title="Deployment dashboard" height={600} />
```
## 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
<Inputs id="cluster">
```yaml
variables:
- name: environment
type: string
default: staging
```
</Inputs>
<Iframe id="region-picker" src="./assets/picker/index.html" inputsId="cluster" outputs={["region"]} />
<Command
id="configure-kubectl"
env={{ REGION: "{{ .outputs.region_picker.region }}" }}
command='aws eks update-kubeconfig --name my-cluster --region "$REGION"'
/>
````
### 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 `<Inputs>` 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 page decides what they hold, so pass them to a script with the block's [`env`](/authoring/blocks/command/#passing-values-as-environment-variables) prop, which hands the script each value as data.
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.
---
# Authoring Runbooks › Blocks › <Inputs>
The `<Inputs>` block renders a form to collect user input. Other blocks use the collected values through their `inputsId` prop: [Command](/authoring/blocks/command/), [Check](/authoring/blocks/check/) and [Finish](/authoring/blocks/finish/) blocks in their scripts, [Template](/authoring/blocks/template/) and [TemplateInline](/authoring/blocks/templateinline/) blocks in their templates, and most other blocks in their props.
## Basic usage
`````mdx
<Inputs id="config">
```yaml
variables:
- name: ProjectName
type: string
description: Name of your project
validations:
- required
```
</Inputs>
`````
## vs. Template
Inputs and [Template](/authoring/blocks/template/) both render forms to collect variable values:
| Feature | `<Inputs>` | `<Template>` |
|---------|------------|--------------|
| 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` (or `boilerplate.yaml`) file, or to the file itself, relative to the runbook. Required unless the block has inline YAML. [Runbook tests](/authoring/testing/) only read a file named `boilerplate.yml`.
- `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 `<Inputs>` tags in the style of a `boilerplate.yml` file, wrapped in a ` ```yaml ` code fence:
`````mdx
<Inputs id="user-settings">
```yaml
variables:
- name: Username
type: string
description: Your username
validations:
- required
- name: NotifyByEmail
type: bool
description: Receive email notifications
default: true
```
</Inputs>
`````
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
<Inputs id="vpc-config" path="templates/vpc" />
```
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, and are what a field's **Reset** button puts back:
`````mdx
<Inputs
id="aws-config"
path="templates/aws-settings"
prefilledVariables={{
AwsRegion: "us-west-2",
Environment: "production"
}}
/>
`````
`prefilledVariables` also sets [locked and hidden](/authoring/boilerplate/#x-locked) fields, which the user can't change. A runbook that loads a shared `boilerplate.yml` can use it to set their values for itself.
## When other blocks get the values
Until the user first clicks **Submit**, other blocks get the values the form started with, such as each variable's `default`, not what the user has typed. Once the form has been submitted, other blocks get each change as the user makes it, without another Submit. An Inputs block [embedded in a Command or Check](#embedded-in-command-or-check) submits itself, so its parent always gets the current values.
## Using variables in Commands and Checks
Command, Check, Finish, Template and TemplateInline blocks read the values of an Inputs block through `inputsId`. A Command or Check whose script or `env` references `{{ .inputs.X }}` needs `inputsId` or an [embedded](#embedded-in-command-or-check) Inputs block. Without one, the block shows only a "Configuration Required" warning and can't run.
### Reference by `inputsId`
`````mdx
<Inputs id="vpc-config">
```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"
```
</Inputs>
<Command
id="create-vpc"
env={{ CIDR_BLOCK: "{{ .inputs.CidrBlock }}", VPC_NAME: "{{ .inputs.VpcName }}" }}
command='aws ec2 create-vpc --cidr-block "$CIDR_BLOCK" --tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=$VPC_NAME}]"'
inputsId="vpc-config"
title="Create VPC"
successMessage="VPC {{ .inputs.VpcName }} created!"
/>
`````
### 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
<Inputs id="global-config">
```yaml
variables:
- name: Environment
type: enum
options: [dev, staging, prod]
default: dev
```
</Inputs>
<Inputs id="app-config">
```yaml
variables:
- name: AppName
type: string
- name: Port
type: int
default: 8080
```
</Inputs>
<Command
id="deploy"
path="scripts/deploy.sh"
inputsId={["global-config", "app-config"]}
env={{
ENVIRONMENT: "{{ .inputs.Environment }}",
APP_NAME: "{{ .inputs.AppName }}",
PORT: "{{ .inputs.Port }}",
}}
title="Deploy Application"
/>
`````
### Embedded in Command or Check
You can embed Inputs directly inside a Command or Check block:
`````mdx
<Command
id="greet"
env={{ NAME: "{{ .inputs.Name }}" }}
command='echo "Hello, $NAME!"'
title="Say Hello"
>
<Inputs id="greeting-inputs">
```yaml
variables:
- name: Name
type: string
description: Your name
validations:
- required
```
</Inputs>
</Command>
`````
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.
The same YAML can also lay the form out further. [`order`](/authoring/boilerplate/#field-order) places fields, [`x-sections`](/authoring/boilerplate/#x-sections) folds sections, and [`x-locked`](/authoring/boilerplate/#x-locked) and [`x-hidden`](/authoring/boilerplate/#x-hidden) show a field read-only or leave it out, for values the runbook sets itself.
## Complete example
`````mdx
# Create a GitHub Repository
Enter your repository settings:
<Inputs id="repo-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
```
</Inputs>
First, verify you're authenticated to GitHub:
<Check
id="check-auth"
command="gh auth status"
title="Verify GitHub Authentication"
successMessage="Authenticated to GitHub!"
failMessage="Not authenticated. Run 'gh auth login' first."
/>
Now create the repository:
<Command
id="create-repo"
env={{
ORG: "{{ .inputs.OrgName }}",
REPO: "{{ .inputs.RepoName }}",
VISIBILITY: "{{ .inputs.Visibility }}",
}}
command='gh repo create "$ORG/$REPO" "--$VISIBILITY"'
inputsId="repo-settings"
title="Create Repository"
successMessage="Repository {{ .inputs.OrgName }}/{{ .inputs.RepoName }} created!"
failMessage="Failed to create repository"
/>
Verify the repository was created:
<Check
id="verify-repo"
env={{ ORG: "{{ .inputs.OrgName }}", REPO: "{{ .inputs.RepoName }}" }}
command='gh repo view "$ORG/$REPO"'
inputsId="repo-settings"
title="Verify Repository Exists"
successMessage="Repository verified!"
/>
`````
---
# Authoring Runbooks › Blocks › <Prerequisites>
The `<Prerequisites>` block lists the tools a runbook needs and checks that each one is installed: on PATH, and at the version the runbook asks for. It marks each tool that is missing or out of date and says how to install it. Once the user has installed it, they click **Check again**, and Runbooks finds the tool without a restart.
Put it near the top of the runbook, before the blocks that use the tools.
## Basic usage
````mdx
<Prerequisites id="tools">
```yaml
tools:
- git
- name: tofu
version: ">= 1.6"
install: "[Install OpenTofu](https://opentofu.org/docs/intro/install/)"
- name: gh
description: Opens the pull request at the end of the runbook.
optional: true
install: Run `brew install gh`.
```
</Prerequisites>
````
The block lists the tools. When the user clicks **Check**, each tool gets a green mark when it's installed at the right version, and a red one when it isn't, with its install hint. A missing optional tool gets a yellow mark and makes the check warn instead of fail.
## vs. Check
A [Check](/authoring/blocks/check/) can test anything, but you write the script, including the version comparison and the message for each tool. A Prerequisites block takes a list of tools and does the rest: it finds each one as a script would, compares versions, shows each tool's result and install hint, and reads PATH again when a tool is missing, so a tool installed while Runbooks is open is found.
## Props
### Required props
- `id` (string): unique identifier for this block
### Optional props
- `title` (string): display title. Default "Tools this runbook needs". Supports inline markdown.
- `description` (string): longer description, shown under the title. Supports inline markdown.
- `autoCollapse` (boolean): whether the block folds down to its title once it's done. Defaults to `false`. Click it to show the block in full again. See [Folding done blocks](/authoring/blocks/#folding-done-blocks).
- `successMessage` (string): message shown when every tool is installed. Default "The tools this runbook needs are installed." Supports inline markdown.
- `warnMessage` (string): message shown when only optional tools are missing or out of date. Default "The required tools are installed. Some optional ones are missing or out of date." Supports inline markdown.
- `failMessage` (string): message shown when a required tool is missing or out of date. Default "Install or update the tools marked below, then check again." Supports inline markdown.
- `runningMessage` (string): message shown while the check runs. Default "Checking the tools...". Supports inline markdown.
### The tool list
The block's content is a YAML code fence with a `tools:` list, and nothing else. Each entry is a tool's name, a map with these keys, or an [`anyOf` list](#alternatives) of tools any one of which will do:
| Key | Description |
|-----|-------------|
| `name` | Required. The command to look for on PATH, such as `tofu`. It starts with a letter or digit, and has only letters, digits, `.`, `_`, `+` and `-`, so it can't be a path. |
| `description` | What the runbook uses the tool for. Supports inline markdown. |
| `version` | The versions the runbook works with, such as `">= 1.6"`. See [Version constraints](#version-constraints). Quote it: YAML reads an unquoted `1.10` as the number 1.1. |
| `versionArgs` | The arguments that make the tool print its version, as a list such as `[version, --client]`, or a string that is split at spaces. Defaults to `--version` for a tool with a `version`. Give it to a tool without a `version` to publish the version it prints. |
| `install` | How to install the tool, shown when it's missing or out of date. Supports inline markdown, such as a link or a command in backticks. |
| `optional` | `true` for a tool the runbook can do without. A missing or outdated optional tool makes the check warn. Default `false`. |
A mistake in the list, such as a key that isn't one of these or a version constraint Runbooks can't read, is a configuration error, which the block shows in place of the tools.
### Alternatives
When any one of several tools will do, list them under `anyOf`. A runbook that opens a pull request with `gh` on GitHub, or a merge request with `glab` on GitLab, needs only the one for the user's Git host:
```yaml
tools:
- git
- anyOf:
- name: gh
install: "[Install the GitHub CLI](https://cli.github.com/)"
- name: glab
install: "[Install the GitLab CLI](https://gitlab.com/gitlab-org/cli#installation)"
description: Opens the pull request on GitHub, or the merge request on GitLab.
```
The block shows the tools under one heading. The check meets the list when it finds one of them, at the right version if that tool has a `version`. The others don't need to be installed: they get a gray mark and no install hint. When it finds none of them, it marks each one, with its install hint.
Each tool under `anyOf` is a name or a map with the keys above, except `optional`. The `anyOf` entry itself has:
| Key | Description |
|-----|-------------|
| `anyOf` | Required. The tools, two or more. |
| `description` | What the runbook uses the tools for. Supports inline markdown. |
| `optional` | `true` when the runbook can do without all of them. A list none of whose tools is found then makes the check warn. Default `false`. |
Each tool in the list publishes its own [outputs](#outputs), so a later block can tell which one is installed from the `<tool>_path` that isn't empty:
```mdx
<Command
id="open-pr"
env={{ GH_CLI_PATH: "{{ .outputs.tools.gh_path }}" }}
command={`if [ -n "$GH_CLI_PATH" ]; then
gh pr create --fill
else
glab mr create --fill
fi`}
/>
```
## How the check works
When the user clicks **Check**, Runbooks:
1. Looks for each tool on PATH, with the session's environment and working directory, the way a script finds a command (`type -P` in bash). Only an executable file counts, not a shell function or alias.
2. If a tool the runbook needs isn't found, reads PATH again from your login shell and looks for the missing tools once more. A tool of an `anyOf` list doesn't count when the check found another of its tools. See [Tools installed after Runbooks started](#tools-installed-after-runbooks-started).
3. Runs each tool it found that has a `version` or `versionArgs`, with its version arguments, and reads the first version number in what the tool prints, on stdout or stderr. A version number is two or more numbers joined by dots, such as `1.8.2`. Each tool gets 15 seconds.
4. Compares each version with the tool's constraint.
The check runs only when the user clicks **Check**. Opening the runbook runs nothing, and neither does resuming its [session](/commands/sessions/): the block shows the last check's result. While the check runs, **Stop** stops it.
**View Logs** shows a line for each tool: where it found the tool and the version it read, or what went wrong. When a tool printed no version number, the log quotes the first line it printed.
### Version constraints
`version` takes the syntax of OpenTofu's `required_version`:
| Constraint | Versions |
|------------|----------|
| `1.6.0` or `= 1.6.0` | Exactly 1.6.0 |
| `!= 1.6.0` | Any but 1.6.0 |
| `> 1.6`, `>= 1.6`, `< 2.0`, `<= 2.0` | Newer or older than the version |
| `~> 1.6` | 1.6 or newer, up to but not including 2.0: only the last number given can go up |
| `~> 1.6.2` | 1.6.2 or newer, up to but not including 1.7 |
Separate conditions with commas to require all of them, as in `">= 1.6, < 2.0"`. Versions compare number by number, and a missing number counts as 0, so `1.6` is the same version as `1.6.0`. Only the numbers count: a tool that prints `1.7.0-beta1` is at version 1.7.0, and a constraint can't name a pre-release.
### Status
A tool is one of:
- **Found**: on PATH, at a version that meets its constraint if it has one.
- **Missing**: not on PATH.
- **Wrong version**: on PATH, at a version that doesn't meet its constraint.
- **Unknown version**: on PATH, but the check couldn't read a version to compare with its `version`: the tool printed no version number, or didn't finish within 15 seconds. The log says which.
The block succeeds when every tool is found, counting an `anyOf` list as found when one of its tools is. It warns when only optional tools or lists aren't found. A required one that isn't found fails it.
## Tools installed after Runbooks started
Runbooks gets PATH once, when it starts. An installer usually adds its directory to PATH in a shell profile, such as `~/.zshrc`, which only a new shell reads. So when a check can't find a tool the runbook needs, Runbooks first runs your login shell to read PATH again, as it does when it [starts from the Dock](/security/shell-execution-context/#starting-environment), and adds the directories that PATH has and Runbooks' doesn't. Then it looks for the missing tools again. The log names the directories it added.
The directories go on the end of PATH, for the open session and for the sessions opened after it, so the blocks that run next find the tools too. Runbooks never removes or reorders a directory, and it changes no other variable. It does this however Runbooks was started, from a terminal too.
On Windows, Runbooks can't read PATH again yet. The log says so: restart Runbooks to pick up a changed PATH.
## Outputs
A check that succeeds or warns publishes outputs for later blocks:
| Output | Value |
|--------|-------|
| `<tool>_path` | Where the tool is, or empty when it isn't on PATH |
| `<tool>_version` | The version the check read, or empty when it couldn't read one. Only for a tool with a `version` or `versionArgs` that is on PATH. |
`<tool>` is the tool's name with each character that isn't a letter, a digit or `_` turned into `_`, so `session-manager-plugin` publishes `session_manager_plugin_path`. A name that starts with a digit gets a `_` in front. Two tools whose names come out the same, such as `docker-compose` and `docker_compose`, are a configuration error.
```mdx
<Command
id="show-tofu"
env={{ TOFU_VERSION: "{{ .outputs.tools.tofu_version }}" }}
command='echo "Using tofu $TOFU_VERSION"'
/>
```
A check that fails publishes no outputs, so the blocks that read them wait, as they do for a failed Check. See [Block outputs](/authoring/inputs-and-outputs/#block-outputs).
An optional tool that is missing has no `<tool>_version` output, so a block that reads one waits until the tool is installed. To read it only when it's there, check for it first, as [Outputs a block emits only sometimes](/authoring/inputs-and-outputs/#outputs-a-block-emits-only-sometimes) shows.
## Run order
Other blocks can name a Prerequisites block in `dependsOn`, so they can't run until its check succeeds or warns:
```mdx
<Command id="deploy" dependsOn="tools" command="tofu apply" />
```
A Prerequisites block can't start while an `exclusive` block runs. See [Run order](/authoring/blocks/command/#run-order).
## Instruction mode
In [instruction mode](/intro/instruction-mode/), the block lists the tools with their version constraints and every install hint, and shows commands that check them: the version command of each tool the block reads a version of, and `command -v <name>` for the others. An `anyOf` list's commands are on one line, joined with `||`, so the line succeeds when any of its tools is installed.
## Testing
`runbooks test` checks the tools the same way, on the PATH the test runs with, but it doesn't read PATH again for a missing tool. See [Testing Prerequisites blocks](/authoring/testing/#testing-prerequisites-blocks).
The [prerequisites feature demo](https://github.com/gruntwork-io/runbooks/tree/main/testdata/feature-demos/prerequisites) checks for a tool that a later block installs, and its test checks it before and after.
---
# Authoring Runbooks › Blocks › <Template>
The `<Template>` 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
<Template id="vpc-setup" path="templates/vpc" />
```
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>` | `<TemplateInline>` |
|---------|-------------|-------------------|
| 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 `<GitClone>` block to have run first. The active worktree is the one selected in the workspace's repository dropdown, which is the first repository added, whether by a clone or a [local checkout](/authoring/blocks/gitclone/#using-a-local-checkout), until the user selects another.
> **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
<Template id="my-template" path="templates/vpc" />
```
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
<Template id="vpc-setup" path="templates/vpc" />
```
The `boilerplate.yml` also lays the form out: [`order`](/authoring/boilerplate/#field-order) places fields, [`x-section`](/authoring/boilerplate/#x-section) and [`x-sections`](/authoring/boilerplate/#x-sections) group them under headings that can fold, and [`x-locked`](/authoring/boilerplate/#x-locked) and [`x-hidden`](/authoring/boilerplate/#x-hidden) show a field read-only or leave it out, for values the runbook sets itself.
### Using `inputsId`
Import variables from a separate Inputs block:
`````mdx
<Inputs id="config">
```yaml
variables:
- name: Environment
type: enum
options: [dev, staging, prod]
default: dev
- name: Region
type: string
default: us-east-1
```
</Inputs>
<Template id="vpc" path="templates/vpc" inputsId="config" />
`````
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
<Inputs id="org-config">
```yaml
variables:
- name: OrgName
type: string
```
</Inputs>
<Inputs id="env-config">
```yaml
variables:
- name: Environment
type: enum
options: [dev, prod]
```
</Inputs>
<Template
id="infrastructure"
path="templates/infra"
inputsId={["org-config", "env-config"]}
/>
`````
## 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, unless its `boilerplate.yml` [locks](/authoring/boilerplate/#x-locked) or [hides](/authoring/boilerplate/#x-hidden) them. 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:
<Inputs id="env-config">
```yaml
variables:
- name: Environment
type: enum
options: [dev, staging, prod]
default: dev
- name: Region
type: string
default: us-east-1
```
</Inputs>
Now configure your VPC. The Environment and Region will be inherited from above:
<Template id="vpc-setup" path="templates/vpc" inputsId="env-config" />
`````
---
# Authoring Runbooks › Blocks › <TemplateInline>
The `<TemplateInline>` block renders a Boilerplate template written in the runbook file itself and shows the rendered output inline. [`<Template>`](/authoring/blocks/template/) loads its templates from a directory.
## Basic usage
`````mdx
<Inputs id="greeting">
```yaml
variables:
- name: Name
type: string
default: World
```
</Inputs>
<TemplateInline id="greeting-preview" inputsId="greeting" outputPath="hello.txt">
```txt
Hello, {{ .inputs.Name }}!
```
</TemplateInline>
`````
## vs. Template
TemplateInline and [Template](/authoring/blocks/template/) both render Boilerplate templates:
| Feature | `<TemplateInline>` | `<Template>` |
|---------|-------------------|--------------|
| 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 `<TemplateInline>` tags. The block uses one code block: if there are several, the last one is the template. The rendered output is shown without syntax highlighting, whatever language hint the code block has.
### 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). The block waits to render until every input the template inserts, such as `{{ .inputs.Name }}`, has a value. An empty string doesn't count, so give an optional input a non-empty default. Without `inputsId`, a template that inserts an input never renders.
- `outputPath` (string). File path for the rendered output (e.g., `config.yaml` or `docs/account.hcl`). Defaults to `template.txt`. 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's repository dropdown, which is the first repository added, whether by a clone or a [local checkout](/authoring/blocks/gitclone/#using-a-local-checkout), until the user selects another. It requires a `<GitClone>` 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
<Inputs id="docker-config">
```yaml
variables:
- name: AppName
type: string
- name: NodeVersion
type: string
default: "18"
```
</Inputs>
<TemplateInline id="docker-preview" inputsId="docker-config" outputPath="Dockerfile">
```dockerfile
FROM node:{{ .inputs.NodeVersion }}-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
```
</TemplateInline>
`````
### 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
<Inputs id="global-config">
```yaml
variables:
- name: Environment
type: enum
options: [dev, staging, prod]
default: dev
```
</Inputs>
<Inputs id="service-config">
```yaml
variables:
- name: ServiceName
type: string
- name: Port
type: int
default: 8080
```
</Inputs>
<TemplateInline id="deploy-preview" inputsId={["global-config", "service-config"]} outputPath="deploy.tf">
```hcl
resource "aws_ecs_service" "main" {
name = "{{ .inputs.ServiceName }}-{{ .inputs.Environment }}"
load_balancer {
container_port = {{ .inputs.Port }}
}
}
```
</TemplateInline>
`````
## Multiple files
You can have multiple `<TemplateInline>` blocks referencing the same `<Inputs>`:
`````mdx
<Inputs id="app-config">
```yaml
variables:
- name: AppName
type: string
- name: Environment
type: enum
options: [dev, prod]
```
</Inputs>
<TemplateInline id="app-config-preview" inputsId="app-config" outputPath="config.yaml">
```yaml
app:
name: {{ .inputs.AppName }}
environment: {{ .inputs.Environment }}
```
</TemplateInline>
<TemplateInline id="app-env-preview" inputsId="app-config" outputPath=".env">
```bash
APP_NAME={{ .inputs.AppName }}
ENVIRONMENT={{ .inputs.Environment }}
```
</TemplateInline>
`````
## With Boilerplate logic
Templates can use Boilerplate template syntax, including conditionals and loops. Reference an input as `{{ .inputs.Name }}`. The shorter `{{ .Name }}`, which works in the files a `<Template>` renders, shows a template error here.
`````mdx
<Inputs id="terraform-config">
```yaml
variables:
- name: Environment
type: enum
options: [dev, prod]
- name: EnableMonitoring
type: bool
default: false
```
</Inputs>
<TemplateInline id="terraform-preview" inputsId="terraform-config" outputPath="main.tf">
```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 }}
}
```
</TemplateInline>
`````
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
<TemplateInline id="account-info" outputPath="account-info.txt">
```txt
Account ID: {{ .outputs.create_account.account_id }}
Region: {{ .outputs.create_account.region }}
```
</TemplateInline>
`````
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, `<TemplateInline>` 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
<Inputs id="config">
```yaml
variables:
- name: ProjectName
type: string
```
</Inputs>
<TemplateInline
id="readme-preview"
inputsId="config"
outputPath="README.md"
generateFile={true}
>
```markdown
# {{ .inputs.ProjectName }}
Welcome to {{ .inputs.ProjectName }}!
```
</TemplateInline>
`````
If `outputPath` changes between renders, for example because it uses a [`<DirPicker>`](/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 `<GitClone>` makes a different repo the active worktree.
## Complete example
`````mdx
# Configure Your Docker Service
Enter your service configuration:
<Inputs id="service-config">
```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
```
</Inputs>
Here's what your Docker Compose file will look like:
<TemplateInline id="docker-compose" inputsId="service-config" outputPath="docker-compose.yaml" generateFile={true}>
```yaml
version: "3.8"
services:
{{ .inputs.ServiceName }}:
build: .
ports:
- "{{ .inputs.Port }}:{{ .inputs.Port }}"
environment:
- NODE_ENV={{ .inputs.Environment }}
```
</TemplateInline>
And your environment file:
<TemplateInline id="env-file" inputsId="service-config" outputPath=".env" generateFile={true}>
```bash
SERVICE_NAME={{ .inputs.ServiceName }}
PORT={{ .inputs.Port }}
ENVIRONMENT={{ .inputs.Environment }}
```
</TemplateInline>
`````
---
# 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 `<Command {...props} />`
- `<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 `<Admonition.Title>`) 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 `<Command>` and `<Check>` blocks run when you click Run, and a `<Finish>` block's final check when you click Finish. A `<Prerequisites>` block runs when you click Check: it runs the tools it lists that it reads a version of, and your login shell if one of the tools is missing.
## Executable registry
When a runbook opens, the main process reads `runbook.mdx`, finds every `<Check>` and `<Command>`, and every `<Finish>` with a final check, 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. It stores the tool list of each `<Prerequisites>` block the same way. A check runs only the tools in its list, with the version arguments the list gives them, and takes no template values or environment overrides.
The main process reads the file as the renderer does. A block in a code block, in inline code or in a `{/* comment */}` isn't stored, and a runbook that isn't valid MDX, or that contains anything listed in [the section above](#opening-a-runbook-never-runs-its-code), stores nothing.
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 `<Check>`, `<Command>` and `<Finish>` 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 references such as `{{ .inputs.VarName }}` and `{{ .outputs.block_id.name }}` into the script.
3. It renders the block's [`env`](/authoring/blocks/command/#passing-values-as-environment-variables) values, from the registry's copy of the runbook, with the same values.
4. It writes the script to a temporary file.
5. It reads the shebang line to pick the interpreter, defaulting to `bash`.
6. It runs the interpreter on the file in a non-interactive shell, with the `env` values set as environment variables for this run.
7. It streams stdout and stderr to the renderer over IPC.
8. It deletes the temporary file.
Scripts run with your user's permissions and full environment. Only run runbooks you trust.
A value rendered into the script becomes part of its source, so the shell runs whatever code the value holds. A value in `env` reaches the script as data and never runs. A block output often holds text from outside the runbook, such as a PR title or an API response, so pass outputs to scripts with `env`.
For interpreter detection, shell limitations and how environment changes persist between scripts, see [Shell execution context](/security/shell-execution-context/).
---
# Security › 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. Runbooks saves the session, and opening the runbook again resumes it, even after you restart the app. See [Sessions](/commands/sessions/). To reset the environment, use the session controls in the UI. To start the runbook over, choose **File > Reset Session**. To go back to an earlier session, choose **File > Switch Session…**.
### 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.
A [resumed session](/commands/sessions/#resuming-a-session) starts from that same environment, and then gets back the variables its scripts and auth blocks had set or unset when it was last open.
When a [<Prerequisites>](/authoring/blocks/prerequisites/) block can't find a tool, Runbooks runs your login shell again, on macOS and Linux, to read `PATH`. It adds the directories that `PATH` has and the session's doesn't to the end of the session's `PATH`, and of the environment later sessions start from. See [Tools installed after Runbooks started](/authoring/blocks/prerequisites/#tools-installed-after-runbooks-started).
Scripts start in the [session's directory](/commands/sessions/), not in the folder that contains the runbook. A script that changes directory moves later scripts with it, as [How script changes are applied](#how-script-changes-are-applied) describes.
### 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 `<GitClone>` 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.
[Switching sessions](/commands/sessions/#switching-sessions) with **File > Switch Session…** stops a running script instead. Runbooks asks first.
### 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 selected in the workspace's repository dropdown, which is the first one added, whether by a clone or a [local checkout](/authoring/blocks/gitclone/#using-a-local-checkout), until you select another. Unset until a `<GitClone>` block has added a repository. |
| `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 `<GitClone>` 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)). |
| A terminal (TTY) | No | Tools may turn off colors and progress bars, and a `sudo` password prompt fails |
| Standard input | No | Input is empty, so `read` and confirmation prompts get no answer. Use non-interactive flags such as `-auto-approve` or `-y` |
### Aliases and binaries
```bash
# Fails: ll is usually a shell alias for "ls -l"
<Check command="ll" ... />
# Works: ls is a binary
<Check command="ls -l" ... />
```
### 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 `<GitClone>` 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
---
# Security › Telemetry
Runbooks sends no usage data. It has no analytics and no crash reporting.
Earlier versions contained code for sending usage events to Mixpanel, but no release was configured to send them. That code has been removed.
## Update checks
The installed app checks GitHub for a new release each time it starts, about ten seconds after launch. It uses [electron-updater](https://github.com/electron-userland/electron-builder/tree/master/packages/electron-updater) with the [releases of gruntwork-io/runbooks](https://github.com/gruntwork-io/runbooks/releases):
1. It reads `https://github.com/gruntwork-io/runbooks/releases.atom` to find the newest release.
2. It downloads that release's update file: `latest-mac.yml` on macOS, `latest.yml` on Windows or `latest-linux.yml` on Linux.
If that release is newer than yours, Runbooks tells you, downloads it from GitHub and asks whether to restart to install it. If you choose not to restart, the update installs when you quit.
These requests go to GitHub, not to Gruntwork. GitHub sees your IP address, as it does for any request. electron-updater also sends an `x-user-staging-id` header with a random ID that it creates once and keeps in a file named `.updaterId` in the Runbooks data folder. The ID is not derived from your machine or your account. GitHub counts how often each release file is downloaded, and Gruntwork can see those counts but not who made the downloads.
A build you run from source with `just dev` does not check for updates. There is no setting to turn the check off.
The update check is in [`electron/main/updater.ts`](https://github.com/gruntwork-io/runbooks/blob/main/electron/main/updater.ts).
## The `--no-telemetry` flag
Runbooks still accepts the `--no-telemetry` flag and the `RUNBOOKS_TELEMETRY_DISABLE` environment variable, so existing launch commands keep working. Neither has any effect, because there is nothing to turn off.
---
# 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 (bun check)
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 `bun check`, `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
---