Hetzner Source Sync
Date: 2026-05-05
Objective
Make the source sync to Hetzner repeatable and safer than a hand-written rsync command.
This step does not deploy the stack by itself. It prepares the server copy that the Hetzner bootstrap, env, Python, stack, health, backup, and evidence gates can run against.
What Was Added
scripts/hetzner-sync.mjsSource sync helper with plan, local safety check, target preflight, dry-run, guarded push, and redacted evidence output.package.jsonscripts:hetzner:sync:planhetzner:sync:checkhetzner:sync:targethetzner:sync:dry-runhetzner:sync:push
Safety Rules
- The helper uses
configs/hetzner/rsync-filter.rules. - The helper checks that required exclude patterns are present before syncing.
- The target preflight refuses missing targets, invalid target strings, and non-standard remote paths before any
rsync --deletecommand can run. - The standard remote path must end with
/opt/LegacyBlinkin-2-Hetzner/. - The helper fails if real local
configs/hetzner/*.envfiles orconfigs/hetzner/Caddyfileexist. - The helper warns if root
.envexists locally, but the rsync filter excludes.envand.env.*. - The real push uses
rsync --delete, so it requiresHETZNER_SYNC_CONFIRM=1or--yes. - Successful dry-run and push commands write redacted evidence files to
docs/evidence/. - No secret values are printed.
Commands
Check local sync safety:
npm run hetzner:sync:check
Set the target:
export HETZNER_SYNC_TARGET="USER@HOST:/opt/LegacyBlinkin-2-Hetzner/"
Check the target path and SSH readiness before any dry-run or push:
npm run hetzner:sync:target
The target preflight:
- runs the local source-sync safety check first;
- validates the target format;
- validates the remote path;
- connects over SSH;
- creates the remote project directory with
mkdir -p; - prints remote platform, architecture, Node, npm, Docker, rsync, and disk-space status;
- refuses a remote root
.envin the target path; - warns if remote runtime config files already exist so the operator knows they must be preserved by the filter.
Preview the sync:
npm run hetzner:sync:dry-run
Push after reviewing the dry-run:
HETZNER_SYNC_CONFIRM=1 npm run hetzner:sync:push
Successful sync commands write:
docs/evidence/<timestamp>-hetzner-source-sync-dry-run.md
docs/evidence/<timestamp>-hetzner-source-sync-push.md
Optional SSH override:
export HETZNER_SYNC_SSH="ssh -p 22"
After Sync
SSH to the server:
cd /opt/LegacyBlinkin-2-Hetzner/
npm install
npm run hetzner:bootstrap:check
npm run hetzner:host:check
npm run hetzner:config:init:dry
npm run hetzner:config:init
npm run hetzner:env:check
npm run hetzner:python:plan
npm run hetzner:stack:plan
Local Verification Evidence
node --check scripts/hetzner-sync.mjspassed.npm run hetzner:sync:planprints the source sync workflow.npm run hetzner:sync:checkpassed.npm run hetzner:sync:targetfails locally as expected withoutHETZNER_SYNC_TARGET.npm run hetzner:sync:target -- --target=user@example.com:/tmp/LegacyBlinkin-2-Hetzner/refuses the non-standard remote path before SSH.- The check reported:
rsyncis available;configs/hetzner/rsync-filter.rulesexists;- 13 required filter patterns are present;
- 0 local Hetzner runtime env/Caddy files exist;
- root
.envexists locally but is excluded by the filter.
npm run hetzner:sync:pushwith a dummy target but without confirmation refused to runrsync --delete, as intended.- No source sync evidence file was generated locally because no real target was provided and no sync command was allowed to run.
Current Boundary
No real source tree has been pushed to Hetzner yet in this slice.
The first real server sync still needs an actual target in HETZNER_SYNC_TARGET.