From 1a390e580d9ac44ea3b5f9e77050dfb5f35a154e Mon Sep 17 00:00:00 2001 From: Abolfazl Date: Wed, 13 May 2026 01:12:29 +0330 Subject: [PATCH] feat: Add Docker release workflow and enhance release process with Termux support --- .github/workflows/docker-release.yml | 78 +++++++ .github/workflows/release.yml | 336 ++++++++++++++------------- 2 files changed, 249 insertions(+), 165 deletions(-) create mode 100644 .github/workflows/docker-release.yml diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..32fbe99 --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,78 @@ +name: Docker Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + publish: + description: "Push image to GHCR" + required: false + default: false + type: boolean + image_name: + description: "GHCR image name (without ghcr.io/), default: owner/masterhttprelayvpn" + required: false + type: string + +permissions: + contents: read + packages: write + +jobs: + docker-multiarch: + name: Build Docker amd64+arm64 + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true') + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Resolve image and tags + id: meta + shell: bash + run: | + set -euo pipefail + owner=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + if [ -n "${{ github.event.inputs.image_name || '' }}" ]; then + image="ghcr.io/${{ github.event.inputs.image_name }}" + else + image="ghcr.io/${owner}/masterhttprelayvpn" + fi + + if [ "${{ github.event_name }}" = "push" ]; then + tag="${{ github.ref_name }}" + else + tag="manual-${{ github.run_number }}" + fi + + { + echo "image=$image" + echo "tag=$tag" + } >> "$GITHUB_OUTPUT" + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.tag }} + ${{ steps.meta.outputs.image }}:latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba66714..c49f7d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,33 +7,23 @@ on: workflow_dispatch: inputs: release_tag: - description: "Tag name to publish (example: v1.2.0). Leave empty for non-publishing build." + description: "Tag name to publish (example: v1.2.0). Leave empty for build-only run." required: false type: string publish: - description: "Publish GitHub Release" + description: "Publish GitHub release" required: false default: false type: boolean make_public: - description: "Make release public immediately (false = hidden draft + prerelease)" + description: "Make release public immediately (false = draft + prerelease)" required: false default: false type: boolean - build_macos_x64: - description: "Also build macOS x64 (intel). Optional and non-blocking." + include_termux: + description: "Publish Termux ARM64 and ARMv7 bundles" required: false - default: false - type: boolean - build_linux_arm64: - description: "Also build Linux ARM64. Optional and non-blocking." - required: false - default: false - type: boolean - build_termux_bundle: - description: "Also publish Termux source bundle (supports arm64/armv7/x86_64 on-device)." - required: false - default: false + default: true type: boolean permissions: @@ -41,18 +31,33 @@ permissions: jobs: build-binaries: - name: Build ${{ matrix.target }} + name: Build ${{ matrix.platform }}-${{ matrix.arch }} runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.optional }} strategy: fail-fast: false matrix: include: - os: windows-latest - target: windows-x64 + platform: windows + arch: amd64 + optional: false - os: ubuntu-latest - target: linux-x64 + platform: linux + arch: amd64 + optional: false + - os: macos-13 + platform: macos + arch: amd64 + optional: false - os: macos-14 - target: macos-arm64 + platform: macos + arch: arm64 + optional: false + - os: ubuntu-24.04-arm + platform: linux + arch: arm64 + optional: true steps: - name: Checkout @@ -71,168 +76,159 @@ jobs: - name: Build standalone binary run: pyinstaller --noconfirm --clean --onefile --name MasterHttpRelayVPN --paths src main.py - - name: Package release bundle - env: - TARGET: ${{ matrix.target }} - run: python scripts/build_release_bundle.py - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: release-${{ matrix.target }} - path: release-assets/* - - build-macos-x64: - name: Build macos-x64 (optional) - if: github.event_name == 'workflow_dispatch' && github.event.inputs.build_macos_x64 == 'true' - runs-on: macos-13 - continue-on-error: true - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install build dependencies + - name: Smoke test binary + shell: bash run: | - python -m pip install --upgrade pip - python -m pip install -r requirements.txt pyinstaller + set -euo pipefail + if [ "${{ runner.os }}" = "Windows" ]; then + BIN="dist/MasterHttpRelayVPN.exe" + else + BIN="dist/MasterHttpRelayVPN" + chmod +x "$BIN" + fi + "$BIN" --version + "$BIN" --help >/dev/null - - name: Build standalone binary - run: pyinstaller --noconfirm --clean --onefile --name MasterHttpRelayVPN --paths src main.py - - - name: Package release bundle - env: - TARGET: macos-x64 - run: python scripts/build_release_bundle.py - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: release-macos-x64 - path: release-assets/* - - build-linux-arm64: - name: Build linux-arm64 (optional) - if: github.event_name == 'workflow_dispatch' && github.event.inputs.build_linux_arm64 == 'true' - runs-on: ubuntu-24.04-arm - continue-on-error: true - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install build dependencies + - name: Package release artifact (Windows) + if: runner.os == 'Windows' + shell: pwsh run: | - python -m pip install --upgrade pip - python -m pip install -r requirements.txt pyinstaller + $ErrorActionPreference = "Stop" + $version = ((Get-Content "src/core/constants.py" -Raw) -match '__version__\s*=\s*"([^"]+)"') > $null; $Matches[1] + if (-not $version) { throw "Could not read version" } + New-Item -ItemType Directory -Path "release-assets" -Force | Out-Null + $staging = "release-staging" + Remove-Item -Recurse -Force $staging -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Path $staging -Force | Out-Null + Copy-Item "dist/MasterHttpRelayVPN.exe" "$staging/MasterHttpRelayVPN.exe" + foreach ($f in @("README.md", "README_FA.md", "config.example.json", "start.bat", "start.sh")) { + if (Test-Path $f) { Copy-Item $f $staging } + } + $archive = "MasterHttpRelayVPN-$version-${{ matrix.platform }}-${{ matrix.arch }}.zip" + Compress-Archive -Path "$staging/*" -DestinationPath "release-assets/$archive" -Force + $hash = (Get-FileHash "release-assets/$archive" -Algorithm SHA256).Hash.ToLower() + "$hash $archive" | Out-File -FilePath "release-assets/$archive.sha256" -Encoding ascii - - name: Build standalone binary - run: pyinstaller --noconfirm --clean --onefile --name MasterHttpRelayVPN --paths src main.py - - - name: Package release bundle - env: - TARGET: linux-arm64 - run: python scripts/build_release_bundle.py - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: release-linux-arm64 - path: release-assets/* - - build-termux-bundle: - name: Build termux-source (optional) - if: github.event_name == 'workflow_dispatch' && github.event.inputs.build_termux_bundle == 'true' - runs-on: ubuntu-latest - continue-on-error: true - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Create Termux source archive + - name: Package release artifact (non-Windows) + if: runner.os != 'Windows' + shell: bash run: | - python - <<'PY' - import hashlib + set -euo pipefail + version=$(python - <<'PY' import re - import tarfile from pathlib import Path - - root = Path('.').resolve() - constants_py = (root / 'src' / 'constants.py').read_text(encoding='utf-8') - m = re.search(r'__version__\s*=\s*"([^"]+)"', constants_py) - version = m.group(1) if m else '0.0.0' - - release_dir = root / 'release-assets' - release_dir.mkdir(parents=True, exist_ok=True) - - archive_name = f"MasterHttpRelayVPN-{version}-termux-source.tar.gz" - archive_path = release_dir / archive_name - - include_items = [ - 'main.py', - 'setup.py', - 'requirements.txt', - 'config.example.json', - 'README.md', - 'README_FA.md', - 'start.sh', - 'apps_script', - 'scripts', - 'src', - ] - - with tarfile.open(archive_path, 'w:gz') as tf: - for item in include_items: - p = root / item - if p.exists(): - tf.add(p, arcname=item) - - digest = hashlib.sha256(archive_path.read_bytes()).hexdigest() - (release_dir / f"{archive_name}.sha256").write_text( - f"{digest} {archive_name}\n", - encoding='utf-8', - ) - - install_script = release_dir / 'termux-install.sh' - install_script.write_text( - "#!/data/data/com.termux/files/usr/bin/bash\n" - "set -euo pipefail\n" - "pkg update -y\n" - "pkg install -y python git clang libffi openssl\n" - "python -m pip install --upgrade pip\n" - "if [ ! -f requirements.txt ]; then\n" - " echo 'Run this inside extracted MasterHttpRelayVPN source directory.' >&2\n" - " exit 1\n" - "fi\n" - "pip install -r requirements.txt\n" - "echo 'Done. Next: python setup.py and then python main.py'\n", - encoding='utf-8', - ) - install_script.chmod(0o755) - print(f"Created {archive_path}") + t = Path('src/core/constants.py').read_text(encoding='utf-8') + m = re.search(r'__version__\s*=\s*"([^"]+)"', t) + print(m.group(1) if m else '0.0.0') PY + ) + mkdir -p release-assets release-staging + rm -rf release-staging/* + cp dist/MasterHttpRelayVPN release-staging/MasterHttpRelayVPN + chmod +x release-staging/MasterHttpRelayVPN + for f in README.md README_FA.md config.example.json start.sh start.bat; do + [ -f "$f" ] && cp "$f" release-staging/ + done + archive="MasterHttpRelayVPN-${version}-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz" + tar -C release-staging -czf "release-assets/$archive" . + sha256sum "release-assets/$archive" | awk '{print $1 " " FILENAME}' FILENAME="$archive" > "release-assets/$archive.sha256" - name: Upload build artifacts uses: actions/upload-artifact@v4 with: - name: release-termux-source + name: release-${{ matrix.platform }}-${{ matrix.arch }} + path: release-assets/* + + build-termux: + name: Build termux-arm64-armv7 + runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.inputs.include_termux == 'true' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build Termux binaries (arm64 + armv7) + shell: bash + run: | + set -euo pipefail + + version=$(python - <<'PY' + import re + from pathlib import Path + t = Path('src/core/constants.py').read_text(encoding='utf-8') + m = re.search(r'__version__\s*=\s*"([^"]+)"', t) + print(m.group(1) if m else '0.0.0') + PY + ) + + mkdir -p release-assets termux-dist + + build_termux_arch () { + arch="$1" + platform="$2" + image="$3" + + rm -rf dist build *.spec || true + + docker run --rm --platform "$platform" \ + -v "$PWD:/work" \ + -w /work \ + "$image" \ + bash -lc ' + set -euo pipefail + pkg update -y + pkg install -y python clang make pkg-config libffi openssl rust binutils + python -m pip install --upgrade pip + pip install pyinstaller + pip install -r requirements.txt + pyinstaller --noconfirm --clean --onefile --name MasterHttpRelayVPN --paths src main.py + ' + + if [ ! -f dist/MasterHttpRelayVPN ]; then + echo "Missing Termux binary output for ${arch}" >&2 + exit 1 + fi + cp dist/MasterHttpRelayVPN "termux-dist/MasterHttpRelayVPN-${arch}" + chmod +x "termux-dist/MasterHttpRelayVPN-${arch}" + } + + build_termux_arch "arm64" "linux/arm64" "termux/termux-docker:aarch64" + build_termux_arch "armv7" "linux/arm/v7" "termux/termux-docker:arm" + + for arch in arm64 armv7; do + staging="termux-staging-${arch}" + rm -rf "$staging" + mkdir -p "$staging" + cp "termux-dist/MasterHttpRelayVPN-${arch}" "$staging/MasterHttpRelayVPN" + [ -f config.example.json ] && cp config.example.json "$staging/" + [ -f README.md ] && cp README.md "$staging/" + [ -f README_FA.md ] && cp README_FA.md "$staging/" + + printf '%s\n' \ + '#!/data/data/com.termux/files/usr/bin/bash' \ + 'set -euo pipefail' \ + 'chmod +x ./MasterHttpRelayVPN' \ + 'exec ./MasterHttpRelayVPN "$@"' \ + > "$staging/termux-run.sh" + chmod +x "$staging/termux-run.sh" + + archive="MasterHttpRelayVPN-${version}-termux-${arch}.zip" + (cd "$staging" && zip -qr "../release-assets/${archive}" .) + + sha256sum "release-assets/${archive}" | awk '{print $1 " " FILENAME}' FILENAME="$archive" > "release-assets/${archive}.sha256" + done + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: release-termux path: release-assets/* publish-release: name: Publish GitHub Release - needs: [build-binaries, build-macos-x64, build-linux-arm64, build-termux-bundle] runs-on: ubuntu-latest + needs: [build-binaries, build-termux] if: always() && !cancelled() && (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true' && startsWith(github.event.inputs.release_tag, 'v'))) steps: @@ -247,6 +243,16 @@ jobs: find release-assets -type f -exec cp {} final-assets/ \; ls -lah final-assets + - name: Fail if no artifacts + run: | + set -euo pipefail + count=$(find final-assets -type f | wc -l) + echo "Artifact count: ${count}" + if [ "${count}" -eq 0 ]; then + echo "No artifacts were produced." + exit 1 + fi + - name: Create GitHub release uses: softprops/action-gh-release@v2 with: