PR-gated GitHub backup for your Home Assistant config. Nothing force-pushes, every change is a reviewable pull request, and CI validates your config against pinned Home Assistant versions before it can merge.
Most config-sync tools upload your files straight onto a branch — some even force-push. That gives you a copy, not control. This add-on treats your config like production infrastructure:
| Direct-push sync tools | GitOps Config Backup | |
|---|---|---|
| Changes land on main | Immediately, sometimes force-pushed | Never — every drift becomes a PR |
| Review before accept | No | Yes — diff, comment, merge (or close) |
| CI validation | No | Yes — HA version-matrix check + smoke boot (config must actually start) gate the merge |
| Bidirectional | Usually one-way upload | Yes — merged PRs flow back on the next run |
| Secrets safety | gitignore defaults | gitignore defaults + CI runs with stub secrets only |
flowchart LR
subgraph HA["🏠 Home Assistant box"]
CFG[("/config")]
ADDON["GitOps Backup add-on<br/>two-way sync, every run:<br/>⬇ pull merged → ⬆ push drift"]
CFG -- "① local edits<br/>(UI, HA upgrades)" --> ADDON
ADDON -- "⑤ merged changes<br/>applied to /config" --> CFG
end
subgraph GH["☁️ GitHub"]
BR["auto-backup/… branch"]
PR{{"Pull Request"}}
subgraph CI["CI gate — must pass"]
LINT["yamllint"]
CHECK["HA config check<br/>pinned + stable matrix"]
BOOT["HA smoke boot<br/>container must start"]
end
MAIN[("main")]
end
YOU(("👤 You / dev<br/>author & review"))
ADDON -- "② drift → branch + PR" --> BR
BR --> PR
YOU -- "② or: submit config<br/>change PR yourself" --> PR
PR --> LINT & CHECK & BOOT
LINT & CHECK & BOOT --> YOU
YOU -- "③ merge" --> MAIN
MAIN -- "④ pull --rebase<br/>on the next run" --> ADDON
classDef box fill:#1c3a5e,stroke:#7fb3ff,color:#ffffff
classDef addon fill:#0d7a5f,stroke:#34c39a,color:#ffffff
classDef gate fill:#7a3b0d,stroke:#ffb066,color:#ffffff
classDef human fill:#5e1c4a,stroke:#ff7fd4,color:#ffffff
classDef store fill:#333d3a,stroke:#9aa5a1,color:#ffffff
class ADDON addon
class LINT,CHECK,BOOT gate
class YOU human
class CFG,MAIN,BR,PR store
The loop, numbered: ① you edit via the HA UI (or an upgrade changes files) →
② the add-on turns that drift into a PR — or you open a config PR yourself →
③ CI gates it, you merge → ④ the add-on's next run pulls merged main →
⑤ and applies it to /config. Both directions go through the same run:
it pulls first, then pushes drift, so box and repo converge on main.
- Daily (configurable) run on your Home Assistant box:
stash local drift →
pull --rebasefromorigin/main(picks up PRs you merged) → re-apply → if the live config changed, commit to a datedauto-backup/…branch and open a PR via the GitHub API. - CI on the PR (template workflows included): yamllint + full
frenck/action-home-assistantvalidation against a pinned HA version (with astableearly-warning leg) + a smoke boot — the pinned HA Core container starts with your config and must answer HTTP with noInvalid config— upgrade breakage surfaces in CI, not on your Pi. - You merge (from your phone, if you like). The next add-on run rebases the merged state back onto the box.
- Add this repository to your Add-on store (badge above), install GitOps Config Backup.
- Create a GitHub repo (private recommended) and a fine-grained PAT with Contents: read/write and Pull requests: read/write on it.
- Configure the add-on:
github_repo: you/your-ha-config
github_token: github_pat_…
base_branch: main
branch_prefix: auto-backup
interval_hours: 24
run_at_start: false
dry_run: true # start with a dry run, check the log, then set false
signoff: true- Copy
template/workflows/validate-ha-config.yamlinto.github/workflows/of your config repo, and add a stub line for every!secretkey you use (CI never sees real secrets). - (Recommended) Protect
mainin the repo settings: require the HA Config Validation check.
First run bootstraps the git repo in /config (seeding a Home Assistant
.gitignore covering secrets.yaml, .storage/, databases, logs) and pushes
the initial import; every run after that only ever opens PRs.
The add-on writes .gitops_backup_status (status:detail:extra) to your config
dir. Expose it in HA:
command_line:
- sensor:
name: GitOps Backup Status
command: "cat /config/.gitops_backup_status 2>/dev/null || echo 'unknown'"
scan_interval: 3600Merged PRs are picked up automatically on the next scheduled run. To apply immediately:
ssh root@<HA_IP> 'cd /homeassistant && git fetch origin && git reset --hard origin/main'
ssh root@<HA_IP> 'ha core restart'| Option | Default | Description |
|---|---|---|
github_repo |
— | owner/repo to back up into (required) |
github_token |
— | Fine-grained PAT (required) |
base_branch |
main |
Branch PRs target |
branch_prefix |
auto-backup |
Prefix for backup branches |
interval_hours |
24 |
Hours between runs (1–168) |
run_at_start |
false |
Also run when the add-on starts |
dry_run |
false |
Log what would change; push nothing |
commit_name / commit_email |
see config | Commit author |
signoff |
true |
Add Signed-off-by (DCO) to commits |
- The token lives in the add-on options (Supervisor-encrypted), never in git.
- The seeded
.gitignoreexcludessecrets.yaml,.storage/,.cloud/, databases, logs, and the add-on's own status/log files (committing status files creates a PR-per-day feedback loop — learned the hard way). - CI validates with stub secrets; your real
secrets.yamlnever leaves the box. - Nothing in this add-on ever force-pushes or writes to your base branch after the initial bootstrap import.
- This is change tracking, not disaster recovery — pair it with HA's
native encrypted backups + the Google Drive Backup add-on for full system
state (
secrets.yaml,.storage/, databases, add-ons). See the add-on DOCS for the full "what is backed up where" table and where secrets belong.
Apache-2.0