Skip to content

chore: T2 2026 platform, CI and dependency updates #437

chore: T2 2026 platform, CI and dependency updates

chore: T2 2026 platform, CI and dependency updates #437

Workflow file for this run

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