diff --git a/scripts/ios-release-archive.sh b/scripts/ios-release-archive.sh index ff3ceecfe8a..24edc097443 100755 --- a/scripts/ios-release-archive.sh +++ b/scripts/ios-release-archive.sh @@ -14,12 +14,24 @@ BUILD_NUMBER="${IOS_RELEASE_BUILD_NUMBER:-}" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "${ROOT_DIR}/scripts/lib/ios-fastlane.sh" +require_option_value() { + local option="$1" + local value="${2-}" + + if [[ -z "${value}" || "${value}" == --* ]]; then + echo "Missing value for ${option}." >&2 + usage >&2 + exit 1 + fi +} + while [[ $# -gt 0 ]]; do case "$1" in --) shift ;; --build-number) + require_option_value "$1" "${2-}" BUILD_NUMBER="${2:-}" shift 2 ;; diff --git a/scripts/ios-release-prepare.sh b/scripts/ios-release-prepare.sh index 38951f9e49e..0e3229a973d 100755 --- a/scripts/ios-release-prepare.sh +++ b/scripts/ios-release-prepare.sh @@ -86,16 +86,29 @@ validate_push_relay_base_url() { fi } +require_option_value() { + local option="$1" + local value="${2-}" + + if [[ -z "${value}" || "${value}" == --* ]]; then + echo "Missing value for ${option}." >&2 + usage >&2 + exit 1 + fi +} + while [[ $# -gt 0 ]]; do case "$1" in --) shift ;; --build-number) + require_option_value "$1" "${2-}" BUILD_NUMBER="${2:-}" shift 2 ;; --team-id) + require_option_value "$1" "${2-}" TEAM_ID="${2:-}" shift 2 ;; diff --git a/scripts/ios-release-upload.sh b/scripts/ios-release-upload.sh index 10e00b842c7..1da19f0bdd8 100755 --- a/scripts/ios-release-upload.sh +++ b/scripts/ios-release-upload.sh @@ -16,12 +16,24 @@ BUILD_NUMBER="${IOS_RELEASE_BUILD_NUMBER:-}" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "${ROOT_DIR}/scripts/lib/ios-fastlane.sh" +require_option_value() { + local option="$1" + local value="${2-}" + + if [[ -z "${value}" || "${value}" == --* ]]; then + echo "Missing value for ${option}." >&2 + usage >&2 + exit 1 + fi +} + while [[ $# -gt 0 ]]; do case "$1" in --) shift ;; --build-number) + require_option_value "$1" "${2-}" BUILD_NUMBER="${2:-}" shift 2 ;; diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 9e6405eb1ff..bf3ebc1a99f 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -960,6 +960,9 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ "scripts/github/run-openclaw-cross-os-release-checks.sh", ["test/scripts/openclaw-cross-os-release-workflow.test.ts"], ], + ["scripts/ios-release-archive.sh", ["test/scripts/ios-release-wrapper-args.test.ts"]], + ["scripts/ios-release-prepare.sh", ["test/scripts/ios-release-wrapper-args.test.ts"]], + ["scripts/ios-release-upload.sh", ["test/scripts/ios-release-wrapper-args.test.ts"]], ["scripts/lib/restart-mac-gateway.sh", ["test/scripts/restart-mac.test.ts"]], [ "scripts/openclaw-release-clawhub-runtime-state.ts", diff --git a/test/scripts/ios-release-wrapper-args.test.ts b/test/scripts/ios-release-wrapper-args.test.ts new file mode 100644 index 00000000000..5b7ef6f7e40 --- /dev/null +++ b/test/scripts/ios-release-wrapper-args.test.ts @@ -0,0 +1,55 @@ +// iOS release wrapper tests keep release args fail-closed before Fastlane work. +import { execFileSync } from "node:child_process"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const BASH_BIN = process.platform === "win32" ? "bash" : "/bin/bash"; + +type WrapperCase = readonly [scriptPath: string, args: readonly string[], option: string]; + +function runScript( + scriptPath: string, + args: readonly string[], +): { ok: boolean; stdout: string; stderr: string } { + const scriptArgs = + process.platform === "win32" ? [scriptPath] : ["--noprofile", "--norc", scriptPath]; + try { + const stdout = execFileSync(BASH_BIN, [...scriptArgs, ...args], { + cwd: process.cwd(), + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + return { ok: true, stdout, stderr: "" }; + } catch (error) { + const e = error as { stdout?: unknown; stderr?: unknown }; + const stdout = Buffer.isBuffer(e.stdout) ? e.stdout.toString("utf8") : String(e.stdout ?? ""); + const stderr = Buffer.isBuffer(e.stderr) ? e.stderr.toString("utf8") : String(e.stderr ?? ""); + return { ok: false, stdout, stderr }; + } +} + +describe("iOS release shell wrapper arguments", () => { + const missingValueCases: readonly WrapperCase[] = [ + ["scripts/ios-release-upload.sh", ["--build-number", "--bogus"], "--build-number"], + ["scripts/ios-release-archive.sh", ["--build-number", "--bogus"], "--build-number"], + ["scripts/ios-release-prepare.sh", ["--build-number", "--team-id"], "--build-number"], + [ + "scripts/ios-release-prepare.sh", + ["--build-number", "7", "--team-id", "--bogus"], + "--team-id", + ], + ]; + + it.each(missingValueCases)( + "rejects missing %s option values before release work", + (scriptPath, args, option) => { + const result = runScript(path.join(process.cwd(), scriptPath), args); + + expect(result.ok).toBe(false); + expect(result.stderr).toContain(`Missing value for ${option}.`); + expect(result.stderr).not.toContain("No such file or directory"); + expect(result.stderr).not.toContain("fastlane"); + expect(result.stdout).toBe(""); + }, + ); +});