Skip to main content
Orchard is built around a modular architecture where every provisioning concern — packages, settings, tools — lives in its own directory with a dedicated install.sh. Adding your own module follows the same pattern: create a directory, add an install.sh, and register it in provision.sh. The scaffold.sh script handles the directory and file creation for you.

How modules work

Each module is a subdirectory of the Orchard root. At minimum, every module contains an install.sh that performs its work. Modules can also include a test.bats file with Bats tests. The main provision.sh runs every registered module in order:
provision.sh
#!/bin/bash -euo pipefail

working_directory=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

install_scripts=(
  brew
  brew-packages
  cask-packages
  dockutil
  dock
  mac-app-store
  finder
  keyboard
  mouse
  trackpad
  security-and-privacy
  bats
  claude-code
)

for script in "${install_scripts[@]}"; do
  "${working_directory}/${script}/install.sh"
done

./test.sh
To register a new module, add its directory name to the install_scripts array. The order matters — modules run top to bottom, so place your module after any dependencies it has.

Creating a module with scaffold.sh

Run scaffold.sh from the Orchard root, passing your module name as the argument:
./scaffold.sh my-module
This creates two files:
  • my-module/install.sh — a minimal shell script with #!/bin/bash -euo pipefail, marked executable
  • my-module/test.bats — a Bats test file with a placeholder failing test to remind you to write real tests
my-module/install.sh
#!/bin/bash -euo pipefail
my-module/test.bats
#!/usr/bin/env bats

@test 'Implement me!' {
  [ 0 -eq 1 ]
}
scaffold.sh automatically runs chmod +x on the generated install.sh. If you create an install.sh manually without using scaffold.sh, you must make it executable yourself: chmod +x my-module/install.sh. Otherwise provision.sh will fail with a permission error when it tries to run it.

Complete example: git global settings

Here is a walkthrough of creating a module that writes global Git configuration settings.
1

Generate the module

Run scaffold.sh from the Orchard root:
./scaffold.sh git-config
This creates git-config/install.sh and git-config/test.bats.
2

Write the install logic

Open git-config/install.sh and add the commands you want to run. For example:
git-config/install.sh
#!/bin/bash -euo pipefail

git config --global core.autocrlf input
git config --global pull.rebase true
git config --global init.defaultBranch main
Each line runs as a standard shell command. You can use any tool that is available on the system — including packages installed by earlier modules in provision.sh.
3

Register the module in provision.sh

Open provision.sh and add git-config to the install_scripts array. Place it after brew-packages so that Git is already installed when the module runs:
provision.sh
install_scripts=(
  brew
  brew-packages
  cask-packages
  dockutil
  dock
  mac-app-store
  finder
  keyboard
  mouse
  trackpad
  security-and-privacy
  bats
  claude-code
  git-config
)
4

Run provision.sh

Execute the provisioner to run your new module along with everything else:
./provision.sh
To run only your new module during development, call its install.sh directly:
./git-config/install.sh
5

Replace the placeholder test

Open git-config/test.bats and replace the failing placeholder with a real assertion. For example:
git-config/test.bats
#!/usr/bin/env bats

@test 'git pull.rebase is set to true' {
  result=$(git config --global pull.rebase)
  [ "$result" = "true" ]
}
Orchard runs all test.bats files at the end of provision.sh via test.sh. A failing placeholder test will cause the overall run to report failure until you replace it.

Adding packages

Add Homebrew, cask, or Mac App Store packages to an existing module.

System settings

Modify defaults write values to tune macOS behavior.