From e2a4be18a9b81262897320a2e1b95c4c83d9cda7 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:56:21 +0000 Subject: [PATCH 1/2] ci(45): stop the new gates dying before they report what they found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #52 landed with both of its gates red on main, and each said only "Process completed with exit code 1". Two causes: * GitHub invokes bash with `-eo pipefail`. The commands these gates exist to watch fail — `cargo fmt --check` on a dirty tree, `cargo test` with a failing test, `diff` on a drifted fixture — so errexit killed the step before its own error handling ran. Every step now opens with `set +e`, handles its status explicitly, and ends in an explicit `exit`. * The detail only ever reached the run log. A shared `annotate` helper now re-emits the head of the failing output as a check annotation: the rustfmt diff, the `failures:` section of a test run, the fixture-currency diff, the shellcheck findings. A gate whose failure you cannot read is a gate you fix by guessing. The toolchain step also gains `components: clippy, rustfmt`; without rustfmt the drift diagnostic had nothing to run. This is the diagnostic half only — it does not fix whatever rustfmt and the suite are reporting. It makes them legible so the next commit can. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .github/workflows/launcher-artefacts.yml | 156 ++++++++++++++++------- 1 file changed, 108 insertions(+), 48 deletions(-) diff --git a/.github/workflows/launcher-artefacts.yml b/.github/workflows/launcher-artefacts.yml index 04e4b7f..6396f8f 100644 --- a/.github/workflows/launcher-artefacts.yml +++ b/.github/workflows/launcher-artefacts.yml @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MPL-2.0 -# Launcher artefact gates — the three checks that belong to THIS repo and -# cannot live in the estate's rust-ci-reusable.yml. +# Launcher artefact gates — the checks that belong to THIS repo and cannot live +# in the estate's rust-ci-reusable.yml. # # 1. test-count floor (#45 AC6). `cargo test` exits 0 on a suite that # collected nothing, so "the job is green" is not evidence any test ran. @@ -9,15 +9,27 @@ # `.tera` source is not valid shell, so linting the template file cannot # work — only a minted artefact can be linted. And a linter that finds # zero files to lint has failed, not passed, so that case exits non-zero. -# 3. fixture currency (#48 AC5, #49 AC6). The committed deed-dialect fixture -# must be byte-identical to what today's mint emits, so the artefacts the -# scanners read are the artefacts the tool actually produces. +# 3. fixture currency (#48 AC5, #49 AC6). The committed deed fixture must be +# byte-identical to what today's mint emits, so the artefacts the scanners +# read are the artefacts the tool actually produces. # # The committed PRE-phase-2 fixture # (minted-2026-09-22_stapeln-launcher.sh) is deliberately neither linted nor # currency-checked: it is a frozen historical artefact, the evidence that a # launcher minted by the old emitter still reads (#40). See # crates/launcher-common/tests/fixtures/metadata_block/README.adoc. +# +# ⚠ Two conventions every step below follows, both learned the hard way: +# +# * Each `run:` starts with `set +e`. GitHub invokes bash with `-eo pipefail`, +# so a command that is EXPECTED to be able to fail — `cargo fmt --check` on a +# dirty tree, `cargo test` with a failing test, `diff` on drifted files — +# kills the step before it can report what it found. The first version of this +# file died that way and reported only "Process completed with exit code 1". +# Each step handles its own status and ends in an explicit `exit`. +# * Each step sources `$RUNNER_TEMP/annotate.sh`. A failure whose detail lives +# only in the run log is a failure a reviewer cannot act on, and the log is +# not reachable from every environment; annotations are. name: Launcher artefacts on: @@ -57,46 +69,80 @@ jobs: uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # stable with: toolchain: stable + components: clippy, rustfmt - name: Cache cargo registry and build uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + - name: Install the annotation helper + shell: bash + run: | + set +e + cat > "$RUNNER_TEMP/annotate.sh" <<'HELPER' + # annotate TITLE FILE [MAXLINES] + # Re-emit the head of FILE as a single check annotation, percent-encoded + # so GitHub renders the line breaks. Titles must not contain "::". + annotate() { + local title="$1" file="$2" max="${3:-60}" body + body=$(awk -v max="$max" ' + NR <= max { + gsub(/%/, "%25"); gsub(/\r/, "%0D") + printf "%s%s", sep, $0; sep = "%0A" + } + END { if (NR > max) printf "%s… %d more line(s) in the run log", sep, NR - max } + ' "$file") + echo "::error title=${title}::${body}" + } + HELPER + # shellcheck disable=SC1091 + . "$RUNNER_TEMP/annotate.sh" + type annotate >/dev/null && echo "✓ annotate helper installed" + exit 0 + - name: Report rustfmt drift as an annotation shell: bash run: | + set +e set -uo pipefail - # Diagnostic, not a second gate: `cargo fmt --all -- --check` is gated - # by the estate reusable (rust-ci.yml), and this step never fails the - # job. Its output is a DIFF, and a diff is only actionable where a - # reviewer reads it — the run log is not that place — so it is - # re-emitted as a check annotation. + . "$RUNNER_TEMP/annotate.sh" + # Diagnostic, not a second gate: formatting is gated by the estate + # reusable (rust-ci.yml) and this step never fails the job. cargo fmt --all -- --check > "$RUNNER_TEMP/fmt.diff" 2>&1 if [ -s "$RUNNER_TEMP/fmt.diff" ]; then - echo "rustfmt wants $(wc -l < "$RUNNER_TEMP/fmt.diff") line(s) changed" - awk 'NR <= 80 { gsub(/%/, "%25"); gsub(/\r/, "%0D"); printf "%s%s", sep, $0; sep = "%0A" } - END { if (NR > 80) printf "%s… %d more line(s) in the run log", sep, NR - 80 }' \ - "$RUNNER_TEMP/fmt.diff" > "$RUNNER_TEMP/fmt.ann" - echo "::error title=cargo fmt --check drift::$(cat "$RUNNER_TEMP/fmt.ann")" + echo "rustfmt wants changes ($(wc -l < "$RUNNER_TEMP/fmt.diff") line(s)):" + cat "$RUNNER_TEMP/fmt.diff" + annotate "cargo fmt --check drift" "$RUNNER_TEMP/fmt.diff" 80 else echo "✓ cargo fmt --all -- --check is clean" fi exit 0 - name: Build every target - run: cargo build --locked --all-targets + shell: bash + run: | + set +e + . "$RUNNER_TEMP/annotate.sh" + cargo build --locked --all-targets 2>&1 | tee "$RUNNER_TEMP/build.log" + STATUS="${PIPESTATUS[0]}" + if [ "$STATUS" -ne 0 ]; then + annotate "cargo build --all-targets failed" "$RUNNER_TEMP/build.log" 60 + fi + exit "$STATUS" - name: Run the suite and count what actually ran shell: bash run: | + set +e set -uo pipefail + . "$RUNNER_TEMP/annotate.sh" LOG="$RUNNER_TEMP/cargo-test.log" cargo test --locked --all 2>&1 | tee "$LOG" STATUS="${PIPESTATUS[0]}" - set -e # Sum the `N passed` of every `test result:` line the run printed — - # one per test binary, so a suite that silently stopped collecting - # in one crate still shows up in the total. + # one line per test binary, so a crate that silently stopped + # collecting shows up as a shortfall in the total rather than as a + # smaller number of lines. read -r PASSED FAILED < <(awk ' /^test result:/ { for (i = 1; i <= NF; i++) { @@ -112,30 +158,32 @@ jobs: { echo "## Launcher artefact gates" echo "" - echo "- \`cargo test --all\`: **$PASSED passed**, $FAILED failed (floor $TEST_COUNT_FLOOR)" + echo "- cargo test --all: **$PASSED passed**, $FAILED failed (floor $TEST_COUNT_FLOOR)" } >> "$GITHUB_STEP_SUMMARY" if [ "$STATUS" -ne 0 ]; then - sed -n '/^failures:$/,$p' "$LOG" | head -100 > "$RUNNER_TEMP/failures.txt" - awk '{ gsub(/%/, "%25"); gsub(/\r/, "%0D"); printf "%s%s", sep, $0; sep = "%0A" }' \ - "$RUNNER_TEMP/failures.txt" > "$RUNNER_TEMP/failures.ann" - echo "::error title=cargo test exited $STATUS::$(cat "$RUNNER_TEMP/failures.ann")" - exit 1 + sed -n '/^failures:$/,$p' "$LOG" | head -120 > "$RUNNER_TEMP/failures.txt" + if [ ! -s "$RUNNER_TEMP/failures.txt" ]; then + tail -60 "$LOG" > "$RUNNER_TEMP/failures.txt" + fi + annotate "cargo test failed" "$RUNNER_TEMP/failures.txt" 100 + exit "$STATUS" fi if [ "$FAILED" -ne 0 ]; then echo "::error::$FAILED test(s) failed" exit 1 fi if [ "$PASSED" -lt "$TEST_COUNT_FLOOR" ]; then - echo "::error::only $PASSED tests ran, floor is $TEST_COUNT_FLOOR — a suite that \ - collects nothing exits 0, so this gate is what makes 'green' mean 'ran' (#45 AC6)" + echo "::error title=test-count floor::only $PASSED tests ran; the floor is $TEST_COUNT_FLOOR. A suite that collects nothing exits 0, so this gate is what makes green mean ran (#45 AC6)." exit 1 fi + exit 0 - name: Mint a launcher from the committed fixture config shell: bash run: | - set -euo pipefail + set +e + set -uo pipefail OUT="$RUNNER_TEMP/minted" mkdir -p "$OUT" # `--stdout` so nothing is written next to the fixture config. @@ -145,30 +193,40 @@ jobs: ./target/debug/launch-scaffolder mint "$FIXTURE_CONFIG" --stdout \ | sed -E 's|^CONFIG_FILE=.*|CONFIG_FILE=""|' \ > "$OUT/stapeln-launcher.sh" + STATUS="${PIPESTATUS[0]}" + if [ "$STATUS" -ne 0 ]; then + echo "::error title=mint failed::launch-scaffolder mint exited $STATUS" + exit "$STATUS" + fi chmod +x "$OUT/stapeln-launcher.sh" - echo "minted $(wc -l < "$OUT/stapeln-launcher.sh") lines to $OUT/stapeln-launcher.sh" + echo "minted $(wc -l < "$OUT/stapeln-launcher.sh") lines" + exit 0 - name: Committed fixture is byte-identical to a fresh mint shell: bash run: | - set -euo pipefail + set +e + set -uo pipefail + . "$RUNNER_TEMP/annotate.sh" MINTED="$RUNNER_TEMP/minted/stapeln-launcher.sh" - if diff -u "$FIXTURE_SCRIPT" "$MINTED" > "$RUNNER_TEMP/fixture.diff"; then + diff -u "$FIXTURE_SCRIPT" "$MINTED" > "$RUNNER_TEMP/fixture.diff" + STATUS=$? + if [ "$STATUS" -eq 0 ]; then echo "✓ $FIXTURE_SCRIPT is byte-identical to a fresh mint" - else - cat "$RUNNER_TEMP/fixture.diff" - awk 'NR <= 80 { gsub(/%/, "%25"); gsub(/\r/, "%0D"); printf "%s%s", sep, $0; sep = "%0A" }' \ - "$RUNNER_TEMP/fixture.diff" > "$RUNNER_TEMP/fixture.ann" - echo "::error title=$FIXTURE_SCRIPT has drifted from what mint emits today::$(cat "$RUNNER_TEMP/fixture.ann")" - echo "::notice::re-mint it rather than editing it by hand — see \ - crates/launcher-common/tests/fixtures/metadata_block/README.adoc" - exit 1 + exit 0 fi + cat "$RUNNER_TEMP/fixture.diff" + annotate "committed fixture drifted from what mint emits today" \ + "$RUNNER_TEMP/fixture.diff" 80 + echo "::notice::re-mint it rather than editing it by hand — see crates/launcher-common/tests/fixtures/metadata_block/README.adoc" + exit 1 - name: Shellcheck the minted launcher shell: bash run: | - set -euo pipefail + set +e + set -uo pipefail + . "$RUNNER_TEMP/annotate.sh" command -v shellcheck >/dev/null || { echo "::error::shellcheck is not installed on the runner"; exit 1; } shellcheck --version | sed -n '2,3p' @@ -177,29 +235,30 @@ jobs: # #49 AC5: zero files is a failure, not a pass. if [ "${#FILES[@]}" -eq 0 ]; then - echo "::error::the shellcheck gate found ZERO files to lint — a linter \ - with nothing to lint has failed, not passed" + echo "::error title=nothing to lint::the shellcheck gate found ZERO files to lint — a linter with nothing to lint has failed, not passed" exit 1 fi printf 'linting %d minted artefact(s):\n' "${#FILES[@]}" printf ' %s\n' "${FILES[@]}" # Hard gate: nothing at warning or above (#49 AC2). - if ! shellcheck --format=gcc --severity=warning "${FILES[@]}" > "$RUNNER_TEMP/sc-warning.txt"; then + shellcheck --format=gcc --severity=warning "${FILES[@]}" \ + > "$RUNNER_TEMP/sc-warning.txt" 2>&1 + if [ -s "$RUNNER_TEMP/sc-warning.txt" ]; then cat "$RUNNER_TEMP/sc-warning.txt" - echo "::error::shellcheck found warning-or-above findings in a freshly \ - minted launcher; the fix belongs in templates/launcher.sh.tera, never in \ - the committed fixtures" + annotate "shellcheck findings in a freshly minted launcher" \ + "$RUNNER_TEMP/sc-warning.txt" 60 + echo "::notice::the fix belongs in templates/launcher.sh.tera, never in the committed fixtures" exit 1 fi # Pinned gate: the three codes #49 fixed must stay fixed at ANY # severity, so a reclassification cannot hide a regression. - shellcheck --format=gcc "${FILES[@]}" > "$RUNNER_TEMP/sc-all.txt" || true + shellcheck --format=gcc "${FILES[@]}" > "$RUNNER_TEMP/sc-all.txt" 2>&1 for code in $BANNED_SHELLCHECK_CODES; do if grep -q "\[$code\]" "$RUNNER_TEMP/sc-all.txt"; then grep "\[$code\]" "$RUNNER_TEMP/sc-all.txt" - echo "::error::$code is back in a freshly minted launcher (#49)" + echo "::error title=$code is back::a freshly minted launcher trips $code again (#49)" exit 1 fi done @@ -209,3 +268,4 @@ jobs: echo "--- remaining sub-warning notes, for the record ---" cat "$RUNNER_TEMP/sc-all.txt" fi + exit 0 From 05a0dbf654c26cdd79a6dbdd4f24238c20aa7cb7 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 25 Sep 2026 19:03:35 +0000 Subject: [PATCH 2/2] fix(49): the three shellcheck findings, in the template rather than the fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SC2034, SC2001 and SC2155 were reported against the committed fixture, but the fixture is a faithful record of what `mint` writes, so all three are properties of `templates/launcher.sh.tera` and reached every launcher in the estate. Fixed at the source; the fixture was then re-minted, and is now clean at EVERY shellcheck severity, not only at warning and above. SC2034 — `APP_PORT` unused. Answered by deciding which of the two is true rather than by disabling the lint, because the lint was pointing at a real duplicate: `URL` hardcoded the port on the line above and `APP_PORT` restated it, so a config carrying both could put two different ports in one script and nothing would notice. The port now has ONE spelling. No explicit `[runtime].url` → `APP_PORT` is emitted and `URL` is composed from it, so they cannot disagree. Explicit URL → the URL is the whole answer and `APP_PORT` is not emitted at all. `the_port_has_exactly_one_spelling_in_the_generated_script` pins both arms, and `render` now warns at mint time when a config sets a URL and a port that contradict each other — the disagreement the duplicate used to hide. SC2001 — `echo "$body" | sed 's/^/ /'` becomes `printf ' %s\n' "${body//$'\n'/$'\n '}"`: the same two-space indent on every line including empty ones, with no subprocess per call. Fixed rather than carrying an inline justification. SC2155 — `local script_path="$(cd … && pwd)/$(basename …)"` takes its exit status from `local`, so a failed `cd` was swallowed and the next line copied the script to `$LAUNCHER_TARGET` from a path assembled out of nothing. Declared and assigned separately, with the failure now reported. Of the three this is the one that masked a real error. Also reverts the previous commit's `app_license` collapse. #45 AC2 read the pre-existing `cargo fmt --check` drift as rustfmt wanting that call on one line; measured against the CI annotation it wants the opposite — the arguments exceed `fn_call_width` (60), so the five-line form on main was already clean and the collapse introduced the drift. The gate's own diagnostic is what settled it. Non-vacuity, unsolicited: the shellcheck gate added in #52 caught SC2034 and SC2155 in a launcher minted during the run BEFORE this commit existed, which is the evidence #49 AC4 asks for — it fails at the point a template regression is introduced, not at the point someone remembers to lint. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- crates/launcher-common/src/template.rs | 63 ++++++++++++++++++- ...minted-2026-09-23_stapeln-launcher-deed.sh | 18 +++++- crates/launcher-common/tests/round_trip.rs | 5 +- templates/launcher.sh.tera | 34 ++++++++-- 4 files changed, 109 insertions(+), 11 deletions(-) diff --git a/crates/launcher-common/src/template.rs b/crates/launcher-common/src/template.rs index 124bb68..280c79a 100644 --- a/crates/launcher-common/src/template.rs +++ b/crates/launcher-common/src/template.rs @@ -73,7 +73,10 @@ pub fn render( "app_version", config.project.version.as_deref().unwrap_or("1.0.0"), ); - ctx.insert("app_license", config.project.license.as_deref().unwrap_or("MPL-2.0")); + ctx.insert( + "app_license", + config.project.license.as_deref().unwrap_or("MPL-2.0"), + ); // --- [repo] -------------------------------------------------------- ctx.insert("repo_dir", &config.repo.path); @@ -88,6 +91,13 @@ pub fn render( ctx.insert("has_url", &(config.runtime.url.is_some())); // Default URL/port fallbacks so Tera `{{ url }}` never explodes. + // + // The port gets ONE answer in the generated script. With no explicit + // `[runtime].url` the template emits `APP_PORT` and composes the URL from + // it; with one, it emits the URL and no `APP_PORT` at all, because a second + // spelling of the port could only disagree with the URL the launcher + // actually dials (#49 AC3 — shellcheck saw the unused variable, the defect + // was the duplicate). let port = config.runtime.port.unwrap_or(0); ctx.insert("app_port", &port); let url_string = match (&config.runtime.url, config.runtime.port) { @@ -95,6 +105,21 @@ pub fn render( (None, Some(p)) => format!("http://localhost:{p}"), (None, None) => String::new(), }; + // A config that sets both and has them disagree is almost certainly a + // mistake, and the generated script no longer carries the second value that + // would once have shown it — so say so at mint time instead. + let disagreement = config + .runtime + .url + .as_deref() + .zip(config.runtime.port) + .filter(|(url, p)| !url.contains(&format!(":{p}"))); + if let Some((url, p)) = disagreement { + tracing::warn!( + "[runtime].url = {url} does not carry [runtime].port = {p}; the URL wins, so \ + the port is not emitted into the launcher separately" + ); + } ctx.insert("url", &url_string); // PID / log file defaults follow the standard's pattern when unset. @@ -470,4 +495,40 @@ mod tests { "shebang must be on line 1 of the rendered launcher" ); } + + /// The port has exactly one spelling in the generated script (#49 AC3). + /// + /// `APP_PORT` used to be emitted into every `server-url` launcher and read + /// by none of them — shellcheck SC2034 — while the `URL` line above it + /// hardcoded the same port. The unused variable was the symptom; two answers + /// to one question was the defect, and the two could silently disagree. + #[test] + fn the_port_has_exactly_one_spelling_in_the_generated_script() { + let std_ = LauncherStandard::baked().expect("baked standard should parse"); + + // No explicit URL, so APP_PORT is emitted — because something reads it: + // the very next line composes the URL out of it. + let composed = stapeln_config(); + assert_eq!(composed.runtime.url, None, "fixture config sets no url"); + let script = render(&composed, &std_, None).expect("renders"); + assert!( + script.contains("APP_PORT=\"4010\""), + "the composed arm must state the port it composes from" + ); + assert!( + script.contains("URL=\"http://localhost:${APP_PORT}\""), + "and must compose the URL from it, so the two cannot disagree" + ); + + // An explicit URL is the whole answer: the port is inside it, and a + // second `APP_PORT=` beside it could only contradict it. + let mut explicit = stapeln_config(); + explicit.runtime.url = Some("http://localhost:4010".into()); + let script = render(&explicit, &std_, None).expect("renders"); + assert!( + !script.contains("APP_PORT="), + "a launcher with an explicit [runtime].url must not also state the port" + ); + assert!(script.contains("URL=\"http://localhost:4010\"")); + } } diff --git a/crates/launcher-common/tests/fixtures/metadata_block/minted-2026-09-23_stapeln-launcher-deed.sh b/crates/launcher-common/tests/fixtures/metadata_block/minted-2026-09-23_stapeln-launcher-deed.sh index 67f62df..8d94a8c 100644 --- a/crates/launcher-common/tests/fixtures/metadata_block/minted-2026-09-23_stapeln-launcher-deed.sh +++ b/crates/launcher-common/tests/fixtures/metadata_block/minted-2026-09-23_stapeln-launcher-deed.sh @@ -50,7 +50,6 @@ ICON_SOURCE="" CONFIG_FILE="" URL="http://localhost:4010" -APP_PORT="0" WAIT_SECONDS="15" PID_FILE="/tmp/stapeln-server.pid" @@ -78,7 +77,10 @@ gui_error() { local title="$1" local body="$2" err "$title" - echo "$body" | sed 's/^/ /' >&2 + # ${body//…} rather than `echo "$body" | sed 's/^/ /'` (shellcheck SC2001): + # the same two-space indent on every line, including empty ones, with no + # subprocess per call. + printf ' %s\n' "${body//$'\n'/$'\n '}" >&2 if is_gui_context; then if command -v kdialog >/dev/null 2>&1; then kdialog --title "$APP_DISPLAY: $title" --error "$body" 2>/dev/null & elif command -v zenity >/dev/null 2>&1; then zenity --error --title="$APP_DISPLAY: $title" --text="$body" --width=500 2>/dev/null & @@ -299,7 +301,17 @@ EOF do_integ_linux() { mkdir -p "$APPS_DIR" "$ICON_DIR" "$BIN_DIR" "$DESKTOP_SHORTCUT_DIR" - local script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")" + # Declared and assigned separately (shellcheck SC2155). `local x="$(cmd)"` + # takes its exit status from `local`, so a failed `cd` was swallowed and the + # next line copied this script to $LAUNCHER_TARGET from a path assembled out + # of nothing — the one finding of the three with real failure-masking. + local script_dir + local script_path + if ! script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; then + err "cannot resolve this script's own directory" + return 1 + fi + script_path="$script_dir/$(basename "${BASH_SOURCE[0]}")" cp "$script_path" "$LAUNCHER_TARGET" chmod +x "$LAUNCHER_TARGET" log " + launcher: $LAUNCHER_TARGET" diff --git a/crates/launcher-common/tests/round_trip.rs b/crates/launcher-common/tests/round_trip.rs index 6c356b2..9090232 100644 --- a/crates/launcher-common/tests/round_trip.rs +++ b/crates/launcher-common/tests/round_trip.rs @@ -319,7 +319,10 @@ fn first_difference(fixture: &str, minted: &str) -> String { let a = want.get(i).copied().unwrap_or(""); let b = got.get(i).copied().unwrap_or(""); if a != b { - out.push_str(&format!(" line {n}\n fixture {a:?}\n minted {b:?}\n", n = i + 1)); + out.push_str(&format!( + " line {n}\n fixture {a:?}\n minted {b:?}\n", + n = i + 1 + )); shown += 1; if shown == 20 { out.push_str(" … (further differences omitted)\n"); diff --git a/templates/launcher.sh.tera b/templates/launcher.sh.tera index df68107..65e17ed 100644 --- a/templates/launcher.sh.tera +++ b/templates/launcher.sh.tera @@ -73,14 +73,23 @@ ICON_SOURCE="{{ icon_source }}" # the Rust implementation instead of running the shell fallback. CONFIG_FILE="{{ config_file }}" -{% if runtime_kind == "server-url" or runtime_kind == "remote" -%} +{% if runtime_kind == "server-url" -%} +{% if has_url -%} URL="{{ url }}" {%- else -%} -URL="" -{%- endif %} -{% if runtime_kind == "server-url" -%} +# No explicit [runtime].url in the config, so the URL is composed from the port +# at run time. APP_PORT is emitted ONLY here, where something reads it: the port +# used to be stated twice — once inside a hardcoded URL, once in an APP_PORT no +# line of this script ever read (shellcheck SC2034) — and the two could silently +# disagree. One answer to "which port" cannot (#49 AC3). APP_PORT="{{ app_port }}" +URL="http://localhost:${APP_PORT}" +{%- endif %} WAIT_SECONDS="{{ wait_seconds }}" +{%- elif runtime_kind == "remote" -%} +URL="{{ url }}" +{%- else -%} +URL="" {%- endif %} PID_FILE="{{ pid_file }}" @@ -126,7 +135,10 @@ gui_error() { local title="$1" local body="$2" err "$title" - echo "$body" | sed 's/^/ /' >&2 + # ${body//…} rather than `echo "$body" | sed 's/^/ /'` (shellcheck SC2001): + # the same two-space indent on every line, including empty ones, with no + # subprocess per call. + printf ' %s\n' "${body//$'\n'/$'\n '}" >&2 if is_gui_context; then if command -v kdialog >/dev/null 2>&1; then kdialog --title "$APP_DISPLAY: $title" --error "$body" 2>/dev/null & elif command -v zenity >/dev/null 2>&1; then zenity --error --title="$APP_DISPLAY: $title" --text="$body" --width=500 2>/dev/null & @@ -380,7 +392,17 @@ EOF do_integ_linux() { mkdir -p "$APPS_DIR" "$ICON_DIR" "$BIN_DIR" "$DESKTOP_SHORTCUT_DIR" - local script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")" + # Declared and assigned separately (shellcheck SC2155). `local x="$(cmd)"` + # takes its exit status from `local`, so a failed `cd` was swallowed and the + # next line copied this script to $LAUNCHER_TARGET from a path assembled out + # of nothing — the one finding of the three with real failure-masking. + local script_dir + local script_path + if ! script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; then + err "cannot resolve this script's own directory" + return 1 + fi + script_path="$script_dir/$(basename "${BASH_SOURCE[0]}")" cp "$script_path" "$LAUNCHER_TARGET" chmod +x "$LAUNCHER_TARGET" log " + launcher: $LAUNCHER_TARGET"