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 </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}"