Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4c96a8c
improve error messaging
rlogwood Mar 10, 2026
219272f
add prune and policy test, add zelta policy config file generator, su…
rlogwood Mar 10, 2026
7a2e0fd
add prune and policy test, remove warnings from clone and revert for …
rlogwood Mar 10, 2026
f0f7470
regenerate tests, add new prune and policy test
rlogwood Mar 10, 2026
9a7d1bd
add more context on sudoers update to include Ubuntu and FreeBSD
rlogwood Mar 10, 2026
6f1cb36
add zman alias
rlogwood Mar 10, 2026
d8278d1
fix wildcarding, move all subs to ruby and out of awk, refactor test …
rlogwood Mar 12, 2026
b1b7773
streamline test gen, regenerate all tests
rlogwood Mar 12, 2026
5023942
improve error handling, prevent shell exit from source files
rlogwood Mar 13, 2026
09dfd5f
regenerate all tests with matcher function printf fix
rlogwood Mar 14, 2026
d8bdd18
use printf instead of echo in normalization
rlogwood Mar 14, 2026
f6d62bc
handle localhost when generating policy
rlogwood Mar 14, 2026
528a5a0
add vm helpers for mimicign github testing, first version of github w…
rlogwood Mar 14, 2026
3daffb3
run shellspec install with sudo
rlogwood Mar 14, 2026
a3430b5
add ShellSpec Tests status badge before what's new
rlogwood Mar 15, 2026
4198bc5
reposition the ShellSpec Tests badge
rlogwood Mar 15, 2026
b93b7bd
regenerated test with matcher func fix
rlogwood Mar 15, 2026
411e6b4
fix line evaluation in matcher function
rlogwood Mar 15, 2026
d9c2566
update bellhye to bell-tower in urls
rlogwood Mar 15, 2026
2793eff
fix typo fo -> of
rlogwood Mar 15, 2026
f9a66da
better defaults for local runs, leave remotes commented out by default
rlogwood Mar 15, 2026
ef29644
clean up comments
rlogwood Mar 15, 2026
2c4f337
do not add empty env vars to the map
rlogwood Mar 15, 2026
521da0f
remove debug info
rlogwood Mar 15, 2026
ceeabc7
run under bash shell
rlogwood Mar 15, 2026
3868d43
fix !# path for env
rlogwood Mar 15, 2026
50b234c
make testuser sudoer file 0440
rlogwood Mar 15, 2026
17a87c6
Replace real user/host identifiers with fake placeholders in zelta_te…
Copilot Mar 15, 2026
75e2cec
fix argument evaluation to allow optional arg passing
rlogwood Mar 15, 2026
9f5d244
update ownership and perm explicitly on testuser sudoers and verify.
rlogwood Mar 15, 2026
a378ae2
git ignore shellspec generated policy file, rename current file to .e…
rlogwood Mar 16, 2026
4c59980
chown, chmod, verify sudoers file
rlogwood Mar 16, 2026
82e4753
improve setup and test instructions
rlogwood Mar 16, 2026
89bc168
ensure no dependency on ~ evaluation
rlogwood Mar 16, 2026
5a09776
avoid awk implementation specific treatment of \$ escapes
rlogwood Mar 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/shellspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: ShellSpec Tests

on:
push:
branches: [main, dev, "feature/**"]
pull_request:
branches: [main, dev]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y zfsutils-linux man-db

- name: Install ShellSpec
run: curl -fsSL https://git.io/shellspec | sudo sh -s -- --yes --prefix /usr/local

- name: Create test user
run: |
sudo useradd -m -s /bin/bash testuser
echo 'testuser ALL=(ALL) NOPASSWD: /usr/bin/dd *, /usr/bin/rm -f /tmp/*, /usr/bin/truncate *, /usr/sbin/zpool *, /usr/sbin/zfs *' \
| sudo tee /etc/sudoers.d/testuser
sudo chown root:root /etc/sudoers.d/testuser
sudo chmod 0440 /etc/sudoers.d/testuser
sudo visudo -cf /etc/sudoers.d/testuser

