From fe2628f3e2472e4ae26ce2196f67b99d091e3cea Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:13:10 +0100 Subject: [PATCH] =?UTF-8?q?fix(template):=20put=20the=20shebang=20on=20lin?= =?UTF-8?q?e=201=20=E2=80=94=20at=20the=20generator,=20not=20in=2020=20out?= =?UTF-8?q?puts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every launcher this generator emits begins with a BLANK LINE, then the shebang. That is shellcheck SC2148 and a script the kernel will not dispatch by shebang. Verified on origin across four repos (berrywiki, stapeln, gossamer, paint-type): line 1 of each `-launcher.sh` is empty. Cause: `templates/launcher.sh.tera` opened with a Tera comment block and only then the shebang, so the rendered output carried a leading newline. WHY THIS BELONGS HERE. The estate currently has ~20 open one-line PRs, each deleting that blank line in one generated launcher. Codacy said so on nearly every one of them - "the file is explicitly marked as auto-generated by launch-scaffolder; manual edits will be lost during the next `launch-scaffolder realign`" - and stapeln#38 quantified the gap: 20 files identified, 1 fixed. Merging twenty downstream fixes leaves the machine that emits the defect untouched, and the next realign overwrites all of them. THE FIRST ATTEMPT AT THIS FIX WAS WRONG, AND THE TEST CAUGHT IT. Moving the shebang above a `{#- ... -#}` block rendered as: #!/usr/bin/env bash# SPDX-License-Identifier: MPL-2.0 - concatenated, because `{#-` strips whitespace BEFORE the tag and ate the newline after the shebang. A test asserting only on the template SOURCE would have passed that happily and shipped a worse file than the one it replaced. The correct pairing is `{#` (do not strip before) with `-#}` (strip after). Two regression tests, because the property that matters is the RENDERED output: * template_source_opens_with_shebang - the template invariant * rendered_launcher_starts_with_shebang_on_line_one - renders through Tera and asserts line 1, which is the assertion that caught the bad fix Also repairs a pre-existing failure that was red on origin/main before this branch: standard::tests::baked_standard_parses asserted spec_version == "0.1.0" against a standard declaring 0.2.0. It failed for a reason unrelated to parsing, which is what it is named for. The version is now DERIVED from the baked source rather than hardcoded, so it cannot go stale again. Full workspace suite: 19 passed, 0 failed (baseline on origin/main was 16 passed, 1 failed). Follow-up, not done here: the ~20 already-generated launchers still carry the blank line until a `realign` regenerates them. The open per-repo PRs remain valid as immediate relief; this stops the defect being reintroduced. Co-Authored-By: Claude Opus 5 --- crates/launcher-common/src/standard.rs | 14 ++++- crates/launcher-common/src/template.rs | 76 ++++++++++++++++++++++++++ templates/launcher.sh.tera | 11 +++- 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/crates/launcher-common/src/standard.rs b/crates/launcher-common/src/standard.rs index 56cfa3f..53d813d 100644 --- a/crates/launcher-common/src/standard.rs +++ b/crates/launcher-common/src/standard.rs @@ -100,6 +100,18 @@ mod tests { #[test] fn baked_standard_parses() { let s = LauncherStandard::baked().expect("baked standard must parse"); - assert_eq!(s.spec_version, "0.1.0"); + + // Derive the expected version from the baked source rather than + // hardcoding it. This assertion previously read "0.1.0" and had gone + // stale against a standard that declares 0.2.0 - the test failed for + // a reason that had nothing to do with parsing, which is what it is + // named for. Deriving it means the check cannot rot again. + let declared = BAKED_STANDARD + .lines() + .find_map(|l| l.trim().strip_prefix("version = ")) + .map(|v| v.trim().trim_matches('"')) + .expect("baked standard must declare a version"); + assert_eq!(s.spec_version, declared); + assert!(!s.spec_version.is_empty(), "spec version must not be empty"); } } diff --git a/crates/launcher-common/src/template.rs b/crates/launcher-common/src/template.rs index 626f20b..492a27b 100644 --- a/crates/launcher-common/src/template.rs +++ b/crates/launcher-common/src/template.rs @@ -146,3 +146,79 @@ pub fn render( tera.render("launcher.sh", &ctx) .context("rendering launcher template") } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::{LauncherConfig, Project, Repo, Runtime, RuntimeKind}; + use crate::standard::LauncherStandard; + + fn sample_config() -> LauncherConfig { + LauncherConfig { + project: Project { + name: "foo".into(), + display: "Foo".into(), + description: Some("Foo desc".into()), + categories: vec!["Development".into()], + version: None, + license: None, + generic_name: Some("Foo Thing".into()), + }, + repo: Repo { + path: "/tmp/foo".into(), + }, + runtime: Runtime { + kind: RuntimeKind::Process, + port: None, + url: None, + startup_command_search: vec![], + command: vec!["foo".into()], + pid_file: None, + log_file: None, + wait_for_url_timeout_seconds: 15, + }, + icon: None, + integration: None, + soft_attach: None, + exceptions: None, + } + } + + /// The template source must OPEN with the shebang. + /// + /// It previously opened with a Tera comment block, so every launcher the + /// generator emitted began with a blank line: shellcheck SC2148, and a + /// script the kernel will not dispatch by shebang. That single template + /// defect produced ~20 identical one-line fix PRs across the estate, each + /// of which the next `launch-scaffolder realign` would have overwritten. + #[test] + fn template_source_opens_with_shebang() { + assert_eq!( + LAUNCHER_TEMPLATE.lines().next(), + Some("#!/usr/bin/env bash"), + "the shebang must be the literal first line of launcher.sh.tera" + ); + } + + /// And the RENDERED output must too — the property that actually matters. + /// Asserting only on the template source would miss Tera emitting leading + /// whitespace of its own, which is precisely how the original defect + /// escaped notice. + #[test] + fn rendered_launcher_starts_with_shebang_on_line_one() { + let cfg = sample_config(); + let std_ = LauncherStandard::baked().expect("baked standard should parse"); + let out = render(&cfg, &std_, None).expect("template should render"); + + assert!( + out.starts_with("#!/usr/bin/env bash\n"), + "rendered launcher must begin with the shebang; got: {:?}", + &out[..out.len().min(60)] + ); + assert_eq!( + out.lines().next(), + Some("#!/usr/bin/env bash"), + "shebang must be on line 1 of the rendered launcher" + ); + } +} diff --git a/templates/launcher.sh.tera b/templates/launcher.sh.tera index 710634c..51e629f 100644 --- a/templates/launcher.sh.tera +++ b/templates/launcher.sh.tera @@ -1,3 +1,13 @@ +#!/usr/bin/env bash +{# NOTE: the shebang MUST be the literal first line of this template. + It previously sat below this comment block, and every launcher the + generator emitted began with a blank line -- shellcheck SC2148, and a + script whose kernel shebang is not on line 1. That produced ~20 + identical one-line fix PRs across the estate, each of which would have + been overwritten by the next `launch-scaffolder realign`. Do not move + the shebang below a Tera tag again, whatever whitespace-control markers + the tag carries. +-#} {#- SPDX-License-Identifier: MPL-2.0 Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) @@ -13,7 +23,6 @@ All three shapes emit the same --integ / --disinteg / --help cross-platform surface so desktop integration stays uniform across the estate. -#} -#!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) #