Build and Release #610
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and Release | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" # validated by set-metadata | |
| schedule: | |
| - cron: "0 4 * * *" | |
| workflow_dispatch: | |
| inputs: | |
| build_type: | |
| description: "Build type" | |
| required: false | |
| type: choice | |
| options: | |
| - nightly | |
| default: nightly | |
| platforms: | |
| description: "Platforms to build ('all', or 'macos,linux,android,ios,windows')" | |
| required: false | |
| type: string | |
| default: "linux" | |
| sign_windows: | |
| description: "Sign Windows binaries (nightly skips signing by default)" | |
| required: false | |
| type: boolean | |
| default: false | |
| linux_arch: | |
| description: "Linux arch to build when Linux is included" | |
| required: false | |
| type: choice | |
| options: | |
| - all | |
| - amd64 | |
| - arm64 | |
| default: all | |
| cleanup_on_failure: | |
| description: "Delete draft release on failure" | |
| required: false | |
| type: boolean | |
| default: true | |
| windows_connect_smoke: | |
| description: "Run Windows connect/disconnect smoke test on manual dispatch" | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| id-token: "write" | |
| concurrency: | |
| group: ${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| set-metadata: | |
| # Allow tag-push releases, scheduled builds from default branch, | |
| # and workflow_dispatch from any branch (non-main branches create | |
| # draft releases that are immediately deleted for testing). | |
| if: ${{ startsWith(github.ref, 'refs/tags/') || (github.event_name == 'schedule' && github.ref_name == github.event.repository.default_branch) || github.event_name == 'workflow_dispatch' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| build_type: ${{ steps.meta.outputs.build_type }} | |
| release_tag: ${{ steps.meta.outputs.release_tag }} | |
| version: ${{ steps.meta.outputs.version }} | |
| installer_base_name: ${{ steps.meta.outputs.installer_base_name }} | |
| platform: ${{ steps.meta.outputs.platform }} | |
| linux_arch: ${{ steps.meta.outputs.linux_arch }} | |
| is_test_run: ${{ steps.meta.outputs.is_test_run }} | |
| smoke_enable_ip_check: ${{ steps.meta.outputs.smoke_enable_ip_check }} | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.ref }} | |
| # For push-tag releases, avoid shallow checkout here so the full | |
| # tag set is available during validation. | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - id: meta | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| EVENT="${{ github.event_name }}" | |
| case "$EVENT" in | |
| schedule) | |
| # Scheduled nightly build | |
| BUILD_TYPE="nightly" | |
| VERSION=$(./scripts/ci/version.sh generate nightly) | |
| RELEASE_TAG="v${VERSION}" | |
| PLATFORM="all" | |
| LINUX_ARCH="all" | |
| ;; | |
| workflow_dispatch) | |
| # Manual trigger | |
| BUILD_TYPE="${{ github.event.inputs.build_type }}" | |
| PLATFORM="${{ github.event.inputs.platforms }}" | |
| LINUX_ARCH="${{ github.event.inputs.linux_arch }}" | |
| case "$BUILD_TYPE" in | |
| nightly) | |
| VERSION=$(./scripts/ci/version.sh generate "$BUILD_TYPE") | |
| RELEASE_TAG="v${VERSION}" | |
| ;; | |
| beta|production) | |
| echo "Error: $BUILD_TYPE builds must use git tags, not manual dispatch" >&2 | |
| echo " Push a tag like 'v1.2.3-beta' (beta) or 'v1.2.3' (production) instead." >&2 | |
| exit 1 | |
| ;; | |
| *) | |
| echo "Error: Unknown build_type '$BUILD_TYPE'" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| ;; | |
| push) | |
| # Tag push (release build) - preserve original tag with 'v' prefix | |
| RELEASE_TAG=${GITHUB_REF#refs/tags/} | |
| # Validate the tag (version.sh strips 'v' for validation) | |
| ./scripts/ci/version.sh validate "$RELEASE_TAG" > /dev/null | |
| # Strip 'v' for VERSION (used by build workflows) | |
| VERSION="${RELEASE_TAG#v}" | |
| case "$RELEASE_TAG" in | |
| *-beta*) | |
| BUILD_TYPE="beta" | |
| ;; | |
| *) | |
| # Production: bare version or version with platform suffix | |
| # e.g., v9.0.15 (all platforms) or v9.0.15-android (single platform) | |
| if [[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(windows|macos|linux|android|ios))?$ ]]; then | |
| BUILD_TYPE="production" | |
| else | |
| echo "Error: Invalid tag format for build" >&2 | |
| echo " Use 'v1.0.0' for production (all platforms)" >&2 | |
| echo " Use 'v1.0.0-<platform>' for production (single platform)" >&2 | |
| echo " Use 'v1.0.0-beta' for beta" >&2 | |
| echo "Got: $RELEASE_TAG" >&2 | |
| exit 1 | |
| fi | |
| ;; | |
| esac | |
| # Extract platform suffix (if any) | |
| platform_suffix="${RELEASE_TAG##*-}" | |
| case "$platform_suffix" in | |
| windows) PLATFORM="windows" ;; | |
| macos) PLATFORM="macos" ;; | |
| linux) PLATFORM="linux" ;; | |
| android) PLATFORM="android" ;; | |
| ios) PLATFORM="ios" ;; | |
| *) PLATFORM="all" ;; | |
| esac | |
| # Strip platform suffix from VERSION (platform is tracked separately) | |
| case "$PLATFORM" in | |
| all) ;; | |
| *) VERSION="${VERSION%-$PLATFORM}" ;; | |
| esac | |
| case "$PLATFORM" in | |
| all|linux) LINUX_ARCH="all" ;; | |
| *) LINUX_ARCH="amd64" ;; | |
| esac | |
| ;; | |
| esac | |
| case "$LINUX_ARCH" in | |
| all|amd64|arm64) ;; | |
| *) | |
| echo "Error: Invalid linux_arch '$LINUX_ARCH' (expected: all, amd64, arm64)" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| # Installer base name - Makefile will append build type | |
| INSTALLER_BASE_NAME="lantern-installer" | |
| echo "Build Configuration:" | |
| echo " Event: $EVENT" | |
| echo " Build Type: $BUILD_TYPE" | |
| echo " Release Tag: $RELEASE_TAG" | |
| echo " Version: $VERSION" | |
| echo " Platform: $PLATFORM" | |
| echo " Linux Arch: $LINUX_ARCH" | |
| echo " Installer: $INSTALLER_BASE_NAME" | |
| # Test run: workflow_dispatch from non-default branch | |
| IS_TEST_RUN="false" | |
| if [[ "$EVENT" == "workflow_dispatch" ]]; then | |
| DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" | |
| if [[ "${{ github.ref_name }}" != "$DEFAULT_BRANCH" ]]; then | |
| IS_TEST_RUN="true" | |
| echo "Test run: draft release will be deleted after build" | |
| fi | |
| fi | |
| SMOKE_ENABLE_IP_CHECK="false" | |
| echo "build_type=$BUILD_TYPE" >> $GITHUB_OUTPUT | |
| echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "platform=$PLATFORM" >> $GITHUB_OUTPUT | |
| echo "linux_arch=$LINUX_ARCH" >> $GITHUB_OUTPUT | |
| echo "installer_base_name=$INSTALLER_BASE_NAME" >> $GITHUB_OUTPUT | |
| echo "is_test_run=$IS_TEST_RUN" >> $GITHUB_OUTPUT | |
| echo "smoke_enable_ip_check=$SMOKE_ENABLE_IP_CHECK" >> $GITHUB_OUTPUT | |
| - name: Update pubspec.yaml version | |
| shell: bash | |
| env: | |
| RELEASE_TAG: ${{ steps.meta.outputs.release_tag }} | |
| # github.run_number is monotonically increasing across all workflow | |
| # runs and never resets when the base version changes. This is | |
| # critical for macOS system extensions, which compare CFBundleVersion | |
| # numerically and refuse to upgrade to a lower build number. | |
| BUILD_NUMBER: ${{ github.run_number }} | |
| run: | | |
| # Strip 'v' prefix if present | |
| VERSION="${RELEASE_TAG#v}" | |
| # Extract base semver (strip suffixes like -beta, -nightly-...) | |
| BASE_VERSION=$(./scripts/ci/version.sh extract "$VERSION") | |
| FULL_VERSION="${BASE_VERSION}+${BUILD_NUMBER}" | |
| echo "Setting pubspec version: $FULL_VERSION" | |
| echo " Base version: $BASE_VERSION" | |
| echo " Build number: $BUILD_NUMBER (github.run_number)" | |
| sed -i.bak -E "s/^version:.*/version: ${FULL_VERSION}/" pubspec.yaml | |
| grep '^version:' pubspec.yaml | |
| - name: Upload pubspec.yaml | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pubspec | |
| path: pubspec.yaml | |
| release-notify: | |
| needs: [set-metadata] | |
| if: | | |
| needs.set-metadata.outputs.build_type == 'production' && | |
| needs.set-metadata.outputs.platform == 'all' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Notify Slack | |
| uses: slackapi/slack-github-action@v2.0.0 | |
| with: | |
| webhook: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| webhook-type: webhook-trigger | |
| payload: | | |
| { | |
| "text": "Production release v${{ needs.set-metadata.outputs.version }} awaiting approval before releasing on all platforms: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| } | |
| release-approval: | |
| needs: [set-metadata] | |
| if: | | |
| needs.set-metadata.outputs.build_type == 'production' && | |
| needs.set-metadata.outputs.platform == 'all' | |
| runs-on: ubuntu-latest | |
| environment: production | |
| steps: | |
| - run: echo "Approved" | |
| build-macos: | |
| needs: [set-metadata, release-create, release-approval] | |
| uses: ./.github/workflows/build-macos.yml | |
| secrets: inherit | |
| if: | | |
| !cancelled() && | |
| needs.set-metadata.result == 'success' && | |
| needs.release-create.result == 'success' && | |
| (needs.release-approval.result == 'success' || needs.release-approval.result == 'skipped') && | |
| (needs.set-metadata.outputs.platform == 'all' || contains(needs.set-metadata.outputs.platform, 'macos')) | |
| with: | |
| version: ${{ needs.set-metadata.outputs.version }} | |
| build_type: ${{ needs.set-metadata.outputs.build_type }} | |
| installer_base_name: ${{ needs.set-metadata.outputs.installer_base_name }} | |
| build-windows: | |
| needs: [set-metadata, release-create, release-approval] | |
| uses: ./.github/workflows/build-windows.yml | |
| secrets: inherit | |
| if: | | |
| !cancelled() && | |
| needs.set-metadata.result == 'success' && | |
| needs.release-create.result == 'success' && | |
| (needs.release-approval.result == 'success' || needs.release-approval.result == 'skipped') && | |
| (needs.set-metadata.outputs.platform == 'all' || contains(needs.set-metadata.outputs.platform, 'windows')) | |
| with: | |
| version: ${{ needs.set-metadata.outputs.version }} | |
| build_type: ${{ needs.set-metadata.outputs.build_type }} | |
| installer_base_name: ${{ needs.set-metadata.outputs.installer_base_name }} | |
| enable_ip_check: ${{ needs.set-metadata.outputs.smoke_enable_ip_check == 'true' }} | |
| run_connect_smoke: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.windows_connect_smoke == 'true' }} | |
| run_split_tunnel_website_smoke: false | |
| run_config_url_smoke: false | |
| force_full_tunnel_smoke: ${{ needs.set-metadata.outputs.build_type == 'nightly' }} | |
| run_auth_smoke: false | |
| skip_signing: ${{ needs.set-metadata.outputs.build_type == 'nightly' && (needs.set-metadata.outputs.is_test_run == 'true' || github.event.inputs.sign_windows != 'true') }} | |
| build-linux: | |
| needs: [set-metadata, release-create, release-approval] | |
| uses: ./.github/workflows/build-linux.yml | |
| secrets: inherit | |
| if: | | |
| !cancelled() && | |
| needs.set-metadata.result == 'success' && | |
| needs.release-create.result == 'success' && | |
| (needs.release-approval.result == 'success' || needs.release-approval.result == 'skipped') && | |
| (needs.set-metadata.outputs.platform == 'all' || contains(needs.set-metadata.outputs.platform, 'linux')) | |
| with: | |
| version: ${{ needs.set-metadata.outputs.version }} | |
| build_type: ${{ needs.set-metadata.outputs.build_type }} | |
| installer_base_name: ${{ needs.set-metadata.outputs.installer_base_name }} | |
| linux_arch: ${{ needs.set-metadata.outputs.linux_arch }} | |
| enable_ip_check: ${{ needs.set-metadata.outputs.smoke_enable_ip_check == 'true' }} | |
| force_full_tunnel_smoke: ${{ needs.set-metadata.outputs.build_type == 'nightly' }} | |
| run_auth_smoke: false | |
| build-ios: | |
| needs: [set-metadata, release-create, release-approval] | |
| uses: ./.github/workflows/build-ios.yml | |
| secrets: inherit | |
| if: | | |
| !cancelled() && | |
| needs.set-metadata.result == 'success' && | |
| needs.release-create.result == 'success' && | |
| (needs.release-approval.result == 'success' || needs.release-approval.result == 'skipped') && | |
| (needs.set-metadata.outputs.platform == 'all' || contains(needs.set-metadata.outputs.platform, 'ios')) | |
| with: | |
| version: ${{ needs.set-metadata.outputs.version }} | |
| build_type: ${{ needs.set-metadata.outputs.build_type }} | |
| installer_base_name: ${{ needs.set-metadata.outputs.installer_base_name }} | |
| build-android: | |
| needs: [set-metadata, release-create, release-approval] | |
| uses: ./.github/workflows/build-android.yml | |
| secrets: inherit | |
| if: | | |
| !cancelled() && | |
| needs.set-metadata.result == 'success' && | |
| needs.release-create.result == 'success' && | |
| (needs.release-approval.result == 'success' || needs.release-approval.result == 'skipped') && | |
| (needs.set-metadata.outputs.platform == 'all' || contains(needs.set-metadata.outputs.platform, 'android')) | |
| with: | |
| version: ${{ needs.set-metadata.outputs.version }} | |
| build_type: ${{ needs.set-metadata.outputs.build_type }} | |
| installer_base_name: ${{ needs.set-metadata.outputs.installer_base_name }} | |
| release-create: | |
| needs: set-metadata | |
| runs-on: ubuntu-latest | |
| env: | |
| BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }} | |
| RELEASE_TAG: ${{ needs.set-metadata.outputs.release_tag }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # Strip 'v' prefix for display | |
| VERSION="${RELEASE_TAG#v}" | |
| case "$BUILD_TYPE" in | |
| production) | |
| PRERELEASE_FLAG="" | |
| TITLE="Lantern $VERSION" | |
| ;; | |
| beta) | |
| PRERELEASE_FLAG="--prerelease" | |
| TITLE="Beta $VERSION" | |
| ;; | |
| nightly) | |
| PRERELEASE_FLAG="--prerelease" | |
| TITLE="Nightly $VERSION" | |
| ;; | |
| esac | |
| WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| gh release create "$RELEASE_TAG" \ | |
| --draft \ | |
| $PRERELEASE_FLAG \ | |
| --title "$TITLE" \ | |
| --notes "Build [in progress](${WORKFLOW_URL})..." | |
| upload-s3: | |
| needs: | |
| [ | |
| set-metadata, | |
| build-macos, | |
| build-windows, | |
| build-linux, | |
| build-android, | |
| build-ios, | |
| ] | |
| # Nightly builds publish any healthy platform; beta/production remain | |
| # all-or-nothing across requested platforms. | |
| if: | | |
| always() && | |
| !cancelled() && | |
| ( | |
| ( | |
| needs.set-metadata.outputs.build_type == 'nightly' && | |
| (needs.build-macos.result == 'success' || needs.build-windows.result == 'success' || | |
| needs.build-linux.result == 'success' || needs.build-android.result == 'success' || | |
| needs.build-ios.result == 'success') | |
| ) || | |
| ( | |
| needs.set-metadata.outputs.build_type != 'nightly' && | |
| (needs.build-macos.result == 'success' || needs.build-macos.result == 'skipped') && | |
| (needs.build-windows.result == 'success' || needs.build-windows.result == 'skipped') && | |
| (needs.build-linux.result == 'success' || needs.build-linux.result == 'skipped') && | |
| (needs.build-android.result == 'success' || needs.build-android.result == 'skipped') && | |
| (needs.build-ios.result == 'success' || needs.build-ios.result == 'skipped') && | |
| (needs.build-macos.result == 'success' || needs.build-windows.result == 'success' || | |
| needs.build-linux.result == 'success' || needs.build-android.result == 'success' || | |
| needs.build-ios.result == 'success') | |
| ) | |
| ) | |
| runs-on: ubuntu-latest | |
| env: | |
| BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }} | |
| RELEASE_TAG: ${{ needs.set-metadata.outputs.release_tag }} | |
| INSTALLER_BASE_NAME: ${{ needs.set-metadata.outputs.installer_base_name }} | |
| PLATFORM: ${{ needs.set-metadata.outputs.platform }} | |
| LINUX_ARCH: ${{ needs.set-metadata.outputs.linux_arch }} | |
| BUCKET: ${{ vars.S3_RELEASES_BUCKET }} | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| - name: Resolve nightly publish scope | |
| if: ${{ env.BUILD_TYPE == 'nightly' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| has_artifact() { | |
| compgen -G "$1" > /dev/null | |
| } | |
| platforms=() | |
| if has_artifact "lantern-installer-dmg/*.dmg"; then | |
| platforms+=("macos") | |
| fi | |
| if has_artifact "lantern-installer-exe/*.exe"; then | |
| platforms+=("windows") | |
| fi | |
| if has_artifact "lantern-installer-apk/*.apk"; then | |
| platforms+=("android") | |
| fi | |
| if has_artifact "lantern-installer-ipa/*.ipa"; then | |
| platforms+=("ios") | |
| fi | |
| linux_amd64=false | |
| linux_arm64=false | |
| if has_artifact "lantern-installer-deb-amd64/*.deb" || has_artifact "lantern-installer-rpm-amd64/*.rpm" || has_artifact "lantern-installer-deb/*.deb" || has_artifact "lantern-installer-rpm/*.rpm"; then | |
| linux_amd64=true | |
| fi | |
| if has_artifact "lantern-installer-deb-arm64/*.deb" || has_artifact "lantern-installer-rpm-arm64/*.rpm"; then | |
| linux_arm64=true | |
| fi | |
| if [[ "$linux_amd64" == true || "$linux_arm64" == true ]]; then | |
| platforms+=("linux") | |
| fi | |
| if [[ "${#platforms[@]}" -eq 0 ]]; then | |
| echo "No publishable nightly artifacts were found." >&2 | |
| exit 1 | |
| fi | |
| nightly_platforms="$(IFS=,; echo "${platforms[*]}")" | |
| nightly_linux_arch="amd64" | |
| if [[ "$linux_amd64" == true && "$linux_arm64" == true ]]; then | |
| nightly_linux_arch="all" | |
| elif [[ "$linux_arm64" == true ]]; then | |
| nightly_linux_arch="arm64" | |
| fi | |
| echo "Nightly publish scope: ${nightly_platforms} (linux_arch=${nightly_linux_arch})" | |
| echo "PLATFORM=${nightly_platforms}" >> "$GITHUB_ENV" | |
| echo "LINUX_ARCH=${nightly_linux_arch}" >> "$GITHUB_ENV" | |
| - name: Upload to S3 | |
| shell: bash | |
| run: | | |
| # Strip 'v' prefix for S3 paths | |
| VERSION="${RELEASE_TAG#v}" | |
| ./scripts/ci/publish-to-s3.sh \ | |
| "$BUILD_TYPE" \ | |
| "$VERSION" \ | |
| "$INSTALLER_BASE_NAME" \ | |
| "$PLATFORM" | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| - name: Format GitHub job summary | |
| shell: bash | |
| run: ./scripts/ci/format.sh job-summary >> "$GITHUB_STEP_SUMMARY" | |
| env: | |
| RELEASE_TAG: ${{ env.RELEASE_TAG }} | |
| - name: Format Slack message | |
| id: slack_msg | |
| env: | |
| RELEASE_TAG: ${{ env.RELEASE_TAG }} | |
| WORKFLOW_URL: ${{ env.WORKFLOW_URL }} | |
| run: | | |
| text=$(./scripts/ci/format.sh slack) | |
| echo "text<<EOF" >> "$GITHUB_OUTPUT" | |
| echo "$text" >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| - name: Notify Slack | |
| uses: slackapi/slack-github-action@v2.0.0 | |
| with: | |
| webhook: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| webhook-type: webhook-trigger | |
| payload: | | |
| { | |
| "text": "${{ steps.slack_msg.outputs.text }}" | |
| } | |
| upload-release-artifacts: | |
| needs: | |
| [ | |
| set-metadata, | |
| release-create, | |
| build-macos, | |
| build-windows, | |
| build-linux, | |
| build-android, | |
| build-ios, | |
| ] | |
| # Run if at least one platform build succeeded. | |
| if: | | |
| !cancelled() && | |
| needs.release-create.result == 'success' && | |
| (needs.build-macos.result == 'success' || needs.build-windows.result == 'success' || | |
| needs.build-linux.result == 'success' || needs.build-android.result == 'success' || | |
| needs.build-ios.result == 'success') | |
| runs-on: ubuntu-latest | |
| env: | |
| BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }} | |
| RELEASE_TAG: ${{ needs.set-metadata.outputs.release_tag }} | |
| INSTALLER_BASE_NAME: ${{ needs.set-metadata.outputs.installer_base_name }} | |
| PLATFORM: ${{ needs.set-metadata.outputs.platform }} | |
| LINUX_ARCH: ${{ needs.set-metadata.outputs.linux_arch }} | |
| BUCKET: ${{ vars.S3_RELEASES_BUCKET }} | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.sha }} | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| - name: Generate AI release notes | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| RELEASE_TAG: ${{ env.RELEASE_TAG }} | |
| BUILD_TYPE: ${{ env.BUILD_TYPE }} | |
| GITHUB_SHA: ${{ github.sha }} | |
| run: | | |
| # Generate AI-powered changelog from commit history | |
| ./scripts/ci/generate-release-notes.sh > ai_notes.md 2>ai_notes_err.log || true | |
| if [ -s ai_notes.md ]; then | |
| echo "AI release notes generated successfully" | |
| else | |
| echo "AI notes empty, will use format.sh fallback" | |
| cat ai_notes_err.log >&2 || true | |
| fi | |
| - name: Update release notes | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GITHUB_SHA: ${{ github.sha }} | |
| RELEASE_TAG: ${{ env.RELEASE_TAG }} | |
| run: | | |
| # Generate download links and metadata | |
| ./scripts/ci/format.sh release-notes > release_notes.md | |
| # Prepend AI changelog if available | |
| if [ -s ai_notes.md ]; then | |
| cat ai_notes.md > final_notes.md | |
| echo "" >> final_notes.md | |
| echo "---" >> final_notes.md | |
| echo "" >> final_notes.md | |
| cat release_notes.md >> final_notes.md | |
| else | |
| cp release_notes.md final_notes.md | |
| fi | |
| # Update the draft release with combined notes | |
| gh release edit "$RELEASE_TAG" --notes-file final_notes.md | |
| - name: Upload artifacts to GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # Construct full filename with build type (Makefile appends it) | |
| FULL_NAME="${INSTALLER_BASE_NAME}" | |
| [[ -n "$BUILD_TYPE" && "$BUILD_TYPE" != "production" ]] && FULL_NAME="${FULL_NAME}-${BUILD_TYPE}" | |
| upload_if_exists() { | |
| local file="$1" | |
| if [[ ! -f "$file" ]]; then | |
| return 0 | |
| fi | |
| echo "Uploading $file to GitHub release..." | |
| local deadline=$(( SECONDS + 600 )) | |
| local delay=5 attempt=0 | |
| while :; do | |
| attempt=$((attempt + 1)) | |
| if gh release upload "$RELEASE_TAG" "$file" --clobber; then | |
| echo "Uploaded $file on attempt $attempt" | |
| return 0 | |
| fi | |
| if [[ "$SECONDS" -ge "$deadline" ]]; then | |
| echo "Failed to upload $file after $attempt attempts (~10 min)" >&2 | |
| return 1 | |
| fi | |
| echo "Upload failed (attempt $attempt); retrying in ${delay}s..." >&2 | |
| sleep "$delay" | |
| delay=$(( delay * 2 )) | |
| [[ "$delay" -gt 60 ]] && delay=60 | |
| done | |
| } | |
| upload_if_exists "lantern-installer-dmg/${FULL_NAME}.dmg" | |
| upload_if_exists "lantern-installer-exe/${FULL_NAME}.exe" | |
| upload_if_exists "lantern-installer-apk/${FULL_NAME}.apk" | |
| upload_if_exists "lantern-installer-deb-amd64/${FULL_NAME}.deb" | |
| upload_if_exists "lantern-installer-rpm-amd64/${FULL_NAME}.rpm" | |
| upload_if_exists "lantern-installer-deb-arm64/${FULL_NAME}-arm64.deb" | |
| upload_if_exists "lantern-installer-rpm-arm64/${FULL_NAME}-arm64.rpm" | |
| upload_if_exists "lantern-installer-deb/${FULL_NAME}.deb" | |
| upload_if_exists "lantern-installer-rpm/${FULL_NAME}.rpm" | |
| upload_if_exists "lantern-installer-pkg-amd64/${FULL_NAME}.pkg.tar.zst" | |
| upload_if_exists "lantern-installer-pkg-arm64/${FULL_NAME}-arm64.pkg.tar.zst" | |
| upload_if_exists "lantern-installer-pkg/${FULL_NAME}.pkg.tar.zst" | |
| upload_if_exists "lantern-installer-ipa/${FULL_NAME}.ipa" | |
| # Post-publish steps (only for non-nightly production/beta builds) | |
| upload-google-play: | |
| needs: [set-metadata, build-android] | |
| if: | | |
| !cancelled() && | |
| needs.set-metadata.result == 'success' && | |
| needs.build-android.result == 'success' && | |
| (needs.set-metadata.outputs.platform == 'all' || contains(needs.set-metadata.outputs.platform, 'android')) && | |
| (needs.set-metadata.outputs.build_type == 'beta' || needs.set-metadata.outputs.build_type == 'production') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download AAB artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: lantern-installer-aab | |
| path: aab | |
| - name: Download mapping.txt | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: play-mapping | |
| path: play | |
| - name: Download native debug symbols | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: play-debug-symbols | |
| path: play | |
| - name: Pick Play track | |
| id: track | |
| run: | | |
| bt="${{ needs.set-metadata.outputs.build_type }}" | |
| if [ "$bt" = "beta" ]; then | |
| echo "track=beta" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "track=production" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Upload to Google Play | |
| uses: r0adkll/upload-google-play@v1.1.3 | |
| with: | |
| serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} | |
| packageName: org.getlantern.lantern | |
| releaseFiles: aab/*.aab | |
| track: ${{ steps.track.outputs.track }} | |
| status: completed | |
| mappingFile: play/mapping.txt | |
| debugSymbols: play/debug-symbols.zip | |
| upload-testflight: | |
| needs: [set-metadata, build-ios] | |
| if: | | |
| !cancelled() && | |
| needs.set-metadata.result == 'success' && | |
| needs.build-ios.result == 'success' && | |
| (needs.set-metadata.outputs.platform == 'all' || contains(needs.set-metadata.outputs.platform, 'ios')) && | |
| (needs.set-metadata.outputs.build_type == 'beta' || needs.set-metadata.outputs.build_type == 'production') | |
| runs-on: macos-14 | |
| steps: | |
| - name: Download iOS Artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: lantern-installer-ipa | |
| path: . | |
| - name: Construct IPA filename | |
| id: filename | |
| shell: bash | |
| run: | | |
| FULL_NAME="${{ needs.set-metadata.outputs.installer_base_name }}" | |
| BUILD_TYPE="${{ needs.set-metadata.outputs.build_type }}" | |
| [[ -n "$BUILD_TYPE" && "$BUILD_TYPE" != "production" ]] && FULL_NAME="${FULL_NAME}-${BUILD_TYPE}" | |
| echo "ipa=${FULL_NAME}.ipa" >> "$GITHUB_OUTPUT" | |
| - name: Upload to TestFlight | |
| uses: apple-actions/upload-testflight-build@v3.0.1 | |
| with: | |
| app-path: ${{ steps.filename.outputs.ipa }} | |
| issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }} | |
| api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }} | |
| api-private-key: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} | |
| update-appcast: | |
| needs: [set-metadata, release-finalize] | |
| if: | | |
| !cancelled() && | |
| needs.release-finalize.result == 'success' && | |
| (needs.set-metadata.outputs.platform == 'all' || | |
| contains(needs.set-metadata.outputs.platform, 'macos') || | |
| contains(needs.set-metadata.outputs.platform, 'windows') || | |
| contains(needs.set-metadata.outputs.platform, 'linux')) && | |
| needs.set-metadata.outputs.build_type != 'nightly' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Install Flutter | |
| uses: subosito/flutter-action@v2.22.0 | |
| with: | |
| channel: stable | |
| flutter-version-file: .github/flutter-version.yaml | |
| - name: Install Python dependencies | |
| run: python3 -m pip install -r scripts/requirements.txt | |
| - name: Update appcast.xml | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| GITHUB_TOKEN: ${{ secrets.CI_PRIVATE_REPOS_GH_TOKEN }} | |
| BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }} | |
| RELEASE_TAG: ${{ needs.set-metadata.outputs.release_tag }} | |
| BUCKET: ${{ vars.S3_RELEASES_BUCKET }} | |
| run: | | |
| # Strip 'v' prefix for S3 paths | |
| VERSION="${RELEASE_TAG#v}" | |
| python3 scripts/generate_appcast.py | |
| # appcast, like installers, always upload a versioned copy alongside "latest" | |
| aws s3 cp appcast.xml "s3://${BUCKET}/releases/${BUILD_TYPE}/${VERSION}/appcast.xml" --acl public-read | |
| aws s3 cp appcast.xml "s3://${BUCKET}/releases/${BUILD_TYPE}/latest/appcast.xml" --acl public-read | |
| if [[ "$BUILD_TYPE" == "production" ]]; then | |
| aws s3 cp appcast.xml "s3://${BUCKET}/releases/appcast.xml" --acl public-read | |
| fi | |
| # Upload appcast.xml to GitHub release, but | |
| # not git because we may be in detached HEAD | |
| gh release upload "$RELEASE_TAG" appcast.xml --clobber | |
| release-finalize: | |
| needs: | |
| [ | |
| set-metadata, | |
| upload-s3, | |
| upload-release-artifacts, | |
| upload-google-play, | |
| upload-testflight, | |
| ] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| env: | |
| RELEASE_TAG: ${{ needs.set-metadata.outputs.release_tag }} | |
| BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }} | |
| CLEANUP_ON_FAILURE: ${{ github.event.inputs.cleanup_on_failure != 'false' }} | |
| IS_TEST_RUN: ${{ needs.set-metadata.outputs.is_test_run }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.sha }} | |
| - name: Publish release on success | |
| if: | | |
| env.IS_TEST_RUN != 'true' && | |
| env.BUILD_TYPE != 'nightly' && | |
| needs.upload-s3.result == 'success' && | |
| needs.upload-release-artifacts.result == 'success' && | |
| (needs.upload-google-play.result == 'success' || needs.upload-google-play.result == 'skipped') && | |
| (needs.upload-testflight.result == 'success' || needs.upload-testflight.result == 'skipped') | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| echo "All steps succeeded - publishing draft release" | |
| gh release edit "$RELEASE_TAG" --draft=false | |
| - name: Delete draft release | |
| if: | | |
| env.IS_TEST_RUN == 'true' || | |
| env.BUILD_TYPE == 'nightly' || | |
| (env.CLEANUP_ON_FAILURE == 'true' && | |
| !(needs.upload-s3.result == 'success' && | |
| needs.upload-release-artifacts.result == 'success' && | |
| (needs.upload-google-play.result == 'success' || needs.upload-google-play.result == 'skipped') && | |
| (needs.upload-testflight.result == 'success' || needs.upload-testflight.result == 'skipped'))) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| if [[ "$IS_TEST_RUN" == "true" ]]; then | |
| echo "Test run from non-main branch - deleting draft release" | |
| elif [[ "$BUILD_TYPE" == "nightly" ]]; then | |
| echo "Nightly build - deleting draft release" | |
| else | |
| echo "Build failed or cancelled - cleaning up draft release" | |
| fi | |
| attempt=0 | |
| max_retries=5 | |
| while [ "$attempt" -lt "$max_retries" ]; do | |
| attempt=$((attempt + 1)) | |
| echo "Deleting release $RELEASE_TAG (attempt $attempt/$max_retries)..." | |
| if delete_output="$(gh release delete "$RELEASE_TAG" --yes 2>&1)"; then | |
| echo "$delete_output" | |
| echo "Release $RELEASE_TAG deleted" | |
| break | |
| fi | |
| echo "$delete_output" | |
| if echo "$delete_output" | grep -qiE "Reference does not exist|release not found"; then | |
| echo "Release cleanup already complete" | |
| break | |
| fi | |
| if [ "$attempt" -eq "$max_retries" ]; then | |
| echo "Failed to delete release after $max_retries attempts" | |
| exit 1 | |
| fi | |
| sleep 3 | |
| done | |
| - name: Report failure (cleanup disabled) | |
| if: | | |
| env.IS_TEST_RUN != 'true' && | |
| env.BUILD_TYPE != 'nightly' && | |
| env.CLEANUP_ON_FAILURE != 'true' && | |
| !(needs.upload-s3.result == 'success' && | |
| needs.upload-release-artifacts.result == 'success' && | |
| (needs.upload-google-play.result == 'success' || needs.upload-google-play.result == 'skipped') && | |
| (needs.upload-testflight.result == 'success' || needs.upload-testflight.result == 'skipped')) | |
| run: | | |
| echo "Build failed or cancelled - draft release preserved for inspection" | |
| echo "Draft: https://github.com/getlantern/lantern/releases/tag/$RELEASE_TAG" | |
| exit 1 |