Skip to content

fix(relay-deploy): repair one-click deploy on CRLF builds and root-owned Docker config - #1801

Merged
bobleer merged 2 commits into
GCWing:mainfrom
bobleer:bob/bitfun-relay-deploy-fix-cd820b
Jul 27, 2026
Merged

fix(relay-deploy): repair one-click deploy on CRLF builds and root-owned Docker config#1801
bobleer merged 2 commits into
GCWing:mainfrom
bobleer:bob/bitfun-relay-deploy-fix-cd820b

Conversation

@bobleer

@bobleer bobleer commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Two real servers, two different failures in the relay one-click deploy wizard.

1. deploy.sh: line 37: $'\r': command not found

net@iZ2ze054i2yu0b18abdvbsZ:~$ bash '/home/net/.bitfun/relay-deploy/deploy.sh'
/home/net/.bitfun/relay-deploy/deploy.sh: line 37: $'\r': command not found

Line 37 of the uploaded driver is the first blank line of the embedded mirror.sh. A client built from a CRLF checkout (Git for Windows' core.autocrlf=true default) carried the CR straight into the generated script — through include_str!, and through relay_deploy.rs's own r#"..."# remote templates — so bash executed the CR as a command and set -euo pipefail aborted on the spot. The repo had no .gitattributes.

Fixed at both layers:

  • .gitattributes pins LF for *.sh / *.rs / Dockerfiles / compose / Caddyfile.
  • to_unix_script normalizes everything sent over SFTP or execute_command, so an already-CRLF working tree can't ship a broken script either. A Windows contributor does not need to renormalize their tree to build a working client.

2. Published-binary deploy always fell back to the source build

WARNING: Error loading config file: open /home/parallels/.bitfun/docker-config/config.json: permission denied
unknown shorthand flag: 't' in -t
>>> Published binary image build failed; falling back to source build.

The Docker-install task elevates to root but keeps the SSH user's HOME, so it left ~/.bitfun/docker-config root-owned. The next unprivileged deploy could not read its own config.json, and the docker CLI then mis-dispatched docker build -t as a top-level -t flag — trading the ~1 minute binary path for a ~20 minute source rebuild.

  • Hand ~/.bitfun back to the SSH user after a root install.
  • bitfun_fix_docker_config repairs ownership, or relocates to a per-uid dir.
  • Stop forwarding the user's DOCKER_CONFIG to the sudo branches of bitfun_run_deploy_sh — the other way root poisoned it.
  • The deploy body repairs it unconditionally. bitfun_resolve_docker_mode was the only previous caller, and it is skipped whenever the driver already resolved a non-direct mode — exactly the sg case above.
  • Retry the runtime image build (clean Docker config → classic builder) before surrendering the binary path.

On the causal link: the permission denied is confirmed by the log and is unambiguously a bug. That it is what makes docker misparse -t is inference — I could not reproduce docker CLI internals off-Linux. Hence the retry ladder as a second line of defence: even if the root cause is something else, this path no longer silently costs 20 minutes, and the log names which rung recovered it.

Three further defects found while tracing those

  • sg docker -c "docker $*" re-parsed arguments through a second shell, losing every boundary — a context path with a space, or -f '{{.State.Running}}', arrived mangled. Each argument is now quoted (bitfun_shell_join / shell_join).
  • A prepare-phase death was invisible. The driver that died at line 37 never installed its cleanup trap, so the preparing flag seeded by start_task stayed forever and the wizard reported "running" indefinitely with an empty log pane. The driver now claims <stem>.driver.pid before anything that can fail; a missing/dead driver past a grace window is failed, while an alive one (an open sudo prompt) stays unbounded.
  • release_download_picks_the_fastest_working_source was already failing on main — the harness hardcoded v0.2.13 against a 0.2.14 crate, so every EXPECT_SOURCE="$GITHUB_URL" case failed. It now reads the tag from the script under test, so it survives release bumps.

Tests

27 pass, was 26 with 1 failing.

New coverage: LF assertions on the embedded and generated scripts; bash -n on the generated driver scripts (previously the only generated script with no syntax coverage) and the install body; a shell_join round trip through a second shell; the dead-driver status case.

New CI job shell-scripts rejects CRLF in shell/deploy assets and bash -ns every tracked shell script.

Invariants 16–20 recorded in src/web-ui/src/features/relay-deploy/README.md; the manual-deploy CRLF recovery is in src/apps/relay-server/README.md.

Rollout

The one-click scripts are generated by the client binary, so this needs a client rebuild to take effect. Only the manual git clone && bash deploy.sh path and the source-build fallback (which clones the repo on the server) pick it up from a merge alone.

For servers already broken, before the rebuilt client reaches them:

sed -i 's/\r$//' ~/.bitfun/relay-deploy/*.sh && bash ~/.bitfun/relay-deploy/deploy.sh
sudo chown -R "$(id -un)" ~/.bitfun && bash ~/.bitfun/relay-deploy/deploy.sh

bobleer added 2 commits July 27, 2026 04:13
…ned Docker config

The relay one-click wizard failed on two distinct servers.

1. `deploy.sh: line 37: $'\r': command not found`

Line 37 of the uploaded driver is the first blank line of the embedded
`mirror.sh`. A client built from a CRLF checkout (Git for Windows'
`core.autocrlf=true` default) put the CR straight into the generated script
through `include_str!` — and through this file's own `r#"..."#` remote
templates — so bash ran the CR as a command and `set -euo pipefail` aborted.
The repo had no `.gitattributes`.

Pin LF for shell/Rust/deploy assets, and normalize with `to_unix_script`
everything sent over SFTP or `execute_command`, so an already-CRLF working
tree cannot ship a broken script either.

2. Published-binary deploy always fell back to the source build

The Docker-install task elevates to root but keeps the SSH user's `HOME`, so
it left `~/.bitfun/docker-config` root-owned. The next unprivileged deploy
could not read its own `config.json`, and the docker CLI then mis-dispatched
`docker build -t` as a top-level `-t` flag — surrendering the ~1 minute
binary path for a ~20 minute source rebuild.

Hand `~/.bitfun` back to the SSH user after a root install, repair or
relocate an unusable `DOCKER_CONFIG`, and stop forwarding the user's
`DOCKER_CONFIG` to the `sudo` branches (the other way root poisoned it). The
deploy body now repairs it unconditionally: `bitfun_resolve_docker_mode`,
the only previous caller, is skipped whenever the driver already resolved a
non-direct mode. Retry the runtime image build (clean config, then classic
builder) before giving up on the binary path.

Three further defects found while tracing those:

- `sg docker -c "docker $*"` re-parsed arguments through a second shell,
  losing every boundary (context paths with spaces, `-f '{{.State.Running}}'`).
  Quote each argument instead.
- A driver that died before installing its cleanup trap left the seeded
  `preparing` flag with an empty log, so the wizard polled "running" forever
  instead of surfacing the failure. The driver now claims a pid file before
  anything that can fail; a dead driver past a grace window is `failed`,
  while an alive one (open sudo prompt) stays unbounded.
- `release_download_picks_the_fastest_working_source` was already failing:
  the harness hardcoded `v0.2.13` against a 0.2.14 crate. Read the tag from
  the script under test.

Tests: 27 pass (was 26 with 1 failing). Adds LF assertions, `bash -n` for the
generated driver scripts (previously the only generated script with no syntax
coverage), a shell-join round trip, and the dead-driver status case. New CI job
rejects CRLF in deploy assets and `bash -n`s every tracked shell script.

Note: the one-click scripts are generated by the client binary, so this needs a
client rebuild to take effect. Only the manual `deploy.sh` path and the
source-build fallback (which clones the repo on the server) pick it up directly.
…cripts

The client-side `to_unix_script` normalization only holds while every upload
path remembers to call it. Add the guarantee where it does not depend on that:
`stage_scripts_command` now strips trailing CR on the host, in the SSH round
trip that already runs after the SFTP writes and before the wizard launches the
driver in a PTY.

Uses POSIX `sed` (no `-i`, whose syntax differs between GNU and BSD userlands)
and re-`chmod`s afterwards, since the rewrite drops the file mode.

Tested by running the real staging command against real CRLF files: asserts
LF-only content, 0700 mode, no scratch file left behind, and the liveness files
seeded/cleared as `poll_task` expects.
@bobleer
bobleer merged commit a5d3a1e into GCWing:main Jul 27, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant