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
148 changes: 146 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ jobs:
python3 tests/features/backup_restore.py
python3 tests/features/self_update.py
python3 tests/features/coolify_pull.py
python3 tests/features/email_interval.py
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Extension trust token is revoked from session storage
run: node --experimental-strip-types tests/features/email_trust_storage.mjs
- name: Login-name eligibility matches the server wrap rule
run: node --experimental-strip-types tests/features/login_name_eligibility.mjs
- name: Validate the pull-only compose file
env:
KOMBOE_SERVER_DIGEST: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Expand Down Expand Up @@ -45,6 +53,9 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
workspaces: server/upstream
- name: Server unit tests
working-directory: server/upstream
run: cargo test --features sqlite --bin vaultwarden -- never_and_window codes_expire email_is_not login_name_shape login_name_uses a_miss_is_stable restore_replaces
- uses: actions/setup-node@v4
with:
node-version: "22"
Expand All @@ -56,15 +67,116 @@ jobs:
working-directory: apps/web-marketing
env:
CARGO_BUILD_JOBS: "2"
- name: Timed email sign-in against the built server and a local mail sink
run: python3 tests/features/email_interval_flow.py
- name: Marketing sign-in keeps mixed-case salts and lowercased accounts
run: node tests/features/marketing_email_case.mjs
- name: New migrations apply on boot and survive a second boot
run: python3 tests/features/migrations_boot.py
- name: Signed-in vault, downloads page, and PIN enroll/unlock/failure
run: node tests/features/ui_proof.mjs
- uses: actions/upload-artifact@v4
if: always()
with:
name: marketing-screenshots
path: apps/web-marketing/e2e/screenshots/*

angular-vault:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.99.0
- run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev
- uses: Swatinem/rust-cache@v2
with:
workspaces: server/upstream
- name: Build the server the vault test logs into
working-directory: server/upstream
run: cargo build --features sqlite --bin vaultwarden
- uses: actions/setup-node@v4
with:
node-version: "24"
- name: Install the web vault dependencies
working-directory: clients/upstream
run: |
set -eu
npm install -g npm@11.6.2
npm ci --ignore-scripts
- name: Build the self-hosted web vault
working-directory: clients/upstream
env:
NODE_OPTIONS: --max-old-space-size=5120
run: npm run dist:oss:selfhost --workspace @bitwarden/web-vault
- name: Install Playwright
working-directory: apps/web-marketing
run: |
set -eu
npm ci
npx playwright install --with-deps chromium
- name: Unlock a real encrypted cipher with PIN and pattern
env:
KOMBOE_BIN: server/upstream/target/debug/vaultwarden
KOMBOE_SHOTS: ${{ github.workspace }}/angular-shots
run: node tests/features/angular_vault_unlock.mjs
- uses: actions/upload-artifact@v4
if: always()
with:
name: angular-vault-screenshots
path: angular-shots/*

extension:
# Builds the Chromium MV3 zip inside the existing clients image and uploads it.
# Does not push to GHCR and does not publish a store listing.
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build the clients image without pushing it
run: |
set -eu
docker buildx build \
--load \
--platform linux/amd64 \
--provenance=false \
--sbom=false \
-f docker/Dockerfile.clients \
--build-context komboe_root=. \
--build-arg KOMBOE_REVISION="${{ github.sha }}" \
-t komboe-clients:ci \
clients/upstream
cid=$(docker create komboe-clients:ci)
mkdir -p dist
docker cp "$cid:/opt/komboe-chrome-mv3.zip" dist/komboe-chrome-mv3.zip
docker cp "$cid:/src/apps/web/build/komboe-downloads.json" dist/komboe-downloads.json
docker rm "$cid"
python3 - << 'PY'
import hashlib, json
from pathlib import Path
blob = Path("dist/komboe-chrome-mv3.zip").read_bytes()
manifest = json.loads(Path("dist/komboe-downloads.json").read_text())
if manifest.get("revision") != "${{ github.sha }}":
raise SystemExit(f"manifest revision is not this commit: {manifest.get('revision')}")
asset = next(item for item in manifest["assets"] if item["id"] == "extension-chromium-mv3")
digest = hashlib.sha256(blob).hexdigest()
if asset["sha256"] != digest or asset["status"] != "published":
raise SystemExit(f"manifest does not match the zip: {asset}")
for other in manifest["assets"]:
if other["id"] == "extension-chromium-mv3":
continue
if other["sha256"] or other["url"] or other["status"] == "published":
raise SystemExit(f"clients image manifest includes a file this image did not build: {other['id']}")
print("extension", digest, asset["size"])
PY
- uses: actions/upload-artifact@v4
with:
name: komboe-chrome-mv3
path: dist/*

image:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: checks
needs: [checks, marketing, angular-vault, extension]
permissions:
contents: read
packages: write
Expand Down Expand Up @@ -109,7 +221,39 @@ jobs:
echo "${name}_digest=${hex}" >> "$GITHUB_OUTPUT"
}
publish_one komboe-server -f docker/Dockerfile.server .
publish_one komboe-clients -f docker/Dockerfile.clients --build-context komboe_root=. clients/upstream
docker buildx build \
--load \
--platform linux/amd64 \
--provenance=false \
--sbom=false \
-f docker/Dockerfile.clients \
--build-context komboe_root=. \
--build-arg KOMBOE_REVISION="${IMAGE_SHA}" \
-t komboe-clients:validate \
clients/upstream
cid=$(docker create komboe-clients:validate)
mkdir -p dist
docker cp "$cid:/src/apps/web/build/komboe-downloads.json" dist/komboe-downloads.json
docker cp "$cid:/opt/komboe-chrome-mv3.zip" dist/komboe-chrome-mv3.zip
docker rm "$cid"
EXPECTED_REVISION="${IMAGE_SHA}" python3 - << 'PY'
import hashlib, json, os
from pathlib import Path
manifest = json.loads(Path("dist/komboe-downloads.json").read_text())
if manifest.get("revision") != os.environ["EXPECTED_REVISION"]:
raise SystemExit(f"published clients image stamped {manifest.get('revision')}")
blob = Path("dist/komboe-chrome-mv3.zip").read_bytes()
asset = next(item for item in manifest["assets"] if item["id"] == "extension-chromium-mv3")
if asset["sha256"] != hashlib.sha256(blob).hexdigest() or asset["status"] != "published":
raise SystemExit(f"published image manifest does not match the zip: {asset}")
for other in manifest["assets"]:
if other["id"] == "extension-chromium-mv3":
continue
if other["sha256"] or other["url"] or other["status"] == "published":
raise SystemExit(f"published image manifest includes a file this image did not build: {other['id']}")
print("clients image revision", manifest["revision"])
PY
publish_one komboe-clients -f docker/Dockerfile.clients --build-arg KOMBOE_REVISION="${IMAGE_SHA}" --build-context komboe_root=. clients/upstream
for name in komboe-server komboe-clients; do
gh api --method PUT "/orgs/${GITHUB_REPOSITORY_OWNER}/packages/container/${name}" -f visibility=public \
|| echo "package ${name} stayed private; Coolify needs a read token to pull it"
Expand Down
159 changes: 159 additions & 0 deletions .github/workflows/client-builds.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
name: client-builds

on:
pull_request:
workflow_dispatch:

jobs:
desktop-linux:
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
- name: Install Electron native libraries
run: sudo apt-get update && sudo apt-get install -y pkg-config libsecret-1-dev libxss1 rpm fakeroot dpkg
- name: Build an unsigned Linux desktop package
working-directory: clients/upstream
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
run: |
set -eu
npm install -g npm@11.6.2
npm ci
npm run build:oss --workspace @bitwarden/desktop
cd apps/desktop
npx electron-builder --linux deb AppImage --x64 -p never
ls -l dist
- uses: actions/upload-artifact@v4
with:
name: desktop-linux-unsigned
path: |
clients/upstream/apps/desktop/dist/*.deb
clients/upstream/apps/desktop/dist/*.AppImage

desktop-windows:
runs-on: windows-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
- name: Build an unsigned Windows desktop package
working-directory: clients/upstream
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
run: |
npm install -g npm@11.6.2
npm ci
npm run build:oss --workspace @bitwarden/desktop
cd apps/desktop
npx electron-builder --win nsis --x64 -p never --config.win.target=nsis
- uses: actions/upload-artifact@v4
with:
name: desktop-windows-unsigned
path: clients/upstream/apps/desktop/dist/*.exe

android-release:
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Production Android signing check
id: sign
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
set -eu
if [ -z "$ANDROID_KEYSTORE_BASE64" ]; then
mkdir -p dist
printf '%s\n' "No ANDROID_KEYSTORE_BASE64 secret is configured." > dist/android-signing-absent.txt
printf '%s\n' "mobile/android-build.sh only runs :app:assembleFdroidDebug." >> dist/android-signing-absent.txt
printf '%s\n' "A release APK was not built and a signing key was not created." >> dist/android-signing-absent.txt
echo "present=false" >> "$GITHUB_OUTPUT"
else
echo "present=true" >> "$GITHUB_OUTPUT"
fi
- uses: android-actions/setup-android@v3
if: steps.sign.outputs.present == 'true'
- name: Build the F-Droid release APK when a keystore already exists
if: steps.sign.outputs.present == 'true'
run: |
set -eu
pin=$(cat mobile/ANDROID_UPSTREAM_PIN)
git clone --depth 1 -b "$pin" https://github.com/bitwarden/android.git /tmp/bw-android
python3 mobile/android_rebrand.py /tmp/bw-android
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > /tmp/komboe.keystore
cd /tmp/bw-android
./gradlew --no-daemon :app:assembleFdroidRelease \
-Pandroid.injected.signing.store.file=/tmp/komboe.keystore \
-Pandroid.injected.signing.store.password="$ANDROID_KEYSTORE_PASSWORD" \
-Pandroid.injected.signing.key.alias="$ANDROID_KEY_ALIAS" \
-Pandroid.injected.signing.key.password="$ANDROID_KEY_PASSWORD"
mkdir -p "$GITHUB_WORKSPACE/dist"
find app/build/outputs/apk -name '*.apk' ! -name '*debug*' -exec cp {} "$GITHUB_WORKSPACE/dist/" \;
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
- uses: actions/upload-artifact@v4
with:
name: android-release-status
path: dist/*

download-manifest:
needs: [desktop-linux, desktop-windows, android-release]
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
continue-on-error: true
with:
pattern: desktop-*
path: gathered
merge-multiple: true
- name: Record checksums without publishing a download URL
run: |
set -eu
mkdir -p dist
if compgen -G "gathered/**/*.deb" > /dev/null || compgen -G "gathered/*.deb" > /dev/null; then
deb=$(find gathered -name '*.deb' | head -1)
cp "$deb" dist/komboe-desktop-amd64.deb
fi
if find gathered -name '*.AppImage' | grep -q .; then
cp "$(find gathered -name '*.AppImage' | head -1)" dist/komboe-desktop-x86_64.AppImage
fi
if find gathered -name '*.exe' | grep -q .; then
cp "$(find gathered -name '*.exe' | head -1)" dist/komboe-desktop-win-x64-setup.exe
fi
python3 scripts/build-downloads-manifest.py \
--artifacts dist \
--record-checksums \
--revision "${{ github.sha }}" \
--out dist/komboe-downloads.json
EXPECTED_REVISION="${{ github.sha }}" python3 - << 'PY'
import json, os
from pathlib import Path
manifest = json.loads(Path("dist/komboe-downloads.json").read_text())
if manifest.get("revision") != os.environ["EXPECTED_REVISION"]:
raise SystemExit(f"checksum manifest revision {manifest.get('revision')} is not {os.environ['EXPECTED_REVISION']}")
for asset in manifest["assets"]:
if asset["url"] or asset["status"] == "published":
raise SystemExit(f"checksum job must not publish a URL: {asset['id']}")
if asset["status"] == "checksum" and not asset["sha256"]:
raise SystemExit(f"checksum missing: {asset['id']}")
print("checksum assets", [(a["id"], a["status"], a["sha256"]) for a in manifest["assets"]])
PY
- uses: actions/upload-artifact@v4
with:
name: komboe-downloads-checksums
path: dist/komboe-downloads.json
Loading
Loading