Defeng fe8f0a875a
Some checks failed
CI / Frontend (Vite) (push) Successful in 24s
CI / Docker Release Tar (push) Has been skipped
CI / Backend (Rust) (push) Failing after 10m15s
Initial project import
2026-07-24 10:28:31 +08:00

639 lines
23 KiB
YAML

name: CI
on:
push:
branches:
- master
- main
tags:
- "*"
pull_request:
workflow_dispatch:
env:
REQUIRED_RUST_VERSION: 1.91.0
jobs:
backend:
name: Backend (Rust)
if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')
runs-on: amd
steps:
- name: Checkout (shell)
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
SERVER_URL="${CI_REPO_BASE_URL:-${GITHUB_SERVER_URL%/}}"
PRIMARY_URL="${SERVER_URL}/${GITHUB_REPOSITORY}.git"
URLS="${PRIMARY_URL}"
case "${PRIMARY_URL}" in
https://*) URLS="${URLS} http://${PRIMARY_URL#https://}" ;;
http://*) URLS="${URLS} https://${PRIMARY_URL#http://}" ;;
esac
[ -d .git ] || git init .
BASIC_AUTH=""
if [ -n "${GITHUB_TOKEN:-}" ]; then
BASIC_AUTH="$(printf '%s' "${GITHUB_ACTOR}:${GITHUB_TOKEN}" | base64 | tr -d '\n')"
fi
git_fetch() {
if [ -n "${BASIC_AUTH}" ]; then
git -c "http.extraHeader=Authorization: Basic ${BASIC_AUTH}" fetch --depth 1 origin "${GITHUB_SHA}"
else
git fetch --depth 1 origin "${GITHUB_SHA}"
fi
}
FETCH_OK=0
for URL in ${URLS}; do
if git remote get-url origin >/dev/null 2>&1; then
git remote set-url origin "${URL}"
else
git remote add origin "${URL}"
fi
if git_fetch; then
FETCH_OK=1
break
fi
done
if [ "${FETCH_OK}" -ne 1 ]; then
echo "Failed to fetch ${GITHUB_SHA} from ${PRIMARY_URL} (tried http/https)."
exit 1
fi
git checkout --force FETCH_HEAD
- name: Setup Rust
run: bash .gitea/scripts/setup-rust.sh
- name: Check format
run: |
set -euo pipefail
NIGHTLY_ONLY_KEYS='^(indent_style|wrap_comments|normalize_comments|format_macro_matchers|format_macro_bodies|struct_lit_single_line|fn_single_line|imports_indent|imports_layout|imports_granularity|group_imports|type_punctuation_density|space_before_colon|space_after_colon|spaces_around_ranges|binop_separator|overflow_delimited_expr|match_arm_blocks|force_multiline_blocks|brace_style|control_brace_style|trailing_semicolon|trailing_comma)\s*='
if [ -f .rustfmt.toml ] && grep -Eq "${NIGHTLY_ONLY_KEYS}" .rustfmt.toml; then
echo "Skip rustfmt check: .rustfmt.toml includes nightly-only options."
exit 0
fi
cargo fmt --all -- --check
- name: Setup Node (shell)
run: |
set -euo pipefail
if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
node --version
npm --version
exit 0
fi
as_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
echo "Need root privileges to install nodejs/npm."
exit 1
fi
}
if command -v pacman >/dev/null 2>&1; then
as_root pacman -Sy --noconfirm --needed nodejs npm
elif command -v apt-get >/dev/null 2>&1; then
as_root apt-get update
as_root apt-get install -y nodejs npm
elif command -v apk >/dev/null 2>&1; then
as_root apk add --no-cache nodejs npm
elif command -v dnf >/dev/null 2>&1; then
as_root dnf install -y nodejs npm
else
echo "Unsupported package manager. Preinstall nodejs/npm on runner."
exit 1
fi
node --version
npm --version
- name: Build frontend assets for RustEmbed
working-directory: frontend
run: |
npm config set registry https://registry.npmjs.org
npm config set replace-registry-host always
npm ci --cache .npm-cache --prefer-online
npm run build
- name: Run tests
run: cargo test --workspace --all-features
frontend:
name: Frontend (Vite)
if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')
runs-on: amd
steps:
- name: Checkout (shell)
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
SERVER_URL="${CI_REPO_BASE_URL:-${GITHUB_SERVER_URL%/}}"
PRIMARY_URL="${SERVER_URL}/${GITHUB_REPOSITORY}.git"
URLS="${PRIMARY_URL}"
case "${PRIMARY_URL}" in
https://*) URLS="${URLS} http://${PRIMARY_URL#https://}" ;;
http://*) URLS="${URLS} https://${PRIMARY_URL#http://}" ;;
esac
[ -d .git ] || git init .
BASIC_AUTH=""
if [ -n "${GITHUB_TOKEN:-}" ]; then
BASIC_AUTH="$(printf '%s' "${GITHUB_ACTOR}:${GITHUB_TOKEN}" | base64 | tr -d '\n')"
fi
git_fetch() {
if [ -n "${BASIC_AUTH}" ]; then
git -c "http.extraHeader=Authorization: Basic ${BASIC_AUTH}" fetch --depth 1 origin "${GITHUB_SHA}"
else
git fetch --depth 1 origin "${GITHUB_SHA}"
fi
}
FETCH_OK=0
for URL in ${URLS}; do
if git remote get-url origin >/dev/null 2>&1; then
git remote set-url origin "${URL}"
else
git remote add origin "${URL}"
fi
if git_fetch; then
FETCH_OK=1
break
fi
done
if [ "${FETCH_OK}" -ne 1 ]; then
echo "Failed to fetch ${GITHUB_SHA} from ${PRIMARY_URL} (tried http/https)."
exit 1
fi
git checkout --force FETCH_HEAD
- name: Setup Node (shell)
run: |
set -euo pipefail
if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
node --version
npm --version
exit 0
fi
as_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
echo "Need root privileges to install nodejs/npm."
exit 1
fi
}
if command -v pacman >/dev/null 2>&1; then
as_root pacman -Sy --noconfirm --needed nodejs npm
elif command -v apt-get >/dev/null 2>&1; then
as_root apt-get update
as_root apt-get install -y nodejs npm
elif command -v apk >/dev/null 2>&1; then
as_root apk add --no-cache nodejs npm
elif command -v dnf >/dev/null 2>&1; then
as_root dnf install -y nodejs npm
else
echo "Unsupported package manager. Preinstall nodejs/npm on runner."
exit 1
fi
node --version
npm --version
- name: Install dependencies
working-directory: frontend
run: |
npm config set registry https://registry.npmjs.org
npm config set replace-registry-host always
npm ci --cache .npm-cache --prefer-online
- name: Build
working-directory: frontend
run: npm run build
docker_release:
name: Docker Release Tar
if: startsWith(github.ref, 'refs/tags/')
runs-on: amd
permissions:
contents: write
releases: write
steps:
- name: Checkout (shell)
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
SERVER_URL="${CI_REPO_BASE_URL:-${GITHUB_SERVER_URL%/}}"
PRIMARY_URL="${SERVER_URL}/${GITHUB_REPOSITORY}.git"
URLS="${PRIMARY_URL}"
case "${PRIMARY_URL}" in
https://*) URLS="${URLS} http://${PRIMARY_URL#https://}" ;;
http://*) URLS="${URLS} https://${PRIMARY_URL#http://}" ;;
esac
[ -d .git ] || git init .
BASIC_AUTH=""
if [ -n "${GITHUB_TOKEN:-}" ]; then
BASIC_AUTH="$(printf '%s' "${GITHUB_ACTOR}:${GITHUB_TOKEN}" | base64 | tr -d '\n')"
fi
git_fetch() {
if [ -n "${BASIC_AUTH}" ]; then
git -c "http.extraHeader=Authorization: Basic ${BASIC_AUTH}" fetch --depth 1 origin "${GITHUB_SHA}"
else
git fetch --depth 1 origin "${GITHUB_SHA}"
fi
}
FETCH_OK=0
for URL in ${URLS}; do
if git remote get-url origin >/dev/null 2>&1; then
git remote set-url origin "${URL}"
else
git remote add origin "${URL}"
fi
if git_fetch; then
FETCH_OK=1
break
fi
done
if [ "${FETCH_OK}" -ne 1 ]; then
echo "Failed to fetch ${GITHUB_SHA} from ${PRIMARY_URL} (tried http/https)."
exit 1
fi
git checkout --force FETCH_HEAD
- name: Setup Rust
run: bash .gitea/scripts/setup-rust.sh
- name: Setup Node (shell)
run: |
set -euo pipefail
if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
node --version
npm --version
exit 0
fi
as_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
echo "Need root privileges to install nodejs/npm."
exit 1
fi
}
if command -v pacman >/dev/null 2>&1; then
as_root pacman -Sy --noconfirm --needed nodejs npm
elif command -v apt-get >/dev/null 2>&1; then
as_root apt-get update
as_root apt-get install -y nodejs npm
elif command -v apk >/dev/null 2>&1; then
as_root apk add --no-cache nodejs npm
elif command -v dnf >/dev/null 2>&1; then
as_root dnf install -y nodejs npm
else
echo "Unsupported package manager. Preinstall nodejs/npm on runner."
exit 1
fi
node --version
npm --version
- name: Setup Docker (shell)
run: |
set -euo pipefail
as_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
echo "Need root privileges to install/start docker."
exit 1
fi
}
if ! command -v docker >/dev/null 2>&1; then
if command -v pacman >/dev/null 2>&1; then
as_root pacman -Sy --noconfirm --needed docker
elif command -v apt-get >/dev/null 2>&1; then
as_root apt-get update
as_root apt-get install -y docker.io
elif command -v apk >/dev/null 2>&1; then
as_root apk add --no-cache docker
elif command -v dnf >/dev/null 2>&1; then
if ! as_root dnf install -y docker; then
as_root dnf install -y moby-engine
fi
else
echo "Unsupported package manager. Please preinstall docker."
exit 1
fi
fi
if ! docker info >/dev/null 2>&1; then
if command -v dockerd >/dev/null 2>&1; then
nohup dockerd >/tmp/dockerd.log 2>&1 &
for _ in $(seq 1 60); do
if docker info >/dev/null 2>&1; then
break
fi
sleep 1
done
fi
fi
if ! docker info >/dev/null 2>&1; then
echo "Docker daemon is not available. Runner must support docker (daemon or mounted /var/run/docker.sock)."
[ -f /tmp/dockerd.log ] && tail -n 80 /tmp/dockerd.log || true
exit 1
fi
docker version
- name: Build Docker image by script
run: |
set -euo pipefail
TAG_NAME="${GITHUB_REF_NAME}"
SAFE_TAG="$(echo "${TAG_NAME}" | tr '/' '-')"
npm config set registry https://registry.npmjs.org
npm config set replace-registry-host always
IMAGE_TAG="${SAFE_TAG}" sh build-docker.sh
docker save "htknow:${SAFE_TAG}" -o "htknow-${SAFE_TAG}.tar"
gzip -f "htknow-${SAFE_TAG}.tar"
echo "TAG_NAME=${TAG_NAME}" >> "${GITHUB_ENV}"
echo "SAFE_TAG=${SAFE_TAG}" >> "${GITHUB_ENV}"
echo "ASSET_FILE=htknow-${SAFE_TAG}.tar.gz" >> "${GITHUB_ENV}"
- name: Create release (if needed) and upload tar
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -euo pipefail
as_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
echo "Need root privileges to install system packages."
exit 1
fi
}
ensure_pkg() {
if command -v pacman >/dev/null 2>&1; then
as_root pacman -Sy --noconfirm --needed "$@"
elif command -v apt-get >/dev/null 2>&1; then
as_root apt-get update
as_root apt-get install -y "$@"
elif command -v apk >/dev/null 2>&1; then
as_root apk add --no-cache "$@"
elif command -v dnf >/dev/null 2>&1; then
as_root dnf install -y "$@"
else
echo "Unsupported package manager. Please preinstall required tools: $*"
exit 1
fi
}
command -v curl >/dev/null 2>&1 || ensure_pkg curl
command -v jq >/dev/null 2>&1 || ensure_pkg jq
if [ -z "${RELEASE_TOKEN:-}" ]; then
echo "secrets.RELEASE_TOKEN is required to create releases and upload assets."
exit 1
fi
REPO_OWNER="${GITHUB_REPOSITORY%%/*}"
REPO_NAME="${GITHUB_REPOSITORY##*/}"
API_BASE="${GITHUB_API_URL:-${GITHUB_SERVER_URL}/api/v1}"
AUTH_HEADER="Authorization: token ${RELEASE_TOKEN}"
RELEASE_URL="${API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/releases"
LOOKUP_CODE="$(curl -sS -o release.json -w '%{http_code}' \
-H "${AUTH_HEADER}" \
"${RELEASE_URL}/tags/${TAG_NAME}")"
if [ "${LOOKUP_CODE}" = "200" ]; then
echo "Release for tag ${TAG_NAME} already exists."
elif [ "${LOOKUP_CODE}" = "404" ]; then
cat > create-release.json <<EOF
{"tag_name":"${TAG_NAME}","name":"${TAG_NAME}","body":"Automated Docker image tar.gz for ${TAG_NAME}.","draft":false,"prerelease":false}
EOF
curl -sS -f -X POST \
-H "${AUTH_HEADER}" \
-H "Content-Type: application/json" \
--data @create-release.json \
"${RELEASE_URL}" \
-o release.json
echo "Created release for ${TAG_NAME}."
else
echo "Unexpected response (${LOOKUP_CODE}) while querying release:"
cat release.json
exit 1
fi
RELEASE_ID="$(jq -r '.id' release.json)"
if [ -z "${RELEASE_ID}" ] || [ "${RELEASE_ID}" = "null" ]; then
echo "Failed to resolve release id."
cat release.json
exit 1
fi
EXISTING_ASSET_ID="$(curl -sS -f \
-H "${AUTH_HEADER}" \
"${RELEASE_URL}/${RELEASE_ID}/assets" \
| jq -r --arg name "${ASSET_FILE}" '.[] | select(.name == $name) | .id')"
if [ -n "${EXISTING_ASSET_ID}" ] && [ "${EXISTING_ASSET_ID}" != "null" ]; then
curl -sS -f -X DELETE \
-H "${AUTH_HEADER}" \
"${RELEASE_URL}/${RELEASE_ID}/assets/${EXISTING_ASSET_ID}" >/dev/null
echo "Deleted existing asset ${ASSET_FILE}."
fi
curl -sS -f -X POST \
-H "${AUTH_HEADER}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"${ASSET_FILE}" \
"${RELEASE_URL}/${RELEASE_ID}/assets?name=${ASSET_FILE}" >/dev/null
echo "Uploaded ${ASSET_FILE} to release ${TAG_NAME}."
- name: Deploy to target server
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PASSWORD: ${{ secrets.DEPLOY_PASSWORD }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_COMMAND: ${{ secrets.DEPLOY_COMMAND }}
run: |
set -euo pipefail
if [ -z "${DEPLOY_HOST:-}" ] || [ -z "${DEPLOY_USER:-}" ]; then
echo "Deploy secrets not fully configured (DEPLOY_HOST/DEPLOY_USER). Skip deployment."
exit 0
fi
if [ -z "${DEPLOY_PASSWORD:-}" ] && [ -z "${DEPLOY_SSH_KEY:-}" ]; then
echo "Neither DEPLOY_PASSWORD nor DEPLOY_SSH_KEY is configured. Skip deployment."
exit 0
fi
as_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
echo "Need root privileges to install openssh client."
exit 1
fi
}
ensure_pkg() {
if command -v pacman >/dev/null 2>&1; then
as_root pacman -Sy --noconfirm --needed "$@"
elif command -v apt-get >/dev/null 2>&1; then
as_root apt-get update
as_root apt-get install -y "$@"
elif command -v apk >/dev/null 2>&1; then
as_root apk add --no-cache "$@"
elif command -v dnf >/dev/null 2>&1; then
as_root dnf install -y "$@"
else
echo "Unsupported package manager. Please preinstall openssh client."
exit 1
fi
}
ensure_ssh_client() {
if command -v ssh >/dev/null 2>&1 && command -v scp >/dev/null 2>&1 && command -v ssh-keyscan >/dev/null 2>&1; then
return
fi
if command -v pacman >/dev/null 2>&1; then
ensure_pkg openssh
elif command -v apt-get >/dev/null 2>&1; then
ensure_pkg openssh-client
elif command -v apk >/dev/null 2>&1; then
ensure_pkg openssh-client
elif command -v dnf >/dev/null 2>&1; then
ensure_pkg openssh-clients
else
echo "Unsupported package manager. Please preinstall ssh/scp."
exit 1
fi
}
ensure_ssh_client
PORT="${DEPLOY_PORT:-22}"
REMOTE_PATH_RAW="${DEPLOY_PATH:-/tmp/htknow-release}"
case "${REMOTE_PATH_RAW}" in
"~") REMOTE_PATH_REMOTE="\$HOME" ;;
"~/"*) REMOTE_PATH_REMOTE="\$HOME/${REMOTE_PATH_RAW#~/}" ;;
*) REMOTE_PATH_REMOTE="${REMOTE_PATH_RAW}" ;;
esac
KNOWN_HOSTS="${HOME}/.ssh/known_hosts"
mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch "${KNOWN_HOSTS}"
chmod 600 "${KNOWN_HOSTS}"
ssh-keyscan -T 10 -p "${PORT}" -H "${DEPLOY_HOST}" >> "${KNOWN_HOSTS}" 2>/dev/null || true
run_ssh() {
ssh -p "${PORT}" \
-o BatchMode=yes \
-o ConnectTimeout=10 \
-o ServerAliveInterval=10 \
-o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=accept-new \
-o UserKnownHostsFile="${KNOWN_HOSTS}" \
-i "${HOME}/.ssh/id_deploy" \
"$@"
}
run_scp() {
scp -P "${PORT}" \
-o BatchMode=yes \
-o ConnectTimeout=10 \
-o ServerAliveInterval=10 \
-o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=accept-new \
-o UserKnownHostsFile="${KNOWN_HOSTS}" \
-i "${HOME}/.ssh/id_deploy" \
"$@"
}
if [ -n "${DEPLOY_PASSWORD:-}" ]; then
command -v sshpass >/dev/null 2>&1 || ensure_pkg sshpass
export SSHPASS="${DEPLOY_PASSWORD}"
run_ssh() {
sshpass -e ssh -p "${PORT}" \
-o PreferredAuthentications=password \
-o PubkeyAuthentication=no \
-o ConnectTimeout=10 \
-o ServerAliveInterval=10 \
-o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=accept-new \
-o UserKnownHostsFile="${KNOWN_HOSTS}" \
"$@"
}
run_scp() {
sshpass -e scp -P "${PORT}" \
-o PreferredAuthentications=password \
-o PubkeyAuthentication=no \
-o ConnectTimeout=10 \
-o ServerAliveInterval=10 \
-o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=accept-new \
-o UserKnownHostsFile="${KNOWN_HOSTS}" \
"$@"
}
else
printf '%s\n' "${DEPLOY_SSH_KEY}" | sed 's/\r$//' > ~/.ssh/id_deploy
chmod 600 ~/.ssh/id_deploy
fi
if ! run_ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "echo 'ssh connectivity ok'"; then
echo "SSH connectivity check failed. Verify DEPLOY_HOST/DEPLOY_PORT/DEPLOY_USER and DEPLOY_PASSWORD or DEPLOY_SSH_KEY."
exit 1
fi
if [ ! -f "${ASSET_FILE}" ]; then
echo "Local asset file not found: ${ASSET_FILE}"
ls -lah
exit 1
fi
run_ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "sh -lc 'set -e; REMOTE_PATH=\"${REMOTE_PATH_REMOTE}\"; REMOTE_PATH=\$(eval echo \"\${REMOTE_PATH}\"); mkdir -p \"\${REMOTE_PATH}\"'"
run_ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "sh -lc 'set -e; REMOTE_PATH=\"${REMOTE_PATH_REMOTE}\"; REMOTE_PATH=\$(eval echo \"\${REMOTE_PATH}\"); cat > \"\${REMOTE_PATH}/${ASSET_FILE}\"'" < "${ASSET_FILE}"
run_ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "sh -lc 'set -e; REMOTE_PATH=\"${REMOTE_PATH_REMOTE}\"; REMOTE_PATH=\$(eval echo \"\${REMOTE_PATH}\"); ls -lh \"\${REMOTE_PATH}/${ASSET_FILE}\"'"
if [ -n "${DEPLOY_COMMAND:-}" ]; then
DEPLOY_CMD_B64="$(printf '%s' "${DEPLOY_COMMAND}" | base64 | tr -d '\n')"
run_ssh "${DEPLOY_USER}@${DEPLOY_HOST}" \
"ASSET_FILE='${ASSET_FILE}' SAFE_TAG='${SAFE_TAG}' DEPLOY_PATH='${REMOTE_PATH_REMOTE}' DEPLOY_CMD_B64='${DEPLOY_CMD_B64}' sh -lc 'DEPLOY_PATH=\$(eval echo \"\$DEPLOY_PATH\"); printf %s \"\$DEPLOY_CMD_B64\" | base64 -d | sh'"
else
run_ssh "${DEPLOY_USER}@${DEPLOY_HOST}" "sh -lc '
set -e
DEPLOY_PATH=\"${REMOTE_PATH_REMOTE}\"
DEPLOY_PATH=\$(eval echo \"\$DEPLOY_PATH\")
EXTRACTED_TAR_FILE=\"${ASSET_FILE%.gz}\"
gzip -dc \"\$DEPLOY_PATH/${ASSET_FILE}\" > \"\$DEPLOY_PATH/\$EXTRACTED_TAR_FILE\"
docker load -i \"\$DEPLOY_PATH/\$EXTRACTED_TAR_FILE\"
rm -f \"\$DEPLOY_PATH/\$EXTRACTED_TAR_FILE\"
docker rm -f htknow >/dev/null 2>&1 || true
docker run -d --name htknow --restart unless-stopped -p 8080:8080 -v /opt/htknow/data:/app/data \"htknow:${SAFE_TAG}\"
'"
fi
echo "Deployed htknow:${SAFE_TAG} to ${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_PATH_RAW}"