I'm new to Linux and Bash, and I wrote this script for a CI workflow that publishes my Bevy game as a WebAssembly build. It detects Arch-based systems or Ubuntu, updates the system, installs native dependencies, updates and configures Rust nightly with the wasm32 target, installs wasm-bindgen-cli if necessary, builds the project in release mode, and assembles a dist/ directory containing the web files, assets, and generated wasm-bindgen output.
The script currently works, but I'm concerned about edge cases and whether there are Bash, portability, or CI-specific problems I should fix. In particular, I'd appreciate feedback on error handling, quoting variables, package installation, Rust toolchain selection, and the deeply nested if statements used throughout the stages.
4 Answers
Quote every variable expansion unless you deliberately need word splitting. Several Stage 5 commands currently use unquoted paths, such as `mkdir -p $DIST_FOLDER`, `cp -a $WEB_FOLDER $DIST_FOLDER`, and the Rust target arguments. They happen to work with these values, but paths containing spaces or wildcard characters could behave unexpectedly. Prefer `mkdir -p -- "$DIST_FOLDER"` and quote the other expansions too.
There is also a typo in the error message `${$DIST_ASSETS_FOLDER}`; that is not valid parameter expansion. ShellCheck will catch this and many other issues. Redirect diagnostics to stderr rather than hiding them or sending them to standard output, and be cautious about redirecting all compiler output to `/dev/null` because it makes CI failures difficult to diagnose.
There are a few operational problems unrelated to Bash style. `apt-update` is not the normal Ubuntu command; it should generally be `apt-get update` or `apt update`, followed by an install command. In an automated environment, package commands may also need noninteractive settings. More importantly, updating the entire operating system during every build can make results less reproducible and may unexpectedly change dependencies. It is usually better to use a fixed CI image and only install the packages required by the build.
Likewise, installing the latest Rust nightly and the latest wasm-bindgen CLI on every run can produce mismatched or changing tool versions. Pin the toolchain and, ideally, the wasm-bindgen version to values known to work with the project. You probably do not need `rustup update` or to change the global default toolchain; invoking Cargo with `cargo +nightly ...` or using a project toolchain file is safer.
The biggest readability issue is the deeply nested error handling. You can run each command separately and use a short-circuit failure handler, for example: `some_command || { echo "operation failed" >&2; exit 1; }`. That keeps the main sequence flat and makes it much easier to see the actual build steps. A small `die()` function for errors would reduce repetition even further.
Also consider putting the whole script in a function or using `set -Eeuo pipefail` near the top, then handling expected failures explicitly. Test it with ShellCheck before relying on it in CI.
That makes sense. The nested structure was mainly me trying to print a different message for every possible failure, but a common error function would be much cleaner.
The script is a reasonable first Bash project, and the overall stages are understandable. The status messages could be moved into helper functions so the build logic is shorter, but don't optimize solely for fewer lines. Clear, boring code is more valuable than clever code.
For a deployment script, make sure it verifies that it is running from the expected project directory, creates or cleans `dist` deliberately, checks that the wasm file exists before calling wasm-bindgen, and fails loudly when a required command is missing. A CI job should also preserve the useful output from failed Cargo and wasm-bindgen commands instead of suppressing it.

ShellCheck is especially useful while learning Bash because it explains why an expansion or construct is risky instead of merely reporting that it is wrong.