공개: 플랫폼 결론과 릴리즈 노트 규칙 반영
Some checks are pending
ci / server (push) Waiting to run
ci / web (push) Waiting to run
ci / desktop-windows (push) Waiting to run

This commit is contained in:
Ian 2026-04-16 13:54:11 +09:00
commit 799b975406
55 changed files with 1440 additions and 115 deletions

View file

@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./scripts/release/build-android-apk.sh --version 2026.04.16-alpha.6 [options]
Options:
--configuration <name> Build configuration. Default: Release
--output <dir> Output directory. Default: artifacts/builds/<version>
--dotnet <path> Explicit dotnet binary path
EOF
}
version=""
configuration="Release"
output_dir=""
dotnet_bin="${DOTNET_BIN:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
version="${2:-}"
shift 2
;;
--configuration)
configuration="${2:-}"
shift 2
;;
--output)
output_dir="${2:-}"
shift 2
;;
--dotnet)
dotnet_bin="${2:-}"
shift 2
;;
-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)"
if [[ -z "$dotnet_bin" ]]; then
if command -v dotnet >/dev/null 2>&1; then
dotnet_bin="$(command -v dotnet)"
elif [[ -x "$HOME/.dotnet/dotnet" ]]; then
dotnet_bin="$HOME/.dotnet/dotnet"
else
echo "Unable to find dotnet. Set --dotnet or DOTNET_BIN." >&2
exit 1
fi
fi
if [[ -z "${JAVA_HOME:-}" && -d /usr/lib/jvm/java-17-openjdk-amd64 ]]; then
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export PATH="$JAVA_HOME/bin:$PATH"
fi
if [[ -z "${ANDROID_SDK_ROOT:-}" ]]; then
export ANDROID_SDK_ROOT="$HOME/Android/Sdk"
fi
if [[ -z "$output_dir" ]]; then
output_dir="$repo_root/artifacts/builds/$version"
fi
publish_dir="$output_dir/android/publish"
apk_path="$output_dir/KoTalk-android-universal-$version.apk"
rm -rf "$publish_dir"
mkdir -p "$publish_dir" "$output_dir"
if [[ ! -d "$ANDROID_SDK_ROOT/platforms" ]]; then
mkdir -p "$ANDROID_SDK_ROOT"
"$dotnet_bin" build "$repo_root/src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj" \
-t:InstallAndroidDependencies \
-f net8.0-android \
-p:AndroidSdkDirectory="$ANDROID_SDK_ROOT" \
-p:JavaSdkDirectory="${JAVA_HOME:-}" >/dev/null
fi
"$dotnet_bin" publish "$repo_root/src/PhysOn.Mobile.Android/PhysOn.Mobile.Android.csproj" \
-c "$configuration" \
-f net8.0-android \
-p:AndroidPackageFormat=apk \
-p:AndroidKeyStore=false \
-p:AndroidSdkDirectory="$ANDROID_SDK_ROOT" \
-p:JavaSdkDirectory="${JAVA_HOME:-}" \
-o "$publish_dir"
apk_source="$(find "$publish_dir" -type f -name '*.apk' | head -n 1)"
if [[ -z "$apk_source" ]]; then
echo "Android publish did not produce an APK." >&2
exit 1
fi
cp "$apk_source" "$apk_path"
(
cd "$output_dir"
sha256sum "$(basename "$apk_path")" > KoTalk-android-universal-$version.apk.sha256
)
echo "Built Android APK in $output_dir"

View file

