diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71ec8625..f36bf056 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,24 +40,53 @@ defaults: jobs: build: - name: Build and Test ${{ matrix.channel }} + name: Build and Test ${{ matrix.channel }} (part ${{ matrix.part }}) runs-on: ubuntu-24.04 timeout-minutes: 120 # Make warnings errors, this is to prevent warnings slipping through. # This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes. env: RUSTFLAGS: "-Dwarnings" + # Which parts passed, read by the `result` job below. Matrix jobs combine their outputs. + outputs: + rust-toolchain-1: ${{ steps.passed.outputs.rust-toolchain-1 }} + rust-toolchain-2: ${{ steps.passed.outputs.rust-toolchain-2 }} + msrv-1: ${{ steps.passed.outputs.msrv-1 }} + msrv-2: ${{ steps.passed.outputs.msrv-2 }} strategy: fail-fast: false matrix: channel: - "rust-toolchain" # The version defined in rust-toolchain - "msrv" # The supported MSRV + # Each step builds the whole vaultwarden crate again, so the steps are split over two jobs of + # about the same length that run in parallel. + part: + - 1 # Several backends together, and the lints + - 2 # One backend at a time steps: # Install dependencies - name: "Install dependencies Ubuntu" - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends openssl build-essential libmariadb-dev-compat libpq-dev libssl-dev pkg-config + run: | + # Separate commands, so a failing update fails the step. The image already has Oracle's MySQL client + # library, so a skipped install would otherwise go unnoticed and build against that one. + sudo apt-get update + sudo apt-get install -y --no-install-recommends openssl build-essential libmariadb-dev-compat libpq-dev libssl-dev pkg-config + + # Pass the MariaDB client library to mysqlclient-sys through its environment variables. + # When it finds the library with pkg-config, its build script makes cargo rerun it whenever + # anything in the library directory changes, and the install above changes that directory on + # every run. So every MySQL build recompiled mysqlclient-sys, diesel and diesel_migrations, + # even with a cached build. Cargo compares environment variables by their value instead. + lib_dir="$(pkg-config --variable=libdir mysqlclient)" + version="$(pkg-config --modversion mysqlclient)" + { + echo "MYSQLCLIENT_NO_PKG_CONFIG=1" + echo "LIBMARIADB_NO_PKG_CONFIG=1" + echo "MYSQLCLIENT_LIB_DIR=${lib_dir}" + echo "MYSQLCLIENT_VERSION=${version}" + } | tee -a "${GITHUB_ENV}" # End Install dependencies # Checkout the repo @@ -119,50 +148,50 @@ jobs: # Like changing the build host from Ubuntu 20.04 to 22.04 for example. # Only update when really needed! Use a .[.] format. prefix-key: "v2025.09-rust" + # The two parts build the dependencies with different features, so each has its own cache. + key: part-${{ matrix.part }} + # Only main saves caches. The repository can keep 10 GB of caches, and the ones saved by pull + # requests and other branches were mostly copies of main's that pushed other caches out. + save-if: ${{ github.ref == 'refs/heads/main' }} # End Enable Rust Caching # Run cargo tests - # First test all features together, afterwards test them separately. + # Part 1 tests several features together, part 2 tests each database backend on its own. + # enable_mimalloc only selects the allocator, so it is tested on and off, not with every combination. - name: "test features: sqlite,mysql,postgresql,enable_mimalloc,s3" id: test_sqlite_mysql_postgresql_mimalloc_s3 - if: ${{ !cancelled() }} + if: ${{ !cancelled() && matrix.part == 1 }} run: | cargo test --profile ci --features sqlite,mysql,postgresql,enable_mimalloc,s3 - - name: "test features: sqlite,mysql,postgresql,enable_mimalloc" - id: test_sqlite_mysql_postgresql_mimalloc - if: ${{ !cancelled() }} - run: | - cargo test --profile ci --features sqlite,mysql,postgresql,enable_mimalloc - - name: "test features: sqlite,mysql,postgresql" id: test_sqlite_mysql_postgresql - if: ${{ !cancelled() }} + if: ${{ !cancelled() && matrix.part == 1 }} run: | cargo test --profile ci --features sqlite,mysql,postgresql - name: "test features: sqlite" id: test_sqlite - if: ${{ !cancelled() }} + if: ${{ !cancelled() && matrix.part == 2 }} run: | cargo test --profile ci --features sqlite - name: "test features: mysql" id: test_mysql - if: ${{ !cancelled() }} + if: ${{ !cancelled() && matrix.part == 2 }} run: | cargo test --profile ci --features mysql - name: "test features: postgresql" id: test_postgresql - if: ${{ !cancelled() }} + if: ${{ !cancelled() && matrix.part == 2 }} run: | cargo test --profile ci --features postgresql # The test seeder only exists with this feature, which no release build has - name: "test features: sqlite,unsafe_test_mode" id: test_unsafe_test_mode - if: ${{ !cancelled() }} + if: ${{ !cancelled() && matrix.part == 2 }} run: | cargo test --profile ci --features sqlite,unsafe_test_mode # End Run cargo tests @@ -171,13 +200,13 @@ jobs: # Run cargo clippy, and fail on warnings - name: "clippy features: sqlite,mysql,postgresql,enable_mimalloc,s3" id: clippy - if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }} + if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' && matrix.part == 1 }} run: | cargo clippy --profile ci --features sqlite,mysql,postgresql,enable_mimalloc,s3 - name: "clippy features: sqlite,unsafe_test_mode" id: clippy_unsafe_test_mode - if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }} + if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' && matrix.part == 1 }} run: | cargo clippy --profile ci --features sqlite,unsafe_test_mode # End Run cargo clippy @@ -186,7 +215,7 @@ jobs: # Run cargo fmt (Only run on rust-toolchain defined version) - name: "check formatting" id: formatting - if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }} + if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' && matrix.part == 1 }} run: | cargo fmt --all -- --check # End Run cargo fmt @@ -198,7 +227,6 @@ jobs: if: ${{ failure() }} env: TEST_DB_M_S3: ${{ steps.test_sqlite_mysql_postgresql_mimalloc_s3.outcome }} - TEST_DB_M: ${{ steps.test_sqlite_mysql_postgresql_mimalloc.outcome }} TEST_DB: ${{ steps.test_sqlite_mysql_postgresql.outcome }} TEST_SQLITE: ${{ steps.test_sqlite.outcome }} TEST_MYSQL: ${{ steps.test_mysql.outcome }} @@ -208,20 +236,25 @@ jobs: CLIPPY_UNSAFE_TEST_MODE: ${{ steps.clippy_unsafe_test_mode.outcome }} FMT: ${{ steps.formatting.outcome }} run: | + # Only list the checks this part runs + row() { + if [[ "${2}" != "skipped" ]]; then + echo "|${1}|${2}|" >> "${GITHUB_STEP_SUMMARY}" + fi + } echo "### :x: Checks Failed!" >> "${GITHUB_STEP_SUMMARY}" echo "" >> "${GITHUB_STEP_SUMMARY}" echo "|Job|Status|" >> "${GITHUB_STEP_SUMMARY}" echo "|---|------|" >> "${GITHUB_STEP_SUMMARY}" - echo "|test (sqlite,mysql,postgresql,enable_mimalloc,s3)|${TEST_DB_M_S3}|" >> "${GITHUB_STEP_SUMMARY}" - echo "|test (sqlite,mysql,postgresql,enable_mimalloc)|${TEST_DB_M}|" >> "${GITHUB_STEP_SUMMARY}" - echo "|test (sqlite,mysql,postgresql)|${TEST_DB}|" >> "${GITHUB_STEP_SUMMARY}" - echo "|test (sqlite)|${TEST_SQLITE}|" >> "${GITHUB_STEP_SUMMARY}" - echo "|test (mysql)|${TEST_MYSQL}|" >> "${GITHUB_STEP_SUMMARY}" - echo "|test (postgresql)|${TEST_POSTGRESQL}|" >> "${GITHUB_STEP_SUMMARY}" - echo "|test (sqlite,unsafe_test_mode)|${TEST_UNSAFE_TEST_MODE}|" >> "${GITHUB_STEP_SUMMARY}" - echo "|clippy (sqlite,mysql,postgresql,enable_mimalloc,s3)|${CLIPPY}|" >> "${GITHUB_STEP_SUMMARY}" - echo "|clippy (sqlite,unsafe_test_mode)|${CLIPPY_UNSAFE_TEST_MODE}|" >> "${GITHUB_STEP_SUMMARY}" - echo "|fmt|${FMT}|" >> "${GITHUB_STEP_SUMMARY}" + row "test (sqlite,mysql,postgresql,enable_mimalloc,s3)" "${TEST_DB_M_S3}" + row "test (sqlite,mysql,postgresql)" "${TEST_DB}" + row "test (sqlite)" "${TEST_SQLITE}" + row "test (mysql)" "${TEST_MYSQL}" + row "test (postgresql)" "${TEST_POSTGRESQL}" + row "test (sqlite,unsafe_test_mode)" "${TEST_UNSAFE_TEST_MODE}" + row "clippy (sqlite,mysql,postgresql,enable_mimalloc,s3)" "${CLIPPY}" + row "clippy (sqlite,unsafe_test_mode)" "${CLIPPY_UNSAFE_TEST_MODE}" + row "fmt" "${FMT}" echo "" >> "${GITHUB_STEP_SUMMARY}" echo "Please check the failed jobs and fix where needed." >> "${GITHUB_STEP_SUMMARY}" echo "" >> "${GITHUB_STEP_SUMMARY}" @@ -231,7 +264,40 @@ jobs: # Check for any previous failures, if there are stop, else continue. # This is useful so all test/clippy/fmt actions are done, and they can all be addressed - name: "All checks passed" + id: passed if: ${{ success() }} + env: + PART: ${{ matrix.channel }}-${{ matrix.part }} run: | echo "### :tada: Checks Passed!" >> "${GITHUB_STEP_SUMMARY}" echo "" >> "${GITHUB_STEP_SUMMARY}" + echo "${PART}=success" >> "${GITHUB_OUTPUT}" + + # The required status checks are named after these jobs, one per channel, so they stay the same + # however the build job is split. It runs even when a part failed or the run was cancelled, because + # a skipped job counts as passed for a required check. + result: + name: Build and Test ${{ matrix.channel }} + needs: build + if: ${{ always() }} + runs-on: ubuntu-24.04 + timeout-minutes: 5 + strategy: + fail-fast: false + matrix: + channel: + - "rust-toolchain" + - "msrv" + + steps: + - name: "Check that both parts passed" + env: + CHANNEL: ${{ matrix.channel }} + PASSED: ${{ toJSON(needs.build.outputs) }} + run: | + for part in 1 2; do + if [[ "$(jq -r --arg part "${CHANNEL}-${part}" '.[$part] // ""' <<< "${PASSED}")" != "success" ]]; then + echo "::error::Build and Test ${CHANNEL} (part ${part}) did not pass" + exit 1 + fi + done