chore: T2 2026 platform, CI and dependency updates #437
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Unit Tests | |
| on: | |
| push: | |
| branches: | |
| - "*.x" | |
| - development | |
| - main | |
| - master | |
| tags: | |
| - "v*" | |
| paths-ignore: | |
| - "*.md" | |
| - "docs/**" | |
| # This check is required by the repository ruleset, so it must report for | |
| # every pull request, including documentation-only changes. | |
| pull_request: {} | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| RAILS_ENV: "test" | |
| DOCKER_BUILD_RECORD_UPLOAD: "false" | |
| DOCKER_BUILD_SUMMARY: "false" | |
| DF_STUDENT_WORK_DIR: "/student-work" | |
| DF_INSTITUTION_HOST: "http://localhost:3000" | |
| DF_INSTITUTION_PRODUCT_NAME: "OnTrack" | |
| DF_SECRET_KEY_BASE: "test-secret-key-test-secret-key!" | |
| DF_SECRET_KEY_ATTR: "test-secret-key-test-secret-key!" | |
| DF_SECRET_KEY_DEVISE: "test-secret-key-test-secret-key!" | |
| DF_TEST_DB_ADAPTER: "mysql2" | |
| DF_TEST_DB_HOST: "mariadb" | |
| DF_TEST_DB_DATABASE: "doubtfire-test" | |
| DF_TEST_DB_USERNAME: "dfire" | |
| DF_TEST_DB_PASSWORD: "pwd" | |
| OVERSEER_ENABLED: "true" | |
| DF_ENCRYPTION_PRIMARY_KEY: "AMLOMYA5GV8B4fTK3VKMhVGn8WdvUW8g" | |
| DF_ENCRYPTION_DETERMINISTIC_KEY: "anlmuJ6cB3bN3biXRbYvmPsC5ALPFqGG" | |
| DF_ENCRYPTION_KEY_DERIVATION_SALT: "hzPR8D4qpOnAg7VeAhkhWw6JmmzKJB10" | |
| DF_REDIS_SIDEKIQ_URL: "redis://redis:6379/0" | |
| LATEX_CONTAINER_NAME: doubtfire-texlive | |
| LATEX_BUILD_PATH: /texlive/shell/latex_build.sh | |
| LTI_SHARED_API_SECRET: "abc123" | |
| LTI_ENABLED: true | |
| jobs: | |
| unit_test_shards: | |
| name: Unit Tests (worker ${{ matrix.worker }}/5) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| worker: [1, 2, 3, 4, 5] | |
| env: | |
| TEST_SHARD_COUNT: "20" | |
| TEST_SHARD_WORKER_COUNT: "5" | |
| TEST_SHARD_WORKER_NUMBER: ${{ matrix.worker }} | |
| TEST_SHARD_WORKER_PLAN: tmp/test-shard-worker-plan.tsv | |
| CI_IMAGE_CACHE_WRITE: ${{ github.event_name != 'pull_request' }} | |
| SKIP_OVERSEER_IMAGE_PULL_ON_POPULATE: "true" | |
| services: | |
| mariadb: | |
| image: mariadb | |
| env: | |
| MARIADB_USER: ${{ env.DF_TEST_DB_USERNAME }} | |
| MARIADB_PASSWORD: ${{ env.DF_TEST_DB_PASSWORD }} | |
| MARIADB_DATABASE: ${{ env.DF_TEST_DB_DATABASE }} | |
| MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes # This is required or the healthcheck script can't connect to the db | |
| options: --health-cmd "/usr/local/bin/healthcheck.sh --connect --innodb_initialized" --health-interval 1s --health-timeout 5s --health-retries 60 | |
| redis: | |
| image: redis:7.0 | |
| options: --health-cmd "redis-cli ping | grep PONG" --health-interval 1s --health-timeout 5s --health-retries 5 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - name: Resolve the job service network | |
| id: service_network | |
| run: | | |
| database_container_id="$(docker ps --filter ancestor=mariadb --format '{{.ID}}' | head -n 1)" | |
| if [ -z "$database_container_id" ]; then | |
| echo "Unable to find the MariaDB service container." | |
| exit 1 | |
| fi | |
| service_network="$( | |
| docker inspect \ | |
| --format '{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{"\n"}}{{end}}' \ | |
| "$database_container_id" | | |
| head -n 1 | |
| )" | |
| if [ -z "$service_network" ]; then | |
| echo "Unable to resolve the GitHub Actions service network." | |
| exit 1 | |
| fi | |
| echo "name=$service_network" >> "$GITHUB_OUTPUT" | |
| - name: Plan test shard | |
| id: plan_shard | |
| run: | | |
| TEST_SHARD_MANIFEST_DIR=tmp/test-shard-manifests \ | |
| TEST_SHARD_SELECTOR_INVENTORY=tmp/test-selector-inventory.txt \ | |
| TEST_SHARD_GITHUB_OUTPUT="$GITHUB_OUTPUT" \ | |
| ruby script/plan_test_shard_worker.rb | |
| echo "seed_date=$(date -u +%F)" >> "$GITHUB_OUTPUT" | |
| - name: Restore populated test database | |
| id: seeded_database_cache | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: | | |
| tmp/ci-seeded-database.sql.gz | |
| tmp/ci-seeded-student-work.tar.gz | |
| key: seeded-test-database-v5-${{ runner.os }}-${{ steps.plan_shard.outputs.seed_date }}-${{ hashFiles('.github/workflows/push.yml', '.dockerignore', 'Dockerfile', 'docker-bake.ci.hcl', 'Gemfile', 'Gemfile.lock', 'Rakefile', 'app/**/*', 'config/**/*', 'db/**/*', 'docker-entrypoint.sh', 'lib/**/*', 'script/prepare_test_database.sh', 'test/factories/**/*', 'test_files/**/*') }} | |
| - name: Set up docker buildx | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 | |
| - name: Build test images concurrently | |
| uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0 | |
| with: | |
| source: . | |
| files: docker-bake.ci.hcl | |
| targets: ${{ steps.plan_shard.outputs.bake_targets }} | |
| load: true | |
| - name: Prepare populated database | |
| env: | |
| SEEDED_DATABASE_CACHE_HIT: ${{ steps.seeded_database_cache.outputs.cache-hit }} | |
| run: | | |
| docker run --rm \ | |
| --network "${{ steps.service_network.outputs.name }}" \ | |
| --volume "$GITHUB_WORKSPACE:/doubtfire" \ | |
| --volume "$GITHUB_WORKSPACE/student-work:/student-work" \ | |
| --volume /var/run/docker.sock:/var/run/docker.sock \ | |
| --env RAILS_ENV \ | |
| --env DF_STUDENT_WORK_DIR \ | |
| --env DF_INSTITUTION_HOST \ | |
| --env DF_INSTITUTION_PRODUCT_NAME \ | |
| --env DF_SECRET_KEY_BASE \ | |
| --env DF_SECRET_KEY_ATTR \ | |
| --env DF_SECRET_KEY_DEVISE \ | |
| --env DF_TEST_DB_ADAPTER \ | |
| --env DF_TEST_DB_HOST \ | |
| --env DF_TEST_DB_DATABASE \ | |
| --env DF_TEST_DB_USERNAME \ | |
| --env DF_TEST_DB_PASSWORD \ | |
| --env OVERSEER_ENABLED \ | |
| --env DF_ENCRYPTION_PRIMARY_KEY \ | |
| --env DF_ENCRYPTION_DETERMINISTIC_KEY \ | |
| --env DF_ENCRYPTION_KEY_DERIVATION_SALT \ | |
| --env DF_REDIS_SIDEKIQ_URL \ | |
| --env LATEX_CONTAINER_NAME \ | |
| --env LATEX_BUILD_PATH \ | |
| --env LTI_SHARED_API_SECRET \ | |
| --env LTI_ENABLED \ | |
| --env SKIP_OVERSEER_IMAGE_PULL_ON_POPULATE \ | |
| --env SEEDED_DATABASE_CACHE_HIT \ | |
| doubtfire-api-ci:local \ | |
| script/prepare_test_database.sh | |
| - name: Verify populated database schema | |
| run: git diff --exit-code -- db/schema.rb | |
| - name: Snapshot populated test database | |
| if: ${{ steps.seeded_database_cache.outputs.cache-hit != 'true' }} | |
| run: | | |
| set -euo pipefail | |
| database_container_id="$(docker ps --filter ancestor=mariadb --format '{{.ID}}' | head -n 1)" | |
| if [ -z "$database_container_id" ]; then | |
| echo "Unable to find the MariaDB service container." | |
| exit 1 | |
| fi | |
| mkdir -p tmp | |
| docker exec "$database_container_id" mariadb-dump \ | |
| --user="$DF_TEST_DB_USERNAME" \ | |
| --password="$DF_TEST_DB_PASSWORD" \ | |
| --single-transaction \ | |
| --skip-comments \ | |
| "$DF_TEST_DB_DATABASE" | | |
| gzip -1 > tmp/ci-seeded-database.sql.gz | |
| tar -C student-work -czf tmp/ci-seeded-student-work.tar.gz . | |
| - name: Save populated test database | |
| if: ${{ steps.seeded_database_cache.outputs.cache-hit != 'true' && matrix.worker == 1 }} | |
| uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: | | |
| tmp/ci-seeded-database.sql.gz | |
| tmp/ci-seeded-student-work.tar.gz | |
| key: ${{ steps.seeded_database_cache.outputs.cache-primary-key }} | |
| - name: Run unit tests | |
| env: | |
| CI_SERVICE_NETWORK: ${{ steps.service_network.outputs.name }} | |
| run: script/run_test_shard_worker.sh | |
| - name: Upload test shard evidence | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: unit-test-shard-evidence-${{ matrix.worker }} | |
| path: | | |
| tmp/test-shard-manifests/ | |
| tmp/test-shard-run-counts/ | |
| tmp/test-shard-executed-runnables/ | |
| tmp/test-selector-inventory.txt | |
| tmp/test-runnable-inventory.txt | |
| if-no-files-found: error | |
| unit-tests: | |
| name: unit-tests | |
| if: ${{ always() }} | |
| needs: unit_test_shards | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download test shard manifests | |
| id: download_manifests | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| pattern: unit-test-shard-evidence-* | |
| path: tmp/all-test-shard-manifests | |
| merge-multiple: true | |
| - name: Verify exact test shard union | |
| id: verify_manifests | |
| run: | | |
| manifest_dir=tmp/all-test-shard-manifests/test-shard-manifests | |
| run_count_dir=tmp/all-test-shard-manifests/test-shard-run-counts | |
| executed_runnables_dir=tmp/all-test-shard-manifests/test-shard-executed-runnables | |
| selector_inventory_path=tmp/all-test-shard-manifests/test-selector-inventory.txt | |
| inventory_path=tmp/all-test-shard-manifests/test-runnable-inventory.txt | |
| manifest_count=$(find "$manifest_dir" -type f -name 'shard-*.txt' | wc -l) | |
| if [ "$manifest_count" -ne 20 ]; then | |
| echo "::error::Expected 20 shard manifests, found $manifest_count." | |
| exit 1 | |
| fi | |
| if [ ! -s "$selector_inventory_path" ]; then | |
| echo "::error::The canonical test selector inventory is missing." | |
| exit 1 | |
| fi | |
| LC_ALL=C sort "$selector_inventory_path" > expected-tests.txt | |
| cat "$manifest_dir"/shard-*.txt | LC_ALL=C sort > assigned-tests.txt | |
| LC_ALL=C uniq -d assigned-tests.txt > duplicate-tests.txt | |
| if [ -s duplicate-tests.txt ]; then | |
| echo "::error::One or more test runnables were assigned to multiple shards." | |
| cat duplicate-tests.txt | |
| exit 1 | |
| fi | |
| LC_ALL=C uniq assigned-tests.txt > assigned-tests-unique.txt | |
| diff -u expected-tests.txt assigned-tests-unique.txt | |
| run_count_file_count=$(find "$run_count_dir" -type f -name 'shard-*.txt' | wc -l) | |
| if [ "$run_count_file_count" -ne 20 ]; then | |
| echo "::error::Expected 20 shard run-count files, found $run_count_file_count." | |
| exit 1 | |
| fi | |
| if [ ! -s "$inventory_path" ]; then | |
| echo "::error::The canonical Minitest runnable inventory is missing." | |
| exit 1 | |
| fi | |
| for run_count_path in "$run_count_dir"/shard-*.txt; do | |
| if ! grep -Eq '^[0-9]+$' "$run_count_path"; then | |
| echo "::error::Invalid shard run count in $run_count_path." | |
| exit 1 | |
| fi | |
| done | |
| expected_run_count=$(wc -l < "$inventory_path") | |
| actual_run_count=$(awk '{ total += $1 } END { print total + 0 }' "$run_count_dir"/shard-*.txt) | |
| if [ "$actual_run_count" -ne "$expected_run_count" ]; then | |
| echo "::error::Shards executed $actual_run_count tests, expected $expected_run_count." | |
| exit 1 | |
| fi | |
| executed_runnables_file_count=$(find "$executed_runnables_dir" -type f -name 'shard-*.txt' | wc -l) | |
| if [ "$executed_runnables_file_count" -ne 20 ]; then | |
| echo "::error::Expected 20 executed-runnable files, found $executed_runnables_file_count." | |
| exit 1 | |
| fi | |
| cat "$executed_runnables_dir"/shard-*.txt | LC_ALL=C sort > actual-executed-runnables.txt | |
| LC_ALL=C uniq -d actual-executed-runnables.txt > duplicate-executed-runnables.txt | |
| if [ -s duplicate-executed-runnables.txt ]; then | |
| echo "::error::One or more Minitest runnables executed more than once." | |
| cat duplicate-executed-runnables.txt | |
| exit 1 | |
| fi | |
| LC_ALL=C sort "$inventory_path" > expected-executed-runnables.txt | |
| diff -u expected-executed-runnables.txt actual-executed-runnables.txt | |
| echo "Verified exact execution parity for $actual_run_count Minitest runnables." | |
| - name: Confirm all unit test shards passed | |
| if: ${{ always() }} | |
| env: | |
| SHARD_RESULT: ${{ needs.unit_test_shards.result }} | |
| MANIFEST_DOWNLOAD_RESULT: ${{ steps.download_manifests.outcome }} | |
| MANIFEST_VERIFY_RESULT: ${{ steps.verify_manifests.outcome }} | |
| run: | | |
| if [ "$MANIFEST_DOWNLOAD_RESULT" != "success" ] || [ "$MANIFEST_VERIFY_RESULT" != "success" ]; then | |
| echo "::error::Test shard manifest verification did not succeed "\ | |
| "(download: $MANIFEST_DOWNLOAD_RESULT, verify: $MANIFEST_VERIFY_RESULT)." | |
| exit 1 | |
| fi | |
| if [ "$SHARD_RESULT" != "success" ]; then | |
| echo "::error::One or more unit test shards did not succeed (result: $SHARD_RESULT)." | |
| exit 1 | |
| fi |