505 lines
13 KiB
Bash
Executable File
505 lines
13 KiB
Bash
Executable File
#!/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'
|