Skip to content

Commit e284ba5

Browse files
committed
chore: release v3.1.0
1 parent 5edc81d commit e284ba5

2 files changed

Lines changed: 35 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,40 @@
22

33
All notable changes to `@devalade/shipnode` will be documented here.
44

5+
## [3.1.0] - 2026-07-02
6+
7+
First real production run of 3.0 surfaced a batch of bugs — all fixed — plus one new feature (incremental restic backups to Cloudflare R2 / any S3-compatible store).
8+
9+
### Added
10+
- **Incremental backup strategy via restic** — new `BackupConfig.strategy: 'snapshot' | 'restic'` (default `'snapshot'` for back-compat). The restic path streams `pg_dump` straight into a restic repository on S3-compatible storage (Cloudflare R2 in practice — set `s3Endpoint` to `https://<account-id>.r2.cloudflarestorage.com`), giving block-level dedup, client-side encryption, and 1–5% incremental daily deltas.
11+
- `setup` reads `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` from the local env, generates a `RESTIC_PASSWORD` if absent (and prints it — save it), installs restic via apt, writes `/etc/shipnode/backup.env` (root:root 600) + script + systemd unit+timer.
12+
- `backup run` goes through `systemctl start --wait` so the `EnvironmentFile` is honored (a bare script exec wouldn't source it under a non-root deploy user).
13+
- `backup list` uses `restic snapshots --compact` for the restic strategy.
14+
- New `backup restore [snapshot]` command — extracts a snapshot (or `latest`) to `--target <path>` with `--tag`/`--host` filters. Files are extracted, never applied — the actual recovery (`psql -f db.sql`, `rsync -a shared/`) stays a manual step so a bad restore can't silently trash production.
15+
- Retention knobs: `keepDaily` (default 7), `keepWeekly` (4), `keepMonthly` (6), enforced by `restic forget --prune`.
16+
- **`shipnode user add <name> [--key <path>] [--sudo] [--no-sync]`** — write/update `.shipnode/users.yml` and sync the user to the server in one step, removing the manual YAML editing that `user sync` previously required.
17+
- **`setup` bootstraps a `deploy` user** — first-run setup creates a `deploy` user (sudo, keyed off `${ssh.identityFile}.pub`) with `NOPASSWD:ALL` sudo, registers it in `.shipnode/users.yml`, and chowns `remotePath` to it. Opt out with `--no-deploy-user`. Downstream `harden` and `deploy` runs land on a non-root account instead of root. Installs mise/node/pm2 into the deploy user's home so `pm2-<user>.service` matches. `pm2 startup` runs as root but targets the deploy user; `pm2 save` runs as deploy so `dump.pm2` is the one systemd resurrects at boot.
18+
- **PM2 boot-resurrection section in `harden`** — verifies `pm2-${ssh.user}.service` is installed, offers to refresh its dump (`pm2 save`), and offers to disable stale `pm2-*.service` units left over from a prior root-scoped setup after switching `ssh.user`.
19+
- **`config show --app <name>`** — filter the resolved config to a single app in a multi-app workspace.
20+
21+
### Changed
22+
- **`cloudflare init` uses the Cloudflare REST API for tunnel creation.** Previously relied on `cloudflared tunnel create` and `cloudflared tunnel route dns`, both of which require the browser-based `cloudflared tunnel login` flow (writes `~/.cloudflared/cert.pem`) — not scriptable. `CloudflareApi` gains `getAccountId`, `findTunnel`, `createTunnel`, and `upsertDnsRecord`; the orchestrator finds-or-creates the tunnel via API with a caller-generated secret, writes the credentials JSON at `/etc/cloudflared/<id>.json`, upserts CNAME records pointing at `<id>.cfargotunnel.com`, and enables/restarts the systemd unit. Reusing a tunnel by name now requires its credentials file to already be on the host (Cloudflare doesn't echo the secret back on GET) — error message points at the fix.
23+
- **`cloudflared` install falls back to the upstream `.deb` from GitHub** when `pkg.cloudflare.com` has no package for the current codename (fresh Ubuntu releases like 26.04/`resolute` lag behind). Stale apt sources entry is cleaned up on fallback so `apt update` stops erroring.
24+
- **`config show` / `deploy --dry-run` render every app in the workspace** instead of just `apps[0]`. `config show` gained `--app <name>` to narrow. Dry-run splits output into a workspace header + per-app plan block with namespace-prefixed PM2 names so the preview matches what actually ships.
25+
- **`shipnode --version`** now reads from `package.json` at runtime instead of a hardcoded `'2.0.0'` string.
26+
27+
### Fixed
28+
- **`asUser` produced broken `-u deploy bash …` under root SSH.** The `SUDO=""; [ "$EUID" -ne 0 ] && SUDO="sudo"; $SUDO -u …` idiom reduced to a bare `-u` command when EUID was 0. Switched to unconditional `sudo -u` (transparent under root, no password prompt).
29+
- **`pm2 startup` targeted an invalid shim.** `~/.local/share/mise/shims/pm2` fails with "not a valid shim" because mise only shims tools it manages itself, not npm-installed globals. Resolve pm2's real path via `mise exec node@X -- which pm2` under the deploy user, then invoke it with `env PATH=<node-bin-dir>:$PATH`.
30+
- **Bootstrapped `deploy` user needed NOPASSWD sudo.** `syncUsers` put the user in the `sudo` group but sudo still prompted for a password, so `harden` / `deploy` runs as deploy would hang. Setup now drops a `/etc/sudoers.d/shipnode-deploy` NOPASSWD file right after user creation.
31+
- **`env` command silently uploaded nothing for nested `envFile` paths.** For `apps/backend/.env.production` in a monorepo, `mkdir -p ${appPath}/shared` didn't create `shared/apps/backend/`, so the `echo | base64 -d > ${sharedEnv}` redirect failed silently. The CLI still printed "Uploaded to …" (executor.exec doesn't check exit codes), and the symlink was created pointing at a non-existent file — the next deploy broke with an obscure `./.env` sourcing error. Now uses `execOrThrow` on each step and mkdirs the actual parent of `sharedEnv` via `dirname`.
32+
- **`CaddyService` wrote `/etc/caddy/conf.d/<app>.caddy` and reloaded Caddy without sudo.** Worked when SSH user was root, broke as the deploy user with `Permission denied`. Both the write (via `| sudo tee`) and the reload (via `sudo systemctl`) go through sudo now.
33+
- **`HealthCheckService` built PM2 names from the workspace-level `deploymentName`** (always `apps[0]`'s namespace), so in a multi-app workspace it looked up e.g. `biormin-biormin-frontend` when PM2 actually runs `biormin-frontend` under its own namespace. Every app's check now derives the namespace from that app.
34+
- **Restic env cache missed across runs.** Restic warned `neither $XDG_CACHE_HOME nor $HOME are defined` under systemd's minimal env and re-fetched the R2 index each daily run. `HOME='/root'` is now injected via the backup env file so `/root/.cache/restic` persists.
35+
36+
### Notes
37+
- The restic strategy is opt-in — existing snapshot backups keep working unchanged. Migrate by setting `strategy: 'restic'` in the `.backup(...)` block, exporting AWS credentials, and re-running `shipnode backup setup`.
38+
539
## [3.0.0] - 2026-07-01
640

741
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devalade/shipnode",
3-
"version": "3.0.0",
3+
"version": "3.1.0",
44
"description": "Deploy Node.js apps to a single VPS with zero configuration",
55
"type": "module",
66
"main": "dist/index.js",

0 commit comments

Comments
 (0)