Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#**
# Line-ending policy.
#
# Git for Windows defaults to `core.autocrlf=true`, which rewrites every text
# file to CRLF on checkout. Two BitFun paths break silently when that happens:
#
# 1. `src/apps/relay-server/*.sh` is embedded into the Desktop binary with
# `include_str!` and uploaded verbatim to the relay host. A CR turns each
# blank line into `$'\r': command not found`, and `set -e` aborts the
# one-click deploy on the first one.
# 2. Rust raw string literals (`r#"..."#`) that hold remote bash carry the
# checkout's CRLF into the generated scripts the same way.
#
# Force LF for everything that has to run on a POSIX host, regardless of where
# it was checked out. Windows-only scripts (`*.ps1`) are left alone — PowerShell
# reads LF fine and pinning them would churn every non-Windows working tree.
#**

*.sh text eol=lf
*.bash text eol=lf
*.rs text eol=lf
*.py text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf

Dockerfile text eol=lf
Dockerfile.* text eol=lf
*.Dockerfile text eol=lf
.dockerignore text eol=lf
docker-compose.yml text eol=lf
docker-compose.*.yml text eol=lf
Caddyfile text eol=lf
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,38 @@ permissions:
contents: read

jobs:
# ── Shell deploy assets: LF endings + syntax ───────────────────────
# Relay one-click deploy embeds these scripts into the Desktop binary and
# uploads them verbatim to a Linux host, where a single CR aborts the deploy
# with `$'\r': command not found`. .gitattributes pins LF; this is the guard.
shell-scripts:
name: Shell Deploy Scripts
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v5

- name: Reject CRLF in shell and deploy assets
run: |
bad=$(git ls-files -z \
'*.sh' '*.bash' 'Dockerfile' 'Dockerfile.*' '*.Dockerfile' 'Caddyfile' \
'docker-compose.yml' 'docker-compose.*.yml' \
| xargs -0 -r grep -lU $'\r' || true)
if [ -n "$bad" ]; then
echo "::error::CRLF line endings found; these must stay LF (see .gitattributes):"
echo "$bad"
exit 1
fi
echo "All shell and deploy assets are LF-only."

- name: bash -n every tracked shell script
run: |
rc=0
while IFS= read -r -d '' f; do
bash -n "$f" || { echo "::error file=$f::bash syntax error"; rc=1; }
done < <(git ls-files -z '*.sh' '*.bash')
exit "$rc"

# ── CLI: independent tests ─────────────────────────────────────────
cli-test:
name: CLI Tests (${{ matrix.os }})
Expand Down
11 changes: 10 additions & 1 deletion scripts/relay/release-download-harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,16 @@ run_case() {
fi
}

GITHUB_URL="https://github.com/GCWing/BitFun/releases/download/v0.2.13/${RELAY_ASSET}"
# Read the tag out of the script under test rather than hardcoding one: Desktop
# pins BITFUN_RELEASE_TAG to its own crate version, so a literal here silently
# rots at every release bump and every `EXPECT_SOURCE="$GITHUB_URL"` case fails.
RELEASE_TAG="$(sed -n 's/^export BITFUN_RELEASE_TAG="\(.*\)"$/\1/p' "$SCRIPT_UNDER_TEST" | head -n1)"
RELEASE_TAG="${RELEASE_TAG:-latest}"
if [ "$RELEASE_TAG" = "latest" ]; then
GITHUB_URL="https://github.com/GCWing/BitFun/releases/latest/download/${RELAY_ASSET}"
else
GITHUB_URL="https://github.com/GCWing/BitFun/releases/download/${RELEASE_TAG}/${RELAY_ASSET}"
fi

# The reported case: GitHub is reachable but crawling, the mirror is fast.
# Ranking must send the download to the mirror instead of crawling for an hour.
Expand Down
14 changes: 14 additions & 0 deletions src/apps/relay-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,20 @@ bash deploy.sh
`deploy.sh` must run **on the target server** (it does not SSH elsewhere).
Requires Docker and Docker Compose on **linux/amd64** or **linux/arm64**.

