fix(relay-deploy): repair one-click deploy on CRLF builds and root-owned Docker config - #1801
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two real servers, two different failures in the relay one-click deploy wizard.
1.
deploy.sh: line 37: $'\r': command not foundLine 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=truedefault) carried the CR straight into the generated script — throughinclude_str!, and throughrelay_deploy.rs's ownr#"..."#remote templates — so bash executed the CR as a command andset -euo pipefailaborted on the spot. The repo had no.gitattributes.Fixed at both layers:
.gitattributespins LF for*.sh/*.rs/ Dockerfiles / compose / Caddyfile.to_unix_scriptnormalizes everything sent over SFTP orexecute_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
The Docker-install task elevates to root but keeps the SSH user's
HOME, so it left~/.bitfun/docker-configroot-owned. The next unprivileged deploy could not read its ownconfig.json, and the docker CLI then mis-dispatcheddocker build -tas a top-level-tflag — trading the ~1 minute binary path for a ~20 minute source rebuild.~/.bitfunback to the SSH user after a root install.bitfun_fix_docker_configrepairs ownership, or relocates to a per-uid dir.DOCKER_CONFIGto thesudobranches ofbitfun_run_deploy_sh— the other way root poisoned it.bitfun_resolve_docker_modewas the only previous caller, and it is skipped whenever the driver already resolved a non-direct mode — exactly thesgcase above.On the causal link: the
permission deniedis confirmed by the log and is unambiguously a bug. That it is what makes docker misparse-tis 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).preparingflag seeded bystart_taskstayed forever and the wizard reported "running" indefinitely with an empty log pane. The driver now claims<stem>.driver.pidbefore anything that can fail; a missing/dead driver past a grace window isfailed, while an alive one (an open sudo prompt) stays unbounded.release_download_picks_the_fastest_working_sourcewas already failing onmain— the harness hardcodedv0.2.13against a 0.2.14 crate, so everyEXPECT_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 -non the generated driver scripts (previously the only generated script with no syntax coverage) and the install body; ashell_joinround trip through a second shell; the dead-driver status case.New CI job
shell-scriptsrejects CRLF in shell/deploy assets andbash -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 insrc/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.shpath 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: