name: Server Test Template on: workflow_call: inputs: name: required: true type: string datasource: required: true type: string drivername: required: true type: string logsartifact: required: true type: string fullyparallel: required: false type: boolean default: true allow-failure: required: false type: boolean default: false enablecoverage: required: false type: boolean default: false build-server-tag: description: "Build image tag (e.g. go1.26.3-node24.11.1)" required: true type: string fips-enabled: required: false default: false type: boolean elasticsearch-version: required: false type: string default: "9.0.0" opensearch-version: required: false type: string default: "3.0.0" test-target: required: false type: string default: "test-server" # -- Test sharding inputs (leave defaults for non-sharded callers) -- shard-index: required: false type: number default: -1 # -1 = no sharding; run all tests shard-total: required: false type: number default: 1 runner: description: "GitHub-hosted runner label (default: ubuntu-latest-8-cores)" required: false type: string default: "ubuntu-latest-8-cores" race-enabled: description: "Run tests with Go race detector (-race)" required: false type: boolean default: false secrets: # Only if enablecoverage is true CODECOV_TOKEN: required: false # Only if fips-enabled is true DOCKERHUB_USERNAME: required: false DOCKERHUB_TOKEN: required: false permissions: contents: read actions: read # for load-buildenv artifact listing jobs: test: name: ${{ inputs.name }} runs-on: ${{ inputs.runner }} continue-on-error: ${{ inputs.allow-failure }} # Used to avoid blocking PRs in case of flakiness env: COMPOSE_PROJECT_NAME: ghactions INPUT_DATASOURCE: ${{ inputs.datasource }} INPUT_DRIVERNAME: ${{ inputs.drivername }} INPUT_ENABLECOVERAGE: ${{ inputs.enablecoverage }} INPUT_FIPS_ENABLED: ${{ inputs.fips-enabled }} INPUT_FULLYPARALLEL: ${{ inputs.fullyparallel }} INPUT_LOGSARTIFACT: ${{ inputs.logsartifact }} INPUT_NAME: ${{ inputs.name }} INPUT_PR_NUMBER: ${{ github.event.pull_request.number }} INPUT_RACE_ENABLED: ${{ inputs.race-enabled }} INPUT_SHARD_TOTAL: ${{ inputs.shard-total }} INPUT_TEST_TARGET: ${{ inputs.test-target }} steps: - name: buildenv/docker-login # Only FIPS requires login for private build container. (Forks won't have credentials.) if: inputs.fips-enabled uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Checkout mattermost project uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: buildenv/load id: buildenv uses: ./.github/actions/load-buildenv with: build-server-tag: ${{ inputs.build-server-tag }} fips-enabled: ${{ inputs.fips-enabled }} - name: Restore Go cache id: go-cache uses: ./.github/actions/restore-go-cache - name: Restore test timing data if: inputs.shard-total > 1 id: timing-cache uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | server/prev-report.xml server/prev-gotestsum.json # Always restore from master — timing is only saved on the default # branch and is stable enough for shard balancing. # NOTE: the v2 prefix invalidates pre-existing caches that were # poisoned by shard failures (a killed shard loses its gotestsum.json, # so the merged report was missing those packages' timings; on the # next run they all defaulted to 1ms and bin-packed onto the lightest # shard, overloading it and perpetuating the cycle). See also the # all-shards-passed guard in server-test-merge-template.yml. key: server-test-timing-v2-master restore-keys: | server-test-timing-v2- - name: Setup log artifact name id: log-artifact run: | if [[ "$INPUT_FIPS_ENABLED" == 'true' ]]; then echo "LOG_ARTIFACT_NAME=${INPUT_LOGSARTIFACT}-fips" >> "${GITHUB_OUTPUT}" else echo "LOG_ARTIFACT_NAME=${INPUT_LOGSARTIFACT}" >> "${GITHUB_OUTPUT}" fi - name: Store required variables for publishing results run: | echo "$INPUT_NAME" > server/test-name echo "$INPUT_PR_NUMBER" > server/pr-number - name: Run docker compose env: ELASTICSEARCH_VERSION: ${{ inputs.elasticsearch-version }} OPENSEARCH_VERSION: ${{ inputs.opensearch-version }} POSTGRES_PASSWORD: 'mostest_password' run: | cd server/build docker compose --ansi never run --rm start_dependencies cat ../tests/custom-schema-objectID.ldif | docker compose --ansi never exec -T openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest_password || true'; cat ../tests/custom-schema-cpa.ldif | docker compose --ansi never exec -T openldap bash -c 'ldapadd -Y EXTERNAL -H ldapi:/// -w mostest_password || true'; cat ../tests/test-data.ldif | docker compose --ansi never exec -T openldap bash -c 'ldapadd -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest_password'; docker compose --ansi never exec -T minio sh -c 'mkdir -p /data/mattermost-test'; docker compose --ansi never ps # ── Test-level sharding ──────────────────────────────────────────── # When shard-total > 1, we split tests across N parallel runners. # # Two-tier splitting strategy: # - "Light" packages (< 5 min): assigned whole to a shard # - "Heavy" packages (≥ 5 min, e.g. api4, app): individual tests # are distributed across shards using -run regex filters # # See server/scripts/shard-split.js for the full algorithm. # ───────────────────────────────────────────────────────────────────── - name: Setup Go for test discovery if: inputs.shard-total > 1 uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version-file: server/.go-version - name: Split tests across shards if: inputs.shard-total > 1 id: test_split working-directory: server env: SHARD_INDEX: ${{ inputs.shard-index }} SHARD_TOTAL: ${{ inputs.shard-total }} # `go work` and shard-split.js's `go test -list` run on the host, so # they need the restored caches that the container gets via -e. GOMODCACHE: ${{ steps.go-cache.outputs.gomodcache }} GOCACHE: ${{ steps.go-cache.outputs.gocache }} run: | set -euo pipefail # ── List all test packages ── echo "::group::Listing test packages" TE_PKGS=$(find ./public/ ./ -name '*_test.go' -not -path './enterprise/*' -not -path './cmd/mmctl/*' 2>/dev/null \ | sed 's|/[^/]*$||' | sort -u \ | sed 's|^\./|github.com/mattermost/mattermost/server/v8/|' \ | sed 's|github.com/mattermost/mattermost/server/v8/public/|github.com/mattermost/mattermost/server/public/|') EE_PKGS=$(find ./enterprise/ -name '*_test.go' 2>/dev/null \ | sed 's|/[^/]*$||' | sort -u \ | sed 's|^\./|github.com/mattermost/mattermost/server/v8/|') ALL_PKGS=$(printf '%s\n%s' "$TE_PKGS" "$EE_PKGS" | grep -v '^$' | sort -u) TOTAL_PKGS=$(echo "$ALL_PKGS" | wc -l) echo "Found $TOTAL_PKGS test packages" echo "::endgroup::" if [[ "$TOTAL_PKGS" -eq 0 ]]; then echo "WARNING: No test packages found" echo "has_packages=false" >> "$GITHUB_OUTPUT" exit 0 fi echo "$ALL_PKGS" > all-packages.txt # ── Create go.work for test discovery ── # Without it, `go test -list` in shard-split.js resolves # github.com/mattermost/mattermost/server/public against the published # module version from server/go.mod, which lags HEAD and causes build # failures when heavy packages reference new symbols in ./public. go work init go work use . ./public # ── Run shard solver ── node scripts/shard-split.js echo "has_packages=true" >> "$GITHUB_OUTPUT" - name: Run Tests env: BUILD_IMAGE: ${{ steps.buildenv.outputs.image }} run: | RACE_MODE="" if [[ "$INPUT_RACE_ENABLED" == "true" ]]; then RACE_MODE="-race" fi TEST_TARGET="${INPUT_TEST_TARGET}${RACE_MODE}" BUILD_NUMBER="${GITHUB_HEAD_REF}-${GITHUB_RUN_ID}" DOCKER_CMD="make ${TEST_TARGET}" # When sharding is active, use the multi-run wrapper script. # run-shard-tests.sh detects heavy runs itself and falls back to # light-only mode when shard-heavy-runs.txt is absent or empty. if [[ "$INPUT_SHARD_TOTAL" -gt 1 && -f server/shard-te-packages.txt ]]; then cp server/scripts/run-shard-tests.sh server/run-shard-tests.sh chmod +x server/run-shard-tests.sh DOCKER_CMD="/mattermost/server/run-shard-tests.sh" fi docker run --net ghactions_mm-test \ --ulimit nofile=8096:8096 \ --env-file=server/build/dotenv/test.env \ --env MM_SQLSETTINGS_DRIVERNAME="$INPUT_DRIVERNAME" \ --env MM_SQLSETTINGS_DATASOURCE="$INPUT_DATASOURCE" \ --env TEST_DATABASE_POSTGRESQL_DSN="$INPUT_DATASOURCE" \ --env ENABLE_FULLY_PARALLEL_TESTS="$INPUT_FULLYPARALLEL" \ --env ENABLE_COVERAGE="$INPUT_ENABLECOVERAGE" \ --env FIPS_ENABLED="$INPUT_FIPS_ENABLED" \ --env RACE_MODE \ --env TEST_TARGET \ --env BUILD_NUMBER \ --env GOMODCACHE=/gocache/mod \ --env GOCACHE=/gocache/build \ --env GOBIN=/mattermost/server/bin \ -v "${RUNNER_TEMP}/go-cache:/gocache" \ -v $PWD:/mattermost \ -w /mattermost/server \ $BUILD_IMAGE \ $DOCKER_CMD - name: Upload coverage to Codecov if: ${{ inputs.enablecoverage }} uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} disable_search: true files: server/cover.out flags: server - name: Stop docker compose run: | cd server/build docker compose --ansi never stop - name: Archive logs if: ${{ always() }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ steps.log-artifact.outputs.LOG_ARTIFACT_NAME }} path: | server/gotestsum.json server/report.xml server/cover.out server/test-name server/pr-number