Porting · porting/2026-05-05-hetzner-source-sync.md Docs Home

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.mjs Source sync helper with plan, local safety check, target preflight, dry-run, guarded push, and redacted evidence output.
  • package.json scripts:
    • hetzner:sync:plan
    • hetzner:sync:check
    • hetzner:sync:target
    • hetzner:sync:dry-run
    • hetzner: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 --delete command can run.
  • The standard remote path must end with /opt/LegacyBlinkin-2-Hetzner/.
  • The helper fails if real local configs/hetzner/*.env files or configs/hetzner/Caddyfile exist.
  • The helper warns if root .env exists locally, but the rsync filter excludes .env and .env.*.
  • The real push uses rsync --delete, so it requires HETZNER_SYNC_CONFIRM=1 or --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 .env in 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.mjs passed.
  • npm run hetzner:sync:plan prints the source sync workflow.
  • npm run hetzner:sync:check passed.
  • npm run hetzner:sync:target fails locally as expected without HETZNER_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:
    • rsync is available;
    • configs/hetzner/rsync-filter.rules exists;
    • 13 required filter patterns are present;
    • 0 local Hetzner runtime env/Caddy files exist;
    • root .env exists locally but is excluded by the filter.
  • npm run hetzner:sync:push with a dummy target but without confirmation refused to run rsync --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.