- name: Copy repo for test user
run: |
sudo cp -r "$GITHUB_WORKSPACE" /home/testuser/zelta
sudo chown -R testuser:testuser /home/testuser/zelta

- name: Run ShellSpec tests
run: |
sudo -u testuser env \
SANDBOX_ZELTA_SRC_POOL=apool \
SANDBOX_ZELTA_TGT_POOL=bpool \
SANDBOX_ZELTA_SRC_DS=apool/treetop \
SANDBOX_ZELTA_TGT_DS=bpool/backups \
bash -c 'cd /home/testuser/zelta && shellspec'
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
.*
!.gitignore
!.shellspec
!.github/
test/runners/test_generation/config/zelta_test_policy.conf
*.swp
*.swo
.*
doc/man?
tmp
hide.*
Expand All @@ -14,3 +16,4 @@ A*d
retired/
logs
log
ci/*
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
![Zelta Logo](https://zelta.space/index/zelta-banner.svg)

# The Zelta Backup and Recovery Suite
*Version 1.1, 2026-01-20*

---

> - **What's New:** Check [CHANGELOG.md](CHANGELOG.md) for the latest changes
> - **Found a Bug?** Please [open an issue](https://github.com/bellhyve/zelta/issues)
> - **Previous Release:** [March 2024, Zelta v1.0](https://github.com/bellhyve/zelta/tree/release/1.0)

> - **Found a Bug?** Please [open an issue](https://github.com/bell-tower/zelta/issues)
> - **Previous Release:** [March 2024, Zelta v1.0](https://github.com/bell-tower/zelta/tree/release/1.0)
>
>      ![ShellSpec Tests](https://github.com/bell-tower/zelta/actions/workflows/shellspec.yml/badge.svg)
---

[zelta.space](https://zelta.space) | [Documentation](https://zelta.space/en/home) | [GitHub](https://github.com/bellhyve/zelta)
[zelta.space](https://zelta.space) | [Documentation](https://zelta.space/en/home) | [GitHub](https://github.com/bell-tower/zelta)

**Zelta** provides bulletproof backups that meet strict compliance requirements while remaining straightforward to deploy and operate. It transforms complex backup and recovery operations into safe, auditable commands—protecting your data without requiring specialized expertise.

Expand Down Expand Up @@ -41,7 +40,7 @@ Written in portable Bourne shell and AWK, Zelta runs anywhere ZFS runs. No packa

### From Source (Recommended for Zelta 1.1)
```sh
git clone https://github.com/bellhyve/zelta.git
git clone https://github.com/bell-tower/zelta.git
cd zelta
sudo ./install.sh
# The installer will guide you through setup.
Expand All @@ -59,12 +58,12 @@ pkg install zelta
The following command clones Zelta from the `main` branch and launches the installer. Run this as a personal or backup user for a local, non-root installation. If you prefer a system-wide installation, run the command as root or via `sudo/doas`.

```sh
curl -fsSL https://raw.githubusercontent.com/bellhyve/zelta/main/contrib/install-from-git.sh | sh
curl -fsSL https://raw.githubusercontent.com/bell-tower/zelta/main/contrib/install-from-git.sh | sh
```

The installer will detect your privileges and guide you through adding the necessary environment variables to your shell profile.

*Security Note: As with any script piped from the internet, we encourage you to [inspect the installer source](https://github.com/bellhyve/zelta/blob/main/contrib/install-from-git.sh) before execution.*
*Security Note: As with any script piped from the internet, we encourage you to [inspect the installer source](https://github.com/bell-tower/zelta/blob/main/contrib/install-from-git.sh) before execution.*

---

Expand Down Expand Up @@ -187,7 +186,7 @@ We welcome contributors who are passionate about data protection and recovery. B

### Contact

We welcome questions, bug reports, and feature requests at [GitHub Issues](https://github.com/bellhyve/zelta/issues).
We welcome questions, bug reports, and feature requests at [GitHub Issues](https://github.com/bell-tower/zelta/issues).

For other inquiries including business questions, you can reach the Zelta team at Bell Tower via our [contact form](https://belltower.it/contact/).

Expand Down
2 changes: 1 addition & 1 deletion contrib/install-from-git.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Zelta One-Shot Installer
# Downloads latest Zelta from GitHub and runs install.sh
#
# Usage: curl -fsSL https://raw.githubusercontent.com/bellhyve/zelta/main/contrib/install-from-git.sh | sh
# Usage: curl -fsSL https://raw.githubusercontent.com/bell-tower/zelta/main/contrib/install-from-git.sh | sh
# Or specify branch: curl ... | sh -s -- --branch=develop

set -e
Expand Down
26 changes: 15 additions & 11 deletions test/040_zelta_tests_spec.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Auto-generated ShellSpec test file
# Generated at: 2026-02-12 13:28:34 -0500
# Generated at: 2026-03-15 02:59:07 -0400
# Source: 040_zelta_tests_spec
# WARNING: This file was automatically generated. Manual edits may be lost.

output_for_match_after_divergence() {
while IFS= read -r line; do
# normalize whitespace, remove leading/trailing spaces
normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$normalized" in
"DS_SUFFIX MATCH SRC_LAST TGT_LAST INFO"|\
"[treetop] @start @start @start up-to-date"|\
Expand All @@ -24,7 +24,8 @@ output_for_match_after_divergence() {
"11 total datasets compared")
;;
*)
printf "Unexpected line format: %s\n" "$line" >&2
printf "Unexpected line format : %s\n" "$line" >&2
printf "Comparing to normalized: %s\n" "$normalized" >&2
return 1
;;
esac
Expand All @@ -35,16 +36,17 @@ output_for_match_after_divergence() {
output_for_rotate_after_divergence() {
while IFS= read -r line; do
# normalize whitespace, remove leading/trailing spaces
normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$normalized" in
"source is written; snapshotting: @zelta_"*""|\
"renaming '${SANDBOX_ZELTA_TGT_DS}' to '${SANDBOX_ZELTA_TGT_DS}_start'"|\
"to ensure target is up-to-date, run: zelta backup ${SANDBOX_ZELTA_SRC_EP} ${SANDBOX_ZELTA_TGT_EP}"|\
"no source: ${SANDBOX_ZELTA_TGT_DS}/sub1/kid"|\
"* sent, 10 streams received in * seconds")
""*" sent, 10 streams received in "*" seconds")
;;
*)
printf "Unexpected line format: %s\n" "$line" >&2
printf "Unexpected line format : %s\n" "$line" >&2
printf "Comparing to normalized: %s\n" "$normalized" >&2
return 1
;;
esac
Expand All @@ -55,7 +57,7 @@ output_for_rotate_after_divergence() {
output_for_match_after_rotate() {
while IFS= read -r line; do
# normalize whitespace, remove leading/trailing spaces
normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$normalized" in
"DS_SUFFIX MATCH SRC_LAST TGT_LAST INFO"|\
"[treetop] @zelta_"*" @zelta_"*" @zelta_"*" up-to-date"|\
Expand All @@ -72,7 +74,8 @@ output_for_match_after_rotate() {
"10 total datasets compared")
;;
*)
printf "Unexpected line format: %s\n" "$line" >&2
printf "Unexpected line format : %s\n" "$line" >&2
printf "Comparing to normalized: %s\n" "$normalized" >&2
return 1
;;
esac
Expand All @@ -83,14 +86,15 @@ output_for_match_after_rotate() {
output_for_backup_after_rotate() {
while IFS= read -r line; do
# normalize whitespace, remove leading/trailing spaces
normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$normalized" in
"syncing 10 datasets"|\
"10 datasets up-to-date"|\
"* sent, 3 streams received in * seconds")
""*" sent, 3 streams received in "*" seconds")
;;
*)
printf "Unexpected line format: %s\n" "$line" >&2
printf "Unexpected line format : %s\n" "$line" >&2
printf "Comparing to normalized: %s\n" "$normalized" >&2
return 1
;;
esac
Expand Down
61 changes: 17 additions & 44 deletions test/050_zelta_revert_spec.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# Auto-generated ShellSpec test file
# Generated at: 2026-02-12 13:29:24 -0500
# Generated at: 2026-03-15 02:59:54 -0400
# Source: 050_zelta_revert_spec
# WARNING: This file was automatically generated. Manual edits may be lost.

output_for_snapshot() {
while IFS= read -r line; do
# normalize whitespace, remove leading/trailing spaces
normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$normalized" in
"snapshot created '${SANDBOX_ZELTA_SRC_DS}@manual_test'")
;;
*)
printf "Unexpected line format: %s\n" "$line" >&2
printf "Unexpected line format : %s\n" "$line" >&2
printf "Comparing to normalized: %s\n" "$normalized" >&2
return 1
;;
esac
Expand All @@ -22,14 +23,15 @@ output_for_snapshot() {
output_for_backup_after_delta() {
while IFS= read -r line; do
# normalize whitespace, remove leading/trailing spaces
normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$normalized" in
"source is written; snapshotting: @zelta_"*""|\
"syncing 12 datasets"|\
"* sent, 22 streams received in * seconds")
""*" sent, 22 streams received in "*" seconds")
;;
*)
printf "Unexpected line format: %s\n" "$line" >&2
printf "Unexpected line format : %s\n" "$line" >&2
printf "Comparing to normalized: %s\n" "$normalized" >&2
return 1
;;
esac
Expand All @@ -40,31 +42,13 @@ output_for_backup_after_delta() {
output_for_snapshot_again() {
while IFS= read -r line; do
# normalize whitespace, remove leading/trailing spaces
normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$normalized" in
"snapshot created '${SANDBOX_ZELTA_SRC_DS}@another_test'")
;;
*)
printf "Unexpected line format: %s\n" "$line" >&2
return 1
;;
esac
done
return 0
}

output_for_revert() {
while IFS= read -r line; do
# normalize whitespace, remove leading/trailing spaces
normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$normalized" in
"renaming '${SANDBOX_ZELTA_SRC_DS}' to '${SANDBOX_ZELTA_SRC_DS}_manual_test'"|\
"cloned 12/12 datasets to ${SANDBOX_ZELTA_SRC_DS}"|\
"snapshotting: @zelta_"*""|\
"to retain replica history, run: zelta rotate '${SANDBOX_ZELTA_SRC_DS}' 'TARGET'")
;;
*)
printf "Unexpected line format: %s\n" "$line" >&2
printf "Unexpected line format : %s\n" "$line" >&2
printf "Comparing to normalized: %s\n" "$normalized" >&2
return 1
;;
esac
Expand All @@ -75,16 +59,17 @@ output_for_revert() {
output_for_rotate_after_revert() {
while IFS= read -r line; do
# normalize whitespace, remove leading/trailing spaces
normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$normalized" in
"renaming '${SANDBOX_ZELTA_TGT_DS}' to '${SANDBOX_ZELTA_TGT_DS}_zelta_"*"'"|\
"to ensure target is up-to-date, run: zelta backup ${SANDBOX_ZELTA_SRC_EP} ${SANDBOX_ZELTA_TGT_EP}"|\
"no source: ${SANDBOX_ZELTA_TGT_DS}/sub5"|\
"no source: ${SANDBOX_ZELTA_TGT_DS}/sub5/child1"|\
"* sent, 10 streams received in * seconds")
""*" sent, 10 streams received in "*" seconds")
;;
*)
printf "Unexpected line format: %s\n" "$line" >&2
printf "Unexpected line format : %s\n" "$line" >&2
printf "Comparing to normalized: %s\n" "$normalized" >&2
return 1
;;
esac
Expand Down Expand Up @@ -118,20 +103,8 @@ Describe 'Test revert'
The status should be success
End

It "revert to last snapshot - zelta revert \"$SANDBOX_ZELTA_SRC_EP\"@manual_test"
When call zelta revert "$SANDBOX_ZELTA_SRC_EP"@manual_test
The output should satisfy output_for_revert
The error should equal "warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root
warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root
warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root
warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root
warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root
warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root
warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root
warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root
warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root
warning: unexpected 'zfs clone' output: cannot open '${SANDBOX_ZELTA_SRC_DS}_manual_test/sub5@manual_test': dataset does not exist
warning: unexpected 'zfs clone' output: cannot open '${SANDBOX_ZELTA_SRC_DS}_manual_test/sub5/child1@manual_test': dataset does not exist"
It "revert to last snapshot (ignore warnings) - zelta revert -qq \"$SANDBOX_ZELTA_SRC_EP\"@manual_test"
When call zelta revert -qq "$SANDBOX_ZELTA_SRC_EP"@manual_test
The status should be success
End

Expand Down
30 changes: 6 additions & 24 deletions test/060_zelta_clone_spec.sh
Original file line number Diff line number Diff line change
@@ -1,35 +1,20 @@
# Auto-generated ShellSpec test file
# Generated at: 2026-02-12 13:30:29 -0500
# Generated at: 2026-03-15 03:00:54 -0400
# Source: 060_zelta_clone_spec
# WARNING: This file was automatically generated. Manual edits may be lost.

output_for_clone_sub2() {
while IFS= read -r line; do
# normalize whitespace, remove leading/trailing spaces
normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$normalized" in
"cloned 2/2 datasets to ${SANDBOX_ZELTA_SRC_DS}/copy_of_sub2")
;;
*)
printf "Unexpected line format: %s\n" "$line" >&2
return 1
;;
esac
done
return 0
}

output_for_zfs_list_for_clone() {
while IFS= read -r line; do
# normalize whitespace, remove leading/trailing spaces
normalized=$(echo "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
normalized=$(printf '%s' "$line" | tr -s '[:space:]' ' ' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case "$normalized" in
"NAME ORIGIN"|\
"${SANDBOX_ZELTA_SRC_DS}/copy_of_sub2 ${SANDBOX_ZELTA_SRC_DS}/sub2@zelta_"*""|\
"${SANDBOX_ZELTA_SRC_DS}/copy_of_sub2/orphan ${SANDBOX_ZELTA_SRC_DS}/sub2/orphan@zelta_"*"")
;;
*)
printf "Unexpected line format: %s\n" "$line" >&2
printf "Unexpected line format : %s\n" "$line" >&2
printf "Comparing to normalized: %s\n" "$normalized" >&2
return 1
;;
esac
Expand All @@ -40,11 +25,8 @@ output_for_zfs_list_for_clone() {
Describe 'Test clone'
Skip if 'SANDBOX_ZELTA_SRC_DS undefined' test -z "$SANDBOX_ZELTA_SRC_DS"

It "zelta clone sub2 - zelta clone \"$SANDBOX_ZELTA_SRC_EP/sub2\" \"$SANDBOX_ZELTA_SRC_EP/copy_of_sub2\""
When call zelta clone "$SANDBOX_ZELTA_SRC_EP/sub2" "$SANDBOX_ZELTA_SRC_EP/copy_of_sub2"
The output should satisfy output_for_clone_sub2
The error should equal "warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root
warning: unexpected 'zfs clone' output: filesystem successfully created, but it may only be mounted by root"
It "zelta clone sub2 (ignore warnings) - zelta clone -qq \"$SANDBOX_ZELTA_SRC_EP/sub2\" \"$SANDBOX_ZELTA_SRC_EP/copy_of_sub2\""
When call zelta clone -qq "$SANDBOX_ZELTA_SRC_EP/sub2" "$SANDBOX_ZELTA_SRC_EP/copy_of_sub2"
The status should be success
End

Expand Down
Loading
Loading