arblant
firefox-vpn-client / .github / workflows / build.yml
build.yml390 lines · 15.7 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
# Tests every push and pull request. A tag "v<version>" (the version in
# app/pubspec.yaml) also builds Windows, macOS, Android, Linux, and the Linux
# core, and publishes them as a GitHub release, each file twice: under a
# fixed name, which the README's releases/latest/download links fetch, and
# with the version in its name (Firefox-VPN-1.2.3-Android-arm64-v8a.apk),
# which the release's own notes and the app's Download button use, so a
# downloaded file says which version it is. "Run workflow" builds without
# publishing.
#
# Secrets for the Android release key (required for a tag):
#   ANDROID_KEYSTORE_BASE64    the .jks file, base64-encoded
#   ANDROID_KEYSTORE_PASSWORD  its password (PKCS12: also the key's)
name: Build

on:
  push:
    branches: [main]
    tags: ['v*']
    # A change to the documentation alone (READMEs, docs/, screenshots)
    # runs nothing; GitHub does not apply this filter to tags, so a release
    # always builds.
    paths-ignore: ['**.md', 'docs/**', 'LICENSE']
  pull_request:
    paths-ignore: ['**.md', 'docs/**', 'LICENSE']
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: build-${{ github.ref }}
  cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }}

env:
  FLUTTER_VERSION: 3.47.7
  GOMOBILE_VERSION: v0.0.0-20260908204917-8b95e45f8d3e

