#!/bin/bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "$ROOT_DIR/entrypoint.sh" declare -a CLEANUP_PATHS=() cleanup() { if [ "${#CLEANUP_PATHS[@]}" -gt 0 ]; then rm -rf "${CLEANUP_PATHS[@]}" fi } trap cleanup EXIT fail() { printf '[error] %s\n' "$1" >&2 exit 1 } assert_eq() { local expected="$1" local actual="$2" local label="$3" if [ "$expected" != "$actual" ]; then fail "$label: expected '$expected', got '$actual'" fi } make_mock_curl() { local bin_dir="$1" cat >"$bin_dir/curl" <<'EOF' #!/bin/sh if [ -n "${FAKE_CURL_LOG_FILE:-}" ]; then printf '%s\n' "$*" >> "$FAKE_CURL_LOG_FILE" fi if [ "${FAKE_CURL_STATUS:-0}" != "0" ]; then exit "$FAKE_CURL_STATUS" fi # 分頁模式:依 URL 上的 page=N 回傳對應的 page 檔,超出範圍回傳空陣列 if [ -n "${FAKE_CURL_PAGES_DIR:-}" ]; then page=1 for arg in "$@"; do case "$arg" in *page=*) page=${arg##*page=} page=${page%%&*} ;; esac done page_file="$FAKE_CURL_PAGES_DIR/$page.json" if [ -f "$page_file" ]; then cat "$page_file" else printf '[]' fi exit 0 fi cat "${FAKE_CURL_RESPONSE_FILE:?}" EOF chmod +x "$bin_dir/curl" } make_mock_jq() { local bin_dir="$1" cat >"$bin_dir/jq" <<'EOF' #!/bin/sh python3 -c ' import json, re, sys args = sys.argv[1:] named = {} use_stdin = True i = 0 while i < len(args): a = args[i] if a == "-r": i += 1 elif a == "-n": use_stdin = False i += 1 elif a in ("--arg", "--argjson"): value = args[i + 2] named[args[i + 1]] = json.loads(value) if a == "--argjson" else value i += 3 else: i += 1 payload = sys.stdin.read() if use_stdin else "" def parse_or_exit(text): try: return json.loads(text) except Exception: sys.exit(4) # merge 模式:$acc + $page if "acc" in named and "page" in named: sys.stdout.write(json.dumps(named["acc"] + named["page"])) sys.exit(0) # count 模式:陣列長度,非陣列回 -1 if "is_beta" not in named: data = parse_or_exit(payload) sys.stdout.write(str(len(data)) if isinstance(data, list) else "-1") sys.exit(0) # calculate 模式 data = parse_or_exit(payload) is_beta = named.get("is_beta", "") def to_tuple(ver): nums = [] for part in ver.split("."): try: nums.append(int(part)) except Exception: nums.append(0) while len(nums) < 3: nums.append(0) return tuple(nums[:3]) def next_version(latest): major, minor, patch = to_tuple(latest) patch += 1 if patch >= 10: patch = 0 minor += 1 if minor >= 10: minor = 0 major += 1 return "{}.{}.{}".format(major, minor, patch) def beta_max(items, prefix): values = [] for item in items: if not isinstance(item, dict): continue tag = item.get("tag_name") if isinstance(tag, str) and tag.startswith(prefix): try: values.append(int(tag[len(prefix):])) except Exception: pass return max(values) if values else 0 if not isinstance(data, list): base = "0.0.0" else: stable = [] for item in data: if not isinstance(item, dict): continue tag = item.get("tag_name") if isinstance(tag, str) and "-beta." not in tag: ver = tag[1:] if tag.startswith("v") else tag if re.match(r"^[0-9]+(\.[0-9]+)*$", ver): stable.append(ver) base = max(stable, key=to_tuple) if stable else "0.0.0" next_ver = next_version(base) if is_beta == "true": beta = beta_max(data, "v{}-beta.".format(next_ver)) + 1 sys.stdout.write("{}\t{}-beta.{}".format(base, next_ver, beta)) else: sys.stdout.write("{}\t{}".format(base, next_ver)) ' "$@" EOF chmod +x "$bin_dir/jq" } run_entrypoint() { local response_file="$1" local is_beta="$2" local token="${3:-}" local fake_status="${4:-0}" local workdir local output_file local bin_dir local stdout_file local stderr_file workdir="$(mktemp -d)" CLEANUP_PATHS+=("$workdir") bin_dir="$workdir/bin" mkdir -p "$bin_dir" make_mock_curl "$bin_dir" "$response_file" make_mock_jq "$bin_dir" output_file="$workdir/github_output" stdout_file="$workdir/stdout" stderr_file="$workdir/stderr" if [ -n "$token" ]; then FAKE_CURL_STATUS="$fake_status" \ FAKE_CURL_RESPONSE_FILE="$response_file" \ FAKE_CURL_LOG_FILE="$workdir/curl_args" \ PATH="$bin_dir:$PATH" \ GITEA_SERVER_URL="https://gitea.example.com" \ GITEA_REPOSITORY="org/repo" \ RUNNER_TOKEN="$token" \ IS_BETA="$is_beta" \ GITHUB_OUTPUT="$output_file" \ bash "$ROOT_DIR/entrypoint.sh" >"$stdout_file" 2>"$stderr_file" else FAKE_CURL_STATUS="$fake_status" \ FAKE_CURL_RESPONSE_FILE="$response_file" \ FAKE_CURL_LOG_FILE="$workdir/curl_args" \ PATH="$bin_dir:$PATH" \ GITEA_SERVER_URL="https://gitea.example.com" \ GITEA_REPOSITORY="org/repo" \ IS_BETA="$is_beta" \ GITHUB_OUTPUT="$output_file" \ bash "$ROOT_DIR/entrypoint.sh" >"$stdout_file" 2>"$stderr_file" fi printf '%s\n' "$output_file" } test_unit_helpers() { assert_eq "false" "$(normalize_beta_flag "")" "normalize_beta_flag empty" assert_eq "false" "$(normalize_beta_flag "null")" "normalize_beta_flag null" assert_eq "true" "$(normalize_beta_flag "true")" "normalize_beta_flag true" assert_eq "0.1.0" "$(next_release_version "0.0.9")" "next_release_version carry patch" assert_eq "2.0.0" "$(next_release_version "1.9.9")" "next_release_version carry minor" assert_eq "100.0.0" "$(next_release_version "99.9.9")" "next_release_version carry major" } test_stable_release_flow() { local workdir local response_file local output_file workdir="$(mktemp -d)" CLEANUP_PATHS+=("$workdir") response_file="$workdir/releases.json" cat >"$response_file" <<'EOF' [ {"tag_name":"v1.2.3-beta.1"}, {"tag_name":"v1.2.3"}, {"tag_name":"v1.2.4-beta.1"} ] EOF output_file="$(run_entrypoint "$response_file" "false")" assert_eq "version=1.2.4" "$(cat "$output_file")" "stable release output" } test_beta_release_flow() { local workdir local response_file local output_file workdir="$(mktemp -d)" CLEANUP_PATHS+=("$workdir") response_file="$workdir/releases.json" cat >"$response_file" <<'EOF' [ {"tag_name":"v1.2.3"}, {"tag_name":"v1.2.4-beta.1"}, {"tag_name":"v1.2.4-beta.3"} ] EOF output_file="$(run_entrypoint "$response_file" "true")" assert_eq "version=1.2.4-beta.4" "$(cat "$output_file")" "beta release output" } test_empty_release_list() { local workdir local response_file local output_file workdir="$(mktemp -d)" CLEANUP_PATHS+=("$workdir") response_file="$workdir/releases.json" printf '%s\n' '[]' >"$response_file" output_file="$(run_entrypoint "$response_file" "false")" assert_eq "version=0.0.1" "$(cat "$output_file")" "empty release list" } test_only_beta_releases() { local workdir local response_file local output_file workdir="$(mktemp -d)" CLEANUP_PATHS+=("$workdir") response_file="$workdir/releases.json" cat >"$response_file" <<'EOF' [ {"tag_name":"v2.4.6-beta.1"}, {"tag_name":"v2.4.6-beta.2"} ] EOF output_file="$(run_entrypoint "$response_file" "false")" assert_eq "version=0.0.1" "$(cat "$output_file")" "only beta releases" } test_null_release_payload() { local workdir local response_file local output_file workdir="$(mktemp -d)" CLEANUP_PATHS+=("$workdir") response_file="$workdir/releases.json" printf '%s\n' 'null' >"$response_file" output_file="$(run_entrypoint "$response_file" "false")" assert_eq "version=0.0.1" "$(cat "$output_file")" "null release payload" } test_pagination_collects_all_pages() { local workdir local pages_dir local bin_dir local output_file local stdout_file local stderr_file local curl_args local i workdir="$(mktemp -d)" CLEANUP_PATHS+=("$workdir") pages_dir="$workdir/pages" mkdir -p "$pages_dir" # 第 1 頁滿載 10 筆,且全部是 beta(重現「最新一整頁都是 beta」的情境) { printf '[' for i in $(seq 1 10); do if [ "$i" -gt 1 ]; then printf ','; fi printf '{"tag_name":"v1.9.0-beta.%s"}' "$i" done printf ']' } >"$pages_dir/1.json" # 第 2 頁才出現穩定版,且故意讓最大值不是第一筆(驗證 semver 取最大) cat >"$pages_dir/2.json" <<'JSON' [ {"tag_name":"v1.2.3"}, {"tag_name":"v1.3.1"}, {"tag_name":"v1.2.9"} ] JSON bin_dir="$workdir/bin" mkdir -p "$bin_dir" make_mock_curl "$bin_dir" make_mock_jq "$bin_dir" output_file="$workdir/github_output" stdout_file="$workdir/stdout" stderr_file="$workdir/stderr" FAKE_CURL_STATUS=0 \ FAKE_CURL_PAGES_DIR="$pages_dir" \ FAKE_CURL_LOG_FILE="$workdir/curl_args" \ PATH="$bin_dir:$PATH" \ GITEA_SERVER_URL="https://gitea.example.com" \ GITEA_REPOSITORY="org/repo" \ IS_BETA="false" \ GITHUB_OUTPUT="$output_file" \ bash "$ROOT_DIR/entrypoint.sh" >"$stdout_file" 2>"$stderr_file" # 穩定版在第 2 頁、最大為 1.3.1 -> 下一版 1.3.2 assert_eq "version=1.3.2" "$(cat "$output_file")" "pagination + semver max output" # 確認確實翻到第 2 頁 curl_args="$workdir/curl_args" if ! grep -q 'page=2' "$curl_args"; then fail "pagination: expected a request for page=2" fi } test_token_auth_header() { local workdir local response_file local bin_dir local output_file local curl_args local stdout_file local stderr_file workdir="$(mktemp -d)" CLEANUP_PATHS+=("$workdir") response_file="$workdir/releases.json" printf '%s\n' '[]' >"$response_file" bin_dir="$workdir/bin" mkdir -p "$bin_dir" make_mock_curl "$bin_dir" "$response_file" make_mock_jq "$bin_dir" output_file="$workdir/github_output" stdout_file="$workdir/stdout" stderr_file="$workdir/stderr" FAKE_CURL_STATUS=0 \ FAKE_CURL_RESPONSE_FILE="$response_file" \ FAKE_CURL_LOG_FILE="$workdir/curl_args" \ PATH="$bin_dir:$PATH" \ GITEA_SERVER_URL="https://gitea.example.com" \ GITEA_REPOSITORY="org/repo" \ RUNNER_TOKEN="secret-token" \ IS_BETA="false" \ GITHUB_OUTPUT="$output_file" \ bash "$ROOT_DIR/entrypoint.sh" >"$stdout_file" 2>"$stderr_file" curl_args="$workdir/curl_args" if ! grep -q 'Authorization: token secret-token' "$curl_args"; then fail "token auth header: missing Authorization header" fi assert_eq "version=0.0.1" "$(cat "$output_file")" "token auth output" } test_malformed_release_payload() { local workdir local response_file local bin_dir local output_file local stdout_file local stderr_file workdir="$(mktemp -d)" CLEANUP_PATHS+=("$workdir") response_file="$workdir/releases.json" printf '%s\n' '{' >"$response_file" bin_dir="$workdir/bin" mkdir -p "$bin_dir" make_mock_curl "$bin_dir" "$response_file" make_mock_jq "$bin_dir" output_file="$workdir/github_output" stdout_file="$workdir/stdout" stderr_file="$workdir/stderr" if FAKE_CURL_STATUS=0 \ FAKE_CURL_RESPONSE_FILE="$response_file" \ PATH="$bin_dir:$PATH" \ GITEA_SERVER_URL="https://gitea.example.com" \ GITEA_REPOSITORY="org/repo" \ IS_BETA="false" \ GITHUB_OUTPUT="$output_file" \ bash "$ROOT_DIR/entrypoint.sh" >"$stdout_file" 2>"$stderr_file"; then fail "malformed release payload: expected failure" fi } test_curl_failure() { local workdir local response_file local bin_dir local output_file local stdout_file local stderr_file workdir="$(mktemp -d)" CLEANUP_PATHS+=("$workdir") response_file="$workdir/releases.json" printf '%s\n' '[]' >"$response_file" bin_dir="$workdir/bin" mkdir -p "$bin_dir" make_mock_curl "$bin_dir" "$response_file" output_file="$workdir/github_output" stdout_file="$workdir/stdout" stderr_file="$workdir/stderr" if FAKE_CURL_STATUS=22 \ FAKE_CURL_RESPONSE_FILE="$response_file" \ PATH="$bin_dir:$PATH" \ GITEA_SERVER_URL="https://gitea.example.com" \ GITEA_REPOSITORY="org/repo" \ IS_BETA="false" \ GITHUB_OUTPUT="$output_file" \ bash "$ROOT_DIR/entrypoint.sh" >"$stdout_file" 2>"$stderr_file"; then fail "curl failure: expected failure" fi } test_unit_helpers test_stable_release_flow test_beta_release_flow test_empty_release_list test_only_beta_releases test_null_release_payload test_pagination_collects_all_pages test_token_auth_header test_malformed_release_payload test_curl_failure printf '[info] all tests passed\n'