Clone on the server, as above, rather than uploading a Windows checkout. Git for
Windows rewrites these scripts to CRLF by default, and bash then fails on the
first blank line:

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

If that happens, strip the CR and re-run:

```bash
sed -i 's/\r$//' *.sh && bash deploy.sh
```

After a successful start, the script runs `relay-admin list-users`. If the
database has **no accounts**, it prints the exact `add-user` command to run
next (account login will not work until you create at least one user).
Expand Down
50 changes: 46 additions & 4 deletions src/apps/relay-server/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ resolve_compose() {
exit 1
}

# POSIX single-quote each argument. `sg -c` takes a single string that the shell
# re-parses, so an unquoted "$*" loses argument boundaries — a path with a space
# or a `-f '{{.State.Running}}'` format string arrives mangled.
shell_join() {
local out="" arg
for arg in "$@"; do
out="$out'$(printf '%s' "$arg" | sed "s/'/'\\\\''/g")' "
done
printf '%s' "$out"
}

compose() {
if [ "${#COMPOSE[@]}" -eq 0 ]; then
resolve_compose
Expand All @@ -47,7 +58,7 @@ compose() {
;;
sg)
if sg docker -c 'docker compose version' >/dev/null 2>&1; then
sg docker -c "docker compose $*"
sg docker -c "$(shell_join docker compose "$@")"
return
fi
;;
Expand All @@ -57,10 +68,41 @@ compose() {

# Resolve how to talk to the Docker daemon for the current shell.
# Sets BITFUN_DOCKER_MODE to: direct | sg | sudo
resolve_docker_access() {
check_command docker
# Make DOCKER_CONFIG usable by the current user.
#
# A root-run Docker install (or an earlier `sudo -E docker`) can leave
# ~/.bitfun/docker-config and its config.json owned by root, and every later
# unprivileged docker call then reports
# WARNING: Error loading config file: .../config.json: permission denied
# before misbehaving. Repair it, or fall back to a per-uid dir we can read.
fix_docker_config() {
export DOCKER_CONFIG="${DOCKER_CONFIG:-$HOME/.bitfun/docker-config}"
mkdir -p "$DOCKER_CONFIG" 2>/dev/null || true
docker_config_usable() {
[ -r "$DOCKER_CONFIG" ] && [ -w "$DOCKER_CONFIG" ] &&
{ [ ! -e "$DOCKER_CONFIG/config.json" ] || [ -r "$DOCKER_CONFIG/config.json" ]; }
}
if [ "$(id -u)" = "0" ] || docker_config_usable; then
chmod 700 "$DOCKER_CONFIG" 2>/dev/null || true
return 0
fi
echo "Warning: $DOCKER_CONFIG is not usable by $(id -un) (root-owned by an earlier install)."
if [ "$(id -u)" != "0" ]; then
sudo -n chown -R "$(id -un):$(id -gn)" "$DOCKER_CONFIG" 2>/dev/null ||
sudo chown -R "$(id -un):$(id -gn)" "$DOCKER_CONFIG" 2>/dev/null || true
fi
if ! docker_config_usable; then
DOCKER_CONFIG="$HOME/.bitfun/docker-config-$(id -u)"
export DOCKER_CONFIG
mkdir -p "$DOCKER_CONFIG"
echo " Using DOCKER_CONFIG=$DOCKER_CONFIG instead."
fi
chmod 700 "$DOCKER_CONFIG" 2>/dev/null || true
}

resolve_docker_access() {
check_command docker
fix_docker_config

if [ -e "$HOME/.docker" ] && [ ! -w "$HOME/.docker" ]; then
echo "Warning: $HOME/.docker is not writable (often root-owned after sudo docker)."
Expand Down Expand Up @@ -102,7 +144,7 @@ resolve_docker_access() {

docker_cmd() {
case "${BITFUN_DOCKER_MODE:-direct}" in
sg) sg docker -c "docker $*" ;;
sg) sg docker -c "$(shell_join docker "$@")" ;;
sudo) sudo docker "$@" ;;
*) docker "$@" ;;
esac
Expand Down
75 changes: 73 additions & 2 deletions src/apps/relay-server/release-download.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,22 @@ BITFUN_STALL_SECONDS="${BITFUN_STALL_SECONDS:-30}"
# Docker invocation. relay_deploy.rs and common.sh each define their own
# privilege-aware wrapper before sourcing this file; fall back to a compatible
# one so the file also works standalone.
if ! declare -F bitfun_shell_join >/dev/null 2>&1; then
# `sg -c` re-parses a single string, so an unquoted "$*" loses argument
# boundaries. Single-quote each argument (POSIX-safe for any /bin/sh).
bitfun_shell_join() {
local out="" arg
for arg in "$@"; do
out="$out'$(printf '%s' "$arg" | sed "s/'/'\\\\''/g")' "
done
printf '%s' "$out"
}
fi

if ! declare -F bitfun_docker >/dev/null 2>&1; then
bitfun_docker() {
case "${BITFUN_DOCKER_MODE:-direct}" in
sg) sg docker -c "docker $*" ;;
sg) sg docker -c "$(bitfun_shell_join docker "$@")" ;;
sudo)
if sudo -n true >/dev/null 2>&1; then sudo -n docker "$@"; else sudo docker "$@"; fi
;;
Expand Down Expand Up @@ -100,6 +112,65 @@ bitfun_canonical_checksum_url() {
printf '%s.sha256\n' "$url"
}

