Skip to content

feat(tls): Add TLS/SNI passthrough support with metrics tracking #67

feat(tls): Add TLS/SNI passthrough support with metrics tracking

feat(tls): Add TLS/SNI passthrough support with metrics tracking #67

Workflow file for this run

name: Release
on:
push:
tags:
- "v*.*.*" # Trigger on version tags like v1.0.0, v0.1.2, etc.
permissions:
contents: write # Required for creating releases
packages: write # Required for GHCR Docker publishing
env:
CARGO_TERM_COLOR: always
RUST_VERSION: "1.90.0" # Pin Rust version for reproducible builds
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# Build webapps once and share across all platforms
build-webapps:
name: Build Web Applications
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build dashboard webapp
run: |
cd webapps/dashboard
export VITE_API_BASE_URL=""
bun install
bun run build
- name: Build exit-node-portal webapp
run: |
cd webapps/exit-node-portal
export VITE_API_BASE_URL=""
bun install
bun run build
- name: Upload webapp artifacts
uses: actions/upload-artifact@v4
with:
name: webapps
path: |
webapps/dashboard/dist
webapps/exit-node-portal/dist
retention-days: 1
# Matrix build for all platforms
build-binaries:
name: Build ${{ matrix.target }}
needs: build-webapps
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Linux AMD64
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
archive_name: linux-amd64
strip: strip
# Linux ARM64
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
archive_name: linux-arm64
strip: aarch64-linux-gnu-strip
cross: true
# macOS AMD64 (Intel) - Cross-compiled on Apple Silicon runner
- target: x86_64-apple-darwin
os: macos-14 # Apple Silicon runner (cross-compile for Intel)
archive_name: macos-amd64
strip: strip
# macOS ARM64 (Apple Silicon)
- target: aarch64-apple-darwin
os: macos-14 # Apple Silicon runner (native)
archive_name: macos-arm64
strip: strip
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract version from tag
id: get_version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
shell: bash
- name: Download webapp artifacts
uses: actions/download-artifact@v4
with:
name: webapps
path: webapps/
# Install Bun (required by localup-client build.rs)
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.target }}
# Linux ARM64 requires cross-compilation tools
- name: Install cross-compilation tools (Linux ARM64)
if: matrix.cross == true
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
# Use Swatinem/rust-cache for proper cross-platform Rust caching
# This caches ~/.cargo and target/ directories intelligently
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
# Share cargo registry/index across all targets (huge savings)
# Only target/ is per-target (necessary due to different architectures)
shared-key: "release"
cache-targets: true
cache-on-failure: true
# Cache key includes: OS, target, Cargo.lock hash, and rust version
# Automatically handles cross-platform paths (works on Windows/Linux/macOS)
# Build binaries from target directory
- name: Build binaries
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: ${{ matrix.cross == true && 'aarch64-linux-gnu-gcc' || '' }}
CC_aarch64_unknown_linux_gnu: ${{ matrix.cross == true && 'aarch64-linux-gnu-gcc' || '' }}
AR_aarch64_unknown_linux_gnu: ${{ matrix.cross == true && 'aarch64-linux-gnu-ar' || '' }}
LOCALUP_VERSION: ${{ steps.get_version.outputs.VERSION }}
run: |
cargo build --release --target ${{ matrix.target }} -p localup-cli
# Strip binaries
- name: Strip binaries
run: |
${{ matrix.strip }} target/${{ matrix.target }}/release/localup
# Create release archives (tar.gz)
- name: Create release archives
run: |
mkdir -p release
# Package localup CLI
tar -czf release/localup-${{ matrix.archive_name }}.tar.gz \
-C target/${{ matrix.target }}/release localup
# Create checksums
cd release
shasum -a 256 *.tar.gz > checksums-${{ matrix.archive_name }}.txt
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.archive_name }}-binaries
path: release/*
retention-days: 1
# Build Tauri desktop app for all platforms
build-tauri:
name: Build Desktop App (${{ matrix.platform }})
needs: build-webapps
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# macOS ARM64 (Apple Silicon)
- platform: macos-arm64
os: macos-14
target: aarch64-apple-darwin
bundle_targets: dmg
# macOS AMD64 (Intel)
- platform: macos-amd64
os: macos-14
target: x86_64-apple-darwin
bundle_targets: dmg
# Linux AMD64
- platform: linux-amd64
os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
bundle_targets: appimage,deb
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract version from tag
id: get_version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
shell: bash
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.target }}
# Linux dependencies for Tauri
- name: Install Linux dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
librsvg2-dev \
patchelf \
libssl-dev \
libgtk-3-dev \
libayatana-appindicator3-dev
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
shared-key: "tauri-${{ matrix.platform }}"
cache-targets: true
cache-on-failure: true
# Install frontend dependencies and build frontend
- name: Install frontend dependencies
working-directory: apps/localup-desktop
run: |
bun install
bun run build
# Import Apple certificate (macOS only)
- name: Import Apple certificate
if: runner.os == 'macOS'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
if [ -n "$APPLE_CERTIFICATE" ]; then
echo "Importing Apple certificate..."
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
# Decode certificate
echo -n "$APPLE_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH
# Create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Import certificate
security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
echo "Certificate imported successfully"
else
echo "No Apple certificate configured, skipping code signing"
fi
# Build Tauri app
- name: Build Tauri app
working-directory: apps/localup-desktop
env:
LOCALUP_VERSION: ${{ steps.get_version.outputs.VERSION }}
# Tauri updater signing
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
# macOS code signing - skip if not configured
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY || '-' }}
# macOS notarization
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
# Skip code signing if APPLE_SIGNING_IDENTITY is not set (use '-' to disable)
if [ "$APPLE_SIGNING_IDENTITY" = "-" ] || [ -z "$APPLE_SIGNING_IDENTITY" ]; then
echo "⚠️ No Apple signing identity configured, building without code signing"
export APPLE_SIGNING_IDENTITY="-"
fi
bun run tauri build --target ${{ matrix.target }} --bundles ${{ matrix.bundle_targets }}
# Collect and rename artifacts for upload
- name: Collect artifacts
run: |
mkdir -p release
# Find where Tauri put the bundles (workspace target or local target)
BUNDLE_DIR="target/${{ matrix.target }}/release/bundle"
if [ ! -d "$BUNDLE_DIR" ]; then
BUNDLE_DIR="apps/localup-desktop/src-tauri/target/${{ matrix.target }}/release/bundle"
fi
echo "Looking for bundles in: $BUNDLE_DIR"
ls -laR "$BUNDLE_DIR" 2>/dev/null || echo "Bundle dir not found"
# macOS DMG + signature
if ls "$BUNDLE_DIR/dmg/"*.dmg 1> /dev/null 2>&1; then
for f in "$BUNDLE_DIR/dmg/"*.dmg; do
cp "$f" "release/LocalUp-${{ matrix.platform }}.dmg"
done
fi
if ls "$BUNDLE_DIR/dmg/"*.dmg.sig 1> /dev/null 2>&1; then
for f in "$BUNDLE_DIR/dmg/"*.dmg.sig; do
cp "$f" "release/LocalUp-${{ matrix.platform }}.dmg.sig"
done
fi
# Linux AppImage + signature (tar.gz format for updater)
if ls "$BUNDLE_DIR/appimage/"*.AppImage 1> /dev/null 2>&1; then
for f in "$BUNDLE_DIR/appimage/"*.AppImage; do
cp "$f" "release/LocalUp-${{ matrix.platform }}.AppImage"
done
fi
if ls "$BUNDLE_DIR/appimage/"*.AppImage.sig 1> /dev/null 2>&1; then
for f in "$BUNDLE_DIR/appimage/"*.AppImage.sig; do
cp "$f" "release/LocalUp-${{ matrix.platform }}.AppImage.sig"
done
fi
# Also check for tar.gz (Tauri updater format)
if ls "$BUNDLE_DIR/appimage/"*.AppImage.tar.gz 1> /dev/null 2>&1; then
for f in "$BUNDLE_DIR/appimage/"*.AppImage.tar.gz; do
cp "$f" "release/LocalUp-${{ matrix.platform }}.AppImage.tar.gz"
done
fi
if ls "$BUNDLE_DIR/appimage/"*.AppImage.tar.gz.sig 1> /dev/null 2>&1; then
for f in "$BUNDLE_DIR/appimage/"*.AppImage.tar.gz.sig; do
cp "$f" "release/LocalUp-${{ matrix.platform }}.AppImage.tar.gz.sig"
done
fi
# Linux deb
if ls "$BUNDLE_DIR/deb/"*.deb 1> /dev/null 2>&1; then
for f in "$BUNDLE_DIR/deb/"*.deb; do
cp "$f" "release/LocalUp-${{ matrix.platform }}.deb"
done
fi
# Create checksums for desktop artifacts
cd release
if ls *.dmg *.AppImage *.deb 1> /dev/null 2>&1; then
shasum -a 256 *.dmg *.AppImage *.deb 2>/dev/null > checksums-desktop-${{ matrix.platform }}.txt || true
fi
ls -lah
- name: Upload Tauri artifacts
uses: actions/upload-artifact@v4
with:
name: desktop-${{ matrix.platform }}
path: release/*
retention-days: 1
# Create GitHub Release with all artifacts
# Only requires CLI binaries - desktop apps are optional
create-release:
name: Create GitHub Release
needs: [build-binaries]
if: always() && needs.build-binaries.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts/
continue-on-error: true # Desktop artifacts may not exist if build-tauri failed
- name: Consolidate release files
run: |
mkdir -p release
# CLI binaries
find artifacts/ -type f \( -name "*.tar.gz" -o -name "checksums-*.txt" \) \
-exec cp {} release/ \;
# Desktop app artifacts (DMG, AppImage, deb) and their signatures
find artifacts/ -type f \( -name "*.dmg" -o -name "*.dmg.sig" -o -name "*.AppImage" -o -name "*.AppImage.sig" -o -name "*.AppImage.tar.gz" -o -name "*.AppImage.tar.gz.sig" -o -name "*.deb" \) \
-exec cp {} release/ \;
# Create a combined checksums file
cd release
if ls checksums-*.txt 1> /dev/null 2>&1; then
cat checksums-*.txt > SHA256SUMS.txt
fi
cd ..
ls -lah release/
- name: Extract version from tag
id: get_version
run: |
VERSION=${GITHUB_REF#refs/tags/}
VERSION_NUM=${VERSION#v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "VERSION_NUM=$VERSION_NUM" >> $GITHUB_OUTPUT
# Detect if this is a pre-release (contains alpha, beta, rc, or has dash followed by non-numeric)
if [[ "$VERSION" =~ (alpha|beta|rc|-[a-zA-Z]) ]]; then
echo "IS_PRERELEASE=true" >> $GITHUB_OUTPUT
else
echo "IS_PRERELEASE=false" >> $GITHUB_OUTPUT
fi
- name: Create latest.json for auto-updater
run: |
# Only create latest.json if we have desktop artifacts
if ! ls release/*.dmg release/*.AppImage 1> /dev/null 2>&1; then
echo "No desktop artifacts found, skipping latest.json creation"
exit 0
fi
VERSION="${{ steps.get_version.outputs.VERSION_NUM }}"
RELEASE_URL="https://github.com/localup-dev/localup/releases/download/${{ steps.get_version.outputs.VERSION }}"
PUB_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Read signatures from .sig files
MACOS_ARM64_SIG=""
MACOS_AMD64_SIG=""
LINUX_AMD64_SIG=""
if [ -f "release/LocalUp-macos-arm64.dmg.sig" ]; then
MACOS_ARM64_SIG=$(cat release/LocalUp-macos-arm64.dmg.sig)
fi
if [ -f "release/LocalUp-macos-amd64.dmg.sig" ]; then
MACOS_AMD64_SIG=$(cat release/LocalUp-macos-amd64.dmg.sig)
fi
if [ -f "release/LocalUp-linux-amd64.AppImage.tar.gz.sig" ]; then
LINUX_AMD64_SIG=$(cat release/LocalUp-linux-amd64.AppImage.tar.gz.sig)
fi
# Create latest.json
cat > release/latest.json << EOF
{
"version": "${VERSION}",
"notes": "See the release notes at ${RELEASE_URL}",
"pub_date": "${PUB_DATE}",
"platforms": {
"darwin-aarch64": {
"url": "${RELEASE_URL}/LocalUp-macos-arm64.dmg",
"signature": "${MACOS_ARM64_SIG}"
},
"darwin-x86_64": {
"url": "${RELEASE_URL}/LocalUp-macos-amd64.dmg",
"signature": "${MACOS_AMD64_SIG}"
},
"linux-x86_64": {
"url": "${RELEASE_URL}/LocalUp-linux-amd64.AppImage.tar.gz",
"signature": "${LINUX_AMD64_SIG}"
}
}
}
EOF
echo "Created latest.json:"
cat release/latest.json
- name: Create Release
uses: softprops/action-gh-release@v1
with:
name: Release ${{ steps.get_version.outputs.VERSION }}
draft: false
prerelease: ${{ steps.get_version.outputs.IS_PRERELEASE }}
generate_release_notes: true
body: |
## Desktop App
Download the LocalUp desktop application for GUI-based tunnel management:
### macOS
- **Apple Silicon (M1/M2/M3)**: `LocalUp-macos-arm64.dmg`
- **Intel**: `LocalUp-macos-amd64.dmg`
> **Note for macOS users:** The app is not yet code-signed. If you see _"LocalUp is damaged and can't be opened"_, run this command after installing:
> ```bash
> xattr -cr /Applications/LocalUp.app
> ```
> Then open the app again.
### Linux
- **AppImage (Universal)**: `LocalUp-linux-amd64.AppImage`
- **Debian/Ubuntu**: `LocalUp-linux-amd64.deb`
---
## CLI Tool
### Homebrew (macOS/Linux)
**Note:** The Homebrew formula needs to be updated manually after this release.
**For Maintainers:**
```bash
# Update the formula (interactive)
./scripts/manual-formula-update.sh
# Or quick update
./scripts/quick-formula-update.sh ${{ steps.get_version.outputs.VERSION }}
```
**For Users (after formula is updated):**
```bash
${{ steps.get_version.outputs.IS_PRERELEASE == 'true' && '# BETA/PRE-RELEASE' || '# Stable release' }}
${{ steps.get_version.outputs.IS_PRERELEASE == 'true' && 'brew install https://raw.githubusercontent.com/localup-dev/localup/main/Formula/localup-beta.rb' || 'brew tap localup-dev/tap https://github.com/localup-dev/localup' }}
${{ steps.get_version.outputs.IS_PRERELEASE == 'false' && 'brew install localup' || '' }}
```
### Manual Download
Download the command-line tool for your platform:
#### Linux
- **AMD64**: `localup-linux-amd64.tar.gz`
- **ARM64**: `localup-linux-arm64.tar.gz`
#### macOS
- **Intel (AMD64)**: `localup-macos-amd64.tar.gz`
- **Apple Silicon (ARM64)**: `localup-macos-arm64.tar.gz`
---
## Verify Downloads
All archives include SHA256 checksums. Verify your download:
```bash
sha256sum -c SHA256SUMS.txt
# Or check individual file
shasum -a 256 <filename>
grep <filename> SHA256SUMS.txt
```
## Install CLI
```bash
tar -xzf localup-<platform>-<arch>.tar.gz
sudo mv localup /usr/local/bin/
chmod +x /usr/local/bin/localup
```
files: |
release/*.tar.gz
release/*.dmg
release/*.dmg.sig
release/*.AppImage
release/*.AppImage.tar.gz
release/*.AppImage.tar.gz.sig
release/*.deb
release/checksums-*.txt
release/SHA256SUMS.txt
release/latest.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Build and push Docker image to GHCR
# docker-publish:
# name: Publish Docker Image to GHCR
# needs: create-release
# runs-on: ubuntu-latest
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
# - name: Log in to Container Registry
# uses: docker/login-action@v3
# with:
# registry: ${{ env.REGISTRY }}
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
# - name: Extract metadata
# id: meta
# uses: docker/metadata-action@v5
# with:
# images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# tags: |
# type=semver,pattern={{version}}
# type=semver,pattern={{major}}.{{minor}}
# type=semver,pattern={{major}}
# type=sha,prefix=sha-
# type=raw,value=latest
# - name: Build and push Docker image
# uses: docker/build-push-action@v5
# with:
# context: .
# file: ./Dockerfile
# push: true
# tags: ${{ steps.meta.outputs.tags }}
# labels: ${{ steps.meta.outputs.labels }}
# cache-from: type=gha
# cache-to: type=gha,mode=max
# - name: Test Docker image
# run: |
# echo "Testing Docker image..."
# docker build -f Dockerfile -t localup:test .
# docker run --rm localup:test --version
# docker run --rm localup:test --help
# - name: Print image details
# run: |
# echo "## Docker Image Published to GHCR ✅"
# echo ""
# echo "### Build Metadata"
# echo "**Images:**"
# echo "${{ steps.meta.outputs.tags }}"
# echo ""
# echo "**Labels:**"
# echo "${{ steps.meta.outputs.labels }}"