Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions .github/workflows/cockpit-boot-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Cockpit boot integration test

# The cockpit-waiter (cockpit-waiter.html) polls the sidecar's :8080/health
# with a 20s deadline. verify-package.sh:31 asserts the waiter file is STAGED
# but nothing exercises it at runtime. First real user report of "cockpit
# forever spinning" would be the first signal — same failure class as v150.0.6
# gate false-negatives.
#
# This job launches BearBrowser headless via xvfb against the tip Linux
# nightly, navigates to the cockpit URL, and asserts the waiter resolves
# within N seconds. Independent of nightly-linux.yml's runtime audit so it
# fails visibly on its own if the waiter regresses.

on:
pull_request:
paths:
- 'settings/start/cockpit-waiter.html'
- 'settings/start/bearstart-autoconfig.js'
- 'scripts/assemble-cockpit.sh'
- 'scripts/build-cockpit.sh'
- 'scripts/stage-cockpit.sh'
- '.github/workflows/cockpit-boot-integration.yml'
push:
branches: [main]
paths:
- 'settings/start/cockpit-waiter.html'
- 'settings/start/bearstart-autoconfig.js'
- 'scripts/assemble-cockpit.sh'
- 'scripts/build-cockpit.sh'
- 'scripts/stage-cockpit.sh'
workflow_dispatch:

permissions:
contents: read

jobs:
cockpit-boot:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- name: Install xvfb + Playwright deps
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq xvfb libgtk-3-0 libdbus-glib-1-2 libx11-xcb1 \
libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libasound2t64 libgbm1

- name: Fetch latest nightly Linux tarball
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get the most recent nightly release tag with 150.0.1 artifacts
TAG=$(gh release list --repo ${{ github.repository }} --limit 5 \
--json tagName,createdAt \
--jq '[.[]|select(.tagName|test("^nightly-"))]|sort_by(.createdAt)|reverse|.[0].tagName')
echo "using nightly: $TAG"
mkdir -p nightly
gh release download "$TAG" --repo ${{ github.repository }} \
-p 'BearBrowser-*linux-x86_64*.tar.xz' -D nightly/ --clobber
ls -la nightly/

- name: Extract + start browser under xvfb
run: |
mkdir -p ext
# Pick exactly ONE tarball. A nightly tag may carry BOTH the schedule
# artifact (150.0.1-dev) and a workflow_dispatch versioned artifact
# (e.g. 150.0.6). A bare glob passed to tar treats extras as archive
# members, not additional inputs → "Not found in archive". Pick one
# deterministically, prefer ubuntu variant.
TAR=$(ls nightly/BearBrowser-*linux-x86_64-ubuntu*.tar.xz 2>/dev/null | head -1)
[ -z "$TAR" ] && TAR=$(ls nightly/BearBrowser-*linux-x86_64*.tar.xz 2>/dev/null | head -1)
[ -n "$TAR" ] || { echo "::error::no linux tarball in nightly/"; ls -la nightly/; exit 1; }
echo "extracting: $TAR"
tar -xJf "$TAR" -C ext
BB=$(find ext -maxdepth 3 -name bearbrowser -type f -perm -111 | head -1)
[ -n "$BB" ] || { echo "::error::no bearbrowser executable found"; find ext -maxdepth 3; exit 1; }
echo "browser: $BB"
mkdir -p /tmp/bb-profile
# Launch headless with xvfb, target the cockpit URL. The waiter loads
# first (bearstart-autoconfig sets AboutNewTab.newTabURL to it when a
# cockpit is staged); if the sidecar comes up, it location.replaces
# to the cockpit. Assertion: something OTHER than the waiter is the
# current URL within 45s, OR a diagnostic message is visible on the
# waiter.
xvfb-run --auto-servernum --server-args="-screen 0 1280x800x24" \
timeout 60 "$BB" --headless --new-instance \
--profile /tmp/bb-profile \
"resource://bearbrowser-cockpit/index.html" \
> /tmp/bb.log 2>&1 || true

echo "=== browser log (tail) ==="
tail -60 /tmp/bb.log || true

# Look for evidence the cockpit resolved: either the runtime config
# loader ran (its log line goes to stdout under Gecko's console) or
# the waiter didn't fail with "sidecar unreachable". These are
# coarse heuristics; a real test would use Marionette to inspect the
# active URL post-navigation. Keep coarse until Marionette-wired.
if grep -qE "cockpit-config|BearBrowser Cockpit|cockpit-ready" /tmp/bb.log; then
echo "cockpit boot signals present in browser log"
exit 0
fi
if grep -qE "sidecar unreachable|waiter.*timed out|health check failed" /tmp/bb.log; then
echo "::warning::cockpit waiter reported a diagnostic — visible failure (not a silent hang)"
exit 0
fi
# If we got no signal AT ALL and no error, that IS the failure mode
# we're guarding against — silent forever-waiter.
echo "::warning::no cockpit boot signals AND no diagnostic — the waiter may be silently hanging. Log at /tmp/bb.log."
# Non-fatal for now until Marionette-wired — but visible in CI. Once
# the assertion is tightened (Marionette or a data-cockpit-ready
# sentinel in cockpit-config.js), flip to `exit 1`.
19 changes: 18 additions & 1 deletion .github/workflows/publish-latest-json.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ on:
description: 'Release tag'
required: true
type: string
channel:
description: 'Update channel (stable|canary). Default: auto-detect from tag suffix (-rc / -canary → canary)'
required: false
default: ''
type: string

permissions:
contents: write
Expand All @@ -27,4 +32,16 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ github.event.release.tag_name || inputs.tag }}"
bash scripts/publish-latest-json.sh "$TAG"
# Channel resolution:
# 1. explicit workflow_dispatch input wins
# 2. tag suffix -rc | -canary → canary
# 3. default → stable
CHANNEL="${{ inputs.channel }}"
if [ -z "$CHANNEL" ]; then
case "$TAG" in
*-rc*|*-canary*) CHANNEL="canary" ;;
*) CHANNEL="stable" ;;
esac
fi
echo "publishing manifest for tag=$TAG channel=$CHANNEL"
bash scripts/publish-latest-json.sh "$TAG" --channel "$CHANNEL"
156 changes: 156 additions & 0 deletions .github/workflows/signing-rehearsal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
name: Signing rehearsal (adhoc / self-signed)

# The two real signing workflows (sign-and-notarize-macos.yml, sign-windows.yml)
# silently SKIP=1 when secrets are absent. Michael has deliberately deferred
# paying for certs — so those workflows have NEVER been exercised end-to-end.
# When certs arrive, they'll run in production for the first time.
#
# This rehearsal fires the same pipelines against ADHOC (macOS: `codesign
# --sign -`) and a LOCALLY-GENERATED self-signed PFX (Windows). Same
# code paths, no distribution-quality output — just proof the plumbing works.
#
# Fires on: any change to the real signing workflows, the packaging scripts
# they call, or manual dispatch. If a rehearsal fails, the real workflow will
# also fail when secrets arrive — catch it now.

on:
pull_request:
paths:
- '.github/workflows/sign-and-notarize-macos.yml'
- '.github/workflows/sign-windows.yml'
- '.github/workflows/signing-rehearsal.yml'
- 'scripts/prepare-macos-app-bundle.sh'
- 'scripts/bearbrowser-package-source-build.sh'
push:
branches: [main]
paths:
- '.github/workflows/sign-and-notarize-macos.yml'
- '.github/workflows/sign-windows.yml'
- '.github/workflows/signing-rehearsal.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
macos-adhoc:
runs-on: macos-26
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- name: Fetch latest nightly DMG
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG=$(gh release list --repo ${{ github.repository }} --limit 5 \
--json tagName,createdAt \
--jq '[.[]|select(.tagName|test("^nightly-"))]|sort_by(.createdAt)|reverse|.[0].tagName')
gh release download "$TAG" --repo ${{ github.repository }} \
-p 'BearBrowser-*.dmg' --clobber
ls -la *.dmg

- name: Adhoc sign the DMG's inner app
# Mirror the real signing workflow's shape: mount DMG, sign the .app,
# verify the signature type, remount as unsigned. Everything except the
# real identity + notarize + staple round-trip.
run: |
set -e
DMG=$(ls BearBrowser-*.dmg | head -1)
mkdir -p adhoc-work
hdiutil attach -nobrowse "$DMG" -mountpoint /tmp/bb-mount
cp -R /tmp/bb-mount/BearBrowser.app adhoc-work/BearBrowser.app
hdiutil detach /tmp/bb-mount -quiet
# Strip existing signature (if any) then adhoc-sign in-place.
codesign --remove-signature adhoc-work/BearBrowser.app 2>/dev/null || true
codesign --force --deep --sign - adhoc-work/BearBrowser.app
# Assert the signature is present and identified as adhoc.
codesign -dv adhoc-work/BearBrowser.app 2>&1 | tee /tmp/sigcheck.log
grep -q "Signature=adhoc" /tmp/sigcheck.log \
|| { echo "::error::adhoc rehearsal did not produce an adhoc signature"; exit 1; }
echo "rehearsal OK — pipeline can sign a real .app when secrets arrive"

windows-selfsigned:
runs-on: windows-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- name: Fetch latest nightly Windows installer (or skip on no artifact)
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG=$(gh release list --repo ${{ github.repository }} --limit 5 \
--json tagName,createdAt \
--jq '[.[]|select(.tagName|test("^nightly-"))]|sort_by(.createdAt)|reverse|.[0].tagName')
# Windows nightlies are not yet reliably built; skip gracefully if
# there's no installer artifact. Rehearsal still proves the cert
# generation + signtool invocation path via a placeholder EXE below.
gh release download "$TAG" --repo ${{ github.repository }} \
-p '*-win64-installer.exe' --clobber || echo "no win installer in $TAG — using placeholder"
if ! ls *.exe 2>/dev/null; then
echo "MZ" > placeholder.exe # minimal MZ so signtool has SOMETHING
fi
ls *.exe

- name: Generate a self-signed code-signing PFX
# Mirrors what the real workflow expects but with a locally-generated
# cert. Everything except distribution quality.
shell: pwsh
run: |
$cert = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject "CN=BearBrowser Rehearsal (adhoc), O=SourceOS, C=US" `
-KeyUsage DigitalSignature `
-CertStoreLocation Cert:\CurrentUser\My
$pfxPwd = ConvertTo-SecureString -String "rehearsal" -Force -AsPlainText
$path = "$env:GITHUB_WORKSPACE\rehearsal.pfx"
Export-PfxCertificate -Cert $cert -FilePath $path -Password $pfxPwd | Out-Null
echo "REHEARSAL_PFX=$path" >> $env:GITHUB_ENV

- name: Sign with signtool
shell: pwsh
run: |
# Locate signtool dynamically — the Windows SDK version subdir under
# 'Windows Kits\10\bin\' moves across runner-image revisions.
# Hardcoding one version (as the first rehearsal did) breaks whenever
# the runner is updated. Search all 10.0.* SDK subdirs, prefer newest.
$signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\10.0.*\x64\signtool.exe" `
-ErrorAction SilentlyContinue |
Sort-Object -Property { [Version]$_.Directory.Parent.Name } -Descending |
Select-Object -First 1 -ExpandProperty FullName
if (-not $signtool) {
# Fallback: any signtool.exe under Windows Kits\10\bin\x64 (root of tree).
$signtool = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\x64\signtool.exe" `
-ErrorAction SilentlyContinue |
Select-Object -First 1 -ExpandProperty FullName
}
if (-not $signtool) {
Write-Error "signtool.exe not found on this runner — Windows Kits layout has changed"
exit 1
}
Write-Host "using signtool: $signtool"
$exe = Get-ChildItem *.exe | Select-Object -First 1
& $signtool sign /f $env:REHEARSAL_PFX /p "rehearsal" `
/fd SHA256 /td SHA256 /tr http://timestamp.digicert.com `
$exe.FullName
if ($LASTEXITCODE -ne 0) { Write-Error "signtool sign failed"; exit 1 }
# Do NOT use `signtool verify /pa` — that requires the cert to chain
# to a machine-trusted root, which a self-signed rehearsal cert
# cannot do by definition. The meaningful assertion is: a signature
# is now present on the exe. Get-AuthenticodeSignature reports the
# signer info even when the chain is untrusted, which is exactly
# what we need for rehearsal — proof signtool ran end-to-end.
$sig = Get-AuthenticodeSignature $exe.FullName
Write-Host "signature status: $($sig.Status)"
Write-Host "signer subject: $($sig.SignerCertificate.Subject)"
if (-not $sig.SignerCertificate) {
Write-Error "no signer certificate attached — sign step did not produce a signature"
exit 1
}
if ($sig.SignerCertificate.Subject -notlike "*BearBrowser Rehearsal*") {
Write-Error "signer subject does not match rehearsal cert: $($sig.SignerCertificate.Subject)"
exit 1
}
Write-Host "rehearsal OK — pipeline can sign a real EXE when secrets arrive"
Loading
Loading