# Build the runtime image around the published binary.
#
# Losing this build costs ~20 minutes: the caller falls back to compiling the
# relay from source. Two failure modes are recoverable and worth retrying rather
# than surrendering to that, both observed on real hosts:
#
# - DOCKER_CONFIG holds a root-owned config.json from an earlier elevated run.
# The CLI prints `WARNING: Error loading config file: ... permission denied`
# and then mis-dispatches the build (`unknown shorthand flag: 't' in -t`).
# - BuildKit is requested through inherited DOCKER_BUILDKIT=1 but buildx is
# missing or broken. This image is `FROM debian` + `COPY`, so it needs none
# of BuildKit's cache mounts and the classic builder does just as well.
#
# Each attempt runs in a subshell so its env override cannot leak into the
# source-build path that follows.
bitfun_build_runtime_image() {
local image="$1" context="$2" rc=1

# A config dir this user definitely owns. Empty if it cannot be created, in
# which case the retries keep the inherited DOCKER_CONFIG.
local clean_config="$context.docker-config"
rm -rf "$clean_config"
if ! mkdir -p "$clean_config" 2>/dev/null; then
clean_config=""
fi

local attempt
for attempt in inherited clean-config classic-builder; do
case "$attempt" in
clean-config)
if [ -z "$clean_config" ]; then continue; fi
echo ">>> Retrying the runtime image build with a clean Docker config..."
;;
classic-builder)
echo ">>> Retrying the runtime image build with the classic builder..."
;;
esac
# Subshell: the env overrides must not leak into the source-build path.
if (
case "$attempt" in
clean-config) export DOCKER_CONFIG="$clean_config" ;;
classic-builder)
if [ -n "$clean_config" ]; then export DOCKER_CONFIG="$clean_config"; fi
export DOCKER_BUILDKIT=0
;;
esac
bitfun_docker build -t "$image" "$context"
); then
rc=0
break
fi
done

if [ -n "$clean_config" ]; then
rm -rf "$clean_config"
fi
return "$rc"
}

bitfun_try_release_deploy() {
local release_dir="$HOME/.bitfun/relay-release"
local target archive upstream_url download_dir extracted context image expected_hash
Expand Down Expand Up @@ -340,7 +411,7 @@ DOCKERFILE

image="bitfun-relay:release-${BITFUN_RELEASE_TAG}"
echo ">>> Building lightweight Relay runtime image (no Rust/Cargo compilation)..."
if ! bitfun_docker build -t "$image" "$context"; then
if ! bitfun_build_runtime_image "$image" "$context"; then
echo ">>> Published binary image build failed; falling back to source build."
return 1
fi
Expand Down
Loading