An open API service indexing awesome lists of open source software.

https://github.com/longcipher/nudeploy

Effortless systemd deployments with Nushell, inspired by Ansible
https://github.com/longcipher/nudeploy

Last synced: 4 months ago
JSON representation

Effortless systemd deployments with Nushell, inspired by Ansible

Awesome Lists containing this project

README

          

# nudeploy

[![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/longcipher/nudeploy)
[![Context7](https://img.shields.io/badge/Website-context7.com-blue)](https://context7.com/longcipher/nudeploy)

![hpx](https://socialify.git.ci/longcipher/nudeploy/image?font=Source+Code+Pro&language=1&name=1&owner=1&pattern=Circuit+Board&theme=Auto)

A tiny, stable, idempotent deployment helper to roll out a systemd service to multiple remote hosts using:

- Nushell for orchestration
- SSH/SCP for remote execution and file transfer

It reuses your existing SSH config at `~/.ssh/config`. No extra SSH config in this repo.

## Configuration: one TOML file

Define hosts and services in a single TOML config (defaults to `./nudeploy.toml`). Example:

```toml
[[hosts]]
name = "localhost"
ip = "127.0.0.1"
port = 22
user = "akagi201"
shell = "zsh" # Optional: override shell wrapper (default "sh") to load correct profile/env
enable = true
group = "prod"

[[services]]
name = "axon"
src_dir = "./axon"
dst_dir = "/home/akagi201/axon"
unit_file = "axon.service" # relative to src_dir or absolute
sync_files = [
{ from = "foo.conf", to = "bar.conf" },
# Support downloading on remote directly:
# { from = "https://example.com/file.conf", to = "bar.conf" }
# Optional per-file chmod (applied with sudo after sync); defaults to "0644"
# { from = "bin/myapp", to = "bin/myapp", chmod = "0755" },
]
restart = true # restart service when files changed
enable = true # enable service (default true)
```

You can target hosts by group or explicitly via `--hosts hostA,hostB`.

## Requirements

- macOS or Linux
- Nushell v0.90+ (newer preferred)
- bash (for the CLI wrapper)
- ssh, scp
- Remote machines run systemd and have sudo available if you need to install into /etc
- Remote tools: one of sha256sum | shasum | openssl must be available (most distros have at least one)

## Install

### Quick Install (Bash script)

This installs `nudeploy` to `~/.local/bin`.

```bash
curl -fsSL https://raw.githubusercontent.com/longcipher/nudeploy/main/install.sh | bash
```

Ensure `~/.local/bin` is in your `$PATH`.

### Manual Install

- Ensure Nushell is installed:
- brew install nushell
- Make the wrapper executable:
- chmod +x nudeploy/nudeploy.sh

### Install via nupm (for Nu users)

The repo includes `nupm.nuon`. You can install via nupm and get a `nudeploy` bin on PATH (points to `nudeploy.nu`).

```shell
# Local install from the current directory (install nupm first per its docs)
nu -c 'nupm install --path .'

# Or install from Git (example)
nu -c 'nupm install --git https://github.com/longcipher/nudeploy'

# Now you can run it directly (nupm exposes `nudeploy` on PATH)
nudeploy --help
```

Note: After nupm install, `nudeploy` runs `nudeploy.nu`. The Bash wrapper `nudeploy.sh` still works standalone.

## Usage

- Plan (no changes; shows what would change):

```shell
# All enabled services
nudeploy deploy --dry-run --group prod
# Single service
nudeploy deploy --dry-run --service axon --group prod
```

- Deploy to a group (idempotent):

```shell
# All enabled services
nudeploy deploy --group prod --sudo
# Single service
nudeploy deploy --service axon --group prod --sudo
```

- Deploy to specific hosts:

```shell
nudeploy deploy --service axon --hosts host1,host2 --sudo
```

- Check status:

```shell
# All enabled services
nudeploy status --group prod
# Single service
nudeploy status --service axon --group prod
```

- Restart without redeploying files:

```shell
# All enabled services
nudeploy restart --hosts host1
# Single service
nudeploy restart --service axon --hosts host1
```

- List hosts (enabled by default):

```shell
nudeploy hosts --group prod
```

- Run a shell command on targets:

```shell
nudeploy exec "uname -a" --group prod
```

- Run a playbook (script execution, stop on error):

```shell
# Run silently (shows only last command output)
nudeploy exec playbooks/arch.sh --group prod

# Run verbosely (shows every command and its output)
nudeploy exec playbooks/arch.sh --group prod -v
```

- Download artifacts locally (curl + extract):

```shell
# All enabled downloads in config
nudeploy download

# Only selected names
nudeploy download --name openobserve

# Alternate config file
nudeploy download --config ./nudeploy.toml
```

## Options

- --config: Path to config TOML (default: ./nudeploy.toml)
- --service: Service name from config (optional for deploy/status/restart). If omitted, acts on all services with `enable = true`.
- --group: Hosts group
- --hosts: Comma-separated hostnames (SSH Host aliases)
- --sudo: Use sudo for systemd actions (daemon-reload/enable/start/restart) and installing unit files into /etc. All other file and directory operations run as the SSH user.
- --json: Emit JSON output suitable for CI
- --name: For `download`, comma-separated artifact names to fetch (defaults to all enabled)
- --dry-run: For `deploy`, show what would change without applying (formerly `plan`)

## Idempotency strategy

- Files are uploaded only if remote checksum differs
- Optional chmod is enforced after sync as the SSH user when `chmod` is set on an item
- Systemd daemon-reload runs only when unit changed
- Service is enabled once if not enabled
- Service is restarted only when changes detected (or restart mode forces it)

## Notes

- nudeploy does not install software on remote machines; it only pushes your service unit/config and manages systemd
- Per-file permissions: set `chmod = "0755"` on binaries you need to execute; default mode is `0644`.
- Local `download` subcommand reads `download_dir` and `[[downloads]]` from your config, fetches with curl, extracts by suffix (tar.gz/tgz, tar.xz, zip, tar, gz, xz), and removes archives after extraction.
- For sudo prompts, passwordless sudo is recommended for automation

### Playbooks

Playbooks are shell scripts (Bash syntax) that nudeploy executes remotely over a single SSH session.

- **Single Session**: Context is preserved between lines (e.g., `cd /tmp` affects subsequent commands).
- **Automatic Error Handling**: `set -e` is automatically prepended, so execution stops immediately if any command fails.
- **Verbose Mode (`-v`)**: Shows every executed command (prefixed with `👉 [Line N]`) and its output interleaved.
- **Quiet Mode (default)**: Shows only the output of the *last* executed command (useful for status checks).

Example `playbooks/bootstrap.sh`:

```bash
# Context is preserved
cd /tmp
curl -L -o app.tar.gz https://example.com/app.tar.gz

# Variables work
APP_DIR="/opt/app"
mkdir -p "$APP_DIR"
tar -xzf app.tar.gz -C "$APP_DIR"

# Logic works
if ! id -u deploy >/dev/null 2>&1; then
useradd -m -s /bin/bash deploy
fi
```

## Dev tips

- The Bash wrapper only parses CLI; all orchestration lives in Nushell

## Quick start

1. Install prerequisites (macOS):

```shell
brew install nushell
```

1. Ensure the CLI is executable and callable:

```shell
chmod +x nudeploy/nudeploy.sh
./nudeploy/nudeploy.sh --help
```

1. Define your config at `./nudeploy.toml` with [[hosts]] and [[services]]. Host names are arbitrary labels; you can also set ip/user/port.

```shell
./nudeploy/nudeploy.sh plan \
--service example-service \
--group all \
--sudo
```

When you’re ready:

```shell
./nudeploy/nudeploy.sh deploy \
--service example-service \
--group all \
--sudo
```

Tip: If Nushell is not on PATH or is named differently, set `NU=/path/to/nu` before running.

## Model and behavior

- Unit file is copied to `/etc/systemd/system/.service`.
- sync_files entries copy local files (hash-compared) or download URLs on the remote; only changed files are installed.
- Idempotent: files only update on hash change; `daemon-reload` only when unit changes; enable once; restart on change when `restart=true`.

## End-to-end example

Use the included example config/service to get a feel for the workflow:

```shell
# Plan the changes (no writes)
./nudeploy/nudeploy.sh plan \
--service axon \
--group prod \
--sudo

# Apply changes idempotently
./nudeploy/nudeploy.sh deploy \
--service axon \
--group prod \
--sudo

# Check status
./nudeploy/nudeploy.sh status \
--service example-service \
--group all
```

Outputs:

- Plan shows which items would be uploaded per host.
- Deploy uploads/downloads only when checksums differ, reloads systemd if unit changed, enables once, and restarts only when needed.
- Status reports enabled/active states, plus a few systemctl properties in JSON mode.

## JSON output for CI

Use `--json` to emit structured records per host that you can pipe to `jq` or parse in CI:

```shell
./nudeploy/nudeploy.sh deploy \
--service axon \
--group prod \
--sudo \
--json
```

You can fail a CI job if any host failed or if changes are found (policy dependent). Example:

```shell
./nudeploy/nudeploy.sh deploy --service foo --group all --sudo --json \
| jq -e 'all(.[]; .ok? // true)'
```

## Output details

Plan prints a detailed summary plus per-file actions by default. Use `--json` for structured data.

Note: The main entry is `nudeploy.sh`. You can symlink it to `nudeploy` to match the examples above.

## Troubleshooting

- First SSH to a host prompts for key: we use `StrictHostKeyChecking=accept-new` which will trust new hosts on first connect.
- Permission denied writing files: destinations must be writable by the SSH user. Pre-create directories/files with proper ownership if needed.
- Permission denied (systemctl): configure passwordless sudo for systemctl for your deployment user, or run with a TTY if prompts are needed.
- Remote host missing hasher: needs one of `sha256sum`, `shasum`, or `openssl`. Install `coreutils` or `perl` packages accordingly.
- Remote not systemd: this tool targets systemd-based Linux. Non-systemd hosts aren’t supported.
- Unit not restarting: with `restart = true`, restarts occur when files changed; otherwise not.
- File destinations: ensure the destination parent directory exists or is creatable; we auto-create with `mkdir -p` when needed.
- PATH issues in `exec`: If commands like `mise` are missing, ensure they are in `~/.profile` (for sh/bash) or `~/.zprofile` (for zsh). You can also set `shell = "zsh"` in `[[hosts]]` config to use zsh as the wrapper shell.

## Environment variables

- `NU`: path to Nushell executable. Defaults to `nu` on PATH.

## Release with nupm

The project includes `nupm.nuon`:

```nu
{
name: "nudeploy",
version: "0.1.0",
description: "Idempotent systemd deploy helper over SSH using Nushell",
license: "MIT",
bins: { nudeploy: "nudeploy.nu" },
modules: ["lib.nu"],
}
```

Example release flow (using nupm install from Git tags):

```shell
# Tag a release (version must match `nupm.nuon`)
git tag v0.1.0 && git push origin v0.1.0

# Verify install from the Git tag
nu -c 'nupm install --git https://github.com/longcipher/nudeploy --tag v0.1.0'

# When bumping versions:
# 1) Update `version` in nupm.nuon
# 2) Update README examples
# 3) Re-tag and push
```