공개: alpha.4 기준선 갱신
This commit is contained in:
parent
debf62f76e
commit
b63832706b
37 changed files with 1839 additions and 822 deletions
4
scripts/capture_kotalk_desktop_screenshots.sh
Executable file
4
scripts/capture_kotalk_desktop_screenshots.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
"$SCRIPT_DIR/ci/capture-kotalk-desktop-screenshots.sh" "$@"
|
||||
206
scripts/ci/capture-kotalk-desktop-screenshots.sh
Executable file
206
scripts/ci/capture-kotalk-desktop-screenshots.sh
Executable file
|
|
@ -0,0 +1,206 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
OUTPUT_DIR="${1:-$ROOT_DIR/docs/assets/latest}"
|
||||
CAPTURE_MODE="${2:-all}"
|
||||
PROJECT_PATH="$ROOT_DIR/src/PhysOn.Desktop/PhysOn.Desktop.csproj"
|
||||
DOTNET_BIN="${DOTNET_BIN:-$HOME/.dotnet/dotnet}"
|
||||
|
||||
if [[ ! -x "$DOTNET_BIN" ]]; then
|
||||
echo "dotnet not found at $DOTNET_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
capture_mode() {
|
||||
local mode="$1"
|
||||
local output_path="$2"
|
||||
local main_title="$3"
|
||||
local detached_title="${4:-}"
|
||||
local data_home="$TMP_DIR/$mode-data"
|
||||
local config_home="$TMP_DIR/$mode-config"
|
||||
local cache_home="$TMP_DIR/$mode-cache"
|
||||
local runtime_home="$TMP_DIR/$mode-runtime"
|
||||
local tree_path="$TMP_DIR/$mode-tree.txt"
|
||||
local log_path="$TMP_DIR/$mode.log"
|
||||
mkdir -p "$data_home" "$config_home" "$cache_home" "$runtime_home"
|
||||
|
||||
env \
|
||||
XDG_DATA_HOME="$data_home" \
|
||||
XDG_CONFIG_HOME="$config_home" \
|
||||
XDG_CACHE_HOME="$cache_home" \
|
||||
XDG_RUNTIME_DIR="$runtime_home" \
|
||||
DOTNET_BIN="$DOTNET_BIN" \
|
||||
PROJECT_PATH="$PROJECT_PATH" \
|
||||
TREE_PATH="$tree_path" \
|
||||
LOG_PATH="$log_path" \
|
||||
OUTPUT_PATH="$output_path" \
|
||||
MAIN_TITLE="$main_title" \
|
||||
DETACHED_TITLE="$detached_title" \
|
||||
MODE="$mode" \
|
||||
xvfb-run -a bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
refresh_tree() {
|
||||
xwininfo -root -tree >"$TREE_PATH" 2>/dev/null || true
|
||||
}
|
||||
|
||||
find_window_id() {
|
||||
local title="$1"
|
||||
python3 - "$TREE_PATH" "$title" <<'"'"'PY'"'"'
|
||||
import re
|
||||
import sys
|
||||
|
||||
tree_path, title = sys.argv[1], sys.argv[2]
|
||||
pattern = re.compile(r"^\s*(0x[0-9a-fA-F]+)\s+\"([^\"]+)\"")
|
||||
|
||||
with open(tree_path, "r", encoding="utf-8", errors="ignore") as handle:
|
||||
for line in handle:
|
||||
match = pattern.match(line)
|
||||
if not match:
|
||||
continue
|
||||
window_id, window_title = match.groups()
|
||||
if window_title == title:
|
||||
print(window_id)
|
||||
raise SystemExit(0)
|
||||
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
wait_for_window() {
|
||||
local title="$1"
|
||||
local attempts="${2:-40}"
|
||||
local pause="${3:-0.5}"
|
||||
local window_id=""
|
||||
|
||||
for _ in $(seq 1 "$attempts"); do
|
||||
refresh_tree
|
||||
if window_id="$(find_window_id "$title" 2>/dev/null)"; then
|
||||
echo "$window_id"
|
||||
return 0
|
||||
fi
|
||||
sleep "$pause"
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
window_geometry() {
|
||||
local window_id="$1"
|
||||
python3 - "$window_id" <<'"'"'PY'"'"'
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
window_id = sys.argv[1]
|
||||
output = subprocess.check_output(["xwininfo", "-id", window_id], text=True, errors="ignore")
|
||||
patterns = {
|
||||
"x": r"Absolute upper-left X:\s+(-?\d+)",
|
||||
"y": r"Absolute upper-left Y:\s+(-?\d+)",
|
||||
"w": r"Width:\s+(\d+)",
|
||||
"h": r"Height:\s+(\d+)",
|
||||
}
|
||||
|
||||
values = {}
|
||||
for key, pattern in patterns.items():
|
||||
match = re.search(pattern, output)
|
||||
if not match:
|
||||
raise SystemExit(1)
|
||||
values[key] = int(match.group(1))
|
||||
|
||||
if values["w"] < 240 or values["h"] < 240:
|
||||
raise SystemExit(2)
|
||||
|
||||
print(values["x"], values["y"], values["w"], values["h"])
|
||||
PY
|
||||
}
|
||||
|
||||
wait_for_geometry() {
|
||||
local window_id="$1"
|
||||
local attempts="${2:-30}"
|
||||
local pause="${3:-0.4}"
|
||||
local geometry=""
|
||||
local previous=""
|
||||
|
||||
for _ in $(seq 1 "$attempts"); do
|
||||
if geometry="$(window_geometry "$window_id" 2>/dev/null)"; then
|
||||
if [[ "$geometry" == "$previous" ]]; then
|
||||
echo "$geometry"
|
||||
return 0
|
||||
fi
|
||||
previous="$geometry"
|
||||
fi
|
||||
sleep "$pause"
|
||||
done
|
||||
|
||||
if [[ -n "$previous" ]]; then
|
||||
echo "$previous"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
capture_window() {
|
||||
local window_id="$1"
|
||||
local target_path="$2"
|
||||
local root_capture="${target_path%.*}-root.${target_path##*.}"
|
||||
local geometry
|
||||
read -r crop_x crop_y crop_w crop_h < <(wait_for_geometry "$window_id")
|
||||
import -window root "$root_capture"
|
||||
convert "$root_capture" -crop "${crop_w}x${crop_h}+${crop_x}+${crop_y}" +repage "$target_path"
|
||||
rm -f "$root_capture"
|
||||
}
|
||||
|
||||
create_conversation_fallback() {
|
||||
local source_path="$1"
|
||||
local target_path="$2"
|
||||
convert "$source_path" -gravity east -crop 58%x84%+0+0 +repage "$target_path"
|
||||
}
|
||||
|
||||
if [[ "$MODE" == "sample" ]]; then
|
||||
export KOTALK_DESKTOP_SAMPLE_MODE=1
|
||||
export KOTALK_DESKTOP_OPEN_SAMPLE_WINDOW=1
|
||||
fi
|
||||
export XDG_DATA_HOME="$XDG_DATA_HOME"
|
||||
export XDG_CONFIG_HOME="$XDG_CONFIG_HOME"
|
||||
export XDG_CACHE_HOME="$XDG_CACHE_HOME"
|
||||
export XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR"
|
||||
|
||||
"$DOTNET_BIN" run --project "$PROJECT_PATH" -c Debug >"$LOG_PATH" 2>&1 &
|
||||
app_pid=$!
|
||||
|
||||
cleanup() {
|
||||
kill "$app_pid" >/dev/null 2>&1 || true
|
||||
wait "$app_pid" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
main_id="$(wait_for_window "$MAIN_TITLE" 60 0.5)"
|
||||
capture_window "$main_id" "$OUTPUT_PATH"
|
||||
|
||||
if [[ -n "$DETACHED_TITLE" ]]; then
|
||||
conversation_output="${OUTPUT_PATH%/*}/conversation.${OUTPUT_PATH##*.}"
|
||||
if detached_id="$(wait_for_window "$DETACHED_TITLE" 12 0.5 2>/dev/null)"; then
|
||||
capture_window "$detached_id" "$conversation_output"
|
||||
else
|
||||
create_conversation_fallback "$OUTPUT_PATH" "$conversation_output"
|
||||
fi
|
||||
fi
|
||||
'
|
||||
}
|
||||
|
||||
if [[ "$CAPTURE_MODE" == "all" || "$CAPTURE_MODE" == "onboarding" ]]; then
|
||||
capture_mode "onboarding" "$OUTPUT_DIR/onboarding.png" "KoTalk"
|
||||
fi
|
||||
|
||||
if [[ "$CAPTURE_MODE" == "all" || "$CAPTURE_MODE" == "sample" ]]; then
|
||||
capture_mode "sample" "$OUTPUT_DIR/hero-shell.png" "KoTalk" "제품 운영"
|
||||
fi
|
||||
|
||||
echo "Desktop screenshots written to $OUTPUT_DIR"
|
||||
5
scripts/create-release-tag.sh
Executable file
5
scripts/create-release-tag.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "$script_dir/release/release-create-tag.sh" "$@"
|
||||
5
scripts/publish-github-release.sh
Executable file
5
scripts/publish-github-release.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "$script_dir/release/release-publish-github.sh" "$@"
|
||||
5
scripts/publish-public-release.sh
Executable file
5
scripts/publish-public-release.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "$script_dir/release/release-publish-public.sh" "$@"
|
||||
102
scripts/release/release-create-tag.sh
Executable file
102
scripts/release/release-create-tag.sh
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/release/release-create-tag.sh --version 2026.04.16-alpha.4 [options]
|
||||
|
||||
Options:
|
||||
--ref <git-ref> Target ref or commit. Default: public/main
|
||||
--message <text> Annotated tag message
|
||||
--remote <name> Push tag to a remote after creation
|
||||
--force Replace an existing local tag
|
||||
--dry-run Print what would happen
|
||||
EOF
|
||||
}
|
||||
|
||||
version=""
|
||||
target_ref="public/main"
|
||||
message=""
|
||||
remote_name=""
|
||||
force="false"
|
||||
dry_run="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version)
|
||||
version="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ref)
|
||||
target_ref="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--message)
|
||||
message="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--remote)
|
||||
remote_name="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--force)
|
||||
force="true"
|
||||
shift
|
||||
;;
|
||||
--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)"
|
||||
cd "$repo_root"
|
||||
|
||||
target_commit="$(git rev-parse "$target_ref")"
|
||||
tag_message="${message:-Release $version}"
|
||||
|
||||
echo "Tag: $version"
|
||||
echo "Target ref: $target_ref"
|
||||
echo "Target commit: $target_commit"
|
||||
[[ -n "$remote_name" ]] && echo "Remote: $remote_name"
|
||||
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if git rev-parse -q --verify "refs/tags/$version" >/dev/null; then
|
||||
if [[ "$force" != "true" ]]; then
|
||||
echo "Tag already exists: $version" >&2
|
||||
echo "Use --force to replace it." >&2
|
||||
exit 1
|
||||
fi
|
||||
git tag -d "$version" >/dev/null
|
||||
fi
|
||||
|
||||
git tag -a "$version" "$target_commit" -m "$tag_message"
|
||||
|
||||
if [[ -n "$remote_name" ]]; then
|
||||
push_args=()
|
||||
if [[ "$force" == "true" ]]; then
|
||||
push_args+=(--force)
|
||||
fi
|
||||
git push "${push_args[@]}" "$remote_name" "refs/tags/$version:refs/tags/$version"
|
||||
fi
|
||||
|
||||
echo "Created annotated tag $version at $target_commit"
|
||||
|
|
@ -16,7 +16,7 @@ Options:
|
|||
--force Overwrite an existing release folder
|
||||
|
||||
Environment:
|
||||
DOWNLOAD_BASE_URL Defaults to https://download-vs-messanger.phy.kr
|
||||
DOWNLOAD_BASE_URL Defaults to https://download-vstalk.phy.kr
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ 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}"
|
||||
download_base_url="${DOWNLOAD_BASE_URL:-https://download-vstalk.phy.kr}"
|
||||
published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
|
||||
derive_release_url() {
|
||||
|
|
@ -179,8 +179,8 @@ EOF
|
|||
}
|
||||
|
||||
if [[ -n "$windows_zip" ]]; then
|
||||
windows_release_name="VsMessenger-win-x64-$version.zip"
|
||||
windows_latest_name="VsMessenger-win-x64.zip"
|
||||
windows_release_name="KoTalk-windows-x64-$version.zip"
|
||||
windows_latest_name="KoTalk-windows-x64.zip"
|
||||
windows_release_dir="$release_root/windows/x64"
|
||||
windows_latest_dir="$latest_root/windows"
|
||||
mkdir -p "$windows_release_dir" "$windows_latest_dir"
|
||||
|
|
@ -229,8 +229,8 @@ EOF
|
|||
fi
|
||||
|
||||
if [[ -n "$android_apk" ]]; then
|
||||
android_release_name="VsMessenger-android-universal-$version.apk"
|
||||
android_latest_name="VsMessenger-android-universal.apk"
|
||||
android_release_name="KoTalk-android-universal-$version.apk"
|
||||
android_latest_name="KoTalk-android-universal.apk"
|
||||
android_release_dir="$release_root/android/universal"
|
||||
android_latest_dir="$latest_root/android"
|
||||
mkdir -p "$android_release_dir" "$android_latest_dir"
|
||||
|
|
|
|||
|
|
@ -7,17 +7,23 @@ Usage:
|
|||
./scripts/release/release-publish-forge.sh --version v0.2.0-alpha.1 [options]
|
||||
|
||||
Options:
|
||||
--remote <name> Git remote name. Default: public-stage
|
||||
--base-url <url> Forge base URL. Example: https://forge.example.com
|
||||
--repo <owner/name> Repository in owner/name form
|
||||
--token <token> Gitea API token
|
||||
--target-commitish <ref> Branch or commit to associate when creating the tag
|
||||
--notes <path> Release notes markdown file
|
||||
--dry-run Print planned uploads without calling the API
|
||||
EOF
|
||||
}
|
||||
|
||||
version=""
|
||||
remote_name="public-stage"
|
||||
base_url=""
|
||||
repo_full_name=""
|
||||
token="${FORGE_RELEASE_TOKEN:-${GITEA_RELEASE_TOKEN:-}}"
|
||||
target_commitish=""
|
||||
notes_path=""
|
||||
dry_run="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
|
|
@ -30,6 +36,10 @@ while [[ $# -gt 0 ]]; do
|
|||
base_url="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--remote)
|
||||
remote_name="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--repo)
|
||||
repo_full_name="${2:-}"
|
||||
shift 2
|
||||
|
|
@ -38,6 +48,14 @@ while [[ $# -gt 0 ]]; do
|
|||
token="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--target-commitish)
|
||||
target_commitish="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--notes)
|
||||
notes_path="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run="true"
|
||||
shift
|
||||
|
|
@ -67,20 +85,24 @@ if [[ ! -d "$release_root" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
origin_url="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
|
||||
remote_url="$(git -C "$repo_root" remote get-url "$remote_name" 2>/dev/null || true)"
|
||||
|
||||
if [[ -z "$base_url" ]]; then
|
||||
if [[ "$origin_url" =~ ^(https?://[^/]+)/(.+)\.git$ ]]; then
|
||||
if [[ "$remote_url" =~ ^(https?://[^/]+)(/open-source/projects)/([^/]+)/([^/]+?)(\.git)?$ ]]; then
|
||||
base_url="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
|
||||
elif [[ "$remote_url" =~ ^(https?://[^/]+)/(.+?)(\.git)?$ ]]; then
|
||||
base_url="${BASH_REMATCH[1]}"
|
||||
elif [[ "$origin_url" =~ ^git@([^:]+):(.+)\.git$ ]]; then
|
||||
elif [[ "$remote_url" =~ ^git@([^:]+):(.+)\.git$ ]]; then
|
||||
base_url="https://${BASH_REMATCH[1]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$repo_full_name" ]]; then
|
||||
if [[ "$origin_url" =~ ^https?://[^/]+/(.+)\.git$ ]]; then
|
||||
if [[ "$remote_url" =~ ^https?://[^/]+/open-source/projects/([^/]+)/([^/]+?)(\.git)?$ ]]; then
|
||||
repo_full_name="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}"
|
||||
elif [[ "$remote_url" =~ ^https?://[^/]+/(.+?)(\.git)?$ ]]; then
|
||||
repo_full_name="${BASH_REMATCH[1]}"
|
||||
elif [[ "$origin_url" =~ ^git@[^:]+:(.+)\.git$ ]]; then
|
||||
elif [[ "$remote_url" =~ ^git@[^:]+:(.+)\.git$ ]]; then
|
||||
repo_full_name="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
fi
|
||||
|
|
@ -90,8 +112,47 @@ if [[ -z "$base_url" || -z "$repo_full_name" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$dry_run" != "true" && -z "$token" ]]; then
|
||||
echo "A Gitea API token is required. Use --token or FORGE_RELEASE_TOKEN." >&2
|
||||
if [[ -z "$notes_path" ]]; then
|
||||
notes_path="$release_root/RELEASE_NOTES.ko.md"
|
||||
fi
|
||||
|
||||
if [[ -n "$notes_path" && ! -f "$notes_path" ]]; then
|
||||
echo "Release notes file not found: $notes_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$target_commitish" ]]; then
|
||||
target_commitish="main"
|
||||
fi
|
||||
|
||||
basic_auth_user=""
|
||||
basic_auth_password=""
|
||||
|
||||
if [[ -z "$token" ]]; then
|
||||
for fallback_path in \
|
||||
"$repo_root/.workspace-secrets/${remote_name}.token" \
|
||||
"$repo_root/.workspace-secrets/forge-release.token"; do
|
||||
if [[ -f "$fallback_path" ]]; then
|
||||
token="$(tr -d '\r\n' < "$fallback_path")"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "$token" && -n "$remote_url" ]]; then
|
||||
credential_output="$(printf 'url=%s\n\n' "$remote_url" | git credential fill 2>/dev/null || true)"
|
||||
if [[ -n "$credential_output" ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
username) basic_auth_user="$value" ;;
|
||||
password) basic_auth_password="$value" ;;
|
||||
esac
|
||||
done <<< "$credential_output"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$dry_run" != "true" && -z "$token" && ( -z "$basic_auth_user" || -z "$basic_auth_password" ) ]]; then
|
||||
echo "A Gitea API token or basic credential is required. Use --token, FORGE_RELEASE_TOKEN, a local secret file, or configured git credentials." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -108,7 +169,7 @@ 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' \) \
|
||||
\( -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
|
||||
)
|
||||
|
||||
|
|
@ -118,7 +179,9 @@ if [[ ${#asset_files[@]} -eq 0 ]]; then
|
|||
fi
|
||||
|
||||
echo "Forge release target: $base_url/$repo_full_name"
|
||||
echo "Remote: $remote_name"
|
||||
echo "Version: $version"
|
||||
echo "Target commitish: $target_commitish"
|
||||
printf 'Assets:\n'
|
||||
printf ' - %s\n' "${asset_files[@]#$release_root/}"
|
||||
|
||||
|
|
@ -126,11 +189,16 @@ if [[ "$dry_run" == "true" ]]; then
|
|||
exit 0
|
||||
fi
|
||||
|
||||
auth_header="Authorization: token $token"
|
||||
auth_args=()
|
||||
if [[ -n "$token" ]]; then
|
||||
auth_args=(-H "Authorization: token $token")
|
||||
else
|
||||
auth_args=(-u "$basic_auth_user:$basic_auth_password")
|
||||
fi
|
||||
tmp_response="$(mktemp)"
|
||||
trap 'rm -f "$tmp_response"' EXIT
|
||||
|
||||
existing_status="$(curl -sS -o "$tmp_response" -w '%{http_code}' -H "$auth_header" "$tag_api")"
|
||||
existing_status="$(curl -sS -o "$tmp_response" -w '%{http_code}' "${auth_args[@]}" "$tag_api")"
|
||||
if [[ "$existing_status" == "200" ]]; then
|
||||
existing_release_id="$(python3 - <<'PY' "$tmp_response"
|
||||
import json, sys
|
||||
|
|
@ -138,17 +206,17 @@ 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
|
||||
curl -sS -X DELETE "${auth_args[@]}" "${release_api}/${existing_release_id}" >/dev/null
|
||||
fi
|
||||
|
||||
release_body=$'Windows와 Android 클라이언트 산출물을 병렬로 정리한 릴리즈입니다.\n\n'
|
||||
release_body+=$'동일 버전 번호 아래 OS별 자산을 함께 게시하며, 최신 다운로드 채널은 download-vs-messanger.phy.kr에서 운영합니다.'
|
||||
release_body="$(cat "$notes_path")"
|
||||
|
||||
create_payload="$(python3 - <<'PY' "$version" "$release_body" "$pre_release"
|
||||
create_payload="$(python3 - <<'PY' "$version" "$release_body" "$pre_release" "$target_commitish"
|
||||
import json, sys
|
||||
version, body, prerelease = sys.argv[1], sys.argv[2], sys.argv[3] == "true"
|
||||
version, body, prerelease, target_commitish = sys.argv[1], sys.argv[2], sys.argv[3] == "true", sys.argv[4]
|
||||
print(json.dumps({
|
||||
"tag_name": version,
|
||||
"target_commitish": target_commitish,
|
||||
"name": version,
|
||||
"body": body,
|
||||
"draft": False,
|
||||
|
|
@ -159,7 +227,7 @@ PY
|
|||
|
||||
create_status="$(curl -sS -o "$tmp_response" -w '%{http_code}' \
|
||||
-X POST \
|
||||
-H "$auth_header" \
|
||||
"${auth_args[@]}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$create_payload" \
|
||||
"$release_api")"
|
||||
|
|
@ -181,7 +249,7 @@ for asset in "${asset_files[@]}"; do
|
|||
name="$(basename "$asset")"
|
||||
curl -sS \
|
||||
-X POST \
|
||||
-H "$auth_header" \
|
||||
"${auth_args[@]}" \
|
||||
-H 'Content-Type: application/octet-stream' \
|
||||
--data-binary @"$asset" \
|
||||
"${release_api}/${release_id}/assets?name=${name}" >/dev/null
|
||||
|
|
|
|||
226
scripts/release/release-publish-github.sh
Executable file
226
scripts/release/release-publish-github.sh
Executable file
|
|
@ -0,0 +1,226 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/release/release-publish-github.sh --version 2026.04.16-alpha.4 [options]
|
||||
|
||||
Options:
|
||||
--remote <name> Git remote name. Default: github-public
|
||||
--repo <owner/name> GitHub repository in owner/name form
|
||||
--token <token> GitHub token
|
||||
--target-commitish <ref> Branch or commit to associate when creating the tag
|
||||
--notes <path> Release notes markdown file
|
||||
--dry-run Print planned uploads without calling the API
|
||||
EOF
|
||||
}
|
||||
|
||||
version=""
|
||||
remote_name="github-public"
|
||||
repo_full_name=""
|
||||
token="${GITHUB_RELEASE_TOKEN:-${GITHUB_TOKEN:-}}"
|
||||
target_commitish=""
|
||||
notes_path=""
|
||||
dry_run="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version)
|
||||
version="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--remote)
|
||||
remote_name="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--repo)
|
||||
repo_full_name="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--token)
|
||||
token="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--target-commitish)
|
||||
target_commitish="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--notes)
|
||||
notes_path="${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
|
||||
|
||||
remote_url="$(git -C "$repo_root" remote get-url "$remote_name" 2>/dev/null || true)"
|
||||
if [[ -z "$repo_full_name" ]]; then
|
||||
if [[ "$remote_url" =~ ^https?://github\.com/(.+?)(\.git)?$ ]]; then
|
||||
repo_full_name="${BASH_REMATCH[1]}"
|
||||
elif [[ "$remote_url" =~ ^git@github\.com:(.+)\.git$ ]]; then
|
||||
repo_full_name="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$repo_full_name" ]]; then
|
||||
echo "Unable to infer GitHub repository name from remote: $remote_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$token" && -f "$repo_root/.workspace-secrets/github-public.pat" ]]; then
|
||||
token="$(tr -d '\r\n' < "$repo_root/.workspace-secrets/github-public.pat")"
|
||||
fi
|
||||
|
||||
if [[ "$dry_run" != "true" && -z "$token" ]]; then
|
||||
echo "A GitHub token is required. Use --token, GITHUB_RELEASE_TOKEN, or GITHUB_TOKEN." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$notes_path" ]]; then
|
||||
notes_path="$release_root/RELEASE_NOTES.ko.md"
|
||||
fi
|
||||
|
||||
if [[ -n "$notes_path" && ! -f "$notes_path" ]]; then
|
||||
echo "Release notes file not found: $notes_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$target_commitish" ]]; then
|
||||
target_commitish="main"
|
||||
fi
|
||||
|
||||
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 '*.png' -o -name '*.jpg' -o -name '*.jpeg' -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 "GitHub release target: https://github.com/$repo_full_name"
|
||||
echo "Remote: $remote_name"
|
||||
echo "Version: $version"
|
||||
echo "Target commitish: $target_commitish"
|
||||
printf 'Assets:\n'
|
||||
printf ' - %s\n' "${asset_files[@]#$release_root/}"
|
||||
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
api_root="https://api.github.com/repos/${repo_full_name}"
|
||||
release_api="${api_root}/releases"
|
||||
tag_api="${release_api}/tags/${version}"
|
||||
auth_header="Authorization: Bearer $token"
|
||||
accept_header="Accept: application/vnd.github+json"
|
||||
api_version_header="X-GitHub-Api-Version: 2022-11-28"
|
||||
tmp_response="$(mktemp)"
|
||||
trap 'rm -f "$tmp_response"' EXIT
|
||||
|
||||
existing_status="$(curl -sS -o "$tmp_response" -w '%{http_code}' \
|
||||
-H "$auth_header" \
|
||||
-H "$accept_header" \
|
||||
-H "$api_version_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" \
|
||||
-H "$accept_header" \
|
||||
-H "$api_version_header" \
|
||||
"${release_api}/${existing_release_id}" >/dev/null
|
||||
fi
|
||||
|
||||
release_body="$(cat "$notes_path")"
|
||||
create_payload="$(python3 - <<'PY' "$version" "$release_body" "$pre_release" "$target_commitish"
|
||||
import json, sys
|
||||
version, body, prerelease, target_commitish = sys.argv[1], sys.argv[2], sys.argv[3] == "true", sys.argv[4]
|
||||
print(json.dumps({
|
||||
"tag_name": version,
|
||||
"target_commitish": target_commitish,
|
||||
"name": version,
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": prerelease,
|
||||
"make_latest": "false" if prerelease else "true",
|
||||
}, ensure_ascii=False))
|
||||
PY
|
||||
)"
|
||||
|
||||
create_status="$(curl -sS -o "$tmp_response" -w '%{http_code}' \
|
||||
-X POST \
|
||||
-H "$auth_header" \
|
||||
-H "$accept_header" \
|
||||
-H "$api_version_header" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$create_payload" \
|
||||
"$release_api")"
|
||||
|
||||
if [[ "$create_status" != "201" ]]; then
|
||||
echo "Failed to create GitHub release. HTTP $create_status" >&2
|
||||
cat "$tmp_response" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
upload_url="$(python3 - <<'PY' "$tmp_response"
|
||||
import json, sys
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||||
print(json.load(fh)["upload_url"].split("{", 1)[0])
|
||||
PY
|
||||
)"
|
||||
|
||||
for asset in "${asset_files[@]}"; do
|
||||
name="$(basename "$asset")"
|
||||
curl -sS \
|
||||
-X POST \
|
||||
-H "$auth_header" \
|
||||
-H "$accept_header" \
|
||||
-H "$api_version_header" \
|
||||
-H 'Content-Type: application/octet-stream' \
|
||||
--data-binary @"$asset" \
|
||||
"${upload_url}?name=${name}" >/dev/null
|
||||
done
|
||||
|
||||
echo "Published GitHub release $version with ${#asset_files[@]} assets."
|
||||
181
scripts/release/release-publish-public.sh
Executable file
181
scripts/release/release-publish-public.sh
Executable file
|
|
@ -0,0 +1,181 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/release/release-publish-public.sh --version 2026.04.16-alpha.4 [options]
|
||||
|
||||
Options:
|
||||
--branch <name> Source branch to publish. Default: public/main
|
||||
--target-branch <name> Target branch name on public remotes. Default: main
|
||||
--stage-remote <name> Stage remote. Default: public-stage
|
||||
--github-remote <name> GitHub remote. Default: github-public
|
||||
--notes <path> Release notes markdown file
|
||||
--skip-github Publish to stage only
|
||||
--skip-stage Publish to GitHub only
|
||||
--force-tag Replace an existing local or remote tag
|
||||
--dry-run Print the planned sequence only
|
||||
EOF
|
||||
}
|
||||
|
||||
version=""
|
||||
branch_name="public/main"
|
||||
target_branch="main"
|
||||
stage_remote="public-stage"
|
||||
github_remote="github-public"
|
||||
notes_path=""
|
||||
skip_github="false"
|
||||
skip_stage="false"
|
||||
force_tag="false"
|
||||
dry_run="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version)
|
||||
version="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--branch)
|
||||
branch_name="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--target-branch)
|
||||
target_branch="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--stage-remote)
|
||||
stage_remote="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--github-remote)
|
||||
github_remote="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--notes)
|
||||
notes_path="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--skip-github)
|
||||
skip_github="true"
|
||||
shift
|
||||
;;
|
||||
--skip-stage)
|
||||
skip_stage="true"
|
||||
shift
|
||||
;;
|
||||
--force-tag)
|
||||
force_tag="true"
|
||||
shift
|
||||
;;
|
||||
--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)"
|
||||
cd "$repo_root"
|
||||
|
||||
if [[ -z "$notes_path" ]]; then
|
||||
notes_path="$repo_root/release-assets/releases/$version/RELEASE_NOTES.ko.md"
|
||||
fi
|
||||
|
||||
target_commit="$(git rev-parse "$branch_name")"
|
||||
|
||||
echo "Version: $version"
|
||||
echo "Source branch: $branch_name"
|
||||
echo "Target branch: $target_branch"
|
||||
echo "Target commit: $target_commit"
|
||||
[[ "$skip_stage" != "true" ]] && echo "Stage remote: $stage_remote"
|
||||
[[ "$skip_github" != "true" ]] && echo "GitHub remote: $github_remote"
|
||||
|
||||
tag_args=()
|
||||
if [[ "$force_tag" == "true" ]]; then
|
||||
tag_args+=(--force)
|
||||
fi
|
||||
|
||||
"$repo_root/scripts/release/release-create-tag.sh" \
|
||||
--version "$version" \
|
||||
--ref "$branch_name" \
|
||||
"${tag_args[@]}" \
|
||||
--dry-run
|
||||
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
if [[ "$skip_stage" != "true" ]]; then
|
||||
echo "DRY RUN: ALLOW_PUBLIC_PUSH=1 git push $stage_remote refs/heads/$branch_name:refs/heads/$target_branch"
|
||||
echo "DRY RUN: ALLOW_PUBLIC_PUSH=1 git push $stage_remote refs/tags/$version:refs/tags/$version"
|
||||
"$repo_root/scripts/release/release-publish-forge.sh" \
|
||||
--remote "$stage_remote" \
|
||||
--version "$version" \
|
||||
--target-commitish "$target_branch" \
|
||||
--notes "$notes_path" \
|
||||
--dry-run
|
||||
fi
|
||||
|
||||
if [[ "$skip_github" != "true" ]]; then
|
||||
echo "DRY RUN: ALLOW_PUBLIC_PUSH=1 git push $github_remote refs/heads/$branch_name:refs/heads/$target_branch"
|
||||
echo "DRY RUN: ALLOW_PUBLIC_PUSH=1 git push $github_remote refs/tags/$version:refs/tags/$version"
|
||||
"$repo_root/scripts/release/release-publish-github.sh" \
|
||||
--remote "$github_remote" \
|
||||
--version "$version" \
|
||||
--target-commitish "$target_branch" \
|
||||
--notes "$notes_path" \
|
||||
--dry-run
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"$repo_root/scripts/release/release-create-tag.sh" \
|
||||
--version "$version" \
|
||||
--ref "$branch_name" \
|
||||
"${tag_args[@]}"
|
||||
|
||||
push_branch() {
|
||||
local remote_name="$1"
|
||||
ALLOW_PUBLIC_PUSH=1 git push "$remote_name" "refs/heads/$branch_name:refs/heads/$target_branch"
|
||||
}
|
||||
|
||||
push_tag() {
|
||||
local remote_name="$1"
|
||||
local force_args=()
|
||||
if [[ "$force_tag" == "true" ]]; then
|
||||
force_args+=(--force)
|
||||
fi
|
||||
ALLOW_PUBLIC_PUSH=1 git push "${force_args[@]}" "$remote_name" "refs/tags/$version:refs/tags/$version"
|
||||
}
|
||||
|
||||
if [[ "$skip_stage" != "true" ]]; then
|
||||
push_branch "$stage_remote"
|
||||
push_tag "$stage_remote"
|
||||
"$repo_root/scripts/release/release-publish-forge.sh" \
|
||||
--remote "$stage_remote" \
|
||||
--version "$version" \
|
||||
--target-commitish "$target_branch" \
|
||||
--notes "$notes_path"
|
||||
fi
|
||||
|
||||
if [[ "$skip_github" != "true" ]]; then
|
||||
push_branch "$github_remote"
|
||||
push_tag "$github_remote"
|
||||
"$repo_root/scripts/release/release-publish-github.sh" \
|
||||
--remote "$github_remote" \
|
||||
--version "$version" \
|
||||
--target-commitish "$target_branch" \
|
||||
--notes "$notes_path"
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue