diff --git a/.github/scripts/macos_smoke_suite.sh b/.github/scripts/macos_smoke_suite.sh index fa69ddf0ae..8dbb8b86a1 100755 --- a/.github/scripts/macos_smoke_suite.sh +++ b/.github/scripts/macos_smoke_suite.sh @@ -7,17 +7,113 @@ RUN_CONNECT_SMOKE="${RUN_CONNECT_SMOKE:-true}" ENABLE_IP_CHECK="${ENABLE_IP_CHECK:-false}" FORCE_FULL_TUNNEL="${FORCE_FULL_TUNNEL:-true}" EXTENSION_TIMEOUT_SECONDS="${EXTENSION_TIMEOUT_SECONDS:-120}" +APP_INSTALL_DIR="${APP_INSTALL_DIR:-${RUNNER_TEMP:-/tmp}/lantern-macos-smoke/Lantern.app}" +LANTERN_LOG_DIR="${LANTERN_LOG_DIR:-/Users/Shared/Lantern/Logs}" +DMG_MOUNT_DIR="" log_step() { - printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" + printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >&2 +} + +find_first_name() { + local root="$1" + local name="$2" + + if [[ -z "$root" || ! -e "$root" ]]; then + return 1 + fi + + find "$root" -maxdepth 4 -name "$name" -print -quit +} + +copy_app_bundle() { + local source="$1" + local destination="$2" + + log_step "Copying Lantern app from $source" + rm -rf "$destination" + mkdir -p "$(dirname "$destination")" + ditto "$source" "$destination" + xattr -dr com.apple.quarantine "$destination" 2>/dev/null || true + printf '%s\n' "$destination" +} + +extract_app_zip() { + local zip_path="$1" + local tmp_dir + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' RETURN + + log_step "Extracting Lantern app from $zip_path" + ditto -x -k "$zip_path" "$tmp_dir" + + local app_path + app_path="$(find_first_name "$tmp_dir" "Lantern.app" || true)" + if [[ -z "$app_path" ]]; then + printf 'Lantern.app not found inside %s\n' "$zip_path" >&2 + return 1 + fi + + copy_app_bundle "$app_path" "$APP_INSTALL_DIR" +} + +detach_dmg() { + if [[ -n "$DMG_MOUNT_DIR" && -d "$DMG_MOUNT_DIR" ]]; then + local mount_dir="$DMG_MOUNT_DIR" + hdiutil detach "$mount_dir" -quiet || true + rmdir "$mount_dir" 2>/dev/null || true + DMG_MOUNT_DIR="" + fi +} + +copy_app_from_dmg() { + local dmg_path="$1" + local mount_dir + mount_dir="$(mktemp -d)" + DMG_MOUNT_DIR="$mount_dir" + + log_step "Mounting Lantern DMG $dmg_path" + hdiutil attach "$dmg_path" -nobrowse -readonly -mountpoint "$mount_dir" >/dev/null + + local app_path + app_path="$(find_first_name "$mount_dir" "Lantern.app" || true)" + if [[ -z "$app_path" ]]; then + printf 'Lantern.app not found inside %s\n' "$dmg_path" >&2 + detach_dmg + return 1 + fi + + copy_app_bundle "$app_path" "$APP_INSTALL_DIR" + detach_dmg } resolve_app_path() { - if [[ -n "${APP_PATH:-}" ]]; then + if [[ -n "${APP_PATH:-}" && -x "$APP_PATH/Contents/MacOS/Lantern" ]]; then printf '%s\n' "$APP_PATH" return fi + local dmg_path + dmg_path="$(find_first_name "${DMG_ARTIFACT_DIR:-}" "*.dmg" || true)" + if [[ -n "$dmg_path" ]]; then + copy_app_from_dmg "$dmg_path" + return + fi + + local app_zip + app_zip="$(find_first_name "${APP_ARTIFACT_DIR:-}" "Lantern.app.zip" || true)" + if [[ -n "$app_zip" ]]; then + extract_app_zip "$app_zip" + return + fi + + local app_artifact + app_artifact="$(find_first_name "${APP_ARTIFACT_DIR:-}" "Lantern.app" || true)" + if [[ -n "$app_artifact" ]]; then + copy_app_bundle "$app_artifact" "$APP_INSTALL_DIR" + return + fi + local candidates=( "build/macos/Build/Products/Release/Lantern.app" "build/macos/Build/Products/Debug/Lantern.app" @@ -31,7 +127,8 @@ resolve_app_path() { fi done - printf '%s\n' "${candidates[0]}" + printf 'Lantern.app was not found. Set APP_PATH, or provide APP_ARTIFACT_DIR/DMG_ARTIFACT_DIR.\n' >&2 + return 1 } capture_command() { @@ -46,14 +143,20 @@ capture_lantern_logs() { local output_dir="$ARTIFACT_DIR/lantern-logs" mkdir -p "$output_dir" - if [[ -d "/Users/Shared/Lantern/Logs" ]]; then - cp -R "/Users/Shared/Lantern/Logs/." "$output_dir/" 2>/dev/null || true + if [[ -d "$LANTERN_LOG_DIR" ]]; then + cp -R "$LANTERN_LOG_DIR/." "$output_dir/" 2>/dev/null || true else - printf 'No Lantern log directory found at /Users/Shared/Lantern/Logs\n' \ + printf 'No Lantern log directory found at %s\n' "$LANTERN_LOG_DIR" \ >"$output_dir/missing.txt" fi } +reset_lantern_logs() { + log_step "Resetting Lantern logs at $LANTERN_LOG_DIR" + rm -rf "$LANTERN_LOG_DIR" + mkdir -p "$LANTERN_LOG_DIR" +} + capture_unified_logs() { log_step "Capturing unified logs" log show \ @@ -177,6 +280,7 @@ on_exit() { if [[ "$status" -ne 0 ]]; then capture_diagnostics "failure" fi + detach_dmg exit "$status" } @@ -184,6 +288,7 @@ on_exit() { trap on_exit EXIT mkdir -p "$ARTIFACT_DIR" +reset_lantern_logs capture_command "systemextensionsctl-list-initial" systemextensionsctl list app_path="$(resolve_app_path)" diff --git a/.github/workflows/app-smoke-tests.yml b/.github/workflows/app-smoke-tests.yml index 65918564ae..42c1c2c0b0 100644 --- a/.github/workflows/app-smoke-tests.yml +++ b/.github/workflows/app-smoke-tests.yml @@ -28,7 +28,7 @@ on: type: boolean default: true macos_connect_smoke: - description: "Include macOS connect/disconnect smoke when platforms=all (requires approved system extension)" + description: "Include macOS connect/disconnect smoke when platforms=all (requires self-hosted runner with approved system extension)" required: false type: boolean default: false @@ -113,7 +113,6 @@ jobs: macos: needs: prepare - # macOS smoke stays opt-in for "all" until runners have system-extension approval. if: ${{ inputs.platforms == 'macos' || (inputs.platforms == 'all' && inputs.macos_connect_smoke) }} uses: ./.github/workflows/build-macos.yml secrets: @@ -128,9 +127,18 @@ jobs: version: ${{ needs.prepare.outputs.version }} build_type: nightly installer_base_name: ${{ needs.prepare.outputs.installer_base_name }} + + macos-connect-smoke: + needs: [prepare, macos] + if: ${{ inputs.platforms == 'macos' || (inputs.platforms == 'all' && inputs.macos_connect_smoke) }} + uses: ./.github/workflows/macos-connect-smoke.yml + secrets: + APP_ENV: ${{ secrets.APP_ENV }} + with: + version: ${{ needs.prepare.outputs.version }} + build_type: nightly enable_ip_check: ${{ inputs.enable_ip_check }} force_full_tunnel_smoke: ${{ inputs.force_full_tunnel_smoke }} - run_connect_smoke: true windows: needs: prepare diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index d852522c46..725b1f9c30 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -27,22 +27,6 @@ on: installer_base_name: required: true type: string - run_connect_smoke: - description: "Run macOS connect/disconnect smoke test" - required: false - type: boolean - default: false - enable_ip_check: - description: "Enable public IP change validation in macOS connect smoke" - required: false - type: boolean - default: false - force_full_tunnel_smoke: - description: "Force full tunnel routing mode in macOS connect smoke" - required: false - type: boolean - default: true - jobs: build-macos: permissions: @@ -180,25 +164,22 @@ jobs: BUCKET: ${{ vars.S3_RELEASES_BUCKET }} run: bash ./.github/scripts/check_macos_sysext_build.sh - - name: macOS connect smoke suite - if: ${{ inputs.run_connect_smoke }} + - name: Package macOS app artifact shell: bash - timeout-minutes: 20 - env: - APP_PATH: build/macos/Build/Products/Release/Lantern.app - ARTIFACT_DIR: ${{ runner.temp }}/macos-smoke-artifacts - ENABLE_IP_CHECK: ${{ inputs.enable_ip_check }} - FORCE_FULL_TUNNEL: ${{ inputs.force_full_tunnel_smoke }} - RUN_CONNECT_SMOKE: true - run: ./.github/scripts/macos_smoke_suite.sh - - - name: Upload macOS smoke diagnostics - if: ${{ always() && inputs.run_connect_smoke }} + run: | + APP_PATH="build/macos/Build/Products/Release/Lantern.app" + if [[ ! -d "$APP_PATH" ]]; then + printf 'Lantern.app not found at %s\n' "$APP_PATH" >&2 + exit 1 + fi + + ditto -c -k --keepParent "$APP_PATH" "$RUNNER_TEMP/Lantern.app.zip" + + - name: Upload macOS app uses: actions/upload-artifact@v4 with: - name: macos-smoke-diagnostics - path: ${{ runner.temp }}/macos-smoke-artifacts - if-no-files-found: ignore + name: lantern-macos-app + path: ${{ runner.temp }}/Lantern.app.zip retention-days: 2 - name: Upload macOS installer diff --git a/.github/workflows/macos-connect-smoke.yml b/.github/workflows/macos-connect-smoke.yml new file mode 100644 index 0000000000..38f4f4aa03 --- /dev/null +++ b/.github/workflows/macos-connect-smoke.yml @@ -0,0 +1,148 @@ +name: macOS Connect Smoke + +on: + workflow_call: + inputs: + version: + required: true + type: string + build_type: + required: true + type: string + app_artifact_name: + required: false + type: string + default: lantern-macos-app + dmg_artifact_name: + required: false + type: string + default: lantern-installer-dmg + pubspec_artifact_name: + required: false + type: string + default: pubspec + enable_ip_check: + description: "Validate public IP change during macOS connect smoke" + required: false + type: boolean + default: false + force_full_tunnel_smoke: + description: "Force full tunnel routing mode during macOS connect smoke" + required: false + type: boolean + default: true + extension_timeout_seconds: + description: "Seconds to wait for system extension activation" + required: false + type: number + default: 120 + secrets: + APP_ENV: + required: true + +jobs: + macos-connect-smoke: + permissions: + contents: read + runs-on: [self-hosted, macOS, ARM64, lantern-macos-smoke] + timeout-minutes: 40 + env: + ARTIFACT_DIR: ${{ runner.temp }}/macos-smoke-artifacts + APP_ARTIFACT_DIR: ${{ runner.temp }}/lantern-macos-app + DMG_ARTIFACT_DIR: ${{ runner.temp }}/lantern-installer-dmg + BUILD_TYPE: ${{ inputs.build_type }} + VERSION: ${{ inputs.version }} + ENABLE_IP_CHECK: ${{ inputs.enable_ip_check }} + FORCE_FULL_TUNNEL: ${{ inputs.force_full_tunnel_smoke }} + EXTENSION_TIMEOUT_SECONDS: ${{ inputs.extension_timeout_seconds }} + RUN_CONNECT_SMOKE: true + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download pubspec.yaml + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.pubspec_artifact_name }} + + - name: Download macOS app artifact + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.app_artifact_name }} + path: ${{ env.APP_ARTIFACT_DIR }} + + - name: Download macOS DMG artifact + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.dmg_artifact_name }} + path: ${{ env.DMG_ARTIFACT_DIR }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + cache: true + + - name: Cache Flutter dependencies + uses: actions/cache@v4 + timeout-minutes: 5 + continue-on-error: true + with: + path: | + ~/.pub-cache + key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-flutter- + + - name: Install Flutter + uses: subosito/flutter-action@v2.22.0 + with: + channel: stable + flutter-version-file: .github/flutter-version.yaml + + - name: Decode APP_ENV + uses: timheuer/base64-to-file@v1.2 + with: + fileName: "app.env" + fileDir: ${{ github.workspace }} + encodedString: ${{ secrets.APP_ENV }} + + - name: Set gomobile cache dir + shell: bash + run: | + set -euo pipefail + echo "GOMOBILECACHE=$HOME/.cache/gomobile" >> "$GITHUB_ENV" + mkdir -p "$HOME/.cache/gomobile" + + - name: Cache gomobile + uses: actions/cache@v4 + timeout-minutes: 10 + continue-on-error: true + with: + path: ~/.cache/gomobile + key: ${{ runner.os }}-gomobile-${{ hashFiles('**/go.mod', '**/go.sum') }} + restore-keys: | + ${{ runner.os }}-gomobile- + + - name: Prepare macOS test project + shell: bash + run: | + set -euo pipefail + flutter --version + flutter config --enable-macos-desktop + make install-gomobile + make macos pubget gen + + - name: macOS connect smoke suite + timeout-minutes: 30 + shell: bash + run: ./.github/scripts/macos_smoke_suite.sh + + - name: Upload macOS smoke diagnostics + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: macos-smoke-diagnostics + path: ${{ env.ARTIFACT_DIR }} + if-no-files-found: ignore + retention-days: 2