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
8 changes: 7 additions & 1 deletion scripts/deploy-selfhost-image.sh
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,13 @@ services:
build: !reset null
YAML

mapfile -t compose_args < <(compose_file_args)
# #7765: capture via a checked assignment so compose_file_args's `exit 1` on a missing compose file
# actually aborts this script -- `mapfile < <(compose_file_args)` ran it in a subshell whose non-zero
# exit was swallowed (mapfile itself returns 0), leaving compose_args empty/truncated.
if ! compose_args_raw="$(compose_file_args)"; then
exit 1
fi
mapfile -t compose_args <<< "$compose_args_raw"
compose_args+=(-f "$override_file")

echo "selfhost image deploy: ensuring secret placeholder files exist"
Expand Down
8 changes: 7 additions & 1 deletion scripts/deploy-selfhost-prebuilt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,13 @@ services:
LOOPOVER_VERSION: "\${SENTRY_RELEASE}"
YAML

mapfile -t compose_args < <(compose_file_args)
# #7765: capture via a checked assignment so compose_file_args's `exit 1` on a missing compose file
# actually aborts this script -- `mapfile < <(compose_file_args)` ran it in a subshell whose non-zero
# exit was swallowed (mapfile itself returns 0), leaving compose_args empty/truncated.
if ! compose_args_raw="$(compose_file_args)"; then
exit 1
fi
mapfile -t compose_args <<< "$compose_args_raw"
compose_args+=(-f "$override_file")

echo "selfhost deploy: building $SERVICE runtime-prebuilt image"
Expand Down
8 changes: 7 additions & 1 deletion scripts/selfhost-post-update-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ require_cmd docker
require_cmd curl
docker compose version >/dev/null

mapfile -t compose_args < <(compose_file_args)
# #7765: capture via a checked assignment so compose_file_args's `exit 1` on a missing compose file
# actually aborts this script -- `mapfile < <(compose_file_args)` ran it in a subshell whose non-zero
# exit was swallowed (mapfile itself returns 0), leaving compose_args empty/truncated.
if ! compose_args_raw="$(compose_file_args)"; then
exit 1
fi
mapfile -t compose_args <<< "$compose_args_raw"

container_id="$(docker compose "${compose_args[@]}" ps -q "$SERVICE" 2>/dev/null || true)"
if [ -z "$container_id" ]; then
Expand Down
8 changes: 7 additions & 1 deletion scripts/selfhost-post-update-regression-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ if [[ ! "$THRESHOLD" =~ ^[0-9]+$ ]]; then
THRESHOLD=5
fi

mapfile -t compose_args < <(compose_file_args)
# #7765: capture via a checked assignment so compose_file_args's `exit 1` on a missing compose file
# actually aborts this script -- `mapfile < <(compose_file_args)` ran it in a subshell whose non-zero
# exit was swallowed (mapfile itself returns 0), leaving compose_args empty/truncated.
if ! compose_args_raw="$(compose_file_args)"; then
exit 1
fi
mapfile -t compose_args <<< "$compose_args_raw"

container_id="$(docker compose "${compose_args[@]}" ps -q "$SERVICE" 2>/dev/null || true)"
if [ -z "$container_id" ]; then
Expand Down
52 changes: 52 additions & 0 deletions test/unit/selfhost-deploy-common.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,55 @@ describe("env_put (#7766 -- atomic write + mode preservation)", () => {
}
});
});

describe("compose_file_args exit propagation (#7765)", () => {
// The exact idiom the callers (deploy-selfhost-image.sh etc.) now use to consume compose_file_args.
// Under the old `mapfile -t compose_args < <(compose_file_args)` the function ran in a subshell whose
// `exit 1` on a missing file was swallowed (mapfile returns 0), so the caller continued with an
// empty/truncated -f arg list. The checked assignment must instead abort before REACHED_END.
const CONSUMER = `
set -euo pipefail
. "${libPath.replace(/\\/g, "/")}"
if ! compose_args_raw="$(compose_file_args)"; then
exit 1
fi
mapfile -t compose_args <<< "$compose_args_raw"
printf 'REACHED_END args=[%s]\\n' "\${compose_args[*]}"
`;

function runConsumer(env: Record<string, string> = {}) {
const dir = mkdtempSync(join(tmpdir(), "loopover-compose-args-"));
try {
// Give the default-branch a real docker-compose.yml so the happy path has a file to find.
writeFileSync(join(dir, "docker-compose.yml"), "services: {}\n");
writeFileSync(join(dir, "base.yml"), "services: {}\n");
return spawnSync("bash", ["-c", CONSUMER], {
cwd: dir,
encoding: "utf8",
env: { ...process.env, ...env },
});
} finally {
rmSync(dir, { recursive: true, force: true });
}
}

it("continues with the -f args when every compose file exists", () => {
const result = runConsumer();
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain("REACHED_END args=[-f docker-compose.yml]");
});

it("aborts (never reaching the consumer) when the sole compose file is missing", () => {
const result = runConsumer({ SELFHOST_COMPOSE_FILES: "does-not-exist.yml" });
expect(result.status).not.toBe(0);
expect(result.stdout).not.toContain("REACHED_END");
expect(result.stderr).toContain("compose file not found: does-not-exist.yml");
});

it("aborts instead of continuing with a TRUNCATED arg list when a later compose file is missing", () => {
const result = runConsumer({ SELFHOST_COMPOSE_FILES: "base.yml missing.yml" });
expect(result.status).not.toBe(0);
expect(result.stdout).not.toContain("REACHED_END");
expect(result.stderr).toContain("compose file not found: missing.yml");
});
});