https://github.com/cbrake/brun
The simple way to run native workflows. No containers required.
https://github.com/cbrake/brun
automation ci-cd embedded linux testing workflow
Last synced: about 2 months ago
JSON representation
The simple way to run native workflows. No containers required.
- Host: GitHub
- URL: https://github.com/cbrake/brun
- Owner: cbrake
- License: mit
- Created: 2025-10-03T00:15:46.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2026-04-20T16:36:03.000Z (2 months ago)
- Last Synced: 2026-05-12T13:45:29.412Z (about 2 months ago)
- Topics: automation, ci-cd, embedded, linux, testing, workflow
- Language: Go
- Homepage:
- Size: 10.9 MB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 16
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# BRun
Trigger β Run
Do you find tools like GitHub Actions or Ansible useful, but would like a simple
way to do similar things natively? BRun is a native Linux automation tool that
connects triggers (boot, cron, file changes, git commits) to actions (run
scripts, send emails, log events, reboot). Build CI/CD pipelines, automate
system tasks, or test embedded devicesβall with a single binary and no
dependencies.
**Features/goals:**
- β¨ **simple!!!**
- β‘ **fast!!!**
- π¦ no dependencies - [install](#example-install-on-linux) a single statically
linked binary and go for it ...
- π οΈ built-in [units](#units) for common tasks like boot, scripts, cron, email,
git, file watching
- π units can be chained into pipelines
- π» first priority is to run native
- π« does not require containers (but may support them in the future)
- π simple YAML [config format](#file-format)
- π built-in [secrets management](#secrets-management) with SOPS encryption
**Things you might do with BRun:**
- π Reboot cycle test for embedded systems.
- π Nightly Yocto builds on your powerful workstation.
- ποΈ Run admin tasks like backups.
- π Monitor the `/etc` directory a server for changes.
- π Implemented a watchdog that reboots the system under certain conditions.
- π Run build/test/deploy pipelines.
- π Notify someone when CPU usage is too high or disk space too low.
## Table of Contents
- [BRun](#brun)
- [Table of Contents](#table-of-contents)
- [Example Configuration](#example-configuration)
- [Install](#install)
- [Example Install on Linux:](#example-install-on-linux)
- [Auto Start with Systemd](#auto-start-with-systemd)
- [Updating](#updating)
- [Usage](#usage)
- [Circular Dependency Protection](#circular-dependency-protection)
- [Logging](#logging)
- [State](#state)
- [Secrets Management](#secrets-management)
- [File Format](#file-format)
- [Config](#config)
- [Units](#units)
- [Common Unit Fields](#common-unit-fields)
- [Boot Unit](#boot-unit)
- [Count Unit](#count-unit)
- [Cron Unit](#cron-unit)
- [Email Unit](#email-unit)
- [Email Receive Unit (TODO)](#email-receive-unit-todo)
- [File Unit](#file-unit)
- [Git Unit](#git-unit)
- [Log Unit](#log-unit)
- [Ntfy Unit](#ntfy-unit)
- [Reboot Unit](#reboot-unit)
- [Run Unit](#run-unit)
- [Start Unit](#start-unit)
- [Program Lifecycle](#program-lifecycle)
- [Status](#status)
## π Example Configuration
Here's an example showing how various units are specified and interact (see also
more [examples](examples) and our own [dogfood](build.yaml)):
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
# Start trigger - fires every time brun runs
- start:
name: on-start
on_success:
- build
# Boot trigger - fires once per boot cycle
- boot:
name: on-boot
on_success:
- build
- test
always:
- log-boot
# Run unit - executes shell commands/scripts
- run:
name: build
directory: /home/user/project
script: |
echo "Building project..."
go build -o brun ./cmd/brun
echo "Build complete"
on_success:
- test
on_failure:
- log-build-error
# Run unit - run tests
- run:
name: test
script: |
echo "Running tests..."
go test -v
on_success:
- log-success
on_failure:
- log-test-error
# Log unit - write to log files
- log:
name: log-boot
file: /var/log/brun/boot.log
- log:
name: log-success
file: /var/log/brun/success.log
- log:
name: log-build-error
file: /var/log/brun/build-errors.log
- log:
name: log-test-error
file: /var/log/brun/test-errors.log
# Count unit - track how many times units trigger
- count:
name: build-counter
# Cron trigger - runs every 5 minutes (useful in daemon mode)
- cron:
name: periodic-check
schedule: "*/5 * * * *"
on_success:
- test
# File trigger - monitors source files for changes (daemon mode)
- file:
name: watch-files
pattern: "**/*.go"
on_success:
- build
- test
# Git trigger - monitors repository for changes
- git:
name: watch-repo
repository: /home/user/project
branch: main
poll: 2m
on_success:
- build
# Email unit - send notifications
- email:
name: email-failure
to:
- admin@example.com
from: brun@example.com
subject: "Build/Test Failure"
smtp_host: smtp.gmail.com
smtp_port: 587
smtp_user: brun@example.com
smtp_password: your-app-password
smtp_use_tls: true
include_output: true
# Reboot unit - reboot the system (for reboot cycle testing)
- reboot:
name: reboot-system
delay: 5
```
## πΏ Install
To install, download the
[latest release](https://github.com/cbrake/brun/releases) binary.
### π§ Example Install on Linux:
Copy and paste the following into your terminal:
```
export VER=0.0.9
export ARCH=$(uname -m)
# Convert aarch64 to arm64 to match release binary names
[ "$ARCH" = "aarch64" ] && ARCH="arm64"
export BINARY=brun-v${VER}-Linux-${ARCH}
wget https://github.com/cbrake/brun/releases/download/v${VER}/${BINARY}
# Install to ~/.local/bin for user, /usr/local/bin for root
if [ "$(id -u)" -eq 0 ]; then
mkdir -p /usr/local/bin
install -m 755 ${BINARY} /usr/local/bin/brun
else
mkdir -p ~/.local/bin
install -m 755 ${BINARY} ~/.local/bin/brun
fi
rm ${BINARY}
```
### π€ Auto Start with Systemd
If you would like to install a systemd unit to run BRun automatically, then run:
`brun install` (run BRun once then exit)
or
`brun install -daemon` (run in daemon mode)
If `brun install` is run as root, it installs a systemd service that runs as
root, otherwise as the user who runs the install.
If a config file does not exist, one is created.
**SSH Authentication for Git Units:**
If you're using Git units with SSH repositories, the generated user service file
automatically includes SSH agent support. The service file includes:
```ini
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
```
The `%t` specifier expands to your user runtime directory (typically
`/run/user/$UID`). This setting is harmless if you don't use SSH - Git will
simply use other authentication methods (HTTPS, deploy keys, etc.).
If your SSH agent uses a different socket path, edit
`~/.config/systemd/user/brun.service` and reload:
```bash
systemctl --user daemon-reload
systemctl --user restart brun.service
```
**Alternative SSH Authentication Methods:**
- **Deploy Keys**: Use repository-specific SSH keys that don't require an agent
- **HTTPS with Credentials**: Use HTTPS URLs with credential storage instead of
SSH
- **System Services**: For root services, configure a dedicated service user
with its own SSH key
### β¬οΈ Updating
After initial installation, the `brun update` command can be used to update to
the latest release.
## π― Usage
```
Usage: brun COMMAND [OPTIONS]
Commands:
run Run brun with the given config file
install Install brun as a systemd service
update Updates BRun to the latest version
version Display version information
Run Options:
-daemon Run in daemon mode (continuous monitoring)
-unit Run a single unit (triggers disabled, useful for debugging)
-trigger Trigger a unit and execute its on_success triggers
Install Options:
-daemon Install service in daemon mode (continuous monitoring)
Examples:
brun run config.yaml
brun run config.yaml -daemon
brun run config.yaml -unit my-build
brun install
brun install -daemon
```
**π¬ One-time run:**
By default, BRun runs once, checks all trigger conditions, executes any units
whose conditions are met, and then exits. This is suitable for:
- Running from external cron
- Manual invocation
- Testing configurations
```bash
brun run config.yaml
```
**βΎοΈ Daemon mode:**
BRun supports a daemon mode that continuously monitors trigger conditions and
executes units when triggered. In this mode, triggers are checked every 10
seconds. This is suitable for:
- System service deployment
- Continuous monitoring with cron triggers
- Long-running background processes
```bash
brun run config.yaml -daemon
```
## π Circular Dependency Protection
BRun protects against circular dependencies when units trigger each other. For
example, if Unit A triggers Unit B, and Unit B triggers Unit A, this could cause
an infinite loop.
**How it works:**
- The orchestrator tracks the current execution path (call stack) as units
trigger each other
- Before executing a unit, the orchestrator checks if it's already in the
current call stack
- If a unit is already in the call stack, it is skipped to prevent circular
dependencies
- Units can be triggered multiple times in the same execution as long as they're
not in a circular loop
This approach allows:
- **Flexible trigger chains**: The same unit (like an email or log unit) can be
triggered multiple times from different branches in a single execution
- **Circular dependency protection**: Units cannot trigger themselves directly
or indirectly through other units in the same execution path
**Example - Circular dependency prevented:**
```yaml
units:
- start:
name: start-trigger
on_success:
- task-a
- run:
name: task-a
script: echo "Task A"
on_success:
- task-b
- run:
name: task-b
script: echo "Task B"
on_success:
- task-a # This would create a circular dependency
```
In this example:
- `start-trigger` triggers `task-a`
- `task-a` triggers `task-b`
- `task-b` attempts to trigger `task-a`, but it's already in the call stack
- The circular trigger is prevented, and the log shows:
`"Unit 'task-a' already in call stack, skipping to prevent circular dependency"`
**Example - Multiple triggers allowed:**
```yaml
units:
- start:
name: start-trigger
on_success:
- build-frontend
- build-backend
- run:
name: build-frontend
script: npm run build
always:
- notify-team
- run:
name: build-backend
script: go build ./...
always:
- notify-team
- email:
name: notify-team
to:
- team@example.com
# ... email config ...
```
In this example:
- Both `build-frontend` and `build-backend` trigger `notify-team`
- The `notify-team` email unit runs twice (once from each build)
- This is allowed because `notify-team` is not in a circular dependency
## π Logging
By default, logging is sent to `STDOUT`, and each unit logs:
- when it triggers or runs
- any errors
Additional log units can log specific events.
## πΎ State
BRun uses a single common state file (YAML format) where all units store state
between runs. This unified approach simplifies state management and makes it
easy to:
- Track all unit state in one location
- Back up and restore state atomically
- Clear all state with a single file deletion
- Inspect and debug state using standard YAML tools
The state file location must be set in the BRun config file.
**State Data:**
Units store different types of state information in the YAML file:
- **Boot trigger**: Last boot time (RFC3339 timestamp) and boot count
- **Cron trigger**: Last execution time (RFC3339 timestamp)
- **Count unit**: Trigger counts per triggering unit
- **File trigger**: File hashes for change detection
- **Git trigger**: Last processed commit hash
**State File Format:**
The state file uses YAML format for consistency with the configuration file.
Each unit stores its state under a key corresponding to its name or type.
The state file is automatically created with appropriate permissions (0644) when
BRun runs for the first time.
## π Secrets Management
BRun supports encrypting configuration files with
[SOPS (Secrets OPerationS)](https://github.com/getsops/sops), allowing you to
safely store passwords, API keys, and other sensitive data directly in your
config files.
**Benefits:**
- Keep secrets encrypted at rest in version control
- Transparent decryption at runtime - no UI changes needed
- Support for multiple key providers (age, PGP, AWS KMS, GCP KMS, Azure Key
Vault)
- Backward compatible - plaintext configs still work
**Quick Start:**
1. π₯ Install [SOPS](https://github.com/getsops/sops/releases) and
[age](https://github.com/FiloSottile/age/releases)
2. π Generate an encryption key:
```bash
age-keygen -o ~/.config/sops/age/keys.txt
# Save the public key (age1...) shown in output
```
3. π Encrypt your config file:
```bash
sops --encrypt --age --in-place config.yaml
```
4. βΆοΈ Run BRun normally:
```bash
brun run config.yaml # Automatically decrypts
```
Your secrets are now encrypted in the config file but decrypted transparently
when BRun runs. The file structure remains visible (unit names, triggers, etc.),
only sensitive values are encrypted.
**Selective Field Encryption:**
You can configure SOPS to encrypt only sensitive fields (like passwords and API
keys) while keeping the rest of your config readable. Create a `.sops.yaml` file
in your repository root:
```yaml
creation_rules:
- path_regex: \.yaml$
encrypted_regex: "^(.*password.*|.*secret.*|.*key.*|.*token.*|smtp_user)$"
age: your-public-key-here
```
This will encrypt only fields matching the patterns (password, secret, key,
token, etc.) while leaving structural fields like `name`, `script`, and
`directory` in plaintext for easy review in version control.
See [`.sops.yaml`](.sops.yaml) for a complete example configuration.
## π File Format
YAML is used for the BRun config file and is similar to config files used in
GitLab CI/CD, Drone, Ansible, etc.
The configuration is composed of chainable units. Each unit can trigger
additional units. This allows us to start/sequence operations and create
workflow pipelines.
### βοΈ Config
The BRun file consists of a required `config` section with the following fields:
```yaml
config:
state_location: /var/lib/brun/state.yaml
```
**Fields:**
- **`state_location`** (required): Path to the state file where units store
their state between runs.
- Defaults to `/var/lib/brun/state.yaml` for root installs
- Defaults to `~/.config/brun/state.yaml` for user installs
The config file also contains a `units` section as described below.
**Variables**
_(NOTE: Variables are in planning phase and have not been implemented yet.)_
Variables can be defined in a `vars` block and referenced in any string field
using [Go Templates](https://pkg.go.dev/text/template). Variables are expanded
when a unit is run so that variable updates are processed.
**Syntax:**
Variables are accessed using Go template syntax with double curly braces:
```yaml
vars:
project_name: myapp
build_dir: /home/user/builds
version: 1.0.0
units:
- run:
name: build
directory: { { .build_dir } }
script: |
echo "Building {{ .project_name }} version {{ .version }}"
go build -o {{ .project_name }}
```
**Features:**
- **Basic variables**: Access with `{{ .variable_name }}`
- **Nested variables**: Use dot notation `{{ .config.path }}`
- **Pipelines**: Chain operations `{{ .name | upper | quote }}`
- **Conditionals**:
`{{ if eq .env "prod" }}production{{ else }}development{{ end }}`
- **Loops**: `{{ range .items }}{{ . }}{{ end }}`
- **Functions**: Built-in functions like `eq`, `ne`, `lt`, `gt`, `and`, `or`,
`not`
**Example with nested variables:**
Variables can be nested using maps to organize related configuration:
```yaml
vars:
database:
host: localhost
port: 5432
name: myapp_db
server:
host: 0.0.0.0
port: 8080
units:
- run:
name: start-server
script: |
echo "Starting server on {{ .server.host }}:{{ .server.port }}"
echo "Connecting to database {{ .database.name }} at {{ .database.host }}:{{ .database.port }}"
./start-server
```
**Example with conditionals:**
```yaml
vars:
environment: production
enable_tests: true
units:
- run:
name: deploy
script: |
echo "Deploying to {{ .environment }}"
{{ if eq .environment "production" }}
./deploy-prod.sh
{{ else }}
./deploy-dev.sh
{{ end }}
- run:
name: test
{{ if .enable_tests }}
script: go test -v ./...
{{ else }}
script: echo "Tests disabled"
{{ end }}
```
**Environment variables:**
Environment variables can be accessed using the `env` function (if available):
```yaml
units:
- run:
name: build
script: |
echo "User: {{ env "USER" }}"
echo "Home: {{ env "HOME" }}"
```
**Whitespace control:**
Use `-` to trim whitespace before or after template actions:
```yaml
script: |
{{- if .debug }}
echo "Debug mode enabled"
{{- end }}
```
See the [Go template documentation](https://pkg.go.dev/text/template) for full
syntax reference.
## π§© Units
BRun supports the following unit types:
- π₯Ύ [Boot Unit](#boot-unit) - Triggers once per boot cycle
- π’ [Count Unit](#count-unit) - Tracks trigger counts
- β° [Cron Unit](#cron-unit) - Triggers based on cron schedule
- βοΈ [Email Unit](#email-unit) - Sends email notifications
- π [File Unit](#file-unit) - Monitors files for changes
- π [Git Unit](#git-unit) - Monitors Git repository for commits
- π [Log Unit](#log-unit) - Writes log entries to files
- π [Ntfy Unit](#ntfy-unit) - Sends push notifications
- π [Reboot Unit](#reboot-unit) - Reboots the system
- βΆοΈ [Run Unit](#run-unit) - Executes shell commands/scripts
- β [Start Unit](#start-unit) - Triggers on every program start
### Common Unit Fields
All units share the following common fields:
- **`name`** (required): A unique identifier for the unit. This name is used to
reference the unit when triggering it from other units.
- **`on_success`** (optional): An array of unit names to trigger when this unit
completes successfully.
- **`on_failure`** (optional): An array of unit names to trigger when this unit
fails.
- **`always`** (optional): An array of unit names to trigger regardless of
whether this unit succeeds or fails. These units run after success/failure
triggers.
**Trigger unit behavior:**
When a trigger unit (boot, cron, file, git, start) is triggered by another unit
via `on_success`, `on_failure`, or `always`, the trigger unit's condition is
still checked before execution. For example, if a cron unit triggers a git unit,
the git unit will only execute if there are actual git updates detected. This
prevents unnecessary operations and ensures triggers only fire when their
conditions are truly met.
### π₯Ύ Boot Unit
The boot unit triggers if this is the first time the program has been run since
the system booted. The boot unit stores the last boot time in the common state
file.
**Behavior:**
The boot trigger detects boot events by:
1. Reading `/proc/uptime` to calculate the system boot time
2. Comparing this with a stored boot time from the previous run (saved in the
common state file)
3. Triggering when the boot times differ by more than 10 seconds
**Configuration example:**
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
- boot:
name: boot-trigger
on_success:
- build-unit
- test-unit
```
When the boot trigger fires successfully, it will trigger the units listed in
`on_success` (in this example, `build-unit` and `test-unit`).
The boot time is automatically stored in the common state file under the unit's
name.
### π’ Count Unit
The Count unit creates an entry in the state file for every unit that triggers
this unit and counts how many times it has been triggered. This is useful for
tracking how often specific events (like errors) occur or how many times
particular units execute. The count quickly tells you something happened, and
then the logfiles can be examined to understand why.
**Behavior:**
- Tracks separate counts for each unit that triggers it
- Stores counts in the state file under the count unit's name
- Each triggering unit has its own counter
- Counts persist across runs
**State File Format:**
The count unit stores data in the state file like this:
```yaml
count-runs:
start-trigger: 5
count-builds:
build: 3
count-failures:
build: 1
```
**Configuration example:**
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
- start:
name: start-trigger
on_success:
- build
always:
- count-runs
- run:
name: build
script: |
go build -o brun ./cmd/brun
on_success:
- count-builds
on_failure:
- count-failures
- count:
name: count-runs
- count:
name: count-builds
- count:
name: count-failures
```
### β° Cron Unit
The Cron unit is a trigger that fires based on a cron schedule. It uses the
standard cron format to define when the trigger should activate. In daemon mode,
the trigger is checked every 10 seconds. The
[robfig/cron](https://pkg.go.dev/github.com/robfig/cron/v3) package is used for
schedule parsing.
**Fields:**
- **`schedule`** (required): Cron schedule in standard format (minute hour day
month weekday)
**Behavior:**
- Triggers based on the cron schedule
- Stores last execution time in the state file
- Works in both one-time and daemon modes
- In one-time mode: triggers if schedule indicates it should have run since last
execution
- In daemon mode: continuously monitors and triggers at scheduled times
**Cron Schedule Format:**
Standard 5-field cron format:
```
* * * * *
β β β β β
β β β β ββββ Day of week (0-6, Sunday=0)
β β β ββββββ Month (1-12)
β β ββββββββ Day of month (1-31)
β ββββββββββ Hour (0-23)
ββββββββββββ Minute (0-59)
```
Examples:
- `* * * * *` - Every minute
- `*/5 * * * *` - Every 5 minutes
- `0 2 * * *` - Daily at 2:00 AM
- `30 14 * * 1-5` - Weekdays at 2:30 PM
- `0 0 1 * *` - First day of every month at midnight
**State File Format:**
The cron unit stores the last execution time:
```yaml
daily-backup:
last_execution: "2025-10-03T02:30:00-04:00"
health-check:
last_execution: "2025-10-03T18:00:00-04:00"
```
**Configuration example:**
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
# Cron trigger - runs every day at 2:30 AM
- cron:
name: daily-backup
schedule: "30 2 * * *"
on_success:
- backup-unit
# Cron trigger - runs every 5 minutes
- cron:
name: health-check
schedule: "*/5 * * * *"
on_success:
- check-services
- run:
name: backup-unit
script: |
echo "Running daily backup..."
# backup commands here
- run:
name: check-services
script: |
echo "Checking services..."
# health check commands here
```
### βοΈ Email Unit
The Email unit sends email notifications with optional output from triggering
units. This is useful for alerting on build failures, test results, or other
important events. Supports both plain SMTP and STARTTLS encryption.
**Fields:**
- **`to`** (required): Array of email addresses to send to
- **`from`** (required): Sender email address
- **`subject_prefix`** (optional): Email subject line prefix. ':
:' is appended after prefix and is always included.
- **`smtp_host`** (required): SMTP server hostname
- **`smtp_port`** (optional): SMTP server port. Defaults to 587 (submission
port)
- **`smtp_user`** (optional): SMTP username for authentication
- **`smtp_password`** (optional): SMTP password for authentication
- **`smtp_use_tls`** (optional): Enable STARTTLS encryption. Defaults to true
- **`include_output`** (optional): Include captured output from triggering unit.
Defaults to true
- **`limit_lines`** (optional): limit number email lines emailed to number
specified.
**Behavior:**
- Sends plain text emails using SMTP
- Can include output from the unit that triggered it (useful for log/error
reporting)
- Supports SMTP authentication
- STARTTLS encryption enabled by default
- Works with common email providers (Gmail, SendGrid, Mailgun, etc.)
**Configuration example:**
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
- boot:
name: boot-trigger
on_success:
- build
- run:
name: build
script: |
go build -o brun ./cmd/brun
go test -v
on_failure:
- email-failure
- email:
name: email-failure
to:
- admin@example.com
- alerts@example.com
from: brun@example.com
subject_prefix: "Build Alert"
smtp_host: smtp.gmail.com
smtp_port: 587
smtp_user: brun@example.com
smtp_password: your-app-password
smtp_use_tls: true
include_output: true
```
This will send emails with subjects like:
- `Build Alert: build:success` (on success)
- `Build Alert: build:fail` (on failure)
**Gmail example:**
For Gmail, you need to use an app-specific password:
```yaml
- email:
name: notify-admin
to:
- you@gmail.com
from: your-app@gmail.com
subject_prefix: "CI/CD"
smtp_host: smtp.gmail.com
smtp_port: 587
smtp_user: your-app@gmail.com
smtp_password: your-16-char-app-password
smtp_use_tls: true
```
### π¨ Email Receive Unit (TODO)
This can receive emails to trigger units.
### π File Unit
The File unit monitors files and triggers when they are changed. Files can be
specified using glob patterns with support for `**` recursive matching. New or
removed files are detected as changes.
**Fields:**
- **`pattern`** (required): Glob pattern to match files (supports `**` for
recursive matching)
**Behavior:**
- Monitors files matching the glob pattern
- Triggers when file content changes (detected via SHA256 hash)
- Triggers when files are added or removed
- Stores file hashes in the state file
- Triggers on first run (initial file state)
- Ignores directories (only monitors regular files)
- Works in both one-time and daemon modes
**Pattern Syntax:**
The file unit supports advanced glob patterns including:
- `*` - matches any sequence of non-separator characters
- `?` - matches any single non-separator character
- `[abc]` - matches any character in the set
- `[a-z]` - matches any character in the range
- `**` - matches zero or more directories recursively
**Pattern Examples:**
- `**/*.go` - all Go files recursively
- `src/**/*.ts` - all TypeScript files under `src/`
- `config/*.yaml` - config files non-recursively
- `**/*.{html,css,js}` - multiple filetypes
**State File Format:**
The file unit stores a hash of all monitored files:
```yaml
watch-source:
files_state: "file1.go:a1b2c3...|file2.go:d4e5f6..."
```
**Configuration example:**
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
# File trigger - monitors Go source files
- file:
name: watch-source
pattern: "**/*.go"
on_success:
- build
- test
- run:
name: build
script: |
echo "Building..."
go build -o app ./cmd/app
- run:
name: test
script: |
echo "Running tests..."
go test -v ./...
```
**Daemon mode example:**
When running in daemon mode, the file trigger continuously monitors files and
automatically triggers builds/tests when changes are detected:
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
- file:
name: auto-build
pattern: "**/*.go"
on_success:
- build
- test
always:
- email-notify
- run:
name: build
script: |
go build -o app ./cmd/app
- run:
name: test
script: |
go test -v ./...
- email:
name: email-notify
to:
- team@example.com
from: ci@example.com
subject_prefix: "Build Status"
smtp_host: smtp.example.com
smtp_port: 587
smtp_user: ci@example.com
smtp_password: secret
```
Run with: `brun run config.yaml -daemon`
This creates a continuous integration system that automatically builds and tests
your code whenever source files are modified.
### π Git Unit
The Git unit is a trigger that fires when changes are detected in a Git
repository. It monitors the repository's HEAD commit and triggers when new
commits are detected. This is useful for automatically running builds, tests, or
deployments when code changes.
If the `repository` field points to a local Git workspace (vs a Repo URL), the
workspace and submodules are updated to the latest on the specified branch.
**Fields:**
- **`repository`** (required): Path to the Git repository to monitor
- **`branch`** (required): Branch to monitor
- **`reset`** (optional): optionally reset the workspace to the state of the
repo HEAD (`git reset --hard`)
- **`poll`** (optional): polling interval for checking repository updates (e.g.,
`2m`, `30s`, `1h`). When set, the git unit actively polls for updates at the
specified interval. When omitted, the unit operates in passive mode: it will
NOT check during orchestrator polling, but WILL check when explicitly
triggered by another unit (e.g., via `on_success`). This enables event-driven
workflows where git checks happen on-demand without continuous polling
overhead.
- **`debug`** (optional): when true, logs detailed git operation messages
(fetch, reset, submodule updates). Defaults to false.
**SSH Authentication:**
When using SSH-based Git repositories with systemd, the service requires access
to your SSH agent. See the [Auto tart with systemd](#autostart-with-systemd)
section for configuration details on setting the `SSH_AUTH_SOCK` environment
variable.
**Behavior:**
- Monitors the HEAD commit hash of the specified Git repository
- Triggers when the commit hash changes (new commits detected)
- Stores the last seen commit hash in the state file
- Triggers on first run (initial repository state)
- Uses go-git library (no git CLI tool required)
- Works in both one-time and daemon modes
**State File Format:**
The git unit stores the last seen commit hash:
```yaml
watch-repo:
last_commit_hash: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
```
**Configuration example:**
When running in daemon mode, the git trigger continuously monitors the
repository and automatically triggers builds/tests when new commits are
detected:
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
- git:
name: auto-build
repository: /home/user/project
branch: main
poll: 2m # Check for updates every 2 minutes
debug: false # Suppress verbose git operation logs
on_success:
- build
- run:
name: build
directory: /home/user/project
script: |
go build -o app ./cmd/app
go test -v ./...
always:
- email
- email:
name: email
to:
- team@example.com
from: ci@example.com
subject_prefix: "Build Success"
smtp_host: smtp.example.com
smtp_port: 587
smtp_user: ci@example.com
smtp_password: secret
```
This creates a continuous integration system that automatically builds and tests
your code whenever changes are pushed to the repository.
**Passive mode example (event-driven):**
For efficient resource usage, you can configure git units without polling and
trigger them explicitly:
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
# Check for git updates once per hour
- cron:
name: hourly-check
schedule: "0 * * * *"
on_success:
- check-repo
# Git unit in passive mode (no poll field)
- git:
name: check-repo
repository: /home/user/project
branch: main
# No poll field - only checks when triggered by cron
on_success:
- build
- run:
name: build
directory: /home/user/project
script: |
go build -o app ./cmd/app
go test -v ./...
```
This approach checks for git updates only when the cron triggers it, reducing
system overhead while maintaining automated builds.
### π Log Unit
The Log unit writes log entries to a file. This is useful for recording events,
errors, or other information during pipeline execution. The logfile is created
if it doesn't exist, and entries are appended with timestamps.
**Fields:**
- **`file`** (required): Path to the logfile where entries will be written
**Behavior:**
- Creates the logfile and parent directories if they don't exist
- Appends log entries with timestamps
- File permissions are set to 0644
- Directory permissions are set to 0755
**Configuration example:**
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
- start:
name: start-trigger
on_success:
- build
always:
- log-run
- run:
name: build
script: |
go build -o brun ./cmd/brun
on_failure:
- log-error
- log:
name: log-run
file: /var/log/brun/pipeline.log
- log:
name: log-error
file: /var/log/brun/errors.log
```
### π Ntfy Unit
The ntfy unit allows notifications be sent out using the
[ntfy.sh](https://ntfy.sh/) service.
**Fields:**
- **`topic`** (required): Ntfy topic to post to
- **`server`** (optional): Ntfy server URL. Defaults to `https://ntfy.sh`
- **`title_prefix`** (optional): Notification title prefix. ':
:' is appended after prefix and is always included
- **`priority`** (optional): Notification priority (min, low, default, high,
urgent)
- **`tags`** (optional): Comma-separated tags/emojis for the notification
- **`include_output`** (optional): Include captured output from triggering unit.
Defaults to true
- **`limit_lines`** (optional): Limit number of output lines included in
notification. 20 lines is a good number. More than that, the Android app seems
to turn the log into an attachment.
**Behavior:**
- Sends notifications via HTTP POST to ntfy.sh (or self-hosted server)
- Can include output from the unit that triggered it (useful for log/error
reporting)
- Title automatically includes triggering unit name and success/fail status
**Configuration example:**
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
- boot:
name: boot-trigger
on_success:
- build
- run:
name: build
script: |
go build -o brun ./cmd/brun
go test -v
on_failure:
- notify-failure
on_success:
- notify-success
- ntfy:
name: notify-failure
topic: my-build-alerts
title_prefix: Build Failed
priority: high
tags: warning,skull
include_output: true
limit_lines: 50
- ntfy:
name: notify-success
topic: my-build-alerts
title_prefix: Build Succeeded
priority: default
tags: white_check_mark
include_output: false
```
### π Reboot Unit
The reboot unit logs and reboots the system. This is typically used in reboot
cycle testing where the boot trigger can count boot cycles and trigger test
sequences.
**Fields:**
- **`delay`** (optional): Number of seconds to wait before executing reboot
(default: 0 for immediate reboot)
**Configuration example:**
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
- reboot:
name: reboot-system
delay: 5 # optional delay in seconds before reboot (default: 0)
```
### βΆοΈ Run Unit
The Run unit executes arbitrary shell commands or scripts. This is the primary
execution unit for running builds, tests, or any other commands. The exit code
determines success or failure, which then triggers the appropriate units.
Multiple Run units can be defined in a configuration file to create build and
test pipelines.
**Fields:**
- **`script`** (required): Shell commands to execute. Can be a single command or
a multiline script
- **`directory`** (optional): Working directory where the script will be
executed. Defaults to the directory where BRun was invoked
- **`timeout`** (optional): Time out duration for the task to complete (e.g.,
`30s`, `5m`, `1h`, `1h30m`). If no timeout is specified, it runs until
completion. If the task times out, an error message is logged.
- **`shell`** (optional): specify shell to use when running command (bash,
etc.). By default, 'sh' is used.
- **`use_pty`** (optional): when set to true, wraps the command with `script` to
provide a pseudo-TTY. This is useful for tools like BitBake that require a TTY
environment. Default is false.
**Behavior:**
- The script is executed using the system shell
- Exit code 0 is considered success and triggers `on_success` units
- Nonzero exit codes are considered failures and trigger `on_failure` units
- Both `STDOUT` and `STDERR` are logged
**Configuration example:**
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
- boot:
name: boot-trigger
on_success:
- build
- run:
name: build
directory: /home/user/project
script: |
go build -o brun ./cmd/brun
go test -v
on_success:
- deploy
on_failure:
- notify-failure
- run:
name: deploy
script: |
./deploy.sh
- run:
name: bitbake-build
shell: bash
use_pty: true
script: |
source oe-init-build-env
bitbake core-image-minimal
timeout: 2h
```
### β Start Unit
The Start trigger always fires when BRun runs. This can be used to trigger other
units every time the program executes, regardless of boot state or other
conditions.
**Behavior:**
- Always triggers on every BRun
- Does not maintain any state
- Useful for unconditional execution pipelines
**Configuration example:**
```yaml
config:
state_location: /var/lib/brun/state.yaml
units:
- start:
name: start-trigger
on_success:
- build-unit
- test-unit
```
## π Program Lifecycle
BRun traps kill signals and waits for all triggers to complete before exiting.
## π¦ Status
This project is in the exploratory phase as we explore various concepts. The
syntax of the BRun file may change as we learn how to better do this.
If you are using BRun, please like this repo and subscribe to release updates.
If there are features you would like, open an issue. This provides motivation to
keep the project going.
Feedback/contributions welcome! Please
[discuss](https://github.com/cbrake/brun/discussions) before implementing
anything major.
See [issues](https://github.com/cbrake/brun/issues) and [ideas](ideas.md) for
future direction.
I have no idea if this works on Windows - feel free to try and report status.
If you use BRun, please star/follow the repo and
[let me know](https://github.com/cbrake/brun/discussions).