diff --git a/.gitea/workflows/desktop-client-build.yml b/.gitea/workflows/desktop-client-build.yml new file mode 100644 index 0000000..c6b5a2e --- /dev/null +++ b/.gitea/workflows/desktop-client-build.yml @@ -0,0 +1,765 @@ +name: Desktop Client Build + +on: + push: + branches: + - main + paths: + - "apps/desktop-client/**" + - "packages/**" + - "package.json" + - "pnpm-lock.yaml" + - "pnpm-workspace.yaml" + - ".gitea/workflows/desktop-client-build.yml" + workflow_dispatch: + inputs: + platforms: + description: "Build platforms: mac, win, or mac,win" + required: true + default: "mac,win" + mac_arches: + description: "macOS architectures: universal, arm64, x64, or comma-separated" + required: true + default: "universal" + win_arches: + description: "Windows architectures: x64, arm64, ia32, or comma-separated" + required: true + default: "x64" + version_prefix: + description: "Client version prefix. Final version is .." + required: true + default: "0.1" + version_patch: + description: "Optional fixed patch number. Empty means auto-increment." + required: false + default: "" + output_dir: + description: "NAS output directory" + required: true + default: "/vol1/1000/share/gl-test" + host: + description: "NAS SSH host" + required: true + default: "192.168.100.19" + port: + description: "NAS SSH port" + required: true + default: "22" + user: + description: "NAS SSH user" + required: true + default: "admin" + password_source: + description: "NAS SSH password source" + required: true + default: "repository_secret" + type: choice + options: + - repository_secret + - workflow_input + ssh_password: + description: "NAS SSH password. Used only when password_source is workflow_input." + required: false + default: "" + use_sudo: + description: "Use sudo for NAS file operations" + required: true + default: "true" + type: choice + options: + - "true" + - "false" + +permissions: + contents: read + +concurrency: + group: desktop-client-build + cancel-in-progress: false + +env: + DEFAULT_DESKTOP_PLATFORMS: "mac,win" + DEFAULT_MAC_ARCHES: "universal" + DEFAULT_WIN_ARCHES: "x64" + DEFAULT_VERSION_PREFIX: "0.1" + DEFAULT_NAS_OUTPUT_DIR: "/vol1/1000/share/gl-test" + DEFAULT_NAS_HOST: "192.168.100.19" + DEFAULT_NAS_PORT: "22" + DEFAULT_NAS_USER: "admin" + DEFAULT_NAS_USE_SUDO: "true" + PNPM_VERSION: "10.33.0" + +jobs: + resolve: + name: Resolve Build Metadata + runs-on: ubuntu-latest + timeout-minutes: 15 + outputs: + build_mac: ${{ steps.meta.outputs.build_mac }} + build_win: ${{ steps.meta.outputs.build_win }} + client_version: ${{ steps.meta.outputs.client_version }} + mac_arch_flags: ${{ steps.meta.outputs.mac_arch_flags }} + nas_host: ${{ steps.meta.outputs.nas_host }} + nas_port: ${{ steps.meta.outputs.nas_port }} + nas_user: ${{ steps.meta.outputs.nas_user }} + nas_use_sudo: ${{ steps.meta.outputs.nas_use_sudo }} + output_dir: ${{ steps.meta.outputs.output_dir }} + release_name: ${{ steps.meta.outputs.release_name }} + win_arch_flags: ${{ steps.meta.outputs.win_arch_flags }} + env: + REQUESTED_PLATFORMS: ${{ inputs.platforms }} + REQUESTED_MAC_ARCHES: ${{ inputs.mac_arches }} + REQUESTED_WIN_ARCHES: ${{ inputs.win_arches }} + REQUESTED_VERSION_PREFIX: ${{ inputs.version_prefix }} + REQUESTED_VERSION_PATCH: ${{ inputs.version_patch }} + REQUESTED_OUTPUT_DIR: ${{ inputs.output_dir }} + REQUESTED_NAS_HOST: ${{ inputs.host }} + REQUESTED_NAS_PORT: ${{ inputs.port }} + REQUESTED_NAS_USER: ${{ inputs.user }} + REQUESTED_NAS_USE_SUDO: ${{ inputs.use_sudo }} + NAS_SSH_PASSWORD_SOURCE: ${{ inputs.password_source }} + NAS_SSH_PASSWORD_INPUT: ${{ inputs.ssh_password }} + NAS_SSH_PASSWORD_SECRET: ${{ secrets.NAS_SSH_PASSWORD }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install SSH tools + run: | + set -eu + apt-get update + apt-get install -y openssh-client sshpass + + - name: Resolve version and targets + id: meta + run: | + set -eu + + normalize_csv() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]' + } + + arch_flags() { + value="$(normalize_csv "$1")" + allowed="$2" + result="" + + if [ -z "${value}" ]; then + echo "::error::Architecture list cannot be empty" + exit 1 + fi + + for arch in $(printf '%s' "${value}" | tr ',' ' '); do + found=false + for candidate in ${allowed}; do + if [ "${arch}" = "${candidate}" ]; then + found=true + break + fi + done + if [ "${found}" != "true" ]; then + echo "::error::Unsupported architecture '${arch}'. Allowed: ${allowed}" + exit 1 + fi + result="${result} --${arch}" + done + + printf '%s' "${result# }" + } + + platforms="$(normalize_csv "${REQUESTED_PLATFORMS:-${DEFAULT_DESKTOP_PLATFORMS}}")" + build_mac=false + build_win=false + for platform in $(printf '%s' "${platforms}" | tr ',' ' '); do + case "${platform}" in + mac|macos) + build_mac=true + ;; + win|windows) + build_win=true + ;; + *) + echo "::error::Unsupported platform '${platform}'. Use mac, win, or mac,win." + exit 1 + ;; + esac + done + + if [ "${build_mac}" != "true" ] && [ "${build_win}" != "true" ]; then + echo "::error::At least one platform must be selected." + exit 1 + fi + + mac_arch_flags="$(arch_flags "${REQUESTED_MAC_ARCHES:-${DEFAULT_MAC_ARCHES}}" "x64 arm64 universal")" + win_arch_flags="$(arch_flags "${REQUESTED_WIN_ARCHES:-${DEFAULT_WIN_ARCHES}}" "x64 arm64 ia32")" + + version_prefix="${REQUESTED_VERSION_PREFIX:-${DEFAULT_VERSION_PREFIX}}" + if ! printf '%s' "${version_prefix}" | grep -Eq '^[0-9]+\.[0-9]+$'; then + echo "::error::version_prefix must look like 0.1" + exit 1 + fi + + output_dir="${REQUESTED_OUTPUT_DIR:-${DEFAULT_NAS_OUTPUT_DIR}}" + output_dir="${output_dir%/}" + case "${output_dir}" in + /*) ;; + *) + echo "::error::output_dir must be an absolute NAS path" + exit 1 + ;; + esac + case "${output_dir}" in + *[!A-Za-z0-9_./-]*) + echo "::error::output_dir contains unsupported characters" + exit 1 + ;; + esac + + nas_host="${REQUESTED_NAS_HOST:-${DEFAULT_NAS_HOST}}" + nas_port="${REQUESTED_NAS_PORT:-${DEFAULT_NAS_PORT}}" + nas_user="${REQUESTED_NAS_USER:-${DEFAULT_NAS_USER}}" + nas_use_sudo="${REQUESTED_NAS_USE_SUDO:-${DEFAULT_NAS_USE_SUDO}}" + + case "${nas_port}" in + *[!0-9]*|"") + echo "::error::NAS SSH port must be numeric" + exit 1 + ;; + esac + case "${nas_host}" in + *[!A-Za-z0-9_.:-]*|"") + echo "::error::NAS host contains unsupported characters" + exit 1 + ;; + esac + case "${nas_user}" in + *[!A-Za-z0-9_.@-]*|"") + echo "::error::NAS user contains unsupported characters" + exit 1 + ;; + esac + case "${nas_use_sudo}" in + true|false) ;; + *) + echo "::error::use_sudo must be true or false" + exit 1 + ;; + esac + + if [ -n "${REQUESTED_VERSION_PATCH:-}" ]; then + if ! printf '%s' "${REQUESTED_VERSION_PATCH}" | grep -Eq '^[0-9]+$'; then + echo "::error::version_patch must be a non-negative integer" + exit 1 + fi + version_patch="${REQUESTED_VERSION_PATCH}" + else + password_source="${NAS_SSH_PASSWORD_SOURCE:-repository_secret}" + case "${password_source}" in + repository_secret) + nas_ssh_password="${NAS_SSH_PASSWORD_SECRET:-}" + ;; + workflow_input) + nas_ssh_password="${NAS_SSH_PASSWORD_INPUT:-}" + ;; + *) + echo "::error::password_source must be repository_secret or workflow_input" + exit 1 + ;; + esac + + if [ -n "${nas_ssh_password}" ]; then + echo "::add-mask::${nas_ssh_password}" + fi + + if [ -z "${nas_ssh_password}" ]; then + if [ "${password_source}" = "workflow_input" ]; then + echo "::error::Missing workflow input ssh_password" + else + echo "::error::Missing repository secret NAS_SSH_PASSWORD" + fi + exit 1 + fi + + mkdir -p ~/.ssh + chmod 700 ~/.ssh + ssh-keyscan -p "${nas_port}" "${nas_host}" >> ~/.ssh/known_hosts + sudo_password_b64="$(printf '%s' "${nas_ssh_password}" | base64 | tr -d '\n')" + + remote_names="$( + { + printf 'OUTPUT_DIR=%s\n' "${output_dir}" + printf 'USE_SUDO=%s\n' "${nas_use_sudo}" + printf 'SUDO_PASSWORD_B64=%s\n' "${sudo_password_b64}" + cat <<'REMOTE_SCRIPT' + set -eu + SUDO_PASSWORD="$(printf '%s' "${SUDO_PASSWORD_B64}" | base64 -d 2>/dev/null || true)" + unset SUDO_PASSWORD_B64 + + run_cmd() { + if [ "${USE_SUDO}" = "true" ]; then + printf '%s\n' "${SUDO_PASSWORD}" | sudo -S -p "" "$@" + else + "$@" + fi + } + + run_cmd mkdir -p "${OUTPUT_DIR}" + run_cmd find "${OUTPUT_DIR}" -maxdepth 3 \( -type f -o -type d -o -type l \) -print 2>/dev/null | sed 's#.*/##' + REMOTE_SCRIPT + } | SSHPASS="${nas_ssh_password}" sshpass -e ssh \ + -p "${nas_port}" \ + -o BatchMode=no \ + -o StrictHostKeyChecking=yes \ + "${nas_user}@${nas_host}" \ + "sh -s" + )" + + prefix_regex="$(printf '%s' "${version_prefix}" | sed 's/\./\\./g')" + remote_patch="$( + printf '%s\n' "${remote_names}" \ + | sed -n "s/.*${prefix_regex}\.\([0-9][0-9]*\).*/\1/p" \ + | sort -n \ + | tail -n1 + )" + + if [ -n "${remote_patch}" ]; then + version_patch=$((remote_patch + 1)) + else + version_patch=0 + fi + fi + + client_version="${version_prefix}.${version_patch}" + release_name="desktop-client-${client_version}" + + { + echo "build_mac=${build_mac}" + echo "build_win=${build_win}" + echo "client_version=${client_version}" + echo "mac_arch_flags=${mac_arch_flags}" + echo "nas_host=${nas_host}" + echo "nas_port=${nas_port}" + echo "nas_user=${nas_user}" + echo "nas_use_sudo=${nas_use_sudo}" + echo "output_dir=${output_dir}" + echo "release_name=${release_name}" + echo "win_arch_flags=${win_arch_flags}" + } >> "${GITHUB_OUTPUT}" + + echo "Version: ${client_version}" + echo "Platforms: mac=${build_mac}, win=${build_win}" + echo "macOS arch flags: ${mac_arch_flags}" + echo "Windows arch flags: ${win_arch_flags}" + echo "Use sudo on NAS: ${nas_use_sudo}" + echo "NAS output: ${nas_user}@${nas_host}:${output_dir}/${release_name}" + + build-mac: + name: Build macOS Client + needs: resolve + if: needs.resolve.outputs.build_mac == 'true' + runs-on: macos-latest + timeout-minutes: 120 + env: + CLIENT_VERSION: ${{ needs.resolve.outputs.client_version }} + MAC_ARCH_FLAGS: ${{ needs.resolve.outputs.mac_arch_flags }} + CSC_IDENTITY_AUTO_DISCOVERY: "false" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Setup pnpm + run: | + set -eu + corepack enable + corepack prepare pnpm@${PNPM_VERSION} --activate + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build unsigned macOS package + working-directory: apps/desktop-client + run: | + set -eu + unset CSC_LINK CSC_NAME CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID + export CSC_IDENTITY_AUTO_DISCOVERY=false + + node -e 'const fs = require("fs"); const p = "package.json"; const pkg = JSON.parse(fs.readFileSync(p, "utf8")); pkg.version = process.env.CLIENT_VERSION; fs.writeFileSync(p, `${JSON.stringify(pkg, null, 2)}\n`); console.log(`Desktop client version: ${pkg.version}`);' + + rm -rf release + pnpm run build:obf + pnpm exec electron-builder --mac zip ${MAC_ARCH_FLAGS} --publish never -c.mac.target=zip -c.extraMetadata.version="${CLIENT_VERSION}" + + - name: Collect macOS artifacts + working-directory: apps/desktop-client + run: | + set -eu + artifact_dir="${RUNNER_TEMP}/desktop-client-mac" + mkdir -p "${artifact_dir}" + + zip_count="$(find release -maxdepth 1 -type f -name "*.zip" | wc -l | tr -d ' ')" + if [ "${zip_count}" = "0" ]; then + echo "::error::No macOS artifacts found" + exit 1 + fi + + if [ "${zip_count}" != "1" ]; then + echo "::error::Expected exactly one macOS zip artifact for this build, found ${zip_count}." + find release -maxdepth 1 -type f -name "*.zip" -print + exit 1 + fi + + mac_zip="$(find release -maxdepth 1 -type f -name "*.zip" | head -n1)" + cp -p "${mac_zip}" "${artifact_dir}/省心推-mac-${CLIENT_VERSION}.zip" + ls -lh "${artifact_dir}" + + - name: Upload macOS artifacts + uses: actions/upload-artifact@v3 + with: + name: mac + path: ${{ runner.temp }}/desktop-client-mac + if-no-files-found: error + retention-days: 14 + + build-win: + name: Build Windows Client + needs: resolve + if: needs.resolve.outputs.build_win == 'true' + runs-on: windows-latest + timeout-minutes: 120 + env: + CLIENT_VERSION: ${{ needs.resolve.outputs.client_version }} + WIN_ARCH_FLAGS: ${{ needs.resolve.outputs.win_arch_flags }} + CSC_IDENTITY_AUTO_DISCOVERY: "false" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Setup pnpm + shell: pwsh + run: | + corepack enable + corepack prepare pnpm@$env:PNPM_VERSION --activate + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build unsigned Windows package + working-directory: apps/desktop-client + shell: pwsh + run: | + Remove-Item Env:CSC_LINK -ErrorAction SilentlyContinue + Remove-Item Env:CSC_NAME -ErrorAction SilentlyContinue + Remove-Item Env:CSC_KEY_PASSWORD -ErrorAction SilentlyContinue + Remove-Item Env:WIN_CSC_LINK -ErrorAction SilentlyContinue + Remove-Item Env:WIN_CSC_KEY_PASSWORD -ErrorAction SilentlyContinue + $env:CSC_IDENTITY_AUTO_DISCOVERY = "false" + + node -e "const fs = require('fs'); const p = 'package.json'; const pkg = JSON.parse(fs.readFileSync(p, 'utf8')); pkg.version = process.env.CLIENT_VERSION; fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n'); console.log('Desktop client version: ' + pkg.version);" + + Remove-Item -Recurse -Force release -ErrorAction SilentlyContinue + pnpm run build:obf + + $archFlags = $env:WIN_ARCH_FLAGS -split " " | Where-Object { $_ } + pnpm exec electron-builder --win zip @archFlags --publish never "-c.win.target=zip" "-c.extraMetadata.version=$env:CLIENT_VERSION" + + - name: Collect Windows artifacts + working-directory: apps/desktop-client + shell: pwsh + run: | + $artifactDir = Join-Path $env:RUNNER_TEMP "desktop-client-win" + New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null + + $files = Get-ChildItem -Path release -Filter "*.zip" -File -ErrorAction SilentlyContinue + + if (-not $files) { + throw "No Windows artifacts found" + } + + $zipFiles = $files | Sort-Object FullName -Unique + if ($zipFiles.Count -ne 1) { + $zipFiles | Format-Table FullName + throw "Expected exactly one Windows zip artifact for this build, found $($zipFiles.Count)." + } + + Copy-Item -Path $zipFiles[0].FullName -Destination (Join-Path $artifactDir "省心推-win-$env:CLIENT_VERSION.zip") -Force + Get-ChildItem $artifactDir | Format-Table Name, Length + + - name: Upload Windows artifacts + uses: actions/upload-artifact@v3 + with: + name: win + path: ${{ runner.temp }}/desktop-client-win + if-no-files-found: error + retention-days: 14 + + publish-to-nas: + name: Publish Client Artifacts to NAS + needs: + - resolve + - build-mac + - build-win + if: >- + always() && + needs.resolve.result == 'success' && + (needs.build-mac.result == 'success' || needs.build-mac.result == 'skipped') && + (needs.build-win.result == 'success' || needs.build-win.result == 'skipped') + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + CLIENT_VERSION: ${{ needs.resolve.outputs.client_version }} + NAS_HOST: ${{ needs.resolve.outputs.nas_host }} + NAS_PORT: ${{ needs.resolve.outputs.nas_port }} + NAS_USER: ${{ needs.resolve.outputs.nas_user }} + NAS_USE_SUDO: ${{ needs.resolve.outputs.nas_use_sudo }} + OUTPUT_DIR: ${{ needs.resolve.outputs.output_dir }} + RELEASE_NAME: ${{ needs.resolve.outputs.release_name }} + NAS_SSH_PASSWORD_SOURCE: ${{ inputs.password_source }} + NAS_SSH_PASSWORD_INPUT: ${{ inputs.ssh_password }} + NAS_SSH_PASSWORD_SECRET: ${{ secrets.NAS_SSH_PASSWORD }} + steps: + - name: Install SSH tools + run: | + set -eu + apt-get update + apt-get install -y openssh-client sshpass tar + + - name: Download client artifacts + uses: actions/download-artifact@v3 + with: + path: desktop-artifacts + + - name: Write manifest + run: | + set -eu + artifact_count="$(find desktop-artifacts -type f | wc -l | tr -d ' ')" + if [ "${artifact_count}" = "0" ]; then + echo "::error::No downloaded artifacts found" + exit 1 + fi + + printf '%s\n' "${CLIENT_VERSION}" > desktop-artifacts/VERSION + + cat > desktop-artifacts/manifest.json <> ~/.ssh/known_hosts + + remote="${NAS_USER}@${NAS_HOST}" + ssh_opts="-p ${NAS_PORT} -o BatchMode=no -o StrictHostKeyChecking=yes" + scp_opts="-P ${NAS_PORT} -o BatchMode=no -o StrictHostKeyChecking=yes" + remote_release_dir="${OUTPUT_DIR}/${RELEASE_NAME}" + sudo_password_b64="$(printf '%s' "${nas_ssh_password}" | base64 | tr -d '\n')" + archive_path="${RUNNER_TEMP:-/tmp}/${RELEASE_NAME}.tar.gz" + remote_archive="/tmp/${RELEASE_NAME}-${GITHUB_RUN_ID:-manual}.tar.gz" + + { + printf 'OUTPUT_DIR=%s\n' "${OUTPUT_DIR}" + printf 'RELEASE_NAME=%s\n' "${RELEASE_NAME}" + printf 'USE_SUDO=%s\n' "${NAS_USE_SUDO}" + printf 'SUDO_PASSWORD_B64=%s\n' "${sudo_password_b64}" + cat <<'REMOTE_SCRIPT' + set -eu + SUDO_PASSWORD="$(printf '%s' "${SUDO_PASSWORD_B64}" | base64 -d 2>/dev/null || true)" + unset SUDO_PASSWORD_B64 + + run_cmd() { + if [ "${USE_SUDO}" = "true" ]; then + printf '%s\n' "${SUDO_PASSWORD}" | sudo -S -p "" "$@" + else + "$@" + fi + } + + run_cmd mkdir -p "${OUTPUT_DIR}/${RELEASE_NAME}" + REMOTE_SCRIPT + } | SSHPASS="${nas_ssh_password}" sshpass -e ssh ${ssh_opts} "${remote}" "sh -s" + + tar -C desktop-artifacts -czf "${archive_path}" . + SSHPASS="${nas_ssh_password}" sshpass -e scp ${scp_opts} "${archive_path}" "${remote}:${remote_archive}" + + { + printf 'REMOTE_ARCHIVE=%s\n' "${remote_archive}" + printf 'REMOTE_RELEASE_DIR=%s\n' "${remote_release_dir}" + printf 'USE_SUDO=%s\n' "${NAS_USE_SUDO}" + printf 'SUDO_PASSWORD_B64=%s\n' "${sudo_password_b64}" + cat <<'REMOTE_SCRIPT' + set -eu + SUDO_PASSWORD="$(printf '%s' "${SUDO_PASSWORD_B64}" | base64 -d 2>/dev/null || true)" + unset SUDO_PASSWORD_B64 + + if [ "${USE_SUDO}" = "true" ]; then + printf '%s\n' "${SUDO_PASSWORD}" | sudo -S -p "" tar -xzf "${REMOTE_ARCHIVE}" -C "${REMOTE_RELEASE_DIR}" + rm -f "${REMOTE_ARCHIVE}" + else + tar -xzf "${REMOTE_ARCHIVE}" -C "${REMOTE_RELEASE_DIR}" + rm -f "${REMOTE_ARCHIVE}" + fi + REMOTE_SCRIPT + } | SSHPASS="${nas_ssh_password}" sshpass -e ssh ${ssh_opts} "${remote}" "sh -s" + + { + printf 'OUTPUT_DIR=%s\n' "${OUTPUT_DIR}" + printf 'RELEASE_NAME=%s\n' "${RELEASE_NAME}" + printf 'USE_SUDO=%s\n' "${NAS_USE_SUDO}" + printf 'SUDO_PASSWORD_B64=%s\n' "${sudo_password_b64}" + cat <<'REMOTE_SCRIPT' + set -eu + SUDO_PASSWORD="$(printf '%s' "${SUDO_PASSWORD_B64}" | base64 -d 2>/dev/null || true)" + unset SUDO_PASSWORD_B64 + + if [ "${USE_SUDO}" = "true" ]; then + tmp_file="$(mktemp)" + printf '%s\n' "${RELEASE_NAME}" > "${tmp_file}" + printf '%s\n' "${SUDO_PASSWORD}" | sudo -S -p "" cp "${tmp_file}" "${OUTPUT_DIR}/latest-desktop-client.txt" + rm -f "${tmp_file}" + else + printf '%s\n' "${RELEASE_NAME}" > "${OUTPUT_DIR}/latest-desktop-client.txt" + fi + REMOTE_SCRIPT + } | SSHPASS="${nas_ssh_password}" sshpass -e ssh ${ssh_opts} "${remote}" "sh -s" + + { + printf 'OUTPUT_DIR=%s\n' "${OUTPUT_DIR}" + printf 'CLIENT_VERSION=%s\n' "${CLIENT_VERSION}" + printf 'USE_SUDO=%s\n' "${NAS_USE_SUDO}" + printf 'SUDO_PASSWORD_B64=%s\n' "${sudo_password_b64}" + cat <<'REMOTE_SCRIPT' + set -eu + SUDO_PASSWORD="$(printf '%s' "${SUDO_PASSWORD_B64}" | base64 -d 2>/dev/null || true)" + unset SUDO_PASSWORD_B64 + + run_cmd() { + if [ "${USE_SUDO}" = "true" ]; then + printf '%s\n' "${SUDO_PASSWORD}" | sudo -S -p "" "$@" + else + "$@" + fi + } + + versioned_paths_desc() { + root="$1" + pattern="$2" + prefix="$3" + suffix="$4" + + for candidate in "${root}"/${pattern}; do + [ -e "${candidate}" ] || continue + name="${candidate##*/}" + version="${name#${prefix}}" + version="${version%${suffix}}" + + case "${version}" in + *[!0-9.]*|""|*.*.*.*) + continue + ;; + esac + + major="$(printf '%s' "${version}" | cut -d. -f1)" + minor="$(printf '%s' "${version}" | cut -d. -f2)" + patch="$(printf '%s' "${version}" | cut -d. -f3)" + + case "${major}${minor}${patch}" in + *[!0-9]*|"") + continue + ;; + esac + + printf '%s.%s.%s %s\n' "${major}" "${minor}" "${patch}" "${candidate}" + done | sort -t. -k1,1nr -k2,2nr -k3,3nr | awk '{print $2}' + } + + keep_latest_two() { + root="$1" + pattern="$2" + prefix="$3" + suffix="$4" + mode="$5" + keep=0 + + for path in $(versioned_paths_desc "${root}" "${pattern}" "${prefix}" "${suffix}"); do + keep=$((keep + 1)) + if [ "${keep}" -gt 2 ]; then + if [ "${mode}" = "dir" ]; then + run_cmd rm -rf "${path}" + else + run_cmd rm -f "${path}" + fi + fi + done + } + + for platform in mac win; do + release_platform_dir="${OUTPUT_DIR}/desktop-client-${CLIENT_VERSION}/${platform}" + [ -d "${release_platform_dir}" ] || continue + + platform_root="${OUTPUT_DIR}/${platform}" + run_cmd mkdir -p "${platform_root}" + + for zip_file in "${release_platform_dir}"/*.zip; do + [ -f "${zip_file}" ] || continue + run_cmd cp -p "${zip_file}" "${platform_root}/" + done + + keep_latest_two "${platform_root}" "省心推-${platform}-*.zip" "省心推-${platform}-" ".zip" "file" + done + + keep_latest_two "${OUTPUT_DIR}" "desktop-client-*" "desktop-client-" "" "dir" + REMOTE_SCRIPT + } | SSHPASS="${nas_ssh_password}" sshpass -e ssh ${ssh_opts} "${remote}" "sh -s" + + echo "Uploaded desktop client ${CLIENT_VERSION} to ${remote}:${remote_release_dir}"