{"id":32384986,"url":"https://github.com/cbrake/brun","last_synced_at":"2026-05-17T06:32:32.316Z","repository":{"id":317780498,"uuid":"1068810657","full_name":"cbrake/brun","owner":"cbrake","description":"The simple way to run native workflows. No containers required.","archived":false,"fork":false,"pushed_at":"2026-04-20T16:36:03.000Z","size":11438,"stargazers_count":1,"open_issues_count":16,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-12T13:45:29.412Z","etag":null,"topics":["automation","ci-cd","embedded","linux","testing","workflow"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cbrake.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"cbrake","patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null,"custom":"https://bec-systems.com/product/oss-sponsor/"}},"created_at":"2025-10-03T00:15:46.000Z","updated_at":"2026-04-20T16:36:38.000Z","dependencies_parsed_at":"2026-01-15T19:02:54.986Z","dependency_job_id":null,"html_url":"https://github.com/cbrake/brun","commit_stats":null,"previous_names":["cbrake/brun"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/cbrake/brun","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cbrake%2Fbrun","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cbrake%2Fbrun/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cbrake%2Fbrun/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cbrake%2Fbrun/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cbrake","download_url":"https://codeload.github.com/cbrake/brun/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cbrake%2Fbrun/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33129247,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-17T06:27:06.342Z","status":"ssl_error","status_checked_at":"2026-05-17T06:26:59.432Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","ci-cd","embedded","linux","testing","workflow"],"created_at":"2025-10-25T02:31:29.470Z","updated_at":"2026-05-17T06:32:32.306Z","avatar_url":"https://github.com/cbrake.png","language":"Go","funding_links":["https://github.com/sponsors/cbrake","https://bec-systems.com/product/oss-sponsor/"],"categories":[],"sub_categories":[],"readme":"# BRun\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/brun-logo.png\" alt=\"BRun Logo\" width=\"400\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n\u003cem\u003eTrigger → Run\u003c/em\u003e\n\u003c/p\u003e\n\nDo you find tools like GitHub Actions or Ansible useful, but would like a simple\nway to do similar things natively? BRun is a native Linux automation tool that\nconnects triggers (boot, cron, file changes, git commits) to actions (run\nscripts, send emails, log events, reboot). Build CI/CD pipelines, automate\nsystem tasks, or test embedded devices—all with a single binary and no\ndependencies.\n\n**Features/goals:**\n\n- ✨ **simple!!!**\n- ⚡ **fast!!!**\n- 📦 no dependencies - [install](#example-install-on-linux) a single statically\n  linked binary and go for it ...\n- 🛠️ built-in [units](#units) for common tasks like boot, scripts, cron, email,\n  git, file watching\n- 🔗 units can be chained into pipelines\n- 💻 first priority is to run native\n- 🚫 does not require containers (but may support them in the future)\n- 📄 simple YAML [config format](#file-format)\n- 🔒 built-in [secrets management](#secrets-management) with SOPS encryption\n\n**Things you might do with BRun:**\n\n- 🔄 Reboot cycle test for embedded systems.\n- 🌙 Nightly Yocto builds on your powerful workstation.\n- 🗄️ Run admin tasks like backups.\n- 👀 Monitor the `/etc` directory a server for changes.\n- 🐕 Implemented a watchdog that reboots the system under certain conditions.\n- 🚀 Run build/test/deploy pipelines.\n- 📊 Notify someone when CPU usage is too high or disk space too low.\n\n## Table of Contents\n\n\u003c!--toc:start--\u003e\n\n- [BRun](#brun)\n  - [Table of Contents](#table-of-contents)\n  - [Example Configuration](#example-configuration)\n  - [Install](#install)\n    - [Example Install on Linux:](#example-install-on-linux)\n    - [Auto Start with Systemd](#auto-start-with-systemd)\n    - [Updating](#updating)\n  - [Usage](#usage)\n  - [Circular Dependency Protection](#circular-dependency-protection)\n  - [Logging](#logging)\n  - [State](#state)\n  - [Secrets Management](#secrets-management)\n  - [File Format](#file-format)\n    - [Config](#config)\n  - [Units](#units)\n    - [Common Unit Fields](#common-unit-fields)\n    - [Boot Unit](#boot-unit)\n    - [Count Unit](#count-unit)\n    - [Cron Unit](#cron-unit)\n    - [Email Unit](#email-unit)\n    - [Email Receive Unit (TODO)](#email-receive-unit-todo)\n    - [File Unit](#file-unit)\n    - [Git Unit](#git-unit)\n    - [Log Unit](#log-unit)\n    - [Ntfy Unit](#ntfy-unit)\n    - [Reboot Unit](#reboot-unit)\n    - [Run Unit](#run-unit)\n    - [Start Unit](#start-unit)\n  - [Program Lifecycle](#program-lifecycle)\n  - [Status](#status)\n  \u003c!--toc:end--\u003e\n\n## 📝 Example Configuration\n\nHere's an example showing how various units are specified and interact (see also\nmore [examples](examples) and our own [dogfood](build.yaml)):\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  # Start trigger - fires every time brun runs\n  - start:\n      name: on-start\n      on_success:\n        - build\n\n  # Boot trigger - fires once per boot cycle\n  - boot:\n      name: on-boot\n      on_success:\n        - build\n        - test\n      always:\n        - log-boot\n\n  # Run unit - executes shell commands/scripts\n  - run:\n      name: build\n      directory: /home/user/project\n      script: |\n        echo \"Building project...\"\n        go build -o brun ./cmd/brun\n        echo \"Build complete\"\n      on_success:\n        - test\n      on_failure:\n        - log-build-error\n\n  # Run unit - run tests\n  - run:\n      name: test\n      script: |\n        echo \"Running tests...\"\n        go test -v\n      on_success:\n        - log-success\n      on_failure:\n        - log-test-error\n\n  # Log unit - write to log files\n  - log:\n      name: log-boot\n      file: /var/log/brun/boot.log\n\n  - log:\n      name: log-success\n      file: /var/log/brun/success.log\n\n  - log:\n      name: log-build-error\n      file: /var/log/brun/build-errors.log\n\n  - log:\n      name: log-test-error\n      file: /var/log/brun/test-errors.log\n\n  # Count unit - track how many times units trigger\n  - count:\n      name: build-counter\n\n  # Cron trigger - runs every 5 minutes (useful in daemon mode)\n  - cron:\n      name: periodic-check\n      schedule: \"*/5 * * * *\"\n      on_success:\n        - test\n\n  # File trigger - monitors source files for changes (daemon mode)\n  - file:\n      name: watch-files\n      pattern: \"**/*.go\"\n      on_success:\n        - build\n        - test\n\n  # Git trigger - monitors repository for changes\n  - git:\n      name: watch-repo\n      repository: /home/user/project\n      branch: main\n      poll: 2m\n      on_success:\n        - build\n\n  # Email unit - send notifications\n  - email:\n      name: email-failure\n      to:\n        - admin@example.com\n      from: brun@example.com\n      subject: \"Build/Test Failure\"\n      smtp_host: smtp.gmail.com\n      smtp_port: 587\n      smtp_user: brun@example.com\n      smtp_password: your-app-password\n      smtp_use_tls: true\n      include_output: true\n\n  # Reboot unit - reboot the system (for reboot cycle testing)\n  - reboot:\n      name: reboot-system\n      delay: 5\n```\n\n## 💿 Install\n\nTo install, download the\n[latest release](https://github.com/cbrake/brun/releases) binary.\n\n### 🐧 Example Install on Linux:\n\nCopy and paste the following into your terminal:\n\n```\nexport VER=0.0.9\nexport ARCH=$(uname -m)\n# Convert aarch64 to arm64 to match release binary names\n[ \"$ARCH\" = \"aarch64\" ] \u0026\u0026 ARCH=\"arm64\"\nexport BINARY=brun-v${VER}-Linux-${ARCH}\nwget https://github.com/cbrake/brun/releases/download/v${VER}/${BINARY}\n# Install to ~/.local/bin for user, /usr/local/bin for root\nif [ \"$(id -u)\" -eq 0 ]; then\n  mkdir -p /usr/local/bin\n  install -m 755 ${BINARY} /usr/local/bin/brun\nelse\n  mkdir -p ~/.local/bin\n  install -m 755 ${BINARY} ~/.local/bin/brun\nfi\nrm ${BINARY}\n```\n\n### 🤖 Auto Start with Systemd\n\nIf you would like to install a systemd unit to run BRun automatically, then run:\n\n`brun install` (run BRun once then exit)\n\nor\n\n`brun install -daemon` (run in daemon mode)\n\nIf `brun install` is run as root, it installs a systemd service that runs as\nroot, otherwise as the user who runs the install.\n\nIf a config file does not exist, one is created.\n\n**SSH Authentication for Git Units:**\n\nIf you're using Git units with SSH repositories, the generated user service file\nautomatically includes SSH agent support. The service file includes:\n\n```ini\nEnvironment=SSH_AUTH_SOCK=%t/ssh-agent.socket\n```\n\nThe `%t` specifier expands to your user runtime directory (typically\n`/run/user/$UID`). This setting is harmless if you don't use SSH - Git will\nsimply use other authentication methods (HTTPS, deploy keys, etc.).\n\nIf your SSH agent uses a different socket path, edit\n`~/.config/systemd/user/brun.service` and reload:\n\n```bash\nsystemctl --user daemon-reload\nsystemctl --user restart brun.service\n```\n\n**Alternative SSH Authentication Methods:**\n\n- **Deploy Keys**: Use repository-specific SSH keys that don't require an agent\n- **HTTPS with Credentials**: Use HTTPS URLs with credential storage instead of\n  SSH\n- **System Services**: For root services, configure a dedicated service user\n  with its own SSH key\n\n### ⬆️ Updating\n\nAfter initial installation, the `brun update` command can be used to update to\nthe latest release.\n\n## 🎯 Usage\n\n```\nUsage: brun COMMAND [OPTIONS]\n\nCommands:\n  run \u003cconfig-file\u003e       Run brun with the given config file\n  install                 Install brun as a systemd service\n  update                  Updates BRun to the latest version\n  version                 Display version information\n\nRun Options:\n  -daemon                 Run in daemon mode (continuous monitoring)\n  -unit \u003cname\u003e            Run a single unit (triggers disabled, useful for debugging)\n  -trigger \u003cname\u003e         Trigger a unit and execute its on_success triggers\n\nInstall Options:\n  -daemon                 Install service in daemon mode (continuous monitoring)\n\nExamples:\n  brun run config.yaml\n  brun run config.yaml -daemon\n  brun run config.yaml -unit my-build\n  brun install\n  brun install -daemon\n```\n\n**🎬 One-time run:**\n\nBy default, BRun runs once, checks all trigger conditions, executes any units\nwhose conditions are met, and then exits. This is suitable for:\n\n- Running from external cron\n- Manual invocation\n- Testing configurations\n\n```bash\nbrun run config.yaml\n```\n\n**♾️ Daemon mode:**\n\nBRun supports a daemon mode that continuously monitors trigger conditions and\nexecutes units when triggered. In this mode, triggers are checked every 10\nseconds. This is suitable for:\n\n- System service deployment\n- Continuous monitoring with cron triggers\n- Long-running background processes\n\n```bash\nbrun run config.yaml -daemon\n```\n\n## 🔁 Circular Dependency Protection\n\nBRun protects against circular dependencies when units trigger each other. For\nexample, if Unit A triggers Unit B, and Unit B triggers Unit A, this could cause\nan infinite loop.\n\n**How it works:**\n\n- The orchestrator tracks the current execution path (call stack) as units\n  trigger each other\n- Before executing a unit, the orchestrator checks if it's already in the\n  current call stack\n- If a unit is already in the call stack, it is skipped to prevent circular\n  dependencies\n- Units can be triggered multiple times in the same execution as long as they're\n  not in a circular loop\n\nThis approach allows:\n\n- **Flexible trigger chains**: The same unit (like an email or log unit) can be\n  triggered multiple times from different branches in a single execution\n- **Circular dependency protection**: Units cannot trigger themselves directly\n  or indirectly through other units in the same execution path\n\n**Example - Circular dependency prevented:**\n\n```yaml\nunits:\n  - start:\n      name: start-trigger\n      on_success:\n        - task-a\n\n  - run:\n      name: task-a\n      script: echo \"Task A\"\n      on_success:\n        - task-b\n\n  - run:\n      name: task-b\n      script: echo \"Task B\"\n      on_success:\n        - task-a # This would create a circular dependency\n```\n\nIn this example:\n\n- `start-trigger` triggers `task-a`\n- `task-a` triggers `task-b`\n- `task-b` attempts to trigger `task-a`, but it's already in the call stack\n- The circular trigger is prevented, and the log shows:\n  `\"Unit 'task-a' already in call stack, skipping to prevent circular dependency\"`\n\n**Example - Multiple triggers allowed:**\n\n```yaml\nunits:\n  - start:\n      name: start-trigger\n      on_success:\n        - build-frontend\n        - build-backend\n\n  - run:\n      name: build-frontend\n      script: npm run build\n      always:\n        - notify-team\n\n  - run:\n      name: build-backend\n      script: go build ./...\n      always:\n        - notify-team\n\n  - email:\n      name: notify-team\n      to:\n        - team@example.com\n      # ... email config ...\n```\n\nIn this example:\n\n- Both `build-frontend` and `build-backend` trigger `notify-team`\n- The `notify-team` email unit runs twice (once from each build)\n- This is allowed because `notify-team` is not in a circular dependency\n\n## 📋 Logging\n\nBy default, logging is sent to `STDOUT`, and each unit logs:\n\n- when it triggers or runs\n- any errors\n\nAdditional log units can log specific events.\n\n## 💾 State\n\nBRun uses a single common state file (YAML format) where all units store state\nbetween runs. This unified approach simplifies state management and makes it\neasy to:\n\n- Track all unit state in one location\n- Back up and restore state atomically\n- Clear all state with a single file deletion\n- Inspect and debug state using standard YAML tools\n\nThe state file location must be set in the BRun config file.\n\n**State Data:**\n\nUnits store different types of state information in the YAML file:\n\n- **Boot trigger**: Last boot time (RFC3339 timestamp) and boot count\n- **Cron trigger**: Last execution time (RFC3339 timestamp)\n- **Count unit**: Trigger counts per triggering unit\n- **File trigger**: File hashes for change detection\n- **Git trigger**: Last processed commit hash\n\n**State File Format:**\n\nThe state file uses YAML format for consistency with the configuration file.\nEach unit stores its state under a key corresponding to its name or type.\n\nThe state file is automatically created with appropriate permissions (0644) when\nBRun runs for the first time.\n\n## 🔐 Secrets Management\n\nBRun supports encrypting configuration files with\n[SOPS (Secrets OPerationS)](https://github.com/getsops/sops), allowing you to\nsafely store passwords, API keys, and other sensitive data directly in your\nconfig files.\n\n**Benefits:**\n\n- Keep secrets encrypted at rest in version control\n- Transparent decryption at runtime - no UI changes needed\n- Support for multiple key providers (age, PGP, AWS KMS, GCP KMS, Azure Key\n  Vault)\n- Backward compatible - plaintext configs still work\n\n**Quick Start:**\n\n1. 📥 Install [SOPS](https://github.com/getsops/sops/releases) and\n   [age](https://github.com/FiloSottile/age/releases)\n\n2. 🔑 Generate an encryption key:\n\n```bash\nage-keygen -o ~/.config/sops/age/keys.txt\n# Save the public key (age1...) shown in output\n```\n\n3. 🔐 Encrypt your config file:\n\n```bash\nsops --encrypt --age \u003cyour-public-key\u003e --in-place config.yaml\n```\n\n4. ▶️ Run BRun normally:\n\n```bash\nbrun run config.yaml  # Automatically decrypts\n```\n\nYour secrets are now encrypted in the config file but decrypted transparently\nwhen BRun runs. The file structure remains visible (unit names, triggers, etc.),\nonly sensitive values are encrypted.\n\n**Selective Field Encryption:**\n\nYou can configure SOPS to encrypt only sensitive fields (like passwords and API\nkeys) while keeping the rest of your config readable. Create a `.sops.yaml` file\nin your repository root:\n\n```yaml\ncreation_rules:\n  - path_regex: \\.yaml$\n    encrypted_regex: \"^(.*password.*|.*secret.*|.*key.*|.*token.*|smtp_user)$\"\n    age: your-public-key-here\n```\n\nThis will encrypt only fields matching the patterns (password, secret, key,\ntoken, etc.) while leaving structural fields like `name`, `script`, and\n`directory` in plaintext for easy review in version control.\n\nSee [`.sops.yaml`](.sops.yaml) for a complete example configuration.\n\n## 📑 File Format\n\nYAML is used for the BRun config file and is similar to config files used in\nGitLab CI/CD, Drone, Ansible, etc.\n\nThe configuration is composed of chainable units. Each unit can trigger\nadditional units. This allows us to start/sequence operations and create\nworkflow pipelines.\n\n### ⚙️ Config\n\nThe BRun file consists of a required `config` section with the following fields:\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n```\n\n**Fields:**\n\n- **`state_location`** (required): Path to the state file where units store\n  their state between runs.\n  - Defaults to `/var/lib/brun/state.yaml` for root installs\n  - Defaults to `~/.config/brun/state.yaml` for user installs\n\nThe config file also contains a `units` section as described below.\n\n**Variables**\n\n_(NOTE: Variables are in planning phase and have not been implemented yet.)_\n\nVariables can be defined in a `vars` block and referenced in any string field\nusing [Go Templates](https://pkg.go.dev/text/template). Variables are expanded\nwhen a unit is run so that variable updates are processed.\n\n**Syntax:**\n\nVariables are accessed using Go template syntax with double curly braces:\n\n```yaml\nvars:\n  project_name: myapp\n  build_dir: /home/user/builds\n  version: 1.0.0\n\nunits:\n  - run:\n      name: build\n      directory: { { .build_dir } }\n      script: |\n        echo \"Building {{ .project_name }} version {{ .version }}\"\n        go build -o {{ .project_name }}\n```\n\n**Features:**\n\n- **Basic variables**: Access with `{{ .variable_name }}`\n- **Nested variables**: Use dot notation `{{ .config.path }}`\n- **Pipelines**: Chain operations `{{ .name | upper | quote }}`\n- **Conditionals**:\n  `{{ if eq .env \"prod\" }}production{{ else }}development{{ end }}`\n- **Loops**: `{{ range .items }}{{ . }}{{ end }}`\n- **Functions**: Built-in functions like `eq`, `ne`, `lt`, `gt`, `and`, `or`,\n  `not`\n\n**Example with nested variables:**\n\nVariables can be nested using maps to organize related configuration:\n\n```yaml\nvars:\n  database:\n    host: localhost\n    port: 5432\n    name: myapp_db\n  server:\n    host: 0.0.0.0\n    port: 8080\n\nunits:\n  - run:\n      name: start-server\n      script: |\n        echo \"Starting server on {{ .server.host }}:{{ .server.port }}\"\n        echo \"Connecting to database {{ .database.name }} at {{ .database.host }}:{{ .database.port }}\"\n        ./start-server\n```\n\n**Example with conditionals:**\n\n```yaml\nvars:\n  environment: production\n  enable_tests: true\n\nunits:\n  - run:\n      name: deploy\n      script: |\n        echo \"Deploying to {{ .environment }}\"\n        {{ if eq .environment \"production\" }}\n        ./deploy-prod.sh\n        {{ else }}\n        ./deploy-dev.sh\n        {{ end }}\n\n  - run:\n      name: test\n      {{ if .enable_tests }}\n      script: go test -v ./...\n      {{ else }}\n      script: echo \"Tests disabled\"\n      {{ end }}\n```\n\n**Environment variables:**\n\nEnvironment variables can be accessed using the `env` function (if available):\n\n```yaml\nunits:\n  - run:\n      name: build\n      script: |\n        echo \"User: {{ env \"USER\" }}\"\n        echo \"Home: {{ env \"HOME\" }}\"\n```\n\n**Whitespace control:**\n\nUse `-` to trim whitespace before or after template actions:\n\n```yaml\nscript: |\n  {{- if .debug }}\n  echo \"Debug mode enabled\"\n  {{- end }}\n```\n\nSee the [Go template documentation](https://pkg.go.dev/text/template) for full\nsyntax reference.\n\n## 🧩 Units\n\nBRun supports the following unit types:\n\n- 🥾 [Boot Unit](#boot-unit) - Triggers once per boot cycle\n- 🔢 [Count Unit](#count-unit) - Tracks trigger counts\n- ⏰ [Cron Unit](#cron-unit) - Triggers based on cron schedule\n- ✉️ [Email Unit](#email-unit) - Sends email notifications\n- 📁 [File Unit](#file-unit) - Monitors files for changes\n- 🔀 [Git Unit](#git-unit) - Monitors Git repository for commits\n- 📝 [Log Unit](#log-unit) - Writes log entries to files\n- 🔔 [Ntfy Unit](#ntfy-unit) - Sends push notifications\n- 🔄 [Reboot Unit](#reboot-unit) - Reboots the system\n- ▶️ [Run Unit](#run-unit) - Executes shell commands/scripts\n- ⭐ [Start Unit](#start-unit) - Triggers on every program start\n\n### Common Unit Fields\n\nAll units share the following common fields:\n\n- **`name`** (required): A unique identifier for the unit. This name is used to\n  reference the unit when triggering it from other units.\n- **`on_success`** (optional): An array of unit names to trigger when this unit\n  completes successfully.\n- **`on_failure`** (optional): An array of unit names to trigger when this unit\n  fails.\n- **`always`** (optional): An array of unit names to trigger regardless of\n  whether this unit succeeds or fails. These units run after success/failure\n  triggers.\n\n**Trigger unit behavior:**\n\nWhen a trigger unit (boot, cron, file, git, start) is triggered by another unit\nvia `on_success`, `on_failure`, or `always`, the trigger unit's condition is\nstill checked before execution. For example, if a cron unit triggers a git unit,\nthe git unit will only execute if there are actual git updates detected. This\nprevents unnecessary operations and ensures triggers only fire when their\nconditions are truly met.\n\n### 🥾 Boot Unit\n\nThe boot unit triggers if this is the first time the program has been run since\nthe system booted. The boot unit stores the last boot time in the common state\nfile.\n\n**Behavior:**\n\nThe boot trigger detects boot events by:\n\n1. Reading `/proc/uptime` to calculate the system boot time\n2. Comparing this with a stored boot time from the previous run (saved in the\n   common state file)\n3. Triggering when the boot times differ by more than 10 seconds\n\n**Configuration example:**\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  - boot:\n      name: boot-trigger\n      on_success:\n        - build-unit\n        - test-unit\n```\n\nWhen the boot trigger fires successfully, it will trigger the units listed in\n`on_success` (in this example, `build-unit` and `test-unit`).\n\nThe boot time is automatically stored in the common state file under the unit's\nname.\n\n### 🔢 Count Unit\n\nThe Count unit creates an entry in the state file for every unit that triggers\nthis unit and counts how many times it has been triggered. This is useful for\ntracking how often specific events (like errors) occur or how many times\nparticular units execute. The count quickly tells you something happened, and\nthen the logfiles can be examined to understand why.\n\n**Behavior:**\n\n- Tracks separate counts for each unit that triggers it\n- Stores counts in the state file under the count unit's name\n- Each triggering unit has its own counter\n- Counts persist across runs\n\n**State File Format:**\n\nThe count unit stores data in the state file like this:\n\n```yaml\ncount-runs:\n  start-trigger: 5\n\ncount-builds:\n  build: 3\n\ncount-failures:\n  build: 1\n```\n\n**Configuration example:**\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  - start:\n      name: start-trigger\n      on_success:\n        - build\n      always:\n        - count-runs\n\n  - run:\n      name: build\n      script: |\n        go build -o brun ./cmd/brun\n      on_success:\n        - count-builds\n      on_failure:\n        - count-failures\n\n  - count:\n      name: count-runs\n\n  - count:\n      name: count-builds\n\n  - count:\n      name: count-failures\n```\n\n### ⏰ Cron Unit\n\nThe Cron unit is a trigger that fires based on a cron schedule. It uses the\nstandard cron format to define when the trigger should activate. In daemon mode,\nthe trigger is checked every 10 seconds. The\n[robfig/cron](https://pkg.go.dev/github.com/robfig/cron/v3) package is used for\nschedule parsing.\n\n**Fields:**\n\n- **`schedule`** (required): Cron schedule in standard format (minute hour day\n  month weekday)\n\n**Behavior:**\n\n- Triggers based on the cron schedule\n- Stores last execution time in the state file\n- Works in both one-time and daemon modes\n- In one-time mode: triggers if schedule indicates it should have run since last\n  execution\n- In daemon mode: continuously monitors and triggers at scheduled times\n\n**Cron Schedule Format:**\n\nStandard 5-field cron format:\n\n```\n* * * * *\n│ │ │ │ │\n│ │ │ │ └─── Day of week (0-6, Sunday=0)\n│ │ │ └───── Month (1-12)\n│ │ └─────── Day of month (1-31)\n│ └───────── Hour (0-23)\n└─────────── Minute (0-59)\n```\n\nExamples:\n\n- `* * * * *` - Every minute\n- `*/5 * * * *` - Every 5 minutes\n- `0 2 * * *` - Daily at 2:00 AM\n- `30 14 * * 1-5` - Weekdays at 2:30 PM\n- `0 0 1 * *` - First day of every month at midnight\n\n**State File Format:**\n\nThe cron unit stores the last execution time:\n\n```yaml\ndaily-backup:\n  last_execution: \"2025-10-03T02:30:00-04:00\"\n\nhealth-check:\n  last_execution: \"2025-10-03T18:00:00-04:00\"\n```\n\n**Configuration example:**\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  # Cron trigger - runs every day at 2:30 AM\n  - cron:\n      name: daily-backup\n      schedule: \"30 2 * * *\"\n      on_success:\n        - backup-unit\n\n  # Cron trigger - runs every 5 minutes\n  - cron:\n      name: health-check\n      schedule: \"*/5 * * * *\"\n      on_success:\n        - check-services\n\n  - run:\n      name: backup-unit\n      script: |\n        echo \"Running daily backup...\"\n        # backup commands here\n\n  - run:\n      name: check-services\n      script: |\n        echo \"Checking services...\"\n        # health check commands here\n```\n\n### ✉️ Email Unit\n\nThe Email unit sends email notifications with optional output from triggering\nunits. This is useful for alerting on build failures, test results, or other\nimportant events. Supports both plain SMTP and STARTTLS encryption.\n\n**Fields:**\n\n- **`to`** (required): Array of email addresses to send to\n- **`from`** (required): Sender email address\n- **`subject_prefix`** (optional): Email subject line prefix. ':\n  \u003cunit-name\u003e:\u003csuccess|fail\u003e' is appended after prefix and is always included.\n- **`smtp_host`** (required): SMTP server hostname\n- **`smtp_port`** (optional): SMTP server port. Defaults to 587 (submission\n  port)\n- **`smtp_user`** (optional): SMTP username for authentication\n- **`smtp_password`** (optional): SMTP password for authentication\n- **`smtp_use_tls`** (optional): Enable STARTTLS encryption. Defaults to true\n- **`include_output`** (optional): Include captured output from triggering unit.\n  Defaults to true\n- **`limit_lines`** (optional): limit number email lines emailed to number\n  specified.\n\n**Behavior:**\n\n- Sends plain text emails using SMTP\n- Can include output from the unit that triggered it (useful for log/error\n  reporting)\n- Supports SMTP authentication\n- STARTTLS encryption enabled by default\n- Works with common email providers (Gmail, SendGrid, Mailgun, etc.)\n\n**Configuration example:**\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  - boot:\n      name: boot-trigger\n      on_success:\n        - build\n\n  - run:\n      name: build\n      script: |\n        go build -o brun ./cmd/brun\n        go test -v\n      on_failure:\n        - email-failure\n\n  - email:\n      name: email-failure\n      to:\n        - admin@example.com\n        - alerts@example.com\n      from: brun@example.com\n      subject_prefix: \"Build Alert\"\n      smtp_host: smtp.gmail.com\n      smtp_port: 587\n      smtp_user: brun@example.com\n      smtp_password: your-app-password\n      smtp_use_tls: true\n      include_output: true\n```\n\nThis will send emails with subjects like:\n\n- `Build Alert: build:success` (on success)\n- `Build Alert: build:fail` (on failure)\n\n**Gmail example:**\n\nFor Gmail, you need to use an app-specific password:\n\n```yaml\n- email:\n    name: notify-admin\n    to:\n      - you@gmail.com\n    from: your-app@gmail.com\n    subject_prefix: \"CI/CD\"\n    smtp_host: smtp.gmail.com\n    smtp_port: 587\n    smtp_user: your-app@gmail.com\n    smtp_password: your-16-char-app-password\n    smtp_use_tls: true\n```\n\n### 📨 Email Receive Unit (TODO)\n\nThis can receive emails to trigger units.\n\n### 📁 File Unit\n\nThe File unit monitors files and triggers when they are changed. Files can be\nspecified using glob patterns with support for `**` recursive matching. New or\nremoved files are detected as changes.\n\n**Fields:**\n\n- **`pattern`** (required): Glob pattern to match files (supports `**` for\n  recursive matching)\n\n**Behavior:**\n\n- Monitors files matching the glob pattern\n- Triggers when file content changes (detected via SHA256 hash)\n- Triggers when files are added or removed\n- Stores file hashes in the state file\n- Triggers on first run (initial file state)\n- Ignores directories (only monitors regular files)\n- Works in both one-time and daemon modes\n\n**Pattern Syntax:**\n\nThe file unit supports advanced glob patterns including:\n\n- `*` - matches any sequence of non-separator characters\n- `?` - matches any single non-separator character\n- `[abc]` - matches any character in the set\n- `[a-z]` - matches any character in the range\n- `**` - matches zero or more directories recursively\n\n**Pattern Examples:**\n\n- `**/*.go` - all Go files recursively\n- `src/**/*.ts` - all TypeScript files under `src/`\n- `config/*.yaml` - config files non-recursively\n- `**/*.{html,css,js}` - multiple filetypes\n\n**State File Format:**\n\nThe file unit stores a hash of all monitored files:\n\n```yaml\nwatch-source:\n  files_state: \"file1.go:a1b2c3...|file2.go:d4e5f6...\"\n```\n\n**Configuration example:**\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  # File trigger - monitors Go source files\n  - file:\n      name: watch-source\n      pattern: \"**/*.go\"\n      on_success:\n        - build\n        - test\n\n  - run:\n      name: build\n      script: |\n        echo \"Building...\"\n        go build -o app ./cmd/app\n\n  - run:\n      name: test\n      script: |\n        echo \"Running tests...\"\n        go test -v ./...\n```\n\n**Daemon mode example:**\n\nWhen running in daemon mode, the file trigger continuously monitors files and\nautomatically triggers builds/tests when changes are detected:\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  - file:\n      name: auto-build\n      pattern: \"**/*.go\"\n      on_success:\n        - build\n        - test\n      always:\n        - email-notify\n\n  - run:\n      name: build\n      script: |\n        go build -o app ./cmd/app\n\n  - run:\n      name: test\n      script: |\n        go test -v ./...\n\n  - email:\n      name: email-notify\n      to:\n        - team@example.com\n      from: ci@example.com\n      subject_prefix: \"Build Status\"\n      smtp_host: smtp.example.com\n      smtp_port: 587\n      smtp_user: ci@example.com\n      smtp_password: secret\n```\n\nRun with: `brun run config.yaml -daemon`\n\nThis creates a continuous integration system that automatically builds and tests\nyour code whenever source files are modified.\n\n### 🔀 Git Unit\n\nThe Git unit is a trigger that fires when changes are detected in a Git\nrepository. It monitors the repository's HEAD commit and triggers when new\ncommits are detected. This is useful for automatically running builds, tests, or\ndeployments when code changes.\n\nIf the `repository` field points to a local Git workspace (vs a Repo URL), the\nworkspace and submodules are updated to the latest on the specified branch.\n\n**Fields:**\n\n- **`repository`** (required): Path to the Git repository to monitor\n- **`branch`** (required): Branch to monitor\n- **`reset`** (optional): optionally reset the workspace to the state of the\n  repo HEAD (`git reset --hard`)\n- **`poll`** (optional): polling interval for checking repository updates (e.g.,\n  `2m`, `30s`, `1h`). When set, the git unit actively polls for updates at the\n  specified interval. When omitted, the unit operates in passive mode: it will\n  NOT check during orchestrator polling, but WILL check when explicitly\n  triggered by another unit (e.g., via `on_success`). This enables event-driven\n  workflows where git checks happen on-demand without continuous polling\n  overhead.\n- **`debug`** (optional): when true, logs detailed git operation messages\n  (fetch, reset, submodule updates). Defaults to false.\n\n**SSH Authentication:**\n\nWhen using SSH-based Git repositories with systemd, the service requires access\nto your SSH agent. See the [Auto tart with systemd](#autostart-with-systemd)\nsection for configuration details on setting the `SSH_AUTH_SOCK` environment\nvariable.\n\n**Behavior:**\n\n- Monitors the HEAD commit hash of the specified Git repository\n- Triggers when the commit hash changes (new commits detected)\n- Stores the last seen commit hash in the state file\n- Triggers on first run (initial repository state)\n- Uses go-git library (no git CLI tool required)\n- Works in both one-time and daemon modes\n\n**State File Format:**\n\nThe git unit stores the last seen commit hash:\n\n```yaml\nwatch-repo:\n  last_commit_hash: \"a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0\"\n```\n\n**Configuration example:**\n\nWhen running in daemon mode, the git trigger continuously monitors the\nrepository and automatically triggers builds/tests when new commits are\ndetected:\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  - git:\n      name: auto-build\n      repository: /home/user/project\n      branch: main\n      poll: 2m # Check for updates every 2 minutes\n      debug: false # Suppress verbose git operation logs\n      on_success:\n        - build\n\n  - run:\n      name: build\n      directory: /home/user/project\n      script: |\n        go build -o app ./cmd/app\n        go test -v ./...\n      always:\n        - email\n\n  - email:\n      name: email\n      to:\n        - team@example.com\n      from: ci@example.com\n      subject_prefix: \"Build Success\"\n      smtp_host: smtp.example.com\n      smtp_port: 587\n      smtp_user: ci@example.com\n      smtp_password: secret\n```\n\nThis creates a continuous integration system that automatically builds and tests\nyour code whenever changes are pushed to the repository.\n\n**Passive mode example (event-driven):**\n\nFor efficient resource usage, you can configure git units without polling and\ntrigger them explicitly:\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  # Check for git updates once per hour\n  - cron:\n      name: hourly-check\n      schedule: \"0 * * * *\"\n      on_success:\n        - check-repo\n\n  # Git unit in passive mode (no poll field)\n  - git:\n      name: check-repo\n      repository: /home/user/project\n      branch: main\n      # No poll field - only checks when triggered by cron\n      on_success:\n        - build\n\n  - run:\n      name: build\n      directory: /home/user/project\n      script: |\n        go build -o app ./cmd/app\n        go test -v ./...\n```\n\nThis approach checks for git updates only when the cron triggers it, reducing\nsystem overhead while maintaining automated builds.\n\n### 📝 Log Unit\n\nThe Log unit writes log entries to a file. This is useful for recording events,\nerrors, or other information during pipeline execution. The logfile is created\nif it doesn't exist, and entries are appended with timestamps.\n\n**Fields:**\n\n- **`file`** (required): Path to the logfile where entries will be written\n\n**Behavior:**\n\n- Creates the logfile and parent directories if they don't exist\n- Appends log entries with timestamps\n- File permissions are set to 0644\n- Directory permissions are set to 0755\n\n**Configuration example:**\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  - start:\n      name: start-trigger\n      on_success:\n        - build\n      always:\n        - log-run\n\n  - run:\n      name: build\n      script: |\n        go build -o brun ./cmd/brun\n      on_failure:\n        - log-error\n\n  - log:\n      name: log-run\n      file: /var/log/brun/pipeline.log\n\n  - log:\n      name: log-error\n      file: /var/log/brun/errors.log\n```\n\n### 🔔 Ntfy Unit\n\nThe ntfy unit allows notifications be sent out using the\n[ntfy.sh](https://ntfy.sh/) service.\n\n**Fields:**\n\n- **`topic`** (required): Ntfy topic to post to\n- **`server`** (optional): Ntfy server URL. Defaults to `https://ntfy.sh`\n- **`title_prefix`** (optional): Notification title prefix. ':\n  \u003cunit-name\u003e:\u003csuccess|fail\u003e' is appended after prefix and is always included\n- **`priority`** (optional): Notification priority (min, low, default, high,\n  urgent)\n- **`tags`** (optional): Comma-separated tags/emojis for the notification\n- **`include_output`** (optional): Include captured output from triggering unit.\n  Defaults to true\n- **`limit_lines`** (optional): Limit number of output lines included in\n  notification. 20 lines is a good number. More than that, the Android app seems\n  to turn the log into an attachment.\n\n**Behavior:**\n\n- Sends notifications via HTTP POST to ntfy.sh (or self-hosted server)\n- Can include output from the unit that triggered it (useful for log/error\n  reporting)\n- Title automatically includes triggering unit name and success/fail status\n\n**Configuration example:**\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  - boot:\n      name: boot-trigger\n      on_success:\n        - build\n\n  - run:\n      name: build\n      script: |\n        go build -o brun ./cmd/brun\n        go test -v\n      on_failure:\n        - notify-failure\n      on_success:\n        - notify-success\n\n  - ntfy:\n      name: notify-failure\n      topic: my-build-alerts\n      title_prefix: Build Failed\n      priority: high\n      tags: warning,skull\n      include_output: true\n      limit_lines: 50\n\n  - ntfy:\n      name: notify-success\n      topic: my-build-alerts\n      title_prefix: Build Succeeded\n      priority: default\n      tags: white_check_mark\n      include_output: false\n```\n\n### 🔄 Reboot Unit\n\nThe reboot unit logs and reboots the system. This is typically used in reboot\ncycle testing where the boot trigger can count boot cycles and trigger test\nsequences.\n\n**Fields:**\n\n- **`delay`** (optional): Number of seconds to wait before executing reboot\n  (default: 0 for immediate reboot)\n\n**Configuration example:**\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  - reboot:\n      name: reboot-system\n      delay: 5 # optional delay in seconds before reboot (default: 0)\n```\n\n### ▶️ Run Unit\n\nThe Run unit executes arbitrary shell commands or scripts. This is the primary\nexecution unit for running builds, tests, or any other commands. The exit code\ndetermines success or failure, which then triggers the appropriate units.\n\nMultiple Run units can be defined in a configuration file to create build and\ntest pipelines.\n\n**Fields:**\n\n- **`script`** (required): Shell commands to execute. Can be a single command or\n  a multiline script\n- **`directory`** (optional): Working directory where the script will be\n  executed. Defaults to the directory where BRun was invoked\n- **`timeout`** (optional): Time out duration for the task to complete (e.g.,\n  `30s`, `5m`, `1h`, `1h30m`). If no timeout is specified, it runs until\n  completion. If the task times out, an error message is logged.\n- **`shell`** (optional): specify shell to use when running command (bash,\n  etc.). By default, 'sh' is used.\n- **`use_pty`** (optional): when set to true, wraps the command with `script` to\n  provide a pseudo-TTY. This is useful for tools like BitBake that require a TTY\n  environment. Default is false.\n\n**Behavior:**\n\n- The script is executed using the system shell\n- Exit code 0 is considered success and triggers `on_success` units\n- Nonzero exit codes are considered failures and trigger `on_failure` units\n- Both `STDOUT` and `STDERR` are logged\n\n**Configuration example:**\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  - boot:\n      name: boot-trigger\n      on_success:\n        - build\n\n  - run:\n      name: build\n      directory: /home/user/project\n      script: |\n        go build -o brun ./cmd/brun\n        go test -v\n      on_success:\n        - deploy\n      on_failure:\n        - notify-failure\n\n  - run:\n      name: deploy\n      script: |\n        ./deploy.sh\n\n  - run:\n      name: bitbake-build\n      shell: bash\n      use_pty: true\n      script: |\n        source oe-init-build-env\n        bitbake core-image-minimal\n      timeout: 2h\n```\n\n### ⭐ Start Unit\n\nThe Start trigger always fires when BRun runs. This can be used to trigger other\nunits every time the program executes, regardless of boot state or other\nconditions.\n\n**Behavior:**\n\n- Always triggers on every BRun\n- Does not maintain any state\n- Useful for unconditional execution pipelines\n\n**Configuration example:**\n\n```yaml\nconfig:\n  state_location: /var/lib/brun/state.yaml\n\nunits:\n  - start:\n      name: start-trigger\n      on_success:\n        - build-unit\n        - test-unit\n```\n\n## 🔄 Program Lifecycle\n\nBRun traps kill signals and waits for all triggers to complete before exiting.\n\n## 🚦 Status\n\nThis project is in the exploratory phase as we explore various concepts. The\nsyntax of the BRun file may change as we learn how to better do this.\n\nIf you are using BRun, please like this repo and subscribe to release updates.\nIf there are features you would like, open an issue. This provides motivation to\nkeep the project going.\n\nFeedback/contributions welcome! Please\n[discuss](https://github.com/cbrake/brun/discussions) before implementing\nanything major.\n\nSee [issues](https://github.com/cbrake/brun/issues) and [ideas](ideas.md) for\nfuture direction.\n\nI have no idea if this works on Windows - feel free to try and report status.\n\nIf you use BRun, please star/follow the repo and\n[let me know](https://github.com/cbrake/brun/discussions).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcbrake%2Fbrun","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcbrake%2Fbrun","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcbrake%2Fbrun/lists"}