jobs:
  test:
    runs-on: ubuntu-24.04
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - uses: actions/checkout@v7
      - id: version
        name: Version
        run: |
          v="$(sed -n 's/^version:[[:space:]]*\([0-9]*\.[0-9]*\.[0-9]*\).*/\1/p' app/pubspec.yaml)"
          echo "version=$v" >> "$GITHUB_OUTPUT"
          if [[ "$GITHUB_REF" == refs/tags/* && "$GITHUB_REF_NAME" != "v$v" ]]; then
            echo "::error::tag $GITHUB_REF_NAME does not match app/pubspec.yaml ($v)"
            exit 1
          fi
      - uses: actions/setup-go@v7
        with:
          go-version-file: core/go.mod
          cache-dependency-path: core/go.sum
      - name: Core
        working-directory: core
        run: |
          go vet ./...
          go test ./...
          # The TUN stack's tests need sing-tun's gVisor stack.
          go vet -tags with_gvisor ./internal/tunstack/
          go test -tags with_gvisor ./internal/tunstack/
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: ${{ env.FLUTTER_VERSION }}
          channel: stable
          cache: true
      - name: App
        working-directory: app
        run: |
          flutter pub get
          flutter analyze
          flutter test

  windows:
    needs: test
    # Built unless the repository variable PLATFORMS (Settings, Secrets and
    # variables, Actions, Variables) is set and leaves it out, e.g. "linux".
    if: (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && (vars.PLATFORMS == '' || contains(vars.PLATFORMS, 'windows'))
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-go@v7
        with:
          go-version-file: core/go.mod
          cache-dependency-path: core/go.sum
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: ${{ env.FLUTTER_VERSION }}
          channel: stable
          cache: true
      - name: Inno Setup
        run: choco upgrade innosetup -y --no-progress
      - name: Build
        shell: pwsh
        env:
          # The key updates are signed with (scripts/signing-key.ps1): the
          # core and the TUN service take only updates it signed; without
          # it they take none.
          FIREFOX_VPN_SIGNING_KEY: ${{ secrets.FIREFOX_VPN_SIGNING_KEY }}
        run: ./scripts/build-windows.ps1
      - name: Release names
        shell: pwsh
        run: |
          $v = '${{ needs.test.outputs.version }}'
          New-Item -ItemType Directory out | Out-Null
          Move-Item "dist/Firefox-VPN-$v-Windows-x64.zip" out/Firefox-VPN-Windows-x64.zip
          Move-Item "dist/Firefox-VPN-$v-Windows-Setup-x64.exe" out/Firefox-VPN-Windows-Setup-x64.exe
      - uses: actions/upload-artifact@v7
        with:
          name: windows
          path: out/*
          if-no-files-found: error

  android:
    needs: test
    if: (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && (vars.PLATFORMS == '' || contains(vars.PLATFORMS, 'android'))
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-go@v7
        with:
          go-version-file: core/go.mod
          cache-dependency-path: core/go.sum
      - uses: actions/setup-java@v6
        with:
          distribution: temurin
          java-version: 21
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: ${{ env.FLUTTER_VERSION }}
          channel: stable
          cache: true
      - name: NDK and gomobile
        run: |
          ndk="$(sed -n 's/.*ndkVersion = "\(.*\)".*/\1/p' app/android/app/build.gradle.kts)"
          yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;$ndk" > /dev/null
          echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/$ndk" >> "$GITHUB_ENV"
          go install "golang.org/x/mobile/cmd/gomobile@$GOMOBILE_VERSION" "golang.org/x/mobile/cmd/gobind@$GOMOBILE_VERSION"
      - name: Release key
        env:
          KEYSTORE: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
          PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
        run: |
          if [ -z "$KEYSTORE" ]; then
            if [[ "$GITHUB_REF" == refs/tags/* ]]; then
              echo "::error::a release needs the ANDROID_KEYSTORE_BASE64 and ANDROID_KEYSTORE_PASSWORD secrets"
              exit 1
            fi
            echo "::warning::no release key: the APKs are signed with a debug key"
            exit 0
          fi
          printf '%s' "$KEYSTORE" | base64 -d > app/android/release.jks
          printf 'storeFile=../release.jks\nstorePassword=%s\nkeyAlias=release\nkeyPassword=%s\n' "$PASSWORD" "$PASSWORD" > app/android/key.properties
      - name: Build
        run: |
          v='${{ needs.test.outputs.version }}'
          mkdir -p app/android/app/libs
          (cd core && gomobile bind -target=android -androidapi 24 -trimpath \
            -ldflags "-s -w -X github.com/armanlife/firefox-vpn-client/core/internal/version.Current=$v" \
            -o ../app/android/app/libs/core.aar ./mobile)
          cd app
          flutter pub get
          mkdir -p ../out
          flutter build apk --release --split-per-abi --dart-define=APP_VERSION=$v
          for abi in arm64-v8a armeabi-v7a x86_64; do
            cp "build/app/outputs/flutter-apk/app-$abi-release.apk" "../out/Firefox-VPN-Android-$abi.apk"
          done
          flutter build apk --release --dart-define=APP_VERSION=$v
          cp build/app/outputs/flutter-apk/app-release.apk ../out/Firefox-VPN-Android-universal.apk
      - name: Remove the key
        if: always()
        run: rm -f app/android/release.jks app/android/key.properties
      - uses: actions/upload-artifact@v7
        with:
          name: android
          path: out/*
          if-no-files-found: error

  linux:
    needs: test
    if: (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && (vars.PLATFORMS == '' || contains(vars.PLATFORMS, 'linux'))
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-go@v7
        with:
          go-version-file: core/go.mod
          cache-dependency-path: core/go.sum
      - name: Build
        run: bash scripts/build-linux-core.sh
      - uses: actions/upload-artifact@v7
        with:
          name: linux
          path: dist/firefox-vpn-core-linux-*.tar.gz
          if-no-files-found: error

  linux-desktop:
    needs: test
    if: (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && (vars.PLATFORMS == '' || contains(vars.PLATFORMS, 'linux'))
    strategy:
      matrix:
        include:
          - arch: amd64
            runner: ubuntu-22.04
          - arch: arm64
            runner: ubuntu-22.04-arm
    # The oldest supported runners: the app runs where their glibc and GTK do.
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-go@v7
        with:
          go-version-file: core/go.mod
          cache-dependency-path: core/go.sum
      - uses: subosito/flutter-action@v2
        if: matrix.arch == 'amd64'
        with:
          flutter-version: ${{ env.FLUTTER_VERSION }}
          channel: stable
          cache: true
      # Flutter publishes SDK archives for x64 Linux only; on ARM it runs
      # from its Git tag and fetches its ARM engine itself.
      - name: Flutter (ARM)
        if: matrix.arch == 'arm64'
        run: |
          git clone --depth 1 --branch "$FLUTTER_VERSION" https://github.com/flutter/flutter.git "$HOME/flutter"
          echo "$HOME/flutter/bin" >> "$GITHUB_PATH"
          "$HOME/flutter/bin/flutter" --version
      - name: Build tools
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y -qq clang cmake ninja-build pkg-config libgtk-3-dev
      # A real TUN, as root, for a minute (internal/tunservice/e2e_test.go):
      # this computer is CI's.
      - name: TUN end to end
        working-directory: core
        run: |
          go test -c -tags 'e2e with_gvisor' -o "$RUNNER_TEMP/e2e.test" ./internal/tunservice
          sudo "$RUNNER_TEMP/e2e.test" -test.run TestEndToEnd -test.v
      # The service installed with systemd for this user, who then removes
      # it through the service, without a prompt.
      - name: TUN service with systemd
        working-directory: core
        run: |
          go build -tags with_gvisor -o "$RUNNER_TEMP/firefox-vpn-tun" ./cmd/firefox-vpn-tun
          sudo "$RUNNER_TEMP/firefox-vpn-tun" install
          systemctl is-active firefox-vpn-tun
          "$RUNNER_TEMP/firefox-vpn-tun" uninstall
          for i in $(seq 20); do systemctl is-active --quiet firefox-vpn-tun || break; sleep 0.5; done
          if systemctl is-active --quiet firefox-vpn-tun; then echo "the service still runs"; exit 1; fi
          test ! -e /etc/systemd/system/firefox-vpn-tun.service
          test ! -e /opt/firefox-vpn-tun
      - name: Build
        env:
          # The key the TUN service's updates are signed with; without it
          # the service takes none.
          FIREFOX_VPN_SIGNING_KEY: ${{ secrets.FIREFOX_VPN_SIGNING_KEY }}
        run: bash scripts/build-linux.sh
      - name: Release name
        run: |
          mkdir out
          mv "dist/Firefox-VPN-${{ needs.test.outputs.version }}-Linux-${{ matrix.arch }}.tar.gz" "out/Firefox-VPN-Linux-${{ matrix.arch }}.tar.gz"
      - uses: actions/upload-artifact@v7
        with:
          name: linux-desktop-${{ matrix.arch }}
          path: out/*
          if-no-files-found: error

  macos:
    needs: test
    if: (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && (vars.PLATFORMS == '' || contains(vars.PLATFORMS, 'macos'))
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-go@v7
        with:
          go-version-file: core/go.mod
          cache-dependency-path: core/go.sum
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: ${{ env.FLUTTER_VERSION }}
          channel: stable
          cache: true
      # What runs only on macOS: the system proxy, the TUN service and its
      # socket, DNS settings, and the core's binding outside the TUN.
      - name: Core tests on macOS
        working-directory: core
        run: |
          go vet ./...
          go test ./internal/desktop ./internal/config ./internal/egress ./internal/netstate ./internal/tunapi ./internal/tunservice
          go test -tags with_gvisor ./internal/tunstack/
      # A real TUN, as root, for a minute (internal/tunservice/e2e_test.go):
      # this computer is CI's. Not yet run on a Mac but here, so a failure
      # shows without holding the release back.
      - name: TUN end to end
        continue-on-error: true
        working-directory: core
        run: |
          go test -c -tags 'e2e with_gvisor' -o "$RUNNER_TEMP/e2e.test" ./internal/tunservice
          sudo "$RUNNER_TEMP/e2e.test" -test.run TestEndToEnd -test.v
      # The service installed with launchd for this user, who then removes
      # it through the service, without a prompt.
      - name: TUN service with launchd
        continue-on-error: true
        working-directory: core
        run: |
          go build -tags with_gvisor -o "$RUNNER_TEMP/firefox-vpn-tun" ./cmd/firefox-vpn-tun
          sudo "$RUNNER_TEMP/firefox-vpn-tun" install
          sudo launchctl print system/com.arman.firefoxvpn.tun | head -20
          "$RUNNER_TEMP/firefox-vpn-tun" uninstall
          for i in $(seq 20); do sudo launchctl print system/com.arman.firefoxvpn.tun >/dev/null 2>&1 || break; sleep 0.5; done
          if sudo launchctl print system/com.arman.firefoxvpn.tun >/dev/null 2>&1; then echo "the job is still loaded"; exit 1; fi
          test ! -e /Library/LaunchDaemons/com.arman.firefoxvpn.tun.plist
          test ! -e "/Library/Application Support/Firefox VPN TUN"
      - name: Build
        env:
          # The key the TUN service's updates are signed with; without it
          # the service takes none.
          FIREFOX_VPN_SIGNING_KEY: ${{ secrets.FIREFOX_VPN_SIGNING_KEY }}
        run: bash scripts/build-macos.sh
      - name: Release name
        run: |
          mkdir out
          mv "dist/Firefox-VPN-${{ needs.test.outputs.version }}-macOS.dmg" out/Firefox-VPN-macOS.dmg
      - uses: actions/upload-artifact@v7
        with:
          name: macos
          path: out/*
          if-no-files-found: error

  release:
    needs: [test, windows, android, linux, linux-desktop, macos]
    # A platform left out by PLATFORMS is skipped, not failed: the release
    # goes on with the rest.
    if: startsWith(github.ref, 'refs/tags/v') && !failure() && !cancelled()
    runs-on: ubuntu-24.04
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-go@v7
        with:
          go-version-file: core/go.mod
          cache-dependency-path: core/go.sum
      - uses: actions/download-artifact@v8
        with:
          path: release
          merge-multiple: true
      # A copy of each file with the version after the program's name.
      - name: Versioned names
        working-directory: release
        run: |
          v="${GITHUB_REF_NAME#v}"
          for f in *; do
            case "$f" in
              Firefox-VPN-*) cp "$f" "Firefox-VPN-$v-${f#Firefox-VPN-}" ;;
              firefox-vpn-core-*) cp "$f" "firefox-vpn-core-$v-${f#firefox-vpn-core-}" ;;
            esac
          done
          ls
      - name: Checksums
        working-directory: release
        run: sha256sum * > SHA256SUMS.txt && cat SHA256SUMS.txt
      # The Windows app installs a new version only when the release key
      # signed this list (core/internal/update): SHA256SUMS.txt.sig.
      - name: Sign the checksums
        working-directory: core
        env:
          FIREFOX_VPN_SIGNING_KEY: ${{ secrets.FIREFOX_VPN_SIGNING_KEY }}
        run: |
          if [ -z "$FIREFOX_VPN_SIGNING_KEY" ]; then
            echo "::error::no release key (the secret FIREFOX_VPN_SIGNING_KEY): the apps could not install this release"
            exit 1
          fi
          go run ./tools/sign ../release/SHA256SUMS.txt
      # The notes are the README's download table only, linking to this
      # release's files.
      - name: Publish
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          bash scripts/release-notes.sh "$GITHUB_REF_NAME" > notes.md
          gh release create "$GITHUB_REF_NAME" release/* \
            --repo "$GITHUB_REPOSITORY" \
            --title "Firefox VPN ${{ needs.test.outputs.version }}" \
            --notes-file notes.md