diff --git a/.github/workflows/deploy-dev.yaml b/.github/workflows/deploy-dev.yaml index a71d488..55242cd 100644 --- a/.github/workflows/deploy-dev.yaml +++ b/.github/workflows/deploy-dev.yaml @@ -1,189 +1,223 @@ name: Deploy Dev on: - push: - branches: [dev] - workflow_dispatch: + push: + branches: [dev] + workflow_dispatch: jobs: - build-and-deploy: - name: Deploy to Cloudflare (Dev) - runs-on: ubuntu-latest + build-and-deploy: + name: Deploy to Cloudflare (Dev) + runs-on: ubuntu-latest + env: + # Pin tool versions to avoid upstream updates breaking CI + WORKER_BUILD_VERSION: "0.7.1" + WRANGLER_VERSION: "4.54.0" + # Web Vault frontend (bw_web_builds) version tag (default pinned; override via GitHub Actions Variables) + # - Set repository variable BW_WEB_VERSION_DEV (e.g. v2025.12.0) to override + # - Set to "latest" to follow the latest bw_web_builds release + BW_WEB_VERSION: ${{ vars.BW_WEB_VERSION_DEV || 'v2025.12.0' }} + + # Required for wrangler + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js (for npx wrangler) + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install Rust toolchain (from rust-toolchain.toml) + run: | + set -euo pipefail + TOOLCHAIN="$(sed -n 's/^channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*$/\1/p' rust-toolchain.toml | head -n 1)" + if [ -z "$TOOLCHAIN" ]; then + echo "❌ Failed to read toolchain channel from rust-toolchain.toml" >&2 + exit 1 + fi + echo "Using Rust toolchain: $TOOLCHAIN" + echo "RUSTUP_TOOLCHAIN=$TOOLCHAIN" >> "$GITHUB_ENV" + rustup toolchain install "$TOOLCHAIN" --profile minimal --component rustfmt --component clippy --target wasm32-unknown-unknown + + - name: Install lld + run: sudo apt install -y lld clang gcc llvm + + - name: Install worker-build + run: | + set -euo pipefail + if command -v worker-build &> /dev/null; then + echo "worker-build already installed: $(worker-build --version || true)" + fi + cargo install --locked -q worker-build --version "${WORKER_BUILD_VERSION}" + + - name: Download and extract frontend (bw_web_builds) + run: | + set -euo pipefail + TAG="${BW_WEB_VERSION}" + if [ "${TAG}" = "latest" ]; then + TAG="$(curl -s https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest | jq -r .tag_name)" + fi + + echo "📦 Downloading frontend version: ${TAG}" + + # Download the web vault release + wget -q "https://github.com/dani-garcia/bw_web_builds/releases/download/${TAG}/bw_web_${TAG}.tar.gz" + + # Extract to public/web-vault + tar -xzf "bw_web_${TAG}.tar.gz" -C public/ + + # bw_web_builds extracts into a web-vault folder (expected) + if [ ! -d "public/web-vault" ]; then + echo "❌ Expected ./public/web-vault after extracting bw_web_builds" >&2 + ls -la public/ | head -50 || true + exit 1 + fi + + # Remove large source maps to satisfy Cloudflare static asset per-file limits + find public/web-vault -type f -name '*.map' -delete + + # Cleanup + rm -f "bw_web_${TAG}.tar.gz" + + echo "✅ Frontend files extracted to ./public/web-vault" + ls -la public/web-vault/ | head -20 + + - name: Apply web vault overrides (vaultwarden.css) + run: | + bash scripts/apply-web-vault-overrides.sh public/web-vault + + - name: Replace D1_DATABASE_ID_DEV in wrangler.toml + run: sed -i "s/\${D1_DATABASE_ID_DEV}/${{ secrets.D1_DATABASE_ID_DEV }}/g" wrangler.toml + + - name: Enable R2 binding when R2_NAME_DEV is provided env: - # Pin tool versions to avoid upstream updates breaking CI - WORKER_BUILD_VERSION: "0.7.1" - WRANGLER_VERSION: "4.54.0" - # Web Vault frontend (bw_web_builds) version tag (default pinned; override via GitHub Actions Variables) - # - Set repository variable BW_WEB_VERSION_DEV (e.g. v2025.12.0) to override - # - Set to "latest" to follow the latest bw_web_builds release - BW_WEB_VERSION: ${{ vars.BW_WEB_VERSION_DEV || 'v2025.12.0' }} + R2_NAME_DEV: ${{ secrets.R2_NAME_DEV }} + run: | + if [ -n "$R2_NAME_DEV" ]; then + echo "🔧 Enabling dev R2 bucket binding" + { + echo '' + echo '[[env.dev.r2_buckets]]' + echo 'binding = "ATTACHMENTS_BUCKET"' + echo "bucket_name = \"${R2_NAME_DEV}\"" + } >> wrangler.toml + else + echo "⏭️ R2_NAME_DEV not set, skipping R2 binding" + fi - # Required for wrangler - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + - name: Bootstrap D1 schema when database is empty (dev) + run: | + set -euo pipefail + WRANGLER="npx --yes wrangler@${WRANGLER_VERSION}" - steps: - - uses: actions/checkout@v4 + TABLE_COUNT=$($WRANGLER d1 execute vault1 --env dev --remote --command \ + "SELECT COUNT(*) AS cnt FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT IN ('_cf_METADATA','d1_migrations');" \ + --json | jq -r '.[0].results[0].cnt // 0') - - name: Set up Node.js (for npx wrangler) - uses: actions/setup-node@v4 - with: - node-version: "20" + echo "Existing user table count: ${TABLE_COUNT}" + if [ "${TABLE_COUNT}" = "0" ]; then + echo "🆕 Empty D1 database detected; applying sql/schema.sql..." + $WRANGLER d1 execute vault1 --env dev --remote --file sql/schema.sql - - name: Install Rust toolchain (from rust-toolchain.toml) - run: | - set -euo pipefail - TOOLCHAIN="$(sed -n 's/^channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*$/\1/p' rust-toolchain.toml | head -n 1)" - if [ -z "$TOOLCHAIN" ]; then - echo "❌ Failed to read toolchain channel from rust-toolchain.toml" >&2 - exit 1 - fi - echo "Using Rust toolchain: $TOOLCHAIN" - echo "RUSTUP_TOOLCHAIN=$TOOLCHAIN" >> "$GITHUB_ENV" - rustup toolchain install "$TOOLCHAIN" --profile minimal --component rustfmt --component clippy --target wasm32-unknown-unknown + echo "📝 Marking existing migrations as applied (schema.sql already includes them)" + BOOTSTRAP_SQL="$(mktemp)" + cat >"${BOOTSTRAP_SQL}" <<'SQL' + CREATE TABLE IF NOT EXISTS d1_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + SQL + shopt -s nullglob + for f in migrations/*.sql; do + echo "INSERT OR IGNORE INTO d1_migrations (name) VALUES ('$(basename "${f}")');" >>"${BOOTSTRAP_SQL}" + done + + $WRANGLER d1 execute vault1 --env dev --remote --file "${BOOTSTRAP_SQL}" + rm -f "${BOOTSTRAP_SQL}" + else + echo "⏭️ D1 already has tables; skipping base schema bootstrap" + fi + + - name: Apply D1 database migrations (dev) + run: | + set -euo pipefail # Ensure pipe returns first non-zero exit code + WRANGLER="npx --yes wrangler@${WRANGLER_VERSION}" + + # Apply migrations with retry loop to handle legacy ensure_schema columns + MAX_RETRIES=10 + RETRY_COUNT=0 + + while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + echo "🔄 Attempting to apply migrations (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)..." - - name: Install lld - run: sudo apt install -y lld clang gcc llvm + if $WRANGLER d1 migrations apply vault1 --env dev --remote 2>&1 | tee migration_output.txt; then + echo "✅ All migrations applied successfully" + exit 0 + fi + + # Check if failure is due to duplicate column (from legacy ensure_schema) + if grep -q "duplicate column name" migration_output.txt; then + # Extract the migration name that failed + # Format: "Migration 0001_add_password_salt.sql failed with the following errors:" + FAILED_MIGRATION=$(grep -oP "Migration \K[0-9]+_[a-zA-Z0-9_]+\.sql(?= failed)" migration_output.txt) + if [ -z "$FAILED_MIGRATION" ]; then + echo "❌ Detected duplicate column error, but could not identify the migration filename from output." + cat migration_output.txt + exit 1 + fi + + echo "⚠️ Migration '$FAILED_MIGRATION' failed: column already exists" + echo "📝 Marking migration as applied..." + + # Initialize d1_migrations table and mark this migration as applied + $WRANGLER d1 execute vault1 --env dev --remote --command " + CREATE TABLE IF NOT EXISTS d1_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + INSERT OR IGNORE INTO d1_migrations (name) VALUES ('$FAILED_MIGRATION'); + " + + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "🔁 Retrying remaining migrations..." + else + echo "❌ Migration failed with unexpected error:" + cat migration_output.txt + exit 1 + fi + done - - name: Install worker-build - run: | - set -euo pipefail - if command -v worker-build &> /dev/null; then - echo "worker-build already installed: $(worker-build --version || true)" - fi - cargo install --locked -q worker-build --version "${WORKER_BUILD_VERSION}" + echo "❌ Max retries ($MAX_RETRIES) exceeded" + exit 1 - - name: Download and extract frontend (bw_web_builds) - run: | - set -euo pipefail - TAG="${BW_WEB_VERSION}" - if [ "${TAG}" = "latest" ]; then - TAG="$(curl -s https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest | jq -r .tag_name)" - fi + - name: Seed global equivalent domains (dev, optional) + env: + SEED_GLOBAL_DOMAINS: ${{ vars.SEED_GLOBAL_DOMAINS || 'true' }} + GLOBAL_DOMAINS_URL_DEV: ${{ vars.GLOBAL_DOMAINS_URL_DEV }} + run: | + set -euo pipefail + if [ "${SEED_GLOBAL_DOMAINS}" = "false" ]; then + echo "⏭️ SEED_GLOBAL_DOMAINS=false, skipping" + exit 0 + fi - echo "📦 Downloading frontend version: ${TAG}" - - # Download the web vault release - wget -q "https://github.com/dani-garcia/bw_web_builds/releases/download/${TAG}/bw_web_${TAG}.tar.gz" - - # Extract to public/web-vault - tar -xzf "bw_web_${TAG}.tar.gz" -C public/ - - # bw_web_builds extracts into a web-vault folder (expected) - if [ ! -d "public/web-vault" ]; then - echo "❌ Expected ./public/web-vault after extracting bw_web_builds" >&2 - ls -la public/ | head -50 || true - exit 1 - fi + if [ -n "${GLOBAL_DOMAINS_URL_DEV:-}" ]; then + bash scripts/seed-global-domains.sh --db vault1 --env dev --remote --wrangler-version "${WRANGLER_VERSION}" --url "${GLOBAL_DOMAINS_URL_DEV}" + else + bash scripts/seed-global-domains.sh --db vault1 --env dev --remote --wrangler-version "${WRANGLER_VERSION}" + fi - # Remove large source maps to satisfy Cloudflare static asset per-file limits - find public/web-vault -type f -name '*.map' -delete - - # Cleanup - rm -f "bw_web_${TAG}.tar.gz" - - echo "✅ Frontend files extracted to ./public/web-vault" - ls -la public/web-vault/ | head -20 + - uses: cloudflare/wrangler-action@v3 + id: cf + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: ${{ env.WRANGLER_VERSION }} + packageManager: npm - - name: Apply web vault overrides (vaultwarden.css) - run: | - bash scripts/apply-web-vault-overrides.sh public/web-vault - - - name: Replace D1_DATABASE_ID_DEV in wrangler.toml - run: sed -i "s/\${D1_DATABASE_ID_DEV}/${{ secrets.D1_DATABASE_ID_DEV }}/g" wrangler.toml - - - name: Enable R2 binding when R2_NAME_DEV is provided - env: - R2_NAME_DEV: ${{ secrets.R2_NAME_DEV }} - run: | - if [ -n "$R2_NAME_DEV" ]; then - echo "🔧 Enabling dev R2 bucket binding" - { - echo '' - echo '[[env.dev.r2_buckets]]' - echo 'binding = "ATTACHMENTS_BUCKET"' - echo "bucket_name = \"${R2_NAME_DEV}\"" - } >> wrangler.toml - else - echo "⏭️ R2_NAME_DEV not set, skipping R2 binding" - fi - - - name: Apply D1 database migrations (dev) - run: | - set -euo pipefail # Ensure pipe returns first non-zero exit code - WRANGLER="npx --yes wrangler@${WRANGLER_VERSION}" - - # Apply migrations with retry loop to handle legacy ensure_schema columns - MAX_RETRIES=10 - RETRY_COUNT=0 - - while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do - echo "🔄 Attempting to apply migrations (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)..." - - if $WRANGLER d1 migrations apply vault1 --env dev --remote 2>&1 | tee migration_output.txt; then - echo "✅ All migrations applied successfully" - exit 0 - fi - - # Check if failure is due to duplicate column (from legacy ensure_schema) - if grep -q "duplicate column name" migration_output.txt; then - # Extract the migration name that failed - # Format: "Migration 0001_add_password_salt.sql failed with the following errors:" - FAILED_MIGRATION=$(grep -oP "Migration \K[0-9]+_[a-zA-Z0-9_]+\.sql(?= failed)" migration_output.txt) - if [ -z "$FAILED_MIGRATION" ]; then - echo "❌ Detected duplicate column error, but could not identify the migration filename from output." - cat migration_output.txt - exit 1 - fi - - echo "⚠️ Migration '$FAILED_MIGRATION' failed: column already exists" - echo "📝 Marking migration as applied..." - - # Initialize d1_migrations table and mark this migration as applied - $WRANGLER d1 execute vault1 --env dev --remote --command " - CREATE TABLE IF NOT EXISTS d1_migrations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL - ); - INSERT OR IGNORE INTO d1_migrations (name) VALUES ('$FAILED_MIGRATION'); - " - - RETRY_COUNT=$((RETRY_COUNT + 1)) - echo "🔁 Retrying remaining migrations..." - else - echo "❌ Migration failed with unexpected error:" - cat migration_output.txt - exit 1 - fi - done - - echo "❌ Max retries ($MAX_RETRIES) exceeded" - exit 1 - - - name: Seed global equivalent domains (dev, optional) - env: - SEED_GLOBAL_DOMAINS: ${{ vars.SEED_GLOBAL_DOMAINS || 'true' }} - GLOBAL_DOMAINS_URL_DEV: ${{ vars.GLOBAL_DOMAINS_URL_DEV }} - run: | - set -euo pipefail - if [ "${SEED_GLOBAL_DOMAINS}" = "false" ]; then - echo "⏭️ SEED_GLOBAL_DOMAINS=false, skipping" - exit 0 - fi - - if [ -n "${GLOBAL_DOMAINS_URL_DEV:-}" ]; then - bash scripts/seed-global-domains.sh --db vault1 --env dev --remote --wrangler-version "${WRANGLER_VERSION}" --url "${GLOBAL_DOMAINS_URL_DEV}" - else - bash scripts/seed-global-domains.sh --db vault1 --env dev --remote --wrangler-version "${WRANGLER_VERSION}" - fi - - - uses: cloudflare/wrangler-action@v3 - id: cf - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - wranglerVersion: ${{ env.WRANGLER_VERSION }} - packageManager: npm - - command: deploy --env dev + command: deploy --env dev diff --git a/.github/workflows/push-cloudflare.yaml b/.github/workflows/push-cloudflare.yaml index 4a5af6f..10e7d2b 100644 --- a/.github/workflows/push-cloudflare.yaml +++ b/.github/workflows/push-cloudflare.yaml @@ -1,198 +1,232 @@ name: Build on: - push: - branches: [main, uat, release*] - workflow_dispatch: + push: + branches: [main, uat, release*] + workflow_dispatch: jobs: - build: - name: Run production build - runs-on: ubuntu-latest + build: + name: Run production build + runs-on: ubuntu-latest + env: + # Pin tool versions to avoid upstream updates breaking CI + WORKER_BUILD_VERSION: "0.7.1" + WRANGLER_VERSION: "4.54.0" + # Web Vault frontend (bw_web_builds) version tag (default pinned; override via GitHub Actions Variables) + # - Set repository variable BW_WEB_VERSION (e.g. v2025.12.0) to override + # - Set to "latest" to follow the latest bw_web_builds release + BW_WEB_VERSION: ${{ vars.BW_WEB_VERSION || 'v2025.12.0' }} + + # Required for wrangler + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js (for npx wrangler) + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install Rust toolchain (from rust-toolchain.toml) + run: | + set -euo pipefail + TOOLCHAIN="$(sed -n 's/^channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*$/\1/p' rust-toolchain.toml | head -n 1)" + if [ -z "$TOOLCHAIN" ]; then + echo "❌ Failed to read toolchain channel from rust-toolchain.toml" >&2 + exit 1 + fi + echo "Using Rust toolchain: $TOOLCHAIN" + echo "RUSTUP_TOOLCHAIN=$TOOLCHAIN" >> "$GITHUB_ENV" + rustup toolchain install "$TOOLCHAIN" --profile minimal --component rustfmt --component clippy --target wasm32-unknown-unknown + # - name: Cache Cargo registry and build artifacts + # uses: actions/cache@v3 + # with: + # path: | + # target + # ~/.cargo/bin + # ~/.cargo/registry + # ~/.cargo/git + # key: cargo-build-cache-${{ github.run_id }} + # restore-keys: | + # cargo-build-cache- + - name: Install lld + run: sudo apt install -y lld clang gcc llvm + + - name: Install worker-build + run: | + set -euo pipefail + if command -v worker-build &> /dev/null; then + echo "worker-build already installed: $(worker-build --version || true)" + fi + cargo install --locked -q worker-build --version "${WORKER_BUILD_VERSION}" + + - name: Download and extract frontend (bw_web_builds) + run: | + set -euo pipefail + TAG="${BW_WEB_VERSION}" + if [ "${TAG}" = "latest" ]; then + TAG="$(curl -s https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest | jq -r .tag_name)" + fi + + echo "📦 Downloading frontend version: ${TAG}" + + # Download the web vault release + wget -q "https://github.com/dani-garcia/bw_web_builds/releases/download/${TAG}/bw_web_${TAG}.tar.gz" + + # Extract to public/web-vault + tar -xzf "bw_web_${TAG}.tar.gz" -C public/ + + # bw_web_builds extracts into a web-vault folder (expected) + if [ ! -d "public/web-vault" ]; then + echo "❌ Expected ./public/web-vault after extracting bw_web_builds" >&2 + ls -la public/ | head -50 || true + exit 1 + fi + + # Remove large source maps to satisfy Cloudflare static asset per-file limits + find public/web-vault -type f -name '*.map' -delete + + # Cleanup + rm -f "bw_web_${TAG}.tar.gz" + + echo "✅ Frontend files extracted to ./public/web-vault" + ls -la public/web-vault/ | head -20 + + - name: Apply web vault overrides (vaultwarden.css) + run: | + bash scripts/apply-web-vault-overrides.sh public/web-vault + + - name: Replace D1_DATABASE_ID in wrangler.toml + run: sed -i "s/\${D1_DATABASE_ID}/${{ secrets.D1_DATABASE_ID }}/g" wrangler.toml + + - name: Enable R2 binding when R2_NAME is provided env: - # Pin tool versions to avoid upstream updates breaking CI - WORKER_BUILD_VERSION: "0.7.1" - WRANGLER_VERSION: "4.54.0" - # Web Vault frontend (bw_web_builds) version tag (default pinned; override via GitHub Actions Variables) - # - Set repository variable BW_WEB_VERSION (e.g. v2025.12.0) to override - # - Set to "latest" to follow the latest bw_web_builds release - BW_WEB_VERSION: ${{ vars.BW_WEB_VERSION || 'v2025.12.0' }} + R2_NAME: ${{ secrets.R2_NAME }} + run: | + if [ -n "$R2_NAME" ]; then + echo "🔧 Enabling R2 bucket binding" + { + echo '' + echo '[[r2_buckets]]' + echo 'binding = "ATTACHMENTS_BUCKET"' + echo "bucket_name = \"${R2_NAME}\"" + } >> wrangler.toml + else + echo "⏭️ R2_NAME not set, skipping R2 binding" + fi - # Required for wrangler - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + - name: Bootstrap D1 schema when database is empty + run: | + set -euo pipefail + WRANGLER="npx --yes wrangler@${WRANGLER_VERSION}" - steps: - - uses: actions/checkout@v4 + TABLE_COUNT=$($WRANGLER d1 execute vault1 --remote --command \ + "SELECT COUNT(*) AS cnt FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT IN ('_cf_METADATA','d1_migrations');" \ + --json | jq -r '.[0].results[0].cnt // 0') - - name: Set up Node.js (for npx wrangler) - uses: actions/setup-node@v4 - with: - node-version: "20" + echo "Existing user table count: ${TABLE_COUNT}" + if [ "${TABLE_COUNT}" = "0" ]; then + echo "🆕 Empty D1 database detected; applying sql/schema.sql..." + $WRANGLER d1 execute vault1 --remote --file sql/schema.sql - - name: Install Rust toolchain (from rust-toolchain.toml) - run: | - set -euo pipefail - TOOLCHAIN="$(sed -n 's/^channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*$/\1/p' rust-toolchain.toml | head -n 1)" - if [ -z "$TOOLCHAIN" ]; then - echo "❌ Failed to read toolchain channel from rust-toolchain.toml" >&2 - exit 1 - fi - echo "Using Rust toolchain: $TOOLCHAIN" - echo "RUSTUP_TOOLCHAIN=$TOOLCHAIN" >> "$GITHUB_ENV" - rustup toolchain install "$TOOLCHAIN" --profile minimal --component rustfmt --component clippy --target wasm32-unknown-unknown - # - name: Cache Cargo registry and build artifacts - # uses: actions/cache@v3 - # with: - # path: | - # target - # ~/.cargo/bin - # ~/.cargo/registry - # ~/.cargo/git - # key: cargo-build-cache-${{ github.run_id }} - # restore-keys: | - # cargo-build-cache- - - name: Install lld - run: sudo apt install -y lld clang gcc llvm + echo "📝 Marking existing migrations as applied (schema.sql already includes them)" + BOOTSTRAP_SQL="$(mktemp)" + cat >"${BOOTSTRAP_SQL}" <<'SQL' + CREATE TABLE IF NOT EXISTS d1_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + SQL + shopt -s nullglob + for f in migrations/*.sql; do + echo "INSERT OR IGNORE INTO d1_migrations (name) VALUES ('$(basename "${f}")');" >>"${BOOTSTRAP_SQL}" + done - - name: Install worker-build - run: | - set -euo pipefail - if command -v worker-build &> /dev/null; then - echo "worker-build already installed: $(worker-build --version || true)" - fi - cargo install --locked -q worker-build --version "${WORKER_BUILD_VERSION}" + $WRANGLER d1 execute vault1 --remote --file "${BOOTSTRAP_SQL}" + rm -f "${BOOTSTRAP_SQL}" + else + echo "⏭️ D1 already has tables; skipping base schema bootstrap" + fi - - name: Download and extract frontend (bw_web_builds) - run: | - set -euo pipefail - TAG="${BW_WEB_VERSION}" - if [ "${TAG}" = "latest" ]; then - TAG="$(curl -s https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest | jq -r .tag_name)" - fi + - name: Apply D1 database migrations + run: | + set -euo pipefail # Ensure pipe returns first non-zero exit code + WRANGLER="npx --yes wrangler@${WRANGLER_VERSION}" - echo "📦 Downloading frontend version: ${TAG}" - - # Download the web vault release - wget -q "https://github.com/dani-garcia/bw_web_builds/releases/download/${TAG}/bw_web_${TAG}.tar.gz" - - # Extract to public/web-vault - tar -xzf "bw_web_${TAG}.tar.gz" -C public/ - - # bw_web_builds extracts into a web-vault folder (expected) - if [ ! -d "public/web-vault" ]; then - echo "❌ Expected ./public/web-vault after extracting bw_web_builds" >&2 - ls -la public/ | head -50 || true - exit 1 - fi + # Apply migrations with retry loop to handle legacy ensure_schema columns + MAX_RETRIES=10 + RETRY_COUNT=0 - # Remove large source maps to satisfy Cloudflare static asset per-file limits - find public/web-vault -type f -name '*.map' -delete + while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + echo "🔄 Attempting to apply migrations (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)..." + + if $WRANGLER d1 migrations apply vault1 --remote 2>&1 | tee migration_output.txt; then + echo "✅ All migrations applied successfully" + exit 0 + fi + + # Check if failure is due to duplicate column (from legacy ensure_schema) + if grep -q "duplicate column name" migration_output.txt; then + # Extract the migration name that failed + # Format: "Migration 0001_add_password_salt.sql failed with the following errors:" + FAILED_MIGRATION=$(grep -oP "Migration \K[0-9]+_[a-zA-Z0-9_]+\.sql(?= failed)" migration_output.txt) + if [ -z "$FAILED_MIGRATION" ]; then + echo "❌ Detected duplicate column error, but could not identify the migration filename from output." + cat migration_output.txt + exit 1 + fi + + echo "⚠️ Migration '$FAILED_MIGRATION' failed: column already exists" + echo "📝 Marking migration as applied..." + + # Initialize d1_migrations table and mark this migration as applied + $WRANGLER d1 execute vault1 --remote --command " + CREATE TABLE IF NOT EXISTS d1_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + INSERT OR IGNORE INTO d1_migrations (name) VALUES ('$FAILED_MIGRATION'); + " + + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "🔁 Retrying remaining migrations..." + else + echo "❌ Migration failed with unexpected error:" + cat migration_output.txt + exit 1 + fi + done - # Cleanup - rm -f "bw_web_${TAG}.tar.gz" - - echo "✅ Frontend files extracted to ./public/web-vault" - ls -la public/web-vault/ | head -20 + echo "❌ Max retries ($MAX_RETRIES) exceeded" + exit 1 - - name: Apply web vault overrides (vaultwarden.css) - run: | - bash scripts/apply-web-vault-overrides.sh public/web-vault + - name: Seed global equivalent domains (optional) + env: + SEED_GLOBAL_DOMAINS: ${{ vars.SEED_GLOBAL_DOMAINS || 'true' }} + GLOBAL_DOMAINS_URL: ${{ vars.GLOBAL_DOMAINS_URL }} + run: | + set -euo pipefail + if [ "${SEED_GLOBAL_DOMAINS}" = "false" ]; then + echo "⏭️ SEED_GLOBAL_DOMAINS=false, skipping" + exit 0 + fi - - name: Replace D1_DATABASE_ID in wrangler.toml - run: sed -i "s/\${D1_DATABASE_ID}/${{ secrets.D1_DATABASE_ID }}/g" wrangler.toml + if [ -n "${GLOBAL_DOMAINS_URL:-}" ]; then + bash scripts/seed-global-domains.sh --db vault1 --remote --wrangler-version "${WRANGLER_VERSION}" --url "${GLOBAL_DOMAINS_URL}" + else + bash scripts/seed-global-domains.sh --db vault1 --remote --wrangler-version "${WRANGLER_VERSION}" + fi - - name: Enable R2 binding when R2_NAME is provided - env: - R2_NAME: ${{ secrets.R2_NAME }} - run: | - if [ -n "$R2_NAME" ]; then - echo "🔧 Enabling R2 bucket binding" - { - echo '' - echo '[[r2_buckets]]' - echo 'binding = "ATTACHMENTS_BUCKET"' - echo "bucket_name = \"${R2_NAME}\"" - } >> wrangler.toml - else - echo "⏭️ R2_NAME not set, skipping R2 binding" - fi - - - name: Apply D1 database migrations - run: | - set -euo pipefail # Ensure pipe returns first non-zero exit code - WRANGLER="npx --yes wrangler@${WRANGLER_VERSION}" - - # Apply migrations with retry loop to handle legacy ensure_schema columns - MAX_RETRIES=10 - RETRY_COUNT=0 - - while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do - echo "🔄 Attempting to apply migrations (attempt $((RETRY_COUNT + 1))/$MAX_RETRIES)..." - - if $WRANGLER d1 migrations apply vault1 --remote 2>&1 | tee migration_output.txt; then - echo "✅ All migrations applied successfully" - exit 0 - fi - - # Check if failure is due to duplicate column (from legacy ensure_schema) - if grep -q "duplicate column name" migration_output.txt; then - # Extract the migration name that failed - # Format: "Migration 0001_add_password_salt.sql failed with the following errors:" - FAILED_MIGRATION=$(grep -oP "Migration \K[0-9]+_[a-zA-Z0-9_]+\.sql(?= failed)" migration_output.txt) - if [ -z "$FAILED_MIGRATION" ]; then - echo "❌ Detected duplicate column error, but could not identify the migration filename from output." - cat migration_output.txt - exit 1 - fi - - echo "⚠️ Migration '$FAILED_MIGRATION' failed: column already exists" - echo "📝 Marking migration as applied..." - - # Initialize d1_migrations table and mark this migration as applied - $WRANGLER d1 execute vault1 --remote --command " - CREATE TABLE IF NOT EXISTS d1_migrations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL - ); - INSERT OR IGNORE INTO d1_migrations (name) VALUES ('$FAILED_MIGRATION'); - " - - RETRY_COUNT=$((RETRY_COUNT + 1)) - echo "🔁 Retrying remaining migrations..." - else - echo "❌ Migration failed with unexpected error:" - cat migration_output.txt - exit 1 - fi - done - - echo "❌ Max retries ($MAX_RETRIES) exceeded" - exit 1 - - - name: Seed global equivalent domains (optional) - env: - SEED_GLOBAL_DOMAINS: ${{ vars.SEED_GLOBAL_DOMAINS || 'true' }} - GLOBAL_DOMAINS_URL: ${{ vars.GLOBAL_DOMAINS_URL }} - run: | - set -euo pipefail - if [ "${SEED_GLOBAL_DOMAINS}" = "false" ]; then - echo "⏭️ SEED_GLOBAL_DOMAINS=false, skipping" - exit 0 - fi - - if [ -n "${GLOBAL_DOMAINS_URL:-}" ]; then - bash scripts/seed-global-domains.sh --db vault1 --remote --wrangler-version "${WRANGLER_VERSION}" --url "${GLOBAL_DOMAINS_URL}" - else - bash scripts/seed-global-domains.sh --db vault1 --remote --wrangler-version "${WRANGLER_VERSION}" - fi - - - uses: cloudflare/wrangler-action@v3 - id: cf - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - wranglerVersion: ${{ env.WRANGLER_VERSION }} - packageManager: npm - command: deploy + - uses: cloudflare/wrangler-action@v3 + id: cf + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + wranglerVersion: ${{ env.WRANGLER_VERSION }} + packageManager: npm + command: deploy