@ -0,0 +1,130 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./scripts/release/build-windows-distributions.sh --version 2026.04.16-alpha.6 [options]
Options:
--configuration <name> Build configuration. Default: Release
--runtime <rid> Runtime identifier. Default: win-x64
--output <dir> Output directory. Default: artifacts/builds/<version>
--dotnet <path> Explicit dotnet binary path
EOF
}
version=""
configuration="Release"
runtime="win-x64"
output_dir=""
dotnet_bin="${DOTNET_BIN:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
version="${2:-}"
shift 2
;;
--configuration)
configuration="${2:-}"
shift 2
;;
--runtime)
runtime="${2:-}"
shift 2
;;
--output)
output_dir="${2:-}"
shift 2
;;
--dotnet)
dotnet_bin="${2:-}"
shift 2
;;
-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)"
if [[ -z "$dotnet_bin" ]]; then
if command -v dotnet >/dev/null 2>&1; then
dotnet_bin="$(command -v dotnet)"
elif [[ -x "$HOME/.dotnet/dotnet" ]]; then
dotnet_bin="$HOME/.dotnet/dotnet"
else
echo "Unable to find dotnet. Set --dotnet or DOTNET_BIN." >&2
exit 1
fi
fi
if [[ -z "$output_dir" ]]; then
output_dir="$repo_root/artifacts/builds/$version"
fi
publish_dir="$output_dir/publish/$runtime"
onefile_dir="$output_dir/onefile/$runtime"
zip_path="$output_dir/KoTalk-windows-x64-$version.zip"
portable_exe_path="$output_dir/KoTalk-windows-x64-onefile-$version.exe"
installer_path="$output_dir/KoTalk-windows-x64-installer-$version.exe"
installer_script="$repo_root/packaging/windows/KoTalkInstaller.nsi"
app_icon="$repo_root/branding/ico/kotalk.ico"
rm -rf "$publish_dir" "$onefile_dir"
mkdir -p "$publish_dir" "$onefile_dir" "$output_dir"
"$dotnet_bin" publish "$repo_root/src/PhysOn.Desktop/PhysOn.Desktop.csproj" \
-c "$configuration" \
-r "$runtime" \
--self-contained true \
-p:DebugSymbols=false \
-p:DebugType=None \
-o "$publish_dir"
rm -f "$zip_path"
(cd "$publish_dir" && zip -rq "$zip_path" .)
"$dotnet_bin" publish "$repo_root/src/PhysOn.Desktop/PhysOn.Desktop.csproj" \
-c "$configuration" \
-r "$runtime" \
--self-contained true \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:EnableCompressionInSingleFile=true \
-p:DebugSymbols=false \
-p:DebugType=None \
-o "$onefile_dir"
cp "$onefile_dir/KoTalk.exe" "$portable_exe_path"
makensis \
-DAPP_VERSION="$version" \
-DAPP_ICON="$app_icon" \
-DSOURCE_DIR="$publish_dir" \
-DOUTPUT_FILE="$installer_path" \
"$installer_script" >/dev/null
(
cd "$output_dir"
sha256sum \
"$(basename "$zip_path")" \
"$(basename "$portable_exe_path")" \
"$(basename "$installer_path")" \
> SHA256SUMS.txt
)
echo "Built Windows release assets in $output_dir"

View file

