공개: KoTalk 최신 기준선

This commit is contained in:
Ian 2026-04-16 09:24:26 +09:00
commit debf62f76e
572 changed files with 41689 additions and 0 deletions

View file

@ -0,0 +1,329 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./scripts/release/release-prepare-assets.sh --version v0.2.0-alpha.1 [options]
Options:
--windows-zip <path> Windows x64 ZIP artifact path
--android-apk <path> Android universal APK artifact path
--zip <path> Backward-compatible alias for --windows-zip
--channel <name> Release channel. Default: alpha
--notes <path> Existing Korean release notes file
--screenshots <dir> Directory containing *.png/jpg screenshots
--force Overwrite an existing release folder
Environment:
DOWNLOAD_BASE_URL Defaults to https://download-vs-messanger.phy.kr
EOF
}
version=""
channel="alpha"
windows_zip=""
android_apk=""
notes_path=""
screenshots_dir=""
force="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
version="${2:-}"
shift 2
;;
--channel)
channel="${2:-}"
shift 2
;;
--windows-zip|--zip)
windows_zip="${2:-}"
shift 2
;;
--android-apk)
android_apk="${2:-}"
shift 2
;;
--notes)
notes_path="${2:-}"
shift 2
;;
--screenshots)
screenshots_dir="${2:-}"
shift 2
;;
--force)
force="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$version" ]]; then
usage >&2
exit 1
fi
if [[ -z "$windows_zip" && -z "$android_apk" ]]; then
echo "At least one artifact must be provided: --windows-zip or --android-apk" >&2
usage >&2
exit 1
fi
if [[ -n "$windows_zip" && ! -f "$windows_zip" ]]; then
echo "Windows ZIP artifact not found: $windows_zip" >&2
exit 1
fi
if [[ -n "$android_apk" && ! -f "$android_apk" ]]; then
echo "Android APK artifact not found: $android_apk" >&2
exit 1
fi
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
release_root="$repo_root/release-assets/releases/$version"
latest_root="$repo_root/release-assets/latest"
template_path="$repo_root/release-assets/templates/RELEASE_NOTES.ko.md"
download_base_url="${DOWNLOAD_BASE_URL:-https://download-vs-messanger.phy.kr}"
published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
derive_release_url() {
local origin_url
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
if [[ -z "$origin_url" ]]; then
return 0
fi
if [[ "$origin_url" =~ ^https?:// ]]; then
printf '%s/releases/tag/%s' "${origin_url%.git}" "$version"
return 0
fi
if [[ "$origin_url" =~ ^git@([^:]+):(.+)\.git$ ]]; then
printf 'https://%s/%s/releases/tag/%s' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "$version"
return 0
fi
}
release_url="$(derive_release_url)"
if [[ -e "$release_root" && "$force" != "true" ]]; then
echo "Release directory already exists: $release_root" >&2
echo "Use --force to replace it." >&2
exit 1
fi
rm -rf "$release_root" "$latest_root"
mkdir -p "$release_root/screenshots" "$latest_root/screenshots"
if [[ -n "$notes_path" ]]; then
cp "$notes_path" "$release_root/RELEASE_NOTES.ko.md"
else
sed \
-e "s/{{VERSION}}/$version/g" \
-e "s/{{CHANNEL}}/$channel/g" \
-e "s/{{PUBLISHED_AT}}/$published_at/g" \
"$template_path" > "$release_root/RELEASE_NOTES.ko.md"
fi
cp "$release_root/RELEASE_NOTES.ko.md" "$latest_root/RELEASE_NOTES.ko.md"
if [[ -n "$screenshots_dir" ]]; then
while IFS= read -r screenshot; do
cp "$screenshot" "$release_root/screenshots/$(basename "$screenshot")"
cp "$screenshot" "$latest_root/screenshots/$(basename "$screenshot")"
done < <(find "$screenshots_dir" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | sort)
fi
platform_count=0
platforms_json=""
top_level_windows_alias=""
release_hash_paths=()
latest_hash_paths=()
append_platform_json() {
local body="$1"
if (( platform_count > 0 )); then
platforms_json+=$',\n'
fi
platforms_json+="$body"
platform_count=$((platform_count + 1))
}
write_platform_version_json() {
local path="$1"
local body="$2"
cat > "$path" <<EOF
{
"version": "$version",
"channel": "$channel",
"publishedAt": "$published_at",
"notesUrl": "$download_base_url/releases/$version/RELEASE_NOTES.ko.md",
"releaseUrl": "$release_url",
"platform": {
$body
}
}
EOF
}
if [[ -n "$windows_zip" ]]; then
windows_release_name="VsMessenger-win-x64-$version.zip"
windows_latest_name="VsMessenger-win-x64.zip"
windows_release_dir="$release_root/windows/x64"
windows_latest_dir="$latest_root/windows"
mkdir -p "$windows_release_dir" "$windows_latest_dir"
cp "$windows_zip" "$windows_release_dir/$windows_release_name"
cp "$windows_zip" "$windows_latest_dir/$windows_latest_name"
(
cd "$windows_release_dir"
sha256sum "$windows_release_name" > SHA256SUMS.txt
)
(
cd "$windows_latest_dir"
sha256sum "$windows_latest_name" > SHA256SUMS.txt
)
release_hash_paths+=("windows/x64/$windows_release_name")
latest_hash_paths+=("windows/$windows_latest_name")
windows_platform_body="$(cat <<EOF
"kind": "desktop",
"arch": "x64",
"latestUrl": "$download_base_url/windows/latest",
"portableZipUrl": "$download_base_url/windows/latest/$windows_latest_name",
"sha256Url": "$download_base_url/windows/latest/SHA256SUMS.txt"
EOF
)"
append_platform_json "$(cat <<EOF
"windows": {
$windows_platform_body
}
EOF
)"
top_level_windows_alias="$(cat <<EOF
,
"windows": {
$windows_platform_body
}
EOF
)"
write_platform_version_json "$windows_latest_dir/version.json" "$windows_platform_body"
fi
if [[ -n "$android_apk" ]]; then
android_release_name="VsMessenger-android-universal-$version.apk"
android_latest_name="VsMessenger-android-universal.apk"
android_release_dir="$release_root/android/universal"
android_latest_dir="$latest_root/android"
mkdir -p "$android_release_dir" "$android_latest_dir"
cp "$android_apk" "$android_release_dir/$android_release_name"
cp "$android_apk" "$android_latest_dir/$android_latest_name"
(
cd "$android_release_dir"
sha256sum "$android_release_name" > SHA256SUMS.txt
)
(
cd "$android_latest_dir"
sha256sum "$android_latest_name" > SHA256SUMS.txt
)
release_hash_paths+=("android/universal/$android_release_name")
latest_hash_paths+=("android/$android_latest_name")
android_platform_body="$(cat <<EOF
"kind": "mobile",
"arch": "universal",
"packageName": "kr.physia.vsmessenger",
"minSdk": 26,
"latestUrl": "$download_base_url/android/latest",
"apkUrl": "$download_base_url/android/latest/$android_latest_name",
"sha256Url": "$download_base_url/android/latest/SHA256SUMS.txt"
EOF
)"
append_platform_json "$(cat <<EOF
"android": {
$android_platform_body
}
EOF
)"
write_platform_version_json "$android_latest_dir/version.json" "$android_platform_body"
fi
if (( ${#release_hash_paths[@]} > 0 )); then
(
cd "$release_root"
sha256sum "${release_hash_paths[@]}" > SHA256SUMS.txt
)
fi
if (( ${#latest_hash_paths[@]} > 0 )); then
(
cd "$latest_root"
sha256sum "${latest_hash_paths[@]}" > SHA256SUMS.txt
)
fi
mapfile -t screenshot_files < <(find "$release_root/screenshots" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | sort)
screenshots_json="[]"
if [[ ${#screenshot_files[@]} -gt 0 ]]; then
screenshots_json=$(
for idx in "${!screenshot_files[@]}"; do
name="$(basename "${screenshot_files[$idx]}")"
printf ' "%s/releases/%s/screenshots/%s"' "$download_base_url" "$version" "$name"
if (( idx < ${#screenshot_files[@]} - 1 )); then
printf ',\n'
else
printf '\n'
fi
done
)
screenshots_json="[
$screenshots_json
]"
fi
cat > "$release_root/version.json" <<EOF
{
"version": "$version",
"channel": "$channel",
"publishedAt": "$published_at",
"notesUrl": "$download_base_url/releases/$version/RELEASE_NOTES.ko.md",
"releaseUrl": "$release_url",
"platforms": {
$platforms_json
},
"screenshots": $screenshots_json$top_level_windows_alias
}
EOF
cp "$release_root/version.json" "$latest_root/version.json"
cp "$release_root/version.json" "$latest_root/latest.json"
touch "$latest_root/.gitkeep"
echo "Prepared release bundle:"
echo " release-assets/releases/$version"
echo " release-assets/latest"

View file

@ -0,0 +1,190 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./scripts/release/release-publish-forge.sh --version v0.2.0-alpha.1 [options]
Options:
--base-url <url> Forge base URL. Example: https://forge.example.com
--repo <owner/name> Repository in owner/name form
--token <token> Gitea API token
--dry-run Print planned uploads without calling the API
EOF
}
version=""
base_url=""
repo_full_name=""
token="${FORGE_RELEASE_TOKEN:-${GITEA_RELEASE_TOKEN:-}}"
dry_run="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
version="${2:-}"
shift 2
;;
--base-url)
base_url="${2:-}"
shift 2
;;
--repo)
repo_full_name="${2:-}"
shift 2
;;
--token)
token="${2:-}"
shift 2
;;
--dry-run)
dry_run="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$version" ]]; then
usage >&2
exit 1
fi
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
release_root="$repo_root/release-assets/releases/$version"
if [[ ! -d "$release_root" ]]; then
echo "Release bundle not found: $release_root" >&2
exit 1
fi
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
if [[ -z "$base_url" ]]; then
if [[ "$origin_url" =~ ^(https?://[^/]+)/(.+)\.git$ ]]; then
base_url="${BASH_REMATCH[1]}"
elif [[ "$origin_url" =~ ^git@([^:]+):(.+)\.git$ ]]; then
base_url="https://${BASH_REMATCH[1]}"
fi
fi
if [[ -z "$repo_full_name" ]]; then
if [[ "$origin_url" =~ ^https?://[^/]+/(.+)\.git$ ]]; then
repo_full_name="${BASH_REMATCH[1]}"
elif [[ "$origin_url" =~ ^git@[^:]+:(.+)\.git$ ]]; then
repo_full_name="${BASH_REMATCH[1]}"
fi
fi
if [[ -z "$base_url" || -z "$repo_full_name" ]]; then
echo "Unable to infer forge base URL or repository name from origin remote." >&2
exit 1
fi
if [[ "$dry_run" != "true" && -z "$token" ]]; then
echo "A Gitea API token is required. Use --token or FORGE_RELEASE_TOKEN." >&2
exit 1
fi
api_root="${base_url%/}/api/v1/repos/${repo_full_name}"
release_api="${api_root}/releases"
tag_api="${release_api}/tags/${version}"
pre_release="false"
case "$version" in
*alpha*|*beta*|*rc*)
pre_release="true"
;;
esac
mapfile -t asset_files < <(
find "$release_root" -type f \
\( -name '*.zip' -o -name '*.apk' -o -name 'RELEASE_NOTES.ko.md' -o -name 'SHA256SUMS.txt' -o -name 'version.json' \) \
| sort
)
if [[ ${#asset_files[@]} -eq 0 ]]; then
echo "No release assets found in $release_root" >&2
exit 1
fi
echo "Forge release target: $base_url/$repo_full_name"
echo "Version: $version"
printf 'Assets:\n'
printf ' - %s\n' "${asset_files[@]#$release_root/}"
if [[ "$dry_run" == "true" ]]; then
exit 0
fi
auth_header="Authorization: token $token"
tmp_response="$(mktemp)"
trap 'rm -f "$tmp_response"' EXIT
existing_status="$(curl -sS -o "$tmp_response" -w '%{http_code}' -H "$auth_header" "$tag_api")"
if [[ "$existing_status" == "200" ]]; then
existing_release_id="$(python3 - <<'PY' "$tmp_response"
import json, sys
with open(sys.argv[1], "r", encoding="utf-8") as fh:
print(json.load(fh)["id"])
PY
)"
curl -sS -X DELETE -H "$auth_header" "${release_api}/${existing_release_id}" >/dev/null
fi
release_body=$'Windows와 Android 클라이언트 산출물을 병렬로 정리한 릴리즈입니다.\n\n'
release_body+=$'동일 버전 번호 아래 OS별 자산을 함께 게시하며, 최신 다운로드 채널은 download-vs-messanger.phy.kr에서 운영합니다.'
create_payload="$(python3 - <<'PY' "$version" "$release_body" "$pre_release"
import json, sys
version, body, prerelease = sys.argv[1], sys.argv[2], sys.argv[3] == "true"
print(json.dumps({
"tag_name": version,
"name": version,
"body": body,
"draft": False,
"prerelease": prerelease
}, ensure_ascii=False))
PY
)"
create_status="$(curl -sS -o "$tmp_response" -w '%{http_code}' \
-X POST \
-H "$auth_header" \
-H 'Content-Type: application/json' \
-d "$create_payload" \
"$release_api")"
if [[ "$create_status" != "201" ]]; then
echo "Failed to create forge release. HTTP $create_status" >&2
cat "$tmp_response" >&2
exit 1
fi
release_id="$(python3 - <<'PY' "$tmp_response"
import json, sys
with open(sys.argv[1], "r", encoding="utf-8") as fh:
print(json.load(fh)["id"])
PY
)"
for asset in "${asset_files[@]}"; do
name="$(basename "$asset")"
curl -sS \
-X POST \
-H "$auth_header" \
-H 'Content-Type: application/octet-stream' \
--data-binary @"$asset" \
"${release_api}/${release_id}/assets?name=${name}" >/dev/null
done
echo "Published forge release $version with ${#asset_files[@]} assets."

View file

@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./scripts/release/release-upload-assets.sh --version v0.2.0-alpha.1 --host example.com --user deploy [options]
Options:
--target <path> Remote download root. Default: /srv/vs-messanger/download
--ssh-key <path> Private key used for SSH/rsync
--dry-run Print the rsync plan without changing the server
EOF
}
version=""
host=""
user=""
target="/srv/vs-messanger/download"
ssh_key=""
dry_run="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
version="${2:-}"
shift 2
;;
--host)
host="${2:-}"
shift 2
;;
--user)
user="${2:-}"
shift 2
;;
--target)
target="${2:-}"
shift 2
;;
--ssh-key)
ssh_key="${2:-}"
shift 2
;;
--dry-run)
dry_run="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$version" || -z "$host" || -z "$user" ]]; then
usage >&2
exit 1
fi
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
release_root="$repo_root/release-assets/releases/$version"
latest_root="$repo_root/release-assets/latest"
if [[ ! -d "$release_root" || ! -d "$latest_root" ]]; then
echo "Prepared release bundle not found for version $version" >&2
exit 1
fi
ssh_cmd=(ssh -o StrictHostKeyChecking=accept-new)
if [[ -n "$ssh_key" ]]; then
ssh_cmd+=(-i "$ssh_key")
fi
rsync_opts=(-az)
if [[ "$dry_run" == "true" ]]; then
rsync_opts+=(--dry-run)
else
rsync_opts+=(--delete)
fi
target_host="$user@$host"
rsh="${ssh_cmd[*]}"
"${ssh_cmd[@]}" "$target_host" "mkdir -p '$target/releases/$version' '$target/latest' '$target/windows/latest' '$target/android/latest'"
rsync "${rsync_opts[@]}" -e "$rsh" "$release_root"/ "$target_host:$target/releases/$version/"
rsync "${rsync_opts[@]}" -e "$rsh" "$latest_root"/ "$target_host:$target/latest/"
if [[ -d "$latest_root/windows" ]]; then
rsync "${rsync_opts[@]}" -e "$rsh" "$latest_root/windows/" "$target_host:$target/windows/latest/"
fi
if [[ -d "$latest_root/android" ]]; then
rsync "${rsync_opts[@]}" -e "$rsh" "$latest_root/android/" "$target_host:$target/android/latest/"
fi
echo "Uploaded release assets for $version to $target_host:$target"