name: "Publish: Release Uploads" run-name: "Publish: Release Uploads (Build ${{ inputs.build_number }})" on: workflow_dispatch: inputs: build_number: description: Build number to publish required: true upload_app_store: description: Upload iOS/macOS/visionOS artifacts to App Store Connect required: false default: "false" distribute_testflight: description: Distribute an already-uploaded iOS build to TestFlight required: false default: "false" upload_sparkle: description: Publish Sparkle artifacts required: false default: "true" upload_microsoft_store: description: Upload Windows package to Microsoft Partner Center beta lane required: false default: "false" sparkle_channel: description: Sparkle channel, defaults to build- required: false default: "" sparkle_point_main: description: Point Sparkle main appcast/default channel to selected channel 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" permissions: contents: read concurrency: group: burrow-publish-store-uploads-${{ github.event.inputs.build_number }} cancel-in-progress: true env: BURROW_RELEASE_GCS_BUCKET: ${{ vars.BURROW_RELEASE_GCS_BUCKET || 'burrow-net-releases' }} 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_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_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_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_KMS_LOCATION: ${{ vars.BURROW_KMS_LOCATION || 'global' }} BURROW_KMS_KEYRING: ${{ vars.BURROW_KMS_KEYRING || 'burrow-identity' }} TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS: ${{ vars.TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS || '7200' }} jobs: sign-apple-kms: name: Sign (Apple KMS) if: ${{ github.event.inputs.upload_app_store == 'true' || github.event.inputs.upload_sparkle == '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: Ensure Nix shell: bash run: bash Scripts/ci/ensure-nix.sh - name: Configure Age shell: bash env: AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }} run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV" - name: Decrypt Release Secrets uses: ./.forgejo/actions/decrypt-release-secrets - name: Validate App Store Credentials shell: bash run: Scripts/ci/check-release-config.sh app-store - name: Download Staged Apple Artifacts shell: bash env: BUILD_NUMBER: ${{ github.event.inputs.build_number }} run: | set -euo pipefail nix develop .#ci -c Scripts/ci/google-wif-auth.sh mkdir -p "dist/builds/${BUILD_NUMBER}/apple" nix develop .#ci -c gcloud storage rsync --recursive "gs://${BURROW_RELEASE_GCS_BUCKET}/builds/${BUILD_NUMBER}/apple" "dist/builds/${BUILD_NUMBER}/apple" find "dist/builds/${BUILD_NUMBER}/apple" -type f -print | sort - name: Sync Apple Provisioning Profiles shell: bash run: nix develop .#ci -c Scripts/ci/sync-apple-provisioning-profiles.sh all - name: Sign Apple Artifacts With KMS shell: bash env: BUILD_NUMBER: ${{ github.event.inputs.build_number }} SPARKLE_CHANNEL_INPUT: ${{ github.event.inputs.sparkle_channel }} run: | set -euo pipefail channel="${SPARKLE_CHANNEL_INPUT:-}" if [[ -z "$channel" ]]; then channel="build-${BUILD_NUMBER}" fi SPARKLE_CHANNEL="$channel" nix develop .#ci -c Scripts/apple/sign-release-artifacts-kms.sh all - name: Upload Signed Apple Artifacts shell: bash env: BUILD_NUMBER: ${{ github.event.inputs.build_number }} run: nix develop .#ci -c Scripts/ci/upload-release-storage.sh upload-app-store: name: Upload (App Store Connect) needs: sign-apple-kms if: ${{ github.event.inputs.upload_app_store == 'true' }} runs-on: namespace-profile-macos-large steps: - name: Checkout uses: https://code.forgejo.org/actions/checkout@v4 with: token: ${{ github.token }} fetch-depth: 0 - name: Ensure Nix shell: bash run: bash Scripts/ci/ensure-nix.sh - name: Configure Age shell: bash env: AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }} run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV" - name: Decrypt Release Secrets uses: ./.forgejo/actions/decrypt-release-secrets - name: Validate App Store Credentials shell: bash run: Scripts/ci/check-release-config.sh app-store - name: Download Apple Artifacts From GCS shell: bash env: BUILD_NUMBER: ${{ github.event.inputs.build_number }} run: | set -euo pipefail nix develop .#ci -c Scripts/ci/google-wif-auth.sh mkdir -p publish/apple nix develop .#ci -c gcloud storage rsync --recursive "gs://${BURROW_RELEASE_GCS_BUCKET}/builds/${BUILD_NUMBER}/apple" publish/apple find publish/apple -type f -print | sort - name: Upload Apple Packages shell: bash run: | set -euo pipefail Scripts/ci/check-release-config.sh app-store key_dir="${RUNNER_TEMP:-/tmp}/burrow-asc" altool_key_dir="${HOME}/.appstoreconnect/private_keys" mkdir -p "$key_dir" "$altool_key_dir" key_path="$key_dir/AuthKey_${ASC_API_KEY_ID}.p8" if [[ -n "${ASC_API_KEY_PATH:-}" && -f "${ASC_API_KEY_PATH:-}" ]]; then cp "$ASC_API_KEY_PATH" "$key_path" else printf '%s' "$ASC_API_KEY_BASE64" | base64 --decode > "$key_path" fi cp "$key_path" "$altool_key_dir/AuthKey_${ASC_API_KEY_ID}.p8" chmod 600 "$key_path" "$altool_key_dir/AuthKey_${ASC_API_KEY_ID}.p8" shopt -s nullglob artifacts=() for artifact in publish/apple/*.ipa publish/apple/*.pkg; do [[ "$artifact" == *.unsigned.ipa ]] && continue artifacts+=("$artifact") done if (( ${#artifacts[@]} == 0 )); then echo "::error ::No Apple upload artifacts found for build ${{ github.event.inputs.build_number }}." exit 1 fi for artifact in "${artifacts[@]}"; do upload_type="ios" if [[ "$artifact" == *.pkg ]]; then upload_type="macos" fi xcrun altool --upload-app \ --type "$upload_type" \ --file "$artifact" \ --apiKey "$ASC_API_KEY_ID" \ --apiIssuer "$ASC_API_ISSUER_ID" done release-ios-testflight: name: Release (TestFlight iOS Testers) needs: upload-app-store if: ${{ github.event.inputs.upload_app_store == '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: Ensure Nix shell: bash run: bash Scripts/ci/ensure-nix.sh - name: Configure Age shell: bash env: AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }} run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV" - name: Decrypt Release Secrets uses: ./.forgejo/actions/decrypt-release-secrets - name: Validate App Store Credentials shell: bash run: Scripts/ci/check-release-config.sh app-store - name: Distribute iOS Build To TestFlight shell: bash env: BUILD_NUMBER: ${{ github.event.inputs.build_number }} TESTFLIGHT_DISTRIBUTE_EXTERNAL: ${{ github.event.inputs.testflight_distribute_external || 'false' }} TESTFLIGHT_GROUPS: ${{ github.event.inputs.testflight_groups || '' }} TESTFLIGHT_INTERNAL_GROUP: Internal IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ github.event.inputs.ios_uses_non_exempt_encryption || 'false' }} run: Scripts/ci/distribute-testflight.sh distribute-ios-testflight: name: Distribute (TestFlight iOS Testers) if: ${{ github.event.inputs.upload_app_store != 'true' && github.event.inputs.distribute_testflight == '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: Ensure Nix shell: bash run: bash Scripts/ci/ensure-nix.sh - name: Configure Age shell: bash env: AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }} run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV" - name: Decrypt Release Secrets uses: ./.forgejo/actions/decrypt-release-secrets - name: Validate App Store Credentials shell: bash run: Scripts/ci/check-release-config.sh app-store - name: Distribute iOS Build To TestFlight shell: bash env: BUILD_NUMBER: ${{ github.event.inputs.build_number }} TESTFLIGHT_DISTRIBUTE_EXTERNAL: ${{ github.event.inputs.testflight_distribute_external || 'false' }} TESTFLIGHT_GROUPS: ${{ github.event.inputs.testflight_groups || '' }} TESTFLIGHT_INTERNAL_GROUP: Internal IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ github.event.inputs.ios_uses_non_exempt_encryption || 'false' }} run: Scripts/ci/distribute-testflight.sh upload-sparkle: name: Upload (Sparkle) needs: sign-apple-kms if: ${{ github.event.inputs.upload_sparkle == '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: Resolve Sparkle Channel id: channel shell: bash env: BUILD_NUMBER: ${{ github.event.inputs.build_number }} run: | set -euo pipefail channel="${{ github.event.inputs.sparkle_channel }}" if [[ -z "$channel" ]]; then channel="build-${BUILD_NUMBER}" fi echo "channel=$channel" >> "$GITHUB_OUTPUT" - name: Ensure Nix shell: bash run: bash Scripts/ci/ensure-nix.sh - name: Publish Sparkle Channel shell: bash env: BUILD_NUMBER: ${{ github.event.inputs.build_number }} CHANNEL: ${{ steps.channel.outputs.channel }} SPARKLE_POINT_MAIN: ${{ github.event.inputs.sparkle_point_main }} run: | set -euo pipefail nix develop .#ci -c Scripts/ci/google-wif-auth.sh mkdir -p "publish/sparkle/${CHANNEL}" nix develop .#ci -c gcloud storage rsync --recursive "gs://${BURROW_RELEASE_GCS_BUCKET}/builds/${BUILD_NUMBER}/sparkle/${CHANNEL}" "publish/sparkle/${CHANNEL}" || true if [[ ! -f "publish/sparkle/${CHANNEL}/appcast.xml" ]]; then echo "::warning ::No Sparkle appcast staged for build ${BUILD_NUMBER}; skipping Sparkle publish." find "publish/sparkle/${CHANNEL}" -maxdepth 2 -type f -print || true exit 0 fi nix develop .#ci -c gcloud storage rsync --recursive "publish/sparkle/${CHANNEL}" "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/${CHANNEL}" if [[ "$SPARKLE_POINT_MAIN" == "true" ]]; then printf '%s\n' "$CHANNEL" > main-channel.txt nix develop .#ci -c gcloud storage cp main-channel.txt "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/main-channel.txt" nix develop .#ci -c gcloud storage cp "publish/sparkle/${CHANNEL}/appcast.xml" "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/appcast.xml" nix develop .#ci -c gcloud storage rsync --recursive "publish/sparkle/${CHANNEL}" "gs://${BURROW_RELEASE_GCS_BUCKET}/sparkle/default" fi upload-microsoft-store: name: Upload (Microsoft Store Beta) if: ${{ github.event.inputs.upload_microsoft_store == '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: Validate Microsoft Store Credentials shell: pwsh env: MICROSOFT_TENANT_ID: ${{ secrets.MICROSOFT_TENANT_ID }} MICROSOFT_CLIENT_ID: ${{ secrets.MICROSOFT_CLIENT_ID }} MICROSOFT_CLIENT_SECRET: ${{ secrets.MICROSOFT_CLIENT_SECRET }} run: | foreach ($name in "MICROSOFT_TENANT_ID","MICROSOFT_CLIENT_ID","MICROSOFT_CLIENT_SECRET") { if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) { throw "missing required environment variable: $name" } } - name: Stop Until Store Package Exists shell: pwsh run: | Write-Host "::warning ::Burrow only builds a Windows Rust stub today; Microsoft Store package upload is wired for credentials but waits on MSIX packaging."