From 6048487272df3ece1a2a0d7db8318dea0be121ed Mon Sep 17 00:00:00 2001 From: atavism Date: Thu, 30 Jul 2026 11:53:39 -0700 Subject: [PATCH 1/2] ci: gate nightly releases on smoke tests --- .github/workflows/build-linux.yml | 42 +-- .github/workflows/build-windows.yml | 32 +- .github/workflows/go.yml | 2 +- .github/workflows/release.yml | 526 +++++++++++++++++++++++----- 4 files changed, 476 insertions(+), 126 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index c9edf0c3cb..a84626a7ab 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -184,6 +184,27 @@ jobs: run: | ./scripts/ci/verify_linux_package.sh "./${{ env.FULL_INSTALLER_NAME }}.deb" "${{ matrix.arch }}" + - name: Upload Linux RPM candidate + uses: actions/upload-artifact@v4 + with: + name: lantern-installer-rpm-${{ matrix.arch }} + path: ${{ env.FULL_INSTALLER_NAME }}.rpm + retention-days: 2 + + - name: Upload Linux DEB candidate + uses: actions/upload-artifact@v4 + with: + name: lantern-installer-deb-${{ matrix.arch }} + path: ${{ env.FULL_INSTALLER_NAME }}.deb + retention-days: 2 + + - name: Upload Linux Arch candidate + uses: actions/upload-artifact@v4 + with: + name: lantern-installer-pkg-${{ matrix.arch }} + path: ${{ env.FULL_INSTALLER_NAME }}.pkg.tar.zst + retention-days: 2 + - name: Install .deb and verify postinst started daemon shell: bash run: | @@ -295,24 +316,3 @@ jobs: if [[ "$AUTH_SMOKE_EXIT" -ne 0 ]]; then exit "$AUTH_SMOKE_EXIT" fi - - - name: Upload Linux build - uses: actions/upload-artifact@v4 - with: - name: lantern-installer-rpm-${{ matrix.arch }} - path: ${{ env.FULL_INSTALLER_NAME }}.rpm - retention-days: 2 - - - name: Upload Linux build - uses: actions/upload-artifact@v4 - with: - name: lantern-installer-deb-${{ matrix.arch }} - path: ${{ env.FULL_INSTALLER_NAME }}.deb - retention-days: 2 - - - name: Upload Linux build (Arch) - uses: actions/upload-artifact@v4 - with: - name: lantern-installer-pkg-${{ matrix.arch }} - path: ${{ env.FULL_INSTALLER_NAME }}.pkg.tar.zst - retention-days: 2 diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index d2f5be017a..cc2291ed3e 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -287,6 +287,21 @@ jobs: Move-Item "dist/$env:APP_VERSION/$env:APP_NAME-$env:APP_VERSION-windows-setup.exe" "$env:FULL_INSTALLER_NAME.exe" + - name: Sign installer + if: ${{ !inputs.skip_signing }} + shell: pwsh + env: + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} + FULL_INSTALLER_NAME: ${{ inputs.installer_base_name }}${{ inputs.build_type != 'production' && format('-{0}', inputs.build_type) || '' }} + run: | + ./scripts/ci/sign-windows.ps1 ` + -FilePath "$env:FULL_INSTALLER_NAME.exe" ` + -SigningPolicy "${{ env.SIGNPATH_SIGNING_POLICY }}" ` + -OrganizationId "${{ vars.SIGNPATH_ORG_ID }}" ` + -ProjectSlug "${{ vars.SIGNPATH_PROJECT_SLUG }}" ` + -ApiToken $env:SIGNPATH_API_TOKEN ` + -Description "Installer - GitHub Actions build ${{ inputs.version }}" + - name: Windows installer smoke suite shell: pwsh timeout-minutes: 30 @@ -320,24 +335,11 @@ jobs: run: | flutter test integration_test/auth/auth_smoke_test.dart -d windows --reporter=expanded --dart-define=DISABLE_SYSTEM_TRAY=true - - name: Sign installer - if: ${{ !inputs.skip_signing }} - shell: pwsh - env: - SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} - FULL_INSTALLER_NAME: ${{ inputs.installer_base_name }}${{ inputs.build_type != 'production' && format('-{0}', inputs.build_type) || '' }} - run: | - ./scripts/ci/sign-windows.ps1 ` - -FilePath "$env:FULL_INSTALLER_NAME.exe" ` - -SigningPolicy "${{ env.SIGNPATH_SIGNING_POLICY }}" ` - -OrganizationId "${{ vars.SIGNPATH_ORG_ID }}" ` - -ProjectSlug "${{ vars.SIGNPATH_PROJECT_SLUG }}" ` - -ApiToken $env:SIGNPATH_API_TOKEN ` - -Description "Installer - GitHub Actions build ${{ inputs.version }}" - - name: Upload Windows installer + if: ${{ always() }} uses: actions/upload-artifact@v4 with: name: lantern-installer-exe path: ${{ inputs.installer_base_name }}${{ inputs.build_type != 'production' && format('-{0}', inputs.build_type) || '' }}.exe + if-no-files-found: warn retention-days: 2 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 24b076957f..bace68dfce 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -32,7 +32,7 @@ jobs: run: go version - name: Cache Go modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/Library/Caches/go-build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba030cfcec..6c0fc0d927 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,6 +71,7 @@ env: NIGHTLY_GOOGLE_PLAY_TRACK: ${{ vars.NIGHTLY_GOOGLE_PLAY_TRACK || 'internal' }} permissions: + actions: read contents: write id-token: "write" @@ -242,6 +243,35 @@ jobs: ;; esac + case "$PLATFORM" in + all) ;; + *) + normalized_platforms=() + declare -A seen_platforms=() + IFS=',' read -ra requested_platforms <<< "$PLATFORM" + for requested_platform in "${requested_platforms[@]}"; do + requested_platform="${requested_platform//[[:space:]]/}" + case "$requested_platform" in + macos|windows|linux|android|ios) ;; + *) + echo "Error: invalid platform '$requested_platform'" >&2 + echo "Use 'all' or a comma-separated list of macos,windows,linux,android,ios." >&2 + exit 1 + ;; + esac + if [[ -z "${seen_platforms[$requested_platform]:-}" ]]; then + normalized_platforms+=("$requested_platform") + seen_platforms[$requested_platform]=1 + fi + done + if [[ "${#normalized_platforms[@]}" -eq 0 ]]; then + echo "Error: at least one platform is required" >&2 + exit 1 + fi + PLATFORM="$(IFS=,; echo "${normalized_platforms[*]}")" + ;; + esac + case "$LINUX_ARCH" in all|amd64|arm64) ;; *) @@ -338,9 +368,81 @@ jobs: name: pubspec path: pubspec.yaml - release-notify: + preflight: + name: Release preflight needs: [set-metadata] + if: ${{ !cancelled() && needs.set-metadata.result == 'success' }} + runs-on: ubuntu-24.04 + timeout-minutes: 25 + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + + - name: Validate GitHub Actions workflows + uses: docker://rhysd/actionlint:1.7.12 + with: + args: -color + + - name: Check staging API availability + if: ${{ needs.set-metadata.outputs.build_type == 'nightly' }} + shell: bash + run: | + set -euo pipefail + url="https://api.staging.iantem.io/v1/" + for attempt in 1 2 3; do + status="$(curl --silent --show-error --output /dev/null \ + --write-out '%{http_code}' --connect-timeout 10 --max-time 20 \ + "$url" || true)" + # A 4xx still proves that DNS, TLS, and the staging API route are up. + # The installed-app smoke validates authenticated behavior later. + if [[ "$status" =~ ^[234][0-9][0-9]$ ]]; then + echo "Staging API is reachable (HTTP $status)." + exit 0 + fi + echo "Staging API probe $attempt/3 failed (HTTP ${status:-000})." >&2 + if [[ "$attempt" -lt 3 ]]; then + sleep 5 + fi + done + echo "Staging API is unavailable; stopping before candidate builds." >&2 + exit 1 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Verify Go dependencies + run: | + go mod download + go mod verify + + - name: Install Flutter + uses: subosito/flutter-action@v2.22.0 + with: + channel: stable + flutter-version-file: .github/flutter-version.yaml + cache: true + + - name: Resolve Flutter dependencies and generated sources + run: | + make pubget + make gen + + - name: Run Flutter unit and widget tests + run: | + touch app.env + flutter test --no-pub + + release-notify: + needs: [set-metadata, preflight] if: | + needs.preflight.result == 'success' && needs.set-metadata.outputs.build_type == 'production' && needs.set-metadata.outputs.platform == 'all' runs-on: ubuntu-latest @@ -356,8 +458,9 @@ jobs: } release-approval: - needs: [set-metadata] + needs: [set-metadata, preflight] if: | + needs.preflight.result == 'success' && needs.set-metadata.outputs.build_type == 'production' && needs.set-metadata.outputs.platform == 'all' runs-on: ubuntu-latest @@ -454,7 +557,8 @@ jobs: stealth_leakage_mode: ${{ needs.set-metadata.outputs.stealth_leakage_mode }} release-create: - needs: set-metadata + needs: [set-metadata, preflight] + if: ${{ !cancelled() && needs.set-metadata.result == 'success' && needs.preflight.result == 'success' }} runs-on: ubuntu-latest env: BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }} @@ -495,40 +599,104 @@ jobs: --title "$TITLE" \ --notes "Build [in progress](${WORKFLOW_URL})..." + nightly-gate: + name: Candidate publication gate + needs: + [ + set-metadata, + preflight, + release-create, + build-macos, + build-windows, + build-linux, + build-android, + build-ios, + ] + if: ${{ always() && !cancelled() }} + runs-on: ubuntu-latest + env: + PLATFORMS: ${{ needs.set-metadata.outputs.platform }} + METADATA_RESULT: ${{ needs.set-metadata.result }} + PREFLIGHT_RESULT: ${{ needs.preflight.result }} + RELEASE_CREATE_RESULT: ${{ needs.release-create.result }} + MACOS_RESULT: ${{ needs.build-macos.result }} + WINDOWS_RESULT: ${{ needs.build-windows.result }} + LINUX_RESULT: ${{ needs.build-linux.result }} + ANDROID_RESULT: ${{ needs.build-android.result }} + IOS_RESULT: ${{ needs.build-ios.result }} + steps: + - name: Require every requested candidate to pass + shell: bash + run: | + set -euo pipefail + + failures=() + if [[ "$METADATA_RESULT" != "success" ]]; then + failures+=("release metadata: $METADATA_RESULT") + fi + if [[ "$PREFLIGHT_RESULT" != "success" ]]; then + failures+=("preflight: $PREFLIGHT_RESULT") + fi + if [[ "$RELEASE_CREATE_RESULT" != "success" ]]; then + failures+=("draft release: $RELEASE_CREATE_RESULT") + fi + + platform_requested() { + local platform="$1" + [[ "$PLATFORMS" == "all" || ",$PLATFORMS," == *",$platform,"* ]] + } + + check_candidate() { + local platform="$1" + local result="$2" + if platform_requested "$platform" && [[ "$result" != "success" ]]; then + failures+=("$platform candidate and smoke suite: $result") + fi + } + + check_candidate macos "$MACOS_RESULT" + check_candidate windows "$WINDOWS_RESULT" + check_candidate linux "$LINUX_RESULT" + check_candidate android "$ANDROID_RESULT" + check_candidate ios "$IOS_RESULT" + + { + echo "## Candidate gate" + echo + echo "**Requested platforms:** \`${PLATFORMS:-unknown}\`" + echo + } >> "$GITHUB_STEP_SUMMARY" + + if [[ "${#failures[@]}" -gt 0 ]]; then + { + echo "Publication is blocked:" + for failure in "${failures[@]}"; do + echo "- $failure" + done + } | tee -a "$GITHUB_STEP_SUMMARY" + exit 1 + fi + + echo "Every requested candidate and required smoke suite passed." | + tee -a "$GITHUB_STEP_SUMMARY" + upload-s3: needs: [ set-metadata, + nightly-gate, 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') - ) - ) + needs.set-metadata.result == 'success' && + needs.set-metadata.outputs.is_test_run != 'true' && + needs.nightly-gate.result == 'success' runs-on: ubuntu-latest env: BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }} @@ -538,7 +706,6 @@ jobs: 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 @@ -548,8 +715,7 @@ jobs: - name: Download build artifacts uses: actions/download-artifact@v4 - - name: Resolve nightly publish scope - if: ${{ env.BUILD_TYPE == 'nightly' }} + - name: Verify candidate artifact set shell: bash run: | set -euo pipefail @@ -558,48 +724,58 @@ jobs: 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") + platform_requested() { + local platform="$1" + [[ "$PLATFORM" == "all" || ",$PLATFORM," == *",$platform,"* ]] + } + + require_artifact() { + local label="$1" + shift + for pattern in "$@"; do + if has_artifact "$pattern"; then + return 0 + fi + done + echo "Missing candidate artifact: $label" >&2 + return 1 + } + + missing=0 + if platform_requested macos; then + require_artifact "macOS DMG" "lantern-installer-dmg/*.dmg" || missing=1 fi - if has_artifact "lantern-installer-ipa/*.ipa"; then - platforms+=("ios") + if platform_requested windows; then + require_artifact "Windows installer" "lantern-installer-exe/*.exe" || missing=1 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 + if platform_requested android; then + require_artifact "Android APK" "lantern-installer-apk/*.apk" || missing=1 fi - if has_artifact "lantern-installer-deb-arm64/*.deb" || has_artifact "lantern-installer-rpm-arm64/*.rpm"; then - linux_arm64=true + if platform_requested ios; then + require_artifact "iOS IPA" "lantern-installer-ipa/*.ipa" || missing=1 fi - if [[ "$linux_amd64" == true || "$linux_arm64" == true ]]; then - platforms+=("linux") + if platform_requested linux; then + if [[ "$LINUX_ARCH" == "all" || "$LINUX_ARCH" == "amd64" ]]; then + require_artifact "Linux AMD64 DEB" \ + "lantern-installer-deb-amd64/*.deb" "lantern-installer-deb/*.deb" || missing=1 + require_artifact "Linux AMD64 RPM" \ + "lantern-installer-rpm-amd64/*.rpm" "lantern-installer-rpm/*.rpm" || missing=1 + require_artifact "Linux AMD64 Arch package" \ + "lantern-installer-pkg-amd64/*.pkg.tar.zst" "lantern-installer-pkg/*.pkg.tar.zst" || missing=1 + fi + if [[ "$LINUX_ARCH" == "all" || "$LINUX_ARCH" == "arm64" ]]; then + require_artifact "Linux ARM64 DEB" "lantern-installer-deb-arm64/*.deb" || missing=1 + require_artifact "Linux ARM64 RPM" "lantern-installer-rpm-arm64/*.rpm" || missing=1 + require_artifact "Linux ARM64 Arch package" "lantern-installer-pkg-arm64/*.pkg.tar.zst" || missing=1 + fi fi - if [[ "${#platforms[@]}" -eq 0 ]]; then - echo "No publishable nightly artifacts were found." >&2 + if [[ "$missing" -ne 0 ]]; then + echo "Candidate artifacts are incomplete; publication is blocked." >&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" + echo "All requested candidate artifacts are present." - name: Upload to S3 shell: bash @@ -622,45 +798,24 @@ jobs: 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<> "$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, + nightly-gate, build-macos, build-windows, build-linux, build-android, build-ios, ] - # Run if at least one platform build succeeded. if: | + always() && !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') + needs.set-metadata.outputs.is_test_run != 'true' && + needs.nightly-gate.result == 'success' runs-on: ubuntu-latest env: BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }} @@ -770,10 +925,11 @@ jobs: # Post-publish steps. Production/beta always use their release tracks; nightly # mobile store uploads run only when set-metadata enables them. upload-google-play: - needs: [set-metadata, build-android] + needs: [set-metadata, nightly-gate, build-android] if: | !cancelled() && needs.set-metadata.result == 'success' && + needs.nightly-gate.result == 'success' && needs.build-android.result == 'success' && (needs.set-metadata.outputs.platform == 'all' || contains(needs.set-metadata.outputs.platform, 'android')) && ( @@ -829,10 +985,11 @@ jobs: debugSymbols: play/debug-symbols.zip upload-testflight: - needs: [set-metadata, build-ios] + needs: [set-metadata, nightly-gate, build-ios] if: | !cancelled() && needs.set-metadata.result == 'success' && + needs.nightly-gate.result == 'success' && needs.build-ios.result == 'success' && (needs.set-metadata.outputs.platform == 'all' || contains(needs.set-metadata.outputs.platform, 'ios')) && ( @@ -869,6 +1026,7 @@ jobs: needs: [ set-metadata, + nightly-gate, upload-s3, upload-release-artifacts, upload-google-play, @@ -877,6 +1035,7 @@ jobs: if: | always() && !cancelled() && + needs.nightly-gate.result == 'success' && needs.set-metadata.outputs.is_test_run != 'true' && needs.set-metadata.outputs.build_type != 'nightly' && needs.upload-s3.result == 'success' && @@ -1001,6 +1160,7 @@ jobs: needs: [ set-metadata, + nightly-gate, upload-s3, upload-release-artifacts, upload-google-play, @@ -1024,6 +1184,7 @@ jobs: if: | env.IS_TEST_RUN != 'true' && env.BUILD_TYPE != 'nightly' && + needs.nightly-gate.result == 'success' && needs.upload-s3.result == 'success' && needs.upload-release-artifacts.result == 'success' && needs.publish-update-metadata.result == 'success' && @@ -1040,7 +1201,8 @@ jobs: env.IS_TEST_RUN == 'true' || env.BUILD_TYPE == 'nightly' || (env.CLEANUP_ON_FAILURE == 'true' && - !(needs.upload-s3.result == 'success' && + !(needs.nightly-gate.result == 'success' && + needs.upload-s3.result == 'success' && needs.upload-release-artifacts.result == 'success' && needs.publish-update-metadata.result == 'success' && (needs.upload-google-play.result == 'success' || needs.upload-google-play.result == 'skipped') && @@ -1083,7 +1245,8 @@ jobs: env.IS_TEST_RUN != 'true' && env.BUILD_TYPE != 'nightly' && env.CLEANUP_ON_FAILURE != 'true' && - !(needs.upload-s3.result == 'success' && + !(needs.nightly-gate.result == 'success' && + needs.upload-s3.result == 'success' && needs.upload-release-artifacts.result == 'success' && needs.publish-update-metadata.result == 'success' && (needs.upload-google-play.result == 'success' || needs.upload-google-play.result == 'skipped') && @@ -1092,3 +1255,188 @@ jobs: echo "Build failed or cancelled - draft release preserved for inspection" echo "Draft: https://github.com/getlantern/lantern/releases/tag/$RELEASE_TAG" exit 1 + + release-success-notify: + name: Notify successful publication + needs: + [ + set-metadata, + nightly-gate, + upload-s3, + upload-release-artifacts, + upload-google-play, + upload-testflight, + publish-update-metadata, + release-finalize, + ] + if: | + always() && + !cancelled() && + needs.set-metadata.result == 'success' && + needs.set-metadata.outputs.is_test_run != 'true' && + needs.nightly-gate.result == 'success' && + 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') && + ( + needs.publish-update-metadata.result == 'success' || + (needs.set-metadata.outputs.build_type == 'nightly' && needs.publish-update-metadata.result == 'skipped') + ) && + needs.release-finalize.result == 'success' + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + + - name: Format Slack message + id: slack-message + env: + RELEASE_TAG: ${{ needs.set-metadata.outputs.release_tag }} + INSTALLER_BASE_NAME: ${{ needs.set-metadata.outputs.installer_base_name }} + PLATFORM: ${{ needs.set-metadata.outputs.platform }} + BUCKET: ${{ vars.S3_RELEASES_BUCKET }} + BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }} + GITHUB_REF_NAME: ${{ github.ref_name }} + LINUX_ARCH: ${{ needs.set-metadata.outputs.linux_arch }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + shell: bash + run: | + text="$(./scripts/ci/format.sh slack)" + payload="$(jq --compact-output --null-input --arg text "$text" '{text: $text}')" + echo "payload=$payload" >> "$GITHUB_OUTPUT" + + - name: Notify Slack + uses: slackapi/slack-github-action@v2.0.0 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: webhook-trigger + payload: ${{ steps.slack-message.outputs.payload }} + + nightly-failure-notify: + name: Notify failed nightly + needs: + [ + set-metadata, + preflight, + build-macos, + build-windows, + build-linux, + build-android, + build-ios, + nightly-gate, + upload-s3, + upload-release-artifacts, + upload-google-play, + upload-testflight, + publish-update-metadata, + release-finalize, + ] + if: | + always() && + github.event_name == 'schedule' && + ( + needs.set-metadata.result != 'success' || + needs.preflight.result != 'success' || + needs.nightly-gate.result != 'success' || + 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') || + needs.release-finalize.result != 'success' + ) + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + steps: + - name: Build failure summary + id: failure-message + env: + GH_TOKEN: ${{ github.token }} + GATE_RESULT: ${{ needs.nightly-gate.result }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + shell: bash + run: | + set -euo pipefail + + if ! gh api \ + "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \ + > jobs.json; then + echo '{"jobs":[]}' > jobs.json + fi + + failed_checks="$(jq --raw-output ' + [ + .jobs[] + | select(.conclusion == "failure" or .conclusion == "cancelled") + | select(.name != "Notify failed nightly") + | . as $job + | ( + [.steps[]? | select(.conclusion == "failure") | .name] + | if length == 0 then ["job failed"] else . end + )[] + | "\($job.name): \(.)" + ] + | if length == 0 + then ["No failed step was reported; inspect the workflow run."] + else . + end + | join("\n• ") + ' jobs.json)" + + failure_text="$(printf '%s' "$failed_checks" | tr '[:upper:]' '[:lower:]')" + case "$failure_text" in + *staging*|*stripe*|*shepherd*|*payment*|*checkout*|*webview*) + category="staging or payment-provider availability" + ;; + *smoke*|*connect*|*installer*|*lanternd*|*daemon*) + category="candidate installation or app smoke" + ;; + *actionlint*|*workflow*|*flutter\ test*|*unit*|*widget*|*dependenc*) + category="preflight validation or dependencies" + ;; + *runner*|*setup*|*download*|*checkout\ code*) + category="runner or CI infrastructure" + ;; + *build*|*compile*|*package*|*sign*) + category="candidate build" + ;; + *) + category="release workflow" + ;; + esac + + if [[ "$GATE_RESULT" == "success" ]]; then + headline=":warning: Lantern nightly publication failed after the candidate gate." + publication="Some destinations may need verification before rerunning." + else + headline=":rotating_light: Lantern nightly blocked." + publication="No public nightly artifacts were published." + fi + + short_sha="${GITHUB_SHA:0:12}" + text="$(printf '%s\n*Commit:* `%s`\n*Branch:* `%s`\n*Failure type:* %s\n*Failed checks:*\n• %s\n*Publication:* %s\n*Run and diagnostics:* <%s|Open workflow logs and artifacts>' \ + "$headline" "$short_sha" "$GITHUB_REF_NAME" "$category" \ + "$failed_checks" "$publication" "$RUN_URL")" + + payload="$(jq --compact-output --null-input --arg text "$text" '{text: $text}')" + echo "payload=$payload" >> "$GITHUB_OUTPUT" + { + echo "## Nightly failure" + echo + echo "**Category:** $category" + echo + echo "$publication" + echo + echo "$failed_checks" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Notify Slack + uses: slackapi/slack-github-action@v2.0.0 + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL }} + webhook-type: webhook-trigger + payload: ${{ steps.failure-message.outputs.payload }} From 6d3565b5c3bc6d4903fec15b4063b22d3ccad388 Mon Sep 17 00:00:00 2001 From: atavism Date: Tue, 4 Aug 2026 17:00:46 -0700 Subject: [PATCH 2/2] code review updates --- .github/workflows/release.yml | 68 +++++++++++++++-------------------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c0fc0d927..f1789eb581 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,7 +71,6 @@ env: NIGHTLY_GOOGLE_PLAY_TRACK: ${{ vars.NIGHTLY_GOOGLE_PLAY_TRACK || 'internal' }} permissions: - actions: read contents: write id-token: "write" @@ -469,13 +468,13 @@ jobs: - run: echo "Approved" build-macos: - needs: [set-metadata, release-create, release-approval] + needs: [set-metadata, preflight, release-approval] uses: ./.github/workflows/build-macos.yml secrets: inherit if: | !cancelled() && needs.set-metadata.result == 'success' && - needs.release-create.result == 'success' && + needs.preflight.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: @@ -485,13 +484,13 @@ jobs: runner_label: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.use_self_hosted_macos == 'true' && 'lantern-macos-smoke' || 'macos-15' }} build-windows: - needs: [set-metadata, release-create, release-approval] + needs: [set-metadata, preflight, release-approval] uses: ./.github/workflows/build-windows.yml secrets: inherit if: | !cancelled() && needs.set-metadata.result == 'success' && - needs.release-create.result == 'success' && + needs.preflight.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: @@ -507,13 +506,13 @@ jobs: 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] + needs: [set-metadata, preflight, release-approval] uses: ./.github/workflows/build-linux.yml secrets: inherit if: | !cancelled() && needs.set-metadata.result == 'success' && - needs.release-create.result == 'success' && + needs.preflight.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: @@ -526,13 +525,13 @@ jobs: run_auth_smoke: false build-ios: - needs: [set-metadata, release-create, release-approval] + needs: [set-metadata, preflight, release-approval] uses: ./.github/workflows/build-ios.yml secrets: inherit if: | !cancelled() && needs.set-metadata.result == 'success' && - needs.release-create.result == 'success' && + needs.preflight.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: @@ -541,13 +540,13 @@ jobs: installer_base_name: ${{ needs.set-metadata.outputs.installer_base_name }} build-android: - needs: [set-metadata, release-create, release-approval] + needs: [set-metadata, preflight, release-approval] uses: ./.github/workflows/build-android.yml secrets: inherit if: | !cancelled() && needs.set-metadata.result == 'success' && - needs.release-create.result == 'success' && + needs.preflight.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: @@ -557,18 +556,14 @@ jobs: stealth_leakage_mode: ${{ needs.set-metadata.outputs.stealth_leakage_mode }} release-create: - needs: [set-metadata, preflight] - if: ${{ !cancelled() && needs.set-metadata.result == 'success' && needs.preflight.result == 'success' }} + needs: [set-metadata, nightly-gate] + if: ${{ !cancelled() && needs.set-metadata.result == 'success' && needs.nightly-gate.result == 'success' }} runs-on: ubuntu-latest env: BUILD_TYPE: ${{ needs.set-metadata.outputs.build_type }} RELEASE_TAG: ${{ needs.set-metadata.outputs.release_tag }} + GH_REPO: ${{ github.repository }} steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ github.sha }} - - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} @@ -597,7 +592,7 @@ jobs: --draft \ $PRERELEASE_FLAG \ --title "$TITLE" \ - --notes "Build [in progress](${WORKFLOW_URL})..." + --notes "Publication [in progress](${WORKFLOW_URL})..." nightly-gate: name: Candidate publication gate @@ -605,7 +600,6 @@ jobs: [ set-metadata, preflight, - release-create, build-macos, build-windows, build-linux, @@ -618,7 +612,6 @@ jobs: PLATFORMS: ${{ needs.set-metadata.outputs.platform }} METADATA_RESULT: ${{ needs.set-metadata.result }} PREFLIGHT_RESULT: ${{ needs.preflight.result }} - RELEASE_CREATE_RESULT: ${{ needs.release-create.result }} MACOS_RESULT: ${{ needs.build-macos.result }} WINDOWS_RESULT: ${{ needs.build-windows.result }} LINUX_RESULT: ${{ needs.build-linux.result }} @@ -637,10 +630,6 @@ jobs: if [[ "$PREFLIGHT_RESULT" != "success" ]]; then failures+=("preflight: $PREFLIGHT_RESULT") fi - if [[ "$RELEASE_CREATE_RESULT" != "success" ]]; then - failures+=("draft release: $RELEASE_CREATE_RESULT") - fi - platform_requested() { local platform="$1" [[ "$PLATFORMS" == "all" || ",$PLATFORMS," == *",$platform,"* ]] @@ -1160,6 +1149,7 @@ jobs: needs: [ set-metadata, + release-create, nightly-gate, upload-s3, upload-release-artifacts, @@ -1174,12 +1164,8 @@ jobs: 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 }} + GH_REPO: ${{ github.repository }} steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ github.sha }} - - name: Publish release on success if: | env.IS_TEST_RUN != 'true' && @@ -1198,15 +1184,18 @@ jobs: - name: Delete draft release if: | - env.IS_TEST_RUN == 'true' || - env.BUILD_TYPE == 'nightly' || - (env.CLEANUP_ON_FAILURE == 'true' && - !(needs.nightly-gate.result == 'success' && - needs.upload-s3.result == 'success' && - needs.upload-release-artifacts.result == 'success' && - needs.publish-update-metadata.result == 'success' && - (needs.upload-google-play.result == 'success' || needs.upload-google-play.result == 'skipped') && - (needs.upload-testflight.result == 'success' || needs.upload-testflight.result == 'skipped'))) + needs.release-create.result == 'success' && + ( + env.IS_TEST_RUN == 'true' || + env.BUILD_TYPE == 'nightly' || + (env.CLEANUP_ON_FAILURE == 'true' && + !(needs.nightly-gate.result == 'success' && + needs.upload-s3.result == 'success' && + needs.upload-release-artifacts.result == 'success' && + needs.publish-update-metadata.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: | @@ -1242,6 +1231,7 @@ jobs: - name: Report failure (cleanup disabled) if: | + needs.release-create.result == 'success' && env.IS_TEST_RUN != 'true' && env.BUILD_TYPE != 'nightly' && env.CLEANUP_ON_FAILURE != 'true' &&