snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
This commit is contained in:
78
server/scripts/check_backport_migrations.sh
Executable file
78
server/scripts/check_backport_migrations.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Backport safety check (Postgres only).
|
||||
#
|
||||
# A release branch is an older subset of master. When a PR backports a migration
|
||||
# it must keep the exact version number and name that migration already has on
|
||||
# master; renumbering or renaming a shipped migration breaks the upgrade path
|
||||
# (the MM-68848 class of bug).
|
||||
#
|
||||
# We therefore only look at the migrations this PR ADDS on top of its base branch
|
||||
# and require each of them to already exist on master. Files that are already on
|
||||
# the base branch are left untouched, so historical states such as the Postgres
|
||||
# migrations renamed before the pre-migration infra landed do not produce false
|
||||
# positives.
|
||||
#
|
||||
# MySQL is intentionally skipped: support was dropped after v11.0, so older
|
||||
# release branches still carry MySQL migrations that no longer exist on master.
|
||||
#
|
||||
# Portable across the bash 3.2 that ships with macOS and Linux CI: uses only
|
||||
# POSIX-ish tools plus process substitution, no bash 4 features.
|
||||
#
|
||||
set -euo pipefail
|
||||
export LC_ALL=C
|
||||
|
||||
# Branch the PR targets. The migrations the PR adds are everything HEAD has that
|
||||
# this ref does not.
|
||||
base_ref="${MM_MIGRATION_CHECK_BASE_REF:-origin/master}"
|
||||
# Canonical source of shipped migrations the additions must match.
|
||||
canonical_ref="${MM_MIGRATION_CHECK_CANONICAL_REF:-origin/master}"
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
# Postgres only; MySQL migrations linger on old release branches and are skipped.
|
||||
migrations_dir="server/channels/db/migrations/postgres"
|
||||
|
||||
for ref in "$base_ref" "$canonical_ref"; do
|
||||
if ! git -C "$repo_root" rev-parse --verify --quiet "${ref}^{commit}" >/dev/null; then
|
||||
echo "Ref '$ref' not found. Fetch it first (e.g. 'git fetch origin master')" >&2
|
||||
echo "or set MM_MIGRATION_CHECK_BASE_REF / MM_MIGRATION_CHECK_CANONICAL_REF." >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
|
||||
# path -> "version_name.kind"; the up/down kind is part of the identity so a
|
||||
# one-sided rename can't be masked by its surviving partner file.
|
||||
norm='s#^'"$migrations_dir"'/([0-9]+_.+)\.(up|down)\.sql$#\1.\2#p'
|
||||
|
||||
ref_files() {
|
||||
git -C "$repo_root" ls-tree -r --name-only "$1" -- "$migrations_dir" \
|
||||
| sed -nE "$norm" | sort -u
|
||||
}
|
||||
|
||||
head_files() {
|
||||
# Tracked + untracked files actually present on disk, so a plain `mv`
|
||||
# without `git add` is still caught.
|
||||
git -C "$repo_root" ls-files --cached --others --exclude-standard -- "$migrations_dir" \
|
||||
| while IFS= read -r path; do
|
||||
[ -f "$repo_root/$path" ] && printf '%s\n' "$path"
|
||||
done \
|
||||
| sed -nE "$norm" | sort -u
|
||||
}
|
||||
|
||||
# Migrations this PR adds on top of its base branch.
|
||||
added="$(comm -23 <(head_files) <(ref_files "$base_ref"))"
|
||||
|
||||
# Of those, the ones that don't exist on the canonical branch are renames,
|
||||
# renumbers, or otherwise stray.
|
||||
stray="$(comm -23 <(printf '%s\n' "$added" | sed '/^$/d') <(ref_files "$canonical_ref"))"
|
||||
|
||||
if [ -n "$stray" ]; then
|
||||
count="$(printf '%s\n' "$stray" | wc -l | tr -d '[:space:]')"
|
||||
echo "Found $count migration file(s) added by this branch that do not exist on $canonical_ref:" >&2
|
||||
printf '%s\n' "$stray" | while IFS= read -r m; do
|
||||
echo " - ${m}.sql is added on this branch but not present on $canonical_ref; renaming or renumbering a shipped migration breaks upgrades. Add a new migration instead." >&2
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All migrations added by this branch exist on $canonical_ref."
|
||||
46
server/scripts/config_generator/main.go
Normal file
46
server/scripts/config_generator/main.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
// generateDefaultConfig writes default config to outputFile.
|
||||
func generateDefaultConfig(outputFile *os.File) error {
|
||||
defaultCfg := &model.Config{}
|
||||
defaultCfg.SetDefaults()
|
||||
if data, err := json.MarshalIndent(defaultCfg, "", " "); err != nil {
|
||||
return err
|
||||
} else if _, err := outputFile.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
outputFile := os.Getenv("OUTPUT_CONFIG")
|
||||
if outputFile == "" {
|
||||
fmt.Println("Output file name is missing. Please set OUTPUT_CONFIG env variable to absolute path")
|
||||
os.Exit(2)
|
||||
}
|
||||
if _, err := os.Stat(outputFile); !os.IsNotExist(err) {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "File %s already exists. Not overwriting!\n", outputFile)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if file, err := os.Create(outputFile); err == nil {
|
||||
err = generateDefaultConfig(file)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
38
server/scripts/config_generator/main_test.go
Normal file
38
server/scripts/config_generator/main_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDefaultsGenerator(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "tempconfig")
|
||||
defer os.Remove(tmpFile.Name())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, generateDefaultConfig(tmpFile))
|
||||
_ = tmpFile.Close()
|
||||
var config model.Config
|
||||
|
||||
b, err := os.ReadFile(tmpFile.Name())
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.Unmarshal(b, &config))
|
||||
require.Equal(t, *config.FileSettings.PublicLinkSalt, "")
|
||||
|
||||
require.Equal(t, *config.Office365Settings.Scope, model.Office365SettingsDefaultScope)
|
||||
require.Equal(t, *config.Office365Settings.AuthEndpoint, model.Office365SettingsDefaultAuthEndpoint)
|
||||
require.Equal(t, *config.Office365Settings.UserAPIEndpoint, model.Office365SettingsDefaultUserAPIEndpoint)
|
||||
require.Equal(t, *config.Office365Settings.TokenEndpoint, model.Office365SettingsDefaultTokenEndpoint)
|
||||
|
||||
require.Equal(t, *config.GoogleSettings.Scope, model.GoogleSettingsDefaultScope)
|
||||
require.Equal(t, *config.GoogleSettings.AuthEndpoint, model.GoogleSettingsDefaultAuthEndpoint)
|
||||
require.Equal(t, *config.GoogleSettings.UserAPIEndpoint, model.GoogleSettingsDefaultUserAPIEndpoint)
|
||||
require.Equal(t, *config.GoogleSettings.TokenEndpoint, model.GoogleSettingsDefaultTokenEndpoint)
|
||||
}
|
||||
125
server/scripts/default_permissions_generator/main.go
Normal file
125
server/scripts/default_permissions_generator/main.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store/storetest"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
"github.com/mattermost/mattermost/server/v8/config"
|
||||
)
|
||||
|
||||
type roleEntry struct {
|
||||
Name string
|
||||
Permissions string
|
||||
}
|
||||
|
||||
var tmpl = template.Must(template.New("").Parse(`// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by generate_default_roles_permissions.go. DO NOT EDIT.
|
||||
|
||||
export const defaultRolesPermissions = {
|
||||
{{- range .}}
|
||||
{{.Name}}: '{{.Permissions}}',
|
||||
{{- end}}
|
||||
};
|
||||
`))
|
||||
|
||||
func main() {
|
||||
outPath := flag.String("out", "", "output file path")
|
||||
flag.Parse()
|
||||
|
||||
if *outPath == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: generate_default_roles_permissions -out <path>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := utils.TranslationsPreInit(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: could not load translations: %v\n", err)
|
||||
}
|
||||
|
||||
sqlSettings := storetest.MakeSqlSettings(model.DatabaseDriverPostgres)
|
||||
defer storetest.CleanupSqlSettings(sqlSettings)
|
||||
|
||||
cfg := model.Config{}
|
||||
cfg.SetDefaults()
|
||||
cfg.SqlSettings = *sqlSettings
|
||||
|
||||
memStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{
|
||||
InitialConfig: &cfg,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error creating config store: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfgStore, err := config.NewStoreFromBacking(memStore, nil, false)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error creating config store: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// NewServer runs both schema migrations (via sqlstore.New) and all
|
||||
// app-level migrations (doAppMigrations) as part of its initialization.
|
||||
server, err := app.NewServer(app.ConfigStore(cfgStore))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error creating server: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer server.Shutdown()
|
||||
|
||||
roles, err := server.Store().Role().GetAll()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error reading roles: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
defaultNames := make(map[string]bool, len(model.MakeDefaultRoles()))
|
||||
for name := range model.MakeDefaultRoles() {
|
||||
defaultNames[name] = true
|
||||
}
|
||||
|
||||
var roleNames []string
|
||||
roleByName := make(map[string]*model.Role, len(roles))
|
||||
for _, r := range roles {
|
||||
if defaultNames[r.Name] {
|
||||
roleNames = append(roleNames, r.Name)
|
||||
roleByName[r.Name] = r
|
||||
}
|
||||
}
|
||||
slices.Sort(roleNames)
|
||||
|
||||
entries := make([]roleEntry, 0, len(roleNames))
|
||||
for _, name := range roleNames {
|
||||
perms := make([]string, len(roleByName[name].Permissions))
|
||||
copy(perms, roleByName[name].Permissions)
|
||||
slices.Sort(perms)
|
||||
perms = slices.Compact(perms)
|
||||
entries = append(entries, roleEntry{
|
||||
Name: name,
|
||||
Permissions: strings.Join(perms, " "),
|
||||
})
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
if err := tmpl.Execute(&b, entries); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error executing template: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(*outPath, []byte(b.String()), 0644); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error writing %s: %v\n", *outPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Generated %s\n", *outPath)
|
||||
}
|
||||
47
server/scripts/diff-config.sh
Executable file
47
server/scripts/diff-config.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
./jq-dep-check.sh
|
||||
|
||||
if [ -z "$FROM" ]
|
||||
then
|
||||
echo "Missing FROM version. Usage: make diff-config FROM=1.1.1 TO=2.2.2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$TO" ]
|
||||
then
|
||||
echo "Missing TO version. Usage: make diff-config FROM=1.1.1 TO=2.2.2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Returns the config file for a specific release
|
||||
fetch_config() {
|
||||
local url="https://releases.mattermost.com/$1/mattermost-$1-linux-amd64.tar.gz"
|
||||
curl -sf "$url" | tar -xzOf - mattermost/config/config.json | jq -S .
|
||||
}
|
||||
|
||||
echo Fetching config files
|
||||
from_config="$(fetch_config "$FROM")"
|
||||
if [ -z "$from_config" ]
|
||||
then
|
||||
echo Invalid version "$FROM"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
to_config=$(fetch_config "$TO")
|
||||
if [ -z "$to_config" ]
|
||||
then
|
||||
echo Invalid version "$TO"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo Comparing config files
|
||||
diff -y <(echo "$from_config") <(echo "$to_config")
|
||||
|
||||
# We ignore exits with 1 since it just means there's a difference, which is fine for us.
|
||||
diff_exit=$?
|
||||
if [ $diff_exit -eq 1 ]; then
|
||||
exit 0
|
||||
else
|
||||
exit $diff_exit
|
||||
fi
|
||||
11
server/scripts/diff-email-templates.sh
Executable file
11
server/scripts/diff-email-templates.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
make build-templates
|
||||
|
||||
if [[ `git status templates/ --porcelain` ]]; then
|
||||
echo "mjml templates have changed; Please compile and include compiled files"
|
||||
git diff templates/ # show diffs as part of error message
|
||||
exit 1
|
||||
else
|
||||
echo "PASS"
|
||||
fi
|
||||
1
server/scripts/esrupgrades/README.md
Normal file
1
server/scripts/esrupgrades/README.md
Normal file
@@ -0,0 +1 @@
|
||||
A collection of ad-hoc scripts to upgrade between ESRs.
|
||||
57
server/scripts/get_latest_release.sh
Executable file
57
server/scripts/get_latest_release.sh
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# $1 - repo to check against
|
||||
# $2 - release branch pattern to match
|
||||
# This script will check if the current repo has version pattern $2 ('release-5.20') in the branch name.
|
||||
# If it does it will check for a matching release version in the $1 repo and if found check for the newest dot version.
|
||||
# If the branch pattern isn't found than it will default to the newest release available in $1.
|
||||
REPO_TO_USE=$1
|
||||
BRANCH_TO_USE=$2
|
||||
|
||||
BASIC_AUTH=""
|
||||
|
||||
# If we find a github username and token, we use that.
|
||||
# In CI, these variables are available and useful to avoid rate limits which is
|
||||
# much more strict for unauthenticated requests.
|
||||
if [[ ! -z "$GITHUB_USERNAME" && ! -z "$GITHUB_TOKEN" ]];
|
||||
then
|
||||
BASIC_AUTH="--user $GITHUB_USERNAME:$GITHUB_TOKEN"
|
||||
fi
|
||||
|
||||
LATEST_RELEASE=$(curl \
|
||||
--silent \
|
||||
$BASIC_AUTH \
|
||||
"https://api.github.com/repos/$REPO_TO_USE/releases/latest")
|
||||
|
||||
RELEASES=$(curl \
|
||||
--silent \
|
||||
$BASIC_AUTH \
|
||||
"https://api.github.com/repos/$REPO_TO_USE/releases")
|
||||
|
||||
LATEST_REL=$(echo "$LATEST_RELEASE" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
|
||||
DRAFT=$(echo "$LATEST_RELEASE" | grep '"draft":' | sed -E 's/.*: ([^,]+).*/\1/')
|
||||
|
||||
PRERELEASE=$(echo "$RELEASES" | grep '"prerelease":' | sed -E 's/.*: ([^,]+).*/\1/')
|
||||
|
||||
# Check if this is a release branch
|
||||
THIS_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
#THIS_BRANCH="release-5.27"# - Used to test release logic on a non release branch
|
||||
|
||||
if [[ "$THIS_BRANCH" =~ $BRANCH_TO_USE || $DRAFT =~ "true" ]]; then
|
||||
VERSION_REL=${THIS_BRANCH//$BRANCH_TO_USE/v}
|
||||
REL_TO_USE=$(echo "$RELEASES" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed -n "/$VERSION_REL/p" | sort -rV | head -n 1)
|
||||
elif [[ "$THIS_BRANCH" =~ "master" ]]; then
|
||||
# Get the latest release even if its a pre-release
|
||||
REL_TO_USE=$(echo "$RELEASES" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sort -rV | head -n 1)
|
||||
else
|
||||
REL_TO_USE=$LATEST_REL
|
||||
fi
|
||||
|
||||
if [[ -z "$REL_TO_USE" ]]
|
||||
then
|
||||
echo "An error has occurred trying to get the latest mmctl release. Aborting. Perhaps api.github.com is down, or you are being rate-limited.";
|
||||
echo "Set the GITHUB_USERNAME and GITHUB_TOKEN environment variables to the appropriate values to work around GitHub rate-limiting.";
|
||||
exit 1;
|
||||
else
|
||||
echo "$REL_TO_USE"
|
||||
fi
|
||||
9
server/scripts/jq-dep-check.sh
Executable file
9
server/scripts/jq-dep-check.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
jq_cmd=jq
|
||||
[[ $(type -P "$jq_cmd") ]] || {
|
||||
echo "'$jq_cmd' command line JSON processor not found";
|
||||
echo "Please install on linux with 'sudo apt-get install jq'"
|
||||
echo "Please install on mac with 'brew install jq'"
|
||||
exit 1;
|
||||
}
|
||||
97
server/scripts/ldap-check.sh
Executable file
97
server/scripts/ldap-check.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
|
||||
jq_cmd=jq
|
||||
[[ $(type -P "$jq_cmd") ]] || {
|
||||
echo "'$jq_cmd' command line JSON processor not found";
|
||||
echo "Please install on linux with 'sudo apt-get install jq'"
|
||||
echo "Please install on mac with 'brew install jq'"
|
||||
exit 1;
|
||||
}
|
||||
|
||||
ldapsearch_cmd=ldapsearch
|
||||
[[ $(type -P "$ldapsearch_cmd") ]] || {
|
||||
echo "'$ldapsearch_cmd' shell accessible interface to ldap not found";
|
||||
echo "Please install on linux with 'sudo apt-get install ldap-utils'"
|
||||
exit 1;
|
||||
}
|
||||
|
||||
if [[ -z ${1} ]]; then
|
||||
echo "We could not find a username";
|
||||
echo "usage: ./ldap-check.sh -u/-g [username/groupname]"
|
||||
echo "example: ./ldap-check.sh -u john"
|
||||
echo "example: ./ldap-check.sh -g admin-staff"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
find_config_file() {
|
||||
local config_paths=("./config.json" "./config/config.json" "../config/config.json" "/opt/mattermost/config/config.json")
|
||||
|
||||
for path in "${config_paths[@]}"; do
|
||||
if [[ -e "$path" ]]; then
|
||||
echo "$path"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
config_file=$(find_config_file)
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "We could not find config.json"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LdapServer=`cat $config_file | jq -r .LdapSettings.LdapServer`
|
||||
LdapPort=`cat $config_file | jq -r .LdapSettings.LdapPort`
|
||||
ConnectionSecurity=`cat $config_file | jq -r .LdapSettings.ConnectionSecurity`
|
||||
BindUsername=`cat $config_file | jq -r .LdapSettings.BindUsername`
|
||||
BindPassword=`cat $config_file | jq -r .LdapSettings.BindPassword`
|
||||
BaseDN=`cat $config_file | jq -r .LdapSettings.BaseDN`
|
||||
UserFilter=`cat $config_file | jq -r .LdapSettings.UserFilter`
|
||||
EmailAttribute=`cat $config_file | jq -r .LdapSettings.EmailAttribute`
|
||||
UsernameAttribute=`cat $config_file | jq -r .LdapSettings.UsernameAttribute`
|
||||
IdAttribute=`cat $config_file | jq -r .LdapSettings.IdAttribute`
|
||||
GroupFilter=`cat $config_file | jq -r .LdapSettings.GroupFilter`
|
||||
GroupIdAttribute=`cat $config_file | jq -r .LdapSettings.GroupIdAttribute`
|
||||
|
||||
if [[ -z ${ConnectionSecurity} || ${ConnectionSecurity} == "null" ]]; then
|
||||
LdapUri="ldap://$LdapServer:$LdapPort"
|
||||
StartTlsFlag=""
|
||||
elif [[ ${ConnectionSecurity} == "STARTTLS" ]]; then
|
||||
LdapUri="ldap://$LdapServer:$LdapPort"
|
||||
StartTlsFlag="-ZZ"
|
||||
else
|
||||
LdapUri="ldaps://$LdapServer:$LdapPort"
|
||||
StartTlsFlag=""
|
||||
fi
|
||||
|
||||
if [[ -z ${UserFilter} ]]; then
|
||||
UserFilter="($IdAttribute=$2)"
|
||||
else
|
||||
UserFilter="(&($IdAttribute=$2)$UserFilter)"
|
||||
fi
|
||||
|
||||
if [[ -z ${GroupFilter} ]]; then
|
||||
GroupFilter="($GroupIdAttribute=$2)"
|
||||
else
|
||||
GroupFilter="(&($GroupIdAttribute=$2)$GroupFilter)"
|
||||
fi
|
||||
|
||||
run_ldap_search() {
|
||||
local filter="$1"
|
||||
local attributes="$2"
|
||||
|
||||
cmd_to_run="$ldapsearch_cmd -LLL -x $StartTlsFlag -H \"$LdapUri\" -D \"$BindUsername\" -w \"$BindPassword\" -b \"$BaseDN\" \"$filter\" $attributes"
|
||||
echo $cmd_to_run
|
||||
echo "-------------------------"
|
||||
eval $cmd_to_run
|
||||
}
|
||||
|
||||
if [[ $1 == '-u' ]]; then
|
||||
run_ldap_search "$UserFilter" "$IdAttribute $UsernameAttribute $EmailAttribute"
|
||||
elif [[ $1 == '-g' ]]; then
|
||||
run_ldap_search "$GroupFilter" ""
|
||||
else
|
||||
echo "User or Group not specified"
|
||||
fi
|
||||
1818
server/scripts/mattermost-postgresql-5.0.0.sql
Normal file
1818
server/scripts/mattermost-postgresql-5.0.0.sql
Normal file
File diff suppressed because it is too large
Load Diff
2378
server/scripts/mattermost-postgresql-6.0.0.sql
Normal file
2378
server/scripts/mattermost-postgresql-6.0.0.sql
Normal file
File diff suppressed because it is too large
Load Diff
48
server/scripts/mirror-docker-images.json
Normal file
48
server/scripts/mirror-docker-images.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"postgres": {
|
||||
"10": "postgres:10@sha256:7a484b11fcabd39596b1bf08780cdb61fa9b0d8bfad0844dbdce3a6922df95d1",
|
||||
"12": "postgres:12@sha256:cc7a021d9aff3aa02788d35c27a5cc32d4790ad92d72232a6be75b76ab7d79db",
|
||||
"13": "postgres:13@sha256:1b154a7bbf474aa1a2e67dc7c976835645fe6c3425320e7ad3f5a926d509e8fc",
|
||||
"14": "postgres:14@sha256:1c418702ab77adc7e84c7e726c2ab4f9cb63b8f997341ffcfab56629bab1429d",
|
||||
"15": "postgres:15@sha256:6ab12ad4395ee49ab49fe19530f7e183c5a9c97fc47cf687b3e281bec5f91ee4",
|
||||
"16": "postgres:16@sha256:9c1534cbf839ec70409508a874f4c02bf4739de2f32f77efe080eef1cfd34bf4",
|
||||
"17": "postgres:17@sha256:4f6c1ec9743885ba6438a91f7c08c4ef4d645a41eae31fc8521c2d831c622107"
|
||||
},
|
||||
"minio": {
|
||||
"RELEASE.2026-07-17T12-07-51Z": "cgr.dev/chainguard/minio@sha256:039800e64ec7247d2fde7cff3697e964f6fe20b6d7d2c46aa7d82cc63355d512"
|
||||
},
|
||||
"redis": {
|
||||
"7.4.0": "redis:7.4.0@sha256:6725a7dc7a44a6486b9d0a5172b10ccaf0c2ea600df87c0b93450d0e7769297f"
|
||||
},
|
||||
"inbucket": {
|
||||
"3.0.0": "inbucket/inbucket:3.0.0@sha256:1f10a0efea694592c06799c729aee1d6d71c9a4f72b73031d4a426ef5f26dfc1"
|
||||
},
|
||||
"openldap": {
|
||||
"1.4.0": "osixia/openldap:1.4.0@sha256:d5b2f2b816b25a1b57033b34f5d48c91cc3161a7d041811a9032604030bad9db"
|
||||
},
|
||||
"keycloak": {
|
||||
"10.0.2": "quay.io/keycloak/keycloak:10.0.2@sha256:fa434fd4e96f03242295febb37fb648b9ee271315ba7380d06003b43a42b5195",
|
||||
"23.0.7": "quay.io/keycloak/keycloak:23.0.7@sha256:14e99d6f5dd0516a5bdc82537b732cb85469ecdb15ad7fe5f11ff67521544db8"
|
||||
},
|
||||
"dejavu": {
|
||||
"3.4.2": "appbaseio/dejavu:3.4.2@sha256:8f2f4d45565da53c4235495737fff3921d302955daeb2f53a433c7b0e2044951"
|
||||
},
|
||||
"prometheus": {
|
||||
"v2.27.1": "prom/prometheus:v2.27.1@sha256:5accb68b56ba452e449a5e552411acaeabbbe0f087acf19a1157ce3dd10a8bed"
|
||||
},
|
||||
"grafana": {
|
||||
"8.0.1": "grafana/grafana:8.0.1@sha256:1c3e2fc7896adf9e33be5d062c08066087cb556f63b0a95f8aefe92bd37a6f38"
|
||||
},
|
||||
"cypress-browsers-public": {
|
||||
"node16.14.2-slim-chrome100-ff99-edge": "cypress/browsers:node16.14.2-slim-chrome100-ff99-edge@sha256:f8459ee677ce356eff64698095fbdf48eb9ab018fc5eb0d30c07ba23884edace",
|
||||
"node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1": "cypress/browsers:node-18.16.1-chrome-114.0.5735.133-1-ff-114.0.2-edge-114.0.1823.51-1@sha256:f798d6cc25d724210be3660e95f63ae75d052ef392e3fc5ae728d02da7e52df8"
|
||||
},
|
||||
"node": {
|
||||
"16.10.0": "node:16.10.0",
|
||||
"16.17.0": "node:16.17.0",
|
||||
"18": "node:18@sha256:11e9c297fc51f6f65f7d0c7c8a8581e5721f2f16de43ceff1a199fd3ef609f95",
|
||||
"18.17": "node:18.17@sha256:11e9c297fc51f6f65f7d0c7c8a8581e5721f2f16de43ceff1a199fd3ef609f95",
|
||||
"18.17.1": "node:18.17.1@sha256:11e9c297fc51f6f65f7d0c7c8a8581e5721f2f16de43ceff1a199fd3ef609f95",
|
||||
"20.11": "node:20.11@sha256:e06aae17c40c7a6b5296ca6f942a02e6737ae61bbbf3e2158624bb0f887991b5"
|
||||
}
|
||||
}
|
||||
34
server/scripts/mirror-docker-images.sh
Executable file
34
server/scripts/mirror-docker-images.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
set -eu -o pipefail
|
||||
|
||||
# Assert that IMAGE_FILE var is given, and that the file exists
|
||||
: ${IMAGES_FILE}
|
||||
[ -f ${IMAGES_FILE} ] || {
|
||||
echo "Error: images spec file $IMAGES_FILE does not exist. Aborting." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
DRY_RUN=${DRY_RUN:-yes}
|
||||
log () { echo "[$(date -Is)]" $*; }
|
||||
get_image_specs_per_line () {
|
||||
jq -c '. as $images | keys[] | . as $image | $images[.] | keys[] | {dst_img_name: $image, dst_img_tag: ., src_img: $images[$image][.]}' <$IMAGES_FILE
|
||||
}
|
||||
|
||||
log "Pusing images from given spec file: $IMAGES_FILE"
|
||||
log "Content of the spec file:"
|
||||
cat $IMAGES_FILE
|
||||
get_image_specs_per_line | while read IMAGE_SPEC; do
|
||||
DST_IMG_NAME=$(jq -r '.dst_img_name' <<<$IMAGE_SPEC)
|
||||
DST_IMG_TAG=$(jq -r '.dst_img_tag' <<<$IMAGE_SPEC)
|
||||
SOURCE_IMAGE=$(jq -r '.src_img' <<<$IMAGE_SPEC)
|
||||
DESTINATION_IMAGE=mattermostdevelopment/mirrored-${DST_IMG_NAME}:${DST_IMG_TAG}
|
||||
if [ "${DRY_RUN,,}" = "no" ]; then
|
||||
log "Pushing image: $SOURCE_IMAGE ---> $DESTINATION_IMAGE"
|
||||
docker pull $SOURCE_IMAGE
|
||||
docker tag $SOURCE_IMAGE $DESTINATION_IMAGE
|
||||
docker push $DESTINATION_IMAGE
|
||||
else
|
||||
log "Pushing image: $SOURCE_IMAGE ---> $DESTINATION_IMAGE (dry run mode, set the DRY_RUN=no env var to disable)"
|
||||
fi
|
||||
done
|
||||
log "All images pushed."
|
||||
11
server/scripts/prereq-check-enterprise.sh
Executable file
11
server/scripts/prereq-check-enterprise.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
check_prereq()
|
||||
{
|
||||
local dependency=$1
|
||||
type "$dependency" >/dev/null 2>&1 || { echo >&2 "Mattermost Enterprise requires '$dependency' but it doesn't appear to be installed. Aborting."; exit 1; }
|
||||
}
|
||||
|
||||
echo "Checking enterprise prerequisites"
|
||||
|
||||
check_prereq 'xmlsec1'
|
||||
59
server/scripts/psql-migration-test.sh
Executable file
59
server/scripts/psql-migration-test.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
./scripts/jq-dep-check.sh
|
||||
|
||||
TMPDIR=`mktemp -d 2>/dev/null || mktemp -d -t 'tmpConfigDir'`
|
||||
DUMPDIR=`mktemp -d 2>/dev/null || mktemp -d -t 'dumpDir'`
|
||||
SCHEMA_VERSION=$1
|
||||
|
||||
echo "Creating databases"
|
||||
docker exec mattermost-postgres sh -c 'exec echo "CREATE DATABASE migrated; CREATE DATABASE latest;" | exec psql -U mmuser mattermost_test'
|
||||
|
||||
echo "Importing postgres dump from version ${SCHEMA_VERSION}"
|
||||
docker exec -i mattermost-postgres psql -U mmuser -d migrated < $(pwd)/scripts/mattermost-postgresql-$SCHEMA_VERSION.sql
|
||||
|
||||
docker exec -i mattermost-postgres psql -U mmuser -d migrated -c "INSERT INTO Systems (Name, Value) VALUES ('Version', '$SCHEMA_VERSION')"
|
||||
|
||||
echo "Setting up config for db migration"
|
||||
cat config/config.json | \
|
||||
jq '.SqlSettings.DataSource = "postgres://mmuser:mostest_password@localhost:5432/migrated?sslmode=disable&connect_timeout=10"'| \
|
||||
jq '.SqlSettings.DriverName = "postgres"' > $TMPDIR/config.json
|
||||
|
||||
echo "Running the migration"
|
||||
make ARGS="db migrate --config $TMPDIR/config.json" run-cli
|
||||
|
||||
echo "Setting up config for fresh db setup"
|
||||
cat config/config.json | \
|
||||
jq '.SqlSettings.DataSource = "postgres://mmuser:mostest_password@localhost:5432/latest?sslmode=disable&connect_timeout=10"'| \
|
||||
jq '.SqlSettings.DriverName = "postgres"' > $TMPDIR/config.json
|
||||
|
||||
echo "Setting up fresh db"
|
||||
make ARGS="db migrate --config $TMPDIR/config.json" run-cli
|
||||
|
||||
if [ "$SCHEMA_VERSION" == "5.0.0" ]; then
|
||||
for i in "ChannelMembers MentionCountRoot" "ChannelMembers MsgCountRoot" "Channels TotalMsgCountRoot"; do
|
||||
a=( $i );
|
||||
echo "Ignoring known Postgres mismatch: ${a[0]}.${a[1]}"
|
||||
docker exec mattermost-postgres psql -U mmuser -d migrated -c "ALTER TABLE ${a[0]} DROP COLUMN ${a[1]};"
|
||||
docker exec mattermost-postgres psql -U mmuser -d latest -c "ALTER TABLE ${a[0]} DROP COLUMN ${a[1]};"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Generating dump"
|
||||
docker exec mattermost-postgres pg_dump --schema-only -d migrated -U mmuser > $DUMPDIR/migrated.sql
|
||||
docker exec mattermost-postgres pg_dump --schema-only -d latest -U mmuser > $DUMPDIR/latest.sql
|
||||
|
||||
echo "Removing databases created for db comparison"
|
||||
docker exec mattermost-postgres sh -c 'exec echo "DROP DATABASE migrated; DROP DATABASE latest;" | exec psql -U mmuser mattermost_test'
|
||||
|
||||
echo "Generating diff"
|
||||
git diff --word-diff=color $DUMPDIR/migrated.sql $DUMPDIR/latest.sql > $DUMPDIR/diff.txt
|
||||
diffErrorCode=$?
|
||||
|
||||
if [ $diffErrorCode -eq 0 ]; then
|
||||
echo "Both schemas are same"
|
||||
else
|
||||
echo "Schema mismatch"
|
||||
cat $DUMPDIR/diff.txt
|
||||
fi
|
||||
rm -rf $TMPDIR $DUMPDIR
|
||||
|
||||
exit $diffErrorCode
|
||||
130
server/scripts/run-shard-tests.sh
Executable file
130
server/scripts/run-shard-tests.sh
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
set -uo pipefail
|
||||
|
||||
# run-shard-tests.sh — Multi-run test wrapper for sharded CI
|
||||
#
|
||||
# When a shard has both "light" packages (run whole) and "heavy" package
|
||||
# splits (run with -run regex), we need multiple gotestsum invocations.
|
||||
# The Makefile's test-server target only supports a single invocation,
|
||||
# so this script calls gotestsum directly.
|
||||
#
|
||||
# Each invocation produces its own JUnit XML and JSON log files, which
|
||||
# are merged at the end into the standard report.xml and gotestsum.json
|
||||
# that the CI pipeline expects.
|
||||
#
|
||||
# Input files (in working directory, written by shard-split.js):
|
||||
# shard-te-packages.txt — space-separated TE packages
|
||||
# shard-ee-packages.txt — space-separated EE packages
|
||||
# shard-heavy-runs.txt — one line per heavy run: "pkg REGEX"
|
||||
#
|
||||
# Environment variables (set by CI):
|
||||
# RACE_MODE — "-race" on master, empty on PRs
|
||||
# ENABLE_COVERAGE — "true" to enable coverage profiling
|
||||
|
||||
GOBIN="$(pwd)/bin"
|
||||
|
||||
# Set up build prerequisites (go.work, gotestsum, go versions)
|
||||
# These are normally done by make test-server-pre.
|
||||
make setup-go-work gotestsum golang-versions
|
||||
|
||||
GOFLAGS_BASE="-buildvcs=false -timeout=90m"
|
||||
RACE_FLAG="${RACE_MODE:-}"
|
||||
|
||||
RUN_IDX=0
|
||||
FAILURES=0
|
||||
|
||||
# run_gotestsum PACKAGES [RUN_REGEX]
|
||||
# $1 = space-separated package list
|
||||
# $2 = optional -run regex (passed directly to go test)
|
||||
run_gotestsum() {
|
||||
local junitfile="report-${RUN_IDX}.xml"
|
||||
local jsonfile="gotestsum-${RUN_IDX}.json"
|
||||
local run_flag=""
|
||||
if [[ -n "${2:-}" ]]; then run_flag="-run $2"; fi
|
||||
|
||||
local coverage_flag=""
|
||||
if [[ "${ENABLE_COVERAGE:-false}" == "true" ]]; then
|
||||
coverage_flag="-coverprofile=cover-${RUN_IDX}.out -covermode=atomic"
|
||||
fi
|
||||
|
||||
RUN_IDX=$((RUN_IDX + 1))
|
||||
|
||||
GOTESTSUM_JUNITFILE="$junitfile" GOTESTSUM_JSONFILE="$jsonfile" \
|
||||
"$GOBIN/gotestsum" --format "${GOTESTSUM_FORMAT:-testname}" --rerun-fails=3 --packages="$1" \
|
||||
-- $GOFLAGS_BASE $RACE_FLAG $coverage_flag $run_flag \
|
||||
|| FAILURES=$((FAILURES + 1))
|
||||
}
|
||||
|
||||
# ── Read shard assignments ──
|
||||
SHARD_TE=""
|
||||
SHARD_EE=""
|
||||
HEAVY_RUNS=""
|
||||
|
||||
if [[ -f shard-te-packages.txt ]]; then
|
||||
SHARD_TE=$(cat shard-te-packages.txt)
|
||||
fi
|
||||
if [[ -f shard-ee-packages.txt ]]; then
|
||||
SHARD_EE=$(cat shard-ee-packages.txt)
|
||||
fi
|
||||
if [[ -f shard-heavy-runs.txt && -s shard-heavy-runs.txt ]]; then
|
||||
HEAVY_RUNS=$(cat shard-heavy-runs.txt)
|
||||
fi
|
||||
|
||||
# ── Run light packages (single invocation, no -run filter) ──
|
||||
ALL_LIGHT="${SHARD_TE} ${SHARD_EE}"
|
||||
ALL_LIGHT="${ALL_LIGHT## }"
|
||||
ALL_LIGHT="${ALL_LIGHT%% }"
|
||||
if [[ -n "$ALL_LIGHT" ]]; then
|
||||
LIGHT_COUNT=$(echo "$ALL_LIGHT" | wc -w)
|
||||
echo "Running $LIGHT_COUNT light packages..."
|
||||
run_gotestsum "$ALL_LIGHT"
|
||||
fi
|
||||
|
||||
# ── Run heavy package splits (one invocation per package subset) ──
|
||||
if [[ -n "$HEAVY_RUNS" ]]; then
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
PKG="${line%% *}"
|
||||
REGEX="${line#* }"
|
||||
SHORT_PKG="${PKG##*/}"
|
||||
TEST_COUNT=$(echo "$REGEX" | tr '|' '\n' | wc -l)
|
||||
echo "Running $TEST_COUNT tests from $SHORT_PKG..."
|
||||
run_gotestsum "$PKG" "$REGEX"
|
||||
done <<< "$HEAVY_RUNS"
|
||||
fi
|
||||
|
||||
# ── Merge results from all runs ──
|
||||
echo "Merging results from $RUN_IDX gotestsum runs..."
|
||||
|
||||
if ls report-*.xml 1>/dev/null 2>&1; then
|
||||
# Simple XML concatenation — the merge job uses junit-report-merger for proper merging
|
||||
head -1 report-0.xml > report.xml
|
||||
echo "<testsuites>" >> report.xml
|
||||
for f in report-*.xml; do
|
||||
grep -v "<?xml" "$f" | grep -v "^<testsuites" | grep -v "^</testsuites" >> report.xml || true
|
||||
done
|
||||
echo "</testsuites>" >> report.xml
|
||||
fi
|
||||
|
||||
cat gotestsum-*.json > gotestsum.json 2>/dev/null || true
|
||||
|
||||
# ── Merge coverage profiles within this shard (if coverage is enabled) ──
|
||||
# A single shard may run multiple gotestsum invocations (light packages +
|
||||
# heavy package splits), each producing its own cover-N.out. This merges
|
||||
# them into one cover.out per shard. The cross-shard merge (combining all
|
||||
# shards into a single report) is handled by Codecov's after_n_builds.
|
||||
if [[ "${ENABLE_COVERAGE:-false}" == "true" ]] && ls cover-*.out 1>/dev/null 2>&1; then
|
||||
echo "Merging coverage profiles..."
|
||||
{
|
||||
head -1 cover-0.out # "mode: atomic" header
|
||||
tail -q -n +2 cover-*.out # data lines from all files
|
||||
} > cover.out
|
||||
echo "Merged $(ls cover-*.out | wc -l) coverage files into cover.out"
|
||||
fi
|
||||
|
||||
if [[ $FAILURES -gt 0 ]]; then
|
||||
echo "Shard complete: $RUN_IDX gotestsum runs, $FAILURES failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Shard complete: $RUN_IDX gotestsum runs, all passed"
|
||||
294
server/scripts/shard-split.js
Normal file
294
server/scripts/shard-split.js
Normal file
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* shard-split.js — Test shard assignment solver
|
||||
*
|
||||
* Splits Go test packages across N parallel CI runners using timing data
|
||||
* from previous runs. Uses a two-tier strategy:
|
||||
*
|
||||
* 1. "Light" packages (< HEAVY_MS total runtime): assigned whole to a shard
|
||||
* 2. "Heavy" packages (>= HEAVY_MS): individual tests distributed across
|
||||
* shards using -run regex filters
|
||||
*
|
||||
* Timing data sources (in priority order):
|
||||
* - gotestsum.json (JSONL): per-test elapsed times from previous run
|
||||
* - prev-report.xml (JUnit XML): package-level timing (fallback)
|
||||
* - Round-robin: when no timing data exists at all
|
||||
*
|
||||
* Assignment algorithm: greedy bin-packing (sort by duration desc, assign
|
||||
* each item to the shard with lowest current load). Simple and effective
|
||||
* for our distribution where 2 packages dominate 84% of runtime.
|
||||
*
|
||||
* Environment variables:
|
||||
* SHARD_INDEX — this runner's index (0-based)
|
||||
* SHARD_TOTAL — total number of shards
|
||||
*
|
||||
* Input files (in working directory):
|
||||
* all-packages.txt — newline-separated list of all test packages
|
||||
* prev-gotestsum.json — (optional) JSONL timing data from previous run
|
||||
* prev-report.xml — (optional) JUnit XML from previous run
|
||||
*
|
||||
* Output files (in working directory):
|
||||
* shard-te-packages.txt — space-separated TE packages for this shard
|
||||
* shard-ee-packages.txt — space-separated EE packages for this shard
|
||||
* shard-heavy-runs.txt — heavy package runs, one per line: "pkg REGEX"
|
||||
*/
|
||||
|
||||
const fs = require("node:fs");
|
||||
const { execSync } = require("node:child_process");
|
||||
|
||||
const SHARD_INDEX = parseInt(process.env.SHARD_INDEX);
|
||||
const SHARD_TOTAL = parseInt(process.env.SHARD_TOTAL);
|
||||
const HEAVY_MS = 600000; // 600s (10 min): packages above this get test-level splitting
|
||||
|
||||
// Packages that should always be split test-by-test, even on a cold cache.
|
||||
// Without timing data the splitter falls through to alphabetical round-robin,
|
||||
// which places these adjacent on the same runner and overwhelms postgres.
|
||||
// Forcing them heavy lets `go test -list` enumerate their tests so the
|
||||
// bin-packer can spread them across all shards.
|
||||
const KNOWN_HEAVY_PKGS = new Set([
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4",
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app",
|
||||
]);
|
||||
|
||||
if (isNaN(SHARD_INDEX) || isNaN(SHARD_TOTAL) || SHARD_TOTAL < 1) {
|
||||
console.error("ERROR: SHARD_INDEX and SHARD_TOTAL must be set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const allPkgs = fs
|
||||
.readFileSync("all-packages.txt", "utf8")
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
if (allPkgs.length === 0) {
|
||||
console.error("WARNING: No test packages found in all-packages.txt");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const pkgTimes = {};
|
||||
const testTimes = {}; // "pkg::TestName" -> ms
|
||||
|
||||
// ── Parse gotestsum.json (JSONL) for per-test timing ──
|
||||
// Each line is a JSON event; we want "pass" events with Elapsed times.
|
||||
if (fs.existsSync("prev-gotestsum.json")) {
|
||||
console.log("::group::Parsing gotestsum.json timing data");
|
||||
const lines = fs.readFileSync("prev-gotestsum.json", "utf8").split("\n");
|
||||
for (const line of lines) {
|
||||
if (!line.includes('"pass"')) continue;
|
||||
try {
|
||||
const d = JSON.parse(line);
|
||||
if (!d.Test || !d.Package) continue;
|
||||
const elapsed = Math.round((d.Elapsed || 0) * 1000);
|
||||
// Aggregate package time from test pass events
|
||||
pkgTimes[d.Package] = (pkgTimes[d.Package] || 0) + elapsed;
|
||||
// Top-level test name (use max elapsed for parent vs subtests)
|
||||
const top = d.Test.split("/")[0];
|
||||
const key = d.Package + "::" + top;
|
||||
testTimes[key] = Math.max(testTimes[key] || 0, elapsed);
|
||||
} catch (e) {
|
||||
// Skip malformed lines
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`gotestsum.json: ${Object.keys(pkgTimes).length} packages, ${Object.keys(testTimes).length} tests`,
|
||||
);
|
||||
console.log("::endgroup::");
|
||||
}
|
||||
|
||||
// ── Fallback: parse JUnit XML for package-level timing ──
|
||||
if (Object.keys(pkgTimes).length === 0 && fs.existsSync("prev-report.xml")) {
|
||||
console.log("::group::Parsing JUnit XML timing data (fallback)");
|
||||
const xml = fs.readFileSync("prev-report.xml", "utf8");
|
||||
for (const m of xml.matchAll(/<testsuite[^>]*>/g)) {
|
||||
const name = m[0].match(/name="([^"]+)"/)?.[1];
|
||||
const time = m[0].match(/\btime="([^"]+)"/)?.[1];
|
||||
if (name && time) {
|
||||
pkgTimes[name] =
|
||||
(pkgTimes[name] || 0) + Math.round(parseFloat(time) * 1000);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
`JUnit XML: ${Object.keys(pkgTimes).length} packages (no per-test data)`,
|
||||
);
|
||||
console.log("::endgroup::");
|
||||
}
|
||||
|
||||
const hasTimingData = Object.keys(pkgTimes).length > 0;
|
||||
const hasTestTiming = Object.keys(testTimes).length > 0;
|
||||
|
||||
// ── Identify heavy packages ──
|
||||
// Split at test level for packages above HEAVY_MS (requires per-test timing)
|
||||
// AND for the KNOWN_HEAVY_PKGS list (which uses go test -list discovery
|
||||
// to enumerate tests when no timing cache exists).
|
||||
//
|
||||
// Both checks gate on allPkgs membership so stale entries from the cached
|
||||
// pkgTimes (renamed/deleted packages from a prior run) can't end up in
|
||||
// heavyPkgs — otherwise the post-discovery fallback would emit them as
|
||||
// whole-package items for nonexistent packages.
|
||||
const allPkgsSet = new Set(allPkgs);
|
||||
const heavyPkgs = new Set();
|
||||
if (hasTestTiming) {
|
||||
for (const [pkg, ms] of Object.entries(pkgTimes)) {
|
||||
if (ms > HEAVY_MS && allPkgsSet.has(pkg)) heavyPkgs.add(pkg);
|
||||
}
|
||||
}
|
||||
for (const pkg of allPkgs) {
|
||||
if (KNOWN_HEAVY_PKGS.has(pkg)) heavyPkgs.add(pkg);
|
||||
}
|
||||
if (heavyPkgs.size > 0) {
|
||||
console.log("Heavy packages (test-level splitting):");
|
||||
for (const p of heavyPkgs) {
|
||||
const t = pkgTimes[p];
|
||||
const label = t ? `${(t / 1000).toFixed(0)}s` : "no-timing";
|
||||
console.log(` ${label} ${p.split("/").pop()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Build work items ──
|
||||
// Each item is either a whole package ("P") or a single test from a heavy package ("T")
|
||||
const items = [];
|
||||
for (const pkg of allPkgs) {
|
||||
if (heavyPkgs.has(pkg)) {
|
||||
// Split into individual test items
|
||||
const tests = Object.entries(testTimes)
|
||||
.filter(([k]) => k.startsWith(pkg + "::"))
|
||||
.map(([k, ms]) => ({ ms, type: "T", pkg, test: k.split("::")[1] }));
|
||||
if (tests.length > 0) {
|
||||
items.push(...tests);
|
||||
}
|
||||
// If no per-test timing exists, the discovery step below enumerates
|
||||
// tests via `go test -list`. A final fallback to whole-package is
|
||||
// added after discovery for packages where both lookups failed.
|
||||
} else {
|
||||
items.push({ ms: pkgTimes[pkg] || 1, type: "P", pkg });
|
||||
}
|
||||
}
|
||||
// ── Discover new/renamed tests in heavy packages ──
|
||||
// Tests not in the timing cache won't appear in any shard's -run regex,
|
||||
// silently skipping them. Discover current test names at runtime and
|
||||
// assign any cache-missing tests to the least-loaded shard.
|
||||
if (heavyPkgs.size > 0) {
|
||||
console.log("::group::Discovering new tests in heavy packages");
|
||||
for (const pkg of heavyPkgs) {
|
||||
const cachedTests = new Set(
|
||||
Object.keys(testTimes)
|
||||
.filter((k) => k.startsWith(pkg + "::"))
|
||||
.map((k) => k.split("::")[1]),
|
||||
);
|
||||
try {
|
||||
const out = execSync(`go test -list '.*' ${pkg} 2>/dev/null`, {
|
||||
encoding: "utf8",
|
||||
timeout: 300000,
|
||||
});
|
||||
const currentTests = out
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => /^Test[A-Z]/.test(l));
|
||||
let newCount = 0;
|
||||
for (const t of currentTests) {
|
||||
if (!cachedTests.has(t)) {
|
||||
// Assign a small default duration so it gets picked up
|
||||
items.push({ ms: 1000, type: "T", pkg, test: t });
|
||||
newCount++;
|
||||
}
|
||||
}
|
||||
if (newCount > 0) {
|
||||
console.log(
|
||||
` ${pkg.split("/").pop()}: ${newCount} new test(s) not in cache`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// go test -list can fail for packages whose TestMain requires a DB
|
||||
// connection (e.g. sqlstore) because the GitHub runner cannot reach the
|
||||
// docker-compose postgres network. Log a warning and fall back to
|
||||
// treating the package as a whole unit rather than failing all shards.
|
||||
console.error(
|
||||
`::warning::${pkg.split("/").pop()}: go test -list failed — treating as whole package (new tests may be skipped this run). ${e.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Ensure every heavy package has at least one item. A package can reach
|
||||
// this point with zero items if it has no per-test timing AND `go test
|
||||
// -list` failed (e.g. sqlstore on a cold cache).
|
||||
for (const pkg of heavyPkgs) {
|
||||
const hasItems = items.some((it) => it.pkg === pkg);
|
||||
if (!hasItems) {
|
||||
console.log(
|
||||
` ${pkg.split("/").pop()}: no per-test data, running as whole package`,
|
||||
);
|
||||
items.push({ ms: pkgTimes[pkg] || 1, type: "P", pkg });
|
||||
}
|
||||
}
|
||||
console.log("::endgroup::");
|
||||
}
|
||||
|
||||
// Sort descending by duration for greedy bin-packing
|
||||
items.sort((a, b) => b.ms - a.ms);
|
||||
|
||||
// ── Greedy bin-packing assignment ──
|
||||
const shards = Array.from({ length: SHARD_TOTAL }, () => ({
|
||||
load: 0,
|
||||
whole: [],
|
||||
heavy: {},
|
||||
}));
|
||||
|
||||
if (!hasTimingData && heavyPkgs.size === 0) {
|
||||
// Round-robin fallback only when we have *no* signal — no timing cache
|
||||
// and no known-heavy packages to test-level-split. With heavyPkgs we
|
||||
// can still bin-pack: discovered tests (ms=1000 each) drive the
|
||||
// distribution and whole-package items (ms=1) fill in evenly.
|
||||
console.log("No timing data — using round-robin");
|
||||
allPkgs.forEach((pkg, i) => {
|
||||
shards[i % SHARD_TOTAL].whole.push(pkg);
|
||||
});
|
||||
} else {
|
||||
for (const item of items) {
|
||||
// Find shard with minimum current load
|
||||
const min = shards.reduce(
|
||||
(m, s, i) => (s.load < shards[m].load ? i : m),
|
||||
0,
|
||||
);
|
||||
shards[min].load += item.ms;
|
||||
if (item.type === "P") {
|
||||
shards[min].whole.push(item.pkg);
|
||||
} else {
|
||||
if (!shards[min].heavy[item.pkg]) shards[min].heavy[item.pkg] = [];
|
||||
shards[min].heavy[item.pkg].push(item.test);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Report shard assignments ──
|
||||
console.log("::group::Shard assignment");
|
||||
for (let i = 0; i < SHARD_TOTAL; i++) {
|
||||
const s = shards[i];
|
||||
const hRuns = Object.keys(s.heavy).length;
|
||||
const hTests = Object.values(s.heavy).reduce((n, a) => n + a.length, 0);
|
||||
const marker = i === SHARD_INDEX ? " ← THIS SHARD" : "";
|
||||
console.log(
|
||||
`Shard ${i}: ${(s.load / 1000).toFixed(1)}s | ${s.whole.length} pkgs` +
|
||||
(hRuns > 0 ? `, ${hRuns} heavy splits (${hTests} tests)` : "") +
|
||||
marker,
|
||||
);
|
||||
}
|
||||
console.log("::endgroup::");
|
||||
|
||||
// ── Write output for this shard ──
|
||||
const myShard = shards[SHARD_INDEX];
|
||||
const te = myShard.whole.filter((p) => !p.includes("/enterprise/")).join(" ");
|
||||
const ee = myShard.whole.filter((p) => p.includes("/enterprise/")).join(" ");
|
||||
|
||||
fs.writeFileSync("shard-te-packages.txt", te);
|
||||
fs.writeFileSync("shard-ee-packages.txt", ee);
|
||||
|
||||
// Heavy package runs: one line per run as "pkg REGEX"
|
||||
const heavyRuns = Object.entries(myShard.heavy).map(([pkg, tests]) => {
|
||||
const regex = tests.map((t) => "^" + t + "$").join("|");
|
||||
return pkg + " " + regex;
|
||||
});
|
||||
fs.writeFileSync("shard-heavy-runs.txt", heavyRuns.join("\n"));
|
||||
|
||||
console.log(
|
||||
`Light packages: ${myShard.whole.length} (${te.split(" ").filter(Boolean).length} TE, ${ee.split(" ").filter(Boolean).length} EE)`,
|
||||
);
|
||||
console.log(`Heavy package runs: ${heavyRuns.length}`);
|
||||
333
server/scripts/shard-split.test.js
Normal file
333
server/scripts/shard-split.test.js
Normal file
@@ -0,0 +1,333 @@
|
||||
const { describe, it, beforeEach, afterEach } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const os = require("node:os");
|
||||
|
||||
const SCRIPT = path.join(__dirname, "shard-split.js");
|
||||
const TESTDATA = path.join(__dirname, "testdata");
|
||||
|
||||
/**
|
||||
* Helper: run shard-split.js in a temp directory with given inputs.
|
||||
* Returns the output files and stdout.
|
||||
*/
|
||||
function runSolver({ packages, shardIndex, shardTotal, gotestsumJson, prevReportXml }) {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "shard-test-"));
|
||||
try {
|
||||
fs.writeFileSync(path.join(tmpDir, "all-packages.txt"), packages.join("\n"));
|
||||
|
||||
if (gotestsumJson) {
|
||||
fs.writeFileSync(path.join(tmpDir, "prev-gotestsum.json"), gotestsumJson);
|
||||
}
|
||||
if (prevReportXml) {
|
||||
fs.writeFileSync(path.join(tmpDir, "prev-report.xml"), prevReportXml);
|
||||
}
|
||||
|
||||
const stdout = execFileSync("node", [SCRIPT], {
|
||||
cwd: tmpDir,
|
||||
env: {
|
||||
...process.env,
|
||||
SHARD_INDEX: String(shardIndex),
|
||||
SHARD_TOTAL: String(shardTotal),
|
||||
},
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
const te = fs.readFileSync(path.join(tmpDir, "shard-te-packages.txt"), "utf8");
|
||||
const ee = fs.readFileSync(path.join(tmpDir, "shard-ee-packages.txt"), "utf8");
|
||||
const heavy = fs.readFileSync(path.join(tmpDir, "shard-heavy-runs.txt"), "utf8");
|
||||
|
||||
return { te, ee, heavy, stdout };
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("shard-split.js", () => {
|
||||
describe("round-robin fallback (no timing data)", () => {
|
||||
it("distributes packages evenly across shards", () => {
|
||||
const packages = [
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4",
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app",
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore",
|
||||
"github.com/mattermost/mattermost/server/v8/config",
|
||||
];
|
||||
|
||||
// Collect assignments from all shards
|
||||
const allTe = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const result = runSolver({ packages, shardIndex: i, shardTotal: 2 });
|
||||
allTe.push(...result.te.split(" ").filter(Boolean));
|
||||
}
|
||||
|
||||
// All packages should be assigned exactly once
|
||||
assert.equal(allTe.sort().join("\n"), packages.sort().join("\n"));
|
||||
});
|
||||
|
||||
it("uses round-robin when no timing files exist", () => {
|
||||
const packages = ["pkg/a", "pkg/b", "pkg/c", "pkg/d", "pkg/e"];
|
||||
const r0 = runSolver({ packages, shardIndex: 0, shardTotal: 2 });
|
||||
const r1 = runSolver({ packages, shardIndex: 1, shardTotal: 2 });
|
||||
|
||||
assert.ok(r0.stdout.includes("round-robin"), "Should mention round-robin in output");
|
||||
// No heavy runs
|
||||
assert.equal(r0.heavy.trim(), "");
|
||||
assert.equal(r1.heavy.trim(), "");
|
||||
});
|
||||
});
|
||||
|
||||
describe("timing-based balancing", () => {
|
||||
it("balances shards using gotestsum.json timing data", () => {
|
||||
const gotestsumJson = fs.readFileSync(
|
||||
path.join(TESTDATA, "sample-gotestsum.json"),
|
||||
"utf8"
|
||||
);
|
||||
const packages = [
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4",
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app",
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore",
|
||||
"github.com/mattermost/mattermost/server/v8/config",
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch",
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/compliance",
|
||||
"github.com/mattermost/mattermost/server/public/model",
|
||||
];
|
||||
|
||||
// Run for all 4 shards and check that loads are somewhat balanced
|
||||
const loads = [];
|
||||
const allAssigned = new Set();
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const result = runSolver({
|
||||
packages,
|
||||
shardIndex: i,
|
||||
shardTotal: 4,
|
||||
gotestsumJson,
|
||||
});
|
||||
|
||||
// Track all assigned packages and tests
|
||||
const tePkgs = result.te.split(" ").filter(Boolean);
|
||||
const eePkgs = result.ee.split(" ").filter(Boolean);
|
||||
tePkgs.forEach((p) => allAssigned.add(p));
|
||||
eePkgs.forEach((p) => allAssigned.add(p));
|
||||
|
||||
// Parse heavy runs
|
||||
if (result.heavy.trim()) {
|
||||
result.heavy
|
||||
.trim()
|
||||
.split("\n")
|
||||
.forEach((line) => {
|
||||
const pkg = line.split(" ")[0];
|
||||
allAssigned.add(pkg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Every package should be covered
|
||||
for (const pkg of packages) {
|
||||
assert.ok(
|
||||
allAssigned.has(pkg),
|
||||
`Package ${pkg} should be assigned to some shard`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not produce empty shards with sample data", () => {
|
||||
const gotestsumJson = fs.readFileSync(
|
||||
path.join(TESTDATA, "sample-gotestsum.json"),
|
||||
"utf8"
|
||||
);
|
||||
const packages = [
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4",
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app",
|
||||
"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore",
|
||||
"github.com/mattermost/mattermost/server/v8/config",
|
||||
];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const result = runSolver({
|
||||
packages,
|
||||
shardIndex: i,
|
||||
shardTotal: 4,
|
||||
gotestsumJson,
|
||||
});
|
||||
const hasWork =
|
||||
result.te.trim() !== "" ||
|
||||
result.ee.trim() !== "" ||
|
||||
result.heavy.trim() !== "";
|
||||
assert.ok(hasWork, `Shard ${i} should have some work assigned`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("heavy package splitting", () => {
|
||||
it("splits packages over HEAVY_MS threshold into individual tests", () => {
|
||||
// Create timing data where api4 is very heavy (> 300s = 300000ms)
|
||||
const lines = [];
|
||||
// api4: 6 tests totaling 452.2s (> 300s threshold)
|
||||
for (const [test, elapsed] of [
|
||||
["TestGetChannel", 145.2],
|
||||
["TestCreatePost", 98.1],
|
||||
["TestUpdateChannel", 72.5],
|
||||
["TestDeleteChannel", 58.3],
|
||||
["TestGetChannelMembers", 45.7],
|
||||
["TestSearchChannels", 32.4],
|
||||
]) {
|
||||
lines.push(
|
||||
JSON.stringify({
|
||||
Time: "2025-03-20T10:00:00Z",
|
||||
Action: "pass",
|
||||
Package: "github.com/mattermost/mattermost/server/v8/channels/api4",
|
||||
Test: test,
|
||||
Elapsed: elapsed,
|
||||
})
|
||||
);
|
||||
}
|
||||
// config: 2 tests totaling 8s (< 120s, stays whole)
|
||||
for (const [test, elapsed] of [
|
||||
["TestConfigStore", 5.0],
|
||||
["TestConfigMigrate", 3.0],
|
||||
]) {
|
||||
lines.push(
|
||||
JSON.stringify({
|
||||
Time: "2025-03-20T10:00:00Z",
|
||||
Action: "pass",
|
||||
Package: "github.com/mattermost/mattermost/server/v8/config",
|
||||
Test: test,
|
||||
Elapsed: elapsed,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const gotestsumJson = lines.join("\n");
|
||||
const packages = [
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4",
|
||||
"github.com/mattermost/mattermost/server/v8/config",
|
||||
];
|
||||
|
||||
// With 2 shards, api4 tests should be split across shards
|
||||
let heavyFound = false;
|
||||
const allHeavyTests = [];
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const result = runSolver({
|
||||
packages,
|
||||
shardIndex: i,
|
||||
shardTotal: 2,
|
||||
gotestsumJson,
|
||||
});
|
||||
|
||||
if (result.heavy.trim()) {
|
||||
heavyFound = true;
|
||||
// Parse heavy runs to extract test names
|
||||
for (const line of result.heavy.trim().split("\n")) {
|
||||
const parts = line.split(" ");
|
||||
assert.equal(
|
||||
parts[0],
|
||||
"github.com/mattermost/mattermost/server/v8/channels/api4",
|
||||
"Heavy package should be api4"
|
||||
);
|
||||
// Regex is like "^TestGetChannel$|^TestCreatePost$"
|
||||
const tests = parts[1].split("|").map((r) => r.replace(/[\^$]/g, ""));
|
||||
allHeavyTests.push(...tests);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(heavyFound, "Should have heavy package splits for api4");
|
||||
// All api4 tests should be distributed
|
||||
const expectedTests = [
|
||||
"TestGetChannel",
|
||||
"TestCreatePost",
|
||||
"TestUpdateChannel",
|
||||
"TestDeleteChannel",
|
||||
"TestGetChannelMembers",
|
||||
"TestSearchChannels",
|
||||
];
|
||||
assert.deepEqual(
|
||||
allHeavyTests.sort(),
|
||||
expectedTests.sort(),
|
||||
"All api4 tests should be distributed across shards"
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps light packages whole even with timing data", () => {
|
||||
const gotestsumJson = [
|
||||
'{"Action":"pass","Package":"pkg/light","Test":"TestA","Elapsed":5.0}',
|
||||
'{"Action":"pass","Package":"pkg/light","Test":"TestB","Elapsed":3.0}',
|
||||
].join("\n");
|
||||
|
||||
const result = runSolver({
|
||||
packages: ["pkg/light"],
|
||||
shardIndex: 0,
|
||||
shardTotal: 2,
|
||||
gotestsumJson,
|
||||
});
|
||||
|
||||
// Light package should be assigned whole, not split
|
||||
assert.equal(result.heavy.trim(), "", "Light package should not be in heavy runs");
|
||||
assert.ok(
|
||||
result.te.includes("pkg/light"),
|
||||
"Light package should be in TE packages"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JUnit XML fallback", () => {
|
||||
it("uses JUnit XML when gotestsum.json is missing", () => {
|
||||
const prevReportXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuites>
|
||||
<testsuite name="pkg/fast" time="10.0" tests="5">
|
||||
<testcase name="TestA" time="5.0"/>
|
||||
<testcase name="TestB" time="5.0"/>
|
||||
</testsuite>
|
||||
<testsuite name="pkg/slow" time="50.0" tests="3">
|
||||
<testcase name="TestX" time="25.0"/>
|
||||
<testcase name="TestY" time="25.0"/>
|
||||
</testsuite>
|
||||
</testsuites>`;
|
||||
|
||||
const result = runSolver({
|
||||
packages: ["pkg/fast", "pkg/slow"],
|
||||
shardIndex: 0,
|
||||
shardTotal: 2,
|
||||
prevReportXml,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
result.stdout.includes("JUnit XML"),
|
||||
"Should indicate using JUnit XML fallback"
|
||||
);
|
||||
// No heavy splits with XML-only data (no per-test timing)
|
||||
assert.equal(result.heavy.trim(), "", "Should not split packages without per-test timing");
|
||||
});
|
||||
});
|
||||
|
||||
describe("enterprise package separation", () => {
|
||||
it("separates enterprise packages into EE output", () => {
|
||||
const packages = [
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app",
|
||||
"github.com/mattermost/mattermost/server/v8/enterprise/compliance",
|
||||
];
|
||||
|
||||
const result = runSolver({
|
||||
packages,
|
||||
shardIndex: 0,
|
||||
shardTotal: 1,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
result.te.includes("channels/app"),
|
||||
"TE should include non-enterprise packages"
|
||||
);
|
||||
assert.ok(
|
||||
result.ee.includes("enterprise/compliance"),
|
||||
"EE should include enterprise packages"
|
||||
);
|
||||
assert.ok(
|
||||
!result.te.includes("enterprise"),
|
||||
"TE should not include enterprise packages"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
22
server/scripts/testdata/sample-gotestsum.json
vendored
Normal file
22
server/scripts/testdata/sample-gotestsum.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/api4","Test":"TestGetChannel","Elapsed":45.2}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/api4","Test":"TestCreatePost","Elapsed":38.1}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/api4","Test":"TestUpdateChannel","Elapsed":22.5}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/api4","Test":"TestDeleteChannel","Elapsed":18.3}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/api4","Test":"TestGetChannelMembers","Elapsed":15.7}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/api4","Test":"TestSearchChannels","Elapsed":12.4}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/app","Test":"TestCreateUser","Elapsed":25.6}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/app","Test":"TestUpdateUser","Elapsed":20.3}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/app","Test":"TestDeleteUser","Elapsed":15.8}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/app","Test":"TestGetUser","Elapsed":10.2}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/app","Test":"TestSearchUsers","Elapsed":8.5}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore","Test":"TestChannelStore","Elapsed":35.0}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore","Test":"TestPostStore","Elapsed":28.0}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/channels/store/sqlstore","Test":"TestUserStore","Elapsed":22.0}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/config","Test":"TestConfigStore","Elapsed":5.0}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/config","Test":"TestConfigMigrate","Elapsed":3.0}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch","Test":"TestSearchPosts","Elapsed":8.0}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch","Test":"TestIndexPosts","Elapsed":6.0}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/v8/enterprise/compliance","Test":"TestExportCompliance","Elapsed":4.0}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/public/model","Test":"TestModelValidation","Elapsed":2.0}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"pass","Package":"github.com/mattermost/mattermost/server/public/model","Test":"TestModelSerialization","Elapsed":1.5}
|
||||
{"Time":"2025-03-20T10:00:00Z","Action":"output","Package":"github.com/mattermost/mattermost/server/v8/channels/api4","Test":"TestGetChannel","Output":"--- PASS: TestGetChannel (45.20s)\n"}
|
||||
24
server/scripts/vet-api-check.sh
Executable file
24
server/scripts/vet-api-check.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
API_YAML=$ROOT../api/v4/html/static/mattermost-openapi-v4.yaml
|
||||
OUTPUT=$($GO vet -vettool=$GOBIN/mattermost-govet -openApiSync -openApiSync.spec=$API_YAML ./... 2>&1 || true)
|
||||
|
||||
echo "OpenAPI vet output"
|
||||
echo "=================="
|
||||
echo "$OUTPUT"
|
||||
|
||||
OUTPUT_EXCLUDING_IGNORED=$(echo "$OUTPUT" | grep -Fv \
|
||||
-e 'go: downloading' \
|
||||
2>&1 || true)
|
||||
|
||||
if [[ ! -z "${OUTPUT_EXCLUDING_IGNORED// }" ]]; then
|
||||
echo "Failing vet output"
|
||||
echo "=================="
|
||||
echo "$OUTPUT_EXCLUDING_IGNORED"
|
||||
exit 1
|
||||
else
|
||||
echo "openApiSync passed."
|
||||
exit 0
|
||||
fi
|
||||
20
server/scripts/wait-for-system-start.sh
Executable file
20
server/scripts/wait-for-system-start.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
total=0
|
||||
MAX_WAIT_SECONDS="${MAX_WAIT_SECONDS:=60}"
|
||||
|
||||
echo "waiting $MAX_WAIT_SECONDS seconds for the server to start"
|
||||
|
||||
while [[ "$total" -le "$MAX_WAIT_SECONDS" ]]; do
|
||||
if bin/mmctl system status --local 2> /dev/null; then
|
||||
exit 0
|
||||
else
|
||||
((total=total+1))
|
||||
printf "."
|
||||
sleep 1
|
||||
fi
|
||||
done
|
||||
|
||||
printf "\nserver didn't start in $MAX_WAIT_SECONDS seconds\n"
|
||||
|
||||
exit 1
|
||||
Reference in New Issue
Block a user