Skip to content

Nightly Release Build #6

Nightly Release Build

Nightly Release Build #6

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[@]}"