@ -8,6 +8,10 @@ Usage:
Options:
--windows-zip <path> Windows x64 ZIP artifact path
--windows-portable-exe <path>
Windows onefile portable EXE artifact path
--windows-installer-exe <path>
Windows installer EXE artifact path
--android-apk <path> Android universal APK artifact path
--zip <path> Backward-compatible alias for --windows-zip
--channel <name> Release channel. Default: alpha
@ -23,6 +27,8 @@ EOF
version=""
channel="alpha"
windows_zip=""
windows_portable_exe=""
windows_installer_exe=""
android_apk=""
notes_path=""
screenshots_dir=""
@ -42,6 +48,14 @@ while [[ $# -gt 0 ]]; do
windows_zip="${2:-}"
shift 2
;;
--windows-portable-exe)
windows_portable_exe="${2:-}"
shift 2
;;
--windows-installer-exe)
windows_installer_exe="${2:-}"
shift 2
;;
--android-apk)
android_apk="${2:-}"
shift 2
@ -75,8 +89,8 @@ if [[ -z "$version" ]]; then
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
if [[ -z "$windows_zip" && -z "$windows_portable_exe" && -z "$windows_installer_exe" && -z "$android_apk" ]]; then
echo "At least one artifact must be provided for Windows or Android." >&2
usage >&2
exit 1
fi
@ -86,6 +100,16 @@ if [[ -n "$windows_zip" && ! -f "$windows_zip" ]]; then
exit 1
fi
if [[ -n "$windows_portable_exe" && ! -f "$windows_portable_exe" ]]; then
echo "Windows portable EXE artifact not found: $windows_portable_exe" >&2
exit 1
fi
if [[ -n "$windows_installer_exe" && ! -f "$windows_installer_exe" ]]; then
echo "Windows installer EXE artifact not found: $windows_installer_exe" >&2
exit 1
fi
if [[ -n "$android_apk" && ! -f "$android_apk" ]]; then
echo "Android APK artifact not found: $android_apk" >&2
exit 1
@ -146,6 +170,31 @@ else
fi
cp "$release_root/RELEASE_NOTES.ko.md" "$latest_root/RELEASE_NOTES.ko.md"
append_screenshot_gallery() {
local notes_file="$1"
local screenshot_root_url="$2"
if [[ -z "$screenshots_dir" ]]; then
return 0
fi
mapfile -t note_screenshots < <(find "$screenshots_dir" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | sort)
if [[ ${#note_screenshots[@]} -eq 0 ]]; then
return 0
fi
{
printf '\n## 최신 화면\n\n'
printf '스크린샷은 릴리즈 Assets 대신 변경 노트 안에서 직접 확인할 수 있도록 정리합니다.\n\n'
for screenshot in "${note_screenshots[@]}"; do
name="$(basename "$screenshot")"
label="${name%.*}"
printf '### %s\n\n' "$label"
printf '![%s](%s/%s)\n\n' "$label" "$screenshot_root_url" "$name"
done
} >> "$notes_file"
}
if [[ -n "$screenshots_dir" ]]; then
while IFS= read -r screenshot; do
cp "$screenshot" "$release_root/screenshots/$(basename "$screenshot")"
@ -153,6 +202,9 @@ if [[ -n "$screenshots_dir" ]]; then
done < <(find "$screenshots_dir" -maxdepth 1 -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | sort)
fi
append_screenshot_gallery "$release_root/RELEASE_NOTES.ko.md" "$download_base_url/releases/$version/screenshots"
cp "$release_root/RELEASE_NOTES.ko.md" "$latest_root/RELEASE_NOTES.ko.md"
platform_count=0
platforms_json=""
top_level_windows_alias=""
@ -168,6 +220,23 @@ append_platform_json() {
platform_count=$((platform_count + 1))
}
join_json_lines() {
local result=""
local total=$#
local index=0
local line=""
for line in "$@"; do
index=$((index + 1))
result+="$line"
if (( index < total )); then
result+=$',\n'
fi
done
printf '%s' "$result"
}
write_platform_version_json() {
local path="$1"
local body="$2"
@ -187,38 +256,69 @@ $body
EOF
}
if [[ -n "$windows_zip" ]]; then
windows_release_name="KoTalk-windows-x64-$version.zip"
windows_latest_name="KoTalk-windows-x64.zip"
if [[ -n "$windows_zip" || -n "$windows_portable_exe" || -n "$windows_installer_exe" ]]; then
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"
windows_release_hash_items=()
windows_latest_hash_items=()
windows_platform_lines=(
' "name": "KoTalk for Windows"'
' "kind": "desktop"'
' "arch": "x64"'
" \"landingUrl\": \"$download_base_url/windows/latest\""
)
if [[ -n "$windows_zip" ]]; then
windows_release_name="KoTalk-windows-x64-$version.zip"
windows_latest_name="KoTalk-windows-x64.zip"
cp "$windows_zip" "$windows_release_dir/$windows_release_name"
cp "$windows_zip" "$windows_latest_dir/$windows_latest_name"
release_hash_paths+=("windows/x64/$windows_release_name")
latest_hash_paths+=("windows/$windows_latest_name")
windows_release_hash_items+=("$windows_release_name")
windows_latest_hash_items+=("$windows_latest_name")
windows_platform_lines+=(" \"portableZipUrl\": \"$download_base_url/windows/latest/$windows_latest_name\"")
fi
if [[ -n "$windows_portable_exe" ]]; then
windows_onefile_release_name="KoTalk-windows-x64-onefile-$version.exe"
windows_onefile_latest_name="KoTalk-windows-x64-onefile.exe"
cp "$windows_portable_exe" "$windows_release_dir/$windows_onefile_release_name"
cp "$windows_portable_exe" "$windows_latest_dir/$windows_onefile_latest_name"
release_hash_paths+=("windows/x64/$windows_onefile_release_name")
latest_hash_paths+=("windows/$windows_onefile_latest_name")
windows_release_hash_items+=("$windows_onefile_release_name")
windows_latest_hash_items+=("$windows_onefile_latest_name")
windows_platform_lines+=(" \"portableExeUrl\": \"$download_base_url/windows/latest/$windows_onefile_latest_name\"")
fi
if [[ -n "$windows_installer_exe" ]]; then
windows_installer_release_name="KoTalk-windows-x64-installer-$version.exe"
windows_installer_latest_name="KoTalk-windows-x64-installer.exe"
cp "$windows_installer_exe" "$windows_release_dir/$windows_installer_release_name"
cp "$windows_installer_exe" "$windows_latest_dir/$windows_installer_latest_name"
release_hash_paths+=("windows/x64/$windows_installer_release_name")
latest_hash_paths+=("windows/$windows_installer_latest_name")
windows_release_hash_items+=("$windows_installer_release_name")
windows_latest_hash_items+=("$windows_installer_latest_name")
windows_platform_lines+=(" \"installerUrl\": \"$download_base_url/windows/latest/$windows_installer_latest_name\"")
fi
windows_platform_lines+=(" \"sha256Url\": \"$download_base_url/windows/latest/SHA256SUMS.txt\"")
(
cd "$windows_release_dir"
sha256sum "$windows_release_name" > SHA256SUMS.txt
sha256sum "${windows_release_hash_items[@]}" > SHA256SUMS.txt
)
(
cd "$windows_latest_dir"
sha256sum "$windows_latest_name" > SHA256SUMS.txt
sha256sum "${windows_latest_hash_items[@]}" > SHA256SUMS.txt
)
release_hash_paths+=("windows/x64/$windows_release_name")
latest_hash_paths+=("windows/$windows_latest_name")
windows_platform_body="$(cat <<EOF
"name": "KoTalk for Windows",
"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
)"
windows_platform_body="$(join_json_lines "${windows_platform_lines[@]}")"
append_platform_json "$(cat <<EOF
"windows": {
@ -298,12 +398,12 @@ if (( ${#latest_hash_paths[@]} > 0 )); then
fi
windows_landing_card=""
if [[ -n "$windows_zip" ]]; then
if [[ -n "$windows_zip" || -n "$windows_portable_exe" || -n "$windows_installer_exe" ]]; then
windows_landing_card="$(cat <<EOF
<a class="card" href="/windows/latest">
<span class="eyebrow">Windows</span>
<strong>Latest Windows build</strong>
<span>ZIP package and SHA256 checksum</span>
<span>Installer, onefile portable, ZIP, SHA256</span>
</a>
EOF
)"
@ -321,6 +421,156 @@ EOF
)"
fi
windows_installer_card=""
if [[ -n "$windows_installer_exe" ]]; then
windows_installer_card="$(cat <<EOF
<a class="card" href="/windows/latest/KoTalk-windows-x64-installer.exe">
<span class="eyebrow">Installer</span>
<strong>Windows installer</strong>
<span>설치형 EXE</span>
</a>
EOF
)"
fi
windows_portable_card=""
if [[ -n "$windows_portable_exe" ]]; then
windows_portable_card="$(cat <<EOF
<a class="card" href="/windows/latest/KoTalk-windows-x64-onefile.exe">
<span class="eyebrow">Portable</span>
<strong>Onefile executable</strong>
<span>압축 해제 없이 바로 실행</span>
</a>
EOF
)"
fi
windows_zip_card=""
if [[ -n "$windows_zip" ]]; then
windows_zip_card="$(cat <<EOF
<a class="card" href="/windows/latest/KoTalk-windows-x64.zip">
<span class="eyebrow">Archive</span>
<strong>Extracted bundle ZIP</strong>
<span>폴더 배포본 압축 파일</span>
</a>
EOF
)"
fi
if [[ -d "${latest_root}/windows" ]]; then
cat > "${latest_root}/windows/index.html" <<EOF
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>KoTalk for Windows</title>
<style>
:root {
color-scheme: light;
--bg: #f7f3ee;
--surface: #ffffff;
--border: #ddd1c4;
--text: #20242b;
--soft: #5f5a54;
--accent: #f05b2b;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, "Segoe UI", system-ui, sans-serif; }
main { max-width: 920px; margin: 0 auto; padding: 40px 20px 56px; }
.hero, .card { background: var(--surface); border: 1px solid var(--border); }
.hero { padding: 24px; }
h1 { margin: 0 0 10px; font-size: 30px; letter-spacing: -0.04em; }
p { margin: 0; line-height: 1.6; color: var(--soft); }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-top: 20px; }
.card { display: flex; flex-direction: column; gap: 8px; padding: 16px; text-decoration: none; color: inherit; }
.card:hover { border-color: var(--accent); }
.eyebrow { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--soft); }
.meta { margin-top: 16px; color: var(--soft); }
.meta a { color: inherit; }
</style>
</head>
<body>
<main>
<section class="hero">
<h1>KoTalk for Windows</h1>
<p>설치형과 onefile portable, 압축본을 같은 기준선으로 제공합니다.</p>
<div class="grid">
$windows_installer_card
$windows_portable_card
$windows_zip_card
<a class="card" href="/windows/latest/SHA256SUMS.txt">
<span class="eyebrow">Integrity</span>
<strong>SHA256SUMS</strong>
<span>체크섬 확인</span>
</a>
</div>
<p class="meta"><a href="/latest/version.json">version.json</a></p>
</section>
</main>
</body>
</html>
EOF
fi
if [[ -d "${latest_root}/android" ]]; then
cat > "${latest_root}/android/index.html" <<EOF
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>KoTalk for Android</title>
<style>
:root {
color-scheme: light;
--bg: #f7f3ee;
--surface: #ffffff;
--border: #ddd1c4;
--text: #20242b;
--soft: #5f5a54;
--accent: #f05b2b;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, "Segoe UI", system-ui, sans-serif; }
main { max-width: 760px; margin: 0 auto; padding: 40px 20px 56px; }
.hero, .card { background: var(--surface); border: 1px solid var(--border); }
.hero { padding: 24px; }
h1 { margin: 0 0 10px; font-size: 30px; letter-spacing: -0.04em; }
p { margin: 0; line-height: 1.6; color: var(--soft); }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-top: 20px; }
.card { display: flex; flex-direction: column; gap: 8px; padding: 16px; text-decoration: none; color: inherit; }
.card:hover { border-color: var(--accent); }
.eyebrow { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--soft); }
.meta { margin-top: 16px; color: var(--soft); }
.meta a { color: inherit; }
</style>
</head>
<body>
<main>
<section class="hero">
<h1>KoTalk for Android</h1>
<p>KoTalk Android shell APK와 버전 메타데이터를 같은 기준선으로 제공합니다.</p>
<div class="grid">
<a class="card" href="/android/latest/KoTalk-android-universal.apk">
<span class="eyebrow">APK</span>
<strong>Universal APK</strong>
<span>직접 설치용 패키지</span>
</a>
<a class="card" href="/android/latest/SHA256SUMS.txt">
<span class="eyebrow">Integrity</span>
<strong>SHA256SUMS</strong>
<span>체크섬 확인</span>
</a>
</div>
<p class="meta"><a href="/latest/version.json">version.json</a></p>
</section>
</main>
</body>
</html>
EOF
fi
cat > "$download_root/index.html" <<EOF
<!doctype html>
<html lang="ko">

