공개: KoTalk 최신 기준선
This commit is contained in:
commit
debf62f76e
572 changed files with 41689 additions and 0 deletions
1
scripts/capture_vstalk_web_screenshots.cjs
Normal file
1
scripts/capture_vstalk_web_screenshots.cjs
Normal file
|
|
@ -0,0 +1 @@
|
|||
require('./ci/capture-vstalk-web-screenshots.cjs')
|
||||
397
scripts/ci/capture-vstalk-web-screenshots.cjs
Normal file
397
scripts/ci/capture-vstalk-web-screenshots.cjs
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
const fs = require('node:fs/promises')
|
||||
const path = require('node:path')
|
||||
const process = require('node:process')
|
||||
const { createRequire } = require('node:module')
|
||||
|
||||
const webPackageRequire = createRequire(path.resolve(process.cwd(), 'src/PhysOn.Web/package.json'))
|
||||
const puppeteer = webPackageRequire('puppeteer-core')
|
||||
|
||||
const baseUrl = process.env.VSTALK_CAPTURE_URL ?? 'http://127.0.0.1:4174/'
|
||||
const outputDir = process.env.VSTALK_CAPTURE_OUTPUT_DIR
|
||||
?? path.resolve(process.cwd(), 'docs/assets/latest')
|
||||
const executablePath = process.env.CHROME_BIN ?? '/usr/bin/google-chrome'
|
||||
|
||||
const bootstrapPayload = {
|
||||
me: {
|
||||
userId: 'me-1',
|
||||
displayName: '이안',
|
||||
profileImageUrl: null,
|
||||
statusMessage: '업무와 일상을 가볍게 잇는 중',
|
||||
},
|
||||
session: {
|
||||
sessionId: 'session-alpha-web',
|
||||
deviceId: 'device-web-alpha',
|
||||
deviceName: 'Mobile Web',
|
||||
createdAt: '2026-04-16T04:50:00Z',
|
||||
},
|
||||
ws: {
|
||||
url: 'wss://vstalk.phy.kr/v1/realtime/ws',
|
||||
},
|
||||
conversations: {
|
||||
items: [
|
||||
{
|
||||
conversationId: 'conv-team',
|
||||
type: 'group',
|
||||
title: '제품 운영',
|
||||
avatarUrl: null,
|
||||
subtitle: '10시 전에 공유안만 확인해 주세요.',
|
||||
memberCount: 4,
|
||||
isMuted: false,
|
||||
isPinned: true,
|
||||
sortKey: '2026-04-16T05:04:00Z',
|
||||
unreadCount: 2,
|
||||
lastReadSequence: 10,
|
||||
lastMessage: {
|
||||
messageId: 'msg-11',
|
||||
text: '10시 전에 공유안만 확인해 주세요.',
|
||||
createdAt: '2026-04-16T05:04:00Z',
|
||||
senderUserId: 'u-2',
|
||||
},
|
||||
},
|
||||
{
|
||||
conversationId: 'conv-friends',
|
||||
type: 'group',
|
||||
title: '주말 약속',
|
||||
avatarUrl: null,
|
||||
subtitle: '토요일 2시에 브런치 어때?',
|
||||
memberCount: 3,
|
||||
isMuted: false,
|
||||
isPinned: false,
|
||||
sortKey: '2026-04-16T04:48:00Z',
|
||||
unreadCount: 0,
|
||||
lastReadSequence: 5,
|
||||
lastMessage: {
|
||||
messageId: 'msg-22',
|
||||
text: '토요일 2시에 브런치 어때?',
|
||||
createdAt: '2026-04-16T04:48:00Z',
|
||||
senderUserId: 'u-3',
|
||||
},
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
},
|
||||
}
|
||||
|
||||
const messageMap = {
|
||||
'conv-team': {
|
||||
items: [
|
||||
{
|
||||
messageId: 'msg-8',
|
||||
conversationId: 'conv-team',
|
||||
clientMessageId: 'client-8',
|
||||
kind: 'text',
|
||||
text: '회의 전에 이슈만 짧게 정리해 주세요.',
|
||||
createdAt: '2026-04-16T04:40:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'u-2',
|
||||
displayName: '민지',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: false,
|
||||
serverSequence: 8,
|
||||
},
|
||||
{
|
||||
messageId: 'msg-9',
|
||||
conversationId: 'conv-team',
|
||||
clientMessageId: 'client-9',
|
||||
kind: 'text',
|
||||
text: '공유안은 정리해 두었습니다. 바로 올릴게요.',
|
||||
createdAt: '2026-04-16T04:47:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'me-1',
|
||||
displayName: '이안',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: true,
|
||||
serverSequence: 9,
|
||||
},
|
||||
{
|
||||
messageId: 'msg-10',
|
||||
conversationId: 'conv-team',
|
||||
clientMessageId: 'client-10',
|
||||
kind: 'text',
|
||||
text: '좋아요. 10시 전에 공유안만 확인해 주세요.',
|
||||
createdAt: '2026-04-16T05:04:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'u-2',
|
||||
displayName: '민지',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: false,
|
||||
serverSequence: 10,
|
||||
},
|
||||
{
|
||||
messageId: 'msg-11',
|
||||
conversationId: 'conv-team',
|
||||
clientMessageId: 'client-11',
|
||||
kind: 'text',
|
||||
text: '10시 전에 공유안만 확인해 주세요.',
|
||||
createdAt: '2026-04-16T05:04:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'u-2',
|
||||
displayName: '민지',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: false,
|
||||
serverSequence: 11,
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
},
|
||||
'conv-friends': {
|
||||
items: [
|
||||
{
|
||||
messageId: 'msg-20',
|
||||
conversationId: 'conv-friends',
|
||||
clientMessageId: 'client-20',
|
||||
kind: 'text',
|
||||
text: '이번 주말에 시간 괜찮아?',
|
||||
createdAt: '2026-04-16T04:32:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'u-3',
|
||||
displayName: '수아',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: false,
|
||||
serverSequence: 4,
|
||||
},
|
||||
{
|
||||
messageId: 'msg-21',
|
||||
conversationId: 'conv-friends',
|
||||
clientMessageId: 'client-21',
|
||||
kind: 'text',
|
||||
text: '토요일 2시에 브런치 어때?',
|
||||
createdAt: '2026-04-16T04:48:00Z',
|
||||
editedAt: null,
|
||||
sender: {
|
||||
userId: 'u-3',
|
||||
displayName: '수아',
|
||||
profileImageUrl: null,
|
||||
},
|
||||
isMine: false,
|
||||
serverSequence: 5,
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
},
|
||||
}
|
||||
|
||||
const storedSession = {
|
||||
apiBaseUrl: '',
|
||||
tokens: {
|
||||
accessToken: 'access-token-alpha',
|
||||
accessTokenExpiresAt: '2026-04-16T06:00:00Z',
|
||||
refreshToken: 'refresh-token-alpha',
|
||||
refreshTokenExpiresAt: '2026-05-16T06:00:00Z',
|
||||
},
|
||||
bootstrap: bootstrapPayload,
|
||||
savedAt: '2026-04-16T05:04:00Z',
|
||||
}
|
||||
|
||||
async function ensureOutputDir() {
|
||||
await fs.mkdir(outputDir, { recursive: true })
|
||||
}
|
||||
|
||||
async function createBrowser() {
|
||||
return puppeteer.launch({
|
||||
executablePath,
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
defaultViewport: {
|
||||
width: 390,
|
||||
height: 844,
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
deviceScaleFactor: 2,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function installSessionMocks(page) {
|
||||
await page.evaluateOnNewDocument((session) => {
|
||||
class FakeWebSocket {
|
||||
constructor(url) {
|
||||
this.url = url
|
||||
this.readyState = 0
|
||||
this.onopen = null
|
||||
this.onclose = null
|
||||
this.onerror = null
|
||||
this.onmessage = null
|
||||
window.setTimeout(() => {
|
||||
this.readyState = 1
|
||||
if (this.onopen) {
|
||||
this.onopen({ type: 'open' })
|
||||
}
|
||||
}, 80)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = 3
|
||||
if (this.onclose) {
|
||||
this.onclose({ type: 'close' })
|
||||
}
|
||||
}
|
||||
|
||||
send() {}
|
||||
}
|
||||
|
||||
window.localStorage.setItem('vs-talk.session', JSON.stringify(session))
|
||||
window.localStorage.setItem('vs-talk.invite-code', 'ALPHA')
|
||||
window.WebSocket = FakeWebSocket
|
||||
}, storedSession)
|
||||
|
||||
await page.setRequestInterception(true)
|
||||
page.on('request', (request) => {
|
||||
const url = new URL(request.url())
|
||||
|
||||
if (url.pathname === '/v1/bootstrap') {
|
||||
request.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ data: bootstrapPayload }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (/\/v1\/conversations\/[^/]+\/messages$/.test(url.pathname)) {
|
||||
const match = url.pathname.match(/\/v1\/conversations\/([^/]+)\/messages/)
|
||||
const conversationId = match ? match[1] : ''
|
||||
const payload = messageMap[conversationId] ?? { items: [], nextCursor: null }
|
||||
|
||||
request.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ data: payload }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (/\/v1\/conversations\/[^/]+\/read-cursor$/.test(url.pathname)) {
|
||||
const match = url.pathname.match(/\/v1\/conversations\/([^/]+)\/read-cursor/)
|
||||
const conversationId = match ? match[1] : ''
|
||||
const body = JSON.parse(request.postData() ?? '{}')
|
||||
|
||||
request.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
conversationId,
|
||||
accountId: 'me-1',
|
||||
lastReadSequence: body.lastReadSequence ?? 0,
|
||||
updatedAt: '2026-04-16T05:05:00Z',
|
||||
},
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (url.pathname === '/v1/auth/token/refresh') {
|
||||
request.respond({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
tokens: storedSession.tokens,
|
||||
},
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
request.continue()
|
||||
})
|
||||
}
|
||||
|
||||
async function captureOnboarding(browser) {
|
||||
const page = await browser.newPage()
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle2' })
|
||||
await page.evaluate(() => {
|
||||
window.localStorage.clear()
|
||||
})
|
||||
await page.reload({ waitUntil: 'networkidle2' })
|
||||
await page.screenshot({
|
||||
path: path.join(outputDir, 'vstalk-web-onboarding.png'),
|
||||
fullPage: false,
|
||||
})
|
||||
await page.close()
|
||||
}
|
||||
|
||||
async function captureConversationList(browser) {
|
||||
const page = await browser.newPage()
|
||||
await installSessionMocks(page)
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle2' })
|
||||
await page.waitForSelector('.conversation-row')
|
||||
await page.screenshot({
|
||||
path: path.join(outputDir, 'vstalk-web-list.png'),
|
||||
fullPage: false,
|
||||
})
|
||||
await page.close()
|
||||
}
|
||||
|
||||
async function captureConversation(browser) {
|
||||
const page = await browser.newPage()
|
||||
await installSessionMocks(page)
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle2' })
|
||||
await page.waitForSelector('.conversation-row')
|
||||
await page.click('.conversation-row')
|
||||
await page.waitForSelector('.message-bubble')
|
||||
await page.screenshot({
|
||||
path: path.join(outputDir, 'vstalk-web-chat.png'),
|
||||
fullPage: false,
|
||||
})
|
||||
await page.close()
|
||||
}
|
||||
|
||||
async function captureSearch(browser) {
|
||||
const page = await browser.newPage()
|
||||
await installSessionMocks(page)
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle2' })
|
||||
await page.waitForSelector('.bottom-bar')
|
||||
await page.click('.bottom-bar .nav-button:nth-child(2)')
|
||||
await page.waitForSelector('.search-field')
|
||||
await page.screenshot({
|
||||
path: path.join(outputDir, 'vstalk-web-search.png'),
|
||||
fullPage: false,
|
||||
})
|
||||
await page.close()
|
||||
}
|
||||
|
||||
async function captureSaved(browser) {
|
||||
const page = await browser.newPage()
|
||||
await installSessionMocks(page)
|
||||
await page.goto(baseUrl, { waitUntil: 'networkidle2' })
|
||||
await page.waitForSelector('.bottom-bar')
|
||||
await page.click('.bottom-bar .nav-button:nth-child(3)')
|
||||
await page.waitForSelector('.saved-section')
|
||||
await page.screenshot({
|
||||
path: path.join(outputDir, 'vstalk-web-saved.png'),
|
||||
fullPage: false,
|
||||
})
|
||||
await page.close()
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await ensureOutputDir()
|
||||
const browser = await createBrowser()
|
||||
|
||||
try {
|
||||
await captureOnboarding(browser)
|
||||
await captureConversationList(browser)
|
||||
await captureSearch(browser)
|
||||
await captureSaved(browser)
|
||||
await captureConversation(browser)
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
5
scripts/deploy-mvp-stack.sh
Executable file
5
scripts/deploy-mvp-stack.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/deploy/deploy-stack-mvp.sh" "$@"
|
||||
5
scripts/deploy-webapp.sh
Executable file
5
scripts/deploy-webapp.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/deploy/deploy-webapp-static.sh" "$@"
|
||||
106
scripts/deploy/deploy-stack-mvp.sh
Executable file
106
scripts/deploy/deploy-stack-mvp.sh
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/deploy/deploy-stack-mvp.sh --host example.com --user deploy [options]
|
||||
|
||||
Options:
|
||||
--app-dir <path> Remote application root. Default: /srv/vs-messanger/app
|
||||
--download-root <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
|
||||
|
||||
Notes:
|
||||
- Remote host must already contain a valid deploy/.env file.
|
||||
- This script syncs deploy files and the current src tree, then runs docker compose.
|
||||
EOF
|
||||
}
|
||||
|
||||
host=""
|
||||
user=""
|
||||
app_dir="/srv/vs-messanger/app"
|
||||
download_root="/srv/vs-messanger/download"
|
||||
ssh_key=""
|
||||
dry_run="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--host)
|
||||
host="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--user)
|
||||
user="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--app-dir)
|
||||
app_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--download-root)
|
||||
download_root="${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 "$host" || -z "$user" ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
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)
|
||||
fi
|
||||
|
||||
target_host="$user@$host"
|
||||
rsh="${ssh_cmd[*]}"
|
||||
|
||||
"${ssh_cmd[@]}" "$target_host" "mkdir -p '$app_dir' '$download_root'"
|
||||
|
||||
rsync "${rsync_opts[@]}" \
|
||||
-e "$rsh" \
|
||||
--delete \
|
||||
--filter="protect /deploy/.env" \
|
||||
--include "/VsMessenger.sln" \
|
||||
--include "/global.json" \
|
||||
--include "/deploy/***" \
|
||||
--include "/src/***" \
|
||||
--exclude "*" \
|
||||
"$repo_root"/ "$target_host:$app_dir/"
|
||||
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
echo "Dry run complete. Remote compose was not started."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"${ssh_cmd[@]}" "$target_host" \
|
||||
"cd '$app_dir' && test -f deploy/.env && docker compose --env-file deploy/.env -f deploy/compose.mvp.yml up -d --build --remove-orphans"
|
||||
|
||||
echo "Deployed MVP stack to $target_host:$app_dir"
|
||||
117
scripts/deploy/deploy-webapp-static.sh
Executable file
117
scripts/deploy/deploy-webapp-static.sh
Executable file
|
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/deploy/deploy-webapp-static.sh --host example.com --user deploy --source dist [options]
|
||||
|
||||
Options:
|
||||
--source <path> Local webapp build directory to upload
|
||||
--version <name> Remote release directory name. Default: current timestamp
|
||||
--app-dir <path> Remote application root. Default: /srv/vs-messanger/app
|
||||
--target <path> Remote webapp root. Default: /srv/vs-messanger/webapp
|
||||
--ssh-key <path> Private key used for SSH/rsync
|
||||
--dry-run Print the rsync plan without changing the server
|
||||
|
||||
Notes:
|
||||
- Remote host must already contain a valid deploy/.env file.
|
||||
- This script uploads static webapp files into releases/<version> and repoints current -> releases/<version>.
|
||||
- The webapp is expected to be served at https://vstalk.phy.kr via Caddy + compose.webapp.yml.
|
||||
EOF
|
||||
}
|
||||
|
||||
host=""
|
||||
user=""
|
||||
source_dir=""
|
||||
version="$(date +%Y%m%d-%H%M%S)"
|
||||
app_dir="/srv/vs-messanger/app"
|
||||
target="/srv/vs-messanger/webapp"
|
||||
ssh_key=""
|
||||
dry_run="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--host)
|
||||
host="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--user)
|
||||
user="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--source)
|
||||
source_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
version="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--app-dir)
|
||||
app_dir="${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 "$host" || -z "$user" || -z "$source_dir" ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$source_dir" ]]; then
|
||||
echo "Source directory not found: $source_dir" >&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[*]}"
|
||||
release_dir="$target/releases/$version"
|
||||
current_link="$target/current"
|
||||
|
||||
"${ssh_cmd[@]}" "$target_host" "mkdir -p '$release_dir' '$target/releases' '$app_dir'"
|
||||
rsync "${rsync_opts[@]}" -e "$rsh" "$source_dir"/ "$target_host:$release_dir/"
|
||||
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
echo "Dry run complete. Remote symlink and compose were not updated."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"${ssh_cmd[@]}" "$target_host" \
|
||||
"ln -sfn '$release_dir' '$current_link' && cd '$app_dir' && test -f deploy/.env && docker compose --env-file deploy/.env -f deploy/compose.mvp.yml -f deploy/compose.webapp.yml up -d webapp caddy"
|
||||
|
||||
echo "Deployed webapp release $version to $target_host:$release_dir"
|
||||
5
scripts/prepare-release-assets.sh
Executable file
5
scripts/prepare-release-assets.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-prepare-assets.sh" "$@"
|
||||
5
scripts/publish-gitea-release.sh
Executable file
5
scripts/publish-gitea-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-forge.sh" "$@"
|
||||
329
scripts/release/release-prepare-assets.sh
Executable file
329
scripts/release/release-prepare-assets.sh
Executable 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"
|
||||
190
scripts/release/release-publish-forge.sh
Executable file
190
scripts/release/release-publish-forge.sh
Executable 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."
|
||||
100
scripts/release/release-upload-assets.sh
Executable file
100
scripts/release/release-upload-assets.sh
Executable 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"
|
||||
5
scripts/upload-release-assets.sh
Executable file
5
scripts/upload-release-assets.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-upload-assets.sh" "$@"
|
||||
Loading…
Add table
Add a link
Reference in a new issue