diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cc32f3d1..34f78ea8 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -17,7 +17,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository uses: actions/checkout@v5 diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index bd1a5b4c..7c087e03 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -27,7 +27,7 @@ on: jobs: build-windows: runs-on: windows-latest - if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '' + if: (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '') && github.event.inputs.artifact_destination != 'submit' permissions: contents: write @@ -72,10 +72,6 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm run build && npx electron-builder --win --x64 --ia32 - - name: List release files - run: | - dir release - - name: Upload Windows x64 NSIS Installer uses: actions/upload-artifact@v4 if: hashFiles('release/termix_windows_x64_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none' @@ -136,7 +132,7 @@ jobs: build-linux: runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '' + if: (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '') && github.event.inputs.artifact_destination != 'submit' permissions: contents: write @@ -199,17 +195,6 @@ jobs: cd .. - - name: List release files - run: | - ls -la release/ - - - name: Debug electron-builder output - if: always() - run: | - if [ -f "release/builder-debug.yml" ]; then - cat release/builder-debug.yml - fi - - name: Upload Linux x64 AppImage uses: actions/upload-artifact@v4 if: hashFiles('release/termix_linux_x64_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none' @@ -282,9 +267,96 @@ jobs: path: release/termix_linux_armv7l_portable.tar.gz retention-days: 30 + - name: Install Flatpak builder and dependencies + run: | + sudo apt-get update + sudo apt-get install -y flatpak flatpak-builder imagemagick + + - name: Add Flathub repository + run: | + sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + + - name: Install Flatpak runtime and SDK + run: | + sudo flatpak install -y flathub org.freedesktop.Platform//24.08 + sudo flatpak install -y flathub org.freedesktop.Sdk//24.08 + sudo flatpak install -y flathub org.electronjs.Electron2.BaseApp//24.08 + + - name: Get version for Flatpak + id: flatpak-version + run: | + VERSION=$(node -p "require('./package.json').version") + RELEASE_DATE=$(date +%Y-%m-%d) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT + + - name: Prepare Flatpak files + run: | + VERSION="${{ steps.flatpak-version.outputs.version }}" + RELEASE_DATE="${{ steps.flatpak-version.outputs.release_date }}" + + CHECKSUM_X64=$(sha256sum "release/termix_linux_x64_appimage.AppImage" | awk '{print $1}') + CHECKSUM_ARM64=$(sha256sum "release/termix_linux_arm64_appimage.AppImage" | awk '{print $1}') + + mkdir -p flatpak-build + cp flatpak/com.karmaa.termix.yml flatpak-build/ + cp flatpak/com.karmaa.termix.desktop flatpak-build/ + cp flatpak/com.karmaa.termix.metainfo.xml flatpak-build/ + cp public/icon.svg flatpak-build/com.karmaa.termix.svg + convert public/icon.png -resize 256x256 flatpak-build/icon-256.png + convert public/icon.png -resize 128x128 flatpak-build/icon-128.png + + cd flatpak-build + sed -i "s|https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_x64_appimage.AppImage|file://$(realpath ../release/termix_linux_x64_appimage.AppImage)|g" com.karmaa.termix.yml + sed -i "s|https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_arm64_appimage.AppImage|file://$(realpath ../release/termix_linux_arm64_appimage.AppImage)|g" com.karmaa.termix.yml + sed -i "s/CHECKSUM_X64_PLACEHOLDER/$CHECKSUM_X64/g" com.karmaa.termix.yml + sed -i "s/CHECKSUM_ARM64_PLACEHOLDER/$CHECKSUM_ARM64/g" com.karmaa.termix.yml + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" com.karmaa.termix.metainfo.xml + sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" com.karmaa.termix.metainfo.xml + + - name: Build Flatpak bundle + run: | + cd flatpak-build + flatpak-builder --repo=repo --force-clean --disable-rofiles-fuse build-dir com.karmaa.termix.yml + + # Determine the architecture + ARCH=$(uname -m) + if [ "$ARCH" = "x86_64" ]; then + FLATPAK_ARCH="x86_64" + elif [ "$ARCH" = "aarch64" ]; then + FLATPAK_ARCH="aarch64" + else + FLATPAK_ARCH="$ARCH" + fi + + # Build bundle for the current architecture + flatpak build-bundle repo ../release/termix_linux_flatpak.flatpak com.karmaa.termix --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo + + - name: Create flatpakref file + run: | + VERSION="${{ steps.flatpak-version.outputs.version }}" + cp flatpak/com.karmaa.termix.flatpakref release/ + sed -i "s|VERSION_PLACEHOLDER|release-${VERSION}-tag|g" release/com.karmaa.termix.flatpakref + + - name: Upload Flatpak bundle + uses: actions/upload-artifact@v4 + if: hashFiles('release/termix_linux_flatpak.flatpak') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_flatpak + path: release/termix_linux_flatpak.flatpak + retention-days: 30 + + - name: Upload Flatpakref + uses: actions/upload-artifact@v4 + if: hashFiles('release/com.karmaa.termix.flatpakref') != '' && github.event.inputs.artifact_destination != 'none' + with: + name: termix_linux_flatpakref + path: release/com.karmaa.termix.flatpakref + retention-days: 30 + build-macos: runs-on: macos-latest - if: github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all' + if: (github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all') && github.event.inputs.artifact_destination != 'submit' needs: [] permissions: contents: write @@ -425,11 +497,6 @@ jobs: export GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" npx electron-builder --mac dmg --universal --x64 --arm64 --publish never - - name: List release directory - if: steps.check_certs.outputs.has_certs == 'true' - run: | - ls -R release/ || echo "Release directory not found" - - name: Upload macOS MAS PKG if: steps.check_certs.outputs.has_certs == 'true' && hashFiles('release/termix_macos_universal_mas.pkg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release' || github.event.inputs.artifact_destination == 'submit') uses: actions/upload-artifact@v4 @@ -463,42 +530,51 @@ jobs: path: release/termix_macos_arm64_dmg.dmg retention-days: 30 - - name: Check for App Store Connect API credentials - if: steps.check_certs.outputs.has_certs == 'true' - id: check_asc_creds + - name: Get version for Homebrew + id: homebrew-version run: | - if [ -n "${{ secrets.APPLE_KEY_ID }}" ] && [ -n "${{ secrets.APPLE_ISSUER_ID }}" ] && [ -n "${{ secrets.APPLE_KEY_CONTENT }}" ]; then - echo "has_credentials=true" >> $GITHUB_OUTPUT - fi + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT - - name: Setup Ruby for Fastlane - if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' - uses: ruby/setup-ruby@v1 + - name: Generate Homebrew Cask + if: hashFiles('release/termix_macos_universal_dmg.dmg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release') + run: | + VERSION="${{ steps.homebrew-version.outputs.version }}" + DMG_PATH="release/termix_macos_universal_dmg.dmg" + + CHECKSUM=$(shasum -a 256 "$DMG_PATH" | awk '{print $1}') + + mkdir -p homebrew-generated + cp Casks/termix.rb homebrew-generated/termix.rb + + sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-generated/termix.rb + sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-generated/termix.rb + sed -i '' "s|version \".*\"|version \"$VERSION\"|g" homebrew-generated/termix.rb + sed -i '' "s|sha256 \".*\"|sha256 \"$CHECKSUM\"|g" homebrew-generated/termix.rb + sed -i '' "s|release-[0-9.]*-tag|release-$VERSION-tag|g" homebrew-generated/termix.rb + + - name: Upload Homebrew Cask as artifact + uses: actions/upload-artifact@v4 + if: hashFiles('homebrew-generated/termix.rb') != '' && github.event.inputs.artifact_destination == 'file' with: - ruby-version: "3.2" - bundler-cache: false + name: termix_macos_homebrew_cask + path: homebrew-generated/termix.rb + retention-days: 30 - - name: Install Fastlane - if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' + - name: Upload Homebrew Cask to release + if: hashFiles('homebrew-generated/termix.rb') != '' && github.event.inputs.artifact_destination == 'release' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gem install fastlane -N + VERSION="${{ steps.homebrew-version.outputs.version }}" + RELEASE_TAG="release-$VERSION-tag" - - name: Deploy to App Store Connect (TestFlight) - if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit' - run: | - PKG_FILE=$(find release -name "*.pkg" -type f | head -n 1) - if [ -z "$PKG_FILE" ]; then + gh release list --repo ${{ github.repository }} --limit 100 | grep -q "$RELEASE_TAG" || { + echo "Release $RELEASE_TAG not found" exit 1 - fi + } - mkdir -p ~/private_keys - echo "${{ secrets.APPLE_KEY_CONTENT }}" | base64 --decode > ~/private_keys/AuthKey_${{ secrets.APPLE_KEY_ID }}.p8 - - xcrun altool --upload-app -f "$PKG_FILE" \ - --type macos \ - --apiKey "${{ secrets.APPLE_KEY_ID }}" \ - --apiIssuer "${{ secrets.APPLE_ISSUER_ID }}" - continue-on-error: true + gh release upload "$RELEASE_TAG" homebrew-generated/termix.rb --repo ${{ github.repository }} --clobber - name: Clean up keychains if: always() @@ -508,8 +584,7 @@ jobs: submit-to-chocolatey: runs-on: windows-latest - if: github.event.inputs.artifact_destination == 'submit' - needs: [build-windows] + if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == '') permissions: contents: read @@ -525,20 +600,25 @@ jobs: $VERSION = (Get-Content package.json | ConvertFrom-Json).version echo "version=$VERSION" >> $env:GITHUB_OUTPUT - - name: Download Windows x64 MSI artifact - uses: actions/download-artifact@v4 - with: - name: termix_windows_x64_msi - path: artifact - - - name: Get MSI file info + - name: Download and prepare MSI info from public release id: msi-info run: | $VERSION = "${{ steps.package-version.outputs.version }}" - $MSI_FILE = Get-ChildItem -Path artifact -Filter "*.msi" | Select-Object -First 1 - $MSI_NAME = $MSI_FILE.Name - $CHECKSUM = (Get-FileHash -Path $MSI_FILE.FullName -Algorithm SHA256).Hash + $MSI_NAME = "termix_windows_x64_msi.msi" + $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$($VERSION)-tag/$($MSI_NAME)" + Write-Host "Downloading from $DOWNLOAD_URL" + New-Item -ItemType Directory -Force -Path "release_asset" + $DOWNLOAD_PATH = "release_asset\$MSI_NAME" + + try { + Invoke-WebRequest -Uri $DOWNLOAD_URL -OutFile $DOWNLOAD_PATH -UseBasicParsing + } catch { + Write-Error "Failed to download MSI from $DOWNLOAD_URL. Please ensure the release and asset exist." + exit 1 + } + + $CHECKSUM = (Get-FileHash -Path $DOWNLOAD_PATH -Algorithm SHA256).Hash echo "msi_name=$MSI_NAME" >> $env:GITHUB_OUTPUT echo "checksum=$CHECKSUM" >> $env:GITHUB_OUTPUT @@ -609,8 +689,8 @@ jobs: submit-to-flatpak: runs-on: ubuntu-latest - if: github.event.inputs.artifact_destination == 'submit' - needs: [build-linux] + if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == '') + needs: [] permissions: contents: read @@ -628,30 +708,27 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT - - name: Download Linux x64 AppImage artifact - uses: actions/download-artifact@v4 - with: - name: termix_linux_x64_appimage - path: artifact-x64 - - - name: Download Linux arm64 AppImage artifact - uses: actions/download-artifact@v4 - with: - name: termix_linux_arm64_appimage - path: artifact-arm64 - - - name: Get AppImage file info + - name: Download and prepare AppImage info from public release id: appimage-info run: | VERSION="${{ steps.package-version.outputs.version }}" + mkdir -p release_assets - APPIMAGE_X64_FILE=$(find artifact-x64 -name "*.AppImage" -type f | head -n 1) - APPIMAGE_X64_NAME=$(basename "$APPIMAGE_X64_FILE") - CHECKSUM_X64=$(sha256sum "$APPIMAGE_X64_FILE" | awk '{print $1}') + APPIMAGE_X64_NAME="termix_linux_x64_appimage.AppImage" + URL_X64="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_X64_NAME" + PATH_X64="release_assets/$APPIMAGE_X64_NAME" + echo "Downloading x64 AppImage from $URL_X64" + curl -L -o "$PATH_X64" "$URL_X64" + chmod +x "$PATH_X64" + CHECKSUM_X64=$(sha256sum "$PATH_X64" | awk '{print $1}') - APPIMAGE_ARM64_FILE=$(find artifact-arm64 -name "*.AppImage" -type f | head -n 1) - APPIMAGE_ARM64_NAME=$(basename "$APPIMAGE_ARM64_FILE") - CHECKSUM_ARM64=$(sha256sum "$APPIMAGE_ARM64_FILE" | awk '{print $1}') + APPIMAGE_ARM64_NAME="termix_linux_arm64_appimage.AppImage" + URL_ARM64="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_ARM64_NAME" + PATH_ARM64="release_assets/$APPIMAGE_ARM64_NAME" + echo "Downloading arm64 AppImage from $URL_ARM64" + curl -L -o "$PATH_ARM64" "$URL_ARM64" + chmod +x "$PATH_ARM64" + CHECKSUM_ARM64=$(sha256sum "$PATH_ARM64" | awk '{print $1}') echo "appimage_x64_name=$APPIMAGE_X64_NAME" >> $GITHUB_OUTPUT echo "checksum_x64=$CHECKSUM_X64" >> $GITHUB_OUTPUT @@ -690,10 +767,6 @@ jobs: sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.metainfo.xml sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak-submission/com.karmaa.termix.metainfo.xml - - name: List submission files - run: | - ls -la flatpak-submission/ - - name: Upload Flatpak submission as artifact uses: actions/upload-artifact@v4 with: @@ -703,8 +776,8 @@ jobs: submit-to-homebrew: runs-on: macos-latest - if: github.event.inputs.artifact_destination == 'submit' - needs: [build-macos] + if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'macos') + needs: [] permissions: contents: read @@ -720,19 +793,28 @@ jobs: VERSION=$(node -p "require('./package.json').version") echo "version=$VERSION" >> $GITHUB_OUTPUT - - name: Download macOS Universal DMG artifact - uses: actions/download-artifact@v4 - with: - name: termix_macos_universal_dmg - path: artifact - - - name: Get DMG file info + - name: Download and prepare DMG info from public release id: dmg-info run: | VERSION="${{ steps.package-version.outputs.version }}" - DMG_FILE=$(find artifact -name "*.dmg" -type f | head -n 1) - DMG_NAME=$(basename "$DMG_FILE") - CHECKSUM=$(shasum -a 256 "$DMG_FILE" | awk '{print $1}') + DMG_NAME="termix_macos_universal_dmg.dmg" + URL="https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$DMG_NAME" + + mkdir -p release_asset + DOWNLOAD_PATH="release_asset/$DMG_NAME" + echo "Downloading DMG from $URL" + + if command -v curl &> /dev/null; then + curl -L -o "$DOWNLOAD_PATH" "$URL" + elif command -v wget &> /dev/null; then + wget -O "$DOWNLOAD_PATH" "$URL" + else + echo "Neither curl nor wget is available, installing curl" + brew install curl + curl -L -o "$DOWNLOAD_PATH" "$URL" + fi + + CHECKSUM=$(shasum -a 256 "$DOWNLOAD_PATH" | awk '{print $1}') echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT @@ -745,23 +827,15 @@ jobs: mkdir -p homebrew-submission/Casks/t - cp homebrew/termix.rb homebrew-submission/Casks/t/termix.rb + cp Casks/termix.rb homebrew-submission/Casks/t/termix.rb sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb - name: Verify Cask syntax run: | - if ! command -v brew &> /dev/null; then - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - fi - ruby -c homebrew-submission/Casks/t/termix.rb - - name: List submission files - run: | - find homebrew-submission -type f - - name: Upload Homebrew submission as artifact uses: actions/upload-artifact@v4 with: @@ -789,10 +863,6 @@ jobs: env: GH_TOKEN: ${{ github.token }} - - name: Display artifact structure - run: | - ls -R artifacts/ - - name: Upload artifacts to latest release run: | cd artifacts @@ -808,3 +878,130 @@ jobs: done env: GH_TOKEN: ${{ github.token }} + + submit-to-testflight: + runs-on: macos-latest + if: github.event.inputs.artifact_destination == 'submit' && (github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'macos') + needs: [] + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: | + for i in 1 2 3; + do + if npm ci; then + break + else + if [ $i -eq 3 ]; then + exit 1 + fi + sleep 10 + fi + done + npm install --force @rollup/rollup-darwin-arm64 + npm install dmg-license + + - name: Check for Code Signing Certificates + id: check_certs + run: | + if [ -n "${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.MAC_P12_PASSWORD }}" ]; then + echo "has_certs=true" >> $GITHUB_OUTPUT + fi + + - name: Import Code Signing Certificates + if: steps.check_certs.outputs.has_certs == 'true' + env: + MAC_BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }} + MAC_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MAC_INSTALLER_CERTIFICATE_BASE64 }} + MAC_P12_PASSWORD: ${{ secrets.MAC_P12_PASSWORD }} + MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }} + run: | + APP_CERT_PATH=$RUNNER_TEMP/app_certificate.p12 + INSTALLER_CERT_PATH=$RUNNER_TEMP/installer_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $APP_CERT_PATH + + if [ -n "$MAC_INSTALLER_CERTIFICATE_BASE64" ]; then + echo -n "$MAC_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $INSTALLER_CERT_PATH + fi + + security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + security import $APP_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + + if [ -f "$INSTALLER_CERT_PATH" ]; then + security import $INSTALLER_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + fi + + security list-keychain -d user -s $KEYCHAIN_PATH + + security find-identity -v -p codesigning $KEYCHAIN_PATH + + - name: Build macOS App Store Package + if: steps.check_certs.outputs.has_certs == 'true' + env: + ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + CURRENT_VERSION=$(node -p "require('./package.json').version") + BUILD_VERSION="${{ github.run_number }}" + + npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" + + - name: Check for App Store Connect API credentials + id: check_asc_creds + run: | + if [ -n "${{ secrets.APPLE_KEY_ID }}" ] && [ -n "${{ secrets.APPLE_ISSUER_ID }}" ] && [ -n "${{ secrets.APPLE_KEY_CONTENT }}" ]; then + echo "has_credentials=true" >> $GITHUB_OUTPUT + fi + + - name: Setup Ruby for Fastlane + if: steps.check_asc_creds.outputs.has_credentials == 'true' + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.2" + bundler-cache: false + + - name: Install Fastlane + if: steps.check_asc_creds.outputs.has_credentials == 'true' + run: | + gem install fastlane -N + + - name: Deploy to App Store Connect (TestFlight) + if: steps.check_asc_creds.outputs.has_credentials == 'true' + run: | + PKG_FILE=$(find release -name "termix_macos_universal_mas.pkg" -type f | head -n 1) + if [ -z "$PKG_FILE" ]; then + echo "PKG file not found, exiting." + exit 1 + fi + + mkdir -p ~/private_keys + echo "${{ secrets.APPLE_KEY_CONTENT }}" | base64 --decode > ~/private_keys/AuthKey_${{ secrets.APPLE_KEY_ID }}.p8 + + xcrun altool --upload-app -f "$PKG_FILE" \ + --type macos \ + --apiKey "${{ secrets.APPLE_KEY_ID }}" \ + --apiIssuer "${{ secrets.APPLE_ISSUER_ID }}" + continue-on-error: true + + - name: Clean up keychains + if: always() + run: | + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml new file mode 100644 index 00000000..7c9db568 --- /dev/null +++ b/.github/workflows/translate.yml @@ -0,0 +1,437 @@ +name: Auto Translate + +on: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + translate-zh: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t zh --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-zh + path: src/locales/zh.json + continue-on-error: true + + translate-ru: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t ru --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-ru + path: src/locales/ru.json + continue-on-error: true + + translate-pt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t pt --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-pt + path: src/locales/pt.json + continue-on-error: true + + translate-fr: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t fr --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-fr + path: src/locales/fr.json + continue-on-error: true + + translate-es: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t es --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-es + path: src/locales/es.json + continue-on-error: true + + translate-de: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t de --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-de + path: src/locales/de.json + continue-on-error: true + + translate-hi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t hi --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-hi + path: src/locales/hi.json + continue-on-error: true + + translate-bn: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t bn --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-bn + path: src/locales/bn.json + continue-on-error: true + + translate-ja: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t ja --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-ja + path: src/locales/ja.json + continue-on-error: true + + translate-vi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t vi --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-vi + path: src/locales/vi.json + continue-on-error: true + + translate-tr: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t tr --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-tr + path: src/locales/tr.json + continue-on-error: true + + translate-ko: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t ko --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-ko + path: src/locales/ko.json + continue-on-error: true + + translate-it: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t it --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-it + path: src/locales/it.json + continue-on-error: true + + translate-he: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t he --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-he + path: src/locales/he.json + continue-on-error: true + + translate-ar: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t ar --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-ar + path: src/locales/ar.json + continue-on-error: true + + translate-pl: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t pl --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-pl + path: src/locales/pl.json + continue-on-error: true + + translate-nl: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t nl --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-nl + path: src/locales/nl.json + continue-on-error: true + + translate-sv: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t sv --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-sv + path: src/locales/sv.json + continue-on-error: true + + translate-id: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t id --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-id + path: src/locales/id.json + continue-on-error: true + + translate-th: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t th --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-th + path: src/locales/th.json + continue-on-error: true + + translate-uk: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t uk --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-uk + path: src/locales/uk.json + continue-on-error: true + + translate-cs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t cs --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-cs + path: src/locales/cs.json + continue-on-error: true + + translate-ro: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t ro --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-ro + path: src/locales/ro.json + continue-on-error: true + + translate-el: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t el --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-el + path: src/locales/el.json + continue-on-error: true + + translate-nb: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npx i18n-auto-translation -k ${{ secrets.GOOGLE_TRANSLATE_API_KEY }} -d "src/locales" -f en -t nb --maxLinesPerRequest 1 + - uses: actions/upload-artifact@v4 + with: + name: translations-nb + path: src/locales/nb.json + continue-on-error: true + + create-pr: + needs: + [ + translate-zh, + translate-ru, + translate-pt, + translate-fr, + translate-es, + translate-de, + translate-hi, + translate-bn, + translate-ja, + translate-vi, + translate-tr, + translate-ko, + translate-it, + translate-he, + translate-ar, + translate-pl, + translate-nl, + translate-sv, + translate-id, + translate-th, + translate-uk, + translate-cs, + translate-ro, + translate-el, + translate-nb, + ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GHCR_TOKEN }} + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: translations-temp + + - name: Move translations to src/locales + run: | + cp translations-temp/translations-zh/zh.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-ru/ru.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-pt/pt.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-fr/fr.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-es/es.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-de/de.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-hi/hi.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-bn/bn.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-ja/ja.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-vi/vi.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-tr/tr.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-ko/ko.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-it/it.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-he/he.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-ar/ar.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-pl/pl.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-nl/nl.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-sv/sv.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-id/id.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-th/th.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-uk/uk.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-cs/cs.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-ro/ro.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-el/el.json src/locales/ 2>/dev/null || true + cp translations-temp/translations-nb/nb.json src/locales/ 2>/dev/null || true + rm -rf translations-temp + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GHCR_TOKEN }} + commit-message: "chore: auto-translate to multiple languages" + branch: translations-update + delete-branch: true + title: "chore: Update translations for all languages" diff --git a/.gitignore b/.gitignore index af4f217b..c3d02880 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ dist-ssr /.mcp.json /nul /.vscode/ +/CLAUDE.md diff --git a/Casks/termix.rb b/Casks/termix.rb index 6f6d0d86..ccbeab85 100644 --- a/Casks/termix.rb +++ b/Casks/termix.rb @@ -1,6 +1,6 @@ cask "termix" do - version "1.9.0" - sha256 "8fedd242b3cae1ebfd0c391a36f1c246a26ecac258b02478ee8dea2f33cd6d96" + version "1.10.0" + sha256 "327c5026006c949f992447835aa6754113f731065b410bedbfa5da5af7cb2386" url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg" name "Termix" diff --git a/README-CN.md b/README-CN.md index a882666c..593c6a23 100644 --- a/README-CN.md +++ b/README-CN.md @@ -51,20 +51,22 @@ Termix 是一个开源、永久免费、自托管的一体化服务器管理平 - **SSH 终端访问** - 功能齐全的终端,具有分屏支持(最多 4 个面板)和类似浏览器的选项卡系统。包括对自定义终端的支持,包括常见终端主题、字体和其他组件 - **SSH 隧道管理** - 创建和管理 SSH 隧道,具有自动重新连接和健康监控功能 - **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。无缝上传、下载、重命名、删除和移动文件 +- **Docker 管理** - 启动、停止、暂停、删除容器。查看容器统计信息。使用 docker exec 终端控制容器。它不是用来替代 Portainer 或 Dockge,而是用于简单管理你的容器而不是创建它们。 - **SSH 主机管理器** - 保存、组织和管理您的 SSH 连接,支持标签和文件夹,并轻松保存可重用的登录信息,同时能够自动部署 SSH 密钥 - **服务器统计** - 在任何 SSH 服务器上查看 CPU、内存和磁盘使用情况以及网络、正常运行时间和系统信息 - **仪表板** - 在仪表板上一目了然地查看服务器信息 +- **RBAC** - 创建角色并在用户/角色之间共享主机 - **用户认证** - 安全的用户管理,具有管理员控制以及 OIDC 和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDC/本地帐户链接在一起。 - **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://docs.termix.site/security)了解更多信息。 - **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据 - **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向 -- **现代用户界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁的桌面/移动设备友好界面 -- **语言** - 内置支持英语、中文、德语和葡萄牙语 +- **现代用户界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁的桌面/移动设备友好界面。可选择基于深色或浅色模式的用户界面。 +- **语言** - 内置支持约 30 种语言(通过 Google 翻译批量翻译,结果可能有所不同) - **平台支持** - 可作为 Web 应用程序、桌面应用程序(Windows、Linux 和 macOS)以及适用于 iOS 和 Android 的专用移动/平板电脑应用程序。 - **SSH 工具** - 创建可重用的命令片段,单击即可执行。在多个打开的终端上同时运行一个命令。 - **命令历史** - 自动完成并查看以前运行的 SSH 命令 - **命令面板** - 双击左 Shift 键可快速使用键盘访问 SSH 连接 -- **SSH 功能丰富** - 支持跳板机、warpgate、基于 TOTP 的连接等。 +- **SSH 功能丰富** - 支持跳板机、warpgate、基于 TOTP 的连接、SOCKS5、密码自动填充等。 # 计划功能 @@ -140,6 +142,12 @@ volumes:

Termix Demo 7 + Termix Demo 8 +

+ +

+ Termix Demo 9 + Termix Demo 110

@@ -147,7 +155,7 @@ volumes: 你的浏览器不支持 video 标签。

-视频和图像可能已过时。 +某些视频和图像可能已过时或可能无法完美展示功能。 # 许可证 diff --git a/README.md b/README.md index 0128358a..b13da02b 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,6 @@ Achieved on September 1st, 2025

-#### Top Technologies - -[![React Badge](https://img.shields.io/badge/-React-61DBFB?style=flat-square&labelColor=black&logo=react&logoColor=61DBFB)](#) -[![TypeScript Badge](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&labelColor=black&logo=typescript&logoColor=3178C6)](#) -[![Node.js Badge](https://img.shields.io/badge/-Node.js-3C873A?style=flat-square&labelColor=black&logo=node.js&logoColor=3C873A)](#) -[![Vite Badge](https://img.shields.io/badge/-Vite-646CFF?style=flat-square&labelColor=black&logo=vite&logoColor=646CFF)](#) -[![Tailwind CSS Badge](https://img.shields.io/badge/-TailwindCSS-38B2AC?style=flat-square&labelColor=black&logo=tailwindcss&logoColor=38B2AC)](#) -[![Docker Badge](https://img.shields.io/badge/-Docker-2496ED?style=flat-square&labelColor=black&logo=docker&logoColor=2496ED)](#) -[![SQLite Badge](https://img.shields.io/badge/-SQLite-003B57?style=flat-square&labelColor=black&logo=sqlite&logoColor=003B57)](#) -[![Radix UI Badge](https://img.shields.io/badge/-Radix%20UI-161618?style=flat-square&labelColor=black&logo=radixui&logoColor=161618)](#) -

@@ -134,7 +123,7 @@ Supported Devices: - MSI Installer - Chocolatey Package Manager (coming soon) - Linux (x64/ia32) - - Portable + - Portable [(AUR available)](https://aur.archlinux.org/packages/termix-bin) - AppImage - Deb - Flatpak (coming soon) @@ -170,6 +159,12 @@ volumes: driver: local ``` +# Sponsors + +Thank you to [Digital Ocean](https://www.digitalocean.com/) for sponsoring Termix and covering our documentation server costs! + +Powered by DigitalOcean + # Support If you need help or want to request a feature with Termix, visit the [Issues](https://github.com/Termix-SSH/Support/issues) page, log in, and press `New Issue`. diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000..51095a4b --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,3 @@ +files: + - source: /src/locales/en.json + translation: /src/locales/translated/%two_letters_code%.json diff --git a/docker/Dockerfile b/docker/Dockerfile index c67b6686..29313736 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -19,7 +19,7 @@ COPY . . RUN find public/fonts -name "*.ttf" ! -name "*Regular.ttf" ! -name "*Bold.ttf" ! -name "*Italic.ttf" -delete RUN npm cache clean --force && \ - npm run build + NODE_OPTIONS="--max-old-space-size=2048" npm run build # Stage 3: Build backend FROM deps AS backend-builder @@ -53,16 +53,18 @@ ENV DATA_DIR=/app/data \ RUN apt-get update && apt-get install -y nginx gettext-base openssl && \ rm -rf /var/lib/apt/lists/* && \ - mkdir -p /app/data /app/uploads && \ - chown -R node:node /app/data /app/uploads && \ - useradd -r -s /bin/false nginx + mkdir -p /app/data /app/uploads /app/nginx /app/nginx/logs /app/nginx/cache /app/nginx/client_body && \ + chown -R node:node /app && \ + chmod 755 /app/data /app/uploads /app/nginx && \ + touch /app/nginx/nginx.conf && \ + chown node:node /app/nginx/nginx.conf -COPY docker/nginx.conf /etc/nginx/nginx.conf -COPY docker/nginx-https.conf /etc/nginx/nginx-https.conf +COPY docker/nginx.conf /app/nginx/nginx.conf.template +COPY docker/nginx-https.conf /app/nginx/nginx-https.conf.template -COPY --chown=nginx:nginx --from=frontend-builder /app/dist /usr/share/nginx/html -COPY --chown=nginx:nginx --from=frontend-builder /app/src/locales /usr/share/nginx/html/locales -COPY --chown=nginx:nginx --from=frontend-builder /app/public/fonts /usr/share/nginx/html/fonts +COPY --chown=node:node --from=frontend-builder /app/dist /app/html +COPY --chown=node:node --from=frontend-builder /app/src/locales /app/html/locales +COPY --chown=node:node --from=frontend-builder /app/public/fonts /app/html/fonts COPY --chown=node:node --from=production-deps /app/node_modules /app/node_modules COPY --chown=node:node --from=backend-builder /app/dist/backend ./dist/backend @@ -72,6 +74,12 @@ VOLUME ["/app/data"] EXPOSE ${PORT} 30001 30002 30003 30004 30005 30006 +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD node -e "require('http').get('http://localhost:30001/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))" + COPY docker/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh + +USER node + CMD ["/entrypoint.sh"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 67d389c2..165c9ee2 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -11,24 +11,21 @@ echo "Configuring web UI to run on port: $PORT" if [ "$ENABLE_SSL" = "true" ]; then echo "SSL enabled - using HTTPS configuration with redirect" - NGINX_CONF_SOURCE="/etc/nginx/nginx-https.conf" + NGINX_CONF_SOURCE="/app/nginx/nginx-https.conf.template" else echo "SSL disabled - using HTTP-only configuration (default)" - NGINX_CONF_SOURCE="/etc/nginx/nginx.conf" + NGINX_CONF_SOURCE="/app/nginx/nginx.conf.template" fi -envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /etc/nginx/nginx.conf.tmp -mv /etc/nginx/nginx.conf.tmp /etc/nginx/nginx.conf +envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /app/nginx/nginx.conf mkdir -p /app/data /app/uploads -chown -R node:node /app/data /app/uploads -chmod 755 /app/data /app/uploads +chmod 755 /app/data /app/uploads 2>/dev/null || true if [ "$ENABLE_SSL" = "true" ]; then echo "Checking SSL certificate configuration..." mkdir -p /app/data/ssl - chown -R node:node /app/data/ssl - chmod 755 /app/data/ssl + chmod 755 /app/data/ssl 2>/dev/null || true DOMAIN=${SSL_DOMAIN:-localhost} @@ -84,7 +81,6 @@ EOF chmod 600 /app/data/ssl/termix.key chmod 644 /app/data/ssl/termix.crt - chown node:node /app/data/ssl/termix.key /app/data/ssl/termix.crt rm -f /app/data/ssl/openssl.conf @@ -93,7 +89,7 @@ EOF fi echo "Starting nginx..." -nginx +nginx -c /app/nginx/nginx.conf echo "Starting backend services..." cd /app @@ -110,11 +106,7 @@ else echo "Warning: package.json not found" fi -if command -v su-exec > /dev/null 2>&1; then - su-exec node node dist/backend/backend/starter.js -else - su -s /bin/sh node -c "node dist/backend/backend/starter.js" -fi +node dist/backend/backend/starter.js echo "All services started" diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index 2ffd549b..45455b81 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -1,11 +1,24 @@ +worker_processes 1; +master_process off; +pid /app/nginx/nginx.pid; +error_log /app/nginx/logs/error.log warn; + events { worker_connections 1024; } http { - include mime.types; + include /etc/nginx/mime.types; default_type application/octet-stream; + access_log /app/nginx/logs/access.log; + + client_body_temp_path /app/nginx/client_body; + proxy_temp_path /app/nginx/proxy_temp; + fastcgi_temp_path /app/nginx/fastcgi_temp; + uwsgi_temp_path /app/nginx/uwsgi_temp; + scgi_temp_path /app/nginx/scgi_temp; + sendfile on; keepalive_timeout 65; client_header_timeout 300s; @@ -37,9 +50,17 @@ http { add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + root /app/html; + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + location / { - root /usr/share/nginx/html; + root /app/html; index index.html index.htm; + try_files $uri $uri/ /index.html; } location ~* \.map$ { @@ -93,6 +114,15 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/rbac(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location ~ ^/credentials(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -292,6 +322,10 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; } location ~ ^/uptime(/.*)?$ { @@ -312,9 +346,45 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ^~ /docker/console/ { + proxy_pass http://127.0.0.1:30008/; + proxy_http_version 1.1; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + proxy_connect_timeout 10s; + + proxy_buffering off; + proxy_request_buffering off; + + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; + } + + location ~ ^/docker(/.*)?$ { + proxy_pass http://127.0.0.1:30007; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + error_page 500 502 503 504 /50x.html; location = /50x.html { - root /usr/share/nginx/html; + root /app/html; } } } diff --git a/docker/nginx.conf b/docker/nginx.conf index 8b07d773..9e884581 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,11 +1,24 @@ +worker_processes 1; +master_process off; +pid /app/nginx/nginx.pid; +error_log /app/nginx/logs/error.log warn; + events { worker_connections 1024; } http { - include mime.types; + include /etc/nginx/mime.types; default_type application/octet-stream; + access_log /app/nginx/logs/access.log; + + client_body_temp_path /app/nginx/client_body; + proxy_temp_path /app/nginx/proxy_temp; + fastcgi_temp_path /app/nginx/fastcgi_temp; + uwsgi_temp_path /app/nginx/uwsgi_temp; + scgi_temp_path /app/nginx/scgi_temp; + sendfile on; keepalive_timeout 65; client_header_timeout 300s; @@ -27,14 +40,14 @@ http { add_header X-XSS-Protection "1; mode=block" always; location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - root /usr/share/nginx/html; + root /app/html; expires 1y; add_header Cache-Control "public, immutable"; try_files $uri =404; } location / { - root /usr/share/nginx/html; + root /app/html; index index.html index.htm; try_files $uri $uri/ /index.html; } @@ -90,6 +103,15 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/rbac(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location ~ ^/credentials(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -289,6 +311,10 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; } location ~ ^/uptime(/.*)?$ { @@ -309,9 +335,45 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ^~ /docker/console/ { + proxy_pass http://127.0.0.1:30008/; + proxy_http_version 1.1; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + proxy_connect_timeout 10s; + + proxy_buffering off; + proxy_request_buffering off; + + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; + } + + location ~ ^/docker(/.*)?$ { + proxy_pass http://127.0.0.1:30007; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + error_page 500 502 503 504 /50x.html; location = /50x.html { - root /usr/share/nginx/html; + root /app/html; } } } diff --git a/electron-builder.json b/electron-builder.json index 218153e1..8137f73d 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -124,5 +124,6 @@ "ITSAppUsesNonExemptEncryption": false, "NSAppleEventsUsageDescription": "Termix needs access to control other applications for terminal operations." } - } + }, + "generateUpdatesFilesForAllChannels": true } diff --git a/electron/main.cjs b/electron/main.cjs index 97ced567..06dc9ea2 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -11,13 +11,9 @@ const fs = require("fs"); const os = require("os"); if (process.platform === "linux") { - app.commandLine.appendSwitch("--no-sandbox"); - app.commandLine.appendSwitch("--disable-setuid-sandbox"); - app.commandLine.appendSwitch("--disable-dev-shm-usage"); + app.commandLine.appendSwitch("--ozone-platform-hint=auto"); - app.disableHardwareAcceleration(); - app.commandLine.appendSwitch("--disable-gpu"); - app.commandLine.appendSwitch("--disable-gpu-compositing"); + app.commandLine.appendSwitch("--enable-features=VaapiVideoDecoder"); } app.commandLine.appendSwitch("--ignore-certificate-errors"); diff --git a/electron/preload.js b/electron/preload.js index 1db1b356..ea1f3458 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -2,21 +2,6 @@ const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("electronAPI", { getAppVersion: () => ipcRenderer.invoke("get-app-version"), - getPlatform: () => ipcRenderer.invoke("get-platform"), - checkElectronUpdate: () => ipcRenderer.invoke("check-electron-update"), - - getServerConfig: () => ipcRenderer.invoke("get-server-config"), - saveServerConfig: (config) => - ipcRenderer.invoke("save-server-config", config), - testServerConnection: (serverUrl) => - ipcRenderer.invoke("test-server-connection", serverUrl), - - showSaveDialog: (options) => ipcRenderer.invoke("show-save-dialog", options), - showOpenDialog: (options) => ipcRenderer.invoke("show-open-dialog", options), - - onUpdateAvailable: (callback) => ipcRenderer.on("update-available", callback), - onUpdateDownloaded: (callback) => - ipcRenderer.on("update-downloaded", callback), removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel), isElectron: true, diff --git a/flatpak/com.karmaa.termix.desktop b/flatpak/com.karmaa.termix.desktop index 3aabfd06..59d27c13 100644 --- a/flatpak/com.karmaa.termix.desktop +++ b/flatpak/com.karmaa.termix.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Name=Termix Comment=Web-based server management platform with SSH terminal, tunneling, and file editing -Exec=termix %U +Exec=run.sh %U Icon=com.karmaa.termix Terminal=false Type=Application diff --git a/flatpak/com.karmaa.termix.flatpakref b/flatpak/com.karmaa.termix.flatpakref new file mode 100644 index 00000000..7d2e9892 --- /dev/null +++ b/flatpak/com.karmaa.termix.flatpakref @@ -0,0 +1,12 @@ +[Flatpak Ref] +Name=Termix +Branch=stable +Title=Termix - SSH Server Management Platform +IsRuntime=false +Url=https://github.com/Termix-SSH/Termix/releases/download/VERSION_PLACEHOLDER/termix_linux_flatpak.flatpak +GPGKey= +RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo +Comment=Web-based server management platform with SSH terminal, tunneling, and file editing +Description=Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides SSH terminal access, tunneling capabilities, and remote file management. +Icon=https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.png +Homepage=https://github.com/Termix-SSH/Termix diff --git a/flatpak/com.karmaa.termix.metainfo.xml b/flatpak/com.karmaa.termix.metainfo.xml index 0c3c6895..335d902c 100644 --- a/flatpak/com.karmaa.termix.metainfo.xml +++ b/flatpak/com.karmaa.termix.metainfo.xml @@ -5,7 +5,7 @@

Web-based server management platform with SSH terminal, tunneling, and file editing CC0-1.0 - GPL-3.0-or-later + Apache-2.0 bugattiguy527 diff --git a/flatpak/com.karmaa.termix.yml b/flatpak/com.karmaa.termix.yml index 4405a10f..7b67c0e7 100644 --- a/flatpak/com.karmaa.termix.yml +++ b/flatpak/com.karmaa.termix.yml @@ -1,10 +1,10 @@ app-id: com.karmaa.termix runtime: org.freedesktop.Platform -runtime-version: "23.08" +runtime-version: "24.08" sdk: org.freedesktop.Sdk base: org.electronjs.Electron2.BaseApp -base-version: "23.08" -command: termix +base-version: "24.08" +command: run.sh separate-locales: false finish-args: @@ -16,8 +16,11 @@ finish-args: - --device=dri - --filesystem=home - --socket=ssh-auth - - --talk-name=org.freedesktop.Notifications + - --socket=session-bus - --talk-name=org.freedesktop.secrets + - --env=ELECTRON_TRASH=gio + - --env=XCURSOR_PATH=/run/host/user-share/icons:/run/host/share/icons + - --env=ELECTRON_OZONE_PLATFORM_HINT=auto modules: - name: termix @@ -30,6 +33,21 @@ modules: - cp -r squashfs-root/resources /app/bin/ - cp -r squashfs-root/locales /app/bin/ || true + - cp squashfs-root/*.so /app/bin/ || true + - cp squashfs-root/*.pak /app/bin/ || true + - cp squashfs-root/*.bin /app/bin/ || true + - cp squashfs-root/*.dat /app/bin/ || true + - cp squashfs-root/*.json /app/bin/ || true + + - | + cat > run.sh << 'EOF' + #!/bin/bash + export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID" + exec zypak-wrapper /app/bin/termix "$@" + EOF + - chmod +x run.sh + - install -Dm755 run.sh /app/bin/run.sh + - install -Dm644 com.karmaa.termix.desktop /app/share/applications/com.karmaa.termix.desktop - install -Dm644 com.karmaa.termix.metainfo.xml /app/share/metainfo/com.karmaa.termix.metainfo.xml @@ -40,14 +58,14 @@ modules: sources: - type: file - url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_x64_VERSION_PLACEHOLDER_appimage.AppImage + url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_x64_appimage.AppImage sha256: CHECKSUM_X64_PLACEHOLDER dest-filename: termix.AppImage only-arches: - x86_64 - type: file - url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_arm64_VERSION_PLACEHOLDER_appimage.AppImage + url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_arm64_appimage.AppImage sha256: CHECKSUM_ARM64_PLACEHOLDER dest-filename: termix.AppImage only-arches: diff --git a/flatpak/prepare-flatpak.sh b/flatpak/prepare-flatpak.sh deleted file mode 100644 index 05162b64..00000000 --- a/flatpak/prepare-flatpak.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -set -e - -VERSION="$1" -CHECKSUM="$2" -RELEASE_DATE="$3" - -if [ -z "$VERSION" ] || [ -z "$CHECKSUM" ] || [ -z "$RELEASE_DATE" ]; then - echo "Usage: $0 " - echo "Example: $0 1.8.0 abc123... 2025-10-26" - exit 1 -fi - -echo "Preparing Flatpak submission for version $VERSION" - -cp public/icon.svg flatpak/com.karmaa.termix.svg -echo "✓ Copied SVG icon" - -if command -v convert &> /dev/null; then - convert public/icon.png -resize 256x256 flatpak/icon-256.png - convert public/icon.png -resize 128x128 flatpak/icon-128.png - echo "✓ Generated PNG icons" -else - cp public/icon.png flatpak/icon-256.png - cp public/icon.png flatpak/icon-128.png - echo "⚠ ImageMagick not found, using original icon" -fi - -sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak/com.karmaa.termix.yml -sed -i "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" flatpak/com.karmaa.termix.yml -echo "✓ Updated manifest with version $VERSION" - -sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak/com.karmaa.termix.metainfo.xml -sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak/com.karmaa.termix.metainfo.xml diff --git a/index.html b/index.html index b376f7cd..83e38e8e 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,13 @@ + + + + + + + Termix