name: "Build: Release" run-name: "Build: Release (${{ github.ref_name }})" on: workflow_dispatch: inputs: build_number_override: description: Optional explicit build number required: false default: "" upload_app_store: description: Trigger downstream App Store Connect upload workflow required: false default: "false" upload_sparkle: description: Trigger downstream Sparkle publish workflow required: false default: "true" upload_microsoft_store: description: Trigger downstream Microsoft Store beta upload workflow required: false default: "false" sparkle_point_main: description: Update Sparkle main channel pointers required: false default: "true" testflight_distribute_external: description: Distribute to external TestFlight groups required: false default: "false" testflight_groups: description: Optional comma-separated TestFlight groups for external distribution required: false default: "" ios_uses_non_exempt_encryption: description: Set true only when the iOS build uses non-exempt encryption required: false default: "false" push: tags: - "release/*" permissions: actions: write contents: write concurrency: group: burrow-build-release-${{ github.ref }} cancel-in-progress: true env: BURROW_RELEASE_GCS_BUCKET: ${{ vars.BURROW_RELEASE_GCS_BUCKET || 'burrow-net-releases' }} BURROW_RELEASE_GCS_BACKUP: ${{ vars.BURROW_RELEASE_GCS_BACKUP || 'true' }} BURROW_RELEASE_GARAGE_BUCKET: ${{ vars.BURROW_RELEASE_GARAGE_BUCKET || 'burrow-releases' }} BURROW_RELEASE_REQUIRE_GARAGE: ${{ vars.BURROW_RELEASE_REQUIRE_GARAGE || 'true' }} BURROW_GARAGE_ENDPOINT: ${{ vars.BURROW_GARAGE_ENDPOINT || 'https://objects.burrow.net' }} BURROW_GARAGE_REGION: ${{ vars.BURROW_GARAGE_REGION || 'garage' }} AWS_ACCESS_KEY_ID: ${{ secrets.BURROW_GARAGE_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.BURROW_GARAGE_SECRET_ACCESS_KEY }} BURROW_NIX_CACHE_URL: ${{ vars.BURROW_NIX_CACHE_URL || 'https://nix.burrow.net/burrow' }} BURROW_NIX_CACHE_PUBLIC_KEY: ${{ vars.BURROW_NIX_CACHE_PUBLIC_KEY }} BURROW_GOOGLE_PROJECT_ID: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }} GOOGLE_CLOUD_PROJECT: ${{ vars.GOOGLE_PROJECT_ID || 'project-88c23ce9-918a-470a-b33' }} BURROW_GOOGLE_PROJECT_NUMBER: ${{ vars.GOOGLE_PROJECT_NUMBER || '416198671487' }} BURROW_GOOGLE_WIF_POOL_ID: ${{ vars.BURROW_GOOGLE_WIF_POOL_ID || 'burrow-authentik' }} BURROW_GOOGLE_WIF_PROVIDER_ID: ${{ vars.BURROW_GOOGLE_WIF_PROVIDER_ID || 'forgejo-runners' }} BURROW_GOOGLE_WIF_SERVICE_ACCOUNT: ${{ vars.BURROW_GOOGLE_WIF_SERVICE_ACCOUNT || 'burrow-forgejo-runner@project-88c23ce9-918a-470a-b33.iam.gserviceaccount.com' }} BURROW_AUTHENTIK_WIF_ISSUER: ${{ vars.BURROW_AUTHENTIK_WIF_ISSUER || 'https://auth.burrow.net/application/o/google-cloud/' }} BURROW_AUTHENTIK_WIF_AUDIENCE: ${{ vars.BURROW_AUTHENTIK_WIF_AUDIENCE || 'google-wif.burrow.net' }} BURROW_AUTHENTIK_WIF_CLIENT_ID: ${{ vars.BURROW_AUTHENTIK_WIF_CLIENT_ID || 'google-wif.burrow.net' }} BURROW_AUTHENTIK_WIF_CLIENT_SECRET: ${{ secrets.BURROW_AUTHENTIK_WIF_CLIENT_SECRET }} BURROW_APPLE_BUNDLE_ID: ${{ vars.BURROW_APPLE_BUNDLE_ID || 'net.burrow.app' }} BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID: ${{ vars.BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID || 'net.burrow.app.network' }} BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID: ${{ vars.BURROW_IOS_DISTRIBUTION_CERTIFICATE_ID || '3G42677598' }} BURROW_DEVELOPER_ID_CERTIFICATE_ID: ${{ vars.BURROW_DEVELOPER_ID_CERTIFICATE_ID || '9JKN6HXBHC' }} BURROW_APPLE_CREATE_MISSING_BUNDLE_IDS: ${{ vars.BURROW_APPLE_CREATE_MISSING_BUNDLE_IDS || 'false' }} BURROW_APPLE_ENABLE_CAPABILITIES: ${{ vars.BURROW_APPLE_ENABLE_CAPABILITIES || 'false' }} BURROW_APPLE_SIGNING_MODE: ${{ vars.BURROW_APPLE_SIGNING_MODE || 'google-kms-staged' }} BURROW_NOTARIZE_MACOS: ${{ vars.BURROW_NOTARIZE_MACOS || 'true' }} BURROW_SPARKLE_SIGN_WITH_KMS: ${{ vars.BURROW_SPARKLE_SIGN_WITH_KMS || 'true' }} BURROW_SPARKLE_KMS_KEY: ${{ vars.BURROW_SPARKLE_KMS_KEY || 'sparkle-ed25519' }} BURROW_SPARKLE_KMS_VERSION: ${{ vars.BURROW_SPARKLE_KMS_VERSION || '1' }} BURROW_SPARKLE_FEED_URL: ${{ vars.BURROW_SPARKLE_FEED_URL || 'https://releases.burrow.net/sparkle/appcast.xml' }} BURROW_SPARKLE_PUBLIC_ED_KEY: ${{ vars.BURROW_SPARKLE_PUBLIC_ED_KEY || 'Myv9ZNZT6YGKMtMezh52ra4WqaeEKc4VlvVU0evhJeI=' }} BURROW_VERSION_REQUIRE_REMOTE: "1" jobs: prepare: name: Prepare runs-on: namespace-profile-linux-medium outputs: do_release: ${{ steps.plan.outputs.do_release }} build_number: ${{ steps.plan.outputs.build_number }} steps: - name: Checkout uses: https://code.forgejo.org/actions/checkout@v4 with: token: ${{ github.token }} fetch-depth: 0 - name: Fetch Build Tags shell: bash run: git fetch --force --prune origin "+refs/tags/builds/*:refs/tags/builds/*" - name: Compute Release Plan id: plan shell: bash env: BURROW_BUILD_NUMBER_OVERRIDE: ${{ github.event.inputs.build_number_override || '' }} run: | set -euo pipefail status="$(Scripts/version.sh status)" if [[ "$status" == "clean" ]]; then echo "do_release=false" >> "$GITHUB_OUTPUT" echo "build_number=" >> "$GITHUB_OUTPUT" echo "No unreleased changes." exit 0 fi build_number="$(Scripts/version.sh pre-increment)" echo "do_release=true" >> "$GITHUB_OUTPUT" echo "build_number=${build_number}" >> "$GITHUB_OUTPUT" build-linux: name: Build (Linux) needs: prepare if: ${{ needs.prepare.outputs.do_release == 'true' }} runs-on: namespace-profile-linux-medium steps: - name: Checkout uses: https://code.forgejo.org/actions/checkout@v4 with: token: ${{ github.token }} fetch-depth: 0 - name: Build Linux Release shell: bash env: BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} run: | set -euo pipefail bash Scripts/ci/ensure-nix.sh nix develop .#ci -c Scripts/ci/nscloud-cache.sh nix nix develop .#ci -c Scripts/ci/package-release-artifacts.sh linux - name: Upload Linux Artifacts uses: https://code.forgejo.org/actions/upload-artifact@v4 with: name: burrow-linux-${{ needs.prepare.outputs.build_number }} path: dist/builds/${{ needs.prepare.outputs.build_number }}/linux/* if-no-files-found: error - name: Upload Linux Artifacts To Release Storage shell: bash env: BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} run: | set -euo pipefail bash Scripts/ci/ensure-nix.sh nix develop .#ci -c Scripts/ci/upload-release-storage.sh build-windows: name: Build (Windows Stub) needs: prepare if: ${{ needs.prepare.outputs.do_release == 'true' }} runs-on: namespace-profile-windows-large steps: - name: Checkout uses: https://code.forgejo.org/actions/checkout@v4 with: token: ${{ github.token }} fetch-depth: 0 - name: Build Rust Stub shell: pwsh env: BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} run: | if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { Invoke-WebRequest -Uri "https://win.rustup.rs" -OutFile "rustup-init.exe" .\rustup-init.exe -y --profile minimal --default-toolchain stable $env:Path = "$env:USERPROFILE\.cargo\bin;$env:Path" } cargo build --locked --release -p burrow-windows-stub New-Item -ItemType Directory -Force -Path "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc" | Out-Null Copy-Item "target/release/burrow.exe" "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc/burrow.exe" Copy-Item "README.md" "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc/README.md" Compress-Archive -Path "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc" -DestinationPath "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc.zip" -Force Get-FileHash "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc.zip" -Algorithm SHA256 | ForEach-Object { "$($_.Hash.ToLower()) burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc.zip" } | Set-Content "dist/builds/$env:BUILD_NUMBER/windows/burrow-$env:BUILD_NUMBER-x86_64-pc-windows-msvc.zip.sha256" - name: Upload Windows Artifacts uses: https://code.forgejo.org/actions/upload-artifact@v4 with: name: burrow-windows-${{ needs.prepare.outputs.build_number }} path: dist/builds/${{ needs.prepare.outputs.build_number }}/windows/* if-no-files-found: error build-apple: name: Build (Apple ${{ matrix.platform_name }}) needs: prepare if: ${{ needs.prepare.outputs.do_release == 'true' }} runs-on: namespace-profile-macos-large strategy: fail-fast: false max-parallel: 2 matrix: include: - platform: ios platform_name: iOS bazel_target: //bazel/apple:release_ios_stamp - platform: macos platform_name: macOS bazel_target: //bazel/apple:release_macos_stamp steps: - name: Checkout uses: https://code.forgejo.org/actions/checkout@v4 with: token: ${{ github.token }} fetch-depth: 0 submodules: recursive - name: Select Apple SDK shell: bash run: | set -euo pipefail if ! command -v xcrun >/dev/null 2>&1; then echo "::error ::xcrun is required for Apple release builds" >&2 exit 1 fi xcrun --find swiftc xcrun --sdk macosx --show-sdk-path xcrun --sdk iphoneos --show-sdk-path xcrun --sdk iphonesimulator --show-sdk-path - name: Prepare Rust Apple Targets shell: bash env: PLATFORM: ${{ matrix.platform }} run: | set -euo pipefail if ! command -v rustup >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable export PATH="$HOME/.cargo/bin:$PATH" fi case "$PLATFORM" in ios) rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios ;; macos) rustup target add aarch64-apple-darwin x86_64-apple-darwin ;; *) echo "::error ::unsupported Apple platform ${PLATFORM}" >&2 exit 1 ;; esac - name: Bootstrap Runner Environment shell: bash run: | set -euo pipefail runner_home="${RUNNER_HOME:-}" if [[ -z "$runner_home" ]]; then runner_home="$(python3 -c 'import os,pwd; print(pwd.getpwuid(os.getuid()).pw_dir)' 2>/dev/null || true)" if [[ -z "$runner_home" ]]; then runner_home="$PWD/.home" fi fi mkdir -p "$runner_home" "$PWD/.tmp" { echo "HOME=$runner_home" echo "XDG_CACHE_HOME=$runner_home/.cache" echo "XDG_CONFIG_HOME=$runner_home/.config" echo "XDG_DATA_HOME=$runner_home/.local/share" echo "TMPDIR=$PWD/.tmp" echo "TMP=$PWD/.tmp" } >> "$GITHUB_ENV" - name: Ensure Nix shell: bash run: bash Scripts/ci/ensure-nix.sh - name: Namespace Cache shell: bash run: | set -euo pipefail NIX_BIN="$(bash Scripts/ci/resolve-nix-bin.sh)" "$NIX_BIN" develop .#ci --command bash Scripts/ci/nscloud-cache.sh bazel - name: Configure Age id: age_apple continue-on-error: true shell: bash env: AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }} run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV" - name: Decrypt Release Secrets if: ${{ steps.age_apple.outcome == 'success' }} uses: ./.forgejo/actions/decrypt-release-secrets - name: Sync Apple Provisioning Profiles shell: bash env: PLATFORM: ${{ matrix.platform }} run: | set -euo pipefail NIX_BIN="$(bash Scripts/ci/resolve-nix-bin.sh)" "$NIX_BIN" develop .#ci --command Scripts/ci/sync-apple-provisioning-profiles.sh "$PLATFORM" - name: Install Apple Signing Assets shell: bash run: Scripts/ci/install-apple-signing-assets.sh - name: Build Apple Release shell: bash env: BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} PLATFORM: ${{ matrix.platform }} UPLOAD_APP_STORE: ${{ github.event.inputs.upload_app_store || 'false' }} UPLOAD_SPARKLE: ${{ github.event.inputs.upload_sparkle || 'true' }} SPARKLE_CHANNEL: build-${{ needs.prepare.outputs.build_number }} run: | set -euo pipefail require_signing=false if [[ "$UPLOAD_APP_STORE" == "true" && "$PLATFORM" == "ios" ]]; then require_signing=true fi if [[ "$UPLOAD_SPARKLE" == "true" && "$PLATFORM" == "macos" ]]; then require_signing=true fi bazel_args=() if [[ -n "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" && -f "${BURROW_BAZEL_NSCCACHE_BAZELRC:-}" ]]; then bazel_args+=("--bazelrc=${BURROW_BAZEL_NSCCACHE_BAZELRC}") fi NIX_BIN="$(bash Scripts/ci/resolve-nix-bin.sh)" ci_path="$("$NIX_BIN" develop .#ci --command bash -lc 'printf "%s" "$PATH"')" ci_protoc="$("$NIX_BIN" develop .#ci --command bash -lc 'command -v protoc')" "$NIX_BIN" develop .#ci --command bazel "${bazel_args[@]}" build \ --verbose_failures \ --show_timestamps \ --experimental_ui_max_stdouterr_bytes=16777216 \ --action_env=PATH="$ci_path" \ --action_env=PROTOC="$ci_protoc" \ --action_env=BUILD_WORKSPACE_DIRECTORY="$PWD" \ --action_env=BUILD_NUMBER="$BUILD_NUMBER" \ --action_env=BURROW_RELEASE_OUT="$PWD/dist/builds/$BUILD_NUMBER" \ --action_env=BURROW_APPLE_SIGNING_READY="${BURROW_APPLE_SIGNING_READY:-0}" \ --action_env=BURROW_APPLE_SIGNING_MODE="${BURROW_APPLE_SIGNING_MODE:-keychain}" \ --action_env=BURROW_APPLE_REQUIRE_SIGNING="$require_signing" \ --action_env=BURROW_APPLE_BUNDLE_ID="${BURROW_APPLE_BUNDLE_ID:-}" \ --action_env=BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-}" \ --action_env=BURROW_APPLE_KEYCHAIN_PATH="${BURROW_APPLE_KEYCHAIN_PATH:-}" \ --action_env=BURROW_NOTARIZE_MACOS="${BURROW_NOTARIZE_MACOS:-false}" \ --action_env=BURROW_SPARKLE_SIGN_WITH_KMS="${BURROW_SPARKLE_SIGN_WITH_KMS:-false}" \ --action_env=BURROW_SPARKLE_KMS_KEY="${BURROW_SPARKLE_KMS_KEY:-sparkle-ed25519}" \ --action_env=BURROW_SPARKLE_KMS_VERSION="${BURROW_SPARKLE_KMS_VERSION:-1}" \ --action_env=BURROW_SPARKLE_FEED_URL="${BURROW_SPARKLE_FEED_URL:-https://releases.burrow.net/sparkle/appcast.xml}" \ --action_env=BURROW_SPARKLE_PUBLIC_ED_KEY="${BURROW_SPARKLE_PUBLIC_ED_KEY:-}" \ --action_env=GOOGLE_CLOUD_PROJECT="${GOOGLE_CLOUD_PROJECT:-}" \ --action_env=ASC_API_KEY_ID="${ASC_API_KEY_ID:-}" \ --action_env=ASC_API_ISSUER_ID="${ASC_API_ISSUER_ID:-}" \ --action_env=ASC_API_KEY_PATH="${ASC_API_KEY_PATH:-}" \ --action_env=SPARKLE_CHANNEL="$SPARKLE_CHANNEL" \ --action_env=HOME="$HOME" \ --action_env=XDG_CACHE_HOME="$XDG_CACHE_HOME" \ "${{ matrix.bazel_target }}" - name: Upload Apple Artifacts uses: https://code.forgejo.org/actions/upload-artifact@v4 with: name: burrow-apple-${{ matrix.platform }}-${{ needs.prepare.outputs.build_number }} path: | dist/builds/${{ needs.prepare.outputs.build_number }}/apple/* dist/builds/${{ needs.prepare.outputs.build_number }}/sparkle/** if-no-files-found: error - name: Upload Apple Artifacts To Release Storage shell: bash env: BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} run: | set -euo pipefail bash Scripts/ci/ensure-nix.sh nix develop .#ci -c Scripts/ci/upload-release-storage.sh publish: name: Publish (Forgejo) needs: - prepare - build-linux - build-windows - build-apple if: ${{ always() && needs.prepare.outputs.do_release == 'true' }} runs-on: namespace-profile-linux-medium steps: - name: Checkout uses: https://code.forgejo.org/actions/checkout@v4 with: token: ${{ github.token }} fetch-depth: 0 - name: Download Artifacts uses: https://code.forgejo.org/actions/download-artifact@v4 with: path: dist/downloaded - name: Prepare Release Assets shell: bash env: BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} run: | set -euo pipefail mkdir -p dist mkdir -p "dist/builds/${BUILD_NUMBER}/windows" find dist/downloaded -path '*/burrow-windows-*/*' -type f -exec cp {} "dist/builds/${BUILD_NUMBER}/windows/" \; find dist/downloaded -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.sha256' -o -name '*.ipa' -o -name '*.pkg' -o -name '*.dmg' -o -name 'README*.txt' \) -exec cp {} dist/ \; rm -rf dist/downloaded find dist -maxdepth 1 -type f -print | sort - name: Upload Windows Artifacts To Release Storage shell: bash env: BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} run: | set -euo pipefail if ! find "dist/builds/${BUILD_NUMBER}/windows" -type f -print -quit | grep -q .; then echo "::warning ::No Windows artifacts staged for release storage upload." exit 0 fi bash Scripts/ci/ensure-nix.sh nix develop .#ci -c Scripts/ci/upload-release-storage.sh - name: Tag Build shell: bash env: BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} run: | set -euo pipefail tag="builds/${BUILD_NUMBER}" head_commit="$(git rev-parse HEAD)" remote_commit="$(git ls-remote --tags --refs origin "refs/tags/${tag}" | awk 'NR==1 { print $1 }')" if [[ -n "$remote_commit" && "$remote_commit" != "$head_commit" ]]; then echo "::error ::Remote tag ${tag} already points at ${remote_commit}, not ${head_commit}" >&2 exit 1 fi git tag -f "$tag" "$head_commit" git push --quiet --no-verify origin "refs/tags/${tag}:refs/tags/${tag}" - name: Publish Forgejo Release shell: bash env: RELEASE_TAG: builds/${{ needs.prepare.outputs.build_number }} API_URL: ${{ github.api_url }} REPOSITORY: ${{ github.repository }} TOKEN: ${{ github.token }} run: Scripts/ci/publish-forgejo-release.sh - name: Dispatch Store Uploads shell: bash env: GITHUB_TOKEN: ${{ github.token }} BUILD_NUMBER: ${{ needs.prepare.outputs.build_number }} UPLOAD_APP_STORE: ${{ github.event.inputs.upload_app_store || 'false' }} UPLOAD_SPARKLE: ${{ github.event.inputs.upload_sparkle || 'true' }} UPLOAD_MICROSOFT_STORE: ${{ github.event.inputs.upload_microsoft_store || 'false' }} SPARKLE_POINT_MAIN: ${{ github.event.inputs.sparkle_point_main || 'true' }} TESTFLIGHT_DISTRIBUTE_EXTERNAL: ${{ github.event.inputs.testflight_distribute_external || 'false' }} TESTFLIGHT_GROUPS: ${{ github.event.inputs.testflight_groups || '' }} IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ github.event.inputs.ios_uses_non_exempt_encryption || 'false' }} run: | set -euo pipefail inputs="$(python3 -c 'import json, os; print(json.dumps({"build_number":os.environ["BUILD_NUMBER"],"upload_app_store":os.environ["UPLOAD_APP_STORE"],"upload_sparkle":os.environ["UPLOAD_SPARKLE"],"upload_microsoft_store":os.environ["UPLOAD_MICROSOFT_STORE"],"sparkle_point_main":os.environ["SPARKLE_POINT_MAIN"],"testflight_distribute_external":os.environ["TESTFLIGHT_DISTRIBUTE_EXTERNAL"],"testflight_groups":os.environ["TESTFLIGHT_GROUPS"],"ios_uses_non_exempt_encryption":os.environ["IOS_USES_NON_EXEMPT_ENCRYPTION"]}))')" curl -fsS \ -X POST \ -H "Authorization: token ${GITHUB_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"ref\":\"main\",\"inputs\":${inputs}}" \ "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/workflows/publish-store-uploads.yml/dispatches"