Nightly Release Build #6
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: Nightly Release Build | |
| # Daily UNSIGNED release builds of BrowserOS for all three platforms on | |
| # WarpBuild runners. Signing is intentionally not wired up yet; see | |
| # packages/browseros/build/docs/nightly-warpbuild-ci.md for the design and | |
| # the secret names that enable signing later. | |
| # | |
| # The pinned chromium checkout (packages/browseros/CHROMIUM_VERSION) is | |
| # cached post-`gclient sync`: | |
| # - Linux/macOS: WarpCache (WarpBuilds/cache, no size cap) | |
| # - Windows: R2 tarball via scripts/ci/r2_cache.py (WarpCache does | |
| # not support Windows runners; actions/cache caps at 10GB) | |
| on: | |
| schedule: | |
| # Midnight US Pacific (DST). The signed self-hosted macOS nightly runs | |
| # at 05:00 UTC; keep these apart so version-bump PRs don't race. | |
| - cron: "0 8 * * *" | |
| workflow_dispatch: | |
| inputs: | |
| platforms: | |
| description: Platforms to build | |
| type: choice | |
| default: all | |
| options: | |
| - all | |
| - linux | |
| - windows | |
| - macos | |
| publish_nightly: | |
| description: Update the rolling `nightly` prerelease (main only) | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: nightly-release | |
| cancel-in-progress: false | |
| jobs: | |
| plan: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.plan.outputs.matrix }} | |
| publish: ${{ steps.plan.outputs.publish }} | |
| steps: | |
| - name: Resolve build matrix and publish flag | |
| id: plan | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| PLATFORMS: ${{ inputs.platforms || 'all' }} | |
| PUBLISH_INPUT: ${{ inputs.publish_nightly }} | |
| REF_NAME: ${{ github.ref_name }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| run: | | |
| set -euo pipefail | |
| matrix='[ | |
| {"platform":"linux","arch":"x64","runner":"warp-ubuntu-2204-x64-32x","config":"release.linux.ci.yaml","timeout":660}, | |
| {"platform":"windows","arch":"x64","runner":"warp-windows-2025-x64-32x","config":"release.windows.ci.yaml","timeout":780}, | |
| {"platform":"macos","arch":"arm64","runner":"warp-macos-26-arm64-12x","config":"release.macos.arm64.ci.yaml","timeout":720} | |
| ]' | |
| if [ "$PLATFORMS" != "all" ]; then | |
| matrix="$(jq -c --arg p "$PLATFORMS" '[ .[] | select(.platform == $p) ]' <<<"$matrix")" | |
| else | |
| matrix="$(jq -c . <<<"$matrix")" | |
| fi | |
| publish="false" | |
| if [ "$REF_NAME" = "$DEFAULT_BRANCH" ]; then | |
| if [ "$EVENT_NAME" = "schedule" ] || [ "$PUBLISH_INPUT" = "true" ]; then | |
| publish="true" | |
| fi | |
| fi | |
| { | |
| echo "matrix=$matrix" | |
| echo "publish=$publish" | |
| } >> "$GITHUB_OUTPUT" | |
| # WarpBuild provisions runners on demand via webhook; when none arrive | |
| # (org runner-group policy, bad label, account issue) the build jobs sit | |
| # queued until GitHub discards them 24h later, holding the | |
| # nightly-release concurrency group all day. Fail fast instead — see the | |
| # troubleshooting section of | |
| # packages/browseros/build/docs/nightly-warpbuild-ci.md. | |
| queue-watchdog: | |
| needs: plan | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| actions: write | |
| steps: | |
| - name: Fail fast when no runner picks up the builds | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| deadline=$(( $(date +%s) + 20 * 60 )) | |
| failures=0 | |
| while :; do | |
| if jobs="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?per_page=100" \ | |
| --jq '[.jobs[] | select(.name | startswith("build (")) | {name, status}]')"; then | |
| failures=0 | |
| else | |
| failures=$(( failures + 1 )) | |
| if [ "$failures" -ge 3 ]; then | |
| echo "::error::queue-watchdog could not list this run's jobs (3 consecutive failures); builds are unwatched this run." | |
| exit 1 | |
| fi | |
| sleep 120 | |
| continue | |
| fi | |
| total="$(jq 'length' <<<"$jobs")" | |
| queued="$(jq '[.[] | select(.status == "queued")] | length' <<<"$jobs")" | |
| in_progress="$(jq '[.[] | select(.status == "in_progress")] | length' <<<"$jobs")" | |
| if [ "$total" -gt 0 ] && [ "$queued" -eq 0 ]; then | |
| echo "All $total build jobs picked up by runners." | |
| exit 0 | |
| fi | |
| if [ "$(date +%s)" -ge "$deadline" ]; then | |
| doc="packages/browseros/build/docs/nightly-warpbuild-ci.md" | |
| if [ "$total" -eq 0 ]; then | |
| echo "::error::queue-watchdog matched no 'build (' jobs — were the matrix job names changed? Fix the filter; not cancelling." | |
| exit 1 | |
| fi | |
| stuck="$(jq -r '[.[] | select(.status == "queued") | .name] | join(", ")' <<<"$jobs")" | |
| # Cancel only when nothing is live: a queued job is then the | |
| # only thing keeping the run (and concurrency group) pinned. | |
| if [ "$in_progress" -eq 0 ]; then | |
| echo "::error::No build job is running and these never left the queue: $stuck. Check the runner-group public-repo setting, runner labels, and the WarpBuild dashboard — see $doc. Cancelling the run to free the nightly-release concurrency group." | |
| gh api -X POST "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/cancel" | |
| else | |
| echo "::error::Still queued after 20 min: $stuck. In-progress builds keep running — see $doc." | |
| fi | |
| exit 1 | |
| fi | |
| sleep 120 | |
| done | |
| build: | |
| needs: plan | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.plan.outputs.matrix) }} | |
| name: build (${{ matrix.platform }}-${{ matrix.arch }}) | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: ${{ matrix.timeout }} | |
| defaults: | |
| run: | |
| shell: bash | |
| env: | |
| # Windows runners pipe stdout as cp1252, which crashes the emoji-heavy | |
| # build CLI with UnicodeEncodeError; force UTF-8 for python and all | |
| # its subprocesses (gclient, hooks). No-op on Linux/macOS. | |
| PYTHONUTF8: "1" | |
| steps: | |
| - uses: actions/checkout@v6 | |
| # v8.x.y releases exist but astral-sh publishes no floating `v8` | |
| # major tag — `@v8` is unresolvable and kills the job in Set up job. | |
| - uses: astral-sh/setup-uv@v8.2.0 | |
| - name: Resolve chromium pin and paths | |
| id: pin | |
| run: | | |
| set -euo pipefail | |
| . packages/browseros/CHROMIUM_VERSION | |
| version="$MAJOR.$MINOR.$BUILD.$PATCH" | |
| # Keep the checkout outside the workspace so actions/checkout | |
| # never touches it. pwd -W yields a native path on Windows. | |
| if [ "$RUNNER_OS" = "Windows" ]; then | |
| parent="$(cd "$GITHUB_WORKSPACE/.." && pwd -W)" | |
| else | |
| parent="$(cd "$GITHUB_WORKSPACE/.." && pwd)" | |
| fi | |
| { | |
| echo "version=$version" | |
| echo "cache_key=chromium-src-${{ matrix.platform }}-${{ matrix.arch }}-v1-$version" | |
| } >> "$GITHUB_OUTPUT" | |
| { | |
| echo "CHROMIUM_ROOT=$parent/chromium" | |
| echo "CHROMIUM_SRC=$parent/chromium/src" | |
| } >> "$GITHUB_ENV" | |
| - name: Restore chromium checkout (WarpCache) | |
| if: runner.os != 'Windows' | |
| id: warpcache | |
| uses: WarpBuilds/cache/restore@v1 | |
| with: | |
| path: ${{ env.CHROMIUM_ROOT }} | |
| key: ${{ steps.pin.outputs.cache_key }} | |
| restore-keys: | | |
| chromium-src-${{ matrix.platform }}-${{ matrix.arch }}-v1- | |
| - name: Restore chromium checkout (R2) | |
| if: runner.os == 'Windows' | |
| id: r2cache | |
| env: | |
| R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} | |
| R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| R2_BUCKET: ${{ secrets.R2_BUCKET }} | |
| run: | | |
| set -euo pipefail | |
| uv run --project packages/browseros python scripts/ci/r2_cache.py restore \ | |
| --key "${{ steps.pin.outputs.cache_key }}" \ | |
| --root "$CHROMIUM_ROOT" | |
| - name: Ensure chromium checkout at pinned tag | |
| run: | | |
| set -euo pipefail | |
| uv run --project packages/browseros python scripts/ci/setup_chromium.py \ | |
| --chromium-root "$CHROMIUM_ROOT" --step checkout | |
| - name: Reset chromium tree (clean module) | |
| working-directory: packages/browseros | |
| run: | | |
| set -euo pipefail | |
| uv run browseros build --modules clean \ | |
| --chromium-src "$CHROMIUM_SRC" \ | |
| --build-type release \ | |
| --arch "${{ matrix.arch }}" | |
| - name: Sync chromium dependencies (gclient) | |
| run: | | |
| set -euo pipefail | |
| uv run --project packages/browseros python scripts/ci/setup_chromium.py \ | |
| --chromium-root "$CHROMIUM_ROOT" --step sync | |
| # Save immediately after sync: the tree is pristine (no BrowserOS | |
| # patches, no out/ dir), which keeps the cache deterministic and as | |
| # small as possible. | |
| - name: Save chromium checkout (WarpCache) | |
| if: runner.os != 'Windows' && steps.warpcache.outputs.cache-hit != 'true' | |
| uses: WarpBuilds/cache/save@v1 | |
| with: | |
| path: ${{ env.CHROMIUM_ROOT }} | |
| key: ${{ steps.pin.outputs.cache_key }} | |
| - name: Save chromium checkout (R2) | |
| if: runner.os == 'Windows' && steps.r2cache.outputs.cache-hit != 'true' | |
| env: | |
| R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} | |
| R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| R2_BUCKET: ${{ secrets.R2_BUCKET }} | |
| run: | | |
| set -euo pipefail | |
| uv run --project packages/browseros python scripts/ci/r2_cache.py save \ | |
| --key "${{ steps.pin.outputs.cache_key }}" \ | |
| --root "$CHROMIUM_ROOT" | |
| - name: Install Linux build deps | |
| if: matrix.platform == 'linux' | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update | |
| # libfuse2: appimagetool is itself an AppImage and needs FUSE | |
| sudo apt-get install -y libfuse2 | |
| "$CHROMIUM_SRC/build/install-build-deps.sh" --no-prompt | |
| - name: Build BrowserOS (unsigned) | |
| working-directory: packages/browseros | |
| env: | |
| # download_resources pulls BrowserOS Server bundles from R2 | |
| R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} | |
| R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| R2_BUCKET: ${{ secrets.R2_BUCKET }} | |
| # --- Signing placeholders (unused until signing is wired up) --- | |
| # macOS (sign_macos + notarization): | |
| # MACOS_CERTIFICATE_P12 / MACOS_CERTIFICATE_PWD (import into a CI keychain) | |
| # MACOS_CERTIFICATE_NAME, MACOS_KEYCHAIN_PASSWORD | |
| # PROD_MACOS_NOTARIZATION_APPLE_ID / _TEAM_ID / _PWD | |
| # SPARKLE_PRIVATE_KEY (sparkle_sign) | |
| # Windows (sign_windows via SSL.com CodeSignTool): | |
| # ESIGNER_USERNAME / ESIGNER_PASSWORD / ESIGNER_TOTP_SECRET | |
| # ESIGNER_CREDENTIAL_ID, CODE_SIGN_TOOL_PATH (tool install dir) | |
| run: | | |
| set -euo pipefail | |
| uv run browseros build \ | |
| --config "build/config/${{ matrix.config }}" \ | |
| --chromium-src "$CHROMIUM_SRC" | |
| - name: Report disk usage | |
| if: always() | |
| run: df -h || true | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: browseros-nightly-${{ matrix.platform }}-${{ matrix.arch }} | |
| if-no-files-found: error | |
| retention-days: 14 | |
| compression-level: 0 | |
| path: | | |
| packages/browseros/releases/*/*.dmg | |
| packages/browseros/releases/*/*.AppImage | |
| packages/browseros/releases/*/*.deb | |
| packages/browseros/releases/*/*_installer.exe | |
| packages/browseros/releases/*/*_installer.zip | |
| publish: | |
| needs: [plan, build] | |
| if: needs.plan.outputs.publish == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: dist | |
| merge-multiple: true | |
| - name: Update rolling nightly prerelease | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| files=(dist/*/*.dmg dist/*/*.AppImage dist/*/*.deb dist/*/*_installer.exe dist/*/*_installer.zip dist/*.dmg dist/*.AppImage dist/*.deb dist/*_installer.exe dist/*_installer.zip) | |
| if [ "${#files[@]}" -eq 0 ]; then | |
| echo "::error::No artifacts found to publish" | |
| exit 1 | |
| fi | |
| printf 'Publishing:\n%s\n' "${files[@]}" | |
| notes_file="$(mktemp)" | |
| cat > "$notes_file" <<EOF | |
| Automated unsigned nightly build from \`main\` (commit $GITHUB_SHA). | |
| These artifacts are NOT code signed or notarized — expect OS warnings on install. For signed builds use the regular releases. | |
| EOF | |
| # The `nightly` tag is rolling: recreate it via the API each run | |
| # (release delete + create; no git force-push involved). | |
| gh release delete nightly --cleanup-tag --yes || true | |
| gh release create nightly \ | |
| --prerelease \ | |
| --latest=false \ | |
| --target "$GITHUB_SHA" \ | |
| --title "BrowserOS Nightly (unsigned) $(date -u +%Y-%m-%d)" \ | |
| --notes-file "$notes_file" \ | |
| "${files[@]}" |