View file

@ -168,9 +168,11 @@ case "$version" in
esac
mapfile -t asset_files < <(
find "$release_root" -type f \
\( -name '*.zip' -o -name '*.apk' -o -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' -o -name 'RELEASE_NOTES.ko.md' -o -name 'SHA256SUMS.txt' -o -name 'version.json' \) \
| sort
{
find "$release_root" -type f \( -name '*.zip' -o -name '*.exe' -o -name '*.apk' \)
[[ -f "$release_root/SHA256SUMS.txt" ]] && printf '%s\n' "$release_root/SHA256SUMS.txt"
[[ -f "$release_root/version.json" ]] && printf '%s\n' "$release_root/version.json"
} | sort
)
if [[ ${#asset_files[@]} -eq 0 ]]; then

View file

@ -123,9 +123,11 @@ case "$version" in
esac
mapfile -t asset_files < <(
find "$release_root" -type f \
\( -name '*.zip' -o -name '*.apk' -o -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' -o -name 'RELEASE_NOTES.ko.md' -o -name 'SHA256SUMS.txt' -o -name 'version.json' \) \
| sort
{
find "$release_root" -type f \( -name '*.zip' -o -name '*.exe' -o -name '*.apk' \)
[[ -f "$release_root/SHA256SUMS.txt" ]] && printf '%s\n' "$release_root/SHA256SUMS.txt"
[[ -f "$release_root/version.json" ]] && printf '%s\n' "$release_root/version.json"
} | sort
)
if [[ ${#asset_files[@]} -eq 0 ]]; then